☰
APM32F103C8T6移植RT-Thread nano:从标准库到多任务入门
2026/10/11 17:02:15 网站建设 项目流程

APM32F103C8T6 现在做入门首选,已经和 STM32F103C8T6 成了国内开发者的“默认选项”之一。这颗芯片内核是 Arm Cortex-M3,主频能做到 96MHz,Flash 64KB、SRAM 20KB,管脚和大部分外设寄存器都跟 STM32F103C8T6 对应。很多工程师直接拿着 STM32 的标准外设库代码往 APM32 上烧,跑起来没问题,原因是两者软件兼容性做得非常到位。

如果你刚开始接触嵌入式实时操作系统,又不想一上来就啃复杂的总线和内核文档,那“标准库开发 APM32F103C8T6 + 移植 RT-Thread nano”是一条非常合适的路线。RT-Thread nano 是 RT-Thread 的精简版,保持了实时内核、信号量、互斥量、消息队列、定时器等常用组件,裁剪后对 Flash 和 RAM 的需求很低,在 APM32F103C8T6 这种 64KB Flash、20KB RAM 的 MCU 上可以留出大量资源做业务逻辑。这篇文章会带你梳理完整的开发链路:环境准备、标准库工程模板、GPIO 和串口操作、RT-Thread nano 移植步骤、任务创建、通信机制测试、资源占用分析、常见故障排查。文章后面还针对 APM32 与 STM32 兼容、标准库和 RT-Thread nano 的边界问题做了说明,操作过程中遇到的坑也一并整理。

1. 核心能力速览

能力项说明
主控型号APM32F103C8T6,Arm Cortex-M3,主频常见配置为 72MHz/96MHz
片上资源Flash 64KB,SRAM 20KB,管脚 LQFP48
软件方式标准外设库开发,兼容 STM32F10x 标准外设库的大部分 API
操作系统RT-Thread nano,精简版 RTOS,支持线程、信号量、互斥量、消息队列、软定时器
推荐开发环境Keil MDK,也可以使用 IAR / GCC 工具链
调试方式ST-Link、J-Link、DAP-Link,SWD 接口即可
典型占用RT-Thread nano 裁剪后常规在几 KB Flash 和 1~2 KB RAM 量级,实际以工程配置和编译器优化为准
启动方式通过工程编译后下载到 Flash,上电自动运行
是否支持批量任务支持,RTOS 多线程、定时器、信号量、消息队列可支撑多种周期任务
是否支持接口扩展支持串口、SPI、I2C、ADC、PWM 等外设接口,实际取决于外设初始化

从材料看,这颗芯片的定位非常清晰:给低成本、大批量、对实时性有要求的嵌入式产品提供稳定底座。和直接操作寄存器相比,标准库开发把外设初始化封装好了,阅读和后期维护都更舒服;和裸机主循环相比,引入 RT-Thread nano 之后,多个任务之间的调度由内核来管理,代码结构更接近“每件事一个线程”,而不是在 while(1) 里面手工叠加一堆状态标志。

2. APM32F103C8T6 与 RT-Thread nano 的选型逻辑

APM32F103C8T6 的核心优势在于“兼容”和“供应链自主性”。对于原先基于 STM32F103C8T6 做的产品,只要外设映射和代码规范,替换 APM32 后不需要重新设计 PCB,软件工作量也比较可控。对于正在学习单片机的新手来说,这颗芯片的另一个优点是资料充足,标准库例程、RT-Thread nano 教程、传感器驱动都可以参考 STM32F103 生态,不需要从零摸索寄存器。

RT-Thread nano 解决的不是“能不能跑得更快”,而是“代码复杂度高了以后怎么保持清晰”。

裸机开发时,一个常见问题是延时和轮询逻辑搅在一起。传感器读取需要等待、按键消抖需要延时、LCD 刷新需要时间,一旦逻辑变多,主循环就会越来越臃肿。RT-Thread nano 引入线程概念之后,每个功能模块可以独立成一个线程,内核根据优先级和时间片调度,阻塞住的线程不会拖垮其他线程。举例来说,串口接收等待数据时可以调用等待接口让出 CPU,按键扫描线程则继续运行,整体代码更接近业务逻辑本身。

