2. RT-Thread与STM32开发环境搭建

调试嵌入式系统,说白了就是跟黑盒子打交道。你写了个逻辑,跑起来对不对?中断有没有触发?外设寄存器写进去了没?这些问题,总不能每次都靠点灯、串口打印来猜吧?

FinSH 就是 RT-Thread 给咱们开的一扇窗。它是个命令行交互工具,跑在串口上。你连上终端,敲几个命令,就能直接看系统状态、调变量、甚至执行函数。我个人觉得,这是 RT-Thread 最实用的调试手段之一,没有它,调试效率至少打五折。

2.1 组件配置:让 FinSH 跑起来

FinSH 不是默认就有的。你得先在工程里把它“请”出来。RT-Thread 用 menuconfig 做配置,图形化界面,勾勾选选就行。

配置步骤:

  1. 在工程根目录下打开 env 工具,执行 menuconfig。
  2. 进入路径:RT-Thread Components → Command shell → FinSH shell。
  3. 把 Enable FinSH shell 勾上。

这里有几个关键选项,我列个表,你配置时对着看:

配置项说明我的建议
Enable FinSH shell总开关,必须打开不打开就没得玩
Set FinSH thread stack sizeFinSH 线程栈大小,默认 4096如果自定义命令里用了大数组,建议加到 8192
Set FinSH thread priorityFinSH 线程优先级,默认 20别设太高,否则影响实时任务
Using history feature历史记录功能强烈建议打开,后面会讲
Using tab completionTab 补全功能必须开,谁用谁知道

小提示: 配置完记得执行 pkgs --update 更新包,然后重新生成工程。我见过有人改了配置直接编译,结果报错找不到头文件,就是因为忘了这步。

2.2 命令注册:系统自带的那些“瑞士军刀”

FinSH 启动后,你敲 help 就能看到所有可用命令。系统自带了一批,我挑几个常用的说说:

  • list_thread:列出所有线程及其状态、优先级、栈使用情况。调试死机、栈溢出时,这是第一手信息。
  • list_mem:查看内存池使用情况。内存泄漏?跑一下这个命令,心里就有数了。
  • list_timer:列出所有软件定时器。定时器没触发?看看它还在不在列表里。
  • ps:进程状态,跟 Linux 的 ps 类似,显示线程列表。
  • free:查看堆内存剩余空间。

这些命令怎么来的?其实每个命令背后都是一个函数,通过 MSH_CMD_EXPORT 宏注册到 FinSH 的命令表里。比如 list_thread 的注册代码大概长这样:

#include <rtthread.h>

static void list_thread_func(int argc, char **argv)
{
    // 内部实现:遍历线程控制块链表,打印信息
    rt_list_thread();
}
MSH_CMD_EXPORT(list_thread_func, show thread list);

你看,宏 MSH_CMD_EXPORT 把函数名和描述字符串绑在一起,编译时自动塞进命令表。FinSH 启动后,就能通过名字找到这个函数并执行。

核心理解: FinSH 的命令注册机制,本质上是一个函数指针表 + 字符串匹配。你注册一个命令,就是在表里加一条记录。敲命令时,FinSH 遍历这个表,找到匹配的就调用对应的函数。

2.3 自定义命令编写:写一个属于你的调试工具

系统命令够用,但总有些场景需要定制。比如,你要读一个特定传感器的数据,或者复位某个外设。这时候,写个自定义命令就方便多了。

编写步骤:

  1. 写一个函数,原型必须是 void func_name(int argc, char **argv)。
  2. 用 MSH_CMD_EXPORT 宏导出。
  3. 重新编译、下载、运行。

我举个例子。假设你要读 STM32 内部温度传感器的值,可以这样写:

#include <rtthread.h>
#include "stm32f4xx_hal.h"

