
在 APM32F103C8T6 这类 Cortex-M3 微控制器上做入门开发最常见的一条路线是先使用标准外设库完成时钟、GPIO、串口等底层操作等裸机流程跑通之后再引入 RT-Thread nano 实时操作系统。APM32F103C8T6 的入门不能只停留在点亮一颗 LED真正的难点在于理解中断、调度、线程栈和资源共享这些问题。本文会以“裸机标准库工程 - 集成 RT-Thread nano - 编写双线程实时程序”为主线说明每一步为什么要做、文件怎么放、配置怎么改、程序出现问题又该怎么查。这套知识适合有几周 C 语言基础和简单单片机经验的开发者。如果你之前只接触过 Arduino 或纯寄存器开发可以先按照本文的目录结构把工程建立起来再逐个理解函数背后的逻辑。文章中的代码和配置以常见 APM32F10x SDK 与 RT-Thread nano 源码目录为基础实际工程中如果遇到函数名或宏定义不完全一致以开发板配套的标准库和实际源码为准。1. 先厘清三个概念芯片、标准库和 RT-Thread nano1.1 APM32F103C8T6 的芯片定位与学习价值APM32F103C8T6 是一颗基于 ARM Cortex-M3 内核的国产 32 位微控制器常见封装为 LQFP48管脚定义和 STM32F103C8T6 比较接近。很多入门开发板会把这类芯片做成替代体验方案因此学习资料可以借鉴但不能把另一款芯片的标准库文件无脑复制进来使用。从资源角度看APM32F103C8T6 属于中小容量芯片适合做电机控制、传感器采集、小尺寸人机交互、简单通信网关等场景。对初学者来说它最大的价值不是性能而是可以同时体验三件事Cortex-M3 内核中断机制、标准外设库的分层封装、轻量 RTOS 的任务调度。这里需要纠正一个容易产生的误解芯片名称里的 C8T6 不代表芯片能力只有“点灯和串口”。它内部包含 GPIO、USART、SPI、I2C、ADC、定时器等常用外设。入门时使用标准库是因为寄存器数量多且地址分散人工维护启动代码和外设初始化太容易出错。1.2 标准外设库解决什么问题寄存器开发的特点是直接操作地址例如让一个引脚输出高电平需要知道这个端口对应的寄存器偏移、位定义和总线时钟。寄存器方式的好处是代码量少、执行路径透明但在工程规模变大后可读性会迅速下降。标准库的本质是把寄存器操作封装成初始化结构体和函数例如设置某个外设的时钟、模式、速率、中断优先级等。使用标准库时代码会呈现类似下面的形状GPIO_ConfigStructure.GPIO_Mode GPIO_MODE_OUT_PP; GPIO_ConfigStructure.GPIO_Pin GPIO_PIN_5; GPIO_ConfigStructure.GPIO_Speed GPIO_SPEED_50MHz; GPIO_Config(GPIOA, GPIO_ConfigStructure);具体 API 名称会因厂商标准库版本不同而变化但思想是一致的先定义一个初始化结构体变量填入参数再调用外设初始化函数。标准库做得好是因为它把“外设有哪些模式”变成了一组可枚举的宏开发者不用频繁翻寄存器手册。使用标准库时必须保持谨慎。APM32F103 尽管与 STM32F103 风格类似但时钟树、外设基地址、启动文件和中断向量表可能不同。正确做法是使用芯片厂商提供的 APM32F10x 标准库而不是直接复制 STM32 的标准库文件。项目里一旦出现“编译通过但运行卡死”的问题优先排查是否用错了启动文件或头文件定义。1.3 RT-Thread nano 与完整版 RT-Thread 的差异RT-Thread 是一个国产开源实时操作系统常见形态可以分成完整版和 nano 版。完整版包含内核、设备驱动框架、FinSH 控制台、软件包生态等内容适合功能较多的产品。RT-Thread nano 则是为资源受限 MCU 裁剪出来的轻量内核核心能力包括线程管理、定时器、信号量、互斥量、事件、邮箱和消息队列等。nano 版对 Flash 和 RAM 的占用相对小很多这正适合 APM32F103C8T6 这类中小容量芯片做入门实验。需要注意RT-Thread nano 不等于一个完整的业务框架它通常不会默认提供 PIN 设备、PWM 设备这些抽象层。如果教程里使用rt_pin_write这类函数那说明工程额外加入了设备驱动框架或引脚管理模块而不是 nano 内核自带的能力。所以文章中的示例会尽量保持“标准库负责硬件驱动RT-Thread nano 负责调度和同步”的边界。这样理解起来更清楚硬件外设初始化该用芯片标准库就用标准库实时性、多任务、通信同步才交给 RTOS。1.4 为什么把裸机开发和 RTOS 放在同一篇入门文章里很多初学者会陷入两难先学裸机还是直接学 RTOS实际项目里两者并不是替代关系。APM32F103C8T6 启动时仍然需要标准库完成时钟和底层外设初始化之后才启动 RT-Thread 调度器。即便进入 RTOS 后每个线程访问硬件外设时仍然会调用标准库底层函数只是加上了锁或信号量保护。因此最合适的入门路线是先用标准库建立最小裸机工程验证芯片能运行、串口能输出。再加入 RT-Thread nano验证内核能调度两个线程。最后通过一个信号量或队列的例子理解 RTOS 如何协调硬件资源。这条路线的好处是任何一个环节出问题都能立刻定位是硬件问题、库配置问题还是内核移植问题。如果一开始就把完整 SDK、RTOS、中间件全部堆上来遇到 HardFault 根本不知道从哪查起。2. 工程准备工具链、标准库文件与 Keil 工程骨架2.1 开发环境与调试器选择入门阶段最常用的工具是 Keil MDK。MDK 对 Cortex-M3 的支持成熟创建工程、配置下载算法、单步调试都比较方便。你也可以使用 GCC 工具链配合 Makefile但对第一次接触标准库的新手来说MDK 的图形界面能减少很多目录路径错误。调试器可以选择 ST-Link、J-Link 或 CMSIS-DAP。APM32F103C8T6 开发板上通常预留 SWD 接口接线时确认 SWDIO、SWCLK、GND、3V3 四根线是否对应。下载算法一般可以复用 Cortex-M3 的 Flash 算法如果芯片厂商提供了专用算法优先选择专用算法。硬件准备还要确认开发板的 LED 和串口引脚。不同开发板默认引脚不同下列实践时不要照抄一个 GPIO 号。最好的方式是先看开发板原理图确定 LED 接在哪个端口的哪个引脚串口 TX、RX 是否经过 USB 转串口芯片。2.2 标准库工程目录规划与文件来源一个适合入门的 APM32 标准库工程目录结构可以按下述方式规划project ├─ Core │ ├─ main.c │ ├─ apm32f10x_it.c │ └─ system_apm32f10x.c ├─ PeriphLib │ ├─ inc │ └─ src ├─ Device │ ├─ startup_apm32f10x.s │ └─ apm32f10x.h ├─ RTOS │ ├─ include │ ├─ src │ └─ libcpu └─ MDK-ARM ├─ project.uvprojx └─ RTE目录名称不是必须完全一致重要的是把芯片设备文件、标准外设库、RT-Thread 内核源码分开避免把几百个.c文件堆在同一个目录。APM32F10x 标准库可以通过厂商提供的 SDK 压缩包获得。拿到 SDK 后不要整包塞进工程而应该重点寻找以下内容startup_apm32f10x_hd.s或对应 C8T6 的启动文件system_apm32f10x.c和system_apm32f10x.hapm32f10x.h外设寄存器定义apm32f10x_gpio.c、apm32f10x_usart.c、apm32f10x_rcc.c等外设驱动源文件启动文件的选择需要匹配芯片容量和内存型号。C8T6 的具体启动文件可能标记为ld、md或类似容量档位本文不给出固定文件名因为不同 SDK 包命名习惯不同。如果启动文件选错程序大概率会跑飞或进入异常。2.3 最小工程必须检查的四个文件在加入 RT-Thread 之前先保证最小编译工程能成立。一个最小工程至少包含以下四类内容第一启动文件。它定义了初始栈大小、向量表、Reset_Handler 和中断默认处理函数是芯片上电后第一条指令的入口。第二系统初始化文件。它负责设置 Flash 等待周期、时钟源和 PLL让系统时钟运行到目标频率。第三外设寄存器定义文件。它把地址和位域映射成 C 语言可见的结构体是标准库编写的基础。第四主程序。主程序至少调用一个 LED GPIO 初始化。如果 MDK 工程使用Define窗口配置宏务必检查是否加入了芯片型号相关的宏。宏定义不正确时头文件可能选择了错误的内存容量或外设配置分支。2.4 Keil 工程中的编译选项和下载设置创建 MDK 工程时在 Options for Target 中需要关注几个关键位置。Device 选项卡选择具体的 APM32F103C8T6 型号或者选择与之匹配的 Cortex-M3 设备。如果列表中没有该型号可以安装芯片厂商提供的 Pack 包或者在 Managing Run-Time Environment 中手动选择 Device 文件。Target 选项卡中可以设置ARM Compiler版本。旧工程常用 V5新工程可以用 V6。V6 编译器对 C 标准支持更好但某些标准库代码可能是按 V5 语法写的遇到编译错误时不要急着改逻辑先看是不是编译兼容问题。Debug 选项卡选择调试器并在 Settings 中确认 SW Device 能识别到芯片。如果识别不到检查 SWDIO、SWCLK 接线、供电和目标板 Reset 电路。Utilities 选项卡中设置 Flash Download避免每次下载失败都怀疑代码。裸机阶段常见的一个错误是“能编译、能下载但程序一动不动”。这种现象十有八九是工程里缺少system_apm32f10x.c中的时钟初始化或者启动文件没有调用SystemInit。可以用仿真器停在 Reset_Handler 里单步观察看程序是否跳转到了 main。3. 不加 RTOS 前先用标准库完成一个串口打印工程3.1 先理解 APM32F103 的时钟从哪里来Cortex-M3 芯片内部有很多外设不同外设挂在不同的总线上。GPIO 通常挂在 APB2 总线USART1 通常也挂在 APB2而 USART2、USART3 可能挂在 APB1。使用标准库时第一件事往往是打开对应外设的时钟。例如要让 GPIOA 的引脚工作初始化代码里通常会包含/* 以 APM32F10x 标准库为例实际函数名以 SDK 为准 */ RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOA);这行代码的作用是给 GPIOA 外设提供总线时钟。很多初学者以为“引脚自然带电”实际上 Cortex-M3 芯片为了降低功耗大部分外设默认不供电、不工作必须先使能时钟。这个步骤如果遗漏后续对寄存器写值不会生效而且很难排查。芯片上电后默认使用内部时钟如果板上有外部晶振且你希望系统运行在更高的主频上则由system_apm32f10x.c中的SystemInit配置 PLL。标准库头文件里通常会提供系统时钟频率宏串口波特率计算依赖这个宏。如果宏定义是 72MHz实际时钟却是 8MHz串口打印必然乱码。3.2 使用标准库初始化 GPIO 并控制 LED假设开发板 LED 连接到 PA5低电平点亮那么裸机初始化的代码结构可以这样写#include apm32f10x.h void LED_GPIO_Init(void) { GPIO_Config_T gpioConfig; RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOA); gpioConfig.mode GPIO_MODE_OUT_PP; gpioConfig.pin GPIO_PIN_5; gpioConfig.speed GPIO_SPEED_50MHz; GPIO_Config(GPIOA, gpioConfig); GPIO_SetPin(GPIOA, GPIO_PIN_5); }需要强调的是这里为了展示结构使用了简化后的结构体成员名。不同版本标准库可能把pin写成GPIO_Pin把mode写成GPIO_Mode。你不需要背 API而是先理解初始化流程开时钟、选模式、选引脚、设置速度、调用初始化函数。点灯成功后可以把 LED 翻转放到 while 循环中观察芯片是否真正在运行。不要使用无意义的空循环延时建议使用系统滴答定时器。3.3 串口初始化与 printf 重定向串口是 RTOS 入门最推荐的调试通道。先用串口打印“Hello APM32”再进入 RTOS 会容易很多。串口初始化通常包括以下几个环节打开 USART 时钟和对应 GPIO 时钟把 TX 引脚配置为复用推挽输出把 RX 引脚配置为浮空输入或上拉输入设置波特率、数据位、停止位使能 USART。初始化伪代码如下void USART1_Init(void) { GPIO_Config_T gpioConfig; USART_Config_T usartConfig; RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOA | RCM_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); }为了让串口输出方便可以把标准库的fputc重定向到串口发送函数。MDK 工程中通常使用微库或者在工程选项里勾选 Use MicroLIB。重定向代码大致如下int fputc(int ch, FILE *f) { USART_TxData(USART1, (uint8_t)ch); while (USART_ReadStatusFlag(USART1, USART_FLAG_TX_BE) RESET) { } return ch; }这段代码的关键点在于发送后要等待发送寄存器为空。如果不等待连续打印时可能丢数据。串口波特率计算错误时输出会表现为乱码或完全无输出。3.4 裸机工程的验证清单在进入 RTOS 前建议先完成以下验证清单编译是否零错误零警告。下载后程序是否停在 HardFault_Handler。LED 是否能正常翻转。串口是否能稳定打印一段字符串。复位后是否自动进入 main。用调试器暂停时PC 是否停在合理代码区域。如果这六项全部通过说明芯片时钟、启动文件、标准库、调试链路都正常。此时再加入 RT-Thread nano问题范围会大幅缩小。4. 加入 RT-Thread nano源码引入与移植配置4.1 RT-Thread nano 源码中哪些目录进入工程RT-Thread nano 源码可以从 RT-Thread 官方源码仓库或配套软件包中获得。手动添加到 Keil 工程时通常需要以下三类文件第一类是内核源码例如src/thread.c、src/timer.c、src/scheduler.c、src/ipc.c、src/mem.c等。第二类是头文件目录包括include目录里面最重要的文件是rtthread.h和rtconfig.h。第三类是 CPU 移植层文件针对 Cortex-M3 需要libcpu/arm/cortex-m3下的上下文切换汇编文件以及对应编译器版本的文件MDK 对应的是 RVDS 或 ARMCC 版本。把文件加入工程后需要保证rtconfig.h能被头文件搜索路径找到。这个头文件控制内核功能裁剪基本决定 nano 的内存占用和行为不能缺失。4.2 rtconfig.h内核配置从哪里开始RT-Thread 的裁剪工作主要通过rtconfig.h完成。一个最小 nano 配置至少包含线程优先级数量、系统时钟节拍频率、是否需要动态内存、是否需要信号量和邮箱等。常见配置如下#ifndef RT_CONFIG_H__ #define RT_CONFIG_H__ #define RT_THREAD_PRIORITY_MAX 32 #define RT_TICK_PER_SECOND 1000 #define RT_USING_SEMAPHORE #define RT_USING_MUTEX #define RT_USING_MAILBOX #define RT_USING_HEAP #define RT_USING_HOOK #endifRT_TICK_PER_SECOND决定系统节拍频率。如果设为 1000表示每秒产生 1000 次 SysTick 中断rt_thread_mdelay(1)对应一次 tick。节拍频率越高时间精度越高但 CPU 中断开销也越大。入门开发板通常用 1000Hz 足够。RT_THREAD_PRIORITY_MAX表示系统支持的最大优先级数。数值越大内核内部优先级表越占内存。常见 Cortex-M3 工程配置为 32 或 256。优先级的数字越小线程优先级越高这一点和部分 RTOS 相反需要特别注意。RT_USING_HEAP一旦开启rt_thread_create、rt_sem_create等动态对象创建函数才可用。如果不开则只能使用rt_thread_init静态初始化方式内存完全由用户静态数组提供。4.3 移植的核心SysTick、PendSV 和 board.cRT-Thread nano 需要一个能产生周期性 tick 的定时器中断服务函数里调用rt_tick_increase()。裸机工程中通常使用 SysTick因此要保证SysTick_Handler存在并且不和其他文件重复定义。常见实现类似void SysTick_Handler(void) { rt_tick_increase(); }如果 RT-Thread 移植层文件提供了SysTick_Handler强符号工程中就不能再写另一个。否则链接时会报 multiple definition或者因为启动文件的弱符号机制导致异常。建议在移植前搜索一下整个工程是否已经有SysTick_Handler。PendSV 是 Cortex-M3 专门为操作系统任务切换提供的异常。RT-Thread 的汇编上下文切换代码通常会在 libcpu 中实现PendSV_Handler它会保存当前线程寄存器恢复下一个线程寄存器。因此裸机工程里的PendSV_Handler默认空函数必须被移除或替换否则任务切换无法生效。board.c 是连接芯片和内核的桥梁。除了 SysTick还需要在系统启动阶段初始化内存堆。如果开启了RT_USING_HEAP需要实现类似于下面的代码#define HEAP_START (__bss_end) #define HEAP_END (SRAM_END) void rt_hw_board_init(void) { SystemInit(); rt_hw_interrupt_init(); rt_hw_usart_init(); #ifdef RT_USING_HEAP rt_system_heap_init(HEAP_START, HEAP_END); #endif }堆的起始地址和结束地址必须根据链接脚本和芯片 RAM 大小调整。直接写死地址是最容易出错的做法。不同开发板 SRAM 末尾可能被调试器占用需要查阅芯片数据手册确认可用 RAM 范围。4.4 启动流程先硬件初始化再启动调度器RT-Thread nano 的启动流程和裸机不同。裸机代码在 main 中 while 循环RTOS 的 main 通常只负责初始化内核和创建线程然后调用调度器启动函数让出 CPU 控制权。调度器启动后不会再回到 main 的下方执行普通函数因此不要把复杂业务写在rt_system_scheduler_start()之后。典型的启动流程如下设置中断向量和数据段这是启动文件完成。调用SystemInit初始化系统时钟。调用rt_hw_board_init完成板级初始化和堆初始化。创建若干应用线程。调用rt_system_scheduler_start启动调度。很多用户在移植时会忽略线程创建函数的返回值。如果线程创建失败调度器启动后可能没有任何线程可执行程序看起来就是“卡死”或“直接跑飞”。创建线程后建议用RT_ASSERT检查线程对象是否有效。5. 第一个带操作系统的例子双线程与信号量5.1 定义线程栈和线程控制块在中小容量芯片上使用静态线程比动态线程更容易控制内存。先定义线程栈数组和线程控制块#include rtthread.h #define LED_THREAD_STACK_SIZE 512 #define PRINT_THREAD_STACK_SIZE 1024 static rt_uint8_t led_thread_stack[LED_THREAD_STACK_SIZE]; static struct rt_thread led_thread; static rt_uint8_t print_thread_stack[PRINT_THREAD_STACK_SIZE]; static struct rt_thread print_thread;线程栈大小不是随便定的。Cortex-M3 线程入口函数内部调用的函数越多嵌套层级越深需要的栈就越大。一个只做 LED 翻转的线程可以用较小栈但要预留中断嵌套空间。若线程里调用 printf栈需求会明显增加建议先给足再通过调试器观察实际使用量逐步裁剪。5.2 编写线程入口函数两个线程入口分别处理 LED 翻转和计数打印static void led_thread_entry(void *parameter) { while (1) { GPIO_TogglePin(GPIOA, GPIO_PIN_5); rt_thread_mdelay(500); } } static void print_thread_entry(void *parameter) { rt_uint32_t count 0; while (1) { rt_kprintf(print thread count: %d\r\n, count); rt_thread_mdelay(1000); } }这里的关键点不再是“延时多久”而是线程调用rt_thread_mdelay后会主动让出 CPU。如果没有这个函数低优先级线程可能永远得不到运行。在裸机中延时是一个阻塞循环在 RTOS 中延时是线程状态从就绪态进入等待态的过程。5.3 初始化线程并创建信号量在线程创建之前先把两个线程对象初始化好再创建一个信号量用于控制串口打印的独占访问。这里演示信号量初始化的常见方式static rt_sem_t console_sem; void application_init(void) { rt_err_t result; console_sem rt_sem_create(console, 1, RT_IPC_FLAG_FIFO); RT_ASSERT(console_sem ! RT_NULL); result rt_thread_init(led_thread, led, led_thread_entry, RT_NULL, led_thread_stack, sizeof(led_thread_stack), 8, 20); RT_ASSERT(result RT_EOK); rt_thread_startup(led_thread); result rt_thread_init(print_thread, print, print_thread_entry, RT_NULL, print_thread_stack, sizeof(print_thread_stack), 16, 20); RT_ASSERT(result RT_EOK); rt_thread_startup(print_thread); }这段代码中线程优先级 8 和 16 表示前者比后者更优先。数值越小优先级越高不要搞反。20是时间片长度表示同优先级线程轮询调度时一个线程一次最多运行 20 个 tick。如果只有两个线程且优先级不同时间片参数影响不大但写出来能帮助你理解调度参数。信号量初始值为 1 表示最初可用。假设另一个线程也需要打印必须在打印前获取信号量rt_sem_take(console_sem, RT_WAITING_FOREVER); rt_kprintf(message from thread\r\n); rt_sem_release(console_sem);这样可以让打印过程互斥访问串口避免两个线程的输出交织在一起。需要强调的是这个例子只是说明信号量同步思想如果工程中没有多个线程同时写串口不必为了加锁而加锁。5.4 编译验证和预期输出程序编译下载后SWD 调试器可以看到芯片保持运行。串口终端上如果两个线程都执行会每隔 1000ms 打印一次计数同时 LED 以 500ms 周期翻转。预期输出大致如下print thread count: 0 print thread count: 1 print thread count: 2如果串口没有输出而 LED 正常翻转说明线程已经调度问题出在串口初始化或rt_kprintf底层发送函数。反之如果 LED 不翻转但打印正常可能是 LED 引脚配置错误或优先级、内存配置异常导致 LED 线程被销毁。5.5 为什么这个例子能证明内核已经跑起来一个裸机程序也可以实现“LED 翻转和串口打印”无法证明 RTOS 在工作。只有当程序里存在多个线程、线程状态发生切换、优先级和时间片开始起作用时才能确认调度器运行正常。可以在代码里加入一个手动触发错误或无意义的高优先级忙循环观察低优先级线程是否被饿死以此验证优先级机制。但入门阶段不建议刻意制造这种问题。更好的方式是在led_thread_entry和print_thread_entry入口处分别清零某个全局变量用调试器观察是否都在执行。6. 从现象到根因APM32 RT-Thread nano 排错对照6.1 程序一启动就进入 HardFault现象下载后 LED 不闪调试器停在HardFault_Handler。常见原因有三个。第一个是启动文件与芯片容量不匹配导致向量表被错误放置。第二个是配置了不存在的时钟频率或外设时钟寄存器操作触发总线错误。第三个是 RT-Thread 线程栈溢出任务切换时压栈覆盖了非法地址。检查顺序建议如下先去掉 RT-Thread 相关代码跑裸机串口工程确认硬件和标准库正常。检查SystemInit是否正确调用了SystemCoreClock更新函数。检查向量表里SysTick_Handler、PendSV_Handler是否被重复定义是否被 RT-Thread 移植层接管。检查线程栈大小下载后把栈数组初始化为固定值如0xAA运行一段时间后查看栈剩余情况。检查是否在中断服务函数里调用了会导致调度的函数并且没有正确使用中断嵌套 API。不建议一开始就猜内存地址。应先确认哪些模块加入后触发异常用二分法缩小范围。6.2 串口打印乱码或无输出现象程序能运行LED 能闪但串口输出“锘”或者干脆空白。可能原因包括串口助手波特率与代码不一致系统时钟配置和波特率计算基准不一致TX 引脚复用模式配置错误fputc重定向没有生效底层发送函数没有等待发送完成。检查方式分成两步。第一步查看代码中配置的波特率是否与串口终端一致常见值是 9600、115200、921600。第二步用示波器或逻辑分析仪测量 TX 引脚空闲电平和发送波形尤其是帧格式是否 8N1。如果程序里同时使用了裸机printf和 RT-Threadrt_kprintf还要确认两套底层发送函数是否操作同一个串口并且互斥关系是否处理好。因为两个函数都会发送数据时打印内容会出现乱码或丢失。6.3 创建线程失败或调度后无响应现象编译通过调用rt_thread_init后返回非RT_EOK或者调度器启动后系统完全没反应。第一个原因很可能是优先级参数超出RT_THREAD_PRIORITY_MAX范围。优先级数值填 40而配置最大值是 32线程创建就会失败。第二个原因是内存堆没有初始化使用动态创建函数时得到空句柄。第三个原因是线程栈数组指针为空或长度为 0。检查时先看rt_thread_init返回值并加入断言。如果使用动态方式rt_thread_create需要确认rt_hw_board_init中是否调用rt_system_heap_init堆结束地址是否超过 RAM 末尾。必要时可以把调试器挂在线程创建函数处逐个检查参数值。6.4 常见问题汇总速查表问题现象可能原因检查方式处理建议能编译下载程序不运行启动文件不对或系统时钟配置异常单步查看 Reset_Handler 是否进入 main使用正确 SDK 启动文件核对时钟初始化LED 不闪烁串口正常LED GPIO 引脚配置错误查看原理图确认引脚和电平修改 GPIO 端口和引脚号串口乱码波特率基准不一致核对 SystemCoreClock 与串口工具确保SystemInit正确更新时钟宏卡在 HardFault线程栈溢出或中断重复定义栈填充标记检查搜索异常处理函数扩大栈、去重中断函数线程不切换PendSV_Handler 被空函数覆盖在工程中搜索 PendSV 定义移除默认空实现启用 RT-Thread 移植代码动态对象创建为 NULL堆未初始化或堆空间不足检查rt_system_heap_init设置合理堆范围或改用静态对象7. 工程实践建议从“能跑”走向“能维护”7.1 裸机驱动和 RTOS 代码要分层把 GPIO、USART、I2C、SPI 等驱动函数统一放在driver层不要让业务代码直接操作寄存器。RTOS 线程里调用驱动时要考虑驱动是否可重入。比如多个线程同时读写同一个 SPI 设备时必须在驱动层加锁否则会出现总线数据错乱。推荐的做法是设计一套简单的设备接口比如void BSP_UART_Send(uint8_t *buf, uint16_t len); void BSP_UART_Receive(uint8_t *buf, uint16_t len);上层线程只调用 BSP 函数不关心底层是标准库还是其他库。这样将来从 APM32F103C8T6 换到其他芯片只需重写 BSP 层线程代码不需要大改。7.2 任务划分和内存预算要尽早做RT-Thread nano 的多线程看似方便但不能无限制创建线程。每个线程都有独立栈APM32F103C8T6 的 RAM 有限如果创建 8 个栈为 2KB 的线程内存压力会非常大。因此线程划分要遵循一个原则只有逻辑上并发、有不同实时性要求的模块才拆成独立线程。例如 LED 翻转和按键扫描可以拆开因为它们周期不同。ADC 采集和串口打印不应该简单拆成两个线程后共用全局数组而是应该用消息队列把采集结果从采集线程发给打印线程。内存预算可以按线程数、堆大小、中断栈三个维度统计。Cortex-M3 也需要为中断保留一定栈空间。不建议把所有 RAM 都分配给线程栈否则后续加入驱动、协议栈时就没有余量。7.3 周期任务优先使用定时器或线程延时很多入门 RTOS 初学者会把周期任务写成while(1)rt_thread_mdelay这是可以的。但对于需要低功耗或更精准周期的应用应该考虑使用 RT-Thread 定时器或者让线程等待信号量/事件由其他模块触发。rt_thread_mdelay调用会引发任务调度系统进入空闲线程。如果在空闲线程中做低功耗处理需要先确认芯片在空闲时能不能被 SysTick 或外部中断唤醒。Cortex-M3 提供 WFI 指令让 CPU 进入等待中断状态RT-Thread nano 的 idle 线程通常可以接入这类逻辑但调试时要注意唤醒源和中断优先级配置。7.4 debug 工具断言、栈检查和日志分级在开发阶段不要删掉RT_ASSERT。它会帮助你在问题发生的最早阶段抓住空指针、参数异常等问题。生产版本可以关闭断言但不能在开发阶段就关闭。栈检查有两种方法。第一种是把线程栈数组填充成固定值运行后用工具扫描剩余标记区域。第二种是开启 RT-Thread 的栈溢出检测但这通常需要额外代码或配置。最有效的工程方法是把所有线程栈定义统一放到一个内存配置文件里并注释清楚每个线程栈为什么需要这么大。日志输出也要分级。Nano 版本的rt_kprintf在调试阶段很有用但生产环境如果每秒打印大量日志会占用 CPU 和串口带宽甚至拖慢实时任务。建议用宏包一层例如只保留ERROR级别日志或者通过开关控制调试输出。7.5 标准库与 RT-Thread nano 结合后的扩展方向完成双线程实验后下一步可以根据自己的方向选择继续深入加入消息队列实现“生产者线程采集按键事件消费者线程处理逻辑”的结构。加入互斥量保护 ADC 或外部 Flash 等共享资源。把串口接收放到中断或 DMA 中避免轮询消耗 CPU。在空闲线程中做低功耗进入 STOP 或 STANDBY 状态。尝试把 APM32F10x 标准库驱动封装成统一的设备驱动再对接完整版 RT-Thread 设备框架。整个过程始终沿着“标准库负责硬件RTOS 负责调度”的边界做。先保证每个外设在裸机下可以用再放进线程中最后思考中断与线程的交互。这样即使未来换芯片也能快速复用大部分经验。