Tc36x 双核OS系统
Source Core 源核:调用API、发起跨核请求的核心
Target Core 目标核:执行API、响应跨核请求的核心
多核共享
On Aurix caches must not be used for shared variables
Shared variables must be mapped to uncached memory segments.
Therefore no cache flush or invalidate is needed
在 AURIX/TC366 上,共享变量必须放在 Non-Cached 内存段(DSPR、LUM)中,以保证多核访问数据一致性
Cache缓存相关内容:https://caitlinzhou.site/archives/tircore-cache
内存屏障
内存屏障(Memory Barrier,也叫内存栅栏 Memory Fence)是一类特殊的 CPU 指令或编译器指令。用来约束处理器和编译器对内存读写的执行顺序和可见性。
为什么需要内存屏障
现代系统中有两类重排序:
编译器重排序:编译器为了优化,可能调整指令顺序。
CPU 乱序执行 / 缓存:CPU 有乱序执行、Store Buffer、多级缓存等机制。这会让一个核心的写操作不能立刻被其他核心看到,或者以不同顺序被看到。
单线程下,这些优化会保证结果与顺序执行一致。但在多线程或多核环境下,就可能出现意料之外的结果。
// 初始 x = 0, flag = 0
// Core0
x = 42;
flag = 1;
// Core1
while (flag == 0) ;
print(x); // 可能打印 0!在Tircore下内存屏障主要靠以下两个指令实现:
The Data Cache (DCACHE) is not invalidated by DSYNC
在Tircore1.6.2中,DSYNC用于满足 TriCore 规定的数据访问完成和顺序要求,但它不是数据 Cache invalidate 指令,也不会自动建立跨核缓存一致性
IOC模块用于跨核的Application应用之间的通讯,可以实现消息队列(每个值都会存储到队列里,不丢失)、lastIsBest
原子操作
原子性,Atomicity
一次操作不能被其他并发访问者看成几个分开的步骤
在Tircore1.6.2手册中,介绍以下指令为原子性质操作指令:
其中最核心的指令为CMPSWAP.W ,为典型的CAS指令(Compare And Swap);
它为并发代码提供了一个可靠的“仅当我看到的状态仍然有效时,才更新它”的机制,
基本思想为“通过比较内存中的值与预期值,如果相等则更新为新的值,否则重新尝试,直到成功为止”
但在实际应用中,很少去直接调用这些指令,一般是OS中带有的Atomic模块封装了这些指令,我们直接使用atomic模块里面的API即可
使用示例:
os_atomic_t eventCount = OS_ATOMIC_OBJECT_INITIALIZER(0);
void OnEvent(void)
{
(void)OS_AtomicFetchAdd(&eventCount, 1);
}
os_atomic_value_t ReadEventCount(void)
{
return OS_AtomicLoad(&eventCount);
}我这里调用Atomic写入

在汇编中即对应:

在实际开发中,在保证内存一致性、可见性、有序性的前提下,单方写入+单方读取操作一般是没有问题的
例如:
如果变量为单值,例如uint8, uint16, uint32, uint64这种变量,读写时不会被编译器拆开
例如写的时候对应一行汇编指令:




读取的时候也对应一行汇编指令:




像这种变量的读写都是原子的,一般只需要保证内存可见性即可,一般不会造成双核读写造成错误
内存一致性: Consistency,多个线程观察内存访问时,能获取相同的数据
但如果是一个结构体类型的跨核心读取,就可能出现读取截断的情况
当一个核心在写入一个结构体,还没有将该结构体中全部成员变量更新完时,另一个核心就读取该结构体,导致截断发生:
core0中的程序如下:

core1中读取该结构体:

势必发生这种情况:

避免这种情况的发生只能通过加锁设置临界区来避免
互斥锁、自旋锁
在OS中,名义上来说有两种所,一种是Resource,一种是SpinLock;
Resource用于同核但不同Task, ISR之间的互斥,对其他核心上运行的任务没有影响;同时由于优先级天花板协议,获取资源的Task/ISR会暂时提升到最高优先级,释放Resource后恢复;
SpinLock属于提供跨两个或多个核心的互斥机制,与资源不同,如果未正确配置和使用自旋锁,可能会导致优先级反转和死锁。
Spinlock 用于跨核共享,但属于忙等锁,可能导致长时间等待或死锁。应让持锁时间短、避免反序嵌套;还要避免持有 spinlock 的任务在本核被高优先级任务抢占后,后者又等待同一个锁。AutoCore 安全应用指南建议在支持的版本中给 spinlock 配置合适的本核中断锁,并避免持锁时调用其他 OS API。
对于TC366双核,我们注重于SpinLock,多用于双核之间对临界区的互斥访问;

自旋锁相关参数如下:
相关API如下:
多核多变量读写测试
分别在两个Core中都运行以下逻辑,
分别对三个变量进行同时读写,观察其值递增情况:
测试代码:

测试结果:

通过UDE,运行一段时间后同时暂停两个核心,观察变量值
可以看出没有经过限制的变量share_var2 值偏差别巨大,这是由于双核同时操作变量时造成的错误:
针对share_var2变量的读写:

对应三条汇编指令,分别是:
从内存读放到寄存器d15中;
d15值加1;
将d15值写回内存;

可以看出双核在运行时若同时读取到相同的值,在写回内存的时候势必会覆盖掉其中一个核心的写入操作!
这就是多核同时操作变量时,必须划分临界区或使用原子变量的原因;
通常来说,这种单一变量的加减,若使用SpinLock过于浪费CPU资源,使用原子变量即为最优解;
同时还要注意,共享变量不可放置在缓存区
核间中断
Inter-Core Interrupt 核间中断 属于OS范畴,是在多核处理器中,一个 CPU Core 通过硬件中断机制通知另一个 CPU Core 执行某项操作的方式
例如:Core0激活Core1中Task、Alarm、Schedule Table或者触发Event,等跨核操作、同步;
工作流程

