STM32F103移植RT-Thread实战:从零构建多线程应用与深度裁剪

📅 发布时间:2026/8/19 6:04:45
STM32F103移植RT-Thread实战:从零构建多线程应用与深度裁剪
1. 从零开始为什么要在STM32F103上移植RT-Thread如果你手头有一块经典的“蓝桥杯”或“江协科技”同款的STM32F103C8T6最小系统板并且已经跟着教程点过灯、调过串口那么下一步你大概率会想能不能给它跑个操作系统毕竟裸机编程在任务稍微复杂一点之后代码的维护和扩展就会变得异常痛苦。这时候RTOS实时操作系统就成了一个非常自然的选择。在众多RTOS中RT-Thread是一个来自国内的开源项目它不仅仅是一个内核更是一个包含了文件系统、网络协议栈、GUI框架等丰富组件的物联网操作系统平台。它的生态非常活跃文档也相对完善对于从裸机转向操作系统的嵌入式开发者来说是一个极佳的“垫脚石”。选择RT-Thread v3.15这个版本是因为它是一个长期支持版本稳定性和社区资源都比较好网上能找到的参考案例也最多对于新手来说踩坑的概率会小很多。那么为什么是“移植”而不是“使用”对于STM32F103C8T6这种ARM Cortex-M3内核的芯片RT-Thread官方已经提供了完善的BSP板级支持包。所以我们这里说的“移植”更准确地说是“基于官方BSP创建一个适合我们自己开发板和开发环境Keil MDK5的工程模板”。这个过程本质上是在学习如何将一个操作系统框架与具体的硬件和工具链对接起来。掌握了这个你以后换用其他芯片或者RTOS思路都是相通的。2. 环境准备与工程骨架搭建在开始动手之前我们需要把“战场”打扫干净把必要的“武器”准备好。这个过程看似琐碎但每一步都关系到后续编译、下载、调试能否顺利进行。2.1 软件工具清单与关键配置首先确保你的电脑上已经安装了以下软件并且版本不要太老Keil MDK5 (MDK-ARM)这是我们的主力开发环境。建议使用V5.36或以上版本。安装时务必记得安装对应的ARM Compiler通常选择V6版本编译器兼容性更好和STM32F1系列的Device Family Pack。一个常见的坑是只装了Keil没装芯片包导致新建工程时找不到STM32F103C8T6这个型号。RT-Thread源码我们需要去RT-Thread的GitHub仓库或者Gitee镜像站下载v3.1.5版本的源代码。注意要下载完整的源代码包而不是仅仅下载内核。因为我们的模板需要用到它的构建系统Scons和BSP。STM32CubeMX虽然我们最终用Keil开发但CubeMX在生成底层HAL库驱动和初始化代码方面是无敌的。我们用它来快速生成正确的时钟树、引脚配置特别是SysTick定时器的配置这是RT-Thread系统心跳的来源。串口调试助手如SecureCRT、MobaXterm、或者开源的Putty、Tera Term。用于查看RT-Thread启动后的系统日志输出这是调试的“眼睛”。安装好Keil后一个必须检查的设置是编译器版本。打开Keil点击Project - Manage - Project Items在Folders/Extensions标签页下确保Use ARM Compiler选项选择的是V6.xx。因为RT-Thread的某些组件如C支持、网络栈对编译器有要求V6编译器的兼容性最好。如果这里显示的是Use default compiler version 5你可能会在编译时遇到一堆奇怪的错误。2.2 获取与理解RT-Thread BSP结构从官网下载的RT-Thread源码包解压后目录结构非常清晰。我们重点关注bsp目录。这里面存放了所有官方支持的开发板的板级支持包。找到bsp/stm32/stm32f103-blue-pill或者类似的目录。这个“blue-pill”就是STM32F103C8T6最小系统板在国外的俗称很多BSP都是基于它。进入这个BSP目录你会看到类似这样的结构stm32f103-blue-pill/ ├── applications/ # 用户应用代码目录 ├── drivers/ # 板级驱动如LED、按键、UART等 ├── libraries/ # 芯片外设库HAL或标准库 ├── rt-thread/ # 链接到源码根目录的RT-Thread内核与组件 ├── SConstruct # Scons构建脚本 └── template.uvprojx # Keil工程模板可能没有或需要更新我们的目标就是基于这个官方的BSP创建一个我们自己的、干净的、可以在Keil中直接编译下载的工程。官方的BSP通常使用Scons构建虽然强大但不如Keil工程直观。所以“移植”的一大工作就是将Scons构建的源码组织方式适配到Keil的工程管理方式中。2.3 创建属于自己的Keil工程模板不要直接在官方BSP目录里修改。我建议的做法是新建一个独立的文件夹比如MyRTThread_Project作为我们模板工程的根目录。第一步拷贝核心文件。从官方BSP中我们需要拷贝以下关键内容到我们的新目录rt-thread/目录下的所有内容或者直接从RT-Thread源码根目录拷贝rt-thread目录过来。这是操作系统核心。libraries/目录下的STM32F1xx_HAL_Driver如果你用HAL库或STM32F10x_StdPeriph_Driver如果你用标准库。建议新手用HAL库更现代。drivers/目录下的drv_common.c,drv_usart.c串口驱动等。board.c和board.h是板级初始化的核心必须拷贝。applications/目录可以整个拷贝里面通常有一个main.c示例。第二步处理启动文件与链接脚本。这是最容易出错的地方。在libraries/CMSIS/Device/ST/STM32F1xx/Source/Templates/arm/里找到启动文件startup_stm32f103xe.s注意C8T6属于中等容量对应md但很多BSP直接用xe的也兼容。拷贝它到你的工程目录。同时需要找到链接脚本STM32F103C8T6_FLASH.ldGCC用或STM32F103C8T6.sctKeil用。如果BSP里没有Keil专用的.sct文件我们需要自己准备或修改。一个实用的技巧是先打开一个你能正常编译运行的裸机STM32F103C8T6 Keil工程把它的启动文件和sct链接脚本拷贝过来。然后根据RT-Thread的需求进行微调。RT-Thread需要堆heap空间来动态创建线程、信号量等对象所以在sct文件里确保堆Heap的大小设置得足够大例如设置为0x8002KB。对于C8T6这块只有64KB Flash和20KB RAM的芯片内存规划必须精打细算。3. 内核移植的核心系统时钟与上下文切换操作系统要跑起来最基础的两根支柱是准确的心跳系统时钟和流畅的任务切换上下文切换。在Cortex-M3内核上RT-Thread通常利用SysTick定时器作为系统时钟源利用PendSV异常来实现上下文切换。3.1 配置SysTick系统时钟SysTick的配置一般在board.c文件的rt_hw_board_init()函数中完成。这个函数是硬件初始化的入口比main函数执行得还要早。void rt_hw_board_init() { // 1. 配置系统时钟通常使用外部晶振HSE倍频到72MHz SystemClock_Config(); // 2. 初始化SysTick配置为每秒触发RT_TICK_PER_SECOND次中断 // RT_TICK_PER_SECOND在rtconfig.h中定义默认1000即1ms一个tick SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND); // 3. 初始化控制台串口用于rt_kprintf输出 rt_hw_usart_init(); // 4. 打印板卡信息 rt_console_set_device(RT_CONSOLE_DEVICE_NAME); // 设置控制台设备 rt_kprintf(\n\nHello RT-Thread!\n); rt_kprintf(System Clock: %d Hz\n, SystemCoreClock); // ... 其他板级初始化 }这里的关键是SystemClock_Config()函数。你需要一个正确的时钟树配置让CPU运行在72MHzSTM32F103的典型值。最稳妥的方法是使用STM32CubeMX生成这个函数。在CubeMX中选好芯片型号配置好外部晶振HSE选择PLL倍频生成72MHz的系统时钟。然后将生成的SystemClock_Config()函数及其相关的HAL_Init()调用一并复制到你的board.c中。注意CubeMX生成的代码会初始化所有用到的外设。但在RT-Thread中我们可能希望某些外设如某个定时器由特定的驱动文件来初始化。为了避免冲突可以在CubeMX生成代码时只勾选最基础的系统时钟RCC、SysTick和你要用作控制台的串口比如USART1。其他外设的初始化代码先不生成后续在RT-Thread的驱动框架下添加。3.2 理解上下文切换的汇编“魔术”上下文切换是RTOS的“灵魂”。对于Cortex-M3RT-Thread的上下文切换代码位于rt-thread/libcpu/arm/cortex-m3/目录下主要是context_rvds.S这个汇编文件。你几乎不需要修改这个文件但理解它在做什么对调试有巨大帮助。当系统决定从当前线程切换到另一个线程时它会触发一个PendSV异常。PendSV_Handler这个中断服务程序就是上下文切换的执行者。它的工作可以简单理解为保存现场把当前线程正在被打断的线程的CPU寄存器R0-R12, LR, PC, PSR的值压入这个线程自己的栈里。切换栈指针将系统的当前线程指针指向下一个要运行的线程。恢复现场从下一个线程的栈里把之前保存的寄存器值弹出到CPU寄存器中。异常返回这时PC寄存器会指向下一个线程上次被打断的地方程序就“无缝”地开始运行下一个线程了。在Keil工程中你需要确保这个context_rvds.S文件被正确添加到了工程里并且它的汇编器设置正确通常就是默认的ARM汇编器。一个常见的编译错误是“PendSV_Handler重复定义”。这是因为在启动文件startup_stm32f103xe.s里已经用Weak属性定义了一个默认的PendSV_Handler里面可能是个死循环。而我们的context_rvds.S提供了一个强符号定义。链接时强符号会覆盖弱符号这是正确的。但如果你的工程设置有问题导致启动文件中的弱符号没有被正确覆盖就可能出错。确保你的工程只包含一份context_rvds.S的实现。4. 驱动适配与FinSH控制台的实现操作系统跑起来后我们需要能和它“对话”。FinSH是RT-Thread的组件它提供了一个命令行交互界面可以通过串口输入命令来查看线程状态、动态修改内核对象是开发和调试的利器。4.1 串口驱动适配FinSH依赖于一个能收/发字符的设备通常是串口。所以我们必须先让串口驱动在RT-Thread的设备框架下工作起来。在drivers/drv_usart.c文件中已经有一个基于RT-Thread设备驱动框架的串口驱动模板。我们需要检查并完善它初始化函数rt_hw_usart_init()这个函数应该被rt_hw_board_init()调用。它里面会调用rt_hw_serial_register()来注册一个名为uart1或usart1的设备到RT-Thread的设备框架中。配置参数检查stm32_uart_config结构体数组。这里定义了每个串口的硬件参数如USART1的基地址、中断号、引脚配置等。确保USART1的配置与你的硬件连接一致比如PA9是TXPA10是RX。中断服务程序驱动中已经实现了USART1_IRQHandler。它处理接收和发送中断。当串口收到一个字符时会产生中断驱动会把这个字符放入一个缓冲区并发送一个信号通知等待读取的线程比如FinSH线程。一个关键的适配点是DMA支持。官方驱动可能默认开启了串口接收DMA。但对于简单的FinSH控制台使用中断模式就足够了而且更简单稳定。如果你的串口收发出错可以尝试在drv_usart.c的初始化部分将DMA相关的代码先注释掉强制使用中断模式。4.2 启用并配置FinSH组件FinSH的配置通过rtconfig.h文件进行。这是一个RT-Thread的核心配置文件通过一系列#define RT_USING_XXX的宏来开启或关闭组件。确保以下宏被定义#define RT_USING_FINSH #define FINSH_USING_MSH // 启用模块化shellMSH这是新版FinSH的推荐方式 #define FINSH_THREAD_PRIORITY 20 // FinSH线程的优先级 #define FINSH_THREAD_STACK_SIZE 2048 // FinSH线程的栈大小建议不少于2KB #define FINSH_USING_HISTORY // 允许使用上下箭头查看历史命令 #define FINSH_USING_SYMTAB // 允许导出符号表这样你自定义的函数也能在FinSH中调用然后在你的应用代码比如applications/main.c中你需要导出你想在FinSH中使用的命令或函数。例如你写了一个LED闪烁的函数#include finsh.h // 必须包含这个头文件 void led_toggle(void) { rt_pin_write(LED_PIN, !rt_pin_read(LED_PIN)); rt_kprintf(LED toggled.\n); } MSH_CMD_EXPORT(led_toggle, toggle the LED pin);编译下载后在串口终端里输入led_toggle就能控制LED了。实操心得第一次使用FinSH最容易遇到的问题是串口终端没有反应或者输入字符不回显。请按以下顺序排查检查硬件连接TX/RX线是否接反串口波特率是否匹配通常是115200检查驱动注册在rt_hw_board_init()函数中rt_hw_usart_init()是否被成功调用可以在其后加一句rt_kprintf(“USART1 init OK\n”);来验证。检查FinSH线程系统启动后在终端里按几次回车看是否有msh 提示符出现。如果没有可能是FinSH线程创建失败检查栈大小是否足够或者优先级设置是否合理不要设得太高导致其他高优先级任务饿死FinSH。检查回显设置有些串口工具需要本地回显Local Echo而FinSH本身也会回显。如果两边都回显你会看到每个字符出现两次。通常关闭串口工具的本地回显即可。5. 内存管理与系统裁剪的艺术STM32F103C8T6只有20KB的RAM这在运行RTOS时显得捉襟见肘。因此精细的内存管理和系统裁剪是项目成功的关键。5.1 选择合适的内存管理算法RT-Thread提供了几种内存管理算法在rtconfig.h中配置RT_USING_MEMPOOL静态内存池。适合固定大小的内存块分配无碎片速度快。常用于频繁创建/删除的固定大小对象。RT_USING_MEMHEAP可管理多块不连续内存的堆管理器。如果你的芯片有内部RAM和外部RAM可以用它来统一管理。RT_USING_SLABSLAB分配器。效率很高适合多核心或需要高效分配小对象的场景但复杂度也高。RT_USING_MEMHEAP_AUTO_BINDING自动绑定到默认堆。对于资源紧张的C8T6我强烈建议主要使用静态内存分配。即在编译时就确定好线程栈、信号量、互斥锁等内核对象的内存避免运行时动态分配rt_malloc产生碎片导致系统运行一段时间后因内存不足而崩溃。例如创建线程时使用静态方式static char thread1_stack[512]; // 静态栈空间 static struct rt_thread thread1; // 静态线程控制块 rt_thread_init(thread1, “thread1”, thread1_entry, RT_NULL, thread1_stack[0], sizeof(thread1_stack), 15, 10); rt_thread_startup(thread1);5.2 深度裁剪RT-Thread内核与组件rtconfig.h文件是你的“手术刀”。每一个RT_USING_XXX宏都对应着一个功能模块。我们的原则是用不到的功能坚决关闭。以下是一些可以安全关闭的选项能为你的系统节省不少Flash和RAM文件系统如果你暂时不需要SD卡或Flash文件系统关闭RT_USING_DFS。网络协议栈如果项目不需要联网关闭RT_USING_LWIP。这是个体积庞大的组件。设备虚拟文件系统如果不需要/dev目录下的设备文件抽象可以关闭RT_USING_DEVICE_IPC和RT_USING_POSIX的相关部分。C支持如果只用C语言开发关闭RT_USING_CPLUSPLUS。日志系统ULog日志系统功能强大但也会占用资源。在资源紧张时可以关闭RT_USING_ULOG用简单的rt_kprintf代替。组件自动初始化这是一个方便但略有开销的功能。如果你追求极致的启动速度可以了解并谨慎调整RT_USING_COMPONENTS_INIT。裁剪后务必重新编译并关注编译后生成的map文件Keil中在Options for Target - Listing - Linker Listing中勾选Memory Map生成。查看Code(Flash) 和Zero Initialized DataOther Data(RAM) 的大小确保它们在芯片限制内C8T6 Flash ≤ 64KB, RAM ≤ 20KB。5.3 优化线程栈大小线程栈是RAM消耗的大户。栈大小设置不足会导致栈溢出引发各种难以调试的随机错误如HardFault。设置过大又会浪费宝贵的内存。一个实用的方法是先给一个保守的较大值比如1KB让系统跑起来。然后在FinSH中使用ps或free命令查看线程栈的实际使用情况。RT-Thread的线程调度器会在栈底放置一个魔术字通常是0xDEADBEEF。通过检查这个魔术字是否被破坏可以判断栈是否溢出。更高级的方法是在调试时查看MAP文件中各线程栈的地址范围然后在运行时监控栈指针是否接近边界。对于简单的周期性任务比如一个每500ms闪烁一次LED的线程256字节甚至128字节的栈可能都足够了。而对于处理复杂逻辑或调用层次很深的函数比如FinSH线程本身就需要预留足够的栈空间建议不少于1KB。6. 从模板到应用创建你的第一个多线程程序模板工程搭建好FinSH也能用了现在我们来点实际的创建一个简单的多线程应用感受一下RT-Thread的并发编程。6.1 设计一个简单的多线程场景假设我们有三个任务LED线程每500ms翻转一次LED指示系统“活着”。按键扫描线程每50ms扫描一次按键当按键按下时发送一个消息给日志线程。日志线程等待接收消息并将接收到的消息通过串口打印出来。这个场景涵盖了线程创建、延时、线程间通信消息队列等基本操作。6.2 代码实现与详解首先在applications目录下新建一个app.c文件并把它添加到Keil工程中。#include rtthread.h #include rtdevice.h #define THREAD_PRIORITY 25 #define THREAD_STACK_SIZE 512 #define THREAD_TIMESLICE 5 #define LED_PIN GET_PIN(A, 1) // 假设LED在PA1 #define KEY_PIN GET_PIN(B, 0) // 假设按键在PB0低电平有效 /* 定义消息队列控制块和缓冲区 */ static struct rt_messagequeue mq; static char msg_pool[2048]; // 消息池缓冲区 /* LED线程入口函数 */ static void led_thread_entry(void *parameter) { rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT); while (1) { rt_pin_write(LED_PIN, PIN_HIGH); rt_thread_mdelay(500); rt_pin_write(LED_PIN, PIN_LOW); rt_thread_mdelay(500); } } /* 按键扫描线程入口函数 */ static void key_scan_thread_entry(void *parameter) { rt_pin_mode(KEY_PIN, PIN_MODE_INPUT_PULLUP); // 上拉输入 char msg_buf[] “Key Pressed!”; while (1) { if (rt_pin_read(KEY_PIN) PIN_LOW) // 按键按下 { rt_thread_mdelay(20); // 简单消抖 if (rt_pin_read(KEY_PIN) PIN_LOW) { // 发送消息到消息队列如果队列满则等待最多10个tick if (rt_mq_send(mq, msg_buf, sizeof(msg_buf)) ! RT_EOK) { rt_kprintf(“Message queue full, send failed.\n”); } // 等待按键释放 while (rt_pin_read(KEY_PIN) PIN_LOW) { rt_thread_mdelay(10); } } } rt_thread_mdelay(50); // 每50ms扫描一次 } } /* 日志线程入口函数 */ static void log_thread_entry(void *parameter) { char buf[64]; while (1) { // 从消息队列接收消息永久等待 if (rt_mq_recv(mq, buf, sizeof(buf), RT_WAITING_FOREVER) RT_EOK) { rt_kprintf(“[Log] %s\n”, buf); } } } int app_init(void) { rt_thread_t tid RT_NULL; /* 初始化消息队列 */ rt_mq_init(mq, “mqt”, msg_pool[0], // 消息队列对象名称缓冲区指针 64, // 每条消息的最大长度 sizeof(msg_pool), // 消息池总大小 RT_IPC_FLAG_FIFO); // FIFO模式 /* 创建LED线程 */ tid rt_thread_create(“led”, led_thread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY - 1, // LED线程优先级稍低 THREAD_TIMESLICE); if (tid ! RT_NULL) rt_thread_startup(tid); /* 创建按键扫描线程 */ tid rt_thread_create(“key”, key_scan_thread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY, // 默认优先级 THREAD_TIMESLICE); if (tid ! RT_NULL) rt_thread_startup(tid); /* 创建日志线程 */ tid rt_thread_create(“log”, log_thread_entry, RT_NULL, THREAD_STACK_SIZE, THREAD_PRIORITY 1, // 日志线程优先级最高确保及时响应 THREAD_TIMESLICE); if (tid ! RT_NULL) rt_thread_startup(tid); return 0; } /* 使用组件自动初始化将app_init添加到系统启动 */ INIT_APP_EXPORT(app_init);6.3 代码解析与避坑点线程优先级我们设置了三个优先级。日志线程最高THREAD_PRIORITY 1确保按键消息能被及时处理并打印。按键扫描线程次之LED线程最低。数字越小优先级越高RT-Thread默认。合理的优先级设置是系统稳定运行的基础。消息队列我们使用消息队列rt_messagequeue作为线程间通信机制。它比邮箱更灵活可以传递变长消息。初始化时指定了每条消息最大64字节总池子2048字节。对于简单的字符串传递这足够了。消抖处理在按键扫描中我们用了简单的延时消抖。在实际产品中可能需要更稳健的消抖算法或者使用定时器中断来扫描按键。组件初始化INIT_APP_EXPORT(app_init);是RT-Thread的一个魔法宏。它会把app_init函数放到一个特定的初始化段中系统在启动时会自动按顺序调用这些初始化函数。这比在main函数里手动调用更优雅也符合RT-Thread的组件化思想。GET_PIN宏这是RT-Thread PIN设备驱动提供的便捷宏用于将端口和引脚号编码成一个统一的PIN编号。比直接操作寄存器更可移植。编译并下载这个程序到开发板。上电后你应该能看到LED开始闪烁。按下按键在串口终端里应该能看到[Log] Key Pressed!的打印信息。这就证明你的RT-Thread多线程模板工程已经成功运行起来了7. 进阶调试与性能分析当你的应用越来越复杂可能会遇到线程卡死、优先级反转、内存泄漏等问题。掌握RT-Thread内置的调试工具能让你事半功倍。7.1 利用FinSH进行运行时诊断FinSH不仅是命令输入界面更是强大的调试工具台。ps命令列出所有线程。这是你首先应该学会的命令。它会显示每个线程的状态运行running、就绪ready、挂起suspend、关闭close、优先级、栈的最大使用量max used和剩余量。定期查看max used是调整线程栈大小的最重要依据。free命令查看系统内存堆的使用情况。如果你使用了动态内存分配这个命令可以帮你快速判断是否存在内存泄漏已用内存used持续增长不释放。list_timer命令列出所有系统定时器软定时器。检查是否有定时器没有正确删除。list_sem/list_mutex/list_mq命令列出所有的信号量、互斥锁、消息队列。可以查看它们的名称、当前值、等待线程数等信息帮助诊断线程阻塞问题。7.2 使用日志系统进行分级输出除了简单的rt_kprintfRT-Thread的ULog组件提供了强大的、分级的日志功能。你可以在rtconfig.h中开启RT_USING_ULOG并设置全局日志级别如ULOG_ASSERT,ULOG_ERROR,ULOG_WARN,ULOG_INFO,ULOG_DBG。在代码中你可以这样使用#include ulog.h LOG_D(“This is a debug message, value%d”, some_value); LOG_I(“System started.”); LOG_W(“Memory is running low.”); LOG_E(“Failed to open device!”);然后在FinSH中你可以动态调整前台或后台的日志输出级别msh ulog Usage: ulog [-l] [level] : Set the logger level. levels: 0ALL, 1DBG, 2INFO, 3WARN, 4ERROR, 5ASSERT, 6OFF. msh ulog -l 2这样在开发阶段可以输出详细的调试信息在产品发布时将级别调到WARN或ERROR就可以过滤掉不重要的信息既不影响运行效率又能保留关键错误日志。7.3 应对HardFault等严重错误在嵌入式开发中HardFault硬件错误几乎是不可避免的。RT-Thread提供了一种机制可以在发生HardFault时自动打印出发生错误时的调用栈Backtrace这对于定位野指针、栈溢出等问题极其有用。确保在rtconfig.h中开启了RT_USING_CPU_FFS用于查找栈底和RT_DEBUG相关选项。当发生HardFault时系统会跳转到rt_hw_hard_fault_exception函数通常在context_rvds.S或单独的故障处理文件中。这个函数会尽力打印出发生故障时的PC程序计数器、LR链接寄存器和栈内容。你需要学会解读这些十六进制地址。结合Keil生成的map文件你可以大致定位到是哪个函数出了问题。例如如果PC的值是0x08001234你可以在map文件里搜索这个地址附近的函数名就能知道崩溃时程序执行到了哪里。移植RT-Thread到STM32F103C8T6并创建一个好用的Keil工程模板是一个典型的“麻雀虽小五脏俱全”的嵌入式系统工程实践。它强迫你去理解时钟、中断、内存、链接脚本这些底层知识同时又让你能站在操作系统的肩膀上用更高级的抽象线程、信号量、消息队列来思考问题。这个模板一旦搭建成功就会成为你后续所有STM32F1系列项目甚至其他Cortex-M芯片项目的强大起点。过程中遇到的每一个编译错误、每一个链接警告、每一个运行时异常都是加深你对系统理解的机会。当你第一次在FinSH里输入命令并得到响应时那种成就感就是嵌入式开发最纯粹的乐趣之一。