
这篇文章整理自课程第 11-12 章。SPI 和 USB 一个强调板上总线与设备树,一个强调枚举与主机/设备双视角,合在一起很适合建立"总线 + 设备 + 驱动 + 用户接口"的整体感觉。
源文件地址:git clone https://e.coding.net/weidongshan/linux/doc_and_source_for_drivers.git
11_SPI 学习总结

TL;DR:
- 对象关系链:
SPI 协议 -> 控制器/设备/驱动对象关系 -> 设备树父子节点 -> spidev 作为验证层 -> 专用设备驱动 -> framebuffer 显示接口 -> master/slave 控制器角色- 学习顺序链:
SPI 协议 -> 对象关系 -> 设备树 -> spidev 验证 -> 专用设备驱动 -> framebuffer -> master/slave
章节定位
11_SPI 是这套课里非常关键的一章,因为它第一次把"总线协议、设备树、用户态验证、设备驱动、子系统改造、控制器驱动"放进了同一条学习链里。
它和前后章节的关系可以这样看:
- 往前承接
04_I2C:两者都是"控制器 + 设备 + 驱动"的总线模型,但 SPI 更强调片选、时序模式和一次传输里的收发关系。 - 往前承接
06_Pinctrl、07_GPIO:SPI 控制器能不能工作,先取决于引脚复用;OLED 里的dc-gpios又把 GPIO 控制带回来了。 - 往前承接
03_LCD:这一章后半段把 SPI OLED 改造成 framebuffer,本质上是在把一个小屏幕重新接进 Linux 显示抽象里。 - 往后衔接
12_USB、13_V4L2:从这一章开始,你不该只盯寄存器,而要习惯从"总线/子系统/设备模型"的角度理解驱动。
一句话概括:这一章是在解释 Linux 怎样把"一个按时钟移位的串行总线",接成"用户态可验证、设备树可描述、驱动可扩展、控制器角色可切换"的 SPI 子系统。
命名澄清
先把两个最容易混掉的名字钉住:
- 课程示例代码里有些函数沿用了
spidev_*这种命名,但本文统一把它们称为 SPI 设备驱动 。是否是专用驱动,关键看它是不是围绕某个具体 SPI 设备去实现spi_driver,而不是看函数名叫不叫spidev_*。 - 驱动外部 SPI 从设备 和 控制器进入 slave mode 不是一件事。前者仍然是"本机当 master,通过总线访问外设";后者是"本机控制器自己变成从设备,被别人访问"。
学习主线建议
这一章资料很多,如果一上来就读 virtual_spi_master.c 或者硬啃 spi_message,初学者通常会乱。建议严格按下面顺序学:
- 先看协议和对象关系
看01_SPI视频概述.md、02_SPI协议介绍.md、03_SPI总线设备驱动模型.md。
先只解决 4 个问题:- SPI 为什么至少要有
MOSI/MISO/SCK/CS CPOL/CPHA为什么会形成 4 种模式- Linux 里谁是控制器,谁是设备,谁是驱动
- 为什么 SPI 设备驱动自己不直接"碰硬件时钟",而是调用控制器提供的传输能力
- SPI 为什么至少要有
- 再看设备树怎么把 SPI 设备变成内核对象
看04_SPI设备树处理过程.md。
这一步的目标不是背属性,而是分清:- SPI 控制器节点描述的是
spi_controller/spi_master - 控制器子节点描述的是
spi_device compatible/reg/spi-max-frequency为什么几乎总是必选
- SPI 控制器节点描述的是
- 再用
spidev做最低成本验证
看05_spidev的使用(SPI用户态API).md,然后先做 DAC,再做 OLED。
这一步的目标是先证明:板子、设备树、时序、片选、频率都对了。 - 再写真正的 SPI 设备驱动
看11_编写SPI设备驱动程序.md,然后读 DAC/OLED 驱动示例。
这一步要把"用户态直接调spidev"升级成"内核里有自己的spi_driver和字符设备接口"。 - 再看 framebuffer 改造
看16_使用Framebuffer改造OLED驱动.md。
这一步最重要的不是 API 细节,而是理解:为什么要把"面向 OLED 页寻址的驱动"改成"面向/dev/fbX的通用显示接口"。 - 最后再看 SPI master/slave
看18到24。
这一步学的是"怎么写控制器驱动"和"主从角色变化后,控制器驱动的思维方式为什么会变"。
如果只保留一条最短主线,请记住:
SPI 协议 -> 对象关系 -> 设备树 -> spidev 验证 -> 专用设备驱动 -> framebuffer -> master/slave
学习分层
为了降低路径焦虑,可以先把这一章拆成 3 条线:
- 过关线:学到
SPI 协议 -> 对象关系 -> 设备树 -> spidev 验证 -> 专用设备驱动。走到这里,你已经能独立调通一个常见 SPI 外设。 - 提升线:继续学
framebuffer改造。走到这里,你开始从"会驱动一个设备"升级到"会把设备接进 Linux 子系统接口"。 - 扩展线:最后再看
master/slave。这部分更偏控制器驱动和角色切换,适合作为后续提升内容,不是初学者的第一过关点。
SPI 关键层次与对象
先记一句全章最关键的话:
SPI 设备驱动不直接"实现 SPI 总线",它描述的是这个设备怎么说话;真正负责收发的是 SPI 控制器驱动。
1. 协议层:SPI 不是"只会发数据"
SPI 最容易被误解成"写几个字节就完了",其实它有几个必须先想清楚的约束:
| 概念 | 你要抓住什么 |
|---|---|
MOSI/MISO/SCK/CS |
主设备通过时钟驱动传输,片选决定当前在和谁通信 |
CPOL/CPHA |
决定时钟空闲电平和在哪个边沿采样;模式错了,数据看起来像"能通信但全错位" |
bits_per_word |
一次基本传输到底按 8 位、16 位还是别的宽度组织 |
| 3 线/4 线 | 3 线会把收发线合并,不能默认按标准 4 线去理解 |
DAC 例子里,课程故意让你看到 16bit 传输;OLED 例子里,大量命令和数据又是 8bit 传输。所以不要把"SPI 默认就是 8 位"当成铁律。
2. Linux 对象:controller/master、device、driver 各管什么
课程文档里大量使用 spi_master 这个名字;后面的 slave mode 文档又会提到 spi_controller。对初学者来说,可以先这样记:
| 对象 | 它是什么 | 你要抓住什么 |
|---|---|---|
spi_controller / spi_master |
SPI 控制器对象 | 提供总线传输能力,决定怎么拉时钟、怎么切片选、怎么搬数据 |
spi_device |
挂在某个控制器下的一个具体 SPI 设备 | 记录片选号、模式、最大频率、字宽等 |
spi_driver |
某类 SPI 设备的驱动 | 它不自己造总线,只是告诉内核"这个设备该怎么用 SPI 说话" |
所以这章真正的模型是:
SPI 控制器驱动 提供传输能力
SPI 设备驱动 使用这份能力访问具体外设
3. 设备树:谁在父节点,谁在子节点
SPI 的设备树层次很适合拿来训练"总线脑子":
| 层级 | 常见属性 | 含义 |
|---|---|---|
| 控制器节点 | compatible、#address-cells、#size-cells、cs-gpios |
描述 SPI 控制器本身以及片选资源 |
| 设备子节点 | compatible、reg、spi-max-frequency |
描述挂在该控制器下的一个 SPI 设备 |
| 设备专属属性 | 比如 dc-gpios |
补充这个外设自己的额外控制脚 |
拿这章的 STM32MP157 实验来说,OLED 和 DAC 都是挂在 spi5 下的子节点。reg = <0>、reg = <1> 不是寄存器地址,而是"这是第几个片选"。
4. spidev:它是验证工具,不是最终答案
spidev 这章非常重要,但初学者最容易把它学偏。
它的价值是:
- 快速验证设备树和硬件接线
- 让用户态用
read/write/ioctl先把 SPI 路打通 - 在你还没写设备驱动前,先确认协议、模式、频率、片选是不是正确
它的局限也很明确:
- 它是通用桥接层,不理解具体设备协议语义
- 课程文档明确指出它不支持中断,也只支持同步操作
- 真到设备功能复杂时,还是要落回专用
spi_driver
所以正确姿势是:先用 spidev 验证,再写自己的设备驱动;不要把 spidev 当成所有 SPI 设备的最终方案。
5. spi_transfer 和 spi_message:这是后半章的关键抽象
这一章进入设备驱动和 SPI master 驱动后,最重要的两个结构体就是它们:
| 对象 | 它是什么 | 你要抓住什么 |
|---|---|---|
spi_transfer |
一段具体传输 | 关心 tx_buf、rx_buf、len |
spi_message |
由多个 spi_transfer 组成的一次完整请求 |
让多个传输按顺序执行 |
为什么这两个对象重要?
- SPI 一次传输是"发多少,就同时收多少"
- 很多设备访问不是"一次 write 就结束",而是"先发命令,再发数据,或先发命令,再读数据"
- 所以 Linux 用
message -> transfer这层结构去组织一次完整事务
你只要抓住一条直觉:transfer 是片段,message 是事务。
6. 设备驱动:DAC 和 OLED 的价值不一样
这章用两个外设来讲 SPI 设备驱动,很有代表性:
| 设备 | 学习价值 |
|---|---|
| DAC | 最小的 SPI 设备协议例子,能让你专注看位宽、时序和数据格式 |
| OLED | 设备更复杂,除了 SPI 传输外,还要处理命令/数据区分、显存映射和额外 GPIO |
所以正确顺序一定是:
先学 DAC,再学 OLED
原因很简单:DAC 让你先把"SPI 事务"和"设备协议打包"学会;OLED 则把"设备树 + GPIO + SPI + 显示缓冲"一起带出来。
7. framebuffer 改造:这是"设备驱动"升级成"子系统接口"
OLED 原始驱动的思路是:应用知道 OLED 页地址、列地址、字模格式,然后通过驱动发命令。
framebuffer 改造后的思路是:
- 驱动在内核里分配
fb_info - 应用面对的是
/dev/fbX - 应用往 framebuffer 写像素
- 驱动线程再把 framebuffer 的布局转换成 OLED GDDRAM 需要的布局,通过 SPI 周期性刷新
这一步的意义非常大:驱动不再要求应用懂 OLED 协议,而是把 SPI OLED 包装成 Linux 里的一个通用显示设备。
8. SPI master/slave:这里讨论的是控制器角色,不是设备驱动角色
这一节默认归到扩展线,建议在前面的"过关线"和"提升线"都跑通以后再看。
这是这章最后一个大坑。
很多人会把下面两个概念混在一起:
- SPI Slave Driver:通过本机的 SPI master 去访问外部 SPI 从设备
- SPI Slave Mode:让本机的 SPI 控制器自己变成"被别人访问的从设备"
这完全不是一回事。
本章前半段写 DAC/OLED 驱动时,你写的是 SPI 设备驱动 。
本章后半段写 virtual_spi_master.c 时,你写的是 SPI 控制器驱动 。
讲 slave mode 时,又是在讨论 控制器工作角色。
资料、实验、源码的推荐顺序
这是最适合初学者的一条可执行路径:先看什么,再做什么实验,再读哪些源码。
| 阶段 | 所在线 | 先看什么 | 再做什么实验 | 再读哪些源码 | 重点观察什么 | 通过标志 / 成功信号 |
|---|---|---|---|---|---|---|
| 1 | 过关线 | 01 02 03 |
暂时不做实验 | 暂时不读代码 | 先把协议、控制器、设备、驱动三层关系讲清 | 能口头解释 controller/device/driver 各自负责什么 |
| 2 | 过关线 | 04 |
检查 SPI 控制器节点和子节点写法 | 暂时不读代码 | 父节点是控制器,子节点是设备;reg 是片选号 |
能看懂 spi5 下面为什么能挂出 DAC/OLED 子节点 |
| 3 | 过关线 | 05 06 07 |
用 spidev 跑 DAC |
02_dac_use_spidev/dac_test.c |
SPI_IOC_MESSAGE 如何组织一次 16bit 事务 |
系统里出现 /dev/spidevB.D;dac_test 能正常打开设备并打印返回值;示波器或模块输出随输入值变化 |
| 4 | 过关线 | 08 09 10 |
用 spidev 跑 OLED |
04_oled_use_spidev_ok/spi_oled.c |
用户态怎么同时管 SPI 和 DC GPIO |
/dev/spidevB.D 可用;OLED 能点亮、清屏或显示测试文字/汉字 |
| 5 | 过关线 | 11 12 13 |
加载 DAC 专用驱动并测试 | 06_dac_use_mydrv_ok/dac_drv.c、06_dac_use_mydrv_ok/dac_test.c |
spi_driver 的 probe、字符设备接口、spi_sync |
驱动加载后出现 /dev/100ask_dac;dac_test /dev/100ask_dac <val> 能运行并返回预期数值 |
| 6 | 过关线 | 14 15 |
加载 OLED 专用驱动并测试 | 08_oled_use_mydrv_ok/oled_drv.c、08_oled_use_mydrv_ok/spi_oled.c |
dc-gpios 怎么从设备树进入驱动,命令/数据怎么分流 |
驱动加载后出现 /dev/100ask_oled;执行测试程序后 OLED 有稳定显示内容 |
| 7 | 提升线 | 16 17 |
跑 framebuffer 版本 OLED | 10_oled_framebuffer_ok/oled_drv.c、03_freetype_show_font_angle/freetype_show_font_angle.c |
fb_info、内核线程、显存格式转换 |
insmod 前后 ls /dev/fb* 数量变化;出现新的 /dev/fbX;freetype_show_font_angle 能把内容画到屏上 |
| 8 | 扩展线 | 18 19 20 |
加载老方法 virtual master,跑 spi_test |
12_spi_master_old_ok/virtual_spi_master.c、12_spi_master_old_ok/virt_spi_master.dts、12_spi_master_old_ok/spi_test.c |
master->transfer、队列、完成回调 |
驱动加载后出现新的 /dev/spidev*;spi_test /dev/spidev32765.0 500 之类命令能正常返回,不报 SPI_IOC_MESSAGE 错误 |
| 9 | 扩展线 | 21 22 |
加载新方法 virtual master,跑 spi_test |
14_spi_master_new_ok/virtual_spi_master.c、14_spi_master_new_ok/virt_spi_master.dts、14_spi_master_new_ok/spi_test.c |
spi_bitbang、txrx_bufs、等待完成 |
驱动加载后同样能生成新的 /dev/spidev*;spi_test 能正常跑通,说明 bitbang 路径已接上 SPI Core |
| 10 | 扩展线 | 23 24 |
以讲义分析为主 | 本地 A7 目录没有对应 slave 示例,先按讲义理解角色切换 | 分清 slave handler、slave controller、master/slave 等待机制不同 | 能明确说出"访问外部从设备"和"本机进入 slave mode"是两条不同链路 |
最短实验顺序
如果你的时间有限,建议只保留下面 6 步:
- 先用
spidev跑 DAC,理解 16 位数据格式和SPI_IOC_MESSAGE - 再用
spidev跑 OLED,理解为什么还要额外控制DC引脚 - 再读并运行 DAC 专用驱动,确认
spi_driver怎么替代spidev - 再读并运行 OLED 专用驱动,确认设备树专属属性怎么进驱动
- 再读 framebuffer 版 OLED,理解为什么显示类设备往往最后要接到子系统接口
- 把
master/slave留到最后,当成扩展线,不要反过来卡住前面的过关路径
源码地图:每段代码到底在教什么
这一章最有效的做法,不是把所有代码都看完,而是让每份源码只回答一个问题。
1. 02_dac_use_spidev/dac_test.c
它回答的问题是:用户态怎样最直接地构造一次 SPI 事务。
建议重点看:
open("/dev/spidevB.D")struct spi_ioc_transferioctl(fd, SPI_IOC_MESSAGE(1), xfer)- 发送前如何把 10bit DAC 数值打包成 16bit 数据
这份代码是全章最小的"SPI 协议 -> Linux API"闭环。
2. 04_oled_use_spidev_ok/spi_oled.c
它回答的问题是:当 SPI 设备除了总线本身,还依赖一个额外 GPIO 时,用户态要多做什么。
建议重点看:
dc_pin_initoled_set_dc_pinwrite(fd_spidev, buf, len)OLED_DIsp_Set_Pos、OLED_DIsp_Test
这份代码很适合建立一个直觉:SPI 只负责时钟同步传输,不负责帮你自动区分"这是命令还是数据"。
3. 06_dac_use_mydrv_ok/dac_drv.c
它回答的问题是:专用 SPI 设备驱动最小长什么样。
建议重点看:
spidev_probespi_register_driverspi_message_initspi_message_add_tailspi_synccopy_from_user/copy_to_user
这份代码把"用户态直接调 spidev"改造成了"用户态只跟字符设备说话,驱动内部再发 SPI"。
4. 08_oled_use_mydrv_ok/oled_drv.c + 08_oled_use_mydrv_ok/spi_oled.c
它们回答的问题是:一个稍复杂的 SPI 设备驱动,怎么同时处理 SPI 传输、GPIO 控制和用户接口。
建议重点看:
- 驱动里的
gpiod_get oled_write_cmd_dataspidev_ioctlspidev_write- 应用里的
OLED_IOC_INIT、OLED_IOC_SET_POS
这一步你会看到:设备树里的 dc-gpios 不只是"多一个属性",而是直接改变驱动的硬件控制路径。
5. 10_oled_framebuffer_ok/oled_drv.c
它回答的问题是:为什么要把 SPI OLED 驱动升级成 framebuffer 驱动。
建议重点看:
framebuffer_allocregister_framebuffermyfb_opskthread_run(oled_thread_func, ...)oled_thread_func里怎样把 framebuffer 数据重排成 OLED 需要的列页格式
这份代码是本章最值得反复读的源码之一,因为它把"面向设备协议"升级成了"面向 Linux 子系统接口"。
6. 12_spi_master_old_ok/virtual_spi_master.c
它回答的问题是:老方法写 SPI master 驱动时,master->transfer 到底在做什么。
建议重点看:
spi_alloc_mastermaster->transfer = spi_virtual_transfer- 工作队列里如何从
master->queue取出spi_message - 完成回调
mesg->complete
读这份代码时,重点不要放在"它没真的驱动硬件",而要放在"SPI Core 期望控制器驱动怎么完成一个 message"。
7. 14_spi_master_new_ok/virtual_spi_master.c
它回答的问题是:为什么新方法更强调 spi_bitbang 和单个 transfer 的处理。
建议重点看:
spi_alloc_master(... sizeof(struct spi_bitbang))spi_master_get_devdatag_virtual_bitbang->txrx_bufsspi_bitbang_startwait_for_completion_timeout
这份代码让你看到:新方法并不是推翻前面的模型,而是把控制器驱动和 SPI Core 的协作方式规范化了。
初学者最容易混淆的点
- 混淆 1:
spi_master、spi_controller是不是两套东西?
不是。课程前半段多用spi_master这个历史名字;后面 slave mode 文档会强调它已经演进成更通用的控制器概念。 - 混淆 2:
spidev是不是 SPI 设备驱动?
严格说它是通用用户态接口桥。它能帮你验证硬件,但它并不知道 DAC/OLED 的业务语义。 - 混淆 3:
reg = <1>是不是寄存器地址?
不是。在 SPI 设备树子节点里,它通常表示片选号。 - 混淆 4:SPI "读"是不是就不用"写"?
不是。SPI 是同步移位,总线动起来时一定同时有收和发;只不过你有时忽略收到的数据,有时忽略发出的占位数据。 - 混淆 5:
spi_write和spi_sync是不是谁都能乱用?
不能。简单接口适合简单事务;一旦是"先写命令再读数据"或多段事务,就要回到transfer/message。 - 混淆 6:OLED 驱动里为什么还要 GPIO?
因为 SPI 只解决串行数据传输,像 OLED 的DC这类协议外控制脚,仍然需要额外 GPIO 管理。 - 混淆 7:framebuffer 改造是不是只是"多注册一个设备节点"?
不是。它改变的是驱动对外提供的抽象层次,从"会操作 OLED 命令集"变成"提供通用显示缓冲接口"。 - 混淆 8:本章后半段的 SPI master/slave,是不是还在讲 DAC/OLED 这种外设驱动?
不是。那时讨论的重点已经从"设备驱动"切换到"控制器驱动"和"控制器角色"。 - 混淆 9:SPI Slave Driver 和 SPI Slave Mode 是不是一回事?
不是。前者是"我去访问外部从设备";后者是"我自己变成从设备,被别人访问"。 - 混淆 10:硬件片选和软件片选有没有本质差别?
有。设备树里cs-gpios说明片选可能由 GPIO 模拟;而某些控制器也可能使用硬件片选逻辑。调试片选问题时,这个差别必须先想清楚。
学完这一章后,应该能做到什么
学完 11_SPI,你至少应该能独立做到这些事:
- 能解释 SPI 协议最基本的时序概念:模式、片选、位宽、全双工
- 能看懂一个 SPI 设备树节点,并知道控制器节点和设备子节点分别在描述什么
- 能用
spidev快速验证一个 SPI 外设是否接通 - 能看懂
spi_transfer、spi_message,知道什么时候该用简单接口,什么时候该组织多段事务 - 能写出一个最小的
spi_driver,在probe里拿到spi_device并发起同步传输 - 能理解为什么 OLED 这类设备经常还需要额外 GPIO
- 能理解 framebuffer 改造的价值,知道它不是"多包一层",而是把设备接进通用显示接口
- 能分清 SPI 设备驱动和 SPI 控制器驱动
- 能读懂课程给出的 old/new 两种 SPI master 驱动框架
- 能说清楚 SPI master、SPI slave device、SPI slave mode 三者不是一回事
如果你读完后仍然只能记住"DAC 发 2 字节、OLED 发很多字节",那说明你还停留在"例子记忆"阶段;
如果你已经能把这条链路说顺:
设备树创建设备 -> spi_driver probe -> 构造 transfer/message -> 控制器驱动完成传输 -> 对复杂设备再接入 framebuffer 或切到 master/slave 角色视角
那这一章就算真正学进去了。
12_USB 学习总结

