十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

ThreadX在SoC上的移植:启动向量、时基与中断适配

ThreadX在SoC上的移植:启动向量、时基与中断适配 简介ThreadX 实时操作系统移植资料包面向嵌入式开发者、系统工程师及正在评估 RTOS 选型的团队解决硬实时任务调度、资源受限场景下的系统移植与集成问题。资源覆盖 ARM、PowerPC、MIPS 及 DSP 等多种内核平台可帮助读者跳过繁琐的板级适配直接评估和落地 ThreadX 项目。包内共收录 1635 个文件压缩后约 5.42MB主要以 C 源码、汇编文件、头文件、目标文件及链接脚本为核心同时包含 IAR、Keil、GCC 等常见工具链的工程配置文件和批处理构建脚本便于在不同 SoC 上参考与实际移植。目前已有 413 人学习浏览。资料还提供了多平台启动配置、静态库和内存布局文件配合源码注释可帮助理解 ThreadX 的调度机制、中断管理和内存管理方式。从工程模板到底层驱动衔接均有覆盖适合作为硬实时系统学习与项目落地的实用参考资料。1. ThreadX 实时操作系统在 SOC 上的移植先处理启动向量再谈任务很多从裸机转过来的工程师拿到 ThreadX 官网源码包第一件事是把tx_api.h加入工程然后创建几个线程最后发现板子没有任何输出。这是 SOC 移植最容易被绕过的环节ThreadX 能否跑起来取决于_tx_initialize_low_level、系统时基和中断控制器是否和芯片的异常向量表对齐。ThreadX 是硬实时操作系统镜像体积可以控制在个位数 KB没有产品版权费因此在消费电子、汽车电子和工业控制中大量落地。下面以 ARM SoC 上的 Cortex-M7 和 Cortex-A9 两条分支说明移植链路尽量直接给出可复现的参数和命令。适合跑过裸机、准备在 Zynq 或国产 ARM SoC 上接入实时操作系统的工程师。2. 移植前拆包ThreadX 源码结构与 SOC 内存布局映射2.1 官网源码包里必须动手的 4 类文件拿到 ThreadX 官网源码包后目录会按tx_api、tx_core、tx_port拆开。tx_api是用户能调用的 C 接口例如tx_thread_create、tx_mutex_gettx_core是与处理器无关的调度算法、对象管理逻辑理论上是通用的tx_port是处理器相关的移植层SOC 适配主要改这里。不要在tx_api和tx_core里乱动官方源码经过长时间验证问题多数出在tx_port的宏开关和汇编代码上。下表列出了实际移植时最需要关心的几个文件以及它们各自承担的任务文件职责移植关注点tx_port.h定义基础类型、开关宏和TX_THREAD等结构对齐方式例如TX_UINT在 32 位 ARM 上必须为 4 字节tx_initialize_low_level.S提供_tx_initialize_low_level()初始化 RAM、检查 CPU 类型需要与链接脚本中的段符号配合tx_thread_schedule.S第一次上下文切换和后续任务切入切出寄存器压栈顺序必须符合 AAPCStx_timer_interrupt.S/.c时基 tick 入口要能够被 SoC 的定时器中断直接调用tx_port.h中还有一个容易被忽略的宏TX_INCLUDE_USER_DEFINE_FILE。它的作用是让内核在编译前包含用户自定义的tx_user.h从而覆盖默认调度节拍、消息池深度等参数。如果不创建tx_user.h就把开关打开第一轮编译就会报fatal error: tx_user.h: No such file or directory。反过来如果你想打开TX_ENABLE_STATISTICS、TX_ENABLE_FPU_SUPPORT等功能宏又没把开关注入tx_user.h这些配置不会真正参与编译代码看起来正常行为却不生效。2.2 内存布局链接脚本决定移植能否跑起来SOC 上通常有 TCM、OCRAM、DDR 等多种内存区域不同区域对 cache 和写缓冲的策略不同运行延迟差别很大。ThreadX 内核本身不关心地址是否固定但向量表必须位于 bootloader 或 ROM 跳转指令指向的位置否则芯片上电后根本进不了Reset_Handler。另一个问题是--gc-sections在裁剪无用段时可能误删启动向量所以链接脚本里要把向量段设为KEEP。以 Cortex-M7 SoC 为例一套可用的最小链接脚本如下ENTRY(Reset_Handler) MEMORY { ROM (rx) : ORIGIN 0x08000000, LENGTH 512K RAM (rwx) : ORIGIN 0x24000000, LENGTH 512K } SECTIONS { .text : { KEEP(*(.isr_vector)) *(.text*) *(.rodata*) _etext ALIGN(4); } ROM .data : { _sdata ALIGN(4); *(.data*) _edata ALIGN(4); } RAM AT ROM .bss (NOLOAD) : { _sbss ALIGN(4); *(.bss*) *(COMMON) _ebss ALIGN(4); } RAM /DISCARD/ : { *(.ARM.exidx*) *(.comment*) } }MEMORY块定义了两个区域ROM放只读代码与常量RAM放数据段。AT ROM表示 data 段运行地址在 RAM但初始值和只读段放在一起启动代码需要从_etext处把数据拷贝到_sdata起始地址。ALIGN(4)保证指针 4 字节对齐避免在 Cortex-M4 上触发总线错误。/DISCARD/段里的.ARM.exidx是 C 异常展开表裸机 RTOS 通常不需要也可以主动丢弃它来减小镜像。这段脚本并没有为 ThreadX 预分配内核堆栈只定义了段边界。真正给tx_thread_create使用的栈空间由你在tx_application_define阶段用tx_byte_pool_create从 RAM 区域申请。这样处理比较灵活也便于在链接后看到还剩多少 RAM。2.3 SoC 启动序列Reset_Handler 和 tx_kernel_enter从裸机到 RTOS入口最大的变化是从main()换成tx_kernel_enter()。tx_kernel_enter首先调用移植者实现的_tx_initialize_low_level在其中完成段拷贝、BSS 清零和基础外设初始化然后调用tx_application_define让用户创建线程最后进入调度循环。tx_kernel_enter正常情况不返回。用 ARM GCC 编译时最小启动代码可以写到同一个汇编文件里.syntax unified .thumb .section .isr_vector,a,%progbits .word _estack .word Reset_Handler .section .text .thumb_func .global Reset_Handler Reset_Handler: ldr r0, _estack mov sp, r0 bl SystemInit bl __libc_init_array bl tx_kernel_enter .Lloop: b .Lloop_estack必须在链接脚本里定义一般是 RAM 区域结束地址向下对齐到 8 字节后的位置。SystemInit由芯片固件库提供负责把 PLL、Flash 等待周期和外设时钟调好在 SOC 芯片启动阶段这一步如果没执行后面 SysTick 的频率会按错误时钟计算。__libc_init_array负责调用 C 库的全局构造函数如果使用裸机-nostartfiles需要自己保留它否则静态对象的初始化不会执行。最终tx_kernel_enter不返回所以死循环b .Lloop只是为了兜底。许多 SoC 的启动实际发生在引导 ROM 里DDR 未初始化。这种情况下链接地址不能直接指向 DDR需要先用芯片内部 SRAM 跑一个很小的 loader完成时钟和 DDR 控制器初始化后再跳转到 ThreadX 镜像。这一点在vivado搭建soc教程和 Zynq 相关的 BSP 里体现得最明显FSBL 负责 DDR 初始化然后才把应用程序加载到 DDR 执行的地址。3. kernel_enter 之后的适配时基初始化、GIC 中断与 AXI-4 数据一致性3.1 时基定时器与 TX_TIMER_TICKS_PER_SECONDThreadX 的调度节拍由TX_TIMER_TICKS_PER_SECOND控制常见取 100 或 1000。1000 意味着每个 tick 是 1ms配合tx_thread_sleep(1)可以得到毫秒级延时100 则更适合低功耗场景因为定时器中断频率低CPU 被唤醒的次数少。在硬实时应用里我一般先按 1000 设计等到优化功耗时再降下来。在 Cortex-M 系列 SOC 上用 SysTick 作为 tick 来源最简单#define TX_TIMER_TICKS_PER_SECOND 1000u void soc_tick_start(void) { uint32_t sysclk SystemCoreClock; SysTick_Config(sysclk / TX_TIMER_TICKS_PER_SECOND); SCB-SHP[11] 0xF0u; } void SysTick_Handler(void) { _tx_timer_interrupt(); }SysTick_Config参数是两次 tick 中断之间的内核周期数不是毫秒数所以要用时钟频率除期望频率。SCB-SHP[11]设置 SysTick 的异常优先级0xF0表示最低优先级让外设中断可以抢占 tick 处理。这样做的原因是 ThreadX 的定时器服务会在_tx_timer_interrupt中唤醒延时线程如果它占用了过高优先级必然增加外设中断延迟破坏实时性。_tx_timer_interrupt内部不应执行较长的时间操作所有延时线程的整理工作都会在这里发生因此 tick 频率越高可用的 CPU 时间就越少。3.2 Cortex-A9 的 GIC 中断入口Cortex-A9 的 SOC 上没有 SysTick常用 GIC private timer 或 global timer 提供时基。中断分发由 GIC 完成外设产生中断后GIC distributor 把中断路由到某个 CPU 接口CPU 进入 IRQ 模式读取GICC_IAR清掉 pending再跳转对应中断处理函数。ThreadX 在 Cortex-A 移植层里会提供 IRQ 入口但你要把 SOC 的中断路由配置好。一个精简的 GIC 分发函数如下void soc_irq_dispatcher(void) { uint32_t irq GICC_IAR; if (irq SOC_PRIVATE_TIMER_IRQ) { _tx_timer_interrupt(); } else if (local_irq_vector[irq] ! 0) { local_irq_vector[irq](); } GICC_EOIR irq; }读取GICC_IAR本身会告知 GIC 中断已受理写GICC_EOIR表示中断处理完成。它需要满足“必须和当前中断号一致”的原则。local_irq_vector是一张函数指针表移植时要把 UART、ETH、DMA 等中断服务函数注册进去。特别容易出错的是 GIC 优先级掩码GICC_PMR如果它设得太小低于阈值的中断永远不会被响应。多核 SOC 上每个核心的 private timer 的基准不同ThreadX 的 tick 只在一个核上跑另一个核不能直接调用_tx_timer_interrupt需要核间中断同步。3.3 AXI-4 外设与 ThreadX 共享内存的一致性处理现代 SOC 片内互联的“黄金标准”是从 AMBA 总线演进到 AXI-4 形成的CPU、DMA、GPU 等多个主设备都会通过它访问同一块物理内存。多主访问下CPU 的写回式 cache 很容易让其他总线 master 看到旧数据反过来DMA 写入的数据也可能长时间停留在 cache 里CPU 读到的不是最新值。ThreadX 只是一个 RTOS它不会替你维护 cache 一致性所以这块内容在移植后碰到的问题最多。比较稳的方式是显式 cache 操作。以 Cortex-M7 的线程代码为例#define BUFFER_LEN 4096u uint8_t pdm_buf[BUFFER_LEN] __attribute__((aligned(32))); void consumer_thread(ULONG arg) { TX_MUTEX *m (TX_MUTEX *)arg; while (1) { tx_mutex_get(m, TX_WAIT_FOREVER); SCB_InvalidateDCache_by_Addr((uint32_t *)pdm_buf, BUFFER_LEN); process_pdm(pdm_buf, BUFFER_LEN); tx_mutex_put(m); tx_thread_sleep(2); } }SCB_InvalidateDCache_by_Addr把指定地址范围内的数据 cache 标记为无效下一次读取时从内存重新装入。缓冲区地址进行 32 字节对齐是 Cortex-M7 cache 操作的最小粒度长度不对齐会连带破坏相邻变量的缓存数据。在生产者线程里对应地也要执行SCB_CleanDCache_by_Addr把 CPU 写入的数据刷到内存。更彻底的做法是在 MPU 中把该 buffer 所在 region 配成 Device 或 Non-cacheable这样不需要手动维护但会产生较大的访问延迟。下面的表给出了三种策略的取舍方案优点缺点适用场景显式 clean/invalidate灵活保留 cache 性能每个共享 buffer 都要写维护代码音频帧、图像帧等大块数据MPU 配 Non-cacheable无需 cache 维护所有访问都穿透总线外设寄存器、队列控制块DMA 直写 write-through 区域读 cache 能利用写带宽被写入内存限制发送缓冲区、状态标志无论哪种方案先决条件是 ThreadX 任务栈和内核对象本身不放在这些共享内存区域里避免调度器操作TCB时出现不可预期的总线访问。对于 SoC 芯片架构本身就具备一致性端口如 AMBA ACE 总线的高端芯片可以绕过软件维护但那已经超出普通 RTOS 移植范围不做展开。4. libgcc.a、libc.a 与 libPDMFilter工具链依赖和 ABI 匹配4.1 ThreadX 对标准库的隐式依赖ThreadX 内核只依赖少量由tx_port.h提供的类型定义不强制要求标准 C 库。但应用代码里一个memcpy、一个printf、一个sprintf都会把libgcc.a与libc.a拉进来。libgcc.a解决编译器生成但 CPU 本身没有的指令例如 64 位乘除、浮点数格式转换的辅助函数libc.a提供标准函数实体。对 ARM 裸机来说还有-lgcc -lc -lnosys这样的配套组合。一个用于 ThreadX Cortex-M7 的典型 Makefile 链接片段TRIPLE arm-none-eabi- CFLAGS -mcpucortex-m7 -mthumb -mfpufpv5-d16 -mfloat-abihard CFLAGS -O2 -g -Wall -ffunction-sections -fdata-sections LDFLAGS -mcpucortex-m7 -mthumb -Wl,--gc-sections -Wl,-T,link.ld LDFLAGS --specsnano.specs LIBS -Wl,--start-group -lm -lc -lgcc -lnosys -Wl,--end-group all: app.elf app.elf: main.o port.o app_task.o $(TRIPLE)gcc $(LDFLAGS) $^ $(LIBS) -o $-mfpufpv5-d16是 Cortex-M7 双精度 FPU 的浮点 ABI 描述-mfloat-abihard指定浮点参数用 VFP 寄存器传递。这两个选项必须和预编译库的编译参数一致否则会出现“selected FPU does not match”之类的汇编器警告。--specsnano.specs把默认 newlib 换成 newlib-nano它去掉了文件系统需要的open、close等符号更适合 RTOS。--start-group与--end-group之间的库可以互相引用解决数学函数在libm和libc之间循环依赖的问题。-lnosys提供一组空的系统调用保证printf如果不调用底层 I/O 时也能链接通过。4.2 libPDMFilter 预编译库的选型PDM 滤波库常常以.a预编译形式出现例如libPDMFilter_CM7_IAR_wc32.a、libPDMFilter_CM4_GCC_wc32.a。这些库是给数字麦克风前端做脉冲密度调制解码。后缀里的CM7、CM4对应 ARM 内核CM7库可能使用双精度 FPUCM4一般只使用单精度 FPUIAR与GCC是不同的编译器和 ABIwc16/wc32表示输出样本位宽是 16 位还是 32 位。选择时一条铁律是编译器与 ABI 必须完全匹配。用 GCC 链接_IAR_库轻则archive has no index重则链接成功但运行到库函数时 FPU 寄存器状态错乱。可以按下表快速判断库名后缀适配内核编译工具链典型 CFLAGS_CM4_GCC_wc16Cortex-M4arm-none-eabi-gcc-mcpucortex-m4 -mfloat-abihard -mfpufpv4-sp-d16_CM7_GCC_wc32Cortex-M7arm-none-eabi-gcc-mcpucortex-m7 -mfloat-abihard -mfpufpv5-d16_CM7_IAR_wc32Cortex-M7IAR ARM CompilerIAR 工程选项不可与 GCC 混用除了库本身选对ThreadX 线程还需要启用 FPU 支持。在tx_user.h中定义TX_ENABLE_FPU_SUPPORT后ThreadX 会在任务切换时保存和恢复 FPU 寄存器。如果不启用第一个浮点运算很可能触发 HardFault因为进入线程前 FPU 上下文已经被调度器破坏了。对于 Cortex-M7 这种带有双精度 FPU 的内核还要确认库使用硬浮点还是软浮点编译选项里-mfloat-abisoftfp会和预编译的硬浮点库不兼容。4.3 链接顺序与裁剪边界问题RTOS 移植的真正问题往往出在链接顺序。GCC 在静态库中默认按“左边先解析”的方式工作如果libPDMFilter.a引用了__aeabi_dadd而libgcc.a写在libPDMFilter.a左边链接器可能不会去libgcc里查找这个已经处理过的符号。把所有库放进--start-group/--end-group是解决循环依赖的最直接办法代价只是链接时间稍长。用nm检查预编译库的外部引用也很实用arm-none-eabi-nm build/libPDMFilter_CM7_GCC_wc32.a | grep U 输出中的U符号是未定义引用必须由你的工程或其它库提供。常见的如memcpy、__aeabi_memset、__floatunsidf分别对应libc.a和libgcc.a。如果使用--gc-sections还要保证启动向量被KEEP否则在-ffunction-sections下没有被显式引用的.isr_vector会被当作孤立段丢弃。在处理 ThreadX 静态库时--gc-sections是按“section”粒度裁剪不是按函数名所以不要试图通过修改宏控制裁剪应该在生成库或编译内核时把不需要的模块排除。SoC 芯片架构不同外设桥接部分的寄存器访问宏也不同这部分依赖厂商 HAL与 ThreadX libc 链接没有直接关系但会影响你是否能顺利引用SystemInit和SystemCoreClock。5. 收尾技巧用 DWT/PMU 周期计数器实测 ThreadX 上下文切换开销5.1 开启内核周期计数器上下文切换耗时是 RTOS 移植最常被问到的指标。与其相信官网的手册参数不如在 SoC 上自己测。Cortex-M7 可以使用 DWT 的CYCCNTCortex-A9 则可以用 PMU 的周期计数。不管哪种功能都依赖调试访问权限。一个通用的初始化代码void target_cycle_count_enable(void) { #if defined(__CORTEX_M) CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0u; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; #elif defined(__CORTEX_A) ARM_PMU_Enable(); #endif }CoreDebug_DEMCR_TRCENA_Msk必须先置位否则 DWT 内部模块未使能时写CYCCNT无效。DWT-CTRL的CYCCNTENA位是计数器启动开关。Cortex-A9 的 PMU enable 则需要通过 CP15 指令设置PMCR的E位同时清零溢出标志否则计数器会在溢出后静止不更新。5.2 用同优先级线程测量调度延迟测量时尽量少做额外事情。建两个同优先级线程在一个线程中记录进入切换前的周期值调用tx_thread_relinquish让出 CPU另一个线程读取差值也可以。更简单的是测量切换返回到原线程的时间volatile uint32_t t0, t1; void thread_a(ULONG arg) { while (1) { t0 DWT-CYCCNT; tx_thread_relinquish(); t1 DWT-CYCCNT; /* t1 - t0 近似为一次同优先级切换后回到本线程的时间差 */ } }tx_thread_relinquish直接交出 CPU不走 tick 等待结果能反映 ThreadX 的 ready 队列检索和上下文切换开销。但要注意t0到t1之间包含了被切换出去的线程的一小段执行时间因此得到的是一个“往返”时间不是单次切换时间。更精确的做法是把 DWT 值通过 UART 在采集若干次后做统计剔除毛刺。对比不同-O优化级别和是否打开 cache 的差异可以判断调度器代码是否被放在了慢速内存区域。5.3 用 TX_ENABLE_STATISTICS 验证调度状态除了自己数周期ThreadX 还提供统计宏。定义TX_ENABLE_STATISTICS后每个TX_THREAD控制块会额外记录挂起次数、恢复次数和时间片被抢占次数。在tx_user.h里定义该宏然后重新编译内核库。调用接口可以这样写TX_THREAD_PERFORMANCE_INFO perf_info; UINT status tx_thread_performance_info_get(my_thread, perf_info); if (TX_SUCCESS status) { printf(suspensions: %lu, resumes: %lu\n, perf_info.tx_thread_performance_suspension_count, perf_info.tx_thread_performance_resume_count); }suspension_count表示线程因等待信号量、互斥锁或延时主动挂起的次数resume_count表示从挂起列表移回就绪队列的次数。两者差值异常扩大通常说明某把锁被高频唤醒但线程并未真正执行可能是优先级反转导致。统计宏会使每个调度操作多几条原子加减指令发布时可以关闭然而移植阶段务必打开配合TX_DISABLE_EXTRA_MACROS进一步排除控制块大小不一致的问题。最后再给一个排错点移植完成后若在切换时进入 HardFault优先检查tx_thread_create传入的栈指针是否 8 字节对齐以及_tx_initialize_low_level有没有清理 BSS这一点反复踩坑的人都懂。本文还有配套的精品资源点击获取
返回列表