
FreeRTOS 在 GD32 上的移植很多人第一次做的时候都会卡在同一个地方工程编译通过了但任务调度器一启动就 HardFault或者串口一个字符都打印不出来。我前后在 GD32F303、GD32F407、GD32E230 这几颗片子上都跑过 FreeRTOS踩过的坑基本覆盖了从时钟配置到中断优先级再到堆栈分配的各个环节。这篇内容就把整套流程拆开讲清楚从建立裸机工程开始一直到多任务稳定运行每一步为什么这么做、哪里最容易出问题都会说明白。适合已经有 Keil 和一点单片机基础、准备把 FreeRTOS 落到 GD32 上的朋友也适合从 STM32 转过来、发现两者并不完全一样的人。1. 移植之前先把裸机工程跑通1.1 为什么不能直接拿 STM32 的工程改GD32 和 STM32 在引脚和大部分外设寄存器上确实高度兼容很多人就想着把 STM32 的 FreeRTOS 工程直接改个芯片型号就完事。我一开始也这么干过结果编译能过运行就是不稳定。原因在于两者虽然同属 Cortex-M 内核但 Flash 等待周期、时钟树结构、启动文件里的向量表定义都有差异。GD32 的 Flash 在更高主频下需要插入更多等待周期如果沿用 STM32 的配置高频下取指会出错表现出来就是随机的 HardFault。所以正确的做法是先不要碰 FreeRTOS用 GD32 官方的固件库或者 Embedded Builder 生成一个干净的裸机工程把时钟、串口、GPIO 都调通确认能正常打印、能点灯再往上加操作系统。这一步看起来慢实际上省掉了后面大量到底是系统问题还是底层问题的排查时间。1.2 建立标准裸机工程的关键配置用 Keil 建 GD32 工程核心是这几件事选对器件包GigaDevice 的 DFP、加对启动文件startup_gd32fxxx.s、配好系统时钟。以 GD32F303 跑 120MHz 为例外部晶振一般是 8MHz经过 PLL 倍频到 120MHz。这里有个容易忽略的点GD32 的 PLL 配置和 STM32 不完全一样不能照抄。下面是一个典型的时钟配置思路/* 使能外部高速晶振 */ RCU_CTL | RCU_CTL_HXTALEN; while(!(RCU_CTL RCU_CTL_HXTALSTB)); /* 配置 PLL8MHz 晶振预分频后倍频到 120MHz */ RCU_CFG0 ~(RCU_CFG0_PLLSEL | RCU_CFG0_PLLMF | RCU_CFG0_PREDV0); RCU_CFG0 | RCU_PLLSRC_HXTAL_IRC48M; RCU_CFG0 | RCU_PLL_MUL15; /* 8M * 15 120M */配置完之后一定要用示波器或者 MCO 引脚把系统时钟输出出来量一下确认真的是 120MHz而不是你以为的 120MHz。我遇到过 PLL 没锁定的情况程序照样往下跑但主频只有内部 RC 的 8MHz后面所有基于主频计算的延时全错。1.3 串口打印是移植过程中的眼睛在移植 FreeRTOS 的过程中串口打印几乎是唯一的调试手段。建议在裸机阶段就把串口重定向做好让printf能直接输出。GD32 的 USART 配置和 STM32 类似但要注意 RCU 里外设时钟的使能位名字不同。重定向的代码如下int fputc(int ch, FILE *f) { usart_data_transmit(USART0, (uint8_t)ch); while(RESET usart_flag_get(USART0, USART_FLAG_TBE)); return ch; }提示重定向之前确认在 Keil 的 Target 选项里勾选了 Use MicroLIB否则printf会依赖标准库体积大且可能跑不起来。裸机工程跑通的标志很简单LED 能按预期闪烁串口能稳定打印延时函数的时间是准的。这三条都满足再进入下一步。2. FreeRTOS 源码的取舍与工程接入2.1 到底需要拷贝哪些文件FreeRTOS 的源码包看起来文件很多但真正需要放进工程的核心就几类。我一般这样组织目录目录内容是否必需FreeRTOS/Sourcetasks.c、queue.c、list.c、timers.c 等必需FreeRTOS/Source/portable/RVDS/ARM_CM3port.c必需M3/M4 内核FreeRTOS/Source/portable/MemMangheap_4.c 等必需选一个FreeRTOS/Source/include所有头文件必需portable 目录下的编译器分支要选对。Keil 用的是 ARMCC对应 RVDS 目录如果你用 GCC 或者新版 Keil 的 AC6 编译器要换成 GCC 目录。这个选错了会直接报一堆汇编语法错误。内核分支方面GD32F303 是 Cortex-M3选 ARM_CM3GD32F407 是 M4选 ARM_CM4F带浮点单元的那个。2.2 heap 文件怎么选不是随便挑的FreeRTOS 提供了 heap_1 到 heap_5 五种内存管理方案很多人随手选 heap_4 就完事其实这里值得想一下。heap_1 最简单只能分配不能释放适合任务和队列在启动时就全部创建好、之后不再动态创建的场景代码量最小。heap_4 支持释放和碎片合并是最常用的。heap_5 支持多块不连续的内存区域适合内存分散的芯片。对于 GD32 这种 SRAM 不算特别大的片子我一般用 heap_4并且把configTOTAL_HEAP_SIZE设成实际需要再留 20% 余量。设太大浪费 SRAM设太小创建任务时直接返回失败。以 GD32F303 的 48KB SRAM 为例如果跑三四个任务加两个队列configTOTAL_HEAP_SIZE设 10KB 到 12KB 比较稳妥。2.3 FreeRTOSConfig.h 是移植的灵魂这个头文件决定了整个系统的行为是移植里最需要逐项确认的地方。几个关键配置#define configUSE_PREEMPTION 1 #define configCPU_CLOCK_HZ (120000000UL) #define configTICK_RATE_HZ ((TickType_t)1000) #define configMAX_PRIORITIES (32) #define configMINIMAL_STACK_SIZE ((unsigned short)128) #define configTOTAL_HEAP_SIZE ((size_t)(12 * 1024)) #define configMAX_TASK_NAME_LEN (16) #define configUSE_16_BIT_TICKS 0configCPU_CLOCK_HZ必须和实际系统主频一致这个值错了vTaskDelay的延时就会整体偏掉。configTICK_RATE_HZ设 1000 表示 1ms 一个 tick这是最常用的值。configUSE_16_BIT_TICKS设 0 表示用 32 位 tick 计数对于长时间运行的系统必须这样设否则 tick 会很快溢出。还有几个和中断相关的宏是后面中断优先级配置的基础这里先列出来#define configKERNEL_INTERRUPT_PRIORITY (15 4) #define configMAX_SYSCALL_INTERRUPT_PRIORITY (5 4)这两个值后面会详细解释为什么这么设。3. 中断优先级移植失败的头号元凶3.1 Cortex-M 中断优先级的反直觉设计这是我在 GD32 上移植 FreeRTOS 时踩得最狠的一个坑也是绝大多数人第一次移植失败的根本原因。Cortex-M 的中断优先级有个反直觉的地方数值越小优先级越高。0 是最高优先级15对于 4 位优先级是最低。FreeRTOS 需要一个系统可管理的中断优先级范围。高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断也就是数值更小的不受 FreeRTOS 的临界区保护不能调用任何 FreeRTOS 的 API低于它的中断才能安全调用FromISR结尾的 API。这个机制是为了保证内核临界区的原子性。3.2 GD32 上优先级分组必须配对GD32 和 STM32 一样用 NVIC 的优先级分组来划分抢占优先级和子优先级。FreeRTOS 要求所有优先级位都用作抢占优先级也就是分组要设成 NVIC_PRIORITY_GROUP_44 位全给抢占。如果分组设错了比如设成了 2 位抢占 2 位子优先级那么你算出来的优先级值和 FreeRTOS 的预期就对不上临界区保护会失效表现出来就是偶发的数据错乱或者死机。/* 必须在 FreeRTOS 启动前设置且全工程只设一次 */ nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0);设置好分组之后任何要调用 FreeRTOS API 的中断其优先级数值必须大于等于 5也就是优先级要低于或等于configMAX_SYSCALL_INTERRUPT_PRIORITY。比如串口接收中断如果要在里面发信号量就得设成 5 到 15 之间。3.3 一个真实的 HardFault 排查过程我印象很深的一次串口中断里调用了xQueueSendFromISR程序跑几分钟就 HardFault 一次。排查过程是这样的先怀疑堆栈溢出把任务堆栈加大没用再怀疑队列满了加了判断还是崩。最后用调试器在 HardFault 处理函数里看寄存器发现出错时正好在中断上下文里操作队列。问题就出在串口中断的优先级设成了 4比configMAX_SYSCALL_INTERRUPT_PRIORITY5还高。这个优先级的中断不受内核临界区保护在里面操作队列恰好撞上内核正在改队列链表链表指针就乱了。把串口中断优先级改成 6 之后问题彻底消失。注意判断一个中断能不能调用 FreeRTOS API看它的优先级数值是否大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY右移 4 位后的值。这个换算很容易搞错建议直接写个宏来算。4. 启动流程与第一个任务的落地4.1 从 main 到调度器启动发生了什么裸机程序是main里一个 while 循环跑到底而 FreeRTOS 的main只负责初始化硬件、创建任务最后调用vTaskStartScheduler()把控制权交给调度器。这个函数一调用就不会返回除非堆内存不够导致创建空闲任务失败。int main(void) { /* 硬件初始化 */ system_clock_config(); usart_config(); nvic_priority_group_set(NVIC_PRIGROUP_PRE4_SUB0); /* 创建任务 */ xTaskCreate(led_task, LED, 128, NULL, 2, NULL); xTaskCreate(uart_task, UART, 256, NULL, 3, NULL); /* 启动调度器正常情况下不会返回 */ vTaskStartScheduler(); /* 只有内存不足导致调度器启动失败才会走到这里 */ while(1); }调度器启动时会自动创建空闲任务优先级 0和可选的定时器任务。空闲任务的堆栈由configMINIMAL_STACK_SIZE决定这个值不能设太小否则空闲任务里跑钩子函数会溢出。4.2 任务堆栈大小到底怎么估任务堆栈大小是新手最容易设错的地方。xTaskCreate里的堆栈参数单位是字word不是字节。在 32 位 MCU 上128 表示 128 个字也就是 512 字节。很多人以为 128 是字节结果栈严重不足。估算方法先给一个偏大的值比如 256 字跑起来之后用uxTaskGetStackHighWaterMark()查剩余堆栈。这个函数返回任务运行过程中堆栈剩余的最小值单位还是字。如果返回值很小比如小于 20说明栈快满了要加大如果剩余很多可以适当减小。UBaseType_t watermark uxTaskGetStackHighWaterMark(NULL); printf(stack free: %u words\r\n, watermark);我一般会在调试阶段定期打印这个值等系统稳定运行一段时间后根据实际余量把堆栈调到合理大小。注意printf本身很吃栈如果任务里用了printf堆栈要给足否则打印的时候就会溢出。4.3 第一个任务跑起来后的验证清单任务能跑起来不代表移植就成功了要逐项验证多个不同优先级的任务能否按预期抢占vTaskDelay的延时是否准确用示波器量 LED 翻转周期队列、信号量能否正常收发中断里调用FromISRAPI 是否稳定长时间运行几小时是否会出现 HardFault这几项都过了才算真正移植成功。尤其是最后一项很多隐藏的优先级或堆栈问题要跑一段时间才会暴露。5. 那些文档里不会写的实战细节5.1 系统节拍中断的归属问题FreeRTOS 需要一个周期性的节拍中断来驱动调度默认用的是 SysTick。但 GD32 的固件库里SysTick_Handler可能已经被库函数占用了比如做延时用。如果两边都用了 SysTick就会冲突。解决办法有两个一是把 FreeRTOS 的节拍源改成其他定时器比如 TIMER2在FreeRTOSConfig.h里定义configTICK_SOURCE相关宏二是把库里的 SysTick 延时改成用其他方式实现。我一般选第一种用一个独立的定时器做节拍避免和库函数抢 SysTick。改的时候要注意定时器的中断优先级也要设成最低15因为节拍中断会调用内核 API。5.2 临界区保护和 Flash 等待的配合GD32 在高主频下 Flash 需要插入等待周期这个在裸机阶段就配好了。但要注意FreeRTOS 的临界区会关中断关中断期间如果执行 Flash 里的代码取指延迟是固定的不会出问题。真正要注意的是临界区里不要做耗时操作否则会拉长中断响应延迟影响系统实时性。5.3 从 STM32 工程迁移时的隐藏差异如果你手上有一个 STM32 的 FreeRTOS 工程想迁到 GD32除了前面说的时钟和启动文件还有几个地方要改中断向量表的名字GD32 的中断处理函数名和 STM32 不完全一样、外设寄存器的位定义大部分兼容但有个别差异、以及 Flash 编程相关的操作。我建议不要直接改而是用 GD32 的库重新建工程把应用层的任务代码搬过去这样最干净。5.4 调试 FreeRTOS 时的几个实用技巧在 Keil 里调试 FreeRTOS可以装 FreeRTOS 的调试插件能在 Watch 窗口里直接看到所有任务的状态、优先级、堆栈使用情况非常直观。另外把configASSERT打开在参数错误时直接断下来比事后排查高效得多。configASSERT的定义可以指向一个死循环加打印方便定位。#define configASSERT(x) if((x) 0) { printf(assert failed\r\n); while(1); }还有一点HardFault 处理函数里可以把出错时的堆栈内容打印出来配合__asm读取 LR 和 PC能快速定位是哪条指令出的错。这个技巧在排查内存越界和空指针时特别有用。6. 让系统真正稳定运行的收尾工作移植完成、任务能跑只是第一步。要让系统在真实产品里稳定运行还有几件事要做。首先是堆栈溢出检测把configCHECK_FOR_STACK_OVERFLOW设成 2并实现vApplicationStackOverflowHook一旦某个任务栈溢出就能立刻发现。其次是空闲任务钩子可以在里面做低功耗处理或者喂狗。最后是内存分配失败的钩子vApplicationMallocFailedHook动态创建对象失败时能及时报警。void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { printf(stack overflow: %s\r\n, pcTaskName); while(1); }我个人在实际项目里的体会是FreeRTOS 在 GD32 上的移植难点从来不在能不能编译过而在跑起来之后稳不稳。时钟配置、中断优先级、堆栈大小这三样任何一样没弄对系统都会在某个不确定的时刻出问题。把这三样在移植阶段就彻底搞清楚后面开发应用逻辑会省心很多。另外养成用uxTaskGetStackHighWaterMark定期检查堆栈的习惯很多偶发死机都能提前发现。