TL;DR:这一章真正要学会的,不是"会读一个 USB 鼠标"或者"会跑一个 Gadget 示例",而是把 USB 的两种视角彻底拆开。站在 Host 这边,要看懂
枚举 -> 描述符 -> interface/endpoint -> libusb -> usb_driver -> URB;站在 Device/Gadget 这边,要看懂OTG 角色切换 -> UDC -> Gadget/composite -> configfs -> function/functionfs -> zero/serial/adb。两条线一旦混在一起,初学者就很容易把"谁在发起传输""谁在匹配驱动""谁在提供描述符"全部弄乱。
章节定位
12_USB 是整套驱动课里第一章明显要求你同时站在"总线主机"和"外设设备"两边思考的内容。
前面的 05_Input、09_UART、11_SPI,虽然也涉及总线和子系统,但你大多还是围绕"板子上已经存在的设备,怎么把它接进 Linux"来学习。到了 USB,这个思路不够了,因为你必须同时理解:
- 设备不是板子启动时就固定存在的,它可能随时插入、拔出
- 设备不是先有驱动、后有识别,而是先被 Host 枚举出来,Linux 才知道它有哪些 interface,可以匹配哪些驱动
- 同一个 OTG 口,板子既可能做 Host,也可能做 Device
- USB 鼠标驱动会回到
Input子系统,USB 串口 Gadget 又会回到TTY/UART的老知识点
它和前后章节的关系,可以这样把握:
- 往前承接
05_Input:本章的usbmouse_as_key不是直接读 GPIO,而是把 USB 鼠标数据转换成 Input 事件。 - 往前承接
09_UART:g_serial最终在板子侧变成/dev/ttyGS0,在 PC 侧变成/dev/ttyACM0,本质上是在复用你已经见过的 TTY 心智模型。 - 往前承接
11_SPI:SPI 设备往往是板上固定连线的;USB 设备则是热插拔、先枚举再匹配,更能体现总线框架的意义。 - 往后承接
13_V4L2:以后看 USB 摄像头时,你会继续遇到 interface、endpoint、描述符、等时传输这些概念。
一句话概括:这一章是在解释 Linux 如何把"插上来的一个 USB 世界",变成内核里可识别、可匹配、可传输、可模拟的设备模型。
学习主线总览
这章必须强行拆成两条线来学:
| 主线 | 你站在哪一边 | 要解决的问题 | 对应资料/示例 |
|---|---|---|---|
| Host 主线 | PC/开发板作为主机 | 设备为什么一插上就能被识别?描述符是怎么被读出来的?为什么 usb_driver 匹配的是 interface? |
02~09,02_libusb_mouse_sync,03_libusb_mouse_async,04_usbmouse_as_key |
| Gadget 主线 | 开发板作为 USB Device | 开发板为什么能"伪装成" zero/串口/ADB 设备?描述符是谁提供的?数据通过哪些 endpoint 传? | 10~15,05_libusb_zero,06_gadget_serial,07_adb |
如果你只记一条最短学习路径,请记住:
Host 发现设备 -> ep0 枚举 -> 读取描述符 -> 选配置 -> 生成 usb_interface -> libusb/usb_driver 访问 endpoint
和
OTG 判定角色 -> UDC 进入 Device 模式 -> Gadget 提供描述符 -> Host 枚举 -> function 申请 endpoint -> Host 发起读写
核心概念与关键链路
先把高频术语钉住
| 术语 | 初学者应该怎么理解 |
|---|---|
| 枚举(enumeration) | Host 发现新设备后,在 endpoint 0 上用控制传输读设备信息、分配地址、读取完整描述符、选择配置的过程。没有这一步,后面的驱动匹配都无从谈起。 |
| endpoint | 设备里的数据端点。除了 ep0 是双向控制端点外,其他端点通常都是单向的,并且带有类型:control、bulk、interrupt、isochronous。 |
| interface | 一个 USB 设备里的"逻辑功能单元"。一个物理设备可以有多个 interface,所以 Linux 里的 usb_driver 通常匹配的是 usb_interface,不是整个 usb_device。 |
| descriptor | 设备用格式化数据描述自己的方式。常见有 device/configuration/interface/endpoint/string descriptor。Host 先读这些描述符,才知道设备"是什么"和"怎么跟它通信"。 |
| pipe | Host 软件里对 endpoint 的访问句柄。内核里常见 usb_rcvintpipe、usb_sndbulkpipe 这类宏,本质上是在描述"对哪个端点、按什么方向、用什么类型传输"。 |
| URB | USB Request Block,内核里的一次 USB 传输请求。驱动分配、填充、提交 URB,完成后在回调里继续处理或重提。 |
| UDC | USB Device Controller。开发板作为 USB Device 时,真正负责 endpoint 收发的底层控制器。STM32MP157 讲义里重点落在 dwc2。 |
| gadget function | Gadget 侧的功能模块,可以理解为"我要把自己暴露成什么接口/功能"。比如 acm、loopback、sourcesink、ffs.adb。 |
packet / transaction / transfer / URB 最小层级对照
这一组概念很容易混。最短记法是:前 3 个是 USB 协议线上发生的层级,URB 是 Linux 内核里组织一次 USB 请求的软件对象。
| 层级 | 它是什么 | 典型例子 | 最容易混淆的点 |
|---|---|---|---|
| packet | 总线上的一个包 | token/data/handshake 包 | 一个 packet 只是一次片段,不等于一次完整的数据请求 |
| transaction | 由若干 packet 组成的一次事务 | OUT token + DATA0 + ACK |
transaction 才开始像"一次真正传输动作" |
| transfer | 驱动/协议视角的一次传输 | control transfer、bulk transfer、interrupt transfer | 一个 transfer 可能由一个或多个 transaction 组成 |
| URB | 内核里的 USB 请求对象 | 驱动里分配、填充、提交的 struct urb |
URB 不等于 USB 线上一个包,它是软件里描述"一次请求"的容器,底层会再拆成 transaction 和 packet |
主线一:Host / 枚举 / 描述符 / libusb / 设备驱动
Host 视角最关键的链路是:
设备接入 -> root hub 发现 -> ep0 控制传输 -> 读取描述符 -> 分配地址 -> 选择配置 -> 为每个 interface 创建设备模型 -> 匹配 usb_driver 或交给 libusb
1. 为什么一插上就"能看见设备"
02_USB系统硬件框架和软件框架.md 和 03_软件工程师眼里的USB电气信号.md 解决的是起点问题:
- USB 系统里,Host 这边带着 root hub,它可以理解为"USB 控制器内部自带的那个最顶层 Hub",新设备最先是被它发现的
- 新设备接入时,D+/D- 的电平变化会被 Host 感知
- Host 会复位设备、判断速率、开始在默认地址
0上和它通信
这部分对软件工程师来说,不需要死背电气细节。真正要记住的是:USB 不是"驱动主动扫描某个寄存器",而是 Host 先从总线层面发现有新设备。
2. 枚举到底在做什么
05_USB描述符.md 里最重要的不是描述符字段表,而是枚举过程示例。初学者至少要把下面 5 步记熟:
| 步骤 | Host 做什么 | 目的 |
|---|---|---|
| 1 | 用控制传输读取设备描述符前 8 字节 | 先知道 bMaxPacketSize0,也就是 ep0 一次能收多大 |
| 2 | 发送 SET_ADDRESS |
给新设备分配正式地址,不再用默认地址 0 |
| 3 | 重新读取完整设备描述符 | 获取 VID/PID、配置个数等完整身份信息 |
| 4 | 读取配置描述符树 | 一次拿到 configuration/interface/endpoint 关系 |
| 5 | 选择某个配置 | 让设备进入工作状态,后续才能按 interface/endpoint 通信 |
这里最重要的心智模型是:
- 描述符不是驱动代码,它只是设备的自我说明书
- 枚举不是驱动匹配,它发生在驱动匹配之前
- 真正的数据收发,大多发生在枚举完成之后的其他 endpoint 上
3. endpoint、interface、descriptor 之间是什么关系
这章里很多混乱,其实都来自没把这棵树看清:
一个 USB 设备 -> 一个或多个 configuration -> 每个 configuration 下有多个 interface -> 每个 interface 下有多个 endpoint
对 Linux 驱动开发者来说,要特别注意两点:
- 一个物理 USB 设备可能有多个 interface,所以一个设备可以同时表现出多个功能。
usb_driver.probe()的参数是struct usb_interface *,这说明驱动经常是按 interface 匹配的。
08_USB设备驱动模型.md 明确强调了这一点,而 02_libusb_mouse_sync/readmouse.c 也在用同样的思路遍历:
- 先拿配置描述符
- 再看某个 interface 的
bInterfaceClass、bInterfaceProtocol - 再从这个 interface 里找到目标 endpoint
4. libusb 是最好的第一层验证
这章的 Host 主线,最正确的实验入口不是一上来写内核驱动,而是先用 libusb 在用户态把设备"看清楚"。
06_libusb的使用.md 和 07_使用libusb读取鼠标数据.md 对应的源码角色非常明确:
02_libusb_mouse_sync/readmouse.c
重点看:libusb_get_device_list、libusb_get_config_descriptor、libusb_open、libusb_claim_interface、libusb_interrupt_transfer
其中claim interface可以直白理解为"先把这个 USB 功能接口的使用权拿到自己手里",避免和内核原有驱动同时操作它。
你会看到"按描述符找 HID 鼠标"和"按中断 IN endpoint 读数据"的完整闭环。03_libusb_mouse_async/readmouse.c
重点看:libusb_alloc_transfer、libusb_fill_interrupt_transfer、libusb_submit_transfer、libusb_handle_events_timeout
这个版本比同步版更接近内核里 URB 的思维方式,因为它也是"提交请求,完成后回调,再重提"。
这里建议你刻意建立一个类比:
libusb_interrupt_transfer:像是"用户态同步版的现成收发函数"libusb_transfer + callback:像是"用户态版的异步请求对象"URB + complete callback:是内核里的正式形态
5. 设备驱动主线最后落到 usb_driver + URB + Input
09_编写USB鼠标驱动程序.md 配套的 04_usbmouse_as_key 是本章 Host 主线最关键的源码。
推荐对照着看两个文件:
04_usbmouse_as_key/usbmouse.c
这是标准 USB 鼠标驱动思路的参考版,帮你建立"成熟驱动长什么样"的感觉。04_usbmouse_as_key/usbmouse_as_key.c
这是课程为了教学改写的版本,重点不是鼠标本身,而是让你看清整条链路:usb_device_id决定支持哪些 interfaceprobe里拿到usb_interface,检查中断 IN endpoint- 分配 DMA buffer 和 URB,这里的 DMA buffer 可以理解为"一块适合 USB 控制器直接读写的内存",这样可以少做一次中间拷贝
- 注册
input_dev - 在
open里usb_fill_int_urb + usb_submit_urb - 在 URB 回调里解析鼠标数据,并上报
KEY_L、KEY_S、KEY_ENTER
从学习迁移的角度说,这段代码真正教你的不是"鼠标变键盘",而是:
USB 总线识别 -> usb_driver probe -> 找 endpoint -> 提交 URB -> 回调里接上其他子系统
这正是以后看 U 盘、网卡、摄像头、串口类 USB 驱动时最常见的骨架。
主线二:OTG / Gadget / configfs / function / 实例
Gadget 视角最关键的链路是:
OTG 判定角色 -> UDC 提供 endpoint 能力 -> Gadget/composite 提供描述符和控制处理 -> function 申请 endpoint 并排队请求 -> Host 来枚举和读写
1. 先别急着写 Gadget,先搞清谁是 Host、谁是 Device
10_OTG硬件监测电路.md 的作用,是防止你在后半章一开始就迷路。
它强调两件事:
- Micro USB 用
ID引脚判定角色,Type-C 用CC1/CC2判定角色 - 角色不只是软件概念,还关系到
VBUS该由谁供电
所以一个非常常见的误区是:以为"把某个驱动装上去"就能让板子变成 Device。
不够。前提是硬件角色得对,UDC 得真的工作在 Device 模式。
2. Gadget 的本质:给 Host 造出一个"像真设备一样的描述符树和数据通路"
11_Gadget驱动程序框架.md 是后半章最核心的总图。它解决的是两个问题:
- Gadget 设备如何"表示自己"
- Gadget 设备如何"收发数据"
初学者可以先不深挖所有内核结构体,只记住这几层:
| 层次 | 作用 |
|---|---|
| UDC | 底层 USB Device Controller,真正提供 endpoint 收发能力 |
| usb_gadget / composite | 处理中断、标准请求、描述符组织、控制传输通路 |
| function | 具体功能模块,决定 interface/endpoint 需求和数据逻辑 |
这章里最值得建立的直觉是:
- Host 侧是"先读描述符,再决定怎么访问你"
- Gadget 侧是"先把描述符和 endpoint 准备好,等 Host 来问、来读、来写"
也就是说,Gadget 从来不是主动发起总线传输的一方。
3. zero 是理解"描述符决定你长什么样"的最佳样例
12_Gadget应用实例之zero.md 对应 05_libusb_zero/zero_app.c,这是后半章最值得先做的实验。
它的价值很高,因为它把 Gadget 和 Host 两边一次性串起来了:
- 板子侧装
g_zero,让自己暴露出loopback或sourcesink功能 - PC 侧运行
zero_app.c zero_app.c会:- 通过 VID/PID 打开设备
- 列出不同配置的
bConfigurationValue - 选择配置
- 读取 active config descriptor,也就是"当前已经被设备选中的那套配置描述符树"
- 找到 bulk IN/OUT endpoint
- 用
libusb_bulk_transfer做读写验证
这比单看讲义更有用,因为你会实际看到:
- 同一个 Gadget 可以有多个 configuration
- Host 真的是按当前配置去看到不同 interface/endpoint 的
- "设备长什么样"不是 PC 端拍脑袋决定的,而是 Gadget 提供的描述符决定的
4. serial 是理解"Gadget 功能如何接上 Linux 既有子系统"的最佳样例
13_Gadget应用实例之serial.md 对应 06_gadget_serial/serial_send_recv.c。
这一组资料最适合和 09_UART 联动起来看,因为它说明了一个非常关键的事实:
- 板子侧,
g_serial不是生成一个"神秘 USB 文件",而是让你得到/dev/ttyGS0 - PC 侧,会出现
/dev/ttyACM0 - 也就是说,USB Gadget 并没有绕开 Linux 的串口/TTY 认知,而是把 USB endpoint 进一步包装成了串口语义
serial_send_recv.c 非常朴素,但它恰好说明了学习重点:
- 不要把精力放在复杂协议上
- 就用最普通的
open/read/write - 去感受"Gadget 功能最终如何在用户态变成熟悉的设备节点"
5. configfs 解决的是"怎么在运行时拼装一个 Gadget"
14_configfs的使用与内部机制.md 是后半章另一个分水岭。
你要先从使用角度把这套流程记住:
| 动作 | configfs 里的对应操作 |
|---|---|
| 创建一个 Gadget | mkdir /sys/kernel/config/usb_gadget/<name> |
| 设置设备身份 | 写 idVendor、idProduct、strings/... |
| 创建配置 | mkdir configs/<name>.<num> |
| 创建功能 | mkdir functions/<func>.<instance> |
| 把功能挂到配置下 | ln -s functions/... configs/... |
| 绑定到 UDC,真正生效 | echo <udc> > UDC |
它和 sysfs 的差别也一定要记住:
- sysfs 更像"内核对象的观察窗口"
- configfs 更像"用户态驱动内核去创建配置对象"
所以在 Gadget 这一侧,configfs 解决的是"怎么拼装设备外形",不是"怎么传业务数据"。
6. ADB 让你看到 configfs 还不够,functionfs 才能把端点交给用户态
15_Gadget应用实例之adb.md 和 07_adb/adbd.sh 是整章后半段最容易被看乱、但也最有价值的内容。
这里一定要分清两个名字:
configfs:决定这个 Gadget 有哪些 function、配置、字符串、VID/PIDfunctionfs:把某个 function 的 endpoint 暴露给用户态程序使用
07_adb/adbd.sh 这个脚本本身就值得精读,因为它几乎把整个流程写成了"操作手册":
- 创建
g1 - 设置
idVendor、idProduct和字符串描述符 - 创建
functions/ffs.adb - 创建
configs/b.1 - 建立符号链接,把
ffs.adb挂到配置里 mount -t functionfs adb /dev/usb-ffs/adb- 启动
adbd - 最后把 UDC 名字写入
UDC
这条线真正想教会你的,是下面这件事:
有些 Gadget 功能不是完全写死在内核里的,而是内核先把 endpoint 通道交给用户态,真正的协议逻辑由用户态守护进程完成。
这就是为什么 ADB 不能只理解为"又一个 configfs 配置例子",它其实是:
configfs 负责拼 Gadget 外形 + functionfs 把 endpoint 交给 adbd + adbd 负责具体协议
应该重点读哪些文档和源码
先读这些文档
| 资料 | 作用 | 为什么必须先看 |
|---|---|---|
02_USB系统硬件框架和软件框架.md |
建立 Host/Device/root hub 总图 | 不先分清角色,后面所有流程都会混 |
04_USB协议层数据格式.md |
理解 packet / transaction / transfer | 能帮你分清控制、批量、中断、等时传输到底在说什么 |
05_USB描述符.md |
建立 descriptor 树和枚举过程 | 这是全章最核心的前置知识 |
06_libusb的使用.md |
建立用户态访问 USB 的最短闭环 | 适合作为进入源码前的操作地图 |
08_USB设备驱动模型.md |
建立 usb_interface、pipe、URB 心智模型 |
这是 Host 设备驱动部分的桥梁 |
10_OTG硬件监测电路.md |
明确 OTG 角色切换前提 | 不然后半章容易把硬件前提忽略掉 |
11_Gadget驱动程序框架.md |
建立 Gadget 的总图 | 是后半章的主心骨 |
14_configfs的使用与内部机制.md |
建立运行时拼装 Gadget 的方法 | 后面看 ADB 时会用到 |
再盯这些配套源码
| 源码目录/文件 | 建议关注点 | 它解决什么问题 |
|---|---|---|
source/A7/12_USB/02_libusb_mouse_sync/readmouse.c |
遍历配置描述符、识别 HID 鼠标、声明 interface、同步读取中断端点 | 让你第一次把"描述符"和"真实读数据"连起来 |
source/A7/12_USB/03_libusb_mouse_async/readmouse.c |
libusb_transfer、回调、事件循环、多鼠标链表 |
让你理解异步收发和后续 URB 的相似性 |
source/A7/12_USB/04_usbmouse_as_key/usbmouse.c |
参考驱动骨架 | 看标准 USB 鼠标驱动的成熟写法 |
source/A7/12_USB/04_usbmouse_as_key/usbmouse_as_key.c |
usb_driver、usb_device_id、probe、DMA buffer、URB 回调、Input 上报 |
这是本章 Host 内核驱动的核心样例 |
source/A7/12_USB/05_libusb_zero/zero_app.c |
选择配置、获取 active config descriptor、找 bulk endpoint、做读写 | 最适合理解 Gadget 描述符如何影响 Host 侧行为 |
source/A7/12_USB/06_gadget_serial/serial_send_recv.c |
把 Gadget 串口当普通 TTY 用 | 帮你把 Gadget 和 UART/TTY 知识接上 |
source/A7/12_USB/07_adb/adbd.sh |
configfs + functionfs + adbd 启动顺序 | 这是理解 ADB Gadget 的最短路径 |
实验前先确认
在进入实验顺序前,先把下面这些检查项过一遍。这样能避免你把时间浪费在"不是 USB 主线问题,而是环境没准备好"上。
| 检查项 | 怎么看 | 看到什么算正常 |
|---|---|---|
| UDC 是否存在 | ls /sys/class/udc |
至少能看到一个 UDC 名字,比如 STM32MP157 上常见的 49000000.usb-otg;如果这里是空的,后面的 Gadget 绑定基本做不起来 |
| configfs 是否可用 | mount -t configfs none /sys/kernel/config,再 ls /sys/kernel/config |
能看到 usb_gadget 目录;如果还没挂载,configfs 方式的 Gadget 实验无法开始 |
| 是否有旧 Gadget 残留 | ls /sys/kernel/config/usb_gadget,必要时检查已有 g1、旧的 functions、configs |
如果已经有出厂自带的 adb 或别人留下的 Gadget,先清理,否则新的 serial、zero、ffs.adb 容易冲突 |
| OTG/枚举日志是否正常 | `dmesg | tail -n 50` |
| Gadget 串口节点是否出现 | ls /dev/ttyGS0 |
板子侧能看到 /dev/ttyGS0,说明 g_serial 至少已经把 Gadget 串口节点建出来了 |
| PC 侧串口节点是否出现 | 在 PC 侧看 dmesg 或 ls /dev/ttyACM0 |
出现 /dev/ttyACM0,说明 Host 侧已经把 Gadget 串口识别成 ACM 设备 |
| ADB 是否打通 | adb devices |
能列出板子序列号或设备项;如果列表为空,优先回头检查 configfs、functionfs 挂载、adbd 是否启动、UDC 是否已绑定 |
推荐实验顺序
这一章最怕的学法,就是"按文件名顺序从头看到尾"。更好的方法是:先用讲义建立地图,再用最小实验确认现象,最后回头读源码。
| 阶段 | 先看什么 | 再做什么实验 | 成功信号/观察点 | 最后读哪些源码,重点看什么 |
| --- | --- | --- | --- |
| 1 | 02、03 | 用 lsusb 看看系统里已有的 USB 设备,不急着写代码 | 能看到 root hub 和外接设备,先建立"Host 在管理总线"这件事的直觉 | 这一步可以先不读源码,只建立角色和接入流程 |
| 2 | 04、05 | 用 lsusb -v 观察某个鼠标或键盘的 descriptor 树,确认 device/config/interface/endpoint 关系 | lsusb -v 能清楚看到 Device Descriptor -> Configuration Descriptor -> Interface Descriptor -> Endpoint Descriptor 这棵层级树 | 02_libusb_mouse_sync/readmouse.c:看它如何从配置描述符里定位鼠标 interface 和中断 IN endpoint |
| 3 | 06、07 | 先跑同步版,再跑异步版鼠标读取程序 | readmouse 能持续打印鼠标数据;同步版是阻塞式打印,异步版能在回调里反复打印数据 | 02_libusb_mouse_sync/readmouse.c、03_libusb_mouse_async/readmouse.c:对比同步 API 和回调式 API |
| 4 | 08、09 | 按讲义禁用内核自带 HID 驱动,装入 usbmouse_as_key.ko,观察 /dev/input/event* 变化和按键事件 | 出现新的 /dev/input/event*,hexdump 或控制台测试能看到鼠标按键转成 input 事件 | 04_usbmouse_as_key/usbmouse.c、usbmouse_as_key.c:重点看 probe、usb_fill_int_urb、回调里上报 Input 事件 |
| 5 | 10、11 | 用 OTG 线连接板子和 PC,先确认角色切换和 UDC 是否存在 | ls /sys/class/udc 有设备名,dmesg 里没有明显的 OTG/枚举失败日志 | 这一阶段源码先以讲义里的框架理解为主,不急着深挖内核实现 |
| 6 | 12 | 板子侧 modprobe g_zero,PC 侧运行 zero_app -l/-s/-wstr/-rstr | zero_app -l 能列出配置值,-wstr/-rstr 或读写 bulk 数据能看到回环或源数据返回 | 05_libusb_zero/zero_app.c:重点看"先选配置,再找 endpoint,再 bulk 传输" |
| 7 | 13 | 跑 g_serial,在板子侧和 PC 侧分别操作 /dev/ttyGS0、/dev/ttyACM0 | 板子侧出现 /dev/ttyGS0,PC 侧出现 /dev/ttyACM0,两边程序可以互发字符 | 06_gadget_serial/serial_send_recv.c:重点看"USB 串口最后怎样落成普通 TTY 读写" |
| 8 | 14、15 | 手工过一遍 configfs 目录创建过程,再读 adbd.sh,最后用 adb devices、adb shell 验证 | adb devices 能列出板子,adb shell 能进入设备;如果失败,优先检查 functionfs 是否挂载、adbd 是否启动、UDC 是否已绑定 | 07_adb/adbd.sh:重点看 functions/ffs.adb、mount -t functionfs、最后绑定 UDC 的顺序 |
如果你时间有限,最值得保留的最小闭环是:
05_USB描述符.md02_libusb_mouse_sync/readmouse.c04_usbmouse_as_key/usbmouse_as_key.c11_Gadget驱动程序框架.md05_libusb_zero/zero_app.c07_adb/adbd.sh
这 6 个点足够把本章骨架立起来。
Linux 初学者常见卡点
- 卡点 1:把 USB 驱动理解成"匹配整个设备"。
不准确。很多usb_driver匹配的是usb_interface,因为一个物理设备可以有多个功能接口。 - 卡点 2:把 endpoint 和 interface 混为一谈。
interface是逻辑功能,endpoint是数据通道;一个 interface 下面通常有多个 endpoint。 - 卡点 3:把"中断传输"理解成 CPU 中断。
USB 的 interrupt transfer 指的是一种周期性传输语义,不是说设备直接触发 CPU 硬件中断。 - 卡点 4:以为枚举就是读一下 VID/PID。
不够。枚举至少还包括读ep0包长、设置地址、读取完整配置树、选择配置。 - 卡点 5:以为描述符就是"设备驱动"。
不对。描述符只是设备自我描述的数据;驱动是 Host 侧根据这些信息做匹配和传输的代码。 - 卡点 6:分不清 libusb 和内核驱动的关系。
它们常常是在竞争同一个 interface 的所有权。libusb 访问前,往往要 detach 原内核驱动并 claim interface。 - 卡点 7:把 URB 当成 USB 线上真实的"一个包"。
不是。URB 是内核软件里的请求对象;底层真正上线传输时,会拆成 packet / transaction / transfer。 - 卡点 8:以为 Gadget 设备可以主动往总线上发数据。
不能这么理解。USB 总是由 Host 发起事务,Gadget 只是预先排队好 request,等待 Host 来读或写。 - 卡点 9:分不清 configfs 和 functionfs。
configfs 负责"拼装设备外形",functionfs 负责"把某些 function 的 endpoint 暴露给用户态"。 - 卡点 10:以为 OTG 角色切换只是软件配置。
还取决于 ID/CC 检测和 VBUS 供电方向,硬件前提不成立,软件再配置也没用。
学完这一章后应该能做到什么
- 能用自己的话解释 USB 设备从插入到枚举完成的大致过程。
- 能看懂
device -> configuration -> interface -> endpoint这棵描述符树。 - 能说明为什么
usb_driver.probe()常常拿到的是usb_interface *。 - 能读懂 libusb 示例代码里"枚举设备、认领 interface、读写 endpoint"的流程。
- 能看懂
usbmouse_as_key.c这类 Host 驱动里usb_device_id、URB、回调、Input 上报的基本骨架。 - 能解释 OTG、UDC、Gadget、configfs、function、functionfs 之间的大致分工。
- 能按顺序搭起一个简单的 Gadget 实验,并知道
zero、serial、adb三个示例分别在验证什么。
如果把这章学扎实,后面你再看 USB 摄像头、USB 网卡、Android ADB、USB 串口,都会更容易抓住主线:先分清自己站在 Host 还是 Device 一边,再看描述符、接口、端点和数据路径。