STM32MP157 Linux驱动学习笔记(四):典型总线与设备模型(SPI/USB)

这篇文章整理自课程第 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_Pinctrl07_GPIO:SPI 控制器能不能工作,先取决于引脚复用;OLED 里的 dc-gpios 又把 GPIO 控制带回来了。
  • 往前承接 03_LCD:这一章后半段把 SPI OLED 改造成 framebuffer,本质上是在把一个小屏幕重新接进 Linux 显示抽象里。
  • 往后衔接 12_USB13_V4L2:从这一章开始,你不该只盯寄存器,而要习惯从"总线/子系统/设备模型"的角度理解驱动。

一句话概括:这一章是在解释 Linux 怎样把"一个按时钟移位的串行总线",接成"用户态可验证、设备树可描述、驱动可扩展、控制器角色可切换"的 SPI 子系统。

命名澄清

先把两个最容易混掉的名字钉住:

  • 课程示例代码里有些函数沿用了 spidev_* 这种命名,但本文统一把它们称为 SPI 设备驱动 。是否是专用驱动,关键看它是不是围绕某个具体 SPI 设备去实现 spi_driver,而不是看函数名叫不叫 spidev_*
  • 驱动外部 SPI 从设备控制器进入 slave mode 不是一件事。前者仍然是"本机当 master,通过总线访问外设";后者是"本机控制器自己变成从设备,被别人访问"。

学习主线建议

这一章资料很多,如果一上来就读 virtual_spi_master.c 或者硬啃 spi_message,初学者通常会乱。建议严格按下面顺序学:

  1. 先看协议和对象关系
    01_SPI视频概述.md02_SPI协议介绍.md03_SPI总线设备驱动模型.md
    先只解决 4 个问题:
    • SPI 为什么至少要有 MOSI/MISO/SCK/CS
    • CPOL/CPHA 为什么会形成 4 种模式
    • Linux 里谁是控制器,谁是设备,谁是驱动
    • 为什么 SPI 设备驱动自己不直接"碰硬件时钟",而是调用控制器提供的传输能力
  2. 再看设备树怎么把 SPI 设备变成内核对象
    04_SPI设备树处理过程.md
    这一步的目标不是背属性,而是分清:
    • SPI 控制器节点描述的是 spi_controller/spi_master
    • 控制器子节点描述的是 spi_device
    • compatible/reg/spi-max-frequency 为什么几乎总是必选
  3. 再用 spidev 做最低成本验证
    05_spidev的使用(SPI用户态API).md,然后先做 DAC,再做 OLED。
    这一步的目标是先证明:板子、设备树、时序、片选、频率都对了。
  4. 再写真正的 SPI 设备驱动
    11_编写SPI设备驱动程序.md,然后读 DAC/OLED 驱动示例。
    这一步要把"用户态直接调 spidev"升级成"内核里有自己的 spi_driver 和字符设备接口"。
  5. 再看 framebuffer 改造
    16_使用Framebuffer改造OLED驱动.md
    这一步最重要的不是 API 细节,而是理解:为什么要把"面向 OLED 页寻址的驱动"改成"面向 /dev/fbX 的通用显示接口"。
  6. 最后再看 SPI master/slave
    1824
    这一步学的是"怎么写控制器驱动"和"主从角色变化后,控制器驱动的思维方式为什么会变"。

如果只保留一条最短主线,请记住:

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-cellscs-gpios 描述 SPI 控制器本身以及片选资源
设备子节点 compatibleregspi-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_transferspi_message:这是后半章的关键抽象

这一章进入设备驱动和 SPI master 驱动后,最重要的两个结构体就是它们:

