news 2026/8/19 6:04:41

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 软件工具清单与关键配置

首先,确保你的电脑上已经安装了以下软件,并且版本不要太老:

  1. Keil MDK5 (MDK-ARM):这是我们的主力开发环境。建议使用V5.36或以上版本。安装时,务必记得安装对应的ARM Compiler(通常选择V6版本编译器,兼容性更好)和STM32F1系列的Device Family Pack。一个常见的坑是只装了Keil,没装芯片包,导致新建工程时找不到STM32F103C8T6这个型号。
  2. RT-Thread源码:我们需要去RT-Thread的GitHub仓库或者Gitee镜像站,下载v3.1.5版本的源代码。注意,要下载完整的源代码包,而不是仅仅下载内核。因为我们的模板需要用到它的构建系统(Scons)和BSP。
  3. STM32CubeMX:虽然我们最终用Keil开发,但CubeMX在生成底层HAL库驱动和初始化代码方面是无敌的。我们用它来快速生成正确的时钟树、引脚配置,特别是SysTick定时器的配置(这是RT-Thread系统心跳的来源)。
  4. 串口调试助手:如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.cboard.h是板级初始化的核心,必须拷贝。
  • applications/目录可以整个拷贝,里面通常有一个main.c示例。

第二步,处理启动文件与链接脚本。这是最容易出错的地方。在libraries/CMSIS/Device/ST/STM32F1xx/Source/Templates/arm/里找到启动文件startup_stm32f103xe.s(注意,C8T6属于中等容量,对应md,但很多BSP直接用xe的也兼容)。拷贝它到你的工程目录。同时,需要找到链接脚本STM32F103C8T6_FLASH.ld(GCC用)或STM32F103C8T6.sct(Keil用)。如果BSP里没有Keil专用的.sct文件,我们需要自己准备或修改。

一个实用的技巧是:先打开一个你能正常编译运行的裸机STM32F103C8T6 Keil工程,把它的启动文件和sct链接脚本拷贝过来。然后根据RT-Thread的需求进行微调。RT-Thread需要堆(heap)空间来动态创建线程、信号量等对象,所以在sct文件里,确保堆(Heap)的大小设置得足够大,例如设置为0x800(2KB)。对于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运行在72MHz(STM32F103的典型值)。最稳妥的方法是使用STM32CubeMX生成这个函数。在CubeMX中选好芯片型号,配置好外部晶振(HSE),选择PLL倍频,生成72MHz的系统时钟。然后,将生成的SystemClock_Config()函数及其相关的HAL_Init()调用,一并复制到你的board.c中。

注意:CubeMX生成的代码会初始化所有用到的外设。但在RT-Thread中,我们可能希望某些外设(如某个定时器)由特定的驱动文件来初始化。为了避免冲突,可以在CubeMX生成代码时,只勾选最基础的系统时钟(RCC)、SysTick和你要用作控制台的串口(比如USART1)。其他外设的初始化代码先不生成,后续在RT-Thread的驱动框架下添加。

3.2 理解上下文切换的汇编“魔术”

上下文切换是RTOS的“灵魂”。对于Cortex-M3,RT-Thread的上下文切换代码位于rt-thread/libcpu/arm/cortex-m3/目录下,主要是context_rvds.S这个汇编文件。你几乎不需要修改这个文件,但理解它在做什么,对调试有巨大帮助。

