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
验证步骤:
- 检查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
- 检查设备节点:
# ls -l /dev/sd*
brw-rw---- 1 root root 8, 0 Jan 1 00:00 /dev/sda
- 挂载测试:
# mount /dev/sda1 /mnt
# ls /mnt
# df -h
如果这三步都通了,恭喜你,内核配置和DTS基本没问题。
验证清单:
- ☐ dmesg中USB控制器注册成功(无error/warning)
- ☐ 插入U盘后出现
/dev/sd*设备节点 - ☐ 能正常挂载并读写文件
- ☐ 拔插U盘,dmesg有对应的断开/连接事件
4.4 知识结构图
下面这张图把本章的核心流程串起来了:
这张图把三个步骤和各自的子任务都列出来了。你对照着做,基本不会漏东西。
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是否生效的快速方法。