uC/OS-III 移植完成多任务环境搭建
文章目录
一、目标与环境
硬件
- STM32F103C8T6 最小系统板(Cortex-M3,ARMv7-M,72MHz,64KB Flash / 20KB SRAM)
- 两个 LED(自行接线,接 PB0、PB1)
- USB-TTL 串口模块(CH340,接 PA9/PA10)
- DAPLink 下载器(SWD)
软件工具链
- Keil MDK5(ARM Compiler V5.06,需勾选 MicroLIB)
- STM32CubeMX(图形化配置工程)
- uC/OS-III 源码(官方 GitHub:SiliconLabs 的 uC-OS3 / uC-CPU / uC-LIB 三件套)
二、RTOS 基础知识
2.1 什么是 RTOS?为什么用 RTOS?
裸机(前后台)程序:一个 while(1) 大循环里顺序执行多个任务。缺点是:任务之间必须互相让出时间,任何一个任务耗时都会卡住后面的任务,实时性差、代码耦合。
RTOS(实时操作系统):把"任务"交给操作系统统一调度。每个任务是一个独立的无限循环函数,用延时主动"睡眠"让出 CPU,操作系统根据优先级和时间片决定"现在该运行哪个任务"。多任务看起来是"同时"在跑(伪并行),实际上由 RTOS 快速切换。
uC/OS-III:一个抢占式、可裁剪的实时内核,提供任务管理、延时、信号量、消息队列、互斥量等。相比 uC/OS-II,它允许多个任务同优先级(支持时间片轮转)。
2.2 关键概念
| 概念 | 含义 | 本实验对应 |
|---|---|---|
| 任务(Task) | 一个无限循环的 C 函数 | AppTaskLED1 / LED2 / UART |
| 优先级(Priority) | 数字越小越优先;抢占式调度按优先级决定运行谁 | 2/3/4/5 |
| 任务栈(Stack) | 每个任务独立的栈空间,保存其局部变量与现场 | AppTaskLED1Stk[...] |
| TCB | 任务控制块,内核记录任务状态的结构体 | AppTaskLED1TCB |
| 系统节拍(Tick) | 内核的心跳,由 SysTick 定时器产生,如每 1ms 一次 | OS_CPU_SysTickInitFreq() |
| 上下文切换 | 保存当前任务现场、恢复下一个任务现场,由 PendSV 异常完成 | OS_CPU_PendSVHandler |
| 任务延时 | 任务"睡"指定时长后让出 CPU,到点内核唤醒它 | OSTimeDlyHMSM() |
| 抢占式调度 | 高优先级任务就绪时立即打断低优先级任务 | 内核自动完成 |
2.3 本实验为什么必须动 SysTick 和 PendSV?
uC/OS 要正常工作,需要两个中断:
- SysTick(系统节拍):周期触发产生 tick,驱动任务延时、时间片调度。必须让给 RTOS。
- PendSV(可悬挂的上下文切换):用于任务切换时保存/恢复现场。必须由 RTOS 接管。
因此移植时要把这两个中断向量从"系统默认的空函数"改成 uC/OS 的处理函数,这就是阶段四的核心。
三、总体流程总览
① CubeMX 建工程(配置外设 + 时基改 TIM4)
│
② 整理 uC/OS 源码(复制三件套到工程 uCOS\ 目录)
│
③ Keil 工程集成(加分组、Include 路径、勾 MicroLIB)
│
④ 移植关键修改(启动文件向量、it.c、cpu_cfg.h、include 路径)
│
⑤ 编写应用代码(app.c:3 个任务 + AppInit)
│
⑥ 编译验证(0 Error 0 Warning)→ 烧录 → 接线 → 观察现象
四、阶段一:CubeMX 建立工程
4.1 新建工程
- 打开 STM32CubeMX → New Project → 选择芯片 STM32F103C8Tx。
- 选择引脚图界面后,先配置 RCC 时钟。
4.2 时钟配置(Clock Configuration)
- 选 HSE 外部 8MHz 晶振,PLL 倍频 ×9 → SYSCLK = 72MHz。
- AHB / APB1 / APB2 分频:HCLK=72M、PCLK1=36M、PCLK2=72M。
4.3 外设与引脚配置

