本文记录本项目将 uC/OS-II 移植到 STM32F407IG + IAR 工程的完整过程,并结合当前已经能够正常调试、运行串口与 Modbus RTU 从站的实际代码进行说明。
本文不是通用示例,文中目录、文件名、任务优先级、时基和接口均以当前工程为准,主要用于:
当前工程已经完成以下验证:
AppTaskStart 可以运行;OSTimeDly() 和 OSTimeGet() 可以正常使用;| 项目 | 当前配置 |
|---|---|
| MCU | STM32F407IG |
| CPU 内核 | ARM Cortex-M4 |
| 工程生成工具 | STM32CubeMX |
| 编译器 | IAR Embedded Workbench for ARM |
| IAR 工程配置名 | Modbus |
| uC/OS-II 版本 | V2.92.11 |
| 操作系统节拍 | 1000 Hz,即 1 ms/节拍 |
| 串口 | USART1 |
| 串口参数 | 115200 bit/s、8 个有效数据位、偶校验、1 个停止位(8E1) |
| 串口接收 | DMA2 Stream2 Channel4 |
| 串口发送 | DMA2 Stream7 Channel4 |
| 应用任务 | AppTaskStart |
| 应用任务优先级 | 5 |
| 应用任务栈 | 256 个 OS_STK 单元 |
IAR 命令行编译工具位于:
F:\IAR Systems\Embedded Workbench 8.3\common\bin
将 uC/OS-II 移植到一个新的 MCU 工程,不只是把源码复制进来。完整移植至少包含以下五部分:
它们的关系如下:
应用任务 AppTaskStart
│
│ 调用 OSTimeDly、OSTimeGet 等 API
▼
uC/OS-II 内核源码
│
│ 请求任务切换、初始化任务栈
▼
Cortex-M4 + IAR 移植层
│
├── SysTick:产生操作系统节拍
└── PendSV:保存旧任务现场并恢复新任务现场
当前相关文件位于:
Middlewares/Third_Party/Micrium/
├─ Config/
│ ├─ app_cfg.h
│ ├─ cpu_cfg.h
│ └─ os_cfg.h
├─ uCOS-II/
│ ├─ Source/
│ │ ├─ ucos_ii.h
│ │ ├─ os_core.c
│ │ ├─ os_task.c
│ │ ├─ os_time.c
│ │ ├─ os_sem.c
│ │ ├─ os_mutex.c
│ │ ├─ os_q.c
│ │ ├─ os_mbox.c
│ │ ├─ os_flag.c
│ │ ├─ os_mem.c
│ │ └─ os_tmr.c
│ └─ Ports/ARM-Cortex-M4/IAR/
│ ├─ os_cpu.h
│ ├─ os_cpu_a.asm
│ ├─ os_cpu_c.c
│ └─ os_dbg.c
├─ uC-CPU/
└─ uC-LIB/
| 文件或目录 | 作用 |
|---|---|
ucos_ii.h |
uC/OS-II 总头文件,应用程序通常只需包含它 |
os_core.c |
内核初始化、调度和中断嵌套管理 |
os_task.c |
任务创建、删除、挂起、恢复和栈检查 |
os_time.c |
任务延时及系统节拍计数 |
os_sem.c |
信号量 |
os_mutex.c |
互斥信号量 |
os_q.c |
消息队列 |
os_mbox.c |
邮箱 |
os_flag.c |
事件标志组 |
os_mem.c |
固定大小内存分区 |
os_tmr.c |
软件定时器 |
os_cpu.h |
Cortex-M4/IAR 的数据类型、临界区和任务切换宏 |
os_cpu_c.c |
初始任务栈构造、CPU 钩子及 SysTick 接口 |
os_cpu_a.asm |
上下文切换和寄存器保存/恢复的汇编实现 |
os_dbg.c |
向调试器提供内核配置及数据尺寸信息 |
os_cfg.h |
内核功能裁剪和资源数量配置 |
app_cfg.h |
应用任务优先级和栈大小配置 |
uC-CPU 和 uC-LIB 目录还提供 Micrium 的 CPU 抽象和通用库头文件。本工程已将相应目录加入头文件搜索路径。
在 IAR 工程中建立 Micrium 分组,并添加下列内容。
Micrium Config:
app_cfg.h
cpu_cfg.h
os_cfg.h
uCOS-II Port:
os_cpu_a.asm
os_cpu_c.c
os_dbg.c
uCOS-II Source:
os_core.c
os_flag.c
os_mbox.c
os_mem.c
os_mutex.c
os_q.c
os_sem.c
os_task.c
os_time.c
os_tmr.c
即使某项功能在 os_cfg.h 中被关闭,对应 .c 文件留在工程中通常也不会产生完整功能代码,因为源码内部会根据配置宏进行条件编译。
在 IAR 中进入:
Project → Options → C/C++ Compiler → Preprocessor
将以下路径加入 Additional include directories:
$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\Config
$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Source
$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uCOS-II\Ports\ARM-Cortex-M4\IAR
$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uC-CPU
$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uC-CPU\ARM-Cortex-M4\IAR
$PROJ_DIR$\..\Middlewares\Third_Party\Micrium\uC-LIB
如果编译提示找不到 ucos_ii.h、os_cpu.h、cpu.h 或 lib_def.h,应首先检查这些搜索路径。
app_cfg.h当前应用任务配置为:
#define APP_TASK_START_PRIO 5u
#define APP_TASK_TEST_PRIO 6u
#define APP_TASK_START_STK_SIZE 256u
#define APP_TASK_TEST_STK_SIZE 256u
其中:
APP_TASK_START_PRIO 是启动任务优先级;APP_TASK_TEST_PRIO 是预留的测试任务优先级,当前还未创建该任务;APP_TASK_START_STK_SIZE 是启动任务栈的元素个数,不是字节数;OS_STK 为 32 位,因此 256 个栈单元约占 1024 字节 RAM。uC/OS-II V2 中:
AppTaskStart 使用优先级 5;任务优先级 5 和 NVIC 中断优先级 5 是两个不同体系,不能直接比较大小。
os_cfg.h 的当前关键配置| 配置项 | 当前值 | 含义 |
|---|---|---|
OS_LOWEST_PRIO |
63 | 应用可用的最低任务优先级范围 |
OS_MAX_TASKS |
10 | 最大应用任务数量 |
OS_TICKS_PER_SEC |
1000 | 每秒 1000 个系统节拍 |
OS_TASK_CREATE_EXT_EN |
1 | 启用 OSTaskCreateExt() |
OS_TASK_STAT_EN |
0 | 不启用统计任务 |
OS_MUTEX_EN |
1 | 启用互斥信号量 |
OS_Q_EN |
1 | 启用消息队列 |
OS_SEM_EN |
1 | 启用信号量 |
OS_TIME_GET_SET_EN |
1 | 允许读取和设置系统时间 |
OS_TIME_TICK_HOOK_EN |
1 | 启用节拍钩子 |
OS_TMR_EN |
0 | 不启用软件定时器 |
OS_MEM_EN |
0 | 不启用固定内存分区 |
OS_CPU_HOOKS_EN |
1 | 使用 CPU 移植层钩子 |
OS_DEBUG_EN |
1 | 保留内核调试信息 |
因为:
#define OS_TICKS_PER_SEC 1000u
所以:
OSTimeDly(1U); /* 延时约 1 ms */
OSTimeDly(100U); /* 延时约 100 ms */
OSTimeDly(1000U); /* 延时约 1 s */
OSTimeDly() 的参数单位是“节拍”,只有在当前 1000 Hz 配置下才刚好等于毫秒。
STM32 HAL 使用 SysTick 维护自己的毫秒计数,HAL_GetTick()、HAL 超时判断等功能依赖 HAL_IncTick()。
uC/OS-II 也需要周期节拍,用于:
OSTimeGet() 返回的系统节拍;因此,本工程在同一个 SysTick_Handler() 中同时服务 HAL 和 uC/OS-II。
Core/Src/stm32f4xx_it.c 中的核心代码为:
void SysTick_Handler(void)
{
HAL_IncTick();
if (OSRunning == OS_TRUE)
{
OS_CPU_SysTickHandler();
}
}
必须保留 OSRunning 判断。原因是 HAL_Init() 会先启动 SysTick,而此时 OSInit() 尚未执行。如果在内核未初始化时直接调用 uC/OS-II 的节拍处理,可能访问尚未准备好的内核状态。
OS_CPU_SysTickHandler() 内部的主要过程是:
进入 SysTick 中断
↓
OSIntEnter()
↓
OSTimeTick():更新延时和系统节拍
↓
OSIntExit():检查是否需要调度更高优先级任务
不能只调用 OS_CPU_SysTickHandler() 而删除 HAL_IncTick(),否则 HAL 的毫秒时基和超时机制会停止。
SysTick 负责告诉内核“时间过去了一个节拍”,PendSV 才真正执行任务上下文切换。
一次任务切换大致包括:
这些底层工作由 os_cpu_a.asm 中的 OS_CPU_PendSVHandler 完成。
当前工程不是在 C 语言的空 PendSV_Handler() 中调用内核,而是直接修改了 IAR 启动文件:
EWARM/startup_stm32f407xx.s
启动文件中声明:
EXTERN OS_CPU_PendSVHandler
并将向量表中的 PendSV 项直接设置为:
DCD OS_CPU_PendSVHandler
所以 Core/Src/stm32f4xx_it.c 中虽然还有一个空的 PendSV_Handler(),实际 PendSV 向量并不指向它。
这是本工程移植中非常关键的一步。若 CubeMX 或其他工具重新生成、替换启动文件,必须确认 PendSV 向量仍然指向 OS_CPU_PendSVHandler。否则常见现象是:
OSTimeDly() 或发生任务切换就卡住;不要同时让向量表指向两个 PendSV 实现,也不要在已有直接向量映射的情况下重复定义另一个同名强符号。
main.c 中启动 uC/OS-II当前应用包含:
#include "ucos_ii.h"
#include "modbus_rtu_slave.h"
ucos_ii.h 内部会继续包含 app_cfg.h,因此 main.c 可以使用任务优先级和栈大小宏。
static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];
任务栈不能定义成任务函数内的普通局部数组,否则函数退出或栈被覆盖后会破坏任务运行现场。这里使用 static,使内存在整个程序运行期间一直存在。
static void AppTaskStart(void *p_arg);
uC/OS-II 任务函数必须符合以下形式:
void TaskName(void *p_arg);
任务通常包含永久循环,不能执行完后直接返回。
本工程顺序为:
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USB_DEVICE_Init();
MX_USART1_UART_Init();
OSInit();
/* 创建应用任务 */
OSTaskCreateExt(...);
OSStart();
各步骤含义如下:
HAL_Init():初始化 HAL 和 SysTick;SystemClock_Config():配置芯片主频和总线时钟;MX_xxx_Init():初始化任务要使用的外设;OSInit():初始化 uC/OS-II 内核数据结构;OSTaskCreateExt():创建至少一个应用任务;OSStart():启动最高优先级就绪任务。正常情况下 OSStart() 不会返回。CubeMX 在后面生成的 while (1) 仅作为程序结构保留,操作系统成功启动后不会再执行到那里。
os_err = OSTaskCreateExt(AppTaskStart,
0,
&AppTaskStartStk[APP_TASK_START_STK_SIZE - 1u],
APP_TASK_START_PRIO,
APP_TASK_START_PRIO,
&AppTaskStartStk[0],
APP_TASK_START_STK_SIZE,
0,
(OS_TASK_OPT_STK_CHK |
OS_TASK_OPT_STK_CLR));
if (os_err != OS_ERR_NONE)
{
Error_Handler();
}
参数说明:
| 参数 | 当前传入值 | 作用 |
|---|---|---|
| 任务入口 | AppTaskStart |
任务开始执行的函数 |
| 任务参数 | 0 |
传给 p_arg,本任务不需要参数 |
| 初始栈顶 | 栈数组最后一个元素 | Cortex-M 栈向低地址增长 |
| 优先级 | APP_TASK_START_PRIO |
当前为 5 |
| 任务 ID | 同任务优先级 | 本工程用优先级作为 ID |
| 栈底 | 栈数组第一个元素 | 用于扩展创建和栈检查 |
| 栈大小 | APP_TASK_START_STK_SIZE |
256 个栈单元 |
| 扩展数据 | 0 |
当前不使用 |
| 任务选项 | STK_CHK \| STK_CLR |
启用栈检查并在创建时清零栈 |
检查 os_err 非常重要。如果任务创建失败却继续调用 OSStart(),后续现象不容易定位。
当前启动任务主要代码如下:
static void AppTaskStart(void *p_arg)
{
(void)p_arg;
if (ModbusSlave_Init(&huart1,
MODBUS_SLAVE_DEFAULT_ADDRESS) != HAL_OK)
{
Error_Handler();
}
while (1)
{
(void)ModbusSlave_SetHoldingRegister(
HMI_REG_UPTIME_SECONDS,
(uint16_t)(OSTimeGet() / OS_TICKS_PER_SEC));
(void)ModbusSlave_SetHoldingRegister(
HMI_REG_RX_FRAME_COUNT,
(uint16_t)g_modbus_slave_stats.valid_frame_count);
ModbusSlave_Poll();
OSTimeDly(1U);
}
}
ModbusSlave_Init();ModbusSlave_Poll() 在任务上下文中解析已收到的帧;OSTimeDly(1U)如果永久循环内没有延时、等待信号量或其他阻塞操作,优先级 5 的任务会一直占用 CPU,使更低优先级任务无法运行。
调用 OSTimeDly(1U) 后:
当前 Modbus 接收采用“中断/DMA 快速接收,任务中解析”的结构:
USART1 收到数据
↓
DMA 自动搬运到接收缓冲区
↓
串口空闲事件/接收事件中断
↓
HAL_UARTEx_RxEventCallback()
↓
ModbusSlave_OnRxEvent()
│
│ 仅记录数据、帧状态并尽快退出
▼
AppTaskStart 周期执行 ModbusSlave_Poll()
↓
CRC 校验、功能码解析、寄存器读写、组织应答
↓
USART1 DMA 发送应答
当前回调接口包括:
HAL_UARTEx_RxEventCallback()
→ ModbusSlave_OnRxEvent()
HAL_UART_TxCpltCallback()
→ ModbusSlave_OnTxComplete()
HAL_UART_ErrorCallback()
→ ModbusSlave_OnUartError()
这种结构的优点是:
当前串口回调没有直接调用 uC/OS-II 的信号量、队列等服务,因此不需要额外在这些回调外层手工调用 OSIntEnter() 和 OSIntExit()。
如果后续在外设中断中直接调用 OSSemPost()、OSQPost() 等内核服务,则必须:
OSInit()初始化内核全局变量、就绪表、任务链表,并建立空闲任务等内部对象。必须先调用它,再创建应用任务和启动调度器。
OSTaskCreateExt()创建一个扩展任务。与简化版创建函数相比,它可以传入栈底、栈大小和任务选项,因此可使用栈检查功能。
OSStart()启动调度器,并切换到当前最高优先级的就绪任务。成功后不会返回 main() 的普通执行流程。
OSTimeDly()使当前任务延时指定节拍。它是操作系统延时,不是原地空转:
OSTimeGet()读取内核从启动后累计的节拍数。当前节拍频率为 1000 Hz,因此:
seconds = OSTimeGet() / OS_TICKS_PER_SEC;
可得到近似运行秒数。计数最终会溢出并回绕,不能把它当作永久不溢出的实时时钟。
OSTaskStkChk()在任务使用了 OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR 时,可以检查任务栈使用情况,用于判断栈是否配置过大或过小。
static OS_STK AppTaskTestStk[APP_TASK_TEST_STK_SIZE];
static void AppTaskTest(void *p_arg)
{
(void)p_arg;
while (1)
{
/* 在这里编写周期性测试功能。 */
OSTimeDly(100U);
}
}
在调用 OSStart() 之前增加:
os_err = OSTaskCreateExt(AppTaskTest,
0,
&AppTaskTestStk[APP_TASK_TEST_STK_SIZE - 1u],
APP_TASK_TEST_PRIO,
APP_TASK_TEST_PRIO,
&AppTaskTestStk[0],
APP_TASK_TEST_STK_SIZE,
0,
(OS_TASK_OPT_STK_CHK |
OS_TASK_OPT_STK_CLR));
if (os_err != OS_ERR_NONE)
{
Error_Handler();
}
也可以先只创建启动任务,再由启动任务创建其他任务。但无论采用哪种方式,都必须保证:
OS_MAX_TASKS。CubeMX 的 Keep User Code when re-generating 选项只能保留位于标准标记内的代码:
/* USER CODE BEGIN ... */
/* USER CODE END ... */
本工程下列自定义内容应放在对应的 USER CODE 区域内:
main.c 中的 uC/OS-II 和 Modbus 头文件;AppTaskStart();OSInit()、任务创建和 OSStart();stm32f4xx_it.c 中的 OS_CPU_SysTickHandler() 调用;写在 USER CODE 标记之外的修改可能被覆盖。
每次 CubeMX 重新生成后,至少检查:
EWARM/startup_stm32f407xx.s 的 PendSV 向量是否仍指向 OS_CPU_PendSVHandler;stm32f4xx_it.c 是否仍调用 HAL_IncTick();stm32f4xx_it.c 是否在 OSRunning 为真时调用 OS_CPU_SysTickHandler();main.c 是否仍然调用 OSInit()、创建任务并调用 OSStart();CubeMX 通常不会主动删除独立放置的 Middlewares/Third_Party/Micrium 源文件,但重新生成 IAR 工程文件时,有可能改变 .ewp 中手工添加的分组或路径,因此重新生成前应提交 Git 或备份工程。
在 IAR 中执行:
Project → Rebuild All
确认:
os_cpu_a.asm、os_cpu_c.c 和内核源码均参与编译;在工程根目录执行:
& 'F:\IAR Systems\Embedded Workbench 8.3\common\bin\IarBuild.exe' `
'EWARM\Modbus.ewp' `
-build Modbus `
-log all
链接映射文件位于:
EWARM\Modbus\List\Modbus.map
该文件可以查看代码、只读数据和读写数据的详细占用。
建议设置或观察:
AppTaskStart() 入口设置断点,确认任务已进入;OSRunning 应在内核启动后变为真;OSTimeGet() 应持续增加;OSTimeDly(1U) 后任务应能再次被唤醒;ucos_ii.h原因通常是 IAR 头文件搜索路径未添加或路径层级错误。检查:
Middlewares\Third_Party\Micrium\uCOS-II\Source
OSStart() 后卡死或任务不运行依次检查:
OSInit();OSTaskCreateExt() 返回值是否为 OS_ERR_NONE;os_cpu_a.asm 是否参与编译和链接;OS_CPU_PendSVHandler;OSTimeDly() 后任务再也不运行重点检查:
OS_CPU_SysTickHandler();OSRunning 是否已经变为真;OS_TICKS_PER_SEC 是否与实际 SysTick 频率匹配;PendSV_Handler()。检查 SysTick_Handler() 中是否仍调用:
HAL_IncTick();
uC/OS-II 和 HAL 共用 SysTick 时,两边的节拍处理都要保留。
说明同一异常被多个强符号实现。应明确采用一种连接方式。
当前工程采用:
SysTick_Handler(),其中同时服务 HAL 和 uC/OS-II;OS_CPU_PendSVHandler。常见原因包括:
应先增加任务栈并使用栈检查,再检查 HardFault 时的栈和寄存器。
uC/OS-II 是基于优先级的抢占式内核。若高优先级任务永久循环且从不延时、不挂起、也不等待事件,低优先级任务不会得到运行机会。
周期任务中应使用:
OSTimeDly();例如打开信号量、队列、统计任务或软件定时器后,应同时确认:
| 文件 | 当前项目中的关键内容 |
|---|---|
Core/Src/main.c |
内核初始化、任务创建、启动调度器、Modbus 应用任务 |
Core/Src/stm32f4xx_it.c |
HAL 与 uC/OS-II 共用的 SysTick 处理 |
EWARM/startup_stm32f407xx.s |
PendSV 向量直接指向 OS_CPU_PendSVHandler |
EWARM/Modbus.ewp |
Micrium 源文件分组及头文件搜索路径 |
Middlewares/Third_Party/Micrium/Config/app_cfg.h |
应用任务优先级和栈大小 |
Middlewares/Third_Party/Micrium/Config/os_cfg.h |
内核裁剪、节拍频率和资源数量 |
Middlewares/Third_Party/Micrium/uCOS-II/Ports/ARM-Cortex-M4/IAR/os_cpu_a.asm |
上下文切换 |
Middlewares/Third_Party/Micrium/uCOS-II/Ports/ARM-Cortex-M4/IAR/os_cpu_c.c |
任务栈初始化及 SysTick 接口 |
Modbus/Src/modbus_rtu_slave.c |
Modbus RTU 从站协议处理 |
当前源码头部的授权说明允许评估、教学或研究等用途。若代码用于正式商业产品,应根据所使用版本核实 Micrium/版权方的商业授权要求,不能仅因为源码可以编译就默认具备商业发布许可。
本工程的 uC/OS-II 移植可以概括为:
复制内核和 Cortex-M4/IAR 移植层
↓
加入 IAR 源文件与头文件路径
↓
配置 os_cfg.h 和 app_cfg.h
↓
SysTick 同时服务 HAL 与 uC/OS-II
↓
启动文件 PendSV 向量接入 OS_CPU_PendSVHandler
↓
OSInit → 创建任务 → OSStart
↓
在任务中运行 ModbusSlave_Poll 和应用逻辑
其中最容易在 CubeMX 重新生成后遗漏的是 IAR 启动文件中的 PendSV 向量修改;最容易在增加任务后遇到的问题是 任务优先级重复、任务栈不足,以及高优先级任务没有延时或等待事件。