你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【NUCLEO-H5E5ZJ评测】ThreadX 移植实录:从 CubeMX 到双线程

[复制链接]
xusiwei1236 发布时间:2026-11-11 01:12

【NUCLEO-H5E5ZJ评测】ThreadX 移植实录:从 CubeMX 到双线程

[TOC]

上一篇《CoreMark 移植实录》在这块 NUCLEO-H5E5ZJ 上跑出了 700.89 分的裸机成绩。本篇更进一步:通过 CubeMX 的 Middleware 启用 ThreadX 实时操作系统,在 Project Manager → Thread safe settings 中启用多线程支持并选择加锁策略,生成 CMake 工程,最后创建两个线程,分别以 500ms、1000ms 周期无限循环打印线程名称与计数,并在串口上验证双线程的调度节奏。

全程命令行构建(STM32CubeCLT 1.22 + CMake + Ninja + GCC 14.3.1),CubeMX 6.18.1 完成配置与代码生成。完整工程代码见文末仓库,git 记录了从 ioc 到成品的全过程。

一、准备工作

1.1 ThreadX 简介

ThreadX 是 Eclipse 基金会旗下的 Eclipse ThreadX(原 Azure RTOS,微软 2019 年开源、2023 年捐赠给 Eclipse),一个体积小、确定性强、免认证费(Safety/Cryptography 认证包免费提供)的嵌入式 RTOS。它的核心特点:

  • Picokernel™ 架构:内核服务不嵌套,中断延迟、上下文切换时间均为确定值;
  • 无运行时开销的按需加载:未使用的内核 API 不参与链接,最小内核仅 2KB 左右 Flash;
  • 优先级抢占式调度 + 可选抢占阈值(preemption-threshold):可以在创建线程时限制被同优先级及更低优先级线程抢占,减少不必要的上下文切换与抖动。

在 STM32 生态里,ThreadX 以固件包内置 Middleware 的形式提供(CubeMX 的 Middleware and Software Packs 分类下直接可见),不需要像 FreeRTOS-H7 那样额外安装 X-CUBE 扩展包。

1.2 实验目标

在上一线程实验开始前,本文的工程基线直接复用上一篇成果:

  • 时钟:**HSE 48MHz 晶振 → PLL(M=12, N=125, P=2)→ SYSCLK 250MHz**(板上 X2 48MHz 晶振 @ PH0/PH1,坑点分析见上一篇);
  • 串口:板载 ST-LINK VCP(USART3,PD8/PD9,115200-8-N-1),printf 经 BSP 的 __io_putchar 直接可用;
  • 调试:SWD(PA13/PA14)。

在此基础上启用 ThreadX,创建两个线程并验证调度。

1.3 工程组织

与上一篇一致,目录分为两半:

  • STM32H5E5-ThreadX-Code:只放代码(ioc + CubeMX 生成的 CMake 工程 + 应用代码),git 管理;
  • STM32H5E5-ThreadX-Test:放本篇文稿与截图。

二、项目骨架

2.1 CubeMX 启用 THREADX

ThreadX 在 CubeMX 里是固件包内置的 Middleware IP:Pinout & Configuration 左侧 Middleware and Software Packs 分类下,THREADX 项已经出现(带版本勾选标记)。启用方式与外设一致——点击后在面板中确认核心配置:

  • Generate initialization code:勾选(生成 MX_ThreadX_Init() 与内核入口);
  • Application Thread Creation:勾选(让 CubeMX 自动生成一个示例线程的创建代码);
  • Application memory pool size:3 * 1024(静态字节池,供内核对象/线程栈分配)。

CubeMX 组件表中 ThreadX 已启用(Middleware and Software Packs)

通过参数自动生成的线程命名为 Thread_500ms(入口 Thread500ms_Entry,优先级 2)——本实验的第一个线程完全由 GUI 配置产生。

2.2 Thread-safe 设置

这是本篇最重要的一个配置点。CubeMX 生成的 HAL 库内部有若干非可重入的锁(__HAL_LOCK 宏、newlib 的 malloc/free 堆锁等),裸机下它们形同虚设,上了 RTOS 之后必须给这些锁一个多线程安全的实现,否则两个线程同时触碰 HAL/堆就会踩出诡异问题。

在 Project Manager → Thread-safe Settings 中:

  1. 勾选 Enable multi-threaded support;
  2. Thread-safe Locking Strategy 选择 Default – Mapping suitable strategy depending on RTOS selection——按 RTOS 选择自动映射的策略。

Project Manager:Enable multi-threaded support 与加锁策略