RT-Thread nano 和完整版 RT-Thread 的区别也要明确。完整版带设备驱动框架、FinSH 控制台、软件包管理,资源开销更大。nano 版本只保留了 RTOS 最核心的调度和同步机制,非常适合 Flash 64KB、SRAM 20KB 这一级别 MCU。项目里如果只是需要多线程调度、定时任务、信号量同步、消息传递,nano 完全够用。如果后续要接文件系统、网络协议栈这类重量级组件,再考虑评估完整版 RT-Thread 或者直接换资源更大的芯片。

3. 适用场景与使用边界

这套方案的适合人群很明确:

  1. 刚接触 Cortex-M3 和 RTOS,需要一个软硬件都比较友好的平台。
  2. 产品曾基于 STM32F103C8T6 设计,想评估或者切换到 APM32F103C8T6。
  3. 项目中需要多个周期任务协同,比如按键扫描、传感器周期读取、LCD 刷新、串口日志输出。
  4. 希望通过标准库熟悉 GPIO、USART、SPI、I2C、ADC 等外设,又不想在寄存器配置上花费太多时间。

不适合的场景也要说清楚:

  • 需要复杂 GUI、文件系统、USB Host、网络协议栈时,APM32F103C8T6 的 64KB Flash 和 20KB SRAM 比较紧张,RT-Thread nano 也不是为这些重量级组件设计的。
  • 对功耗控制要求极高的场景,需要深入处理睡眠模式和唤醒源,裸机中断驱动的功耗模型可能比 RTOS 更好优化。
  • 需要硬实时、微秒级确定性响应的场景,RTOS 调度本身有 tick 周期延迟,必须重新评估任务优先级和中断处理方式。

使用边界上,涉及外设访问时要避免多个线程同时直接操作同一个外设而不加保护,例如两个线程同时调用 printf 重定向到串口,可能造成日志交错。标准做法是把同类型资源放到独立线程中访问,或者使用互斥量保护共享外设。涉及产品量产时,固件中的库函数、RT-Thread 软件包、调试器固件都要满足对应许可证要求,代码来源要保留清晰记录。涉及第三方 IP、专利模块或行业认证,应做对应合规评估,不能直接拿个人学习方案套用到量产医疗、工业安全等场景。

4. 环境准备与前置条件

4.1 硬件准备

准备一块 APM32F103C8T6 最小系统板即可,常见蓝色或者黑色核心板都可以。板载引出了 SWD 调试口、电源、串口等。需要额外准备一个 ST-Link、J-Link 或者 DAP-Link 调试器,通过杜邦线连接 SWDIO、SWCLK、GND,部分板子自带 USB 转串口芯片,下载程序和查看日志都会方便很多。

连接示意:

调试器APM32F103C8T6 目标板
SWDIOPA13
SWCLKPA14
GNDGND
3.3V(可选)3.3V

如果是 DAP-Link,通常还提供虚拟串口,可以把板子的 USART1_TX (PA9)、USART1_RX (PA10) 接到调试器的 TX/RX,实现串口日志查看。

4.2 软件准备

  • Keil MDK 5.x,芯片支持包需要覆盖 APM32F103 系列。极海官网会提供 APM32F10x 系列的器件支持包,也可以使用 Keil 官方包描述文件进入 Pack Installer 安装。
  • APM32F10x 标准外设库或者与 STM32F103 标准外设库兼容的固件包。极海官方 SDK 中提供了标准库例程、启动文件和链接脚本。
  • RT-Thread nano 源码包。可以从 RT-Thread 官方仓库的 nano 目录获取,也可以通过 RT-Thread Studio 创建 nano 工程后导出源码。
  • 串口工具,如 MobaXterm、PuTTY、SecureCRT 或者开源串口助手。波特率常见设置为 115200 或 9600,后续例程使用 115200。

环境检查建议使用如下命令确认调试器可以被识别。这里以 Windows 下常见的CMSIS-DAP设备为例,不同调试器设备名不同:

# Windows 下查看调试器是否被系统识别 Get-PnpDevice -PresentOnly | Where-Object { $_.FriendlyName -match "CMSIS|STLink|JLink" }

