☰
GD32F103C8T6移植UCOSIII全流程:从工程搭建到稳定运行
2026/10/10 23:02:59 网站建设 项目流程

简介:一份面向嵌入式开发者的GD32F103C8T6与UCOSIII实时操作系统移植资源,适用于希望在该MCU上快速开启多任务设计的工程师。包内提供已调试并成功运行的完整工程源码、配置文件及官方示例套件,涵盖中断向量表配置、硬件初始化、任务调度、延时函数与printf串口调试等关键实现,可直接基于MDK5.24环境编译参考。资源共2000个文件,以h、c源码为主,辅以uvproj、uvopt工程配置及ewp、ewd等IAR相关文件,另有pdf文档与bin/hex固件,总计约36.39MB。已有375人学习浏览,适合正在学习GD32或UCOSIII、需要实际移植范例的软硬件工程师快速上手,也可作为工程模板扩展使用。 GD32F103C8T6 这颗国产 MCU 和 UCOSIII 实时操作系统放一起,是我最近项目中真正调通的一套组合。以前用裸机点灯、跑串口,觉得 20KB SRAM 根本没必要上 RTOS,但等你把任务拆开,塞进抢占式调度、信号量、消息队列之后,整个代码结构完全是另一套逻辑。这篇博客就围绕“GD32F103C8T6 上如何正确移植 UCOSIII 并稳定跑起来”展开,把从工程搭建、中断向量修改、SysTick 配置到任务栈裁剪的完整过程记录下来,也把几个容易翻车的细节单独拎出来讲。适合正在学 uC/OS-III 的入门者,也适合打算把裸机工程迁移到实时操作系统的开发者。

1. 项目概述与方案选型

1.1 为什么选 GD32F103C8T6 这颗芯片

GD32F103C8T6 是兆易创新推出的 Cortex-M3 内核 MCU,64KB Flash、20KB SRAM,标称主频 108MHz,常规设计按 72MHz 使用。它和 STM32F103C8T6 在引脚上是兼容的,很多板子可以互换,价格和供货在近两年也更有优势,因此不少产品设计直接把它当作替代方案来评估。

但从做 RTOS 移植的角度看,真正友好的地方在于:Cortex-M3 内核自带 SysTick、SVC、PendSV 这三个触发机制,UCOSIII 的任务切换正是依赖它们完成的。无论是 GD32 还是 STM32,内核部分行为是一致的,差异只集中在外设库、启动文件和部分寄存器映射上。这意味着只要你把基于 Cortex-M3 的 UCOSIII 移植层放对位置,内核跑起来是没有任何障碍的。

另外一个现实问题是资源。C8T6 只有 20KB RAM,UCOSIII 内核本身、空闲任务、统计任务、中断栈都要占用内存。所以这块芯片能不能跑,不取决于“能否移植”,而取决于“能否把内存预算控制住”。做方案选型时需要把这一点想清楚:如果业务逻辑复杂、队列和信号量开得多,建议直接上 C8T6 的高配兄弟比如 GD32F103VCT6,省得后面优化内存调到想哭。

1.2 UCOSIII 内核特点与本次目标

UCOSIII(uC/OS-III)是抢占式实时内核,和早些年的 uC/OS-II 相比,最大变化是支持“一个优先级上挂多个任务”,用时间片轮转调度,并且支持不限数量的任务创建。它提供了信号量、互斥量、消息队列、事件标志组、软件定时器等组件,源码结构清晰、注释详细,很适合深入学习 RTOS 原理。

这次项目目标很明确:在 GD32F103C8T6 上跑通一个最小可用的 UCOSIII 系统,创建两个业务任务,一个是 LED 闪烁,一个是用串口周期性打印系统状态。在这个基础上再打开内核统计任务,读取 CPU 利用率和任务栈使用量,用来验证调度是否正常。整个过程中不会用到太复杂的 IPC 机制,但我会把相关配置和调试方法讲透,方便你后续扩展。

2. 移植前准备与工程目录搭建

2.1 硬件与工具链准备

我用的硬件很简单:一块 GD32F103C8T6 核心板,ST-Link 下载器,一个 USB 转 TTL 串口模块,外加板载 LED。如果你手头是蓝 Pill 那种兼容板,注意看一下晶振是不是 8MHz 的无源晶振,这会影响后面 SystemCoreClock 的计算。