Default 策略在 ThreadX 工程里最终落为编译宏 STM32_THREAD_SAFE_STRATEGY=2——对照生成的 stm32_lock.h,即 Generic Strategy #2:基于关中断的临界区实现,允许在中断中使用锁。同时 CubeMX 生成 newlib_lock_glue.c / stm32_lock.h 一对胶水文件,把 newlib 的堆锁挂到这套抽象上——这是很多人上 RTOS 后 malloc 偶发损坏却找不到原因的解药。

2.3 代码生成

Toolchain/IDE 选 CMake 后 GENERATE CODE。生成的工程关键内容:

STM32H5E5-ThreadX/
├── AZURE_RTOS/App/            # app_azure_rtos.c(字节池与对象定义)
├── Core/Src/
│   ├── main.c                 # MX_ThreadX_Init() 调用
│   ├── app_threadx.c          # 线程创建 + 线程入口(USER CODE 区)
│   ├── tx_initialize_low_level.S   # SysTick/中断栈等底层初始化
│   └── stm32h5xx_hal_timebase_tim.c # HAL 时基 = TIM6(!)
├── Middlewares/ST/threadx/    # ThreadX 内核(common + cortex_m33/gnu 移植层)
├── newlib_lock_glue.c         # newlib 堆锁 ↔ STM32 锁抽象胶水
└── stm32_lock.h               # 多线程锁抽象(策略 1~5)

几个值得注意的生成细节:

  • HAL 时基被自动挪到 TIM6:ThreadX 的调度 tick 接管了 SysTick,HAL 的 HAL_GetTick()/HAL_Delay() 改由 TIM6 中断驱动(优先级 15,最低),SysTick 优先级被降为 14——避免高优先级中断抢占调度器;
  • PendSV/SVCall/SysTick 异常处理函数由 ThreadX 移植层提供,CubeMX 不再生成;
  • 内核 tick 频率 100Hz(tx_initialize_low_level.S 中 SYSTICK_CYCLES = SYSTEM_CLOCK / 100),即 1 tick = 10ms。

2.4 一个必踩的坑:固件包子模块

用 GitHub 克隆方式获取固件包的话(本文环境如此),首次构建会直接报错:

CMake Error at cmake/stm32cubemx/CMakeLists.txt:260 (target_sources):
  Cannot find source file:
    .../Middlewares/ST/threadx/ports/cortex_m33/gnu/src/tx_thread_context_restore.S

工程里 Middlewares/ST/threadx 是空目录——与上一篇 BSP 缺失同根同源:固件包里 ThreadX/FileX/USBX/NetXDuo 等中间件全部是 git 子模块,CubeMX 生成时从固件包拷贝内核源码,子模块没拉齐就只会拷出一个空壳。补齐子模块(国内网络需代理):

cd ~/STM32Cube/Repository/STM32Cube_FW_H5_V1.7.0
git -c http.proxy=http://127.0.0.1:7890 submodule update --init --depth 1 \
  Middlewares/ST/threadx

然后重新生成,内核源码(common/src 185 个文件 + ports/cortex_m33/gnu/src 17 个文件)被正确拷入工程。

2.5 构建验证

cmake --preset Release
cmake --build build\Release

构建 197 个目标全部通过、零告警:FLASH 26780 B / RAM 8376 B。ThreadX 内核 + 双线程应用的全部开销不到 27KB——Picokernel 的体积优势名副其实。

三、双线程实验

3.1 线程代码

两个线程采用两种创建方式,正好覆盖 CubeMX + ThreadX 的两种典型用法:

线程一(Thread_500ms):完全由 CubeMX 参数生成。App_ThreadX_Init() 中由 CubeMX 自动生成创建代码,栈从 3KB 字节池分配(TX_APP_STACK_SIZE=512),优先级 2。我们只需填充它的入口函数:

void Thread500ms_Entry(ULONG thread_input)
{
  /* USER CODE BEGIN Thread500ms_Entry */
  uint32_t count = 0;
  for (;;)
  {
    printf("[Thread_500ms] hello %lu\r\n", count++);
    tx_thread_sleep(50);  /* 100Hz tick:50 tick = 500ms */
  }
  /* USER CODE END Thread500ms_Entry */
}

线程二(Thread_1000ms):在 app_threadx.c 的 USER CODE 区手动创建,静态线程控制块 + 静态栈,不经字节池:

/* USER CODE BEGIN PV */
static TX_THREAD tx_thread_1000ms;
static UCHAR thread_1000ms_stack[1024];
/* USER CODE END PV */

/* USER CODE BEGIN App_ThreadX_Init */
if (tx_thread_create(&tx_thread_1000ms, "Thread_1000ms", Thread1000ms_Entry, 0,
                     thread_1000ms_stack, sizeof(thread_1000ms_stack),
                     3, 3, TX_NO_TIME_SLICE, TX_AUTO_START) != TX_SUCCESS)
{
  ret = TX_THREAD_ERROR;
}
/* USER CODE END App_ThreadX_Init */

