2. 驱动开发基础

做驱动开发,说白了就是跟各种硬件接口打交道。我刚开始入行那会儿,总觉得协议规范背得滚瓜烂熟就行,结果第一次调PCIe设备就被现实狠狠教育了一顿——纸上谈兵和真刀真枪完全是两码事。

这一章,咱们把驱动开发最常用的四种接口掰开揉碎了讲。PCIe、USB、I2C/SPI、GPIO与中断,这四样东西你玩明白了,市面上八成以上的驱动你都能搞定。

核心观点:驱动开发不是背协议,而是理解硬件与软件之间的"翻译规则"。每种总线都有自己的脾气,摸透了才能写出靠谱的驱动。

硬件接口知识体系图 硬件接口基础 PCIe总线 USB协议 I2C/SPI GPIO+中断 驱动开发 = 理解协议 + 操作寄存器 高速·DMA·配置空间 枚举·端点·描述符 低速·主从·寄存器级 电平·边沿·中断号

PCIe总线协议:驱动开发的"高速公路"

PCIe是目前PC和服务器领域最主流的高速总线。我当年调第一个PCIe网卡驱动时,光看spec就看了两周,后来发现真正干活时最常用的就那几样东西。

PCIe的核心概念,其实就三条:

  • 拓扑结构:根复合体(RC) → 交换器(Switch) → 端点(EP)。树形结构,每个设备有唯一的BDF号(Bus:Device.Function)。
  • 配置空间:每个PCIe设备都有256字节的标准配置空间,前64字节是必须实现的。Vendor ID、Device ID、Class Code这些都在里面。
  • 地址空间:PCIe支持三种地址空间——内存空间、IO空间、配置空间。驱动主要操作内存空间,通过BAR(Base Address Register)来映射。

我的经验:调试PCIe设备时,先用 lspci -vvv 看设备有没有被正确枚举。如果连设备都看不到,别急着调驱动,先查硬件连接和复位时序。我曾经被一个虚焊的PCIe金针坑了整整两天。

驱动开发中最重要的几个寄存器:

寄存器 偏移 作用
Vendor ID 0x00 厂商编号,0x8086是Intel
Device ID 0x02 设备型号,驱动匹配的依据
Command 0x04 控制IO/内存访问使能
Status 0x06 状态位,包括中断状态
BAR0~BAR5 0x10~0x24 基地址寄存器,映射设备内存
Capabilities Pointer 0x34 指向能力链表,MSI/MSI-X在这里

写驱动时,第一步就是读取Vendor ID和Device ID确认设备存在。然后配置Command寄存器开启内存访问,再通过BAR地址把设备内存映射到系统空间。嗯,流程就这么简单,但细节能坑死人。

注意:PCIe的MSI/MSI-X中断和传统INTx中断完全不同。MSI是写内存事务,没有物理中断线。很多新手在设备树里配了中断号却发现收不到中断,八成是MSI配置没搞定。

USB协议基础:即插即用的"万能接口"

USB协议比PCIe复杂得多,但驱动开发常用的部分其实有限。我个人习惯把USB协议分成三层来看:物理层、协议层、软件层。

USB的枚举过程,是驱动开发必须吃透的:

  1. 主机检测到设备插入(D+/D-电平变化)
  2. 主机复位设备,设备地址为0
  3. 主机发送GET_DESCRIPTOR请求,获取设备描述符
  4. 主机分配唯一地址(SET_ADDRESS)
  5. 主机再次获取设备描述符、配置描述符、接口描述符等
  6. 主机加载对应的驱动程序

你看,从硬件插入到驱动加载,中间有6个步骤。我遇到过一个问题:设备描述符里的bMaxPacketSize0填错了,导致主机一直枚举失败。查了两天才发现是固件里这个字段写死了64,但硬件只支持8字节。

关键点:USB描述符是设备和主机沟通的"身份证"。描述符写错了,驱动写得再好也白搭。调试时用 lsusb -v 可以看完整的描述符树。