工具链方面,我推荐 Keil MDK 5 加 ARMCC5 编译器。原因很实际:官方 uC/OS-III 移植包里多数示例工程都是 ARMCC 语法,os_cpu_a.asm 里大量使用的是老式汇编伪指令,用 AC6 编译时容易报错,还得改汇编文件格式,新手很容易卡在这一步。如果你非要用 AC6,后面 5.4 节我再单独说处理办法。

另外需要安装 GigaDevice 的 GD32F10x DFP 设备支持包,或者使用 GD 官方标准外设库。注意不要直接用 STM32 的 SVD 或标准库去编译 GD32 工程,因为 GD 的外设位定义和部分寄存器布局有差异,最稳妥的做法是到兆易创新官网下载 GD32F10x 固件库,或者直接用 Keil 里从 pack 安装好的库。

2.2 拿到 UCOSIII 源码与移植文件

UCOSIII 源码建议直接从 Silicon Labs 的 GitHub 仓库获取,搜索 uC-OS3 即可。仓库里的目录结构大致是:

  • uC-CPU:CPU 抽象层,包含#ifdef 汇编、数据类型定义、CPU_TS 时间戳等;
  • uC-LIB:库函数实现,比如内存拷贝、字符串操作、数学运算;
  • uC-OS3:内核源码,Source 目录下是 os_core.c、os_task.c、os_time.c、os_sem.c 等一系列文件;
  • uC-OS3/Ports:按 CPU 架构和编译器划分的移植层,选 ARM-Cortex-M3 目录下的 ARMCC 版本。

把下面这些文件加入 Keil 工程:

uC-CPU/ARM-Cortex-M3/ARMCC/os_cpu.h uC-CPU/ARM-Cortex-M3/ARMCC/os_cpu_a.asm uC-CPU/ARM-Cortex-M3/ARMCC/os_cpu_c.c uC-CPU/ARM-Cortex-M3/ARMCC/cpu_core.c uC-CPU/ARM-Cortex-M3/ARMCC/cpu_core.h uC-LIB/lib_mem.c uC-LIB/lib_str.c uC-LIB/lib_math.c uC-OS3/Source/os_core.c uC-OS3/Source/os_flag.c uC-OS3/Source/os_mutex.c uC-OS3/Source/os_q.c uC-OS3/Source/os_sem.c uC-OS3/Source/os_task.c uC-OS3/Source/os_time.c uC-OS3/Source/os_tick.c uC-OS3/Source/os_tmr.c uC-OS3/Source/os_pend_multi.c

配置文件 os_cfg.h 和 os_cfg_app.h 通常放在应用层目录,不要直接改官方原始文件,方便后续版本维护。os_cfg.h 控制内核功能裁剪,os_cfg_app.h 控制任务优先级、Tick 频率、栈大小等参数。

2.3 工程目录组织与 Keil 配置

推荐目录结构如下:

GD32_UCOS3_Demo/ ├── APP/ // main.c、用户任务 ├── BSP/ // bsp_led.c、bsp_uart.c、bsp.c ├── OS/ // uC-CPU、uC-LIB、uC-OS3 ├── FWLIB/ // GD32F10x 外设库 └── startup/ // 启动文件

Keil 工程里需要注意几个配置项:C/C++ 的 Include Path 要包含 APP、BSP、uC-CPU 及其 ARMCC 子目录、uC-LIB、uC-OS3/Source、FWLIB 等路径;Target 页面勾选 Use MicroLIB,这能显著减少 printf 相关的代码尺寸和栈开销;Linker 页面的堆栈建议设为 0x400 以上,虽然 UCOSIII 中任务栈是单独分配的,但启动文件里的主栈仍然会被中断和部分库函数使用。

3. 核心移植步骤与代码实现

3.1 启动文件与中断向量修改

这一步是整个移植的命门。GD32F10x 的启动文件 startup_gd32f10x.s 里,中断向量表默认是这样的:

SVC_Handler DCD SVC_Handler PendSV_Handler DCD PendSV_Handler SysTick_Handler DCD SysTick_Handler

需要改成:

SVC_Handler DCD OS_CPU_SVC_Handler PendSV_Handler DCD OS_CPU_PendSVHandler SysTick_Handler DCD OS_CPU_SysTickHandler