当系统决定从当前线程切换到另一个线程时,它会触发一个PendSV异常。PendSV_Handler这个中断服务程序就是上下文切换的执行者。它的工作可以简单理解为:

  1. 保存现场:把当前线程(正在被打断的线程)的CPU寄存器(R0-R12, LR, PC, PSR)的值,压入这个线程自己的栈里。
  2. 切换栈指针:将系统的当前线程指针,指向下一个要运行的线程。
  3. 恢复现场:从下一个线程的栈里,把之前保存的寄存器值弹出到CPU寄存器中。
  4. 异常返回:这时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设备驱动框架的串口驱动模板。我们需要检查并完善它:

  1. 初始化函数rt_hw_usart_init():这个函数应该被rt_hw_board_init()调用。它里面会调用rt_hw_serial_register()来注册一个名为uart1usart1的设备到RT-Thread的设备框架中。
  2. 配置参数:检查stm32_uart_config结构体数组。这里定义了每个串口的硬件参数,如USART1的基地址、中断号、引脚配置等。确保USART1的配置与你的硬件连接一致(比如,PA9是TX,PA10是RX)。
  3. 中断服务程序:驱动中已经实现了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 // 启用模块化shell(MSH),这是新版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,最容易遇到的问题是串口终端没有反应,或者输入字符不回显。请按以下顺序排查:

  1. 检查硬件连接:TX/RX线是否接反?串口波特率是否匹配(通常是115200)?
  2. 检查驱动注册:在rt_hw_board_init()函数中,rt_hw_usart_init()是否被成功调用?可以在其后加一句rt_kprintf(“USART1 init OK\n”);来验证。
  3. 检查FinSH线程:系统启动后,在终端里按几次回车,看是否有msh >提示符出现。如果没有,可能是FinSH线程创建失败,检查栈大小是否足够,或者优先级设置是否合理(不要设得太高,导致其他高优先级任务饿死FinSH)。
  4. 检查回显设置:有些串口工具需要本地回显(Local Echo),而FinSH本身也会回显。如果两边都回显,你会看到每个字符出现两次。通常关闭串口工具的本地回显即可。

5. 内存管理与系统裁剪的艺术

STM32F103C8T6只有20KB的RAM,这在运行RTOS时显得捉襟见肘。因此,精细的内存管理和系统裁剪,是项目成功的关键。

5.1 选择合适的内存管理算法

RT-Thread提供了几种内存管理算法,在rtconfig.h中配置:

  • RT_USING_MEMPOOL:静态内存池。适合固定大小的内存块分配,无碎片,速度快。常用于频繁创建/删除的固定大小对象。
  • RT_USING_MEMHEAP:可管理多块不连续内存的堆管理器。如果你的芯片有内部RAM和外部RAM,可以用它来统一管理。
  • RT_USING_SLAB:SLAB分配器。效率很高,适合多核心或需要高效分配小对象的场景,但复杂度也高。
  • 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_IPCRT_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 Data+Other Data(RAM) 的大小,确保它们在芯片限制内(C8T6 Flash ≤ 64KB, RAM ≤ 20KB)。

5.3 优化线程栈大小

线程栈是RAM消耗的大户。栈大小设置不足,会导致栈溢出,引发各种难以调试的随机错误(如HardFault)。设置过大,又会浪费宝贵的内存。

一个实用的方法是先给一个保守的较大值(比如1KB),让系统跑起来。然后,在FinSH中使用psfree命令查看线程栈的实际使用情况。RT-Thread的线程调度器会在栈底放置一个魔术字(通常是0xDEADBEEF)。通过检查这个魔术字是否被破坏,可以判断栈是否溢出。更高级的方法是,在调试时查看MAP文件中各线程栈的地址范围,然后在运行时监控栈指针是否接近边界。

对于简单的周期性任务(比如一个每500ms闪烁一次LED的线程),256字节甚至128字节的栈可能都足够了。而对于处理复杂逻辑或调用层次很深的函数(比如FinSH线程本身),就需要预留足够的栈空间(建议不少于1KB)。

6. 从模板到应用:创建你的第一个多线程程序

模板工程搭建好,FinSH也能用了,现在我们来点实际的,创建一个简单的多线程应用,感受一下RT-Thread的并发编程。

6.1 设计一个简单的多线程场景

假设我们有三个任务:

  1. LED线程:每500ms翻转一次LED,指示系统“活着”。
  2. 按键扫描线程:每50ms扫描一次按键,当按键按下时,发送一个消息给日志线程。
  3. 日志线程:等待接收消息,并将接收到的消息通过串口打印出来。