对象 它是什么 你要抓住什么
spi_transfer 一段具体传输 关心 tx_bufrx_buflen
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.Ddac_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.c06_dac_use_mydrv_ok/dac_test.c spi_driverprobe、字符设备接口、spi_sync 驱动加载后出现 /dev/100ask_dacdac_test /dev/100ask_dac <val> 能运行并返回预期数值
6 过关线 14 15 加载 OLED 专用驱动并测试 08_oled_use_mydrv_ok/oled_drv.c08_oled_use_mydrv_ok/spi_oled.c dc-gpios 怎么从设备树进入驱动,命令/数据怎么分流 驱动加载后出现 /dev/100ask_oled;执行测试程序后 OLED 有稳定显示内容
7 提升线 16 17 跑 framebuffer 版本 OLED 10_oled_framebuffer_ok/oled_drv.c03_freetype_show_font_angle/freetype_show_font_angle.c fb_info、内核线程、显存格式转换 insmod 前后 ls /dev/fb* 数量变化;出现新的 /dev/fbXfreetype_show_font_angle 能把内容画到屏上
8 扩展线 18 19 20 加载老方法 virtual master,跑 spi_test 12_spi_master_old_ok/virtual_spi_master.c12_spi_master_old_ok/virt_spi_master.dts12_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.c14_spi_master_new_ok/virt_spi_master.dts14_spi_master_new_ok/spi_test.c spi_bitbangtxrx_bufs、等待完成 驱动加载后同样能生成新的 /dev/spidev*spi_test 能正常跑通,说明 bitbang 路径已接上 SPI Core
10 扩展线 23 24 以讲义分析为主 本地 A7 目录没有对应 slave 示例,先按讲义理解角色切换 分清 slave handler、slave controller、master/slave 等待机制不同 能明确说出"访问外部从设备"和"本机进入 slave mode"是两条不同链路

最短实验顺序

如果你的时间有限,建议只保留下面 6 步:

  1. 先用 spidev 跑 DAC,理解 16 位数据格式和 SPI_IOC_MESSAGE
  2. 再用 spidev 跑 OLED,理解为什么还要额外控制 DC 引脚
  3. 再读并运行 DAC 专用驱动,确认 spi_driver 怎么替代 spidev
  4. 再读并运行 OLED 专用驱动,确认设备树专属属性怎么进驱动
  5. 再读 framebuffer 版 OLED,理解为什么显示类设备往往最后要接到子系统接口
  6. master/slave 留到最后,当成扩展线,不要反过来卡住前面的过关路径

源码地图:每段代码到底在教什么

这一章最有效的做法,不是把所有代码都看完,而是让每份源码只回答一个问题。

1. 02_dac_use_spidev/dac_test.c

它回答的问题是:用户态怎样最直接地构造一次 SPI 事务。