理由要讲清楚:UCOSIII 在调用 OSStart 时,是通过 SVC 指令触发一次软中断,进入 OS_CPU_SVC_Handler 来完成第一个任务的上下文装载;之后的上下文切换依靠 PendSV 完成;SysTick 则产生系统节拍,驱动时间片轮转。三个中断处理器在 os_cpu_a.asm 里其实都已经实现了,你只要让启动文件在中断发生时能跳到正确函数。

如果这里不改或者只改了部分,最典型的症状就是 OSStart 之后程序直接进入 HardFault,或者卡死在启动任务中,连第一个任务都不亮。我建议改完后,先反汇编看一眼启动文件里向量表地址有没有指向 OS_CPU_xxxHandler,再继续往下走。

3.2 时钟与 SysTick 初始化

GD32F103C8T6 常用 8MHz 外部晶振,通过 PLL 倍频到 72MHz。GD 标准外设库里的 system_clock_config 函数会帮我们配置好,SystemCoreClock 全局变量也会自动更新为 72000000。

接下来要初始化 UCOSIII 的系统节拍。官方移植层提供了 OS_CPU_SysTickInit,需要传入重装载值。假设系统主频 72MHz,_OS_CFG_TICK_RATE_HZ 配置为 1000Hz,那么 SysTick 每次计数 72000 个时钟周期产生一次中断,重装载值就是 71999。

#include "gd32f10x.h" #include "os.h" int main(void) { OS_ERR err; system_clock_config(); /* 72MHz 主频 */ OSInit(&err); /* 初始化 UCOSIII */ if (err != OS_ERR_NONE) { while (1); } OS_CPU_SysTickInit(SystemCoreClock / OS_CFG_TICK_RATE_HZ); /* 创建任务... */ OSStart(&err); /* 启动调度器 */ }

这里容易犯的错误是传入频率而不是重装载值。如果调用 OS_CPU_SysTickInit(72000000),SysTick 中断频率会变得异常高,系统一启动就频繁进中断,表现为任务调度完全混乱。注意看移植层源码中该函数的实现细节,确认参数含义。

3.3 内核配置裁剪与内存估算

C8T6 的 20KB SRAM 对 UCOSIII 来说比较紧张,所以配置裁剪非常关键。我的建议配置是:

/* os_cfg.h */ #define OS_CFG_APP_HOOKS_EN DEF_ENABLED #define OS_CFG_ISR_POST_DEFERRED_EN DEF_DISABLED #define OS_CFG_STAT_TASK_EN DEF_ENABLED #define OS_CFG_TASK_STK_LIMIT_EN DEF_ENABLED #define OS_CFG_TICK_EN DEF_ENABLED /* os_cfg_app.h */ #define OS_CFG_TICK_RATE_HZ 1000u #define OS_CFG_IDLE_TASK_STK_SIZE 128u #define OS_CFG_STAT_TASK_STK_SIZE 128u #define OS_CFG_ISR_STK_SIZE 256u

ISR_POST_DEFERRED_EN 一定要先关掉,这是把中断中的 post 操作延迟到任务里执行的机制,会额外引入定时器中断和时间戳管理,对新手调试很不友好。统计任务建议保留,它能直接观察 CPU 占用,对验证系统是否正常很有价值。

根据上面配置,我估算过内存占用:

项目估算值
UCOSIII 内核数据段约 1.5KB
空闲任务栈 128 x 4B0.5KB
统计任务栈 128 x 4B0.5KB
中断栈 256 x 4B1KB
两个用户任务栈约 1KB ~ 2KB
总计约 5KB 左右

这样算下来,还剩 15KB 左右给业务数据,LED 加串口的演示项目非常充裕。但如果你以后要开消息队列、信号量、互斥量,每个对象还要额外分配 RAM,务必先做好规划。

3.4 创建自己的任务

任务代码我写成了两个独立函数:LedTask 负责翻转 LED,UartTask 负责串口打印。注意 UCOSIII 任务函数必须是一个无限循环,且不能return。

static OS_TCB LedTaskTCB; static CPU_STK LedTaskStk[128]; static OS_TCB UartTaskTCB; static CPU_STK UartTaskStk[256]; static void LedTask(void *p_arg) { OS_ERR err; (void)p_arg; while (DEF_TRUE) { gpio_bit_write(GPIOA, GPIO_PIN_8, SET); OSTimeDlyHMSM(0, 0, 0, 500, OS_OPT_TIME_HMSM_STRICT, &err); gpio_bit_write(GPIOA, GPIO_PIN_8, RESET); OSTimeDlyHMSM(0, 0, 0, 500, OS_OPT_TIME_HMSM_STRICT, &err); } } static void UartTask(void *p_arg) { OS_ERR err; (void)p_arg; while (DEF_TRUE) { printf("UCOSIII on GD32F103C8T6 running...\r\n"); OSTimeDlyHMSM(0, 0, 2, 0, OS_OPT_TIME_HMSM_STRICT, &err); } }

