第3章:调试环境搭建——内核配置、串口日志与动态调试

做USB OTG驱动调试,环境没搭好,后面全是白忙活。
我见过不少新手,代码改了一堆,结果日志打不出来,或者内核根本没把OTG功能编译进去。
今天咱们就把这三件事儿一次性搞定:内核配置选项、串口日志输出、动态调试(debugfs)启用。

3.1 先搞定内核配置:CONFIG_USB_OTG 及相关选项

这一步是基础,说白了就是让内核知道,你要用OTG功能。
我个人习惯,在正式调试前,先把内核 .config 文件翻一遍,确保相关宏都开了。

核心选项如下表:

配置宏 意义 建议值
CONFIG_USB_OTG USB OTG 核心支持 y
CONFIG_USB_OTG_FSM OTG 有限状态机(驱动核心逻辑) y
CONFIG_USB_OTG_WHITELIST OTG 设备白名单过滤 按需选 y
CONFIG_USB_OTG_BLACKLIST_HUB 禁止 OTG 端口接 Hub y(建议启用)
CONFIG_USB_OTG_PRODUCTINFO 产品信息字符串传递 y
CONFIG_USB_GADGET USB Gadget 框架(设备侧) y 或 m
CONFIG_USB_DWC3 DWC3 控制器驱动(常见平台) y 或 m
CONFIG_USB_DWC3_OTG DWC3 OTG 模式支持 y

核心要点:

  • CONFIG_USB_OTG_FSM 不开,状态机不跑,HNP/SRP 协议直接罢工。
  • CONFIG_USB_OTG_BLACKLIST_HUB 建议开启,我遇到过因为插了 Hub 导致角色协商失败的情况,排查了好久才发现是 Hub 干扰了 ID 中断时序。

配置方法(以 dwc3 平台为例):

# 进入内核源码目录
make menuconfig

# 依次进入
Device Drivers → USB support → USB Gadget Support
  → [*] USB Gadget Drivers
  → <*> DesignWare USB3 Core Support
  → [*] DesignWare USB3 DRD Mode Support

# OTG 相关通常在
Device Drivers → USB support
  → [*] USB OTG Support
  → [*] USB OTG FSM

嗯,这里要注意:不同厂商的 SoC(比如 TI、NXP、Rockchip)可能还有自己的平台配置宏。
建议在 defconfig 或 fragment 里一次性加好,避免每次 menuconfig 漏选。

小技巧:

我习惯在 .config 文件里直接搜索“OTG”、“DWC3”、“GADGET”,确认所有相关项都被编入内核(=y)。如果是模块(=m),记得确保开机时能正确装载,否则调试时可能发现驱动没在运行。

3.2 串口日志输出:调试第一通道

OTG 驱动调试,串口日志是救命稻草。
为什么呢?因为 OTG 角色切换、ID 中断、VBUS 变化这些事件,其它接口(比如网络、adb)可能根本不可用。
说白了,串口是你最后还能看到“系统在干什么”的窗口。

我一般这么配置:

  1. 内核日志级别调低,让更多信息刷到串口:

    # 在 kernel cmdline 里加:
    loglevel=8 console=ttyS0,115200n8
  2. 开启 USB 子系统日志:

    # 在 /etc/sysctl.conf 或 sysctl -w 中设置:
    kernel.printk = 7 4 1 7
    
    # 动态调整:
    echo "file drivers/usb/* +p" > /sys/kernel/debug/dynamic_debug/control
  3. 针对 Gadget/OTG 专用日志开关:

    # 在 drivers/usb/gadget/ 下,可以通过 module_param 打开 verbose:
    modprobe dwc3.ko debug=1
    # 或写入 /sys/module/dwc3/parameters/debug

避坑指南:

我曾经遇到过一款平台,串口被其他模块(比如蓝牙)复用了。导致 OTG 驱动在角色切换时,串口突然失效,日志全丢。最后不得不通过 GPIO 拉出第二个串口,才抓到关键信息。
所以,调试前一定确认串口线路没有被其它外设抢占。

常用的 USB 相关日志标签(dmesg 过滤用):

# 过滤 OTG 相关
dmesg | grep -iE "otg|fsm|hnp|srp|id|vbus"

# 过滤 Gadget/UDC
dmesg | grep -iE "gadget|udc|dwc3"

# 实时追踪
watch -n 0.5 "dmesg | tail -50 | grep -iE 'otg|role|switch'"

3.3 动态调试(dynamic debug / debugfs)

内核的动态调试框架,是调试驱动的神器。
你不用重新编译内核,就能打开或关闭某个文件的调试信息。
尤其适合像 OTG 这种状态机复杂的驱动,你根据现场现象,动态决定要看哪段代码的 log。

启用 debugfs:

# 确保内核选项已开
CONFIG_DEBUG_FS=y
CONFIG_DYNAMIC_DEBUG=y

# 挂载 debugfs(通常自动挂载,手动也可以)
mount -t debugfs none /sys/kernel/debug

常用动态调试命令示例:

# 打开 drivers/usb/dwc3/core.c 里所有 pr_debug/dev_dbg 输出
echo "file drivers/usb/dwc3/core.c +p" > /sys/kernel/debug/dynamic_debug/control

# 打开 drivers/usb/gadget/ 目录下所有文件的调试日志
echo "file drivers/usb/gadget/* +p" > /sys/kernel/debug/dynamic_debug/control

# 只打开 otg_fsm.c 里包含 "state" 的调试语句
echo "file drivers/usb/common/otg_fsm.c line * state* +p" > /sys/kernel/debug/dynamic_debug/control

# 查看当前已打开哪些动态调试规则
cat /sys/kernel/debug/dynamic_debug/control | grep dwc3

我个人的排查习惯:

  1. 先挂载 debugfs,确认路径存在。
  2. 打开待调文件的所有动态调试。
  3. 如果日志太多,再配合 grep -v 过滤噪声。
  4. 对于 OTG 状态机,我通常会同时打开 otg_fsm.c 和 dwc3_otg.c 的调试,因为它们处理的是同一事件的不同层级。

为什么要特别强调 dynamic debug?
因为在 OTG 调试中,时序就是一切。
ID 中断来了,状态机做出反应,VBUS 变化,然后又触发中断。
如果每个细节都 printk,你会被信息淹没;如果都不打,你什么都看不到。
动态调试让你精准定位,只在你想看的代码路径上输出信息。

3.4 本章知识体系总览

下面这张图,是我梳理的整套调试环境依赖关系:

USB OTG 调试环境搭建结构图 调试环境搭建 - 核心依赖关系 1. 内核配置 (CONFIG_USB_OTG等) 2. 串口日志输出 3. debugfs + 动态调试 console=ttyS0 loglevel=8 module_param(debug=1) echo "file ... +p" 动态开/关调试信息 精准打印 & 过滤 目标: 看清 OTG 状态机 & 角色切换 提示:三个环节缺一不可,且建议按“内核配置→串口→debugfs”顺序搭建

有了这三样东西——正确配置的内核、可靠的串口、动态调试能力——
你就有了“看清 OTG 内部运转”的眼睛。
后面章节讲的所有具体调试案例,都是在这个环境基础上展开的。

另外,别小看 debugfs 的动态调试功能。
我在调试某厂商平台的 OTG HNP 切换时,就是靠动态调试临时打开了 dwc3 中断处理函数的日志,才抓到了 ID 中断丢失的问题。
如果没有动态调试,我要么重新编译内核,要么加一堆全局 printk 然后还得删掉——太折腾了。