static void read_temp(int argc, char **argv)
{
    float temp;
    // 假设已经初始化了 ADC,这里直接读取
    HAL_ADC_Start(&hadc1);
    if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
    {
        uint32_t adc_val = HAL_ADC_GetValue(&hadc1);
        temp = ((float)adc_val - 0.76f) / 0.0025f + 25.0f;
        rt_kprintf("Internal temperature: %.2f °C\n", temp);
    }
    HAL_ADC_Stop(&hadc1);
}
MSH_CMD_EXPORT(read_temp, read STM32 internal temperature sensor);

编译下载后,在终端敲 read_temp,就能看到温度值。是不是很方便?

注意: 自定义命令里不要做阻塞太久的操作。FinSH 线程优先级不高,你如果在这里面 delay 几秒钟,其他高优先级任务可能会受影响。我曾经在命令里写了个 HAL_Delay(5000),结果整个系统响应变慢,排查了半天才发现是这里的问题。

2.4 历史记录与 Tab 补全:效率翻倍的小技巧

这两个功能,说白了就是让你少敲键盘。但别小看它们,用好了调试效率能翻倍。

历史记录:

FinSH 默认保存最近 10 条命令(可配置)。按键盘的 上/下方向键 就能翻出来。我调试时经常反复执行同一个命令,比如 list_thread,按两下上键就出来了,不用重新敲。

配置项在 menuconfig 里:FinSH shell → Set the history record number,默认 10,我一般改成 20。

Tab 补全:

这个更实用。你敲命令的前几个字母,按 Tab 键,FinSH 会自动补全。如果有多个匹配项,它会列出所有可能的命令。

比如你敲 list_ 然后按 Tab,终端会显示:

list_mem   list_thread   list_timer

你再敲个 t 再按 Tab,就自动补全成 list_thread。

为什么会这样?FinSH 内部维护了一个命令前缀树。你每按一次 Tab,它就遍历这个树,找到所有匹配的前缀。这个机制在命令很多时特别有用,不用记全名。

我的习惯: 调试时我很少完整敲命令,都是敲两三个字母然后 Tab 补全。既快又不容易敲错。特别是那些长命令,比如 list_memheap,手敲很容易漏字母。

2.5 知识结构图:FinSH 控制台全景

下面这张图把 FinSH 控制台的核心逻辑串起来了。从配置到命令注册,再到交互使用,一目了然。

FinSH 控制台核心逻辑 menuconfig 配置 使能 FinSH、设置栈/优先级 命令注册 MSH_CMD_EXPORT 宏 交互使用 Tab补全、历史记录 命令表(函数指针数组) list_thread → 函数指针 read_temp → 函数指针 自定义命令 → 函数指针 交互流程 用户输入 → 字符串解析 → 命令表查找 → 执行函数 Tab补全:前缀树匹配 | 历史记录:环形缓冲区

从图上你能看到,FinSH 的整个工作流程分三层:配置层决定功能开关,命令注册层把函数挂到命令表里,交互层负责接收输入、解析、查找和执行。Tab 补全和历史记录是交互层的两个“加速器”。

2.6 避坑指南:我踩过的几个坑

最后,分享几个实际项目中遇到的坑,你遇到了可以少走弯路。

  • 命令名冲突: 自定义命令不要跟系统命令重名。比如你写个 list_mem,系统也有一个,编译不会报错,但运行时只会执行其中一个。我建议自定义命令加个前缀,比如 my_。
  • 栈溢出: FinSH 线程栈默认 4096 字节。如果你在命令里用了局部大数组(比如 char buf[2048]),很容易栈溢出,导致系统崩溃。我曾经在命令里定义了一个 4KB 的缓冲区,结果一执行就 hard fault。排查了半天,最后把栈加到 8192 才解决。
  • 串口中断优先级: FinSH 依赖串口中断接收数据。如果串口中断优先级设得太低,被其他高优先级中断长时间屏蔽,会导致输入丢字符。我建议串口中断优先级设到 2 或 3(数值越小优先级越高),别用 0 和 1,那是给系统时钟和关键外设留的。