环境说明

项目 版本
MCU STM32F407IGT6
IDE IAR EWARM V8.32
RTOS uC/OS-III V3.04.04 (Micrium)
HAL库 STM32Cube FW_F4 V1.28.3
CubeMX 6.15.0

本文基于一个由 CubeMX 生成的 STM32F407 HAL 库工程,详细记录如何将 uC/OS-III 移植进去。

一、工程目录结构

移植前的工程结构(CubeMX 生成):

1
2
3
4
5
6
7
8
9
10
11
TrainCamp_wangjunjie_Modbus/
├── Core/
│ ├── Inc/ ← HAL 头文件(main.h, stm32f4xx_hal_conf.h 等)
│ └── Src/ ← HAL 源码(main.c, stm32f4xx_it.c 等)
├── Drivers/
│ ├── CMSIS/
│ └── STM32F4xx_HAL_Driver/
├── EWARM/ ← IAR 工程目录(.ewp 文件所在位置)
│ ├── XDM-60T4-E.ewp
│ └── startup_stm32f407xx.s
└── XDM-60T4-E.ioc

移植后新增的目录和文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
TrainCamp_wangjunjie_Modbus/
├── Core/Inc/ ← 新增 4 个配置头文件
│ ├── os_cfg.h
│ ├── os_cfg_app.h
│ ├── cpu_cfg.h
│ └── lib_cfg.h
├── EWARM/
│ ├── startup_stm32f407xx.s ← 修改(FPU 使能 + PendSV 重定向)
│ └── XDM-60T4-E.ewp ← 修改(include 路径 + 源文件分组)
├── uCOS3/ ← 新增,从 Micrium 源码复制
│ ├── uC-CPU/
│ │ ├── cpu_core.c, cpu_core.h, cpu_def.h
│ │ └── ARM-Cortex-M4/IAR/
│ │ ├── cpu_a.asm
│ │ ├── cpu_c.c
│ │ └── cpu.h
│ ├── uC-LIB/
│ │ ├── lib_ascii.c/.h, lib_math.c/.h, lib_mem.c/.h, lib_str.c/.h, lib_def.h
│ │ └── Ports/ARM-Cortex-M4/IAR/
│ │ └── lib_mem_a.asm
│ └── uCOS-III/
│ ├── Source/ ← 内核源码(20 个 .c 和 .h)
│ └── Ports/ARM-Cortex-M4/Generic/IAR/
│ ├── os_cpu_a.asm
│ ├── os_cpu_c.c
│ └── os_cpu.h
└── Core/Src/
├── main.c ← 修改(UCOS3 初始化 + 任务)
└── stm32f4xx_it.c ← 修改(SysTick_Handler)

二、源码获取

Micrium 官方源码可从 Micrium GitHub 或官网下载。下载后的目录结构为:

1
2
3
4
Micrium/Software/
├── uC-CPU/
├── uC-LIB/
└── uCOS-III/

将这三个文件夹直接复制到工程根目录下的 uCOS3/ 文件夹中。

三、创建配置头文件

uC/OS-III 需要 4 个用户配置头文件。放在 Core/Inc/ 下,因为 IAR 工程已有的 include 路径 $PROJ_DIR$/../Core/Inc 能覆盖到。

路径说明:IAR 的 $PROJ_DIR$ 指向 .ewp 文件所在的目录,即 EWARM/$PROJ_DIR$/../ 即工程根目录 TrainCamp_wangjunjie_Modbus/

3.1 os_cfg.h — 内核功能开关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#ifndef OS_CFG_H
#define OS_CFG_H

#define OS_CFG_APP_HOOKS_EN 1u
#define OS_CFG_ARG_CHK_EN 1u
#define OS_CFG_CALLED_FROM_ISR_CHK_EN 0u
#define OS_CFG_DBG_EN 1u
#define OS_CFG_ISR_POST_DEFERRED_EN 0u
#define OS_CFG_OBJ_TYPE_CHK_EN 1u
#define OS_CFG_TS_EN 1u
#define OS_CFG_PEND_MULTI_EN 0u
#define OS_CFG_PRIO_MAX 64u
#define OS_CFG_SCHED_LOCK_TIME_MEAS_EN 1u
#define OS_CFG_SCHED_ROUND_ROBIN_EN 0u
#define OS_CFG_STK_SIZE_MIN 64u

