嵌入式系统中的数据一致性、内存克隆与定时优化
1. 数据一致性与自旋锁
在多核心处理器的环境中,数据一致性是一个重要的问题。为了确保资源的安全访问,常常会使用自旋锁(Spinlocks)。以下是使用
TryToGetSpinlock
的最佳实践代码:
1 TryToGetSpinlockType success;
2 DisableOSInterrupts( );
3 (void)TryToGetSpinlock( spinlock, &success );
4 while( TRYTOGETSPINLOCK_NOSUCCESS == success )
5 {
6 EnableOSInterrupts( );
7 /* Interrupts that occur will be handled here */
8 DisableOSInterrupts( );
9 (void)TryToGetSpinlock( spinlock, &success );
10 }
11 /* Access to the protected resource
12 occurs here */
13 ReleaseSpinlock( );
14 EnableOSInterrupts( );
在这个循环中,短暂地启用中断以允许在等待自旋锁时发生中断,这可以避免一些潜在的问题。然而,这段代码在等待期间会频繁调用
TryToGetSpinlock
服务。由于自旋锁的状态需要对处理器的所有核心可用,相应的操作系统内部变量会存储在共享内存中。高频调用
TryToGetSpinlock
会导致对共享内存总线的大量访问,增加总线访问冲突的概率,进而可能减慢当前访问资源的核心速度,延迟资源的释放。
为了应对这个问题,可以在循环体中插入无操作命令(如
nop
)来降低
TryToGetSpinlock
的调用频率。但这种方法也有缺点,通常会在等待资源释放时增加额外的延迟。
2. 确保数据一致性的理想解决方案
最好的确保数据一致性的方法是不需要额外的保护机制。例如,在处理中断数据不一致和多核处理器中多个 CPU 竞争的情况时,可以采用以下简单方法:
1 unsigned int counterISR_low_prio = 0;
2 unsigned int counterISR_high_prio = 0;
3
4 void ISR_low_prio (void) __attribute__ ((signal,used));
5 void ISR_low_prio (void)
6 {
7 _enable(); // globally enable interrupts
8 counterISR_low_prio++;
9 DoSomething();
10 }
11
12 void ISR_high_prio (void) __attribute__ ((signal,used));
13 void ISR_high_prio (void)
14 {
15 _enable(); // globally enable interrupts
16 counterISR_high_prio++;
17 DoSomethingElse();
18 }
19
20 unsigned int GetCounterSum(void)
21 {
22 return counterISR_low_prio + counterISR_high_prio;
23 }
每个中断使用自己的计数器,当需要执行次数的总和时,在查询时进行计算。即使查询被中断,仍然可以获得正确的值。
3. 确保数据一致性的成本
回顾过去,早期嵌入式软件大多是手动编写的,在需要确保数据一致性的地方使用全局中断锁。这可以通过汇编语言代码(如
__asm(di)
和
__asm(ei)
)或特殊命令(如
__disable()
和
__enable()
)实现。这种方式只需要一条机器指令,成本极低。
随着操作系统接口的引入,如 OSEK 提供的接口,成本增加了一倍多。后来,安全操作系统的广泛应用,在禁用中断时需要额外检查上下文是否有权限,运行时开销进一步大幅增加。
在多核处理器中,单纯阻塞中断已不足以确保数据一致性,需要使用自旋锁。当多个 CPU 同时访问受保护的资源时,可能会导致多个 CPU 等待,增加了成本。
4. 内存地址克隆
英飞凌在 TriCore AURIX 架构开发中引入了“内存地址克隆”功能。每个 AURIX CPU 都有本地程序内存(PSPR)和本地数据内存(DSPR)。DSPR 除了在全局线性地址空间有特定地址外,还可以通过从
0xD000_0000
开始的地址范围访问。例如,当 CPU1 访问地址
0xD000_00C4
时,使用的是 CPU1 的 DSPR,在全局线性地址空间中对应地址
0x6000_00C4
。
这种结构的好处在多核心代码中很明显。以实时操作系统为例,对于单核心操作系统,内部变量的定义和使用可能如下:
1 unsigned int runningTask;
2
3 void someOSfunction(void)
4 {
5 ...
6 runningTask = ... ;
7 ...
8 }
当在多核处理器上使用时,通常需要将
runningTask
定义为数组,每个 CPU 使用数组的一个元素:
1 unsigned int runningTask[NOF_CORES];
2
3 void someOSfunction(void)
4 {
5 ...
6 runningTask[GetCoreId()] = ... ;
7 ...
8 }
而在 AURIX 上,可以使用内存地址克隆功能,只需要告诉编译器该变量是克隆变量:
1 __clone unsigned int runningTask;
2
3 void someOSfunction(void)
4 {
5 ...
6 runningTask = ... ;
7 ...
8 }
这样代码与单核心代码基本相同,除了内存限定符
__clone
。使用内存克隆不仅代码处理更简单,访问也更高效,还不需要查询核心 ID,节省了运行时和代码内存。
5. 定时优化
定时优化遵循“自上而下”的方法,先分析和优化调度级别,再优化代码级别。内存使用的优化与这两个级别有一定的独立性,优化的内存会以最小化总 CPU 利用率的方式分配符号,同时要确保避免编译时和运行时的内存溢出,并考虑安全要求。
5.1 调度级别的定时优化
调度级别的运行时优化措施较少可以通过清单方式进行。最大的优化潜力在于项目特定的参数,如应用程序在多核处理器不同核心上的基本分布、操作系统的配置以及函数或可运行体到任务的分配等。
以下是一些调度级别的优化建议:
1.
避免跨核心通信
:功能在多核处理器不同核心上的分布应尽量减少核心边界之间的通信。
2.
分离计算密集型代码和中断
:尽可能将中断处理集中在一个核心,将计算密集型代码部署到另一个核心,以提高缓存和流水线的使用效率。
3.
避免使用 ECC 任务
:对于 OSEK/AUTOSAR CP 项目,当需要调度循环可运行体时,每个周期使用单个循环 BCC1 任务的配置比非终止的 ECC 任务更好。
4.
合理使用异构多核处理器
:将软件的计算密集部分分配给最强大的核心。可以通过运行时测量来决定代码在哪个核心上执行,对于安全相关项目,静态代码分析可以确定理论最坏情况的运行时间。
5.
避免使用数据一致性机制
:理想情况下,系统(特别是操作系统)的配置应避免使用数据一致性机制。可以通过使用相同优先级或优先级组、采用协作式多任务处理等方法避免抢占式中断。如果无法避免抢占式中断,可以将任务或中断分为必须抢占的部分和非抢占的部分,减少需要数据一致性机制处理的共享数据量。
6.
周期性任务的负载均衡
:当配置多个周期性任务时,通过设置偏移量来平衡负载。“最快”的周期性任务偏移量设为零,其他周期性任务的偏移量为最快任务周期的整数倍,这样可以减少调度中断的数量,同时实现足够的负载均衡。
综上所述,在嵌入式系统开发中,要充分考虑数据一致性、内存管理和定时优化等方面的问题,采用合适的方法和技术来提高系统的性能和效率。
嵌入式系统中的数据一致性、内存克隆与定时优化
6. 调度级别优化措施的详细分析
6.1 避免跨核心通信
跨核心通信会带来额外的开销,因为数据需要在不同核心之间传输。为了避免这种情况,在设计应用程序时,应尽量将相关的功能模块分配到同一个核心上。例如,一个多核心系统中有数据采集、数据处理和数据存储三个功能模块。如果将数据采集和处理分配到一个核心,数据存储分配到另一个核心,那么在数据采集完成后,需要将数据从采集核心传输到处理核心,可能还需要再传输到存储核心,这就增加了通信开销。而如果将这三个功能模块都分配到一个核心上,就可以避免这种跨核心的通信。
6.2 分离计算密集型代码和中断
计算密集型代码通常需要大量的 CPU 资源和连续的执行时间,而中断需要及时响应。如果将它们混合在一个核心上,可能会导致中断响应不及时或计算密集型代码的执行效率降低。例如,在一个实时控制系统中,有一个复杂的算法用于控制电机的转速,这是计算密集型代码。同时,系统会接收来自传感器的中断信号,用于检测电机的状态。将中断处理集中在一个核心,计算密集型代码部署到另一个核心,可以使计算密集型代码在执行时不会被中断打断,充分利用核心的缓存和流水线,提高执行效率;同时,中断处理核心可以专注于及时响应中断,保证系统的实时性。
6.3 避免使用 ECC 任务
在 OSEK/AUTOSAR CP 项目中,ECC(Event Chain Control)任务存在一些缺点。ECC 任务通常是一个非终止的任务,用于处理多个周期的循环可运行体。这种方式会增加系统的复杂性和开销。而每个周期使用单个循环 BCC1(Basic Cyclic Task Class 1)任务的配置更加简单和高效。例如,有三个周期分别为 1ms、2ms 和 5ms 的循环可运行体,如果使用 ECC 任务,需要一个任务来协调这三个周期的执行,可能会导致调度复杂。而使用三个 BCC1 任务,每个任务负责一个周期的可运行体,调度更加清晰,开销也更小。
6.4 合理使用异构多核处理器
异构多核处理器具有不同性能的核心,如英飞凌 AURIX 的 1.6P(性能核心)和 1.6E(效率核心)。不同的代码在不同核心上的运行时间可能会有很大差异。为了充分发挥异构多核处理器的优势,需要通过运行时测量来确定代码在哪个核心上执行效率最高。例如,对于一个图像处理算法,在 1.6P 核心上可能执行速度更快,但功耗也更高;而在 1.6E 核心上可能执行速度稍慢,但功耗更低。通过运行时测量,可以根据实际需求选择合适的核心。对于安全相关项目,静态代码分析可以确定理论最坏情况的运行时间,帮助开发者评估代码在不同核心上的性能。
6.5 避免使用数据一致性机制
数据一致性机制通常会消耗额外的资源,如 RAM、Flash 和运行时间。为了避免使用这些机制,可以从以下几个方面入手:
-
使用相同优先级或优先级组
:当任务或中断使用相同的优先级或优先级组时,可以避免抢占式中断的发生,从而减少数据不一致的风险。例如,在一个多任务系统中,有两个任务 A 和 B,如果它们的优先级相同,那么在任务 A 执行时,任务 B 不会抢占它,这样就不需要额外的数据一致性机制来保护共享数据。
-
采用协作式多任务处理
:协作式多任务处理中,任务主动放弃 CPU 控制权,而不是被其他任务抢占。这样可以避免任务之间的冲突,减少数据不一致的可能性。例如,在一个简单的嵌入式系统中,有两个任务,一个负责数据采集,一个负责数据显示。这两个任务可以通过协作的方式,依次执行,不需要额外的数据一致性机制。
-
拆分抢占式任务或中断
:如果无法避免抢占式中断,可以将任务或中断拆分为必须抢占的部分和非抢占的部分。例如,一个中断服务程序可以分为两部分,一部分是必须立即执行的关键代码,另一部分是可以稍后执行的非关键代码。将非关键代码作为非抢占式任务或在后台任务中执行,减少需要数据一致性机制处理的共享数据量。
6.6 周期性任务的负载均衡
周期性任务的负载均衡可以通过设置偏移量来实现。以下是一个简单的示例,假设有三个周期性任务 Task_1ms、Task_2ms 和 Task_5ms:
| 任务名称 | 周期(ms) | 偏移量(ms) |
| ---- | ---- | ---- |
| Task_1ms | 1 | 0 |
| Task_2ms | 2 | 1 |
| Task_5ms | 5 | 2 |
通过这样的设置,任务的执行时间会更加分散,避免了多个任务同时执行导致的高负载情况。同时,由于偏移量的设置,调度中断的数量也会减少。例如,当多个任务的偏移量都为 0 时,可能会在同一时刻触发多个调度中断;而通过合理设置偏移量,这些任务的调度中断可以合并,减少了系统的开销。
7. 定时优化的流程总结
为了更好地理解定时优化的过程,我们可以用一个 mermaid 流程图来表示:
graph TD;
A[开始] --> B[分析调度级别];
B --> C{是否有优化空间};
C -- 是 --> D[进行调度级别优化];
C -- 否 --> E[分析代码级别];
D --> E;
E --> F{是否有优化空间};
F -- 是 --> G[进行代码级别优化];
F -- 否 --> H[结束];
G --> H;
这个流程图展示了定时优化的“自上而下”方法。首先从调度级别开始分析,如果有优化空间,就进行调度级别优化;如果调度级别没有优化空间,就转向代码级别分析。同样,如果代码级别有优化空间,就进行代码级别优化;如果都没有优化空间,优化过程结束。
8. 总结
在嵌入式系统开发中,数据一致性、内存管理和定时优化是非常重要的方面。数据一致性问题需要通过合适的机制来解决,如自旋锁和理想的无保护机制。内存地址克隆技术可以提高多核心代码的处理效率和访问性能。而定时优化则需要从调度级别和代码级别入手,充分考虑项目特定的参数,采用合适的优化措施,如避免跨核心通信、分离计算密集型代码和中断等。通过综合考虑这些方面,可以提高嵌入式系统的性能和效率,满足不同应用场景的需求。
总之,开发者在嵌入式系统开发过程中,要不断探索和实践,根据具体的项目需求和硬件平台,选择最合适的方法和技术,以实现系统的最佳性能。
超级会员免费看
22

被折叠的 条评论
为什么被折叠?