建议重点看:

  • open("/dev/spidevB.D")
  • struct spi_ioc_transfer
  • ioctl(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_init
  • oled_set_dc_pin
  • write(fd_spidev, buf, len)
  • OLED_DIsp_Set_PosOLED_DIsp_Test

这份代码很适合建立一个直觉:SPI 只负责时钟同步传输,不负责帮你自动区分"这是命令还是数据"。

3. 06_dac_use_mydrv_ok/dac_drv.c

它回答的问题是:专用 SPI 设备驱动最小长什么样。

建议重点看:

  • spidev_probe
  • spi_register_driver
  • spi_message_init
  • spi_message_add_tail
  • spi_sync
  • copy_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_data
  • spidev_ioctl
  • spidev_write
  • 应用里的 OLED_IOC_INITOLED_IOC_SET_POS

这一步你会看到:设备树里的 dc-gpios 不只是"多一个属性",而是直接改变驱动的硬件控制路径。

5. 10_oled_framebuffer_ok/oled_drv.c

它回答的问题是:为什么要把 SPI OLED 驱动升级成 framebuffer 驱动。

建议重点看:

  • framebuffer_alloc
  • register_framebuffer
  • myfb_ops
  • kthread_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_master
  • master->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_devdata
  • g_virtual_bitbang->txrx_bufs
  • spi_bitbang_start
  • wait_for_completion_timeout

这份代码让你看到:新方法并不是推翻前面的模型,而是把控制器驱动和 SPI Core 的协作方式规范化了。

初学者最容易混淆的点

  • 混淆 1:spi_masterspi_controller 是不是两套东西?
    不是。课程前半段多用 spi_master 这个历史名字;后面 slave mode 文档会强调它已经演进成更通用的控制器概念。
  • 混淆 2:spidev 是不是 SPI 设备驱动?
    严格说它是通用用户态接口桥。它能帮你验证硬件,但它并不知道 DAC/OLED 的业务语义。
  • 混淆 3:reg = <1> 是不是寄存器地址?
    不是。在 SPI 设备树子节点里,它通常表示片选号。
  • 混淆 4:SPI "读"是不是就不用"写"?
    不是。SPI 是同步移位,总线动起来时一定同时有收和发;只不过你有时忽略收到的数据,有时忽略发出的占位数据。
  • 混淆 5:spi_writespi_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_transferspi_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_Input09_UART11_SPI,虽然也涉及总线和子系统,但你大多还是围绕"板子上已经存在的设备,怎么把它接进 Linux"来学习。到了 USB,这个思路不够了,因为你必须同时理解:

  • 设备不是板子启动时就固定存在的,它可能随时插入、拔出
  • 设备不是先有驱动、后有识别,而是先被 Host 枚举出来,Linux 才知道它有哪些 interface,可以匹配哪些驱动
  • 同一个 OTG 口,板子既可能做 Host,也可能做 Device
  • USB 鼠标驱动会回到 Input 子系统,USB 串口 Gadget 又会回到 TTY/UART 的老知识点

它和前后章节的关系,可以这样把握:

  • 往前承接 05_Input:本章的 usbmouse_as_key 不是直接读 GPIO,而是把 USB 鼠标数据转换成 Input 事件。
  • 往前承接 09_UARTg_serial 最终在板子侧变成 /dev/ttyGS0,在 PC 侧变成 /dev/ttyACM0,本质上是在复用你已经见过的 TTY 心智模型。
  • 往前承接 11_SPI:SPI 设备往往是板上固定连线的;USB 设备则是热插拔、先枚举再匹配,更能体现总线框架的意义。
  • 往后承接 13_V4L2:以后看 USB 摄像头时,你会继续遇到 interface、endpoint、描述符、等时传输这些概念。

一句话概括:这一章是在解释 Linux 如何把"插上来的一个 USB 世界",变成内核里可识别、可匹配、可传输、可模拟的设备模型。

学习主线总览

这章必须强行拆成两条线来学:

主线 你站在哪一边 要解决的问题 对应资料/示例
Host 主线 PC/开发板作为主机 设备为什么一插上就能被识别?描述符是怎么被读出来的?为什么 usb_driver 匹配的是 interface 02~0902_libusb_mouse_sync03_libusb_mouse_async04_usbmouse_as_key
Gadget 主线 开发板作为 USB Device 开发板为什么能"伪装成" zero/串口/ADB 设备?描述符是谁提供的?数据通过哪些 endpoint 传? 10~1505_libusb_zero06_gadget_serial07_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_rcvintpipeusb_sndbulkpipe 这类宏,本质上是在描述"对哪个端点、按什么方向、用什么类型传输"。
URB USB Request Block,内核里的一次 USB 传输请求。驱动分配、填充、提交 URB,完成后在回调里继续处理或重提。
UDC USB Device Controller。开发板作为 USB Device 时,真正负责 endpoint 收发的底层控制器。STM32MP157 讲义里重点落在 dwc2
gadget function Gadget 侧的功能模块,可以理解为"我要把自己暴露成什么接口/功能"。比如 acmloopbacksourcesinkffs.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系统硬件框架和软件框架.md03_软件工程师眼里的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 的 bInterfaceClassbInterfaceProtocol
  • 再从这个 interface 里找到目标 endpoint
4. libusb 是最好的第一层验证

这章的 Host 主线,最正确的实验入口不是一上来写内核驱动,而是先用 libusb 在用户态把设备"看清楚"。

06_libusb的使用.md07_使用libusb读取鼠标数据.md 对应的源码角色非常明确:

  • 02_libusb_mouse_sync/readmouse.c
    重点看:libusb_get_device_listlibusb_get_config_descriptorlibusb_openlibusb_claim_interfacelibusb_interrupt_transfer
    其中 claim interface 可以直白理解为"先把这个 USB 功能接口的使用权拿到自己手里",避免和内核原有驱动同时操作它。
    你会看到"按描述符找 HID 鼠标"和"按中断 IN endpoint 读数据"的完整闭环。
  • 03_libusb_mouse_async/readmouse.c
    重点看:libusb_alloc_transferlibusb_fill_interrupt_transferlibusb_submit_transferlibusb_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 决定支持哪些 interface
    • probe 里拿到 usb_interface,检查中断 IN endpoint
    • 分配 DMA buffer 和 URB,这里的 DMA buffer 可以理解为"一块适合 USB 控制器直接读写的内存",这样可以少做一次中间拷贝
    • 注册 input_dev
    • openusb_fill_int_urb + usb_submit_urb
    • 在 URB 回调里解析鼠标数据,并上报 KEY_LKEY_SKEY_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,让自己暴露出 loopbacksourcesink 功能
  • 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>
设置设备身份 idVendoridProductstrings/...
创建配置 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.md07_adb/adbd.sh 是整章后半段最容易被看乱、但也最有价值的内容。

这里一定要分清两个名字:

  • configfs:决定这个 Gadget 有哪些 function、配置、字符串、VID/PID
  • functionfs:把某个 function 的 endpoint 暴露给用户态程序使用

07_adb/adbd.sh 这个脚本本身就值得精读,因为它几乎把整个流程写成了"操作手册":

  • 创建 g1
  • 设置 idVendoridProduct 和字符串描述符
  • 创建 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_driverusb_device_idprobe、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、旧的 functionsconfigs 如果已经有出厂自带的 adb 或别人留下的 Gadget,先清理,否则新的 serialzeroffs.adb 容易冲突
OTG/枚举日志是否正常 `dmesg tail -n 50`
Gadget 串口节点是否出现 ls /dev/ttyGS0 板子侧能看到 /dev/ttyGS0,说明 g_serial 至少已经把 Gadget 串口节点建出来了
PC 侧串口节点是否出现 在 PC 侧看 dmesgls /dev/ttyACM0 出现 /dev/ttyACM0,说明 Host 侧已经把 Gadget 串口识别成 ACM 设备
ADB 是否打通 adb devices 能列出板子序列号或设备项;如果列表为空,优先回头检查 configfs、functionfs 挂载、adbd 是否启动、UDC 是否已绑定

推荐实验顺序

这一章最怕的学法,就是"按文件名顺序从头看到尾"。更好的方法是:先用讲义建立地图,再用最小实验确认现象,最后回头读源码。

| 阶段 | 先看什么 | 再做什么实验 | 成功信号/观察点 | 最后读哪些源码,重点看什么 |

| --- | --- | --- | --- |

| 1 | 0203 | 用 lsusb 看看系统里已有的 USB 设备,不急着写代码 | 能看到 root hub 和外接设备,先建立"Host 在管理总线"这件事的直觉 | 这一步可以先不读源码,只建立角色和接入流程 |

| 2 | 0405 | 用 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 | 0607 | 先跑同步版,再跑异步版鼠标读取程序 | readmouse 能持续打印鼠标数据;同步版是阻塞式打印,异步版能在回调里反复打印数据 | 02_libusb_mouse_sync/readmouse.c03_libusb_mouse_async/readmouse.c:对比同步 API 和回调式 API |

| 4 | 0809 | 按讲义禁用内核自带 HID 驱动,装入 usbmouse_as_key.ko,观察 /dev/input/event* 变化和按键事件 | 出现新的 /dev/input/event*hexdump 或控制台测试能看到鼠标按键转成 input 事件 | 04_usbmouse_as_key/usbmouse.cusbmouse_as_key.c:重点看 probeusb_fill_int_urb、回调里上报 Input 事件 |

| 5 | 1011 | 用 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 | 1415 | 手工过一遍 configfs 目录创建过程,再读 adbd.sh,最后用 adb devicesadb shell 验证 | adb devices 能列出板子,adb shell 能进入设备;如果失败,优先检查 functionfs 是否挂载、adbd 是否启动、UDC 是否已绑定 | 07_adb/adbd.sh:重点看 functions/ffs.adbmount -t functionfs、最后绑定 UDC 的顺序 |

如果你时间有限,最值得保留的最小闭环是:

  1. 05_USB描述符.md
  2. 02_libusb_mouse_sync/readmouse.c
  3. 04_usbmouse_as_key/usbmouse_as_key.c
  4. 11_Gadget驱动程序框架.md
  5. 05_libusb_zero/zero_app.c
  6. 07_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 实验,并知道 zeroserialadb 三个示例分别在验证什么。

如果把这章学扎实,后面你再看 USB 摄像头、USB 网卡、Android ADB、USB 串口,都会更容易抓住主线:先分清自己站在 Host 还是 Device 一边,再看描述符、接口、端点和数据路径。

相关推荐
坚持就完事了2 小时前
Linux中的mv命令
linux·运维·服务器
SongYuLong的博客2 小时前
Claude Code安装配置(Linux)
linux·运维·服务器
2401_827499992 小时前
数据分析学习05(黑马)-Pandas
学习·数据分析·pandas
小柯博客2 小时前
STM32MP2安全启动技术深度解析
c语言·c++·stm32·嵌入式硬件·安全·开源·github
栈低来信2 小时前
kernel信号量源码分析
linux
结衣结衣.3 小时前
手把手教你实现文档搜索引擎
linux·c++·搜索引擎·开源·c++11
实在太懒于是不想取名3 小时前
STM32N6的开发日记(8):在N6中部署自训练的火焰检测模型
stm32·单片机·嵌入式硬件
jiayong233 小时前
第 38 课:任务列表里高亮当前正在查看详情的任务
开发语言·前端·javascript·vue.js·学习
木子单片机3 小时前
基于51单片机温度上下限报警设计 数码管显示
stm32·单片机·嵌入式硬件·51单片机·keil