APM32F103C8T6标准库工程移植RT-Thread nano实战指南 最近在交流群里看到两类问题出现频率很高一类是从 STM32F103C8T6 转过来看 APM32F103C8T6 的开发者想知道标准库能不能继续用另一类是刚刚点完灯、跑完串口的新手想往实时操作系统方向走一步却在“如何把 RT-Thread nano 加到标准库工程”这里卡住了。这篇文章就围绕 APM32F103C8T6 标准库开发展开从建工程开始一步步把 RT-Thread nano 移植进来最后跑通多个线程任务。全文不涉及晦涩难懂的源码分析适合刚掌握 GPIO、串口、中断想真正上手 RTOS 的开发者。如果你手里是 STM32F103C8T6也可以参考同样的思路因为这两颗芯片在引脚、内存和大量外设寄存器上都高度相似。1. APM32F103C8T6 是一颗什么样的芯片1.1 与 STM32F103C8T6 的兼容关系APM32F103C8T6 是国产芯片厂商极海半导体推出的 Cortex-M3 内核 MCU常见配置为 LQFP48 封装、64KB Flash、20KB SRAM最高主频 72MHz。无论从封装、存储容量、命名规则还是引脚定义来看它都瞄准了 STM32F103C8T6 这一经典型号。这意味着什么呢对学习阶段来说最大的好处是网上海量的 STM32F103 裸机教程、标准库示例、外设驱动代码大多数都可以对照移植到 APM32F103C8T6 上。很多从 STM32 转过来的工程师第一感觉就是“工程结构几乎不用大改”。需要提醒的是兼容不代表完全一致。不同厂商的库函数命名、启动文件、时钟配置细节可能会略有区别特别是某些寄存器的别名和库函数前缀。真正动手时以你下载到的 APM32 SDK 实际内容为准这一点在后续代码示例中我会反复强调。1.2 为什么用标准库而不用寄存器或 HAL很多新手在选开发方式时会纠结直接操作寄存器、标准外设库、HAL 库到底学哪个三者的定位并不相同开发方式优点缺点寄存器开发最底层能看清芯片工作细节代码量大可读性差开发速度慢标准外设库兼顾可读性与执行效率资料最丰富不同厂商需要适配库函数命名HAL 库配合图形配置工具生成代码上手快封装层级多编译体积大细节被隐藏对 APM32F103C8T6 这类 Cortex-M3 入门芯片我比较推荐标准库。原因是市面上的经典教程、竞赛代码、老项目大多采用标准库写法当你把外设初始化流程看懂之后再看寄存器或者 HAL 都会轻松很多。1.3 裸机开发到 RTOS 的跳跃点在哪里裸机程序通常是一个while(1)大循环配合定时器中断和标志位来实现多任务效果。代码少的时候很好用但任务一旦增多问题就来了多个功能模块共享一个循环相互之间容易产生时序耦合某个任务执行时间稍长其他任务就会被“饿死”中断里不适合做耗时操作只能置标志位但标志位的处理时机难以精确控制后续想增加优先级、延时、超时、任务通信时自己维护状态机越来越吃力。实时操作系统的作用就是由内核统一管理线程调度、延时、同步和资源竞争。你只需要把业务拆成若干线程每个线程看起来就像“独占 CPU”一样顺序执行内核会按照优先级和时间片来决定谁运行、谁睡眠。RT-Thread nano 是 RT-Thread 的精简版本非常适合 APM32F103C8T6 这种 20KB SRAM 的 MCU。它保留了线程调度、信号量、互斥量、消息队列、定时器等常用组件却不带完整版 RT-Thread 的设备驱动框架和 POSIX 层资源占用很克制。2. 开发环境准备与标准库工程基础2.1 需要准备的工具本文的示例以 Keil MDK 为主因为大多数 Cortex-M3 入门资料默认使用 Keil。你需要准备好硬件APM32F103C8T6 核心板或最小系统板调试器ST-Link、J-Link、DAP-Link 均可IDEKeil MDK 5.x并确保已安装对应 ARM CompilerAPM32 官方 SDK里面包含标准外设库、启动文件和模板工程串口调试助手用于观察 RTOS 线程打印信息USB 转 TTL 模块如果板载串口不能直接连电脑需要外接。正式建工程之前建议先到极海官网或可信渠道下载对应型号的 SDK。很多初学者喜欢从第三方博客下载“聚合版库”这样容易把版本搞混也会引入未知的安全风险。标准库不属于“过时技术”在工业项目里仍有大量存量代码关键是找到正确的原始来源。2.2 APM32F103C8T6 标准库工程结构建议不同 SDK 提供的模板略有差异但核心目录基本是这样的Project/ │ ├── Libraries/ │ ├── APM32F10x_StdPeriph_Driver/ // 标准外设库源码 │ ├── CMSIS/ │ └── Startup/ │ ├── User/ │ ├── main.c │ ├── apm32f10x_it.c │ └── delay.c / usart.c / led.c // 自己封装的驱动文件 │ └── MDK-ARM/ └── Project.uvprojx很多人卡在“新建标准库工程”这一步本质原因是没有分清库文件路径、启动文件和头文件包含路径。最稳妥的做法是打开 SDK 自带的 Keil 模板工程先确认能编译、能下载再在此基础上添加自己的代码。不要一上来就尝试从零搭建一个完全空白的工程。如果你的 SDK 模板中 LED、串口等例程能够直接运行说明环境基本没问题。下面我们以点灯为例快速验证外设库的使用。2.3 点灯验证GPIO 标准库代码到底怎么改下面这段代码是典型的 STM32F1 标准库风格。APM32 的 API 整体上非常接近如果你发现RCC_APB2PeriphClockCmd或GPIO_Init在当前 SDK 中找不到请打开对应头文件确认新函数名常见差异是时钟外设名或枚举名不同// 文件路径User/bsp_led.c #include apm32f10x.h void LED_GPIO_Config(void) { GPIO_InitTypeDef gpioInitStruct; /* 开启 GPIOB 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); /* 配置 PB0 为推挽输出 */ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); } void LED_On(void) { GPIO_SetBits(GPIOB, GPIO_Pin_0); } void LED_Off(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); } void LED_Toggle(void) { if (GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0) Bit_SET) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); } else { GPIO_SetBits(GPIOB, GPIO_Pin_0); } }对刚入门的朋友我建议不要直接复制编译而是手动敲一遍并逐步理解四步打开外设时钟任何外设使用前首先要通过时钟控制模块打开对应 GPIO 端口时钟声明并填充初始化结构体标准库通过结构体把引脚号、模式、速度一次性写进寄存器调用初始化函数后续通过置位、清零函数控制电平。如果这一部分能顺利完成说明你已经掌握了 APM32F103C8T6 标准库开发的基本路径。接下来做 RT-Thread nano 移植时LED 和串口会作为调试工具反复使用。2.4 串口输出后续观察 RTOS 运行的重要窗口裸机开发中串口主要用于打印调试信息引入 RTOS 后串口是观察线程是否被正确调度的重要工具。建议在进入移植前先把串口 1 配置好并完成 printf 重定向。// 文件路径User/uart1.c #include apm32f10x.h void USART1_Config(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); /* PA9 复用推挽输出TX */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* PA10 浮空输入RX */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate baudrate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_RX | USART_Mode_TX; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); }然后在任意源文件中添加 printf 重定向函数#include stdio.h int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }这段代码的核心原理是C 库中的printf最终会调用fputc来输出单个字符我们把这个底层函数重定向到串口就能在串口助手里看到格式化打印结果。常见坑点是 Keil 中需要勾选 MicroLIB否则可能因为半主机模式导致程序卡死。3. RT-Thread nano 的核心概念与源码组成3.1 实时操作系统到底解决了什么问题实时操作系统简称 RTOS并不是“运行速度更快的操作系统”。它的核心价值是提供“可预测的调度”系统能保证高优先级线程在满足条件后的确定时间内得到运行。为了实现这一点内核需要完成上下文切换、定时管理、任务同步等基础工作。从代码结构上看裸机程序是自己管理while(1)RTOS 程序则是把业务拆成多个入口函数让内核决定谁先执行、谁后执行。例如static void led_thread_entry(void *parameter) { while (1) { LED_On(); rt_thread_mdelay(500); LED_Off(); rt_thread_mdelay(500); } }看起来只是一个普通的死循环函数但在 RTOS 中它被内核视为一个独立线程。当该线程调用rt_thread_mdelay(500)时内核会让出 CPU让其他线程运行而不是像裸机 delay 那样死等。3.2 RT-Thread nano 与完整版 RT-Thread 的差异很多人第一次听说 RT-Thread 时会以为它是一个很庞大的系统。实际上 RT-Thread 提供了不同规模的版本RT-Thread 完整版包含设备驱动框架、组件层、POSIX 接口、在线包管理器功能丰富适合功能复杂的项目RT-Thread nano精简版主要提供内核核心能力包括线程管理、调度、通信、定时器和内存管理适合 Flash/RAM 受限的 MCU。对 APM32F103C8T6 这样的入门级芯片来说nano 是更合适的选择。它代码量小源码容易读懂尤其适合用来学习“操作系统到底是怎么切换线程的”。3.3 需要先理解的几个关键术语移植之前建议先建立下面几个概念术语含义线程操作系统调度和执行的基本单位每个线程有自己的入口函数和独立栈空间优先级RT-Thread 中数值越小优先级越高高优先级线程会抢占低优先级线程tick操作系统的心跳通常由 SysTick 定时器产生线程延时的时间单位就是 tick空闲线程系统中没有其他线程就绪时运行的特殊线程负责回收资源信号量用于线程与线程之间、中断与线程之间的同步通知机制这几个概念不要求一开始全懂等看到多线程示例后回头再看会有更直观的理解。3.4 RT-Thread nano 源码中必须知道的目录从官网下载 RT-Thread nano 源码后通常可以看到以下目录rt-thread/ ├── include/ // RT-Thread 内核头文件 ├── src/ // 内核源码调度器、线程、IPC、定时器等 ├── libcpu/ │ └── arm/cortex-m3/ │ ├── context_gcc.S │ ├── context_rvds.S // Keil MDK 常用 │ └── context_iar.S └── tools/其中src下最常见的文件包括scheduler.c任务调度核心thread.c线程创建、删除、启动管理timer.c软件定时器与 tick 管理ipc.c信号量、互斥量、消息队列等同步机制mem.c 与 mempool.c内存管理idle.c空闲线程。在手动移植时需要把这些源码文件和 cortex-m3 下的汇编文件加入工程并保证头文件路径正确。不同版本源码文件数量可能有细微差异但目录结构基本一致。4. 将 RT-Thread nano 移植到标准库工程4.1 移植前要先想清楚的思路从零手动移植 RT-Thread nano 是理解 RTOS 的最好方式。这里介绍的不是使用 Keil 软件包自动生成的“傻瓜式方案”而是把 RT-Thread 源码作为第三方组件加入标准库工程的手工方案。整条链路可以概括为把 RT-Thread 内核源码加到 Keil 工程中编写rtconfig.h告诉内核当前芯片配置提供系统时钟初始化与堆内存地址对接 SysTick、PendSV 等关键中断创建线程并启动调度器。因为 APM32F103C8T6 的 SRAM 只有 20KB所以 rtconfig.h 中的线程数量、栈大小参数要谨慎设置不要盲目复制给大芯片使用的配置。4.2 新建 RTOS 子目录并添加源码在标准库工程目录下新建一个RT-Thread文件夹并放入源码RT-Thread/ ├── include/ ├── src/ └── libcpu/ └── arm/cortex-m3/在 Keil 中打开 Manage Project Items添加几组 Source Group例如RT-Thread/src把src目录下的所有.c文件加入RT-Thread/cpu把libcpu/arm/cortex-m3下适合 MDK 的汇编文件加入。汇编文件的选择要特别注意。Keil MDK 使用 ARMCC 或 armclang 编译器时通常选择context_rvds.S。如果编译器不识别.S后缀可以在 Keil 中把文件类型改为 Assembler。4.3 编写 rtconfig.hrtconfig.h 是 RT-Thread 的配置文件。nano 版本并不需要你手动编写每一项配置官方源码或模板工程中会提供一份默认配置。入门阶段至少需要关注几项// 文件路径RT-Thread/rtconfig.h #ifndef RT_CONFIG_H__ #define RT_CONFIG_H__ /* 内核基础配置 */ #define RT_THREAD_PRIORITY_MAX 32 // 最大优先级数 #define RT_TICK_PER_SECOND 1000 // 每秒 tick 数 #define RT_ALIGN_SIZE 8 /* 内存管理 */ #define RT_USING_HEAP #define RT_HEAP_SIZE 4096 // 内核堆大小可根据需要调整 /* 内核功能裁剪 */ #define RT_USING_TIMER_SOFT #define RT_USING_CONSOLE #define RT_CONSOLEBUF_SIZE 128 #define RT_USING_COMPONENTS_INIT #define RT_USING_USER_MAIN #define RT_MAIN_THREAD_STACK_SIZE 1024 /* 调试开关 */ // #define RT_DEBUG #endif /* RT_CONFIG_H__ */这里有一个容易忽略的点如果工程中没有开启RT_USING_HEAP那么像rt_thread_create这种需要动态分配内存的函数可能无法使用或者只能改用静态创建方式。开启后还需要在 board 初始化中为内核提供堆区起始地址。4.4 实现 board.c 中的系统初始化RT-Thread 的移植关键文件是 board.c 和 board.h。不同版本对 board.c 的要求略有差异但核心工作始终是两件事初始化时钟、提供堆空间起始地址。参考思路如下// 文件路径User/bsp_rtthread_board.c #include rtthread.h #include apm32f10x.h /* 使用链接脚本中的 __bss_end 作为堆起始地址更精确 入门阶段也可以直接用数组方式预留一块内存给内核堆。 */ #define RT_HEAP_BEGIN (void*)0x20000000 // 示例写法