
简介一份面向嵌入式开发者的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 内核 MCU64KB Flash、20KB SRAM标称主频 108MHz常规设计按 72MHz 使用。它和 STM32F103C8T6 在引脚上是兼容的很多板子可以互换价格和供货在近两年也更有优势因此不少产品设计直接把它当作替代方案来评估。但从做 RTOS 移植的角度看真正友好的地方在于Cortex-M3 内核自带 SysTick、SVC、PendSV 这三个触发机制UCOSIII 的任务切换正是依赖它们完成的。无论是 GD32 还是 STM32内核部分行为是一致的差异只集中在外设库、启动文件和部分寄存器映射上。这意味着只要你把基于 Cortex-M3 的 UCOSIII 移植层放对位置内核跑起来是没有任何障碍的。另外一个现实问题是资源。C8T6 只有 20KB RAMUCOSIII 内核本身、空闲任务、统计任务、中断栈都要占用内存。所以这块芯片能不能跑不取决于“能否移植”而取决于“能否把内存预算控制住”。做方案选型时需要把这一点想清楚如果业务逻辑复杂、队列和信号量开得多建议直接上 C8T6 的高配兄弟比如 GD32F103VCT6省得后面优化内存调到想哭。1.2 UCOSIII 内核特点与本次目标UCOSIIIuC/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-CPUCPU 抽象层包含#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 256uISR_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 负责翻转 LEDUartTask 负责串口打印。注意 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格式化输出会占用较大栈空间我给了 2561KB。你如果 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. 踩坑记录与 FAQ5.1 一上电就 HardFault 的原因与排查这是移植过程中最常遇到的问题我把它列成了一张排查清单症状常见原因解决办法OSStart 后直接 HardFault启动文件 SVC/PendSV/SysTick 向量没改检查向量表是否指向 OS_CPU_xxxHandlerSysTick 中断频率异常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。如果确实需要高精度计时再在系统初始化时提前使能 DWTCoreDebug-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 → 任务创建 → 栈检查”的顺序逐项验证每一步稳定了再走下一步。这套流程虽然慢但出问题的时候能快速定位反而比你试图一口吃成胖子走得更快。本文还有配套的精品资源点击获取