任务栈大小的选择有讲究。LED 任务逻辑简单,128 个 CPU_STK(也就是 512 字节)足够。UartTask 里用了 printf,格式化输出会占用较大栈空间,我给了 256(1KB)。你如果 printf 里还带浮点,建议至少给到 512,否则很容易栈溢出。

main 里创建任务的顺序不影响最终运行结果,但建议把优先级较低且不依赖的任务放前面,高优先级任务放后面:

OSTaskCreate(&LedTaskTCB, "led task", LedTask, (void *)0, 4u, LedTaskStk, 128u, 0u, 0u, (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), &err); OSTaskCreate(&UartTaskTCB, "uart task", UartTask, (void *)0, 5u, UartTaskStk, 256u, 0u, 0u, (OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR), &err);

这里有一个细节:UCOSIII 里空闲任务的优先级是 OS_CFG_PRIO_MAX - 1,也就是默认 63,最高优先级是 0。普通用户任务优先级不要用 0,也不要用 63,否则会跟内核任务冲突。

4. 板级验证与内核功能实测

4.1 下载运行与观察

编译下载后,最直观的验证就是 LED。如果 LED 以 500ms 周期翻转,说明任务切换是正常的;串口每 2 秒打印一行,说明 UartTask 也活着。建议把调试器接上,在 LedTask 和 UartTask 的 while 循环里打断点,跑几步看是不是两个任务交错执行。

如果一运行就 HardFault,先在主循环里加一个延时或者直接暂停查看 PC 指针指向哪里。通常问题出在启动文件中断向量没改、SysTick 参数错误、或者任务栈上溢,这些内容我在第 5 节统一整理。

4.2 查看 CPU 利用率

打开统计任务后,UCOSIII 会在后台计算 CPU 利用率,结果放在全局变量 OSStatTaskCPUUsage 中,单位是百分比乘以 100。也就是说 OSStatTaskCPUUsage 的 0~10000 对应 0.00%~100.00%,方便你打印浮点而不增加太多开销。

if (OSRunning == OS_STATE_OS_RUNNING) { printf("CPU Usage: %d.%02d%%\r\n", OSStatTaskCPUUsage / 100, OSStatTaskCPUUsage % 100); }

统计任务的原理是:空闲任务每进入一次就做一次计数,通过对比空闲时间占比反推出 CPU 使用率。所以如果你的系统几乎无负载,CPU Usage 会接近 0%,符合预期。

4.3 用 OSTaskStkChk 检查任务栈余量

调试阶段我最喜欢用的函数是 OSTaskStkChk。它需要任务创建时带 OS_OPT_TASK_STK_CHK 选项,然后就能查询每个任务栈的实际使用情况:

CPU_STK_SIZE free_stk; CPU_STK_SIZE used_stk; OS_ERR err; OSTaskStkChk(&LedTaskTCB, &used_stk, &free_stk, &err); printf("LedTask stack: used=%u free=%u\r\n", used_stk, free_stk);

这个函数输出的单位是 CPU_STK 个数,不是字节。乘以 4 才是实际字节数。如果 free_stk 已经接近 0,说明任务栈分配过小,需要调大,否则后续运行可能由于栈越界产生随机崩溃。

5. 踩坑记录与 FAQ

5.1 一上电就 HardFault 的原因与排查

这是移植过程中最常遇到的问题,我把它列成了一张排查清单:

症状常见原因解决办法
OSStart 后直接 HardFault启动文件 SVC/PendSV/SysTick 向量没改检查向量表是否指向 OS_CPU_xxxHandler
SysTick 中断频率异常OS_CPU_SysTickInit 参数传成频率而非重装载值传 SystemCoreClock / OS_CFG_TICK_RATE_HZ
运行一段时间后随机崩溃某个任务栈溢出用 OSTaskStkChk 检查栈余量,调大栈
在中断里调用 OSTimeDly阻塞 API 不能在 ISR 中调用改用 OSTimeDly 之外的 ISR 安全 API,或把逻辑移到任务中