#define OS_CFG_FLAG_EN 1u
#define OS_CFG_FLAG_DEL_EN 1u
#define OS_CFG_FLAG_MODE_CLR_EN 1u
#define OS_CFG_FLAG_PEND_ABORT_EN 1u

#define OS_CFG_MEM_EN 1u

#define OS_CFG_MUTEX_EN 1u
#define OS_CFG_MUTEX_DEL_EN 1u
#define OS_CFG_MUTEX_PEND_ABORT_EN 1u

#define OS_CFG_Q_EN 1u
#define OS_CFG_Q_DEL_EN 1u
#define OS_CFG_Q_FLUSH_EN 1u
#define OS_CFG_Q_PEND_ABORT_EN 1u

#define OS_CFG_SEM_EN 1u
#define OS_CFG_SEM_DEL_EN 1u
#define OS_CFG_SEM_PEND_ABORT_EN 1u
#define OS_CFG_SEM_SET_EN 1u

#define OS_CFG_STAT_TASK_EN 1u
#define OS_CFG_STAT_TASK_STK_CHK_EN 1u
#define OS_CFG_TASK_CHANGE_PRIO_EN 1u
#define OS_CFG_TASK_DEL_EN 1u
#define OS_CFG_TASK_Q_EN 1u
#define OS_CFG_TASK_Q_PEND_ABORT_EN 1u
#define OS_CFG_TASK_PROFILE_EN 1u
#define OS_CFG_TASK_REG_TBL_SIZE 1u
#define OS_CFG_TASK_SEM_PEND_ABORT_EN 1u
#define OS_CFG_TASK_SUSPEND_EN 1u

#define OS_CFG_TIME_DLY_HMSM_EN 1u
#define OS_CFG_TIME_DLY_RESUME_EN 1u
#define OS_CFG_TLS_TBL_SIZE 0u

#define OS_CFG_TMR_EN 1u
#define OS_CFG_TMR_DEL_EN 1u

#define TRACE_CFG_EN 0u

#endif

3.2 os_cfg_app.h — 应用层配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef OS_CFG_APP_H
#define OS_CFG_APP_H

#define OS_CFG_MSG_POOL_SIZE 100u
#define OS_CFG_ISR_STK_SIZE 128u
#define OS_CFG_TASK_STK_LIMIT_PCT_EMPTY 10u

#define OS_CFG_IDLE_TASK_STK_SIZE 64u

#define OS_CFG_INT_Q_SIZE 10u
#define OS_CFG_INT_Q_TASK_STK_SIZE 100u

#define OS_CFG_STAT_TASK_PRIO 11u
#define OS_CFG_STAT_TASK_RATE_HZ 10u
#define OS_CFG_STAT_TASK_STK_SIZE 100u

#define OS_CFG_TICK_RATE_HZ 1000u // 1ms tick
#define OS_CFG_TICK_TASK_PRIO 1u
#define OS_CFG_TICK_TASK_STK_SIZE 100u

#define OS_CFG_TMR_TASK_PRIO 11u
#define OS_CFG_TMR_TASK_RATE_HZ 10u
#define OS_CFG_TMR_TASK_STK_SIZE 100u

#endif

3.3 cpu_cfg.h — CPU 层配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#ifndef  CPU_CFG_MODULE_PRESENT
#define CPU_CFG_MODULE_PRESENT

#define CPU_CFG_NAME_EN DEF_ENABLED
#define CPU_CFG_NAME_SIZE 16

#define CPU_CFG_TS_32_EN DEF_ENABLED
#define CPU_CFG_TS_64_EN DEF_DISABLED
#define CPU_CFG_TS_TMR_SIZE CPU_WORD_SIZE_32

#if 0
#define CPU_CFG_INT_DIS_MEAS_EN
#endif
#define CPU_CFG_INT_DIS_MEAS_OVRHD_NBR 1u

#if 1
#define CPU_CFG_LEAD_ZEROS_ASM_PRESENT
#endif

#if 0
#define CPU_CFG_TRAIL_ZEROS_ASM_PRESENT
#endif

#define CPU_CFG_CACHE_MGMT_EN DEF_DISABLED

#endif

3.4 lib_cfg.h — 库配置

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef  LIB_CFG_MODULE_PRESENT
#define LIB_CFG_MODULE_PRESENT

