一、目标与环境

硬件

  • 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 新建工程

  1. 打开 STM32CubeMX → New Project → 选择芯片 STM32F103C8Tx。
  2. 选择引脚图界面后,先配置 RCC 时钟。

4.2 时钟配置(Clock Configuration)

  • 选 HSE 外部 8MHz 晶振,PLL 倍频 ×9 → SYSCLK = 72MHz。
  • AHB / APB1 / APB2 分频:HCLK=72M、PCLK1=36M、PCLK2=72M。

4.3 外设与引脚配置

在这里插入图片描述

外设引脚配置
USART1PA9=TX、PA10=RX115200,8 位,1 停止位,无校验,无流控
GPIO 输出PB0 → 命名 LED0;PB1 → 命名 LED1Output 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_COREos_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_PORTos_cpu_c.c、os_cpu_a.asm
uCOS_CPUcpu_core.c、cpu_c.c、cpu_a.asm
uCOS_LIBlib_ascii.c、lib_math.c、lib_mem.c、lib_str.c
uCOS_APPos_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 两个最容易踩的坑(本版本)

  1. 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 类型不兼容。

  2. 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

十、遇到的问题与解决

#问题现象解决
1OSTaskCreate 参数个数/顺序不同#165 too few arguments、#167 类型不兼容补传 p_ext((void *)0),它位于 opt 之前(13 参数)
2启动文件改向量名后未 IMPORTA1516E: Bad symbol 'OS_CPU_PendSVHandler'在 __Vectors 前加 IMPORT OS_CPU_PendSVHandler / OS_CPU_SysTickHandler
3os_cpu_c.c include 路径是官方仓库相对路径#5 cannot open "../../../Source/os.h"改为 #include "os.h"(Include 路径已含 uCOS/CORE)
4CPU_CFG_NVIC_PRIO_BITS 被模板 #if 0 禁用临界区/中断优先级相关编译错cpu_cfg.h 启用为 4u
5SysTick 节拍未初始化程序能烧入但不动作(任务延时失效)AppInit 里、OSStart 前调用 OS_CPU_SysTickInitFreq(SystemCoreClock)
6HAL 时基仍用 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闪烁与串口输出并行进行的效果。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