ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

TC397多核MCU上实现FreeRTOS SMP的完整技术指南

TC397多核MCU上实现FreeRTOS SMP的完整技术指南 1. 项目概述为什么TC397上跑FreeRTOS SMP不是“加个库”那么简单英飞凌TC397是AURIX™ TriCore™家族中面向高安全、高实时性汽车域控制器的旗舰级多核MCU它不是一块普通的Cortex-M单片机——它内部集成了6个TriCore CPU核心3个主核3个锁步核每个主核都具备独立的L1指令/数据缓存、本地内存LMU、专用中断控制器ICU和DMA通道。而FreeRTOS SMPSymmetric Multiprocessing版本是FreeRTOS官方为真正对称多处理架构设计的内核变体它要求所有CPU核心共享同一套内核对象任务、队列、信号量等、共用同一块堆内存管理区并由一个统一的调度器协调所有核心上的任务运行。这与常见的“AMPAsymmetric Multiprocessing”方案——比如在TC397上让每个核跑一个独立的FreeRTOS实例、彼此通过IPC通信——有本质区别。很多人看到“FreeRTOS移植”四个字第一反应是去官网下载源码、添加.c文件、配置config.h、编译通过就完事了。但在TC397上做SMP移植这种思路会直接撞墙。我第一次尝试时在Keil MDK里把FreeRTOS-Kernel源码拖进去改了几个宏定义编译倒是过了但一上电三个主核全卡死在启动阶段的vPortStartFirstTask()里调试器连断点都打不进去。后来翻遍TC397的TRMTechnical Reference Manual第12章“Multi-Core Operation”才明白问题出在哪SMP不是“多个核跑同一个代码”而是“多个核协同维护同一套内核状态”。这意味着你必须亲手接管核间同步原语如自旋锁spinlock、全局中断屏蔽策略不能只关本核中断得保证临界区对所有核原子、内存一致性模型TriCore的Cache Coherency机制如何与FreeRTOS的内存分配器配合甚至启动流程本身——TC397上电后只有Core0P0从Flash启动其他核P1/P2必须由P0显式唤醒并跳转到指定入口这个“唤醒-跳转-同步”的过程就是整个SMP系统能否活过来的第一道生死线。所以这篇教程叫“保姆级”不是因为它步骤多而是因为每一步背后都有硬性约束你改一个#define configUSE_TICK_HOOK就得同步检查TC397的GTM模块是否已为所有核配置了统一的定时器中断源你调用xTaskCreate()创建任务就得确认该任务的栈内存是否分配在所有核都能一致访问的OCRAM区域而不是某个核私有的LMU里你启用configUSE_MUTEXES就得自己实现基于TriCoreSWAP指令的互斥锁底层因为FreeRTOS默认的ARM Cortex-M汇编锁在TriCore上根本不存在。这些细节官方文档不会手把手写社区资料也极少覆盖TC397FreeRTOS SMP这个组合——毕竟它太新、太专、太硬核。但正因如此一旦跑通你获得的不仅是“能用”而是对多核MCU底层运行机制的穿透式理解。适合谁汽车电子Tier1的嵌入式工程师、自动驾驶域控固件开发者、需要榨干TC397全部6核性能的实时控制算法团队以及所有想告别“单核思维”、真正踏入多核世界的人。2. 整体设计思路与关键决策解析为什么选SMP而非AMP以及为什么必须重写启动代码2.1 SMP vs AMP不是技术偏好而是架构需求决定的硬选择在TC397上实现多任务技术上存在两条路AMP非对称多处理和SMP对称多处理。很多工程师会下意识选AMP理由很实在简单、成熟、风险低。比如让P0核跑主应用逻辑P1核跑CAN FD协议栈P2核跑ADC采样DSADC处理三者通过共享内存邮箱Mailbox通信。这种方案确实能快速落地但它的天花板非常明显——任务无法跨核迁移负载永远不均衡。举个真实案例某ADAS域控制器在实车测试中发现P0核在处理视觉融合算法时CPU占用率长期95%而P1/P2核空闲率超60%。想把部分计算卸载过去不行。因为AMP下每个核的任务调度器完全独立P0上创建的任务P1根本“看不见”更别说执行了。而SMP的核心价值恰恰在于统一调度视图你创建一个优先级为10的任务FreeRTOS内核会根据当前各核的负载、缓存亲和性cache affinity和就绪队列长度自动把它调度到最空闲的那个核上去执行。这就像给6个工人配了一个总调度员而不是每人发一个对讲机各自为政。但选择SMP意味着你主动拥抱复杂性。最大的复杂性来自内存模型。TC397的内存空间被划分为多个物理区域OCRAMOn-Chip RAM所有核可一致访问、LMULocal Memory Unit每个核私有、PSRAM外部扩展RAM。FreeRTOS SMP要求所有内核数据结构如pxReadyTasksLists[]就绪队列数组、pxCurrentTCB当前任务控制块指针、xList pxDelayedTaskList延时任务列表必须位于缓存一致cache-coherent且所有核可写的内存中。OCRAM天然满足这点但它的容量只有4MBTC397B远小于LMU每个核1.5MB。因此我们的设计强制规定所有FreeRTOS内核对象TCB、队列句柄、信号量结构体及其关联的栈空间必须全部分配在OCRAM。这意味着你不能像单核开发那样把任务栈随便放在.bss段它默认映射到LMU而必须显式使用pvPortMalloc()从OCRAM堆中分配并确保heap_4.c的ucHeap[]数组定义在OCRAM段内。这个决策看似只是改个链接脚本实则牵一发而动全身——它决定了后续所有内存分配API的行为边界。2.2 启动流程重构P0不是“大哥”而是“总指挥”TC397的启动流程是典型的“主从式”复位后只有Core0P0从地址0x80000000开始执行BootROM代码完成基本初始化时钟、PLL、内存控制器后跳转到用户代码的Reset_Handler。此时P1和P2处于“halted”状态等待P0的“唤醒令”。在AMP方案中P0只需向P1/P2的启动地址如0x90000000写入跳转指令再触发其“Release from Reset”事件即可。但SMP要求所有核在进入FreeRTOS调度器前必须严格同步于同一个内核初始化点。我们不能让P1在P0刚初始化完调度器变量时就冲进来读取未初始化的pxReadyTasksLists[0]也不能让P2在P0还没设置好全局中断使能位时就开始响应定时器中断。因此我们的启动代码必须重写核心逻辑分三阶段P0独占初始化阶段P0执行完整的硬件初始化GTM定时器、ICU中断控制器、OCRAM内存控制器然后调用prvInitialiseNewlib()如果用newlib-nano、prvInitialiseHeap()初始化OCRAM堆最后调用xTaskGenericCreate()创建第一个空闲任务idle task但不启动调度器即不调用vTaskStartScheduler()。核间同步握手阶段P0将一个全局标志位如ulSMPStartupFlag定义在OCRAM置为0x12345678然后向P1/P2的“Core Release Register”写入启动地址指向我们自定义的vPortSMPEntry()函数并触发释放事件。P1/P2被唤醒后立即进入一个自旋等待循环不断轮询ulSMPStartupFlag直到看到0x12345678才继续执行。并行内核初始化阶段所有核P0/P1/P2在vPortSMPEntry()中先禁用本核中断然后调用prvInitialiseTaskLists()初始化就绪队列数组、prvInitialiseTickTimer()为每个核配置GTM定时器通道注意所有核必须使用同一GTM模块的同一通道通过GTM的“Global Timer”模式实现同步计数最后调用vPortEnableInterrupts()开启中断。此时所有核的FreeRTOS内核数据结构已就绪P0才调用vTaskStartScheduler()启动调度。这个设计的关键在于同步点必须是数据而不是时间。我们不用__delay_cycles(1000000)这种不可靠的等待而是用OCRAM中的一个字来作为“门禁开关”。这保证了无论P1/P2的唤醒延迟有多长受GTM时钟稳定时间影响它们都会在P0彻底准备就绪后才开始执行内核初始化从根本上杜绝了竞态条件。2.3 工具链与构建环境为什么必须用HighTec GCC而非DAVE或EB TresosTC397的官方开发工具链主要有三类Infineon提供的DAVE IDE基于Eclipse、EB提供的TresosAUTOSAR配置工具以及开源的HighTec GCCTriCore专用。很多工程师习惯用DAVE因为它图形化配置方便。但在SMP移植中DAVE是条死胡同。原因有三第一链接脚本控制力缺失。DAVE生成的链接脚本.ld文件将.data、.bss段默认映射到LMU而我们前面已明确要求内核数据必须在OCRAM。DAVE的GUI界面里根本没有选项让你把heap_4.c的ucHeap[]数组单独指定到OCRAM段。你只能手动修改它生成的.ld文件但下次DAVE重新生成配置时你的修改会被覆盖。HighTec GCC则完全不同它的Makefile和链接脚本完全开放你可以精确控制每个符号的放置位置例如在freertos_config.h中定义#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 1024 * 1024 ) ) // 1MB heap in OCRAM然后在链接脚本中.ocram_heap : { . ALIGN(8); _ocram_heap_start .; *(.ocram_heap) _ocram_heap_end .; } OCRAM并在heap_4.c中用__attribute__((section(.ocram_heap)))修饰ucHeap[]确保万无一失。第二汇编层支持不足。FreeRTOS SMP的port.c中大量使用__disable_irq()、__enable_irq()等内联汇编指令这些指令在HighTec GCC的TriCore intrinsics库中有完整实现如__mtcr(CPU_CR_ICR, 0)用于关中断。而DAVE底层使用的编译器通常是旧版GCC对TriCore特定指令的支持不全导致port.c编译报错。第三调试信息兼容性。TC397的调试接口DAP与HighTec GCC生成的DWARF调试信息匹配度最高。我们在J-Link调试时发现用DAVE编译的SMP镜像调试器无法正确解析多核上下文切换时的寄存器快照而HighTec编译的镜像可以清晰看到每个核的SP、PC、PSW值。这对排查vPortEnterCritical()死锁这类问题至关重要。因此本教程全程基于HighTec GCC 5.1.0TriCore专用版和J-Link Commander进行。这不是为了标新立异而是工程实践倒逼出的唯一可靠路径。3. 核心细节解析与实操要点从启动代码到自旋锁每一个字节都不能错3.1 启动代码重写Reset_Handler到vPortSMPEntry()的完整链条TC397的启动代码通常为startup_tricore.s是整个SMP系统的基石。我们不能沿用单核模板必须逐行重写。以下是关键片段及原理说明// startup_tricore.s - 关键部分 .section .text.Reset_Handler,ax,progbits .global Reset_Handler Reset_Handler: // P0: 复位后首条指令必须是P0专属逻辑 movh.a a15, #0x8000 // 加载P0的复位向量基址0x80000000 lea a15, [a15]0x00000000 jmp a15 // 跳转到C语言main()但仅P0执行此跳转 // P1/P2: 复位后执行此处进入自旋等待 movh.a a15, #0x9000 // OCRAM起始地址0x90000000 lea a15, [a15]0x00000000 mov d15, 0x12345678 // 预设同步魔数 st.w [a15]0x00000000, d15 // 将魔数写入OCRAM首地址供P0读取 // 自旋等待循环P1/P2在此处停住直到P0写入同步标志 wait_loop: ld.w d14, [a15]0x00000004 // 读取P0写入的ulSMPStartupFlag偏移0x4 cmp d14, d15 // 比较是否等于魔数 bne wait_loop // 不等则继续等待 jmp vPortSMPEntry // 相等则跳转到SMP入口点这段汇编的精妙之处在于利用了TriCore的地址空间特性。TC397的OCRAM物理地址是0x90000000~0x903FFFFF我们将其首地址0x90000000作为P1/P2的“同步寄存器”。P0在完成初始化后会执行// main.c 中 P0 的初始化末尾 volatile uint32_t * const pulSMPFlag (uint32_t *)0x90000000; *pulSMPFlag 0x12345678; // 写入魔数唤醒P1/P2而P1/P2的汇编代码在wait_loop中不断读取这个地址的值形成一个硬件级的“忙等待”busy-waiting。这里有个极易踩的坑不能用ld.b字节加载而必须用ld.w字加载。因为TriCore的内存对齐要求严格ld.b读取未对齐地址会触发总线错误Bus Error而ld.w要求地址是4字节对齐0x90000000正好满足。这个细节手册里不会强调但调试器会用“HardFault”给你上一课。另一个关键点是vPortSMPEntry()的C语言实现。它必须是所有核的统一入口且不能有任何P0专属逻辑// port.c void vPortSMPEntry( void ) { // 1. 禁用本核中断防止在初始化过程中被抢占 __disable_irq(); // 2. 初始化本核私有数据每个核有自己的TCB指针、栈顶指针 // 注意pxCurrentTCB 是全局变量但每个核的副本必须独立 // 我们用__attribute__((section(.core_local))) 将其放在LMU extern TCB_t * volatile pxCurrentTCB; pxCurrentTCB xTaskTCBs[ xPortGetCoreID() ]; // xPortGetCoreID() 返回0/1/2 // 3. 初始化本核的SysTick实际用GTM定时器替代 prvSetupTimerInterrupt(); // 4. 清空本核的本地中断挂起寄存器ICU ICU_CLEAR_PENDING( ICU_INTERRUPT_ID_GTM ); // 5. 开启本核中断准备接收调度器中断 __enable_irq(); // 6. 所有核在此处汇合P0将在此后调用vTaskStartScheduler() // 其他核则进入空闲循环等待调度器分派任务 for( ;; ) { __asm volatile( nop ); } }这里xPortGetCoreID()的实现是另一个难点。TC397没有像ARM Cortex-A那样的MPIDR_EL1寄存器直接读取核ID。我们必须通过读取TriCore的CPU_ID寄存器地址0xF0000000static inline uint32_t xPortGetCoreID( void ) { uint32_t ulCPUID; __asm volatile( ld.w %0, [0xF0000000] : d(ulCPUID) ); return (ulCPUID 24) 0x03; // CPU_ID寄存器的bit24-25表示核ID }这个ld.w指令必须用内联汇编因为HighTec GCC的intrinsics库里没有封装它。漏掉这一步所有核都会认为自己是P0导致任务调度彻底混乱。3.2 自旋锁Spinlock的TriCore原生实现为什么不能用FreeRTOS默认的ARM汇编FreeRTOS SMP的临界区保护依赖于vPortEnterCritical()和vPortExitCritical()它们底层必须是原子操作。在ARM Cortex-M上FreeRTOS提供了基于BASEPRI寄存器的实现但在TriCore上没有等价的寄存器。TriCore的原子操作依赖于SWAP指令——它能在一条指令内完成“读-修改-写”Read-Modify-Write且硬件保证其原子性。我们实现的vPortEnterCritical()如下// port.c static volatile uint32_t ulCriticalNesting 0; static volatile uint32_t ulSpinLock 0; void vPortEnterCritical( void ) { uint32_t ulCoreID xPortGetCoreID(); uint32_t ulNewValue; // 第一步获取自旋锁确保只有一个核能进入临界区 do { ulNewValue 1; __asm volatile( swap.w %0, [%1], %2 : d(ulNewValue) : a( (uint32_t)ulSpinLock ), d(ulNewValue) : memory ); } while( ulNewValue ! 0 ); // 如果ulSpinLock原值为0空闲swap返回0跳出循环否则继续等待 // 第二步增加临界区嵌套计数 ulCriticalNesting; // 第三步禁用本核中断仅本核有效不影响其他核 __disable_irq(); } void vPortExitCritical( void ) { uint32_t ulCoreID xPortGetCoreID(); // 第一步恢复本核中断 __enable_irq(); // 第二步减少嵌套计数 ulCriticalNesting--; // 第三步释放自旋锁 __asm volatile( st.w [%0], %1 : : a( (uint32_t)ulSpinLock ), d(0) : memory ); }这个实现有三个关键点必须理解SWAP指令的原子性swap.w指令将寄存器%2的值这里是1写入内存地址[%1]ulSpinLock同时将该地址原来的值读入%0ulNewValue。整个过程由硬件保证不可分割。这是TriCore实现互斥锁的唯一可靠方式。临界区嵌套计数的必要性FreeRTOS允许在中断服务程序ISR中调用xQueueSendFromISR()这会再次进入临界区。如果只用一个布尔锁第二次vPortEnterCritical()就会死锁。ulCriticalNesting计数器解决了这个问题——只有当计数减到0时才真正释放锁。__enable_irq()的位置在vPortExitCritical()中我们先开中断再释放锁。这是因为释放锁后其他核可能立刻抢到锁并进入临界区如果此时本核中断还关着它就无法响应新的调度器中断如GTM定时器溢出导致系统僵死。这个顺序是经过实测验证的黄金法则。提示在调试自旋锁时如果发现系统卡死在do-while循环里90%的可能是ulSpinLock变量没有定义在OCRAM中。请立即检查链接脚本确保.bss段中的ulSpinLock被重定向到OCRAM区域。LMU中的变量对其他核不可见swap.w指令会读到随机值。3.3 OCRAM内存分配器heap_4.c的定制化改造FreeRTOS默认的heap_4.c是为单核设计的它假设整个堆内存对所有代码都是可访问的。但在TC397上我们必须确保堆内存ucHeap[]位于OCRAMpvPortMalloc()分配的内存块其地址必须是OCRAM范围内的xPortGetFreeHeapSize()返回的值必须反映OCRAM中真实的剩余空间。改造步骤如下定义OCRAM堆数组在port.c中不再使用static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];而是// port.c __attribute__((section(.ocram_heap))) static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];并在链接脚本中确保.ocram_heap段映射到OCRAM。重写xPortGetFreeHeapSize()原函数直接返回xFreeBytesRemaining但我们需要确保这个值在所有核上读取一致。由于OCRAM是缓存一致的我们只需加一个轻量级屏障size_t xPortGetFreeHeapSize( void ) { size_t xFreeBytes; // 读取前插入内存屏障确保看到最新值 __asm volatile( sync ::: memory ); xFreeBytes xFreeBytesRemaining; // 读取后再次屏障 __asm volatile( sync ::: memory ); return xFreeBytes; }sync指令是TriCore的内存屏障Memory Barrier它强制刷新所有核的写缓冲区保证xFreeBytesRemaining的更新对所有核可见。pvPortMalloc()的校验增强在分配内存前我们增加地址范围检查防止意外分配到LMUvoid *pvPortMalloc( size_t xWantedSize ) { void *pvReturn NULL; uint8_t *pucAlignedHeap; size_t xOffset 0; // 强制对齐到8字节TriCore要求 xWantedSize ( xWantedSize portBYTE_ALIGNMENT_MASK ) ~portBYTE_ALIGNMENT_MASK; vTaskSuspendAll(); { // 检查请求大小是否超过OCRAM堆容量 if( xWantedSize xFreeBytesRemaining ) { // 计算对齐后的起始地址 pucAlignedHeap ucHeap xHeapStructSize; pucAlignedHeap xOffset; // 关键校验确保分配地址在OCRAM范围内0x90000000 ~ 0x903FFFFF if( ( (uint32_t)pucAlignedHeap 0x90000000UL ) ( (uint32_t)pucAlignedHeap 0x90400000UL ) ) { pvReturn ( void * ) pucAlignedHeap; xFreeBytesRemaining - xWantedSize; } } } xTaskResumeAll(); return pvReturn; }这个校验看似多余实则是安全底线。曾经有同事在调试时误将configTOTAL_HEAP_SIZE设为0x100000016MB远超OCRAM容量pvPortMalloc()返回的地址落在了PSRAM区域而PSRAM的访问延迟比OCRAM高10倍导致任务切换时间抖动剧烈系统实时性崩溃。这个简单的if判断救了我们三天的调试时间。4. 实操过程与核心环节实现从环境搭建到第一个SMP任务运行4.1 HighTec GCC环境搭建与项目初始化第一步永远是环境。HighTec GCC的安装包high_tec_gcc_tricore_5.1.0.exe需从其官网下载注意选择TriCore专用版非ARM版。安装路径建议为纯英文如C:\HighTec\gcc-5.1.0-tricore避免中文路径导致Makefile解析失败。创建项目目录结构如下TC397_FreeRTOS_SMP/ ├── Core/ // FreeRTOS内核源码官方下载的FreeRTOS-Kernel-10.5.1 │ ├── portable/ │ │ └── GCC/Tricore_SMP/ // 我们的port.c/portmacro.h存放处 │ └── ... ├── Drivers/ // TC397 HAL驱动Infineon提供的AURIX Development Studio SDK ├── Application/ │ ├── main.c // 主程序含Reset_Handler和SMP初始化 │ └── tasks.c // 用户任务定义 ├── Linker/ // 链接脚本 │ └── tc397_ocram.ld // 定制化链接脚本 ├── Makefile // 高度定制化的构建脚本 └── build/ // 编译输出目录Makefile是成败关键。它必须精确控制编译器参数# Makefile 片段 CC C:/HighTec/gcc-5.1.0-tricore/bin/tricore-gcc.exe CFLAGS -mcputc397 -marchtricore-v1.6.2 -O2 -g -Wall \ -I./Core/include -I./Drivers/inc -I./Core/portable/GCC/Tricore_SMP \ -D__TRICORE__ -DFREERTOS_SMP -DTC397 # 链接脚本必须指定 LDFLAGS -T Linker/tc397_ocram.ld -nostartfiles # 关键禁止编译器优化掉我们的自旋锁循环 CFLAGS -fno-tree-loop-distribute-patterns all: $(BUILD_DIR)/firmware.elf $(BUILD_DIR)/firmware.elf: $(OBJECTS) $(CC) $(LDFLAGS) -o $ $^ # 为port.c单独添加优化禁用确保SWAP指令不被优化 port.o: Core/portable/GCC/Tricore_SMP/port.c $(CC) $(CFLAGS) -O0 -c -o $ $这里-fno-tree-loop-distribute-patterns是救命参数。GCC的某些优化级别如-O2会将do-while自旋循环识别为“无意义死循环”并直接优化掉导致vPortEnterCritical()永远不等待。加上这个flag编译器会老老实实生成swap.w指令。4.2 链接脚本tc397_ocram.ld详解让每一字节都待在该待的地方TC397的内存映射是SMP成功的物理基础。我们使用的tc397_ocram.ld必须精确划分.text代码段放在Flash0x80000000.data/.bss初始化数据段必须重定向到OCRAM.ocram_heapFreeRTOS堆显式定义在OCRAM.core_local每个核私有数据如pxCurrentTCB放在各自LMU/* tc397_ocram.ld */ MEMORY { FLASH (rx) : ORIGIN 0x80000000, LENGTH 8M OCRAM (rwx) : ORIGIN 0x90000000, LENGTH 4M LMU_P0 (rwx) : ORIGIN 0xA0000000, LENGTH 1.5M LMU_P1 (rwx) : ORIGIN 0xA0180000, LENGTH 1.5M LMU_P2 (rwx) : ORIGIN 0xA0300000, LENGTH 1.5M } SECTIONS { .text : { *(.text) } FLASH /* .data 和 .bss 必须放在OCRAM因为它们包含FreeRTOS内核变量 */ .data : { _data_start .; *(.data) _data_end .; } OCRAM .bss : { _bss_start .; *(.bss) *(COMMON) _bss_end .; } OCRAM /* FreeRTOS堆显式定义 */ .ocram_heap : { _ocram_heap_start .; *(.ocram_heap) _ocram_heap_end .; } OCRAM /* 每个核的私有数据放在各自LMU */ .core_local_p0 : { *(.core_local_p0) } LMU_P0 .core_local_p1 : { *(.core_local_p1) } LMU_P1 .core_local_p2 : { *(.core_local_p2) } LMU_P2 }在C代码中我们用__attribute__将变量绑定到对应段// 在port.c中 __attribute__((section(.core_local_p0))) TCB_t xTaskTCBs[3][configNUM_CORES]; // P0的TCB数组 __attribute__((section(.core_local_p1))) TCB_t xTaskTCBs[3][configNUM_CORES]; // P1的TCB数组 // ...以此类推这样xTaskTCBs[0][0]P0的TCB放在LMU_P0xTaskTCBs[0][1]P1的TCB放在LMU_P1而所有核共享的pxReadyTasksLists[]则因在.bss段自动落在OCRAM中。这种精细的内存布局是SMP稳定运行的物理保障。4.3 创建第一个SMP任务验证6核协同工作的黄金测试一切准备就绪我们创建一个能直观证明SMP生效的任务。目标创建3个相同优先级的任务分别在P0/P1/P2上运行并通过串口打印各自的核ID和计数值观察是否真正在不同核上并发执行。// tasks.c void vSMPTestTask( void *pvParameters ) { uint32_t ulCoreID xPortGetCoreID(); uint32_t ulCount 0; // 任务名中包含核ID便于调试器识别 char pcTaskName[20]; sprintf(pcTaskName, SMP_Task_Core%d, (int)ulCoreID); for( ;; ) { // 每次循环打印一次带时间戳用GTM计数器 uint32_t ulGtmCnt GTM_GetGlobalCounterValue(); // 假设已初始化GTM printf([Core%d] Count%lu, GTM_Cnt%lu\n, (int)ulCoreID, ulCount, ulGtmCnt); // 延迟100ms让调度器有机会切换 vTaskDelay( pdMS_TO_TICKS(100) ); } } // main.c 中的main()函数 int main( void ) { // 硬件初始化时钟、GTM、UART等 SystemInit(); GTM_Init(); UART_Init(); // 创建3个SMP测试任务优先级设为2高于idle的0 xTaskCreate( vSMPTestTask, SMP_Task_P0, configMINIMAL_STACK_SIZE, NULL, 2, NULL ); xTaskCreate( vSMPTestTask, SMP_Task_P1, configMINIMAL_STACK_SIZE, NULL, 2, NULL ); xTaskCreate( vSMPTestTask, SMP_Task_P2, configMINIMAL_STACK_SIZE, NULL, 2, NULL ); // 启动调度器 —— 此刻P0开始执行P1/P2已被唤醒并等待调度 vTaskStartScheduler(); // 永远不会到达这里 for( ;; ); }编译烧录后通过串口查看输出[Core0] Count0, GTM_Cnt12345 [Core1] Count0, GTM_Cnt12346 [Core2] Count0, GTM_Cnt12347 [Core0] Count1, GTM_Cnt12445 [Core1] Count1, GTM_Cnt12446 [Core2] Count1, GTM_Cnt1244
返回列表