特点
核间中断的优先级设置为最高 255
The signaling is done via inter-core interrupts which have the highest priority of all used interrupts assigned to them.
核间中断是通过软件触发GPSR(General Purpose Service Request)通用服务请求节点 来实现的
通过触发对应核心的GPSR中断即可响应
Core 0
|
| 触发 Core 1 对应的 GPSR
v
Core 1 Service Request Node
|
v
Core 1 Inter-Core ISR每个核心都需要一个核间中断
单核芯片不提供核间中断
同步核间请求与异步核间请求
同步请求是在另一个核心上触发工作并返回该工作结果的服务机制,源核需要等待请求结果,比较消耗时间;
而异步请求属于“发送即遗忘”,立即返回E_OK,如有错误信息会直接响应在目标核的ErrorHook中。
异步核间请求API如下:

核间中断的实践记录过程查看启动流程部分 https://caitlinzhou.site/archives/tc36x-shuang-he#双核os启动——核间中断
启动流程
Core0在复位后跳转至启动代码Startup逻辑:
这里为MK_Entry
MK_Entry:
disable
/* If all symbols are mapped to cached addresses (0x8 segment), we won't see our
* source code in the debugger, because all symbols are in a different segment than
* our start address. We fix this by jumping to the symbol in the respective segment.
*/
movh.a a15, MK_imm(#,MK_hi(_realaddr))
lea a15, [a15] MK_lo(_realaddr)
ji a15这里执行了一次地址跳转:如果符号都映射在缓存地址段(0x8 段),启动时当前执行地址与调试器显示的符号地址可能不在同一地址区域;因此这里跳到 _realaddr 对应的符号地址;
在MCU复位后,会根据BMHD跳转到启动地址,一般为0xA段的区域;
即使ld链接文件中将代码链接到了0x8段,烧录器也会将其转换为0xA段;
跳转到0x8段之后,准备一个临时的CSA区域
_realaddr:
#if MK_MAXCORES > 1
/* The user is responsible for starting up all cores, which are under control
* of the Safety OS and let them jump to or call MK_Entry2.
* So let's call a C function, which will do this here.
*/
/* The stack can't be used here */
movh.a a10, MK_imm(#, 0x0100)
/* Setup the CSA list, so we can call C functions.
*
* We will use space for 32 CSAs at the beginning of our core-local data scratch-pad.
* That's OK, because our demo doesn't have meaningful data in RAM before startup.
*
* We don't start at offset 0, because some compilers use loop unrolling optimizations
* which will cause traps due to accesses to "different segments" (trap class 2, tin 5).
*/
movh d4, MK_imm(#, 0xd000)
add d4, d4, MK_imm(#, 0x40) // d4 = 0xD0000040:CSA 区域起始地址
mov d6, MK_imm(#, 0x800) // d6 = 0x800:区域大小为 0x800 字节
add d5, d4, d6
jl MK_BootCsaList // 初始化Boot CSA
/* Now call the C function which starts the other cores etc.
*/
call Tmpl_DemoStartup // 初始化CSA后,允许调用Call指令,进入多核启动流程
#endif
j MK_Entry2 // OS启动流程初始化CSA后,跳入Tmpl_DemoStartup 启动core1
先初始化BTV,然后进入Tmpl_StartupAutosarCores 初始化Core1的PSW、A11等寄存器
并将Core1的PC也指向MK_Entry
MK_ATTR_NO_STACKPROT void Tmpl_DemoStartup(void)
{
mk_objectid_t const coreId = MK_HwGetCoreIndex();
/* The OS expects that its startup is not continued, if an exception occurs.
* For our demo we set the startup exception table.
*/
MK_WriteEndinit(0u);
MK_MTCR(MK_BTV, (mk_uint32_t) &MK_StartupExceptionTable);
MK_WriteEndinit(1u);
/* Core 0 is the core that is started by hardware.
* If other cores are supposed to run, we have to start them here.
*/
if (coreId == 0)
{
/* Initialize core synchronization spots, _before_ the other cores are started.
*/
MK_InitSyncHere();
/* Starts all cores which are meant to be controlled by the OS.
*/
Tmpl_StartupAutosarCores(); // 配置PSW, A11, PC指向MK_Entry,并启动Core1
}
/* If this core isn't supposed to run during the demo, it shall halt here.
*/
if ((MK_coreProperties[coreId] & MK_COREPROP_STARTMASK) == MK_COREPROP_NOTUSED)
{
/* This core is not supposed to run.
* Before halting we switch off the watchdog of the current core.
*/
MK_WriteEndinit(0);
MK_GetScuWdtCpuCon()->wdtxcon1 |= TMPL_SCU_WDTCON1_DR;
MK_WriteEndinit(1);
TMPL_ENDLESSLOOP();
}
}之后Core1也从MK_Entry启动,最后两个核心都进入MK_Entry2,设置各个核心的BIV、BTV等,做最后进入main函数前的初始化工作
双核OS启动——核间中断
MK_Entry2结束后Core0会直接跳入main函数,Core1跳入idle函数,等待Core0启动

Core0在main中调用EcuM_init从而调用StartOS API,正式启动OS,再通过核间中断,将Core1跳入main()

Core1跳入中断处理程序,准备好跳入main并返回状态

Core1通过软件中断触发核间中断,将状态反馈给Core0

之后Core1进入main函数

Core0接收到Core1状态正常后,继续执行main逻辑,然后各自核心跳入到设置好的initTask中 完成应用逻辑初始化
错误处理与Hook函数
每个核心的ErrorHook是独立的
