第二章 内核驱动层移植:高通WLAN内核驱动架构(CNSS)
好,咱们直接进入正题。内核驱动层移植,说白了就是让Linux内核认识你的高通WiFi芯片。我做了这么多年,发现很多新手一上来就急着改代码,结果连驱动框架都没搞清楚。嗯,今天咱们就把CNSS这个核心架构彻底讲明白。
2.1 CNSS驱动架构概览
CNSS,全称是Qualcomm Connectivity Subsystem。你可以把它理解成一个“中间人”——它负责把上层的无线协议栈和底层的硬件隔离开来。
我个人习惯把CNSS分成三层:
- 核心层(Core Layer):提供基础框架,比如设备注册、总线管理
- 总线适配层(Bus Adapter):处理SDIO或PCIe的通信细节
- 硬件抽象层(HAL):直接和固件打交道
为什么要这么分?你想想看,如果今天用的是SDIO接口,明天换成PCIe,难道要把整个驱动重写一遍吗?当然不是。CNSS的设计思路就是“换接口不换逻辑”。
核心要点:移植工作的80%都在总线适配层和HAL层。核心层基本不用动,除非你换了内核大版本。
2.2 SDIO/PCIe接口适配
这里我重点讲SDIO,因为我在项目中遇到最多的就是SDIO适配问题。PCIe相对简单,但原理相通。
先看一个典型的SDIO设备注册代码:
static const struct sdio_device_id wlan_sdio_ids[] = {
{ SDIO_DEVICE(0x0271, 0x0400) }, // 高通WCN3990
{ SDIO_DEVICE(0x0271, 0x0401) }, // 高通WCN3998
{ /* end: all zeroes */ },
};
static struct sdio_driver wlan_sdio_driver = {
.name = "qcom_wlan",
.id_table = wlan_sdio_ids,
.probe = wlan_sdio_probe,
.remove = wlan_sdio_remove,
};
这里有个坑,我曾经踩过——VID/PID一定要和硬件完全匹配。有一次客户拿了个定制芯片,VID是0x0271没错,但PID是0x0402。我忘了更新id_table,结果驱动死活加载不上。查了两天才发现是这个问题。
注意:SDIO的时钟频率不能乱设。高通芯片一般支持50MHz,但有些老平台只能跑到25MHz。我建议先按25MHz调,稳定了再往上提。
PCIe适配其实更简单,主要是配置BAR空间和MSI中断。代码结构类似:
static int wlan_pcie_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
// 1. 启用PCIe设备
pci_enable_device(pdev);
// 2. 请求BAR0空间
pci_request_region(pdev, 0, "qcom_wlan");
// 3. 设置DMA掩码
dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
// 4. 注册中断
devm_request_irq(&pdev->dev, pdev->irq,
wlan_isr, IRQF_SHARED, "qcom_wlan", priv);
}
2.3 电源管理与时钟配置
电源管理这块,说实话,是移植过程中最容易出问题的环节。高通芯片的电源状态有四种:
| 状态 | 功耗 | 唤醒延迟 | 典型场景 |
|---|---|---|---|
| Active | ~500mW | 0ms | 数据传输中 |
| Sleep | ~50mW | ~5ms | 空闲但保持连接 |
| Deep Sleep | ~5mW | ~50ms | 待机 |
| Off | 0mW | ~200ms | 关机 |
时钟配置更讲究。高通芯片需要三个时钟:
- 主时钟(XCXO):19.2MHz或38.4MHz,必须精确
- 睡眠时钟(Sleep CLK):32.768KHz,用于低功耗模式
- 参考时钟(Ref CLK):可选,用于特定功能
我的经验:时钟精度不够会导致WiFi断连。我曾经遇到一个案子,睡眠时钟用了内部RC振荡器,结果每过几分钟就掉线一次。换成外部晶振后问题解决。所以,能走外部时钟就别省这个成本。
2.4 中断与DMA配置
中断处理是驱动性能的关键。高通WLAN驱动用的是MSI中断(PCIe)或GPIO中断(SDIO)。
先看SDIO的中断配置:
// 在probe函数中注册SDIO中断
sdio_claim_host(func);
ret = sdio_claim_irq(func, wlan_sdio_irq_handler);
sdio_release_host(func);
// 中断处理函数
static irqreturn_t wlan_sdio_irq_handler(struct sdio_func *func)
{
struct wlan_priv *priv = sdio_get_drvdata(func);
// 读取中断状态寄存器
u32 int_status = sdio_readl(func, WLAN_INT_STATUS, &ret);
// 根据中断类型分发处理
if (int_status & INT_DATA_READY)
schedule_work(&priv->rx_work);
if (int_status & INT_TX_DONE)
complete(&priv->tx_done);
return IRQ_HANDLED;
}
DMA配置这块,说白了就是告诉硬件“数据该往内存哪里放”。高通芯片支持两种DMA模式:
- Legacy DMA:一次传输一个buffer,简单但效率低
- Scatter-Gather DMA:一次传输多个buffer,效率高
我个人强烈建议用Scatter-Gather模式。为什么?因为WiFi数据包大小不一,用SG模式可以避免内存碎片。我在一个项目中做过对比,SG模式比Legacy模式吞吐量提升了约30%。
避坑指南:我曾经在DMA地址对齐上吃过亏。高通芯片要求DMA buffer的物理地址必须按4字节对齐。如果不对齐,数据会错位。检查方法很简单——在分配内存时用dma_alloc_coherent,它会自动处理对齐。
2.5 移植实战要点
最后,我总结几个移植时的关键检查点:
- 设备树配置:确保dts文件中正确描述了SDIO/PCIe节点
- 固件路径:确认固件文件放在/lib/firmware/下,且文件名匹配
- 内核配置:打开CONFIG_QCOM_WLAN、CONFIG_SDIO_UART等选项
- 电源序列:检查上电时序,特别是1.8V和3.3V的先后顺序
嗯,内核驱动层移植就讲到这里。下一章咱们会深入HAL层,看看怎么和固件打交道。记住,驱动移植不是一蹴而就的事,遇到问题别慌,按这个框架一步步排查就行。