2. RT-Thread与STM32开发环境搭建
调试嵌入式系统,说白了就是跟黑盒子打交道。你写了个逻辑,跑起来对不对?中断有没有触发?外设寄存器写进去了没?这些问题,总不能每次都靠点灯、串口打印来猜吧?
FinSH 就是 RT-Thread 给咱们开的一扇窗。它是个命令行交互工具,跑在串口上。你连上终端,敲几个命令,就能直接看系统状态、调变量、甚至执行函数。我个人觉得,这是 RT-Thread 最实用的调试手段之一,没有它,调试效率至少打五折。
2.1 组件配置:让 FinSH 跑起来
FinSH 不是默认就有的。你得先在工程里把它“请”出来。RT-Thread 用 menuconfig 做配置,图形化界面,勾勾选选就行。
配置步骤:
- 在工程根目录下打开 env 工具,执行
menuconfig。 - 进入路径:
RT-Thread Components → Command shell → FinSH shell。 - 把
Enable FinSH shell勾上。
这里有几个关键选项,我列个表,你配置时对着看:
| 配置项 | 说明 | 我的建议 |
|---|---|---|
Enable FinSH shell | 总开关,必须打开 | 不打开就没得玩 |
Set FinSH thread stack size | FinSH 线程栈大小,默认 4096 | 如果自定义命令里用了大数组,建议加到 8192 |
Set FinSH thread priority | FinSH 线程优先级,默认 20 | 别设太高,否则影响实时任务 |
Using history feature | 历史记录功能 | 强烈建议打开,后面会讲 |
Using tab completion | Tab 补全功能 | 必须开,谁用谁知道 |
小提示: 配置完记得执行 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 自定义命令编写:写一个属于你的调试工具
系统命令够用,但总有些场景需要定制。比如,你要读一个特定传感器的数据,或者复位某个外设。这时候,写个自定义命令就方便多了。
编写步骤:
- 写一个函数,原型必须是
void func_name(int argc, char **argv)。 - 用
MSH_CMD_EXPORT宏导出。 - 重新编译、下载、运行。
我举个例子。假设你要读 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 的整个工作流程分三层:配置层决定功能开关,命令注册层把函数挂到命令表里,交互层负责接收输入、解析、查找和执行。Tab 补全和历史记录是交互层的两个“加速器”。
2.6 避坑指南:我踩过的几个坑
最后,分享几个实际项目中遇到的坑,你遇到了可以少走弯路。
- 命令名冲突: 自定义命令不要跟系统命令重名。比如你写个
list_mem,系统也有一个,编译不会报错,但运行时只会执行其中一个。我建议自定义命令加个前缀,比如my_。 - 栈溢出: FinSH 线程栈默认 4096 字节。如果你在命令里用了局部大数组(比如
char buf[2048]),很容易栈溢出,导致系统崩溃。我曾经在命令里定义了一个 4KB 的缓冲区,结果一执行就 hard fault。排查了半天,最后把栈加到 8192 才解决。 - 串口中断优先级: FinSH 依赖串口中断接收数据。如果串口中断优先级设得太低,被其他高优先级中断长时间屏蔽,会导致输入丢字符。我建议串口中断优先级设到 2 或 3(数值越小优先级越高),别用 0 和 1,那是给系统时钟和关键外设留的。