USB传输类型对比:

传输类型 应用场景 特点
控制传输 枚举、配置命令 双向,保证送达,速度慢
批量传输 U盘、打印机 可靠,带宽尽力而为
中断传输 鼠标、键盘 保证延迟,但带宽有限
等时传输 音频、视频 保证带宽,但不保证送达

写USB驱动时,最常用的是控制传输和批量传输。控制传输用来发命令,批量传输用来搬数据。中断传输其实也是轮询的,别被名字骗了。

I2C/SPI总线:嵌入式驱动的"基本功"

I2C和SPI是嵌入式领域最常用的板级总线。速度不快,但胜在简单。我刚开始做嵌入式驱动时,调I2C触摸屏调了一周,最后发现是上拉电阻没焊——硬件问题,软件怎么调都没用。

I2C的核心要点:

  • 两线制:SCL(时钟)+ SDA(数据)
  • 每个设备有7位或10位地址
  • 主设备发起通信,从设备响应
  • 标准模式100kHz,快速模式400kHz

SPI的核心要点:

  • 四线制:SCLK(时钟)+ MOSI(主出从入)+ MISO(主入从出)+ CS(片选)
  • 全双工通信,同一时刻收发
  • 没有设备地址,靠片选线选择设备
  • 速度可达几十MHz

避坑指南:我曾经调一个SPI Flash驱动,读出来的数据全是0xFF。查了半天发现是SPI的极性(CPOL)和相位(CPHA)配反了。记住:主设备和从设备的模式必须一致,否则数据对不上。

I2C和SPI怎么选?我的经验是:设备少、距离近、需要多主通信时用I2C;设备多、速度快、数据量大时用SPI。I2C的地址冲突是个坑,SPI的片选线不够也是个坑——看场景选吧。

GPIO与中断控制器:驱动开发的"神经末梢"

GPIO是最基础的硬件接口,但也是最容易出问题的地方。中断控制器则是驱动性能的关键。这两样东西配合好了,你的驱动才能"灵敏"。

GPIO驱动开发的核心:

  • 配置方向:输入还是输出
  • 配置上下拉:内部上拉、下拉还是浮空
  • 读取/写入电平:高电平还是低电平
  • 中断配置:上升沿、下降沿、高电平、低电平

嗯,听起来简单吧?但实际项目中,GPIO的复用功能(Alternate Function)才是真正的坑。一个引脚可能同时是GPIO、UART TX、I2C SCL,配置错了整个功能都跑不起来。

注意:中断处理函数里不要做耗时操作!我见过有人在中断里调用 printk 调试,结果系统直接死锁。中断上下文不能睡眠,不能获取信号量,不能做内存分配——这些规矩是血的教训换来的。

中断控制器的层次结构:

现代SoC的中断控制器通常是树形结构。以ARM GIC为例:

  • 外设产生中断信号
  • GIC分发器(Distributor)接收并路由
  • GIC CPU接口向核心触发IRQ或FIQ
  • 核心跳转到中断向量表执行处理函数

写驱动时,你需要知道中断号、触发方式、是否共享中断。设备树里通常会这样描述:

my_device {
    compatible = "vendor,my-device";
    reg = <0x10000000 0x1000>;
    interrupts = <0 42 4>;  // GIC SPI 42号,高电平触发
    interrupt-parent = <&gic>;
};

这个 interrupts 属性里的三个值,第一个是中断类型(0=PPI,1=SPI),第二个是中断号,第三个是触发方式。很多人搞混了SPI和PPI的区别——SPI是共享外设中断,可以多个核心共享;PPI是私有外设中断,每个核心独享。

总结一下:PCIe是高速总线,适合大吞吐量设备;USB是通用外设总线,即插即用;I2C/SPI是板级低速总线,适合传感器和控制类设备;GPIO+中断是驱动开发的"手脚"和"神经"。这四样东西,你每样都亲手调过几个设备,驱动开发就算入门了。