不管在哪个平台,能识别到调试器后才能继续下一步烧录。如果使用 GCC 工具链,还需要安装arm-none-eabi-gcc并配置好环境变量:

arm-none-eabi-gcc --version

4.3 建立工程时容易漏掉的文件

标准库工程的核心文件可以分为四部分:

  1. 启动文件,例如startup_apm32f10x_md.s,负责设置初始栈指针、向量表、复位处理。
  2. 内核和系统配置文件,包括核心寄存器定义头文件和system_apm32f10x.c。
  3. 标准外设库源文件,例如 GPIO、USART、SPI、I2C、ADC 等外设的.c/.h文件。
  4. 用户代码,包括main.c、中断处理文件、应用层模块。

RT-Thread nano 目录通常包含:

  • src/:线程调度、时钟、对象管理等内核源码。
  • libcpu/arm/cortex-m3/:Cortex-M3 上下文切换和调度汇编代码。
  • board.c:板级初始化、系统时钟、SysTick 和rt_tick_increase调用入口。
  • rtconfig.h:内核裁剪配置,决定是否启用信号量、互斥量、消息队列、动态内存等。
  • components.c:空函数桩,占位一些 RTOS 组件接口。

工程中加入源码时,要特别注意“宏定义”和“头文件搜索路径”的完整,否则编译后会出现大量未声明标识符的错误。推荐先编写一个最小点灯工程,编译下载确认无误,再逐步加入 RT-Thread nano 源码。

5. 标准库工程模板搭建与点灯测试

5.1 创建 Keil 工程

这里以常见的裸机标准库工程为例,给出一份可复制的目录结构,实际工程按使用的 SDK 调整:

apm32f103c8t6_project/ ├── Core/ │ └── Src/ │ └── main.c ├── Periph/ │ ├── Include/ │ └── Source/ ├── Startup/ ├── RTOS/ │ ├── src/ │ └── libcpu/ └── MDK-ARM/

在 Keil MDK 中新建工程时,芯片型号选择 APM32F103C8T6,如果 Pack 安装正确,可以直接从设备列表选到。之后按需添加标准外设库源文件。为了减少编译时间和出错概率,第一版工程只保留用到的外设模块,例如 GPIO、USART、RCC。

5.2 标准库点亮板载 LED

板载 LED 常见接在 PC13 或 PB2,不同板子不一样,可以看原理图。下面以 PC13 输出高电平点亮为例,实际引脚根据原理图替换。

#include "apm32f10x.h" #include "apm32f10x_gpio.h" #include "apm32f10x_rcc.h" void LED_GPIO_Config(void) { GPIO_Config_T gpioConfig; RCC_EnableAPB2Periph(RCC_APB2_PERIPH_GPIOC); gpioConfig.mode = GPIO_MODE_OUT_PP; gpioConfig.pin = GPIO_PIN_13; gpioConfig.speed = GPIO_SPEED_50MHZ; GPIO_Config(GPIOC, &gpioConfig); } int main(void) { LED_GPIO_Config(); while (1) { GPIO_SetBits(GPIOC, GPIO_PIN_13); volatile uint32_t delay = 0; for (delay = 0; delay < 500000; delay++); GPIO_ResetBits(GPIOC, GPIO_PIN_13); for (delay = 0; delay < 500000; delay++); } }

需要说明:这段代码使用极海标准外设库风格的 API。如果用 STM32 标准外设库替代,函数名可能略有差异,例如RCC_APB2PeriphClockCmd和GPIO_Init,需要结合实际库自行替换。APM32 官网提供的标准外设库和 STM32F10x SPL 在代码习惯上比较接近,建议以官方 SDK 例程为准。

编译通过后,使用调试器下载,观察 LED 是否周期性闪烁。这个步骤的目标不是做产品功能,而是确认最小系统、启动文件、标准库、烧录链路全部正常。只有这一步稳定了,后续加 RT-Thread nano 才好定位问题。

5.3 裸机条件下 printf 输出

嵌入式开发中,最简单直接的观察手段就是串口输出。先配置 USART1,再把fputc重定向到串口,让printf可以直接输出日志。

