前面的博客中,我们已经分别介绍了 SPI、SDIO 和 USB 框架的基本结构、初始化流程以及数据传输主线。接下来,我们将结合实际调试过程,分析这些外设框架中常见的问题,并梳理相应的排查思路。
目录
[一、SPI 框架](#一、SPI 框架)
[1. 设备树描述硬件资源,并创建 platform_device](#1. 设备树描述硬件资源,并创建 platform_device)
[2. spi_imx_probe() 完成控制器基础初始化](#2. spi_imx_probe() 完成控制器基础初始化)
[3. 注册 spi_master 到 SPI Core](#3. 注册 spi_master 到 SPI Core)
[4. spi_master、spi_imx_data 和 spi_bitbang 的关系](#4. spi_master、spi_imx_data 和 spi_bitbang 的关系)
[5. 数据传输最终进入 PIO 或 DMA 路径](#5. 数据传输最终进入 PIO 或 DMA 路径)
[二、SPI 框架常见问题及排查方法](#二、SPI 框架常见问题及排查方法)
[1、确认 SPI 控制器是否注册](#1、确认 SPI 控制器是否注册)
[2、确认 SPI 从设备是否创建](#2、确认 SPI 从设备是否创建)
[4.1. WHO_AM_I 读取错误](#4.1. WHO_AM_I 读取错误)
[4.2. ECSPI PIO 传输超时](#4.2. ECSPI PIO 传输超时)
[4.3. ICM20608 数据就绪 IRQ 申请失败](#4.3. ICM20608 数据就绪 IRQ 申请失败)
[三、SDIO 框架](#三、SDIO 框架)
[1. 整体分层](#1. 整体分层)
[2. 四个核心结构体](#2. 四个核心结构体)
[2.1 struct sdhci_host](#2.1 struct sdhci_host)
[2.2 struct mmc_host](#2.2 struct mmc_host)
[2.3 struct mmc_card](#2.3 struct mmc_card)
[2.4 struct sdio_func](#2.4 struct sdio_func)
[3. Host注册和扫卡](#3. Host注册和扫卡)
[4. SDIO识别和设备信息](#4. SDIO识别和设备信息)
[5. 创建Function并匹配驱动](#5. 创建Function并匹配驱动)
[6. CMD52、CMD53和mmc_host->ops](#6. CMD52、CMD53和mmc_host->ops)
[7. SDIO Wi-Fi收发](#7. SDIO Wi-Fi收发)
[7.1 发送](#7.1 发送)
[7.2 接收](#7.2 接收)
[8. 两类中断的区别](#8. 两类中断的区别)
[1. USDHC 控制器没有注册](#1. USDHC 控制器没有注册)
[2. 已经有 mmc_host,但识别不到 SDIO 设备](#2. 已经有 mmc_host,但识别不到 SDIO 设备)
[2.1 -110:ETIMEDOUT](#2.1 -110:ETIMEDOUT)
[2.2 -84:EILSEQ](#2.2 -84:EILSEQ)
[3. SDIO 枚举成功,但驱动没有匹配](#3. SDIO 枚举成功,但驱动没有匹配)
[4. SDIO 驱动匹配成功,但 probe 失败](#4. SDIO 驱动匹配成功,但 probe 失败)
[5. 固件或 NVRAM 加载失败](#5. 固件或 NVRAM 加载失败)
[6. CMD53 运行时收发失败](#6. CMD53 运行时收发失败)
[7. 中断相关问题](#7. 中断相关问题)
[第1层:设备树与 i.MX 平台适配](#第1层:设备树与 i.MX 平台适配)
[1.1 主要职责](#1.1 主要职责)
[1.2 两个容易混淆的数据对象](#1.2 两个容易混淆的数据对象)
[1.3 数据关系](#1.3 数据关系)
[2.1 ci_hdrc 中心对象](#2.1 ci_hdrc 中心对象)
[2.2 角色操作表](#2.2 角色操作表)
[第3层:Host控制器、EHCI与USB Core](#第3层:Host控制器、EHCI与USB Core)
[3.1 创建 usb_hcd](#3.1 创建 usb_hcd)
[3.2 hc_driver 不是外设驱动](#3.2 hc_driver 不是外设驱动)
[3.3 注册 HCD 与 Root Hub](#3.3 注册 HCD 与 Root Hub)
[4.1 插入与枚举](#4.1 插入与枚举)
[4.2 usb_driver 匹配 Interface](#4.2 usb_driver 匹配 Interface)
[5.1 OUT URB](#5.1 OUT URB)
[5.2 OUT调用链](#5.2 OUT调用链)
[6.1 必须先提交 IN URB](#6.1 必须先提交 IN URB)
[6.2 IN完成链](#6.2 IN完成链)
[六、USB 框架的注册日志](#六、USB 框架的注册日志)
[4、注册虚拟Root Hub接口](#4、注册虚拟Root Hub接口)
[1. 核心对象](#1. 核心对象)
[2. 第一层:设备树与Host初始化](#2. 第一层:设备树与Host初始化)
[3. 第二层:注册adapter、创建client并匹配驱动](#3. 第二层:注册adapter、创建client并匹配驱动)
[4. 第三层:提交并执行i2c_msg\[\]](#4. 第三层:提交并执行i2c_msg[])
[5. 第四层:PIO中断和等待队列](#5. 第四层:PIO中断和等待队列)
[6. 两个容易混淆的点](#6. 两个容易混淆的点)
[① PIO和DMA的唤醒方式不同](#① PIO和DMA的唤醒方式不同)
[② Host IRQ和从设备IRQ不同](#② Host IRQ和从设备IRQ不同)
一、SPI 框架
先简单总结一下 SPI 框架中几个重要概念。
1、spi_master 是 SPI Core 对一个 SPI 控制器的通用抽象,用来保存控制器能力、片选数量、消息队列和传输回调函数。
2、当 SPI 控制器初始化完成后,会把对应的 spi_master 注册到 SPI Core 中,spi_master 会被加入 SPI Core 对应的管理链表中,方便后续上层驱动通过 SPI Core 找到并使用这个控制器。这样后续上层驱动调用 spi_sync() 或 spi_async() 时,SPI Core 就可以找到对应的 spi_master,并通过它完成数据传输。
3、对于 i.MX6ULL 的 ECSPI 控制器来说,spi_master 会关联一个私有数据结构 spi_imx_data。spi_imx_data 用来保存 ECSPI 控制器的寄存器、时钟、FIFO、DMA 和当前传输状态。同时,spi_imx_data 里面内嵌了一个 spi_bitbang。这里的 spi_bitbang 可以理解为 SPI Core 和 spi-imx 驱动之间的传输适配层,它保存了 spi_imx_chipselect()、spi_imx_setupxfer()、spi_imx_transfer() 等回调函数。
下面详细回顾 Linux SPI 框架的分层流程。
1. 设备树描述硬件资源,并创建 platform_device
首先,设备树描述 ECSPI 控制器的寄存器地址、IRQ、时钟、DMA 和片选等硬件资源。内核根据设备树创建 platform_device,并与 spi-imx 驱动匹配。
2. spi_imx_probe() 完成控制器基础初始化
进入 spi_imx_probe() 后,驱动通过 platform 接口取得这些资源,映射寄存器、申请中断、获取时钟、初始化 DMA、复位控制器并关闭暂时不需要的中断。同时,通过 spi_alloc_master() 创建代表该 SPI 控制器的通用对象 spi_master,并为它附带私有数据 spi_imx_data。
需要注意,probe 并不会一次性完成所有传输寄存器配置:寄存器映射、中断申请、时钟获取和控制器复位等基础初始化在 probe 中完成,而传输速度、位宽、工作模式、DMA 或 PIO 选择等参数,会在每次 message 或 transfer 执行前动态配置。
3. 注册 spi_master 到 SPI Core
随后,spi_imx_probe() 调用 spi_bitbang_start(),最终将 spi_master 注册到 SPI Core,就是把 master 放到对应的链表中,方便以后调用。SPI Core 在注册过程中为这个 master 初始化软件消息队列。
4. spi_master、spi_imx_data 和 spi_bitbang 的关系
spi_master 本身是 SPI Core 认识的通用控制器抽象,保存控制器能力、消息队列和传输回调。
它不会主动调用 SPI Core,而是由上层设备驱动调用 spi_sync() 或 spi_async() 进入 SPI Core,再由 SPI Core 调度这个 spi_master。spi_master 的私有数据指向 spi_imx_data,而 spi_imx_data 内部封装了 spi_bitbang 传输适配层。
spi_bitbang 将通用的 transfer_one 回调转换为 spi_imx_transfer()。
5. 数据传输最终进入 PIO 或 DMA 路径
当 SPI Core 从 master 的软件队列中取出一个 spi_message 后,会逐个处理其中的 spi_transfer。然后通过 spi_bitbang_transfer_one()、spi_imx_transfer() 进入 PIO 或 DMA 路径,最终操作 ECSPI 的 TX/RX FIFO 和寄存器完成硬件传输。
二、SPI 框架常见问题及排查方法
按"控制器 -> SPI 设备 -> 驱动匹配 -> 数据传输"的顺序逐层排查。
1、确认 SPI 控制器是否注册
ls /sys/class/spi_master/
本板 ECSPI3 正常应出现 spi2。如果没有,检查:
1. 是否加载了正确的 DTB。
2. ECSPI3 是否为 status = "okay"。
3. pinctrl 引脚是否被 UART 或其他外设占用。
4. CONFIG_SPI、CONFIG_SPI_MASTER、CONFIG_SPI_IMX 是否开启。
可同时查看:
dmesg | grep -Ei "spi|ecspi|pinctrl|gpio|dma"
2、确认 SPI 从设备是否创建
ls -l /sys/bus/spi/devices/
本板 ICM20608 使用 reg = <0>,正常应出现 spi2.0。
如果没有,检查设备树子节点的 compatible、reg、spi-max-frequency 和 status。
3、确认设备与驱动是否匹配
cat /sys/bus/spi/devices/spi2.0/modalias
readlink /sys/bus/spi/devices/spi2.0/driver
有 spi2.0 但没有 driver 链接,通常是:
1. compatible 与 id_table 不匹配。
2. 驱动没有编译。
3. 驱动是模块但未加载。
4. 驱动 probe 失败后取消了绑定。
4、设备已匹配,但初始化失败
4.1. WHO_AM_I 读取错误
whoami mismatch got 0xff expected 0xaf for ICM20608
whoami mismatch got 0x00 expected 0xaf for ICM20608
这表示 SPI 控制器已完成传输,但收到的数据错误。
0xff:优先检查 MISO 悬空、CS 未选中、芯片未供电。
0x00:优先检查 MISO 被拉低、芯片复位或供电异常。
解决顺序:将频率降至 500 kHz 或 1 MHz,确认 GPIO1_20 低有效 CS 和 SPI Mode 0,再用逻辑分析仪检查 MOSI 是否发送 0xF5、MISO 是否返回 0xAF。
4.2. ECSPI PIO 传输超时
spi2.0: I/O Error in PIO
spi2.0: SPI transfer failed: -110
spi2.0: Could not initialize device.
inv-mpu6000-spi: probe of spi2.0 failed with error -110
-110 是 -ETIMEDOUT。真正的第一现场是 I/O Error in PIO:ECSPI 已启动传输,但驱动没等到 spi_imx_isr() 调用 complete(&xfer_done)。Could not initialize device 只是上层结果。
cpp
regmap_write()
└─ spi_sync()
└─ spi_transfer_one_message()
└─ spi_bitbang_transfer_one()
└─ spi_imx_transfer()
└─ spi_imx_pio_transfer()
│
├─ 保存tx_buf和rx_buf
│
├─ spi_imx_push()
│ // 先向TX FIFO写入数据并启动ECSPI
│
├─ intctrl(MXC_INT_TE)
│ // 打开ECSPI控制器发送中断
│
└─ wait_for_completion_timeout(xfer_done)
│
├─ 正常情况
│ └─ spi_imx_isr()
│ ├─ 读取RX FIFO
│ ├─ 继续填充TX FIFO
│ └─ complete(&xfer_done)
│ // 唤醒等待传输完成的任务
│
└─ 超时情况
├─ 打印"I/O Error in PIO"
├─ 复位ECSPI控制器
└─ 返回-ETIMEDOUT
优先检查:
1. ECSPI 的 ipg/per 时钟是否开启。
2. ECSPI 控制器中断计数是否增加。
3. 中断是否被屏蔽。
4. RX FIFO、count 和 txfifo 是否能正常清零。
cat /proc/interrupts | grep -Ei "2010000|ecspi|spi"
cat /sys/kernel/debug/clk/clk_summary | grep -i ecspi
注意:SPI 没有 ACK。即使从设备未连接,主机通常也能完成移位,只是读到 0xff 或 0x00。因此 PIO 超时应先查 ECSPI 时钟、内部中断和 FIFO,而不是先查传感器是否"回应"。
4.3. ICM20608 数据就绪 IRQ 申请失败
trigger probe fail -22
trigger probe fail -16
-22 / -EINVAL:IRQ 号、interrupt-parent 或 interrupts 配置错误。
-16 / -EBUSY:GPIO 或 IRQ 已被其他驱动占用。
这里是 ICM20608 的 GPIO1_1 数据就绪中断,与 ECSPI 完成 PIO 传输的内部中断不是同一路。
5、逻辑分析仪快速判断
cpp
CS 和 SCLK 都没有
└─ 消息未执行、CS/pinctrl 错误或控制器未启动。
CS 拉低,但没有 SCLK
└─ 检查 ECSPI 时钟和控制器配置。
CS、SCLK、MOSI 都正常,但 ECSPI 中断不增加
└─ 检查 ECSPI 内部 IRQ 映射、屏蔽和 GIC。
ECSPI 中断增加,但仍然超时
└─ 检查 RX FIFO、count、txfifo 和 ISR 退出条件。
传输不超时,但 MISO 为 0xff 或 0x00
└─ 检查芯片供电、CS、MISO、SPI 模式和读命令。
三、SDIO 框架
这一部分主要总结 SDIO 框架中的重点知识。
-
SDIO 框架复用 MMC Core:SDIO 框架复用了 Linux 的 MMC Core。其中
struct sdhci_host是 SDHCI 控制器层的对象,主要保存控制器寄存器地址、中断、DMA、当前请求和硬件特性等信息。它只适用于 SDHCI 类控制器,并不是所有 MMC/SD/SDIO 控制器的通用抽象。 -
sdhci_host与mmc_host的关系:sdhci_host通过host->mmc指针关联一个struct mmc_host。而mmc_host才是 MMC Core 对 Host 控制器的通用抽象。控制器驱动通过mmc_add_host()将它注册到内核。 -
注册 Host 后启动扫卡流程:注册时并不会同时创建
mmc_card,而是 MMC Core 随后启动扫卡流程。MMC Core 通过mmc->ops->request()调用 SDHCI 驱动发送命令。识别 SDIO 设备时,首先用 CMD5 获得 R4 响应,其中包含设备是否就绪、支持的电压范围、I/O Function 数量以及是否带有存储功能等信息,然后才分配并填写mmc_card。 -
读取 SDIO 设备信息并创建
sdio_func:接着通过 CMD52 读取 CCCR、FBR 和 CIS。其中 CCCR 记录整张 SDIO 卡的协议版本、总线宽度、高速模式和中断等公共能力。CIS(不是 CSI)记录厂商号、设备号、最大传输速率和块大小等描述信息。MMC Core 再根据 Function 数量,为 Function 1~N 分别创建sdio_func对象,读取各 Function 的 FBR 和 CIS,并将指针保存到card->sdio_func[fn-1]中。最后把这些 Function 注册到 SDIO 总线,与对应的sdio_driver匹配。 -
SDIO 运行时数据传输路径:SDIO 设备运行时的 CMD52 寄存器访问和 CMD53 数据收发,最终仍然会经过
mmc_host->ops->request()交给底层 SDHCI/USDHC 控制器完成。
1. 整体分层
SDIO复用Linux的MMC Core,SD、MMC、eMMC和SDIO都通过mmc_host发送请求,设备识别后再进入不同的上层驱动。
设备树:描述USDHC、引脚、电源和总线宽度
↓
sdhci-esdhc-imx:i.MX6ULL USDHC平台适配
↓
SDHCI层:操作寄存器、DMA和中断
↓
MMC Core:注册Host、供电、扫卡和发送请求
↓
SDIO Core/Bus:创建sdio_func并匹配sdio_driver
↓
SDIO设备驱动:例如BCMDHD Wi-Fi驱动
2. 四个核心结构体
2.1 struct sdhci_host
SDHCI控制器层对象,保存寄存器地址、中断、DMA、当前请求和控制器特性。它只适用于SDHCI类控制器,不是所有MMC控制器的通用抽象。
2.2 struct mmc_host
MMC Core的通用Host抽象,保存控制器能力、时钟、电压、总线状态、mmc_host_ops和当前卡指针。
sdhci_host与mmc_host通过私有区和指针关联,不是简单的结构体包含关系:
host = mmc_priv(mmc);
host->mmc = mmc;
mmc->ops = &host->mmc_host_ops;
2.3 struct mmc_card
代表Host识别到的一张物理卡或板载SDIO模块。它不是注册mmc_host时创建,而是扫卡识别成功后才分配。
2.4 struct sdio_func
代表SDIO卡内部的一个I/O Function,保存Function编号、class、vendor/device、块大小等信息,并作为设备注册到sdio_bus_type。
sdhci_host
└─ host->mmc ──► mmc_host
└─ host->card ──► mmc_card
├─ sdio_func[0] ──► Function 1
├─ sdio_func[1] ──► Function 2
└─ ...
Function 0是公共控制区,不创建普通sdio_func;数组下标0对应Function 1。
3. Host注册和扫卡
控制器初始化完成后调用mmc_add_host()注册mmc_host并启动扫描:
mmc_add_host(host)
└─ mmc_start_host(host)
└─ _mmc_detect_change()
└─ mmc_rescan()
└─ mmc_rescan_try_freq()
├─ 上电并设置初始化时钟
├─ sdio_reset() // CMD52协议复位
├─ mmc_go_idle() // CMD0
├─ mmc_attach_sdio() // 尝试SDIO
├─ mmc_attach_sd() // 尝试SD
└─ mmc_attach_mmc() // 尝试MMC/eMMC
mmc_start_host()只是启动扫描机制,真正的SDIO识别和命令交互在后续扫描函数中完成。
4. SDIO识别和设备信息
mmc_attach_sdio()先发送CMD5。CMD5返回的R4响应包含:
- 设备是否ready;
- 支持的电压范围;
- I/O Function数量;
- 是否支持1.8 V;
- 是否同时带有SD存储功能。
设备能正常响应CMD5,说明它支持SDIO协议。随后MMC Core选择合适电压并初始化卡:
CMD5:等待设备ready
↓
mmc_alloc_card():创建mmc_card
↓
CMD3:获取RCA
↓
CMD7:选中卡
↓
CMD52:读取CCCR、FBR和CIS
↓
配置高速模式、时钟和总线宽度
几个信息区的区别:
| 信息 | 主要作用 |
|---|---|
| R4/OCR | ready、电压、Function数量、memory-present |
| CCCR | 整张卡的协议版本、总线宽度、高速和中断能力 |
| FBR | 某个Function的class、CIS地址和块大小等信息 |
| CIS | 厂商号、设备号、速率、块大小等描述信息 |
是CIS,不是CSI。card->ocr主要保存协商后的当前电压设置,不能简单等同于最初CMD5返回的完整R4。
5. 创建Function并匹配驱动
MMC Core从CMD5的R4响应中得到Function数量,再为Function 1~N创建软件对象:
sdio_init_func(card, fn)
├─ sdio_alloc_func(card)
├─ func->num = fn
├─ 读取FBR和Function CIS
└─ card->sdio_func[fn - 1] = func
SDIO设备不会返回Linux指针;sdio_func和指针都是内核自己分配的。
mmc_add_card(card)
└─ 注册整张卡
sdio_add_func(func)
└─ 注册到sdio_bus_type
└─ 按class/vendor/device匹配sdio_driver.id_table
└─ 调用驱动probe(func, id)
6. CMD52、CMD53和mmc_host->ops
- CMD52:单字节读写,主要访问CCCR、FBR和控制寄存器。
- CMD53:批量数据读写,支持字节/块模式,常用于SDIO Wi-Fi数据包传输。
SDIO设备驱动通常调用SDIO Core API,不直接调用mmc_host->ops:
sdio_memcpy_toio()/sdio_memcpy_fromio()/sdio_readsb()
└─ mmc_io_rw_extended() // 构造CMD53和mmc_request
└─ mmc_wait_for_req()
└─ host->ops->request(host, mrq)
└─ sdhci_request()
└─ USDHC寄存器/DMA执行传输
└─ sdhci_irq()
└─ mmc_request_done()
因此,mmc_host->ops是MMC Core调用Host控制器驱动的回调,不是SDIO Wi-Fi驱动的操作函数表。
7. SDIO Wi-Fi收发
7.1 发送
网络协议栈产生skb
↓
BCMDHD处理并排队
↓
sdio_memcpy_toio()
↓
CMD53写
↓
mmc_host->ops->request()
↓
USDHC把数据写入Wi-Fi芯片
7.2 接收
SDIO总线由主机发起传输,Wi-Fi芯片不能直接把完整数据包推入CPU内存。
Wi-Fi芯片收到无线数据并存入FIFO
↓
DAT1带内中断或OOB GPIO中断通知主机
↓
BCMDHD调用sdio_readsb()/sdio_memcpy_fromio()
↓
主机发起CMD53读
↓
USDHC把数据读入内存
↓
netif_rx()交给Linux网络协议栈
8. 两类中断的区别
-
SDIO Function中断:由Wi-Fi芯片通过DAT1或OOB GPIO通知"有数据或事件需要处理",本身不搬运完整数据。
-
USDHC传输完成中断:主机发出CMD52/CMD53后,由USDHC通知"本次命令或数据传输已完成"。
Wi-Fi数据就绪中断
↓
驱动发起CMD53读取
↓
USDHC传输完成中断
↓
mmc_request_done()
四、SDIO框架常见问题和排查
按下面的顺序寻找"第一个没有出现的对象",前一层没有成功时,不要先排查后一层。
mmc_host
-> mmc_card
-> sdio_func
-> sdio_driver 匹配/probe
-> 固件和 NVRAM 下载
-> wlan0
-> Wi-Fi 扫描、关联和 DHCP
1. USDHC 控制器没有注册
现象:/sys/class/mmc_host/ 中没有对应的 mmcX。常见原因
cpp
1) 设备树 USDHC 节点的 status 没有设置为 "okay";
2) compatible 与 sdhci-esdhc-imx 驱动不匹配;
3) pinctrl、clock、regulator 或控制器 IRQ 配置错误;
4) 控制器 probe 失败;
5) 内核没有启用 CONFIG_MMC_SDHCI_ESDHC_IMX。
排查命令
cpp
ls -l /sys/class/mmc_host
dmesg | grep -Ei "usdhc|sdhci|mmc"
cat /proc/interrupts | grep -Ei "usdhc|mmc"
当前工程已经启用:
cpp
CONFIG_MMC_SDHCI_ESDHC_IMX=y
对应驱动
drivers/mmc/host/sdhci-esdhc-imx.c
drivers/mmc/host/sdhci.c
2. 已经有 mmc_host,但识别不到 SDIO 设备
典型日志
mmc0: Timeout waiting for hardware cmd interrupt.
mmc0: Timeout waiting for hardware interrupt.
mmc0: error -110 whilst initialising SDIO card
mmc0: error -84 whilst initialising SDIO card
2.1 -110:ETIMEDOUT
-110 表示某次命令或数据请求没有在规定时间内完成。正常流程是:
MMC Core 下发请求
-> USDHC 发送 CMD/传输数据
-> SDIO 设备在总线上返回响应或数据
-> USDHC 产生命令/数据完成状态
-> USDHC 向 CPU 触发控制器 IRQ
-> sdhci_irq() 完成请求
因此超时不一定只能解释成"SDIO 设备没有应答",还可能是:
1) Wi-Fi 模块没有供电或 WL_REG_ON/复位没有释放;
2) CLK、CMD、DAT 接线或 pinmux 错误;
3) bus-width 与实际硬件不一致;
4) CMD/DAT 上拉、电压或时钟配置错误;
5) USDHC 控制器 IRQ 没有送到 CPU 或驱动没有正确处理;
6) 控制器时钟、寄存器或状态机异常;
7) 工作频率过高或信号质量太差。
日志区别
Timeout waiting for hardware cmd interrupt.
主要表示没有等到命令完成。
Timeout waiting for hardware interrupt.
主要表示带数据阶段的请求没有完成。
error -110 whilst initialising SDIO card
是 SDIO 初始化后半段的统一错误日志,不等于"第一次 CMD5 没有 Function"。
第一次探测式 CMD5是mmc_send_io_op_cond(host, 0, &ocr)。如果它直接失败,mmc_attach_sdio() 会直接返回,通常不会走到这条统一错误日志。这条 -110 更可能来自:
1) 第二次 CMD5 携带电压参数等待 ready;
2) CMD3 获取 RCA;
3) CMD7 选卡;
4) CMD52 读取 CCCR、FBR 或 CIS;
5) Function 初始化或注册前的其他访问。
排查方向
1) 测量 Wi-Fi 供电、WL_REG_ON 和所需的 32 kHz 时钟;
2) 检查 USDHC pinctrl、CLK/CMD/DAT 接线和上拉;
3) 检查 bus-width 及 non-removable/cd-gpios;
4) 查看 /proc/interrupts 中 USDHC IRQ 计数是否增长;
5) 用示波器或逻辑分析仪确认 CMD5 是否发出、设备是否返回响应;
6) 临时降低 max-frequency 帮助判断时序和信号问题。
2.2 -84:EILSEQ
-84 通常表示 CRC 或命令/数据序列校验错误,说明控制器往往已经接收到信号,但采样或校验没有通过。常见原因
1) 频率过高或采样时序不合适;
2) 走线、焊接、地参考或信号完整性差;
3) CMD/DAT 缺少正确上拉;
4) 电压不稳定或电平不匹配;
5) bus-width 错误,某根 DAT 线异常;
6) pinctrl 驱动强度或高速状态配置不合适。
临时降频后恢复,通常说明高速时序或信号完整性存在问题;但降频只是诊断方法,最终仍应检查电源、上拉、走线、焊接和 pinctrl 配置。
3. SDIO 枚举成功,但驱动没有匹配
检查是否枚举成功
ls -l /sys/bus/sdio/devices
cat /sys/bus/sdio/devices/mmc*:*/uevent
cat /sys/bus/sdio/devices/mmc*:*/modalias
如果能看到:
mmc0:0001:1
mmc0:0001:2
通常说明 CMD5、CMD3、CMD7、CMD52 以及 CCCR/FBR/CIS 读取基本成功,MMC Core 已经创建了对应的 sdio_func。这个名字的含义是:
mmc0:0001:1
| | |
| | +-- Function 编号 1
| +------ 卡的 RCA,按 4 位十六进制显示,这里是 0x0001
+----------- mmc_host 名称/索引 mmc0
如果设备存在但没有 driver 链接:
ls -l /sys/bus/sdio/devices/mmc*:*/driver
常见原因
1) BCMDHD 模块没有加载;
2) CONFIG_BCMDHD 或 CONFIG_BCMDHD_SDIO 没有启用;
3) 驱动 id_table 不包含当前 Function 的 class/vendor/device;
4) 加载了不匹配的 Wi-Fi 驱动版本;
5) 驱动注册失败。
4. SDIO 驱动匹配成功,但 probe 失败
典型日志
sdioh_probe: sdioh_attach failed
sdioh_probe: bcmsdh_probe failed
bcmsdh_sdmmc: Failed to enable F1
bcmsdh_sdmmc: Failed to set F1 blocksize
bcmsdh_sdmmc: Failed to set F2 blocksize
这说明 sdio_func 已经匹配到驱动,但驱动在访问寄存器、使能 Function、设置块大小或初始化芯片时失败。
排查方向
1) 确认实际 Wi-Fi 芯片型号与 BCMDHD 驱动版本匹配;
2) 确认 Function 1/2 枚举正确;
3) 检查 CMD52 访问是否有 -110、-84 或 -5;
4) 检查供电、复位、时钟及 SDIO 信号;
5) 检查固件和 NVRAM 是否属于当前芯片及板卡。
5. 固件或 NVRAM 加载失败
典型日志
firmware path not found
nvram path not found
failed to download firmware
dongle nvram file download failed
dhd_bus_init failed
failed bus is not ready
sdio设备固件的下载流程
1) BCMDHD 先从根文件系统读取固件和 NVRAM 文件到主机内存;
2) 再通过 SDIO 把它们下载到 Wi-Fi 芯片内部 RAM;
3) 批量传输通常主要使用 CMD53;
4) 寄存器和控制访问会使用 CMD52;
5) 默认路径可以由内核配置、平台数据或模块参数决定,并非永远不可修改。
当前工程默认路径
/lib/firmware/bcm/ZP_BCM4339/fw_bcmdhd.bin
/lib/firmware/bcm/ZP_BCM4339/bcmdhd.ZP.OOB.cal
需要同时确认
1) 文件存在且根文件系统挂载正常;
2) 路径、权限和文件内容正确;
3) 固件版本与 BCM4339 及当前驱动匹配;
4) NVRAM/校准文件与具体模组和 PCB 匹配;
5) 下载过程没有 CMD52/CMD53 错误。
成功标志通常包括:
Firmware up: op_mode=..., MAC=...
文件存在但 NVRAM 不匹配,仍可能出现 RF 频段、发射功率、天线链、MAC 地址或扫描异常。
6. CMD53 运行时收发失败
典型日志
CMD53 write failed with code -110
CMD53 read failed with code -84
TX FAILED ... ERR=-110
RX FAILED ... ERR=-84
RXHEADER FAILED
这些日志通常说明:
1) SDIO 卡已经枚举;
2) 驱动通常已经匹配;
3) 问题发生在固件下载或运行期数据传输阶段。
数据链
BCMDHD
-> sdio_memcpy_toio()/sdio_readsb()/sdio_memcpy_fromio()
-> mmc_io_rw_extended() 构造 CMD53
-> mmc_wait_for_req()
-> mmc_host->ops->request()
-> USDHC
判断
-110
本次命令或数据请求超时。可能是设备没有完成响应,也可能是 USDHC IRQ、
时钟、DMA 或控制器状态异常,不能只凭该错误断定一定是设备不应答。
-84
CRC 或采样时序错误,优先检查频率、信号质量、电压、上拉和 pinctrl。
-5
一般 I/O 错误,需要结合它前面的 MMC、SDHCI 和 BCMDHD 日志寻找原始错误。
RXHEADER FAILED
可能是前面的 CMD53 读取失败,也可能是读取到的 Wi-Fi 协议头损坏,必须结合
之前是否出现 -110、-84、RX FAILED 等日志判断。
如果只在大流量时出错,重点检查:
1) Wi-Fi 供电瞬时压降;
2) 高速 CLK/CMD/DAT 信号完整性;
3) USDHC DMA 和中断;
4) 块大小、对齐和驱动版本兼容性;
5) runtime PM、时钟门控和休眠唤醒。
如果降频后恢复,基本指向高速时序或信号完整性,但仍应完成硬件根因检查。
7. 中断相关问题
SDIO Wi-Fi 中常见两类中断:
1) USDHC 命令/数据完成中断
表示 CMD52/CMD53 请求已经完成,由 USDHC 控制器触发,进入 sdhci_irq()。
2) SDIO Function 数据就绪中断
表示 Wi-Fi 芯片内部有事件或接收数据,可能通过 DAT1 带内中断或独立 OOB GPIO。
OOB 错误出现的日志时Host failed to register for OOB。排查方向
1) OOB GPIO 和 pinctrl;
2) interrupts、interrupt-parent 和触发边;
3) GPIO 是否被其他驱动占用;
4) /proc/interrupts 中的计数是否增长;
5) NVRAM 和驱动是否配置成一致的带内/OOB 模式。
五、usb框架
我们简单梳理一下 Linux USB 框架的大致流程。
1、设备树中的USB节点首先生成i.MX USB平台设备。i.MX适配驱动使用ci_hdrc_imx_data保存时钟、PHY、USBMISC和电源管理等平台私有状态,同时把MEM、IRQ资源以及ci_hdrc_platform_data传给通用ChipIdea子设备。
2、ChipIdea核心创建ci_hdrc作为整个控制器的中心状态对象,并根据ci_hdrc_platform_data.dr_mode初始化Host或Gadget对应的ci_role_driver。选定角色后,ci_role_start()调用对应角色的start()。
3、Host角色的host_start()创建通用Host控制器对象usb_hcd,并让它使用EHCI操作表ci_ehci_hc_driver。随后调用usb_add_hcd()将HCD内部的usb_bus注册到USB Core,同时创建并注册虚拟Root Hub。
4、Root Hub检测到设备插入后,Hub驱动创建usb_device,通过Endpoint 0发送控制传输,完成端口复位、读取描述符、设置地址和设置Configuration。USB Core解析Configuration、Interface和Endpoint描述符,并在/sys/bus/usb/devices/中注册usb_device及对应的usb_interface。
5、USB功能驱动以usb_driver形式注册到usb_bus_type,通常与usb_interface匹配。匹配成功后,OUT输出功能:驱动通过URB提交数据,最终经过usb_hcd_submit_urb()和hc_driver->urb_enqueue()交给EHCI硬件传输。
6、IN接收时功能:驱动必须提前提交接收URB,由EHCI主动发送IN请求,设备有数据就返回DATA、没有数据就返回NAK,收到的数据通过DMA写入URB缓冲区,传输完成后EHCI产生中断,经usb_hcd_giveback_urb()调用urb->complete()通知功能驱动,而连续接收则在完成回调中重新提交IN URB。
总览
第1层:i.MX平台适配
设备树 → platform_device → ci_hdrc_imx_data / ci_hdrc_platform_data
↓
第2层:ChipIdea核心与角色
ci_hdrc → ci_role_driver → ci_role_start()
↓
第3层:Host控制器抽象
host_start() → usb_hcd → hc_driver(EHCI) → usb_bus / Root Hub
↓
第4层:设备枚举与驱动匹配
usb_device → Configuration → usb_interface → Endpoint → usb_driver
↓
第5层:OUT发送
SoC内存 → DMA → EHCI → USB设备OUT Endpoint
↓
第6层:IN接收
USB设备IN Endpoint → EHCI → DMA → SoC内存 → urb->complete()
先记住三个边界:
ci_hdrc 是整个 ChipIdea 控制器的中心状态对象,不只是角色层。
usb_driver 是外接 USB 设备的功能驱动;hc_driver 是 SoC Host 控制器的硬件操作表。
USB Host 模式下,发送和接收都由 Host 先提交 URB;设备不能绕过 Host 调度自行向 SoC 推送普通数据。
第1层:设备树与 i.MX 平台适配
1.1 主要职责
设备树中的 usbotg1、usbotg2 节点生成 i.MX USB platform_device。i.MX 平台驱动 ci_hdrc_imx_probe() 负责处理 SoC 特有资源,包括:
时钟;
USB PHY;
USBMISC;
ANATOP;
regulator、pinctrl 和 Runtime PM;
MEM、IRQ 等 platform_device 资源。
1.2 两个容易混淆的数据对象
struct ci_hdrc_imx_data {
struct usb_phy *phy;
struct platform_device *ci_pdev;
struct clk *clk;
struct imx_usbmisc_data *usbmisc_data;
struct regmap *anatop;
bool supports_runtime_pm;
bool in_lpm;
/* 省略其他i.MX平台字段 */
};
ci_hdrc_imx_data 是父 i.MX USB 设备的私有运行状态,主要供 i.MX 平台驱动自己使用。它不代表外接 USB 设备,也不是 HCD。
struct ci_hdrc_platform_data {
const char *name;
struct phy *phy;
struct usb_phy *usb_phy;
enum usb_phy_interface phy_mode;
unsigned long flags;
enum usb_dr_mode dr_mode;
int (*notify_event)(struct ci_hdrc *ci, unsigned event);
};
ci_hdrc_platform_data 是从 i.MX 平台层传递给通用 ChipIdea 核心的配置包,除了 dr_mode,还包含 PHY、平台 flags、VBUS 和事件回调等配置。
1.3 数据关系
i.MX父platform_device
├─ drvdata ───────────────→ ci_hdrc_imx_data
├─ MEM/IRQ resources ─────┐
└─ ci_hdrc_platform_data ─┼→ ci_hdrc子platform_device
└→ 由通用ChipIdea驱动接管
MEM、IRQ 不主要保存在 ci_hdrc_imx_data 中,而是作为 platform_device 资源传给 ChipIdea 子设备,最终由 ci_hdrc 保存映射后的寄存器信息和 IRQ 号。
第2层:ChipIdea核心与角色管理
2.1 ci_hdrc 中心对象
struct ci_hdrc {
struct device *dev;
struct hw_bank hw_bank;
int irq;
struct ci_hdrc_platform_data *platdata;
struct ci_role_driver *roles[CI_ROLE_END];
enum ci_role role;
struct usb_hcd *hcd; /* Host角色 */
struct usb_gadget gadget; /* Gadget角色 */
/* 省略其他状态 */
};
ci_hdrc 表示一套 ChipIdea 控制器实例,集中保存寄存器、IRQ、PHY、平台配置、当前角色以及 Host/Gadget 对象。
2.2 角色操作表
struct ci_role_driver {
int (*start)(struct ci_hdrc *ci);
void (*stop)(struct ci_hdrc *ci);
irqreturn_t (*irq)(struct ci_hdrc *ci);
void (*suspend)(struct ci_hdrc *ci);
void (*resume)(struct ci_hdrc *ci, bool power_lost);
const char *name;
};
ChipIdea 根据 ci->platdata->dr_mode 初始化角色:
USB_DR_MODE_HOST
└─ ci_hdrc_host_init()
└─ ci->roles[CI_ROLE_HOST]
USB_DR_MODE_PERIPHERAL
└─ ci_hdrc_gadget_init()
└─ ci->roles[CI_ROLE_GADGET]
USB_DR_MODE_OTG
└─ 同时准备Host和Gadget,再根据ID/VBUS等条件选择当前角色
选定角色后执行:
ci_role_start(ci, ci->role);
它最终调用:
ci->roles[ci->role]->start(ci);
当前 100ask 设备树将 usbotg1 和 usbotg2 都配置成 Host,所以主线进入 host_start(),通常不会动态切换为 Gadget。
第3层:Host控制器、EHCI与USB Core
3.1 创建 usb_hcd
Host 角色启动时:
hcd = __usb_create_hcd(&ci_ehci_hc_driver, ...);
hcd->regs = ci->hw_bank.abs;
usb_add_hcd(hcd, 0, 0);
ci->hcd = hcd;
usb_hcd 表示一套 Host 控制器实例:
struct usb_hcd {
struct usb_bus self;
const struct hc_driver *driver;
void __iomem *regs;
struct phy *phy;
struct usb_phy *usb_phy;
/* 省略状态、Root Hub和DMA字段 */
};
它内嵌一条逻辑 USB 总线 self,并通过 driver 指向硬件操作表。
3.2 hc_driver 不是外设驱动
struct hc_driver {
irqreturn_t (*irq)(struct usb_hcd *hcd);
int (*reset)(struct usb_hcd *hcd);
int (*start)(struct usb_hcd *hcd);
void (*stop)(struct usb_hcd *hcd);
int (*urb_enqueue)(struct usb_hcd *hcd,
struct urb *urb, gfp_t mem_flags);
int (*urb_dequeue)(struct usb_hcd *hcd,
struct urb *urb, int status);
int (*hub_status_data)(struct usb_hcd *hcd, char *buf);
int (*hub_control)(struct usb_hcd *hcd, ...);
};
本板 Host 路径使用 ci_ehci_hc_driver,其核心传输实现来自 EHCI。它负责控制器启停、中断、DMA 队列、Root Hub 端口和 URB 入队。
3.3 注册 HCD 与 Root Hub
usb_add_hcd() 主要完成:
usb_add_hcd(hcd)
├─ usb_register_bus(&hcd->self)
├─ 创建虚拟Root Hub usb_device
├─ 调用hc_driver->reset()
├─ 调用hc_driver->start()
└─ 注册Root Hub
Root Hub 是 Host 控制器端口的软件表示,不是 USB 线上外接的一颗普通 Hub 芯片。它的端口状态和控制请求主要通过 hc_driver->hub_status_data()、hub_control() 访问控制器寄存器。
第4层:设备枚举、Interface与驱动匹配
4.1 插入与枚举
Root Hub 或外接 Hub 发现端口连接变化后,Hub 驱动开始枚举:
端口状态变化
→ hub_event()
→ hub_port_connect()
→ usb_alloc_dev()
→ 端口复位
→ 通过Endpoint 0发送控制传输
→ usb_new_device()
Endpoint 0 上的典型请求包括:
GET_DESCRIPTOR 读取设备和配置描述符
SET_ADDRESS 分配USB地址
SET_CONFIGURATION 选择并启用配置
USB Core 解析描述符后建立:
usb_device
└─ usb_host_config
└─ usb_interface
└─ usb_host_interface
└─ usb_host_endpoint
其中:
usb_device表示整个物理USB设备;
usb_interface表示一个可独立匹配驱动的功能接口;
usb_host_endpoint表示真正收发数据的Endpoint;
Interface组织Endpoint,但Interface本身不直接发送数据。
注册后可以在 /sys/bus/usb/devices/ 看到设备和 Interface,例如:
1-1 整个usb_device
1-1:1.0 Interface 0
1-1:1.1 Interface 1
4.2 usb_driver 匹配 Interface
struct usb_driver {
const char *name;
int (*probe)(struct usb_interface *intf,
const struct usb_device_id *id);
void (*disconnect)(struct usb_interface *intf);
const struct usb_device_id *id_table;
};
功能驱动通过 usb_register() 注册到 usb_bus_type,通常根据设备 ID 或 Interface class 匹配 usb_interface。
usb_driver 不是 USB Core 内部的 HCD 操作表;它表示 U 盘、HID、USB 串口、USB 网卡等外设功能驱动。
第5层:OUT方向------SoC向USB设备发送数据
5.1 OUT URB
具体 USB 功能驱动选择 Interface 中的 OUT Endpoint,准备发送缓冲区并构造 URB:
struct urb {
struct usb_device *dev;
struct usb_host_endpoint *ep;
unsigned int pipe;
void *transfer_buffer;
u32 transfer_buffer_length;
u32 actual_length;
int status;
void *context;
usb_complete_t complete;
};
URB 描述"向哪个设备、哪个 Endpoint、使用什么传输类型、传多少数据以及完成后调用谁"。
5.2 OUT调用链
上层业务产生数据
→ USB功能驱动填充OUT URB
→ usb_submit_urb()
→ usb_hcd_submit_urb()
→ 根据urb->dev->bus找到usb_hcd
→ DMA映射
→ hcd->driver->urb_enqueue()
→ ehci_urb_enqueue()
→ 建立EHCI QH/qTD
→ EHCI通过DMA读取SoC内存
→ Host发送OUT Token和DATA
→ 设备返回USB链路层ACK
→ EHCI产生完成中断
→ usb_hcd_giveback_urb()
→ urb->complete()
数据方向:
SoC内存 → DMA → EHCI → USB PHY → USB设备OUT Endpoint
USB 链路层 ACK 由设备控制器和 Host 控制器自动处理,不需要功能驱动另外提交一个 URB 确认。
第6层:IN方向------USB设备向SoC返回数据
6.1 必须先提交 IN URB
USB Host 控制总线调度。设备即使已经把采集数据放进 IN Endpoint 缓冲区,也不能直接触发一次普通 USB 数据传输;SoC 端功能驱动必须先准备接收缓冲区并提交 IN URB。
功能驱动准备接收buffer
→ 填充IN URB
→ usb_submit_urb()
→ USB Core和EHCI建立IN方向QH/qTD
→ Host主动发送IN Token
设备收到 IN Token 后:
IN Endpoint没有数据 → 返回NAK,EHCI之后继续询问
IN Endpoint已有数据 → 返回DATA0/DATA1
→ Host硬件自动返回ACK
即使名为"Interrupt IN Endpoint",也仍然是 Host 按照 bInterval 周期轮询,不是设备用独立中断线把普通数据直接推给 SoC。
6.2 IN完成链
USB设备采集数据
→ 数据进入设备端IN Endpoint缓冲区
→ EHCI发送IN Token
→ 设备返回DATA
→ EHCI通过DMA写入URB buffer
→ EHCI产生传输完成中断
→ hc_driver->irq()
→ usb_hcd_giveback_urb()
→ urb->complete()
→ 功能驱动处理urb->actual_length字节
连续接收通常在完成回调中重新提交 URB:
static void rx_complete(struct urb *urb)
{
if (urb->status == 0 && urb->actual_length)
process_data(urb->transfer_buffer, urb->actual_length);
/* 请求下一批IN数据,不是确认上一批数据 */
usb_submit_urb(urb, GFP_ATOMIC);
}
数据方向:
USB设备IN Endpoint → USB PHY → EHCI → DMA → SoC内存
重新提交 IN URB 表示"继续接收下一批数据"。如果设备自己的上层协议另外规定应答命令,功能驱动才需要再提交 OUT URB;这与 USB 协议自动完成的 ACK 不是一回事。
六、USB 框架的注册日志
1、创建ChipIdea子设备
节点:/sys/bus/platform/devices/ci_hdrc.N
函数:ci_hdrc_add_device() → platform_device_add()
日志:成功通常不打印;失败打印:
ci_hdrc_add_device failed, err=-xx
含义:ChipIdea平台子设备已经创建。
2、ChipIdea驱动绑定
节点:/sys/bus/platform/drivers/ci_hdrc/ci_hdrc.N
函数:ci_hdrc_probe() → ci_role_start()
日志:Host角色启动后通常看到:
ci_hdrc.N: EHCI Host Controller
含义:ci_hdrc_driver已经绑定,并开始启动Host角色。
3、注册HCD和USB总线
节点:/sys/bus/usb/devices/usbN
函数:__usb_create_hcd() → usb_add_hcd()
日志:
ci_hdrc.N: new USB bus registered, assigned bus number N
ci_hdrc.N: USB 2.0 started, EHCI 1.00
含义:HCD和EHCI控制器已经注册并启动。
4、注册虚拟Root Hub接口
节点:/sys/bus/usb/devices/N-0:1.0
函数:register_root_hub() → usb_new_device() → hub_probe()
日志:
hub N-0:1.0: USB hub found
hub N-0:1.0: 1 port detected
含义:虚拟Root Hub注册完成,可以检测USB设备插拔。
七、IIC框架
下面将对 I²C 的核心框架内容进行简要介绍。
1、i.MX I2C控制器驱动解析设备树资源后,分配包含i2c_adapter的私有对象imx_i2c_struct,完成寄存器映射、时钟、IRQ、等待队列和i2c_algorithm初始化,
2、然后把已有的adapter注册到I2C Core。Core根据设备树创建i2c_client并完成从设备驱动匹配。
3、传输时,从设备驱动通过client->adapter提交i2c_msg[],Core调用adapter->algo->master_xfer()进入i2c_imx_xfer();
4、PIO传输期间线程睡眠在私有等待队列上,控制器每完成一个字节便通过Host IRQ调用wake_up(),最终同步返回传输结果。
1. 核心对象
imx_i2c_struct i.MX控制器私有对象
└── i2c_adapter 通用I2C Host/总线抽象
└── i2c_algorithm Host操作表
└── master_xfer = i2c_imx_xfer
i2c_client 一个I2C从设备
├── addr 7位从设备地址
└── adapter 所属I2C Host
i2c_msg 一段传输数据
├── addr 从设备地址
├── flags 读写方向
├── len 数据长度
└── buf 数据缓冲区
一句话理解:i2c_adapter是通用Host抽象,imx_i2c_struct是它在i.MX6ULL上的具体硬件实现,i2c_client是挂在Host上的从设备,i2c_msg是传输载体。
2. 第一层:设备树与Host初始化
设备树描述两类设备:
i2c2@021a4000 I2C Host控制器
└── gt9xx@5d 地址为0x5d的I2C从设备
初始化流程:
设备树
└── of_platform_default_populate() // 创建控制器platform_device
└── compatible匹配
└── i2c_imx_probe()
├── devm_ioremap_resource() // 映射寄存器
├── devm_kzalloc() // 分配imx_i2c_struct
├── devm_clk_get() // 获取并使能时钟
├── devm_request_irq() // 注册Host IRQ
├── init_waitqueue_head() // 初始化PIO等待队列
├── adapter.algo = &i2c_imx_algo // 设置Host操作表
└── i2c_set_adapdata() // adapter关联私有对象
私有对象保存控制器运行所需的资源:
imx_i2c_struct
├── adapter 通用Host对象,直接内嵌
├── base 寄存器虚拟地址
├── clk 控制器时钟
├── queue PIO等待队列
├── i2csr 中断状态
├── bitrate 总线频率
└── dma 可选DMA资源
注意:i2c_adapter不是I2C Core创建的。它内嵌在imx_i2c_struct中,分配私有对象时就已经存在。
3. 第二层:注册adapter、创建client并匹配驱动
i2c_add_numbered_adapter(&i2c_imx->adapter)
└── i2c_register_adapter()
├── 检查adapter->algo
├── 初始化bus_lock、timeout等通用字段
├── device_register() // 注册为i2c-N
└── of_i2c_register_devices()
└── 遍历控制器设备树子节点
└── i2c_new_device()
└── 创建i2c_client
GT9xx设备树节点会形成:
i2c_client
├── addr = 0x5d
├── adapter = i2c2对应的i2c_adapter
└── dev.of_node = gt9xx设备树节点
驱动匹配:
i2c_client + goodix_ts_driver
└── i2c_device_match() // 匹配compatible或id_table
└── i2c_device_probe()
└── gtp_probe(client, id)
i2c_client和i2c_driver谁先注册都可以。匹配成功后,从设备驱动直接通过client->adapter找到对应Host。
4. 第三层:提交并执行i2c_msg\[\]
从设备驱动构造i2c_msg[]并提交:
从设备驱动
└── i2c_transfer(client->adapter, msgs, num)
├── i2c_lock_bus() // 锁住总线
└── __i2c_transfer()
└── adapter->algo->master_xfer()
└── i2c_imx_xfer()
i2c_imx_xfer()通过i2c_get_adapdata(adapter)取回imx_i2c_struct,随后操作具体硬件:
i2c_imx_xfer()
├── i2c_imx_start() // START
├── 遍历i2c_msg[]
│ ├── 后续消息产生Repeated START
│ ├── i2c_imx_write() // 写消息
│ └── i2c_imx_read() // 读消息
├── i2c_imx_stop() // STOP
└── 返回消息数或负错误码
典型寄存器读使用两个消息:
msgs[0]:写寄存器地址
msgs[1]:读寄存器数据
START → 地址+W → 寄存器地址 → Repeated START
→ 地址+R → DATA... → NACK → STOP
i2c_transfer()成功返回完成的消息数量。两消息组合读成功返回2,不是返回字节数。
5. 第四层:PIO中断和等待队列
PIO模式下,每发送或接收一个地址/数据字节,都要等待控制器完成:
i2c_imx_write()/i2c_imx_read()
├── 读写I2DR寄存器
└── i2c_imx_trx_complete()
└── wait_event_timeout(queue) // 当前传输线程睡眠
硬件完成一个字节
└── 置位I2SR.IIF并触发Host IRQ
└── i2c_imx_isr()
├── 保存并清除中断状态
└── wake_up(queue) // 唤醒传输线程
└── 检查ACK并继续下一字节
整组消息完成后,i2c_imx_xfer()逐层返回,最后i2c_transfer()同步返回给从设备驱动。
这里的"同步"表示调用线程必须等传输完成;底层虽然由中断推进,但不会在最后调用从设备驱动的异步完成回调。
6. 两个容易混淆的点
① PIO和DMA的唤醒方式不同
PIO:wait_event_timeout() + wake_up()
DMA:wait_for_completion_timeout() + complete()
② Host IRQ和从设备IRQ不同
Host控制器IRQ
└── i2c_imx_isr() // 通知一个地址/数据字节传输完成
GT9xx从设备IRQ
└── Goodix中断处理函数 // 通知触摸数据已经准备好
└── CPU再调用i2c_transfer()读取数据
iic框架和spi框架类似,常见的错误也是类似,可以直接进行参考。