这个场景涵盖了线程创建、延时、线程间通信(消息队列)等基本操作。

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 代码解析与避坑点

  1. 线程优先级:我们设置了三个优先级。日志线程最高(THREAD_PRIORITY + 1),确保按键消息能被及时处理并打印。按键扫描线程次之,LED线程最低。数字越小优先级越高(RT-Thread默认)。合理的优先级设置是系统稳定运行的基础。
  2. 消息队列:我们使用消息队列(rt_messagequeue)作为线程间通信机制。它比邮箱更灵活,可以传递变长消息。初始化时指定了每条消息最大64字节,总池子2048字节。对于简单的字符串传递,这足够了。
  3. 消抖处理:在按键扫描中,我们用了简单的延时消抖。在实际产品中,可能需要更稳健的消抖算法,或者使用定时器中断来扫描按键。
  4. 组件初始化INIT_APP_EXPORT(app_init);是RT-Thread的一个魔法宏。它会把app_init函数放到一个特定的初始化段中,系统在启动时会自动按顺序调用这些初始化函数。这比在main函数里手动调用更优雅,也符合RT-Thread的组件化思想。
  5. 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_kprintf,RT-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: 0=ALL, 1=DBG, 2=INFO, 3=WARN, 4=ERROR, 5=ASSERT, 6=OFF. msh >ulog -l 2

这样,在开发阶段可以输出详细的调试信息,在产品发布时,将级别调到WARNERROR,就可以过滤掉不重要的信息,既不影响运行效率,又能保留关键错误日志。

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里输入命令并得到响应时,那种成就感,就是嵌入式开发最纯粹的乐趣之一。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 6:02:46

利用IFTTT与Webhook实现Google Assistant声控电脑锁屏

1. 从语音指令到物理锁屏&#xff1a;一个被忽视的自动化场景“嘿&#xff0c;谷歌&#xff0c;锁上我的电脑。” 这句话听起来像是科幻电影里的桥段&#xff0c;但如果你恰好是Google智能家居生态的用户&#xff0c;并且你的电脑就在几步之外&#xff0c;这个想法就会变得非常…

作者头像 李华
网站建设 2026/8/19 5:58:42

DNS协议深度解析:为何首选UDP,何时切换TCP?

在实际网络面试中&#xff0c;“DNS 走 TCP 还是 UDP&#xff1f;”是一个高频且经典的面试题。很多开发者能脱口而出“DNS 主要用 UDP&#xff0c;端口 53”&#xff0c;但一旦被追问“为什么用 UDP&#xff1f;”、“什么时候会用 TCP&#xff1f;”、“UDP 丢包了怎么办&…

作者头像 李华
网站建设 2026/8/19 5:55:34

基于Arduino Uno R4与MAX7219的LED矩阵时钟制作全攻略

1. 项目概述&#xff1a;为什么用Arduino Uno R4做LED矩阵时钟&#xff1f;如果你手头有一块Arduino Uno R4&#xff0c;又对点亮LED矩阵、制作一个属于自己的桌面数字时钟感兴趣&#xff0c;那这个项目再合适不过了。它不像那些复杂的物联网项目需要连接网络&#xff0c;核心就…

作者头像 李华
网站建设 2026/8/19 5:52:43

Axure高保真下拉列表原型设计:从单选到多选与分级交互实现

你有没有遇到过这样的场景&#xff1a;原型评审会上&#xff0c;产品经理指着屏幕上的下拉列表说&#xff1a;“这里用户应该能多选&#xff0c;但选了A就不能选B&#xff0c;而且最好能按部门分级展示。”你一边点头&#xff0c;一边心里盘算着&#xff0c;用Axure怎么又快又好…

作者头像 李华
网站建设 2026/8/19 5:52:31

基于Vitis AI NPU的PointPillars点云检测模型部署与优化实战

1. 项目概述&#xff1a;为什么要在Versal平台上部署PointPillars&#xff1f; 最近和几个做自动驾驶感知的朋友聊天&#xff0c;大家普遍在头疼一个问题&#xff1a;算法模型在实验室里跑得飞起&#xff0c;一上车端或者边缘设备&#xff0c;实时性就大打折扣。特别是像点云目…

作者头像 李华
网站建设 2026/8/19 5:51:54

Arduino创意项目:从入门到实现“恭喜发财”智能互动装置

1. 项目概述&#xff1a;一个充满节日氛围的Arduino创意项目“恭喜发财”这个项目标题&#xff0c;一听就充满了喜庆和祝福的意味。在电子创客圈&#xff0c;尤其是Arduino爱好者中&#xff0c;这通常指向一个结合了LED灯效、声音播放或简单机械动作的互动装置&#xff0c;旨在…

作者头像 李华