13
0
0

Tc36x 双核OS系统

Pinned
2026-10-12
2026-10-23

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下内存屏障主要靠以下两个指令实现:

指令

作用

典型场景

DSYNC

数据同步。

等待此前所有的 load/store 完成(包括排空 store buffer),之后的访存才开始。

外设寄存器写入、自旋锁释放前、Cache 维护操作后、修改内存保护数据区之后

ISYNC

指令同步。

制完成所有先前指令,然后刷新 CPU 流水线并使任何缓存的流水线状态失效,再继续执行下一条指令

MTCR 写 CSFR 之后,需要即使刷新才能生效

The Data Cache (DCACHE) is not invalidated by DSYNC

在Tircore1.6.2中,DSYNC用于满足 TriCore 规定的数据访问完成和顺序要求,但它不是数据 Cache invalidate 指令,也不会自动建立跨核缓存一致性

IOC模块用于跨核的Application应用之间的通讯,可以实现消息队列(每个值都会存储到队列里,不丢失)、lastIsBest

原子操作

原子性,Atomicity

一次操作不能被其他并发访问者看成几个分开的步骤

在Tircore1.6.2手册中,介绍以下指令为原子性质操作指令:

指令

功能

是否返回旧值

典型用途

LDMST

按位掩码修改一个内存字

否

原子设置/清除某些状态位

SWAP.W

交换寄存器与内存中的 32 位字

是

Test-and-set、二值信号量、锁

ST.T

原子地存储某个 bit

通常不返回完整旧字

设置/操作状态位

CMPSWAP.W

比较内存值和期望值,相等时才写入新值

根据指令结果判断是否成功

CAS、无锁算法、抢锁

SWAPMSK.W

按掩码交换内存中的部分 bit

是

原子更新指定 bit,同时保留其他 bit

其中最核心的指令为CMPSWAP.W ,为典型的CAS指令(Compare And Swap);

它为并发代码提供了一个可靠的“仅当我看到的状态仍然有效时,才更新它”的机制,

基本思想为“通过比较内存中的值与预期值,如果相等则更新为新的值,否则重新尝试,直到成功为止”

但在实际应用中,很少去直接调用这些指令,一般是OS中带有的Atomic模块封装了这些指令,我们直接使用atomic模块里面的API即可

类别

API

行为

屏障

OS_AtomicThreadFence()

插入顺序一致的内存屏障。

初始化

OS_ATOMIC_OBJECT_INITIALIZER(v)、OS_AtomicInit(obj, v)

分别用于静态初始化和运行期初始化。

读写

OS_AtomicLoad()、OS_AtomicStore()

原子读取或存入值。

交换

OS_AtomicExchange()

写入新值并返回操作前的值。

条件交换

OS_AtomicCompareExchange()

当前值等于 *expected 时写入新值;返回是否成功。失败时会用观察到的当前值更新 *expected。

算术/位运算

OS_AtomicFetchAdd/Sub/Or/And/Xor()

原子执行对应操作,均返回操作前的值。

标志位

OS_AtomicTestAndSetFlag()、OS_AtomicClearFlag()

mask 只能选一个 bit;前者返回操作前该 bit 的状态并置位,后者清位。

使用示例:

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,多用于双核之间对临界区的互斥访问;

自旋锁相关参数如下:

参数

描述

OsSpinlockAccessingApplication

允许引用该SpinLock的OsApplication 应用,可以获取或释放此 OsSpinlock。

OsSpinlockSuccessor

当需要SpinLock嵌套SpinLock时,指向下一个允许调用的SpinLock

OsSpinlockLockMethod

获取锁时额外应用的措施

LOCK_NOTHING 不做任何额外措施

LOCK_ALL_INTERRUPTS 禁用所有中断

LOCK_CAT2_INTERRUPTS 禁用2类中断

LOCK_WITH_RESCHEDULER 获取调度器,将优先级提升到最高,防止被其他任务抢占

相关API如下:

API函数

示例

作用

GetSpinlock

StatusType GetSpinlock( SpinlockIdType SpinlockId )

试图占用一个自旋锁,若锁被占用将一直在此阻塞自旋,直到该锁被释放为止

TryToGetSpinlock

StatusType TryToGetSpinlock( SpinlockIdType SpinlockId,

TryToGetSpinlockType* Success )

尝试获取一个自旋锁,不阻塞等待,通过参数来获取自旋锁状态

ReleaseSpinlock

StatusType ReleaseSpinlock( SpinlockIdType SpinlockId )

释放当前任务占用的自旋锁;

任务必须在Terminate、WaitEvent前必须完全释放全部的自旋锁;

多核多变量读写测试

分别在两个Core中都运行以下逻辑,

分别对三个变量进行同时读写,观察其值递增情况:

变量名称

特点

shared_var1

通过SpinLock界限临界区读写

shared_var2

不做限制,两核直接读写

shared_var3

声明为原子变量

测试代码:

测试结果:

通过UDE,运行一段时间后同时暂停两个核心,观察变量值

可以看出没有经过限制的变量share_var2 值偏差别巨大,这是由于双核同时操作变量时造成的错误:

针对share_var2变量的读写:

对应三条汇编指令,分别是:

  1. 从内存读放到寄存器d15中;

  2. d15值加1;

  3. 将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中。

对比项

同步核间请求

异步核间请求

调用后何时返回

目标核处理并回复后

请求成功入队后

能否取得目标核执行结果

能

不能

动态错误如何处理

返回给调用者,并在发现错误的核上报告 ErrorHook()

在发现错误的核上报告 ErrorHook(),调用者不再收到该错误

源核 CPU 开销

较高,需要等待队列空间及回复

通常较低,但不能理解为任何情况下都不等待

典型 API

ActivateTask()

MK_AsyncActivateTask(),EB 微内核扩展 API

异步核间请求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是独立的

Support

If this article helped you, feel free to share it or give support!