#define LIB_MEM_CFG_ARG_CHK_EXT_EN DEF_DISABLED
#define LIB_MEM_CFG_OPTIMIZE_ASM_EN DEF_ENABLED
#define LIB_MEM_CFG_DBG_INFO_EN DEF_DISABLED
#define LIB_MEM_CFG_HEAP_SIZE (3u * 1024u)
#define LIB_MEM_CFG_HEAP_PADDING_ALIGN LIB_MEM_PADDING_ALIGN_NONE

#define LIB_STR_CFG_FP_EN DEF_DISABLED
#define LIB_STR_CFG_FP_MAX_NBR_DIG_SIG LIB_STR_FP_MAX_NBR_DIG_SIG_DFLT

#endif

四、修改 IAR 工程配置(XDM-60T4-E.ewp)

4.1 添加 Include 路径

在 IAR 工程选项 C/C++ Compiler → Additional include directories 中,原有路径保持不变,追加以下 6 条:

1
2
3
4
5
6
$PROJ_DIR$/../uCOS3/uCOS-III/Source
$PROJ_DIR$/../uCOS3/uCOS-III/Ports/ARM-Cortex-M4/Generic/IAR
$PROJ_DIR$/../uCOS3/uC-CPU
$PROJ_DIR$/../uCOS3/uC-CPU/ARM-Cortex-M4/IAR
$PROJ_DIR$/../uCOS3/uC-LIB
$PROJ_DIR$/../uCOS3/uC-LIB/Ports/ARM-Cortex-M4/IAR

其中 $PROJ_DIR$ 是 IAR 的内置变量,指向 .ewp 文件所在目录(即 EWARM/),$PROJ_DIR$/../ 即工程根目录。以上路径展开后分别对应:

IAR 路径 实际文件系统路径
$PROJ_DIR$/../uCOS3/uCOS-III/Source TrainCamp_wangjunjie_Modbus/uCOS3/uCOS-III/Source/
$PROJ_DIR$/../uCOS3/uCOS-III/Ports/ARM-Cortex-M4/Generic/IAR TrainCamp_wangjunjie_Modbus/uCOS3/uCOS-III/Ports/ARM-Cortex-M4/Generic/IAR/
$PROJ_DIR$/../uCOS3/uC-CPU TrainCamp_wangjunjie_Modbus/uCOS3/uC-CPU/
$PROJ_DIR$/../uCOS3/uC-CPU/ARM-Cortex-M4/IAR TrainCamp_wangjunjie_Modbus/uCOS3/uC-CPU/ARM-Cortex-M4/IAR/
$PROJ_DIR$/../uCOS3/uC-LIB TrainCamp_wangjunjie_Modbus/uCOS3/uC-LIB/
$PROJ_DIR$/../uCOS3/uC-LIB/Ports/ARM-Cortex-M4/IAR TrainCamp_wangjunjie_Modbus/uCOS3/uC-LIB/Ports/ARM-Cortex-M4/IAR/

4.2 添加源文件到工程

在 IAR 工程面板中创建以下分组,添加对应源文件。路径均相对于 $PROJ_DIR$/../uCOS3/

UCOS-III → Source 组(文件位于 uCOS3/uCOS-III/Source/):

  • os_core.c, os_flag.c, os_int.c, os_mem.c, os_msg.c
  • os_mutex.c, os_pend_multi.c, os_prio.c, os_q.c, os_sem.c
  • os_stat.c, os_task.c, os_tick.c, os_time.c, os_tmr.c
  • os_var.c, os_dbg.c, os_cfg_app.c

UCOS-III → Ports 组(文件位于 uCOS3/uCOS-III/Ports/ARM-Cortex-M4/Generic/IAR/):

  • os_cpu_a.asm
  • os_cpu_c.c

uC-CPU 组

  • uCOS3/uC-CPU/cpu_core.c
  • uCOS3/uC-CPU/ARM-Cortex-M4/IAR/cpu_a.asm
  • uCOS3/uC-CPU/ARM-Cortex-M4/IAR/cpu_c.c

uC-LIB 组

  • uCOS3/uC-LIB/lib_ascii.c, lib_math.c, lib_mem.c, lib_str.c
  • uCOS3/uC-LIB/Ports/ARM-Cortex-M4/IAR/lib_mem_a.asm

五、修改启动文件(EWARM/startup_stm32f407xx.s)

这是移植中最关键的一步,需要修改两处。

5.1 在 Reset_Handler 中使能 FPU

