硬件基础:外设总线与接口速览
做嵌入式驱动开发,说白了就是跟各种外设打交道。而外设怎么跟CPU通信?靠的就是总线。我见过不少新手,上来就撸代码,结果调了三天发现是总线配置错了——这冤枉路我走过,所以今天咱们先把这几条常见总线的脾气摸清楚。
嵌入式系统里最常用的总线,无非就是I2C、SPI、UART、USB、PCIe这五兄弟。它们各有各的擅长场景,也各有各的坑。咱们一个一个聊。
I2C:两根线,慢但稳
I2C总线只用两根线:SCL(时钟)和SDA(数据)。所有设备挂在这两根线上,靠地址区分。你想想看,两根线挂几十个设备,省引脚吧?
关键参数:
- 标准模式:100 kHz
- 快速模式:400 kHz
- 高速模式:3.4 MHz
我个人习惯,传感器、EEPROM、RTC这类低速设备,首选I2C。为什么?因为它的应答机制能让你实时知道设备有没有收到数据。调试的时候特别友好。
驱动裁剪要点:
- 如果系统里只有一两个I2C设备,可以裁剪掉多主模式支持
- 不需要10位地址的话,关掉它,能省不少代码空间
- 时钟延展(clock stretching)功能,很多从设备不用,可以关
SPI:四根线,快但简单粗暴
SPI比I2C快得多,但代价是多两根线。它用四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。每个从设备独占一根CS线,所以设备多了,引脚也多了。
速度对比:
| 总线 | 典型速率 | 最大设备数 | 引脚数 |
|---|---|---|---|
| I2C | 100 kHz - 3.4 MHz | 理论127个 | 2 |
| SPI | 10 MHz - 50 MHz | 受限于CS引脚 | 4 + N×CS |
SPI适合什么场景?ADC、DAC、显示驱动、SD卡、Flash存储器——这些需要高速数据传输的设备。我曾经在一个工业相机项目里用SPI驱动图像传感器,跑40 MHz,一帧数据几毫秒就传完了。换成I2C?想都别想。
驱动裁剪要点:
- 如果只用单向通信(比如只写显示),可以裁剪掉MISO相关的代码
- SPI有四种模式(CPOL/CPHA组合),如果设备只支持一种,其他三种的配置代码可以删掉
- DMA传输不是必须的,低速率场景用PIO模式更省资源
UART:老当益壮的串口
UART可能是最古老、也最通用的接口了。它只需要两根线:TX和RX。没有时钟线,靠双方约定好的波特率来同步。
常见波特率: 9600、19200、38400、115200、921600
UART最大的优点是简单。我调试嵌入式系统,第一件事就是把UART打印打开。没有它,你连系统跑没跑起来都不知道。但它的缺点也很明显——只能点对点通信,而且速度上不去。
嗯,这里要注意:UART的驱动裁剪,重点在流控。硬件流控(RTS/CTS)不是所有场景都需要。如果只是调试用,关掉流控,能省不少中断处理逻辑。
USB:万能但复杂
USB是个大家伙。它支持热插拔、供电、多种传输类型(控制、批量、中断、同步),还能挂127个设备。但代价是协议栈极其庞大。
USB版本速览:
| 版本 | 速率 | 典型应用 |
|---|---|---|
| USB 1.1 | 12 Mbps | 键盘、鼠标 |
| USB 2.0 | 480 Mbps | U盘、摄像头 |
| USB 3.0 | 5 Gbps | 高速存储、视频 |
做驱动裁剪时,USB是最值得下刀子的地方。完整的USB协议栈动辄几百KB,对于资源受限的MCU来说,根本装不下。
裁剪思路:
- 如果只做USB设备(比如虚拟串口),裁剪掉主机协议栈
- 如果只做主机(比如读U盘),裁剪掉设备协议栈
- 不需要的传输类型可以去掉——比如只做批量传输,中断和同步的代码就可以删
- USB描述符也可以精简,只保留必要的
PCIe:高性能,高复杂度
PCIe是PC和服务器领域的王者。它用差分信号传输,每条lane(通道)可以跑2.5 GT/s(Gen1)、5 GT/s(Gen2)、8 GT/s(Gen3)甚至更高。而且它支持多lane捆绑,x1、x4、x8、x16,带宽线性叠加。
在嵌入式领域,PCIe主要用于连接高速外设:NVMe固态硬盘、GPU、万兆网卡、FPGA加速卡。我做过一个边缘计算设备,用PCIe连接FPGA做AI推理加速,带宽需求是10 GB/s以上,其他总线根本扛不住。
PCIe裁剪要点:
- 如果只用x1,裁剪掉多lane协商的代码
- 不需要SR-IOV(单根虚拟化)的话,关掉它
- 高级错误报告(AER)在嵌入式场景里可以简化
- 电源管理(ASPM)如果不需要,直接禁用,省掉一堆状态机
总线选择决策树
说了这么多,到底该用哪条总线?我画了张图,帮你快速决策。
驱动裁剪的核心原则
好了,五条总线都过了一遍。最后说说驱动裁剪的通用原则——不管你面对的是哪条总线,这几条都适用。
- 砍掉不用的功能:这是最直接的。系统里没有I2C设备?整个I2C子系统都可以删掉。别觉得可惜,代码少就是好。
- 静态配置代替动态检测:比如SPI的时钟极性,如果设备固定,就别在运行时去探测。写死在代码里,省掉一堆判断逻辑。
- 中断合并:高速总线的中断可能很频繁。把多个小中断合并成一个批量处理,能显著降低CPU负载。
- DMA vs PIO:数据量大的场景用DMA,数据量小的场景用PIO。别盲目上DMA——它的初始化开销不小。
- 错误处理精简:量产产品里,很多错误处理代码其实永远不会被执行到。可以适当简化,但别完全去掉——调试的时候你会后悔的。