还有一个容易忽略的点是 PendSV 优先级。Cortex-M3 上 PendSV 和 SysTick 必须设置为最低优先级(数值最大),保证任务切换不会被其他中断抢占。UCOSIII 官方移植层在 OSStart 时一般会设置好,但如果你在启动代码里改了 NVIC 优先级分组,可能导致 PendSV 的优先级不是预期值。遇到诡异的调度问题,先检查这两项。

5.2 RAM 不够用的裁剪方案

20KB SRAM 的边界效应很明显。一旦你开了多个队列、事件标志组,或者给任务分配了比较大的栈,链接时可能直接报空间不足。我的建议是:

  • 不用的内核模块源码不要加进工程,比如如果不使用软件定时器,os_tmr.c 可以从工程中移除,能省一部分 Flash 和辅助 RAM;
  • 统计任务可以关闭,即把 OS_CFG_STAT_TASK_EN 改为 DEF_DISABLED,但这会失去 CPU 利用率读取能力;
  • 任务栈尽量按实际需求分配,通过 OSTaskStkChk 数据精准调整,而不是拍脑袋加大;
  • printf 尽量不用浮点格式,格式化浮点会吃掉大量栈空间。

实测下来,LED 加串口打印这种轻量任务,LED 任务栈 128、串口任务栈 256 都够。如果业务更复杂,我只建议把 C8T6 换成 GD32F103VCT6 这类大 RAM 芯片,没必要在 20KB 里省到寸步难行。

5.3 时间戳与 DWT 的问题

UCOSIII 的调试和时间统计功能依赖 CPU_TS 时间戳。官方移植层在部分例程里使用 DWT 的 CYCCNT 定时器来实现高精度计时。DWT 在 Cortex-M3 中默认是关闭的,如果系统使能了任务时间戳却没有初始化 DWT,会导致某些 API 卡住或者时间值异常。

如果你不需要高精度时间戳,最简单的做法是在 os_cfg.h 中关闭相关选项:

#define CPU_CFG_TS_TASK_EN DEF_DISABLED #define CPU_CFG_TS_TICKS_EN DEF_ENABLED

这样就只使用 SysTick 计数,不依赖 DWT。如果确实需要高精度计时,再在系统初始化时提前使能 DWT:

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0u; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

5.4 编译器版本与汇编文件兼容性

Keil MDK 从 5.36 之后默认编译器版本是 AC6,老版本的 os_cpu_a.asm 会出现一堆汇编语法错误。如果遇到这类问题,有两个方向:一是切换到 ARMCC5 编译器,在 Keil 的 Manage Project Items 里选择 Use default compiler version 5;二是把 os_cpu_a.asm 的语法改成 armclang 兼容格式。

改汇编文件比较繁琐,涉及 AREA、PRESERVE8、EXPORT 等伪指令的兼容性调整。我的经验是:如果项目不急,先用 ARMCC5 把系统跑通,后续再抽时间升级。如果你一定要用 AC6,网上也有适配过的新版 port 文件,可以搜 uC-OS3 armclang 或者直接在 GitHub 上找较新的 SDK。

5.5 串口重定向 printf 的细节

printf 重定向到串口在 GD32 外设库下可以用 fputc 实现,但要注意勾选 Keil 的 Use MicroLIB,否则 printf 会默认使用半主机模式,导致程序卡住。

int fputc(int ch, FILE *f) { usart_data_transmit(USART0, (uint8_t)ch); while (usart_flag_get(USART0, USART_FLAG_TBE) == RESET); return ch; }

另外,GD32 的 USART 发送完成标志在不同库版本中名字略有差异,建议参考你手里的标准外设库头文件确认。串口波特率可以在初始化时用 115200,我实测在 72MHz 主频下误差很小,完全满足调试需求。

最后再分享一点实际体会:移植 UCOSIII 到 GD32F103C8T6 这件事,最难的地方并不是操作系统本身,而是对底层启动流程和中断机制的理解是否到位。很多人习惯把 STM32 的工程直接搬过来,结果在时钟初始化、启动文件、库函数调用上反复踩坑。如果你也正在做类似的移植,建议按“启动文件 → 时钟 → SysTick → 任务创建 → 栈检查”的顺序逐项验证,每一步稳定了再走下一步。这套流程虽然慢,但出问题的时候能快速定位,反而比你试图一口吃成胖子走得更快。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询