#include "apm32f10x.h" #include "apm32f10x_gpio.h" #include "apm32f10x_rcc.h" #include "apm32f10x_usart.h" void USART1_GPIO_Config(void) { GPIO_Config_T gpioConfig; USART_Config_T usartConfig; RCC_EnableAPB2Periph(RCC_APB2_PERIPH_GPIOA | RCC_APB2_PERIPH_USART1); gpioConfig.mode = GPIO_MODE_AF_PP; gpioConfig.pin = GPIO_PIN_9; gpioConfig.speed = GPIO_SPEED_50MHZ; GPIO_Config(GPIOA, &gpioConfig); gpioConfig.mode = GPIO_MODE_IN_FLOATING; gpioConfig.pin = GPIO_PIN_10; GPIO_Config(GPIOA, &gpioConfig); usartConfig.baudRate = 115200; usartConfig.wordLength = USART_WORD_LEN_8B; usartConfig.stopBits = USART_STOP_BIT_1; usartConfig.parity = USART_PARITY_NONE; usartConfig.mode = USART_MODE_TX_RX; USART_Config(USART1, &usartConfig); USART_Enable(USART1); } int fputc(int ch, FILE *f) { USART_TxData(USART1, (uint8_t)ch); while (USART_ReadStatusFlag(USART1, USART_FLAG_TXBE) == RESET); return ch; }

加入重定向之后,在main的while(1)中调用printf("APM32 serial log ok\r\n"),打开串口助手就能看到输出。这一步验证了 GPIO 复用配置、串口工作模式和调试链路,是后续 RT-Thread nano 日志输出的基础。

6. 移植 RT-Thread nano 到 APM32F103C8T6

裸机点灯和串口正常之后,开始加入 RT-Thread nano。移植的核心是把 RTOS 内核源码放进工程,提供板级时钟和SysTick心跳,然后在主函数中完成线程栈初始化,并启动调度器。

6.1 添加 RT-Thread nano 源码

从 RT-Thread 官方仓库获取 nano 源码后,在工程中至少加入:

board.c rtconfig.h src/clock.c src/components.c src/idle.c src/ipc.c src/irq.c src/kservice.c src/mem.c src/object.c src/scheduler.c src/thread.c src/timer.c libcpu/arm/cortex-m3/context_gcc.S libcpu/arm/cortex-m3/cpuport.c

具体参与编译的源文件由rtconfig.h决定。例如只使用静态线程和信号量时,内存堆、消息队列相关组件可以不编译,能省一部分 Flash。

如果使用 IAR 或 Keil,上下文切换汇编文件要选择对应的.s,例如context_rvds.S是 RVDS/Keil 编译器格式。抄错了汇编文件会出现“调度后系统跑飞”这类问题。

6.2 修改 board.c 和系统心跳

board.c的主要职责是初始化系统时钟,并让 SysTick 中断周期性调用rt_tick_increase()。在 Cortex-M3 中,SysTick 中断例程名需要与启动文件保持一致,也可以自己定义并注册到中断向量表。

#include "board.h" #include "apm32f10x.h" void SysTick_Handler(void) { rt_tick_increase(); } void rt_hw_board_init(void) { SystemInit(); SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND); }

RT_TICK_PER_SECOND在rtconfig.h中定义,常见值是 1000,即 1ms 一个 tick。这个宏决定了时间片、延时精度的基础粒度。配置过低,任务调度时延变大;配置过高,SysTick 中断频繁,CPU 有效利用率下降。对于 APM32F103C8T6 跑 RT-Thread nano,1000Hz 是常用配置,实际项目可根据实时性要求调整。

6.3 主函数启动调度

RT-Thread nano 的启动流程通常是在main初始化硬件外设,创建应用线程,然后调用rt_system_scheduler_start()。注意调度器启动后不会返回,裸机while(1)中的代码不会继续执行。

