4. 协议栈移植第一步:内核配置裁剪与设备树编写

好,咱们正式开始动手了。这一章要做的事,说白了就是三件:把内核里用不到的USB功能关掉、在设备树里把USB控制器描述清楚、编译烧录验证能不能跑起来。这三步走顺了,后面移植协议栈才有底。

我记得刚带团队做第一个USB主机项目时,内核配置直接用了默认的defconfig,烧进去发现枚举卡死。查了两天才发现——EHCI中断号和设备树里写的不一致。嗯,这种坑踩过一次就记住了。

4.1 内核裁剪:只留你需要的

Linux内核的USB子系统很庞大。从主机控制器驱动(HCD)到设备类驱动,再到各种协议层,全编进去的话,镜像体积能翻一倍。嵌入式设备Flash本来就金贵,裁剪是必须的。

我个人习惯用make menuconfig,路径是:

Device Drivers  --->
    [*] USB support  --->
        <*>   Support for Host-side USB
        [*]   USB device filesystem
        [ ]   USB verbose debug messages
        <*>   EHCI HCD (USB 2.0) support
        <*>   OHCI HCD (USB 1.1) support
        < >   UHCI HCD (most Intel/VIA) support
        <*>   USB Mass Storage support
        < >   USB HIDBP keyboards (basic) support
        < >   USB HIDBP mice (basic) support

这里有几个关键点:

  • Host-side USB:必须选上,否则整个主机功能都没有。
  • HCD选择:根据你的硬件来。我做过一个i.MX6ULL的项目,它内置两个控制器——一个EHCI(高速)和一个OHCI(全速/低速)。两个都编进去,运行时通过设备树动态绑定。
  • USB Mass Storage:如果你只需要U盘读写,这个就够了。别把HID、CDC ACM、打印机驱动全勾上,除非你真的需要。
  • USB verbose debug:调试阶段可以开,量产前一定关掉。否则dmesg会被刷爆。
我的经验:裁剪时先做最小集——只选HCD + Mass Storage。烧进去验证枚举和读写正常后,再按需加其他类驱动。这样出问题时定位范围小很多。

4.2 设备树(DTS)中的USB节点编写

设备树是嵌入式Linux里描述硬件的“语言”。USB控制器在DTS里通常是一个节点,包含寄存器地址、中断号、时钟、复位等信息。

以我常用的NXP i.MX6ULL为例,它的USB控制器节点长这样:

&usbotg1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_usbotg1>;
    dr_mode = "otg";
    srp-disable;
    hnp-disable;
    adp-disable;
    status = "okay";
};

&usbotg2 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_usbotg2>;
    dr_mode = "host";
    status = "okay";
};

这里要注意几个属性:

属性 含义 常见取值
dr_mode 工作模式 host / peripheral / otg
srp-disable 关闭会话请求协议 纯主机时可关闭
hnp-disable 关闭角色切换协议 纯主机时可关闭
status 节点使能 okay / disabled
我曾经踩过的坑:有一次把dr_mode写成了"host",但硬件上USB_ID引脚没接。结果内核在OTG驱动初始化时卡在ID检测上,等了5秒超时才回退到主机模式。后来直接改成dr_mode = "host"并关闭OTG相关功能,启动时间缩短了4秒。

另外,时钟和复位也不能漏。很多SoC的USB控制器需要先开启时钟才能访问寄存器:

&usbotg1 {
    clocks = <&clks IMX6UL_CLK_USBOH3>;
    clock-names = "usboh3";
    assigned-clocks = <&clks IMX6UL_CLK_USBOH3>;
    assigned-clock-parents = <&clks IMX6UL_CLK_PLL3_60M>;
};

时钟频率不对,控制器根本没法正常工作。我见过一个案例,同事把PLL配成了480MHz给USB模块,结果EHCI寄存器读出来全是0xDEADBEEF——嗯,这种错误很隐蔽。

4.3 编译与烧录验证

配置和DTS都改好后,就是编译、烧录、验证三板斧。

编译命令大致如下:

# 配置内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

# 编译内核和设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

# 生成的文件在 arch/arm/boot/ 下
# zImage 和对应板子的 .dtb 文件

烧录方式取决于你的板子。我习惯用TFTP + NFS组合,调试效率高:

# U-Boot 下设置
setenv serverip 192.168.1.100
setenv ipaddr 192.168.1.10
setenv bootcmd 'tftp 0x80800000 zImage; tftp 0x83000000 imx6ull-myboard.dtb; bootz 0x80800000 - 0x83000000'
saveenv
boot

验证步骤:

  1. 检查dmesg:看USB控制器是否被正确识别。
# dmesg | grep -i usb
[    1.234567] usb 1-1: new high-speed USB device number 2 using ehci-platform
[    1.345678] usb-storage 1-1:1.0: USB Mass Storage device detected
  1. 检查设备节点:
# ls -l /dev/sd*
brw-rw---- 1 root root 8, 0 Jan  1 00:00 /dev/sda
  1. 挂载测试:
# mount /dev/sda1 /mnt
# ls /mnt
# df -h

如果这三步都通了,恭喜你,内核配置和DTS基本没问题。

验证清单:
  • ☐ dmesg中USB控制器注册成功(无error/warning)
  • ☐ 插入U盘后出现/dev/sd*设备节点
  • ☐ 能正常挂载并读写文件
  • ☐ 拔插U盘,dmesg有对应的断开/连接事件

4.4 知识结构图

下面这张图把本章的核心流程串起来了:

USB协议栈移植第一步:内核配置裁剪与设备树编写 内核配置裁剪 menuconfig → 最小集 设备树节点编写 DTS → 属性配置 编译烧录验证 zImage + dtb → 测试 HCD选择(EHCI/OHCI/UHCI) 根据硬件选,不要全编 类驱动裁剪(Mass Storage / HID) 按需添加,最小集优先 调试选项开关 调试开,量产关 dr_mode / srp / hnp 配置 纯主机时关闭OTG协议 时钟与复位 频率不对 → 寄存器全0 引脚复用(pinctrl) USB_DP/DN/ID/VBUS dmesg检查 控制器注册 + 枚举事件 设备节点检查 /dev/sd* 出现 读写测试 mount + dd + umount 核心原则 1. 裁剪做减法:只编需要的,减少镜像体积和启动时间 2. DTS做加法:每个属性都要确认,特别是时钟和中断 3. 验证做闭环:dmesg → 设备节点 → 读写测试,缺一不可

这张图把三个步骤和各自的子任务都列出来了。你对照着做,基本不会漏东西。

4.5 常见问题与避坑

最后分享几个我实际项目中遇到过的问题:

  • 内核配置了但没生效:检查.config文件,确认选项是=y(编入内核)还是=m(模块)。嵌入式环境我建议全编入内核,省去模块加载的麻烦。
  • DTS编译报错:语法错误最常见——少了个分号、引号不匹配。用make dtbs编译时仔细看错误信息,它会告诉你哪一行有问题。
  • 烧录后系统起不来:先确认DTS中status = "okay"有没有写。我见过有人复制节点时忘了改状态,结果控制器根本没使能。
  • U盘插上没反应:先量VBUS有没有5V。很多板子需要GPIO控制VBUS使能,DTS里漏了vbus-supply属性。
调试小技巧:在DTS中给USB节点加一个status = "okay"后,如果dmesg里还是看不到控制器,可以手动cat /sys/firmware/devicetree/base/soc/...查看对应节点的属性是否被正确解析。这是验证DTS是否生效的快速方法。