第二章 内核驱动层移植:高通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 移植实战要点

最后,我总结几个移植时的关键检查点:

  1. 设备树配置:确保dts文件中正确描述了SDIO/PCIe节点
  2. 固件路径:确认固件文件放在/lib/firmware/下,且文件名匹配
  3. 内核配置:打开CONFIG_QCOM_WLAN、CONFIG_SDIO_UART等选项
  4. 电源序列:检查上电时序,特别是1.8V和3.3V的先后顺序

嗯,内核驱动层移植就讲到这里。下一章咱们会深入HAL层,看看怎么和固件打交道。记住,驱动移植不是一蹴而就的事,遇到问题别慌,按这个框架一步步排查就行。