Cortex-M4 复位后 FPU 默认禁用。如果 IAR 工程开启了 FPU(General Options → FPU → FPUv4-SP),必须在 Reset_Handler 中、调用 SystemInit 之前使能 CP10/CP11 协处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Reset_Handler

;; 使能 FPU(CP10 和 CP11 协处理器)
LDR R0, =0xE000ED88 ; CPACR 寄存器地址
LDR R1, [R0] ; 读取 CPACR
ORR R1, R1, #(0xF << 20) ; 设置 bits 20-23
STR R1, [R0] ; 写回
DSB
;; 禁用 FPU 上下文自动保存(UCOS3 自己管理)
LDR R0, =0xE000EF34 ; FPCCR 寄存器地址
LDR R1, [R0]
BIC R1, R1, #0xC0000000 ; 清除 ASPEN 和 LSPEN
STR R1, [R0]
ISB

LDR R0, =SystemInit
BLX R0
LDR R0, =__iar_program_start
BX R0

为什么必须这样做?

  • CPACR (0xE000ED88):不使能 CP10/CP11 的话,UCOS3 上下文切换中的 FPU 指令(VMRS/VSTMDB)会触发 UsageFault → HardFault
  • FPCCR (0xE000EF34):UCOS3 在 OSTaskSwHook 中手动管理 FPU 寄存器(S0-S31)的保存/恢复,必须禁用硬件自动保存

5.2 重定向 PendSV_Handler

UCOS3 的上下文切换通过 PendSV 异常实现。需要将 startup 文件中的默认 PendSV_Handler 替换为跳转到 UCOS3 的 OS_CPU_PendSVHandler

找到原文件中的:

1
2
3
4
        PUBWEAK PendSV_Handler
SECTION .text:CODE:REORDER:NOROOT(1)
PendSV_Handler
B PendSV_Handler

替换为:

1
2
3
4
5
6
        EXTERN  OS_CPU_PendSVHandler
PUBWEAK PendSV_Handler
SECTION .text:CODE:REORDER:NOROOT(2)
PendSV_Handler
LDR R0, =OS_CPU_PendSVHandler
BX R0

注意点:

  • NOROOT(2) 而非 NOROOT(1)LDR R0, =label 生成的字面量池需要 4 字节对齐,NOROOT(1) 只保证 2 字节对齐,会产生 Warning[440]
  • 不能用 C 函数包装 PendSV_Handler 来调用 OS_CPU_PendSVHandler:PendSV 处理函数修改栈指针后通过异常返回退出,C 函数的返回流程会破坏这个机制

六、修改中断处理文件(Core/Src/stm32f4xx_it.c)

6.1 添加 UCOS3 头文件

在文件顶部的 USER CODE Includes 区域加入:

1
2
3
/* USER CODE BEGIN Includes */
#include "os.h"
/* USER CODE END Includes */

6.2 实现 SysTick_Handler

HAL 和 UCOS3 共享 SysTick 定时器。SysTick_Handler 需要同时服务两者。该函数定义在 stm32f4xx_it.c 的 USER CODE 区域,会覆盖 startup 文件中的 weak 默认实现:

1
2
3
4
5
void SysTick_Handler(void)
{
HAL_IncTick(); // HAL 超时计数(HAL_Delay 等依赖此函数)
OS_CPU_SysTickHandler(); // UCOS3 tick(任务延时、时间片轮转等)
}

七、实现 CPU_TS_TmrRd(Core/Src/main.c)

UCOS3 的时间戳功能(用于测量调度锁时间等)需要 CPU_TS_TmrRd() 函数。使用 Cortex-M4 内置的 DWT 周期计数器实现。

main.c 的 USER CODE 0 区域添加函数定义:

1
2
3
4
5
6
/* USER CODE BEGIN 0 */
CPU_TS_TMR CPU_TS_TmrRd (void)
{
return ((CPU_TS_TMR)DWT->CYCCNT);
}
/* USER CODE END 0 */

main() 的 USER CODE Init 区域初始化 DWT:

1
2
3
4
5
/* USER CODE BEGIN Init */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // 使能 DWT 跟踪
DWT->CYCCNT = 0; // 清零周期计数器
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // 启动计数
/* USER CODE END Init */

DWT 是 ARM Cortex-M 核心调试组件,STM32CubeMX 没有对应的配置入口,只能手动初始化。这是标准做法,Micrium 官方 BSP 也是如此。