#include "rtthread.h" #include "apm32f10x.h" #include "apm32f10x_gpio.h" static rt_thread_t led_thread = RT_NULL; static rt_thread_t log_thread = RT_NULL; static void led_thread_entry(void *parameter) { GPIO_Config_T gpioConfig; RCC_EnableAPB2Periph(RCC_APB2_PERIPH_GPIOC); gpioConfig.mode = GPIO_MODE_OUT_PP; gpioConfig.pin = GPIO_PIN_13; gpioConfig.speed = GPIO_SPEED_50MHZ; GPIO_Config(GPIOC, &gpioConfig); while (1) { GPIO_SetBits(GPIOC, GPIO_PIN_13); rt_thread_mdelay(500); GPIO_ResetBits(GPIOC, GPIO_PIN_13); rt_thread_mdelay(500); } } static void log_thread_entry(void *parameter) { while (1) { rt_kprintf("rt-thread nano running\r\n"); rt_thread_mdelay(1000); } } int main(void) { static rt_uint8_t led_thread_stack[512]; static rt_uint8_t log_thread_stack[512]; led_thread = rt_thread_create("led", led_thread_entry, RT_NULL, (rt_uint8_t *)&led_thread_stack, sizeof(led_thread_stack), 3, 20); if (led_thread != RT_NULL) { rt_thread_startup(led_thread); } log_thread = rt_thread_create("log", log_thread_entry, RT_NULL, (rt_uint8_t *)&log_thread_stack, sizeof(log_thread_stack), 2, 20); if (log_thread != RT_NULL) { rt_thread_startup(log_thread); } rt_system_scheduler_start(); return 0; }

rt_thread_create创建的线程栈建议放在静态数组或者专用内存池里,避免编译器把栈分配到不可预期区域。初次测试线程栈 256~512 字节即可,但栈大小必须根据实际调用深度调整。例如线程里调用了复杂驱动库,函数嵌套层数深、局部变量多,512 字节可能不够。可以先用较大栈测试,稳定后再逐步裁剪。

另外要注意,rt_kprintf的底层输出通常需要提供字符发送接口。RT-Thread nano 的rt_hw_console_output函数实现中,可以复用前面 USART1 发送逻辑来完成日志输出。

void rt_hw_console_output(const char *str) { while (*str != '\0') { if (*str == '\n') { USART_TxData(USART1, '\r'); while (USART_ReadStatusFlag(USART1, USART_FLAG_TXBE) == RESET); } USART_TxData(USART1, *str); while (USART_ReadStatusFlag(USART1, USART_FLAG_TXBE) == RESET); str++; } }

7. 功能测试与效果验证

移植完成以后,不能只看能不能编译通过,要分维度验证任务调度、延时、日志输出、通信机制。

7.1 双线程调度验证

第一个测试建议保留 LED 线程和 log 线程,分别运行在不同优先级。LED 线程优先级高,每 500ms 翻转一次;log 线程优先级低,每 1000ms 输出一条日志。如果芯片上电后 LED 闪烁稳定,串口每 1 秒出现一条日志,说明线程创建、优先级、延时调度和系统 tick 都工作正常。

判断标准:

  • LED 周期接近 1s,而且长时间运行不卡死。
  • 串口日志没有丢数据,也没有出现两条日志挤在同一条线上的情况。
  • 复位按键后能重新启动到任务运行状态。

7.2 线程优先级和时间片验证

两个线程同为 5ms 周期时,需要确认优先级高的任务能抢占低优先级任务。可以在高优先级线程里执行 GPIO 翻转,低优先级线程里进行一个占用时间较长的整型累加,用逻辑分析仪或者示波器观察引脚波形。没有示波器的话,可以用串口打印运行次数和时间戳辅助判断。

时间片轮转调度在多线程同优先级时生效,RT-Thread nano 默认每个线程时间片为rtconfig.h里的配置或创建线程时参数,测试时可以把两个同优先级线程都设成 10 tick,观察两者是否交替输出。

7.3 信号量同步验证

信号量是 RTOS 中最常用的同步方式。典型场景:按键按下触发外部中断,中断服务程序只释放信号量,不执行耗时逻辑;等待信号量的线程被唤醒后处理按键业务。这样中断里的耗时很短,符合实时系统中断不拖沓的要求。

static rt_sem_t key_sem = RT_NULL; static rt_uint8_t key_stack[512]; void EXTI_IRQHandler(void) { if (EXTI_GetIntStatus(EXTI_LINE_0) == SET) { EXTI_ClearIntPendingBit(EXTI_LINE_0); if (key_sem != RT_NULL) { rt_sem_release(key_sem); } } } static void key_process_entry(void *parameter) { while (1) { if (rt_sem_take(key_sem, RT_WAITING_FOREVER) == RT_EOK) { rt_kprintf("key pressed\r\n"); } } }