| 外设 | 引脚 | 配置 |
|---|---|---|
| USART1 | PA9=TX、PA10=RX | 115200,8 位,1 停止位,无校验,无流控 |
| GPIO 输出 | PB0 → 命名 LED0;PB1 → 命名 LED1 | Output Push-Pull,无上下拉,初始 High |
生成的宏:
LED0_Pin=GPIO_PIN_0、LED1_Pin=GPIO_PIN_1(均在 GPIOB),见main.h。
4.4 关键一步:把 HAL 时基从 SysTick 改到 TIM4
CubeMX 默认用 SysTick 做 HAL 的延时基准(HAL_Delay)。但 SysTick 要留给 uC/OS 做系统节拍,二者会冲突。

操作:SYS → Timebase Source → 选 TIM4(或任意通用定时器)。生成后工程里会出现一个专门的 stm32f1xx_hal_timebase_tim.c,把 HAL 的 HAL_IncTick 挪到 TIM4 的中断里做。这一步做对,RTOS 才能独占 SysTick。
4.5 生成工程
Project Manager → 选 MDK-ARM(V5)、勾"Generate under root"等 → 生成代码,先用 Keil 编译一遍确认 0 Error 后再往下做。
五、阶段二:uC/OS 源码整理
5.1 下载源码
从官方 GitHub 下载三件套:
uC-OS3-develop(内核)uC-CPU-develop(CPU 无关层 / CPU 移植辅助)uC-LIB-develop(数学、内存、字符串等库)




5.2 确定移植层
STM32F103 是 Cortex-M3 = ARMv7-M,用 Keil/ARMCC 编译器,所以要找的移植层在:
uC-OS3-develop/Ports/ARM-Cortex-M/ARMv7-M/ARM/
该目录下对应 Keil 的文件是:
os_cpu_a.asm(汇编,上下文切换、启动最高优先级任务)os_cpu_c.c(C,SysTick 初始化/中断处理等)os_cpu.h
5.3 复制到工程内
在工程根目录建 uCOS 目录并复制:
uCOS/
├── CORE/ # 内核全部 .c/.h(os_core.c, os_task.c, os_tick.c ... 共约 20 个)
│ # 含配置实例 os_cfg_app.c
├── PORT/ # os_cpu_c.c, os_cpu_a.asm, os_cpu.h (ARMv7-M/ARM,Keil)
├── CPU/ # cpu_core.c, cpu_c.c, cpu_a.asm 及 cpu.h、cpu_cfg.h
├── LIB/ # lib_mem.c, lib_str.c, lib_math.c, lib_ascii.c ...
└── APP/ # 从 Cfg/Template 复制 os_cfg.h、os_cfg_app.h、
# os_app_hooks.c/h 等配置模板 + 我们自己写的 app.c/app.h
说明:
os_cfg.h / os_cfg_app.h / os_app_hooks.c / os_app_hooks.h / cpu_cfg.h / lib_cfg.h属于配置/应用模板,从源码的Cfg/Template目录复制过来再修改。
六、阶段三:Keil 工程集成
打开工程 MDK-ARM\LEDS.uvprojx,做三件事:
6.1 新建文件分组并加入 uC/OS 源文件
在 Project 树中新增分组(与 CubeMX 生成的 Application/MDK-ARM 并列):
| 分组 | 加入的文件 |
|---|---|
| uCOS_CORE | os_cfg_app.c、os_core.c、os_dbg.c、os_flag.c、os_mem.c、os_msg.c、os_mutex.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 |
| uCOS_PORT | os_cpu_c.c、os_cpu_a.asm |
| uCOS_CPU | cpu_core.c、cpu_c.c、cpu_a.asm |
| uCOS_LIB | lib_ascii.c、lib_math.c、lib_mem.c、lib_str.c |
| uCOS_APP | os_app_hooks.c、app.c |
注意:
.asm汇编文件在工程里要标记为汇编类型(Keil 里 FileType=2),否则会被当 C 编译报错。


6.2 配置 Include 路径(C/C++ → Include Paths)
追加 uC/OS 头文件目录:
../uCOS/CORE;../uCOS/PORT;../uCOS/CPU;../uCOS/LIB;../uCOS/APP

