硬件接口基础:驱动开发的"四门功课"
做驱动开发,说白了就是跟各种硬件接口打交道。我刚开始入行那会儿,总觉得协议规范背得滚瓜烂熟就行,结果第一次调PCIe设备就被现实狠狠教育了一顿——纸上谈兵和真刀真枪完全是两码事。
这一章,咱们把驱动开发最常用的四种接口掰开揉碎了讲。PCIe、USB、I2C/SPI、GPIO与中断,这四样东西你玩明白了,市面上八成以上的驱动你都能搞定。
核心观点:驱动开发不是背协议,而是理解硬件与软件之间的"翻译规则"。每种总线都有自己的脾气,摸透了才能写出靠谱的驱动。
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的枚举过程,是驱动开发必须吃透的:
- 主机检测到设备插入(D+/D-电平变化)
- 主机复位设备,设备地址为0
- 主机发送GET_DESCRIPTOR请求,获取设备描述符
- 主机分配唯一地址(SET_ADDRESS)
- 主机再次获取设备描述符、配置描述符、接口描述符等
- 主机加载对应的驱动程序
你看,从硬件插入到驱动加载,中间有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+中断是驱动开发的"手脚"和"神经"。这四样东西,你每样都亲手调过几个设备,驱动开发就算入门了。