Linux内核与驱动调试基础:printk与动态调试、procfs与sysfs接口调试、设备树调试与验证、GPIO与UART驱动调试实战

调试内核和驱动,说白了就是跟内核「对话」。你得知道它现在在干什么,卡在哪里,数据对不对。我做了这么多年嵌入式,最深的体会就是:调试工具用得好,能省一半的加班时间。这一章,我把最常用的几招教给你。

本章核心脉络:从最基础的printk打日志,到动态调试开关,再到procfs/sysfs的用户态接口,最后落到设备树和具体外设驱动。一层层递进,让你遇到问题知道从哪下手。
Linux内核与驱动调试知识体系 内核/驱动调试 printk 日志 动态调试 procfs/sysfs 设备树调试 日志级别 控制台过滤 debugfs控制 动态使能/关闭 读写接口 属性文件 dtc反编译 GPIO模拟验证 UART回环测试 从printk入门 → 动态调试精细化 → procfs/sysfs用户态交互 → 设备树验证 → 外设驱动实战

2.1 printk:驱动调试的「Hello World」

printk 是内核版的 printf。你想想看,驱动跑在内核态,不能直接用 printf,那怎么输出信息?printk 就是干这个的。我个人习惯,写一个新驱动时,第一步就是在 probe 函数里加一句 printk,确认驱动被加载了。

日志级别与使用

printk 有 8 个日志级别,从 KERN_EMERG 到 KERN_DEBUG。级别越低,越紧急。默认情况下,只有级别小于 console_loglevel 的消息才会打印到控制台。

宏定义级别值含义典型场景
KERN_EMERG0紧急事件系统崩溃前
KERN_ALERT1必须立即处理严重硬件故障
KERN_CRIT2临界条件驱动加载失败
KERN_ERR3错误条件寄存器读写异常
KERN_WARNING4警告降级模式
KERN_NOTICE5正常但重要驱动初始化完成
KERN_INFO6信息版本号打印
KERN_DEBUG7调试信息变量值跟踪
我的习惯:开发阶段把 console_loglevel 设成 8,所有调试信息都能看到。用 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
调试技巧:我经常用 sysfs 来模拟中断触发。写一个 attribute,store 函数里直接调用中断处理函数,这样不用硬件就能验证中断处理逻辑对不对。

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
我曾经踩过的坑:设备树里 reg 属性写了两个地址范围,但驱动只解析了第一个。结果第二个内存区域没映射,访问就崩溃。后来我养成习惯,每次改设备树都用 dtc -I dtb -O dts 反编译确认一下。

2.5 GPIO 与 UART 驱动调试实战

理论讲完了,咱们来点实际的。GPIO 和 UART 是最常用的外设,也是调试最多的。

GPIO 调试三板斧

  1. 查看 GPIO 状态:cat /sys/kernel/debug/gpio 能看到所有 GPIO 的申请者、方向和电平。
  2. 用户态操作:通过 sysfs 或 libgpiod 直接控制 GPIO,验证硬件连接。
  3. 逻辑分析仪:实在不行就上逻辑分析仪,看波形对不对。
# 通过 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
实战案例:有一次客户说设备偶尔死机,我怀疑是 GPIO 中断冲突。在中断处理函数里加了个 GPIO 翻转,用示波器看波形,发现中断频率远高于预期。最后定位到是设备树里 interrupt 属性配错了,触发了错误的中断号。

UART 调试要点

UART 驱动调试,核心是验证数据能不能正确收发。我一般按这个顺序排查:

  1. 检查设备树:确认 uart 节点 status = "okay",pinctrl 配置正确。
  2. 查看串口设备:ls /dev/ttyS* 或 ls /dev/ttyAMA*,确认设备节点存在。
  3. 回环测试:把 TX 和 RX 短接,用 echo 和 cat 测试自发自收。
  4. 查看中断:cat /proc/interrupts | grep uart,确认中断在收发时增加。
  5. 波特率验证:用示波器测 TX 引脚,看波形周期是否匹配。
# 回环测试(硬件上短接 TX 和 RX)
stty -F /dev/ttyS0 115200 cs8 -cstopb -parenb
echo "hello uart" > /dev/ttyS0
cat /dev/ttyS0 &
# 如果能收到 "hello uart",说明基本通路没问题
注意:UART 调试时,dmesg 里经常会出现 ttyS0: 3 input overrun(s) 之类的警告。这通常意味着波特率不匹配或流控没配好。我遇到过好几次,都是因为设备树里 uart-has-rtscts 配错了,导致硬件流控乱拉。

小结

这一章的内容,说白了就是让你掌握内核调试的「基本功」。printk 是入门,动态调试是进阶,procfs/sysfs 是用户态交互的窗口,设备树是硬件描述的桥梁,GPIO 和 UART 是实战练兵场。把这些工具用熟了,大部分驱动问题都能快速定位。