4、Shell调试子系统:Shell配置与启用、自定义命令开发、内存查看命令、线程信息查看
Shell调试子系统,说白了就是Zephyr的「人机交互窗口」。你想想看,嵌入式开发最痛苦的是什么?是看不到系统内部的状态。代码跑起来了,但到底跑得对不对?内存有没有泄漏?线程有没有死锁?全靠猜。有了Shell,这些问题就迎刃而解了。
我个人习惯,拿到一个新板子,第一件事就是把Shell打开。为什么?因为后面所有的调试工作,几乎都离不开它。今天我们就来聊聊,怎么用好这个强大的工具。
4.1 Shell配置与启用
启用Shell其实很简单,但有几个坑,我踩过好几次。先看最基础的配置:
# prj.conf 中的配置
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_SHELL_CMD_BUFFER_SIZE=256
CONFIG_SHELL_HISTORY=10
嗯,这里要注意。第一行是总开关,第二行是选择串口作为交互通道。后面两个参数,我建议你根据实际需求调整。
还有一个容易被忽略的配置:
CONFIG_SHELL_STATS=y
这个打开后,你可以看到Shell命令的执行时间统计。我在做性能调优时,经常用它来快速定位哪个命令响应慢。
配置完成后,编译烧录。用串口工具连上,按回车,你应该能看到Shell提示符:
uart:~$
如果没出来,别慌。我曾经遇到过一个问题:串口波特率不匹配。Zephyr默认用115200,但有些开发板的bootloader改了波特率。检查一下你的终端设置。
4.2 自定义命令开发
内置命令虽然好用,但真正体现Shell威力的,是自定义命令。你可以把调试用的函数,直接注册成Shell命令,随时调用。
先看一个最简单的例子:
#include <shell/shell.h>
static int cmd_hello(const struct shell *shell, size_t argc, char **argv)
{
shell_print(shell, "Hello from Zephyr!");
return 0;
}
SHELL_CMD_REGISTER(hello, NULL, "Say hello", cmd_hello);
编译烧录后,在Shell里输入hello,就能看到输出。就这么简单。
但实际项目中,命令往往需要参数。比如我想查看某个内存地址的值:
static int cmd_peek(const struct shell *shell, size_t argc, char **argv)
{
uint32_t addr;
uint32_t val;
if (argc < 2) {
shell_error(shell, "Usage: peek <address>");
return -EINVAL;
}
addr = strtoul(argv[1], NULL, 16);
val = *(volatile uint32_t *)addr;
shell_print(shell, "0x%08x: 0x%08x", addr, val);
return 0;
}
SHELL_CMD_REGISTER(peek, NULL, "Read 32-bit value from address", cmd_peek);
strtoul比atoi更安全,因为它能处理十六进制。
命令多了之后,建议用子命令来组织。比如:
SHELL_STATIC_SUBCMD_SET_CREATE(sub_memory,
SHELL_CMD(read, NULL, "Read memory", cmd_mem_read),
SHELL_CMD(write, NULL, "Write memory", cmd_mem_write),
SHELL_SUBCMD_SET_END
);
SHELL_CMD_REGISTER(mem, &sub_memory, "Memory operations", NULL);
这样,你就可以用mem read 0x20000000和mem write 0x20000000 0x12345678来操作内存了。结构清晰,也方便扩展。
4.3 内存查看命令
Zephyr内置了一个非常好用的内存查看命令——devmem。但说实话,它只能看物理地址。对于调试来说,我们更关心的是堆内存的使用情况。
我写了一个小工具,用来查看堆内存的分配情况:
static int cmd_heap_info(const struct shell *shell, size_t argc, char **argv)
{
struct k_mem_slab *slab;
struct k_heap *heap;
heap = k_heap_get(&_system_heap);
if (heap) {
shell_print(shell, "System heap:");
shell_print(shell, " Total: %zu bytes", heap->size);
shell_print(shell, " Free: %zu bytes", k_heap_free_size(heap));
shell_print(shell, " Used: %zu bytes", heap->size - k_heap_free_size(heap));
}
return 0;
}
SHELL_CMD_REGISTER(heapinfo, NULL, "Show heap memory info", cmd_heap_info);
这个命令能帮你快速判断内存是否泄漏。我在一次项目中,发现系统运行几天后突然卡死。用heapinfo一看,空闲内存从32KB降到了2KB。嗯,明显有地方在持续分配但不释放。
另外,mem命令也很有用。你可以直接查看任意地址的内容:
uart:~$ mem read 0x20000000 64
0x20000000: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
0x20000010: 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
...
这个命令在调试DMA传输、验证数据缓冲区内容时,简直是神器。
4.4 线程信息查看
多线程系统里,最怕的就是线程死锁或者优先级反转。Zephyr的kernel threads命令,能帮你把当前所有线程的状态扒个底朝天。
输入命令:
uart:~$ kernel threads
输出大概长这样:
| 线程名 | 优先级 | 状态 | 栈使用 | 运行时间 |
|---|---|---|---|---|
| idle | -1 | 就绪 | 12% | 12345ms |
| main | 0 | 运行 | 45% | 6789ms |
| uart_rx | 2 | 挂起 | 78% | 234ms |
这里有几个关键信息:
- 栈使用:如果超过80%,就要小心栈溢出了。我一般会留30%的余量。
- 状态:如果某个线程长期处于「挂起」状态,可能是信号量没等到,或者消息队列满了。
- 运行时间:可以帮你发现哪个线程占用了过多的CPU。
printk,导致栈空间被大量占用。改成用消息队列传递数据后,栈使用率降到了30%。
如果你想看更详细的线程信息,可以用:
uart:~$ kernel threads verbose
这个命令会显示每个线程的寄存器状态、等待的内核对象等。在调试死锁时特别有用。比如,你可以看到线程A在等待信号量S,而线程B持有信号量S但被线程C阻塞了。一眼就能看出问题所在。
嗯,最后提醒一句。Shell虽然好用,但别在生产固件里留太多调试命令。我一般会在发布前,通过Kconfig把调试相关的命令全部关掉。毕竟,安全第一。