这个测试的目标是验证中断服务程序调用rt_sem_release是否成功唤醒线程。实际项目里不要在中断服务函数中打印复杂日志,避免优先级反转和长时间临界区问题,测试例程里仅做演示。测试时按一下按键,串口出现一条日志,说明中断信号量链路通。整个过程重点看两个点:中断中释放信号量是否丢失,线程被唤醒的时序是否正常。

7.4 消息队列传递数据验证

消息队列用于线程间带数据传递。一个线程周期发送数据,另一个线程接收数据并解析。比如传感器线程每 100ms 读取一个 ADC 值,包装成结构体发给处理线程,处理线程收到以后更新显示或执行控制逻辑。

struct sensor_msg { uint16_t adc_value; uint16_t sample_id; }; static rt_mq_t sensor_mq = RT_NULL; static uint8_t sensor_thread_stack[512]; static uint8_t process_thread_stack[512]; void sensor_thread_entry(void *parameter) { struct sensor_msg msg; uint16_t counter = 0; while (1) { msg.adc_value = ADC_ReadValue(); msg.sample_id = counter++; rt_mq_send(sensor_mq, &msg, sizeof(msg)); rt_thread_mdelay(100); } } void process_thread_entry(void *parameter) { struct sensor_msg msg; while (1) { if (rt_mq_recv(sensor_mq, &msg, sizeof(msg), RT_WAITING_FOREVER) == RT_EOK) { rt_kprintf("adc=%d id=%d\r\n", msg.adc_value, msg.sample_id); } } }

如果消息队列长度或消息大小配置过小,rt_mq_send会返回错误码。调试时可以在发送端检查返回值,将错误码打印出来,避免数据“悄悄丢失”。队列最大消息数取决于项目的峰值积压量,不宜设置太大浪费 RAM。

8. RT-Thread nano 的资源占用观察与优化

APM32F103C8T6 的 Flash 64KB、SRAM 20KB,这两项资源要习惯性关注。启动 RT-Thread nano 后,可以在工程编译输出窗口查看 Program Size 字段。Code、RO-data、RW-data、ZI-data 四个值分别对应不同类型的区域占用。ZI-data 包含未初始化全局变量和线程栈数组,RAM 估算要重点关注 ZI 和堆栈。

如果发现 Code 占用偏高,可以先检查是否加入了过多 APM32 标准外设库源文件。很多标准外设库源文件里只有少量函数被实际调用,但编译器仍然会进行分离和链接。正确做法是在 Keil 中勾选 One ELF Section per Function,让每个函数单独成节,链接器才能剔除未被调用的函数。也可以在工程设置中开启--split_sections对应选项,使用 GCC 时对应-ffunction-sections -fdata-sections -Wl,--gc-sections。

RAM 优化上,RT-Thread nano 的线程栈需要静态分配。如果每个线程都预留 1KB,4 个线程就占用 4KB,20KB SRAM 很快会紧张。建议按实际调用深度配置,不要盲目取整。可以在调试器里观察线程栈高水位标记,RT-Thread 的线程控制块中通常维护栈顶和栈底信息,调试时监测当前线程栈使用情况,把多余空间逐步回收。

CPU 占用率也是需要观察的指标。虽然没有材料给出量化数据,但可以从系统 tick 中断频率、线程延时长度和任务执行时间估算。SysTick 配置为 1000Hz 时,每秒进入 1000 次中断,如果中断处理时间很短,系统负载主要来自实际业务代码和调度切换。要降低 CPU 开销,就减少无意义轮询,将等待型逻辑改为信号量、消息队列等阻塞等待方式。

常见性能瓶颈还包括:

  • 线程栈设置过大,造成 RAM 浪费。
  • 多个线程都调用rt_kprintf,串口输出慢,阻塞线程执行。
  • 中断里执行耗时操作,导致高优先级线程被延迟。
  • 临界区过长,调度器被长时间挡住,实时性下降。

9. 标准库与 RT-Thread nano 的常见问题排查