6.3 勾选 MicroLIB
在 Target 选项 → Target 页勾选 Use MicroLIB。
作用:提供精简版 C 库,printf 等可用,并支持我们后面做 fputc 重定向到串口。

七、阶段四:移植关键修改
7.1 启动文件:向量表指向 uC/OS 的中断处理函数
原理:前面讲过,RTOS 必须接管 SysTick(节拍)和 PendSV(任务切换)。CubeMX 生成的启动文件 startup_stm32f103xb.s 里这两个中断向量默认指向弱定义的空函数 PendSV_Handler、SysTick_Handler。我们要把它们改成 uC/OS 的实现。
修改 startup_stm32f103xb.s:
(a)在向量表 __Vectors 之前声明外部符号(IMPORT):
IMPORT OS_CPU_PendSVHandler
IMPORT OS_CPU_SysTickHandler
__Vectors DCD __initial_sp ; Top of Stack
(b)把向量表第 74 / 75 项改指向 uC/OS:
DCD OS_CPU_PendSVHandler ; PendSV_Handler
DCD OS_CPU_SysTickHandler ; SysTick_Handler
这一步不改会报
A1516E: Bad symbol,因为改了名字又没 IMPORT。
7.2 stm32f1xx_it.c:删除两个空的默认中断函数
CubeMX 生成的 Core/Src/stm32f1xx_it.c 里有空的 PendSV_Handler() 和 SysTick_Handler()。向量表已指向 OS_CPU_*,这两个空函数不会被调用,但删掉更干净,避免与 RTOS 的中断定义混淆(否则一旦有地方仍引用 SysTick_Handler,会冲突或走错分支)。
删除这两个空函数:
// 删掉以下两个空函数:
// void PendSV_Handler(void) { }
// void SysTick_Handler(void) { }
7.3 cpu_cfg.h:启用 NVIC 优先级位数宏
uC/CPU 层的 cpu_cfg.h 里 CPU_CFG_NVIC_PRIO_BITS 模板默认被 #if 0 禁用。不启用的话,os_cpu_c.c 里关于临界区/中断优先级边界(CPU_CRITICAL 相关、OS_CPU_PendSV_IRQn 优先级赋值依赖它)的计算编译不过。
启用它(STM32 Cortex-M3 支持 4 位优先级,取值 4):
#define CPU_CFG_NVIC_PRIO_BITS 4u
7.4 os_cpu_c.c:修正 include 路径
SiliconLabs 官方仓库里 os_cpu_c.c 开头是:
#include "../../../Source/os.h" // 这是官方仓库的相对路径,工程内不适用
我们已把内核放在 uCOS/CORE/,且已加入 Include 路径,改成:
#include "os.h"
八、阶段五:应用代码(3 个任务)
8.1 app.h
#ifndef APP_H
#define APP_H
#include "os.h"
void AppInit(void);
#endif /* APP_H */
8.2 app.c(核心:3 个任务 + 启动任务 + 系统初始化)
#include "app.h"
#include "main.h" /* LED0_Pin / LED1_Pin 引脚宏 */
#include "os_cpu.h" /* OS_CPU_SysTickInitFreq 声明 */
#include <stdio.h>
/* ---- 任务优先级(数字越小越优先) ---- */
#define APP_CFG_TASK_START_PRIO 2u
#define APP_CFG_TASK_LED1_PRIO 3u
#define APP_CFG_TASK_LED2_PRIO 4u
#define APP_CFG_TASK_UART_PRIO 5u
/* ---- 任务栈大小(CPU_STK 单位 = 4 字节) ---- */
#define APP_CFG_TASK_START_STK_SIZE 256u
#define APP_CFG_TASK_LED1_STK_SIZE 128u
#define APP_CFG_TASK_LED2_STK_SIZE 128u
#define APP_CFG_TASK_UART_STK_SIZE 256u
static OS_TCB AppTaskStartTCB;
static CPU_STK AppTaskStartStk[APP_CFG_TASK_START_STK_SIZE];
static OS_TCB AppTaskLED1TCB;
static CPU_STK AppTaskLED1Stk[APP_CFG_TASK_LED1_STK_SIZE];
static OS_TCB AppTaskLED2TCB;
static CPU_STK AppTaskLED2Stk[APP_CFG_TASK_LED2_STK_SIZE];
static OS_TCB AppTaskUARTTCB;
static CPU_STK AppTaskUARTStk[APP_CFG_TASK_UART_STK_SIZE];
/* 任务 1:每 1s 翻转 LED0(PB0) */
static void AppTaskLED1(void *p_arg)
{
OS_ERR err;
(void)p_arg;
while (1)
{
HAL_GPIO_TogglePin(LED0_GPIO_Port, LED0_Pin);
OSTimeDlyHMSM(0u, 0u, 1u, 0u, OS_OPT_TIME_HMSM_STRICT, &err); /* 延时 1s */
}
}
/* 任务 2:每 3s 翻转 LED1(PB1) */
static void AppTaskLED2(void *p_arg)
{
OS_ERR err;
(void)p_arg;
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
OSTimeDlyHMSM(0u, 0u, 3u, 0u, OS_OPT_TIME_HMSM_STRICT, &err); /* 延时 3s */
}
}
/* 任务 3:每 2s 通过串口发送中文问候(printf 已重定向到 USART1) */
static void AppTaskUART(void *p_arg)
{
OS_ERR err;
(void)p_arg;
while (1)
{
printf("hello uc/OS! 欢迎来到RTOS多任务环境!\r\n");
OSTimeDlyHMSM(0u, 0u, 2u, 0u, OS_OPT_TIME_HMSM_STRICT, &err); /* 延时 2s */
}
}
/* 启动任务:创建另外 3 个任务后删除自己,回收资源 */
void AppTaskStart(void *p_arg)
{
OS_ERR err;
(void)p_arg;
OSTaskCreate(&AppTaskLED1TCB, "LED1", AppTaskLED1, (void *)0,
APP_CFG_TASK_LED1_PRIO, AppTaskLED1Stk,
0u, APP_CFG_TASK_LED1_STK_SIZE, 0u, 0u,
(void *)0,
OS_OPT_TASK_STK_CHK, &err);
OSTaskCreate(&AppTaskLED2TCB, "LED2", AppTaskLED2, (void *)0,
APP_CFG_TASK_LED2_PRIO, AppTaskLED2Stk,
0u, APP_CFG_TASK_LED2_STK_SIZE, 0u, 0u,
(void *)0,
OS_OPT_TASK_STK_CHK, &err);
OSTaskCreate(&AppTaskUARTTCB, "UART", AppTaskUART, (void *)0,
APP_CFG_TASK_UART_PRIO, AppTaskUARTStk,
0u, APP_CFG_TASK_UART_STK_SIZE, 0u, 0u,
(void *)0,
OS_OPT_TASK_STK_CHK, &err);
OSTaskDel(NULL, &err); /* 删除启动任务自己 */
}
/* 应用初始化:OSInit -> 创建启动任务 -> OSStart(由 main 调用) */
void AppInit(void)
{
OS_ERR err;
OSInit(&err); /* 1. 初始化内核 */
OSTaskCreate(&AppTaskStartTCB, "AppTaskStart", AppTaskStart, (void *)0,
APP_CFG_TASK_START_PRIO, AppTaskStartStk,
0u, APP_CFG_TASK_START_STK_SIZE, 0u, 0u,
(void *)0,
OS_OPT_TASK_STK_CHK, &err); /* 2. 创建启动任务 */
OS_CPU_SysTickInitFreq(SystemCoreClock); /* 3. 初始化系统节拍(1ms) */
OSStart(&err); /* 4. 启动调度器(不再返回) */
}
8.3 main.c 的改造
只在 CubeMX 预留的 USER CODE 区添加,CubeMX 再次生成不会丢失:
(a)USER CODE Includes 加头文件:
/* USER CODE BEGIN Includes */
#include "app.h"
/* USER CODE END Includes */
(b)USER CODE 2 调用 AppInit()(放在外设初始化之后):
/* USER CODE BEGIN 2 */
AppInit(); /* uC/OS-III: OSInit -> 创建启动任务 -> OSStart */
/* USER CODE END 2 */
(c)USER CODE 4 加 printf 重定向到 USART1:
/* USER CODE BEGIN 4 */
#include <stdio.h>
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 0xFFFF);
return ch;
}
/* USER CODE END 4 */
这样
printf输出的每个字符都会从串口 USART1 发出(配合已勾选的 MicroLIB)。
8.4 两个最容易踩的坑(本版本)
-
OSTaskCreate是 13 个参数,比常见教程多一个p_ext。
本版本签名(见os.h):(... , OS_TICK time_quanta, void *p_ext, OS_OPT opt, OS_ERR *p_err);
p_ext在opt之前,必须传(void *)0。漏传会报#165 too few arguments/#167类型不兼容。 -
SysTick 系统节拍必须自己初始化。
移植层(os_cpu_c.c)只提供OS_CPU_SysTickInitFreq(),不会自动调用。若不调用,SysTick 不启动,OSTimeDlyHMSM的延时全部失效,任务烧进去"不动"。已在AppInit里于OSStart前调用:OS_CPU_SysTickInitFreq(SystemCoreClock); /* 72MHz 下 1ms 一个节拍 */
九、阶段六:编译、烧录与验证
9.1 编译、烧录配置
在 Keil 里 Build(或命令行),最终结果:
"LEDS\LEDS.axf" - 0 Error(s), 0 Warning(s).
Program Size: Code=11408 RO-data=388 RW-data=156 ZI-data=8036
编译通过、链接成功,说明 OS_CPU_PendSVHandler / OS_CPU_SysTickHandler 都已正确链接,内核完整。
Options for Target → Debug → 选择 CMSIS-DAP Debugger(DAPLink) → Settings 确认识别到设备。
Utilities → Settings → Flash Download → 勾选 Reset and Run,并 Add 一个 STM32F10x Med-density 128k Flash 烧录算法。
LOAD 下载。
9.2 硬件接线
两个 LED(低电平点亮接法):
3.3V ──[限流电阻 330Ω~1kΩ]── LED阳极(+) ── LED阴极(–) ── 单片机GPIO
- LED0(任务1)阴极接 PB0;
- LED1(任务2)阴极接 PB1。
USB-TTL 串口(交叉接 + 共地):
STM32最小系统板 USB-TTL模块
PA9 (USART1_TX) ──────────→ RXD
PA10 (USART1_RX) ──────────← TXD (任务一只发,可不接)
GND ────────── GND (必须共地)
- 波特率 115200;串口助手编码选 GBK
十、遇到的问题与解决
| # | 问题 | 现象 | 解决 |
|---|---|---|---|
| 1 | OSTaskCreate 参数个数/顺序不同 | #165 too few arguments、#167 类型不兼容 | 补传 p_ext((void *)0),它位于 opt 之前(13 参数) |
| 2 | 启动文件改向量名后未 IMPORT | A1516E: Bad symbol 'OS_CPU_PendSVHandler' | 在 __Vectors 前加 IMPORT OS_CPU_PendSVHandler / OS_CPU_SysTickHandler |
| 3 | os_cpu_c.c include 路径是官方仓库相对路径 | #5 cannot open "../../../Source/os.h" | 改为 #include "os.h"(Include 路径已含 uCOS/CORE) |
| 4 | CPU_CFG_NVIC_PRIO_BITS 被模板 #if 0 禁用 | 临界区/中断优先级相关编译错 | cpu_cfg.h 启用为 4u |
| 5 | SysTick 节拍未初始化 | 程序能烧入但不动作(任务延时失效) | AppInit 里、OSStart 前调用 OS_CPU_SysTickInitFreq(SystemCoreClock) |
| 6 | HAL 时基仍用 SysTick | 与 RTOS 抢占 SysTick 冲突 | CubeMX 把 Timebase 改为 TIM4(生成 stm32f1xx_hal_timebase_tim.c) |
| 7 | 中文字符乱码 | 串口收到乱码 | 源文件用 GBK 保存;串口助手编码选 GBK |
十一、运行效果
烧录运行后,在串口助手中每 2 秒收到一行:
hello uc/OS! 欢迎来到RTOS多任务环境!
hello uc/OS! 欢迎来到RTOS多任务环境!
hello uc/OS! 欢迎来到RTOS多任务环境!
...
同时观察板上 LED0 每秒闪一次、LED1 每 3 秒闪一次,与串口输出并行进行、互不阻塞——任务一(uC/OS-III 多任务移植)实验完成。

动画效果:

十二、小结
本次实验圆满完成 ,实现了LED闪烁与串口输出并行进行的效果。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)