ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

高通Wi-Fi驱动核心:MHI协议与PCIe传输机制解析

高通Wi-Fi驱动核心:MHI协议与PCIe传输机制解析 高通 Wi-Fi 驱动实录揭秘 PCIe 总线上的“搬运工”——MHI 协议详解熟悉高通平台 Wi-Fi 驱动的朋友应该都有这种感觉表面上看驱动要处理的是 ath11k、WCN6855、QCA6390 这些芯片可真把代码追进去以后最先拦住你的往往不是射频和 802.11 协议而是 PCIe 总线上那套低调却无处不在的 MHI 机制。MHI 的全称是 Modem Host Interface最早是高通为了 modem 和 host 之间的通信设计的但后来却成了 Wi-Fi、Atheros 网卡、甚至部分车载芯片对外数据通路的标准骨架。写这篇文章我就是想把这条藏在驱动底层的“搬运流水线”掰开揉碎讲清楚它解决什么问题、内核里是怎么组织的、驱动初始化时发生过什么、以及我踩过的那些坑。这篇文章适合正在啃高通 Wi-Fi 驱动的内核开发者也适合刚接触 PCIe 外设驱动、想搞懂 MHI 到底在传输层干了什么的人。我不会只贴宏定义和结构体会更侧重“为什么这么设计”和“出问题怎么排查”。如果你已经看过一遍 mhi 核心代码但觉得没抓住主线这篇应该能帮你把散落的点串起来。1. 背景与思路为什么 PCIe 上的 Wi-Fi 需要“搬运协议”1.1 高通 Wi-Fi 设备的 PCIe 连接形态高通目前主流 Wi-Fi 6 / 6E 芯片大多采用 SoC 内集成的 PCIe Root Complex 连接独立 Wi-Fi 芯片形态上是“处理器端 PCIe RC 无线端 PCIe EP”的标准组合。以 QCA6390 或 WCN6855 这类芯片为例它们在硬件上更像一个完整的通信子系统有自己的 CPU、DSP、协议栈固件而不是一颗简单的寄存器型外设。这意味着 host 和 Wi-Fi 芯片之间需要传递的内容非常复杂不只是几条配置寄存器而是包括固件镜像、QMI 控制消息、WMI 无线管理命令、网络数据包、以及大量异步事件。如果我们只用普通 PCIe bar 寄存器来传递这些信息要么软件轮询消耗 CPU要么中断泛滥把系统打残。所以高通的方案是在 PCIe 之上再加一层抽象的、支持多通道、多队列的传输协议这就是 MHI。从内核视角看一颗 PCIe 接口的高通 Wi-Fi 芯片在 lspci 输出里一般长这样$ lspci -nn | grep -i qualcomm 01:00.0 Network controller [0280]: Qualcomm Device [17cb:1101] (rev 01)能看到它的 vendor 是新加的 17cb而不是老的 Atheros 168c因为 MHI 方案下的设备已经被当作独立的“host interface controller”来枚举了。映射到内存后驱动会拿到一个 BAR 区域里面既包含 MHI 的寄存器集合也包含 MMIO 方式访问的 doorbell 寄存器。整个 PCIe 设备对驱动来说就是一块“控制寄存器 数据搬运总线”的组合。1.2 从 modem 走来的 MHI为什么被 Wi-Fi 选中MHI 最初被设计用于 modem 与 application processor 之间的通信核心目标有两个一是用尽量少的硬件资源承载“多通道、多方向、高吞吐”的数据流二是让 host 侧不需要关心 device 内部的内存布局和消息格式。Wi-Fi 芯片采纳 MHI 其实是顺理成章的事。Wi-Fi 驱动除了数据面还有很大的控制面需要下载固件、需要和 firmware 进行 QMI 握手、需要通过 WMI 下发扫描/连接/断开/信道切换等命令。这些消息长短不一、实时性要求不同、流量也不算小如果每一种都单独设计一套传输机制驱动会迅速失控。MHI 的好处在于它天然提供了一条“命令通道 数据通道 事件通知”的统一总线抽象只要固件和 host 都遵守同样的 ring 和 doorbell 规则上层驱动完全不用关心某个消息到底走了哪个物理缓冲。另外还有一个很实际的理由链路复用。一颗 Wi-Fi 芯片未来可能要同时承载蓝牙、定位、甚至其它协同处理能力。MHI 的多 channel 模型天然适合做一个“n-to-1”的复用器每一个功能模块独占一个或几个 channel互不干扰。这样驱动架构从第一天起就是可扩展的而不是等到功能变多之后再推翻重来。1.3 搬砖模型用仓库、货单和门铃理解 MHI 的数据流我第一次看 MHI 代码时被 tre、ring、doorbell、event 这些概念绕得头晕后来找到一个比较顺的类比分享给大家假设你要往一座仓库里搬运货物仓库管理员叫固件搬运工叫 MHI core而你本人就是 Wi-Fi 驱动。环形缓冲区ring就是仓库门口的一条传送带上面有很多固定大小的货位每个货位叫一个treTransfer Ring Element。你把货数据 buffer放到传送带货位上写清楚“这格是什么、长度多少、在哪个 channel”这就是填充 tre。放完之后你不能站在原地等管理员发现必须去按一下墙上的电铃这个电铃就是doorbell 寄存器。管理员听到铃声后会顺着传送带拿走货物并在另一个传送带上放回一张“已处理”的回执——这个回执队列叫event ring。你看到回执之后才可以把对应的 buffer 释放或复用。这套模型几乎盖住了 MHI 的全部数据面逻辑。控制面的固件下载、QMI 握手、WMI 命令本质上也走同样的传送带只是 channel 编号不同、event ring 的回执处理逻辑不同罢了。理解了这个模型后面代码不管多复杂你永远能定位到“当前是在往货位里写 tre还是在按门铃或者在看回执”。2. MHI 核心机制拆解2.1 MHI 状态机与复位流程MHI 不是一上电就能传数据它有一套完整的状态机。主机侧通过操作 MHI 寄存器来推动状态迁移设备侧也会在状态寄存器里反映当前所处阶段。常见状态包括MHI_STATE_RESET、MHI_STATE_READY、MHI_STATE_M0运行态、MHI_STATE_M1低功耗、MHI_STATE_M2休眠等。在drivers/bus/mhi中状态管理核心路径如下驱动注册后MHI core 会先把设备置于 RESET 状态写MHIREG_CTRL触发复位然后等待设备通过中断或状态轮询进入 READY 状态最后把状态推进到 M0。M0 状态意味着底层物理链路已经就绪channel 才能正常收发。实际调试中最常遇到的问题是设备一直停在 RESET 或死活进不了 READY。常见原因BAR 地址没映射对控制寄存器读回来全是 0xFF或者电源/时钟没起来设备根本没有能力响应寄存器操作还有一类的坑是 PCIe 链路被 BIOS 或上层固件设成了低功耗模式设备虽然枚举到了但没法正常响应 MMIO。所以遇到状态机卡住时我的习惯是先看两个东西/sys/bus/pci/devices/.../config里的 device 是否响应以及 MHI 控制寄存器读出来的十六进制值是否符合规格。不要一上来就去翻驱动代码很多时候问题根本不在 MHI 逻辑内部。2.2 三种 Ringchannel ring、event ring 与 command ringMHI 的数据面由三类 ring 组成。channel ring每个 channel 对应一条数据流水线方向由 channel 编号和配置决定。比如用于固件下载的 BHI channel、用于 QMI 消息的控制 channel、用于 WMI 数据的 channel。host 和 device 各自维护rpread pointer和wpwrite pointer但两边看到的是同一块 DMA 内存。event ringdevice 用来向 host 发送“处理完成”回执的 ring。它不是按 channel 一对一而是多个 channel 可以共用一个 event ring。这样设计是为了减少中断源否则几十个 channel 每个都触发一次 MSI系统会先被中断打爆。command ringhost 给 device 下发命令用比如注册 channel、启动 channel、复位 channel。命令 ring 也对应一个 doorbell但它和普通数据 doorbell 是分开的。理解这三类 ring 的关键是记住任何 ring 都只是一段环形 DMA buffer用指针表示生产者和消费者位置。MHI 协议真正狠的地方在于这些 ring 的操作规则可以在无锁情况下工作——生产端写完新的 tre 并做内存屏障然后通过 doorbell 通知消费端消费端处理完之后更新自己的指针再通过 event ring 机制通知生产端。两端不会同时修改同一个指针所以不需要传统意义上的锁。用代码看方便更多struct mhi_ring { struct mhi_buf_info *ctxt; // 上下文指针 void *base; // ring 基地址 dma_addr_t dma_addr; // DMA 地址 size_t el_size; // 每个元素大小 size_t len; // ring 总长度 size_t wp; // 写指针 size_t rp; // 读指针 };这里的wp和rp是相对于 ring 首地址的元素偏移不是绝对地址。实际数据要动到哪个内存地址还需要通过上下文里的iova换算。这也就是为什么有些驱动 bug 看起来像是“数据错乱”往 ring 里写的位置和 device 读的位置差了固定偏移。2.3 Doorbell 机制与 MSI 中断Doorbell 是 MHI 和普通 DMA 驱动最大的区别所在。PCIE 设备通常有两种通知方式一种靠门铃寄存器一种靠 MSI 中断。MHI 把两者结合得很紧密host 向 device 发数据时写 device 的 doorbell 寄存器device 要给 host 回执时则触发一个 MSI 中断host 在中断处理里扫描对应的 event ring。从实现上看doorbell 其实就是一个 MMIO 写操作地址位于 BAR 空间中。MHI 规范里doorbell 地址不是固定的而是根据 channel 和 event ring 编号计算出偏移。内核里这个计算在mhi_db_addr相关逻辑里完成不同 SoC 的偏移基址可能不同这也是移植高通 Wi-Fi 驱动到新平台时最容易出错的地方之一。中断侧常见的坑是“中断风暴”。MSI 中断触发条件是 event ring 里新增了未处理的事件如果驱动在中断处理里没有及时把 event ring 的读指针更新回设备侧设备会认为 host 一直没处理完于是反复触发中断甚至可能把所有 CPU 核都打满。这种问题通常表现为Wi-Fi 一跑流量/proc/interrupts里对应 MSI 的中断计数暴涨CPU 的 softirq 占用居高不下但吞吐很低。排查的方法是先确认 event ring 的读指针是否及时写回了共享内存再看是否用了正确的内存屏障。我见过有人改了 DMA 缓冲区的大小之后event ring 配置里长度字段没同步更新导致设备实际认为它能写入的区域和 host 配置的内存区域不一致最后中断像机关枪一样扫射。2.4 内存屏障与一致性问题MHI 是典型的需要开发者手动维护一致性的协议。因为 host 和 device 共享内存而 PCIe 体系里有复杂的写入排序规则如果不做屏障就会出现 host 明明已经把 tre 写进 ring设备却读到旧数据的情况。内核里通常用dma_wmb()在提交 tre 后、doorbell 写之前插入屏障。含义是保证 tre 内容的写入对 device 可见然后再去按门铃。反过来在事件回执路径中读 event ring 之前需要dma_rmb()或readl配合避免 CPU 缓存导致读到过期的回执。这个“先数据后门铃”的顺序极其重要。有一次我把一段新加的 TX 路径里的dma_wmb()漏掉了表现非常隐蔽平时不发流量没事一旦大流量并发偶尔出现某几包数据内容不对抓包也看不出规律。最后用强一致性工具对比 host 发送缓冲和 device 回显内容才发现是典型的写重排问题。所以大家在往 MHI 路径里加自己的逻辑时一定要管住手不要把工程师自己习惯的普通内存赋值当成已经同步到外设了。3. 驱动初始化全流程实操3.1 从 pci_probe 开始以 ath11k 驱动为例PCIe 探测函数的简化流程大致是static int ath11k_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct ath11k_pci *ab_pci; int ret; ret pci_enable_device(pdev); if (ret) return ret; pci_set_master(pdev); ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(64)); if (ret) { dev_err(pdev-dev, dma set mask failed\n); goto err_disable; } ret pci_request_mem_regions(pdev, ath11k); if (ret) goto err_disable; ret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); if (ret 0) goto err_release; // 分配并初始化 ath11k_pci 结构 ab_pci ath11k_pci_alloc(pdev); ... // 之后注册 mhi controller、准备 mhi 上电 }这段代码里有几个细节值得注意。第一pci_set_master必做否则 PCIe 设备无法主动发起 DMA后面 ring 的操作会全部无效第二DMA mask 要设成 64 位因为现代平台 IOMMU/SMMU 可能映射出高位地址ring 描述符的地址字段也需要支持 64 位第三MSI 向量数量至少申请 1 个MHI 的事件通知依靠它。实际工作中我还遇到过pci_request_mem_regions失败的情况多半是同一个 PCIe 设备被另一个驱动先占用了比如把网卡同时绑定到了ath11k_pci和某个调试用驱动。这种问题在 dmesg 里一般能看到 EPERM 或资源忙不要怀疑设备坏了去/sys/bus/pci/drivers看看绑定情况就行。3.2 用 BHI 通道下载固件MHI 上电以后第一件正事不是建立 WMI 通道而是下载 Wi-Fi 芯片自身的固件镜
返回列表