八、编写 main.c 框架

8.1 完整的关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include "main.h"
#include "os.h"

/* 任务栈和 TCB */
#define APP_TASK_START_STK_SIZE 256u
static OS_TCB AppTaskStartTCB;
static CPU_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];

#define APP_TASK_GPIO_STK_SIZE 256u
static OS_TCB AppTaskGPIOTCB;
static CPU_STK AppTaskGPIOStk[APP_TASK_GPIO_STK_SIZE];

/* 函数声明 */
static void AppTaskStart(void *p_arg);
static void AppTaskGPIO(void *p_arg);

/* DWT 时间戳读取 */
CPU_TS_TMR CPU_TS_TmrRd(void)
{
return ((CPU_TS_TMR)DWT->CYCCNT);
}

int main(void)
{
OS_ERR err;

HAL_Init();

/* DWT 初始化 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();

/* UCOS3 初始化 */
OSInit(&err);

/* 创建启动任务 */
OSTaskCreate((OS_TCB *)&AppTaskStartTCB,
(CPU_CHAR *)"App Task Start",
(OS_TASK_PTR ) AppTaskStart,
(void *) 0,
(OS_PRIO ) 3,
(CPU_STK *)&AppTaskStartStk[0],
(CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_START_STK_SIZE,
(OS_MSG_QTY ) 0,
(OS_TICK ) 0,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);

/* 启动多任务调度 */
OSStart(&err);

while (1) {} // 不应到达此处
}

/* 启动任务:创建其他任务后自删除 */
static void AppTaskStart(void *p_arg)
{
OS_ERR err;
(void)p_arg;

OSTaskCreate((OS_TCB *)&AppTaskGPIOTCB,
(CPU_CHAR *)"App Task GPIO",
(OS_TASK_PTR ) AppTaskGPIO,
(void *) 0,
(OS_PRIO ) 4,
(CPU_STK *)&AppTaskGPIOStk[0],
(CPU_STK_SIZE) APP_TASK_GPIO_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_GPIO_STK_SIZE,
(OS_MSG_QTY ) 0,
(OS_TICK ) 0,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);

OSTaskDel((OS_TCB *)0, &err);
}

/* GPIO 切换任务 */
static void AppTaskGPIO(void *p_arg)
{
OS_ERR err;
(void)p_arg;

while (DEF_TRUE) {
HAL_GPIO_WritePin(Y0_GPIO_Port, Y0_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(Y2_GPIO_Port, Y2_Pin, GPIO_PIN_SET);
OSTimeDlyHMSM(0, 0, 0, 500, OS_OPT_TIME_HMSM_STRICT, &err);

HAL_GPIO_WritePin(Y0_GPIO_Port, Y0_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(Y2_GPIO_Port, Y2_Pin, GPIO_PIN_RESET);
OSTimeDlyHMSM(0, 0, 0, 500, OS_OPT_TIME_HMSM_STRICT, &err);
}
}

8.2 关于 __disable_irq()

HAL_Init() 内部会调用 HAL_InitTick() 配置 SysTick 并使能其中断。理论上如果 SysTick 中断在 OSInit() 之前触发,OS_CPU_SysTickHandler() 会访问未初始化的 UCOS3 变量导致 HardFault。

实际测试中,从 HAL_Init()OSInit() 之间的执行时间极短(微秒级),SysTick 的首次中断(1ms 后)几乎不可能在这个窗口触发,因此不加 __disable_irq() 通常也能正常运行。

作为防御性编程,可以在 main() 最开头加 __disable_irq()OSStart()OSStartHighRdy() 会在启动第一个任务时执行 CPSIE I 开中断。这不是必须的,但推荐加上。

九、踩坑总结

问题 现象 原因 解决
FPU 未使能 PendSV 上下文切换时 HardFault IAR 开启了 FPU 但 startup 文件没有使能 CPACR Reset_Handler 中写 CPACR 使能 CP10/CP11
PendSV_Handler 链接错误 上下文切换异常 C 包装函数破坏异常返回流程 用汇编重定向,NOROOT(2) 对齐
CPU_TS_TmrRd 未定义 链接报错 Error[Li005] 时间戳功能需要该函数 用 DWT CYCCNT 实现
字面量池对齐警告 Warning[440] / Warning[22] LDR =label 需要 4 字节对齐 NOROOT(1) 改为 NOROOT(2)

参考