四大驱动框架的常见问题

前面的博客中,我们已经分别介绍了 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 从设备是否创建)

3、确认设备与驱动是否匹配

4、设备已匹配,但初始化失败

[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 申请失败)

5、逻辑分析仪快速判断

[三、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. 两类中断的区别)

四、SDIO框架常见问题和排查

[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. 中断相关问题)

五、usb框架

[第1层:设备树与 i.MX 平台适配](#第1层:设备树与 i.MX 平台适配)

[1.1 主要职责](#1.1 主要职责)

[1.2 两个容易混淆的数据对象](#1.2 两个容易混淆的数据对象)

[1.3 数据关系](#1.3 数据关系)

第2层:ChipIdea核心与角色管理

[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层:设备枚举、Interface与驱动匹配

[4.1 插入与枚举](#4.1 插入与枚举)

[4.2 usb_driver 匹配 Interface](#4.2 usb_driver 匹配 Interface)

第5层:OUT方向------SoC向USB设备发送数据

[5.1 OUT URB](#5.1 OUT URB)

[5.2 OUT调用链](#5.2 OUT调用链)

第6层:IN方向------USB设备向SoC返回数据

[6.1 必须先提交 IN URB](#6.1 必须先提交 IN URB)

[6.2 IN完成链](#6.2 IN完成链)

[六、USB 框架的注册日志](#六、USB 框架的注册日志)

1、创建ChipIdea子设备

2、ChipIdea驱动绑定

3、注册HCD和USB总线

[4、注册虚拟Root Hub接口](#4、注册虚拟Root Hub接口)

七、IIC框架

[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_dataspi_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 选择等参数,会在每次 messagetransfer 执行前动态配置。


3. 注册 spi_master 到 SPI Core

随后,spi_imx_probe() 调用 spi_bitbang_start(),最终将 spi_master 注册到 SPI Core,就是把 master 放到对应的链表中,方便以后调用。SPI Core 在注册过程中为这个 master 初始化软件消息队列。


4. spi_masterspi_imx_dataspi_bitbang 的关系

spi_master 本身是 SPI Core 认识的通用控制器抽象,保存控制器能力、消息队列和传输回调。

它不会主动调用 SPI Core,而是由上层设备驱动调用 spi_sync()spi_async() 进入 SPI Core,再由 SPI Core 调度这个 spi_masterspi_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

如果没有,检查设备树子节点的 compatibleregspi-max-frequencystatus


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。即使从设备未连接,主机通常也能完成移位,只是读到 0xff0x00。因此 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 框架中的重点知识。

  1. SDIO 框架复用 MMC Core:SDIO 框架复用了 Linux 的 MMC Core。其中 struct sdhci_host 是 SDHCI 控制器层的对象,主要保存控制器寄存器地址、中断、DMA、当前请求和硬件特性等信息。它只适用于 SDHCI 类控制器,并不是所有 MMC/SD/SDIO 控制器的通用抽象。

  2. sdhci_hostmmc_host 的关系:sdhci_host 通过 host->mmc 指针关联一个 struct mmc_host。而 mmc_host 才是 MMC Core 对 Host 控制器的通用抽象。控制器驱动通过 mmc_add_host() 将它注册到内核。

  3. 注册 Host 后启动扫卡流程:注册时并不会同时创建 mmc_card,而是 MMC Core 随后启动扫卡流程。MMC Core 通过 mmc->ops->request() 调用 SDHCI 驱动发送命令。识别 SDIO 设备时,首先用 CMD5 获得 R4 响应,其中包含设备是否就绪、支持的电压范围、I/O Function 数量以及是否带有存储功能等信息,然后才分配并填写 mmc_card

  4. 读取 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 匹配。

  5. 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_hostmmc_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,不是CSIcard->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. 两类中断的区别

  1. SDIO Function中断:由Wi-Fi芯片通过DAT1或OOB GPIO通知"有数据或事件需要处理",本身不搬运完整数据。

  2. 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 -110ETIMEDOUT

-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 -84EILSEQ

-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 主要职责

设备树中的 usbotg1usbotg2 节点生成 i.MX USB platform_devicei.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 设备树将 usbotg1usbotg2 都配置成 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_clienti2c_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框架类似,常见的错误也是类似,可以直接进行参考。