/* USER CODE BEGIN 1 */
static void Thread1000ms_Entry(ULONG thread_input)
{
  uint32_t count = 0;
  for (;;)
  {
    printf("[Thread_1000ms] hello %lu\r\n", count++);
    tx_thread_sleep(100);  /* 100Hz tick:100 tick = 1000ms */
  }
}
/* USER CODE END 1 */

两个线程的时延都用 tx_thread_sleep() 实现——线程让出 CPU,由 ThreadX tick 唤醒,与裸机 HAL_Delay() 的忙等有本质区别:等待期间 CPU 可以跑别的线程或进入低功耗。

另外在 main.c 的 USER CODE 区、MX_ThreadX_Init() 之前打印系统时钟横幅(此时 BSP COM 已初始化):

printf("\r\n[STM32H5E5-ThreadX] System Clock: %lu Hz\r\n", SystemCoreClock);
printf("[STM32H5E5-ThreadX] ThreadX RTOS demo (two threads: 500ms / 1000ms)\r\n");

3.2 串口验证

烧录(STM32_Programmer_CLI -c port=SWD -d xxx.elf)后用 pyserial 打开 COM29 复位监听,串口输出:

pyserial 观察 ThreadX 双线程调度输出

[Thread_500ms] hello 132
[Thread_1000ms] hello 66
[Thread_500ms] hello 133
[Thread_500ms] hello 134
[Thread_1000ms] hello 67
[Thread_500ms] hello 135
...

四、结果讨论

调度节奏精确 2:1。 每打印 2 行 Thread_500ms 恰好插入 1 行 Thread_1000ms(hello 计数 132↔66、133↔67……严格对半),两个线程的计数器随时间同步增长——ThreadX 的 tick 调度没有漂移,tx_thread_sleep 的周期精确到 tick(10ms)粒度。

为什么输出不会串行化交叠? 两个线程共用 printf → 轮询式 HAL_UART_Transmit。一行约 25 字节 @115200bps ≈ 2.2ms,小于 1 个 tick(10ms);且两个线程的唤醒相位都落在 5 tick 的整数倍上,调度时序天然错开。当然这是"恰好安全"——生产代码里多个线程共享同一个 UART 输出,应该用 TX_MUTEX 保护或者走队列汇总到单一打印线程。

Thread-safe 策略的实际含义。 Default 策略在本工程映射为 STM32_THREAD_SAFE_STRATEGY=2(关中断式临界区、允许中断内加锁)。它不是 ThreadX 原生 API 实现的锁——对 HAL 这种"临界区极短"的场景,关中断的开销反而小于内核 mutex。这印证了 CubeMX 那句 "Mapping suitable strategy depending on RTOS selection":Default 不是敷衍选项,而是按 RTOS 与 HAL 使用模式给出的工程默认值。若应用需要在中断里频繁使用 HAL 且临界区较长,可再评估 Generic #3(deny from interrupts)或自定义策略 #1。

HAL 时基迁移是 RTOS 工程的隐形关键点。 启用 ThreadX 后 SysTick 归调度器所有,HAL 时基自动改挂 TIM6。如果手工建工程时忽略了这一步(保留 SysTick 给 HAL),会出现 tick 冲突、HAL_Delay 卡死或调度抖动等疑难杂症——CubeMX 自动处理了这一切。

两种线程创建方式的选择。 字节池分配(CubeMX 默认)省心但引入分配失败的可能性;静态分配(本文线程二)则把内存预算完全静态化,更适合安全关键场景。ThreadX 两者都支持,且可以在同一工程里混用。

五、参考资料

本文完整工程代码(ioc + CubeMX 生成的 CMake 工程 + 双线程应用):

https://gitcode.com/xusiwei1236/STM32H5E5-ThreadX

仓库以 git 管理,从 ioc 创建、ThreadX 子模块排障到双线程实现的完整变更历史都在仓库提交记录中,可逐一回溯。

参考链接:

  1. Eclipse ThreadX 官方文档:https://eclipse-threadx.github.io/
  2. ThreadX 用户手册(内核服务):https://github.com/eclipse-threadx/threadx
  3. STM32CubeH5 固件包(GitHub):https://github.com/STMicroelectronics/STM32CubeH5
  4. 上一篇:【NUCLEO-H5E5ZJ评测】CoreMark 移植实录
  5. NUCLEO-H5E5ZJ 原理图(ST-LINK VCP 与 USART3 连接关系)
赞 收藏 评论0 发布时间:2026-11-11 01:12

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版