在实际开发中,编译和运行最容易卡在下面几类问题上。遇到问题时分清是“编译链接问题”还是“运行调度问题”,排查效率会高很多。

问题现象可能原因排查方式解决方案
编译报错:未定义rt_thread_createRT-Thread 源码没有加入工程,或者头文件路径缺失检查 rtconfig.h、rtthread.h 是否被编译,查看输出窗口里的错误文件位置在工程中正确加入 kernel 源文件,并配置 include path
编译报错:core_cm3.h或 APM32 头文件找不到Keil 的 Pack 路径或 SDK 路径不完整打开 C/C++ 的 Include Paths,确认目录存在添加 APM32 SDK 的 CMSIS 目录和核心头文件目录
下载后 LED 不亮,程序未运行芯片型号选错、启动文件不匹配、复位电路不可靠在 Debug 窗口检查内核是否能停止到 main,尝试复位后运行根据芯片选择对应启动文件,确认 SWD 复位连接
调度器启动后系统进入 HardFault线程栈分配地址不对,或者 SysTick 中断未进入rt_tick_increase在 HardFault_Handler 打断点,查看压栈的 PC 地址检查线程栈内存对齐,确认启动文件里的中断向量与源码一致
rt_kprintf无输出串口未初始化或rt_hw_console_output为空先裸机测试串口发送,确认能出字符在 RT-Thread 初始化前完成串口外设初始化
两个线程的日志相互交错多个线程同时调用输出函数在输出函数外用互斥量保护,或者统一日志线程使用互斥量包裹串口发送,或通过消息队列收集日志后再统一输出
rt_sem_take一直阻塞不返回信号量未创建成功,或者释放信号量的线程未运行检查信号量创建返回值;在两个操作处打断点增加信号量创建返回判断,避免RT_NULL指针调用
编译通过后 RAM 占用飙高线程栈数组过大,或者链接脚本里栈堆配置过大查看 map 文件中的 ZI 数据和各线程栈地址按实际需求裁剪线程栈和堆大小
标准库函数与 RTOS 冲突裸机上使用的Delay延时以及部分驱动库不兼容多线程调度断点查看进入死循环的位置将裸机轮询延时改成rt_thread_mdelay,并保证共享外设访问互斥

9.1 Keil 编译链接细节

在 Keil MDK 中,工程配置里的宏定义很关键。APM32F10x 标准库通常会要求定义对应型号宏,例如APM32F10X_MD,没有定义可能导致外设寄存器地址范围或中断向量表不匹配。RT-Thread nano 可能需要RT_USING_...类宏来决定裁剪哪些组件,rtconfig.h中已经定义,但要确保rtconfig.h被找到。

9.2 调试接口和复位问题

如果 SWD 下载时提示找不到目标芯片,优先检查接线。SWDIO、SWCLK、GND 是三根必不可少的基础线,部分调试器还需要目标板供电。APM32F103C8T6 的 3.3V 供电不能省,否则芯片虽然不会烧毁,但也不会进入调试状态。调试器连接不成功时,在 Keil Options for Target -> Debug -> Settings 中查看 IDCODE 是否出现,如果显示 No Target,再检查引脚接触和复位键。

9.3 RT-Thread nano 启动 HardFault 定位思路

进程运行中 HardFault 的常见原因是指针越界、栈溢出、中断里调用不支持的操作。先查看 HardFault_Handler 处的调用栈,如果无法直接定位,可以在线程入口和关键循环处添加 GPIO 翻转标记,缩小范围。也可以利用 Keil 的View -> Watch Window查看rt_interrupt_nest和当前线程控制块,确认是否在中断中触发了调度操作。

如果怀疑线程栈溢出,可以把线程栈数组向下翻倍,比如从 256 改到 512,看现象是否消失。去掉动态内存、改用静态内存分配可以降低内存碎片风险。RT-Thread nano 在静态线程模式下,不依赖堆管理器,线程栈直接传数组地址,启动时如果调度器发现栈地址不符合对齐要求,也容易产生问题。一般情况下,Cortex-M3 要求 8 字节对齐,RT-Thread 对线程栈地址有自身要求,定义线程栈数组时不要随意用__align之类的关键字,按 RT-Thread 官方例程做法来即可。

