Linux内核与驱动调试基础:printk与动态调试、procfs与sysfs接口调试、设备树调试与验证、GPIO与UART驱动调试实战
调试内核和驱动,说白了就是跟内核「对话」。你得知道它现在在干什么,卡在哪里,数据对不对。我做了这么多年嵌入式,最深的体会就是:调试工具用得好,能省一半的加班时间。这一章,我把最常用的几招教给你。
2.1 printk:驱动调试的「Hello World」
printk 是内核版的 printf。你想想看,驱动跑在内核态,不能直接用 printf,那怎么输出信息?printk 就是干这个的。我个人习惯,写一个新驱动时,第一步就是在 probe 函数里加一句 printk,确认驱动被加载了。
日志级别与使用
printk 有 8 个日志级别,从 KERN_EMERG 到 KERN_DEBUG。级别越低,越紧急。默认情况下,只有级别小于 console_loglevel 的消息才会打印到控制台。
| 宏定义 | 级别值 | 含义 | 典型场景 |
|---|---|---|---|
| KERN_EMERG | 0 | 紧急事件 | 系统崩溃前 |
| KERN_ALERT | 1 | 必须立即处理 | 严重硬件故障 |
| KERN_CRIT | 2 | 临界条件 | 驱动加载失败 |
| KERN_ERR | 3 | 错误条件 | 寄存器读写异常 |
| KERN_WARNING | 4 | 警告 | 降级模式 |
| KERN_NOTICE | 5 | 正常但重要 | 驱动初始化完成 |
| KERN_INFO | 6 | 信息 | 版本号打印 |
| KERN_DEBUG | 7 | 调试信息 | 变量值跟踪 |
echo 8 > /proc/sys/kernel/printk 就行。发布前记得改回去,不然 dmesg 会被刷爆。
实际用法
// 基本用法
printk(KERN_INFO "my_driver: probe success, irq=%d\n", irq_num);
// 带函数名和行号,方便定位
printk(KERN_DEBUG "%s: %d: reg_val = 0x%08x\n", __func__, __LINE__, reg_val);
// 我曾经踩过的坑:printk 里用了 %lld 但参数是 u32,结果打印出乱码
// 查了半天才发现是格式符不匹配
printk(KERN_ERR "wrong: val = %lld\n", u32_var); // 错误!
printk(KERN_ERR "correct: val = %d\n", u32_var); // 正确
2.2 动态调试:不用重新编译就能开关日志
printk 有个痛点:调试信息太多时,刷屏严重;关掉又怕出问题时抓不到。动态调试就是解决这个问题的。它让你在运行时动态控制哪些文件的哪些函数打印日志。
开启动态调试
内核编译时需要打开 CONFIG_DYNAMIC_DEBUG。然后通过 debugfs 控制:
# 挂载 debugfs
mount -t debugfs none /sys/kernel/debug
# 查看当前所有动态调试开关
cat /sys/kernel/debug/dynamic_debug/control
# 打开 drivers/i2c/busses/i2c-imx.c 中所有调试信息
echo "file i2c-imx.c +p" > /sys/kernel/debug/dynamic_debug/control
# 只打开某个函数的调试信息
echo "func imx_i2c_readb +p" > /sys/kernel/debug/dynamic_debug/control
# 关闭所有调试信息
echo "file * -p" > /sys/kernel/debug/dynamic_debug/control
pr_debug()、dev_dbg() 等宏的代码有效。如果你用的是 printk(KERN_DEBUG ...),动态调试管不了。我刚开始就犯过这个错,折腾了半天才发现。
2.3 procfs 与 sysfs:用户态与内核态的桥梁
调试驱动时,经常需要从用户态读写内核数据。procfs 和 sysfs 就是干这个的。简单说:procfs 放进程和系统信息,sysfs 放设备和驱动信息。
procfs 调试接口
我习惯在驱动里创建一个 /proc/my_driver 文件,用来查看内部状态:
static int my_proc_show(struct seq_file *m, void *v) {
seq_printf(m, "irq_count: %d\n", irq_count);
seq_printf(m, "fifo_level: %d\n", fifo_level);
seq_printf(m, "last_error: 0x%x\n", last_error);
return 0;
}
static int my_proc_open(struct inode *inode, struct file *file) {
return single_open(file, my_proc_show, NULL);
}
static const struct proc_ops my_proc_fops = {
.proc_open = my_proc_open,
.proc_read = seq_read,
.proc_release = single_release,
};
// 在 probe 中创建
proc_create("my_driver", 0444, NULL, &my_proc_fops);
用户态直接 cat 就能看到:
# cat /proc/my_driver
irq_count: 1024
fifo_level: 3
last_error: 0x0
sysfs 属性文件
sysfs 更规范,每个设备一个 kobject,属性文件放在 /sys/devices/ 下。调试时我经常用 attribute 来读写寄存器:
static ssize_t reg_show(struct device *dev,
struct device_attribute *attr, char *buf) {
u32 val = readl(mydev->base + MY_REG);
return sprintf(buf, "0x%08x\n", val);
}
static ssize_t reg_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count) {
u32 val;
sscanf(buf, "%x", &val);
writel(val, mydev->base + MY_REG);
return count;
}
static DEVICE_ATTR(reg, 0644, reg_show, reg_store);
// 在 probe 中创建
device_create_file(dev, &dev_attr_reg);
用户态操作:
# 读寄存器
cat /sys/devices/platform/my_device/reg
# 写寄存器
echo 0x1234 > /sys/devices/platform/my_device/reg
2.4 设备树调试与验证
设备树(Device Tree)是嵌入式 Linux 的硬件描述语言。驱动能不能正确匹配硬件,全靠它。调试设备树,说白了就是确认「内核看到的硬件跟你描述的是不是一样」。
查看设备树
# 查看完整设备树(需要内核支持)
cat /proc/device-tree/
# 查看某个节点
ls /proc/device-tree/soc/
cat /proc/device-tree/soc/compatible
# 查看设备树源文件(如果 bootloader 传了 dtb)
dtc -I dtb -O dts /sys/firmware/fdt > /tmp/device_tree.dts
常见问题与验证方法
| 问题现象 | 可能原因 | 验证方法 |
|---|---|---|
| 驱动 probe 没调用 | compatible 不匹配 | 检查 /proc/device-tree/ 下节点的 compatible 属性 |
| GPIO 申请失败 | gpios 属性格式错误 | cat /sys/kernel/debug/gpio 查看 GPIO 使用情况 |
| 中断不触发 | interrupts 属性错误 | cat /proc/interrupts 查看中断号 |
| 时钟频率不对 | clocks 属性缺失 | cat /sys/kernel/debug/clk/clk_summary |
dtc -I dtb -O dts 反编译确认一下。
2.5 GPIO 与 UART 驱动调试实战
理论讲完了,咱们来点实际的。GPIO 和 UART 是最常用的外设,也是调试最多的。
GPIO 调试三板斧
- 查看 GPIO 状态:
cat /sys/kernel/debug/gpio能看到所有 GPIO 的申请者、方向和电平。 - 用户态操作:通过 sysfs 或 libgpiod 直接控制 GPIO,验证硬件连接。
- 逻辑分析仪:实在不行就上逻辑分析仪,看波形对不对。
# 通过 sysfs 操作 GPIO(旧方法,但调试时很好用)
echo 120 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio120/direction
echo 1 > /sys/class/gpio/gpio120/value
# 通过 libgpiod(新方法)
gpioset gpiochip0 120=1
gpioget gpiochip0 120
UART 调试要点
UART 驱动调试,核心是验证数据能不能正确收发。我一般按这个顺序排查:
- 检查设备树:确认 uart 节点 status = "okay",pinctrl 配置正确。
- 查看串口设备:
ls /dev/ttyS*或ls /dev/ttyAMA*,确认设备节点存在。 - 回环测试:把 TX 和 RX 短接,用
echo和cat测试自发自收。 - 查看中断:
cat /proc/interrupts | grep uart,确认中断在收发时增加。 - 波特率验证:用示波器测 TX 引脚,看波形周期是否匹配。
# 回环测试(硬件上短接 TX 和 RX)
stty -F /dev/ttyS0 115200 cs8 -cstopb -parenb
echo "hello uart" > /dev/ttyS0
cat /dev/ttyS0 &
# 如果能收到 "hello uart",说明基本通路没问题
ttyS0: 3 input overrun(s) 之类的警告。这通常意味着波特率不匹配或流控没配好。我遇到过好几次,都是因为设备树里 uart-has-rtscts 配错了,导致硬件流控乱拉。
小结
这一章的内容,说白了就是让你掌握内核调试的「基本功」。printk 是入门,动态调试是进阶,procfs/sysfs 是用户态交互的窗口,设备树是硬件描述的桥梁,GPIO 和 UART 是实战练兵场。把这些工具用熟了,大部分驱动问题都能快速定位。