10. 最佳实践与合规提醒

10.1 工程级实践建议

  1. 第一次跑 RT-Thread nano,先保留一个裸机最小工程备份,保证随时能回退到可运行状态。
  2. 所有线程栈数组、消息队列缓冲区、信号量对象,建议集中定义到一个内存规划文件中,并做注释说明大致用途,方便后面评估 RAM 占用。
  3. 线程命名要能看懂,“led_thread”“comm_thread”这类名称比“thread1”更容易维护。
  4. 串口日志里加入 tick 时间戳或线程名,方便定位输出顺序和任务调度时机。
  5. 如果使用标准库的断言(assert)机制,正式版固件建议关闭,避免断言失败进入死循环造成设备无响应。
  6. 批量下载固件时,调试器脚本里应当包含全片擦除和自动复位步骤,防止旧固件残留导致测试结果异常。
  7. 使用版本管理工具管理代码时,将 Keil 的自定义工程文件、链接脚本和 SDK 版本说明一起提交,方便跨电脑编译。

10.2 与外设驱动相关的实践建议

APM32F103C8T6 的 GPIO 和 USART 配置完成后,一定要熟悉读状态标志、读取输入数据寄存器、写输出数据寄存器的库函数。嵌入式调试中,判断外设是否正常运行,通常不是看代码写得多巧妙,而是看寄存器状态和引脚电平。例如串口发送失败,先测量 TX 引脚是否有电平翻转;GPIO 输出异常,先确认复用时钟是否打开、模式是否配成了复用推挽。

多个线程操作同一个外设时,必须设计好保护策略。以 USART1 为例,一个线程打印温度日志,另一线程打印按键事件,两者同时调用printf会导致输出交错。可以把所有打印请求通过消息队列发送给日志线程,由日志线程统一串行处理,既避免了临界区锁的复杂度,也能集中控制日志开关。如果是调试阶段不想引入额外队列,可以直接用一个互斥量保护printf调用。

10.3 合规与安全提醒

使用 APM32F103C8T6 做产品评估时,要注意芯片数据手册、标准外设库和调试工具固件都有各自的使用授权规则。开发学习阶段没有问题,但做量产项目时要提前确认供应链渠道和固件授权范围。涉及医疗器械、安全控制、汽车电子等对可靠性和认证有强制性要求的场景,需要严格按照行业标准进行设计验证,不能仅依赖个人学习工程的经验。

涉及敏感数据时需要注意,任何通信数据、日志输出和固件传输如果被非授权方读取,都有可能造成信息泄漏。开发阶段的串口日志不应保留到正式版本。使用 RT-Thread nano 时,要留意是否开启了 FinsH 或调试相关组件,正式发布固件应关闭这些调试通道。

11. 总结与下一步

APM32F103C8T6 标准库开发和 RT-Thread nano 移植,非常适合作为嵌入式 RTOS 的入门路线。整套方案不需要高端调试器,不需要大容量芯片,一块几十元的开发板加上 ST-Link 或者 DAP-Link 就能把任务调度、信号量同步、消息队列这些 RTOS 核心概念跑通。相比裸机 while(1) 加标志位的写法,RT-Thread nano 引入线程模型后,代码职责更清晰,外设等待和业务处理可以分离开来,后续功能扩展也更容易。

最先建议验证的是第二部分的双线程点灯和串口日志输出,这一步能同时确认工程配置、调度器启动和串口重定向是否正常。只要这个基础跑通,信号量、消息队列、软件定时器都可以在同一套工程里快速补充。最容易踩的坑集中在启动文件不匹配、SysTick 未调用 rt_tick_increase、线程栈溢出这几类问题上,建议把断点下在 HardFault_Handler 和线程入口处,逐步缩小范围。

如果想继续深入,可以在当前基础上增加软件定时器周期任务、外部中断与信号量配合的按键扫描,或者用 ADC 读取电位器并通过消息队列把采样值发送到显示线程。进一步还可以把 APM32 的 I2C、SPI、PWM 驱动和 RT-Thread nano 的多线程模型结合,做一个小型环境监测节点。建议把这套工程作为自己的基础模板保存,后续新项目可以直接复制,快速进入业务开发。

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

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

立即咨询