硬件基础:外设总线与接口速览

做嵌入式驱动开发,说白了就是跟各种外设打交道。而外设怎么跟CPU通信?靠的就是总线。我见过不少新手,上来就撸代码,结果调了三天发现是总线配置错了——这冤枉路我走过,所以今天咱们先把这几条常见总线的脾气摸清楚。

嵌入式系统里最常用的总线,无非就是I2C、SPI、UART、USB、PCIe这五兄弟。它们各有各的擅长场景,也各有各的坑。咱们一个一个聊。

I2C:两根线,慢但稳

I2C总线只用两根线:SCL(时钟)和SDA(数据)。所有设备挂在这两根线上,靠地址区分。你想想看,两根线挂几十个设备,省引脚吧?

关键参数:

  • 标准模式:100 kHz
  • 快速模式:400 kHz
  • 高速模式:3.4 MHz

我个人习惯,传感器、EEPROM、RTC这类低速设备,首选I2C。为什么?因为它的应答机制能让你实时知道设备有没有收到数据。调试的时候特别友好。

经验之谈: 我在项目中遇到过I2C总线锁死的问题——从设备拉低SDA不放,主设备干等。后来加了个超时复位机制,每次通信前先检查总线状态,问题就解决了。

驱动裁剪要点:

  • 如果系统里只有一两个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?想都别想。

注意: SPI没有标准的应答机制。你发出去的数据,从设备收没收到,它不会告诉你。所以驱动里一定要加校验,比如CRC或者简单的回读验证。我曾经因为这个吃过亏——数据写进去,读出来全是错的,查了半天才发现是片选时序没配好。

驱动裁剪要点:

  • 如果只用单向通信(比如只写显示),可以裁剪掉MISO相关的代码
  • SPI有四种模式(CPOL/CPHA组合),如果设备只支持一种,其他三种的配置代码可以删掉
  • DMA传输不是必须的,低速率场景用PIO模式更省资源

UART:老当益壮的串口

UART可能是最古老、也最通用的接口了。它只需要两根线:TX和RX。没有时钟线,靠双方约定好的波特率来同步。

常见波特率: 9600、19200、38400、115200、921600

UART最大的优点是简单。我调试嵌入式系统,第一件事就是把UART打印打开。没有它,你连系统跑没跑起来都不知道。但它的缺点也很明显——只能点对点通信,而且速度上不去。

嗯,这里要注意:UART的驱动裁剪,重点在流控。硬件流控(RTS/CTS)不是所有场景都需要。如果只是调试用,关掉流控,能省不少中断处理逻辑。

我的建议: 量产产品里,UART尽量只保留一个作为调试口,其他的用I2C或SPI替代。为什么?因为UART没有多设备能力,每个设备都要独占一组引脚,板子布线会很痛苦。

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描述符也可以精简,只保留必要的
避坑指南: 我曾经在一个项目里把USB协议栈裁得太狠,结果设备枚举总是失败。后来发现是去掉了某个看似无关的描述符解析函数。所以裁剪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)如果不需要,直接禁用,省掉一堆状态机
注意: PCIe的链路训练(link training)是个很耗时的过程。如果系统对启动时间有要求,可以考虑在驱动里跳过一些不必要的重训练步骤。但别太过分——我见过有人把训练超时设得太短,结果某些兼容性差的设备死活连不上。

总线选择决策树

说了这么多,到底该用哪条总线?我画了张图,帮你快速决策。

外设总线选择决策树 外设总线选择决策树 需要什么速度? 低速(< 10 Mbps) 中速(10-500 Mbps) 高速(> 500 Mbps) 设备多?→ I2C 设备少?→ SPI 点对点?→ UART 多设备?→ USB 板内高速?→ PCIe 决策说明 • 低速场景:传感器、RTC、EEPROM等,I2C省引脚,SPI省代码 • 中速场景:音频、串口屏、U盘等,UART最简单,USB最灵活 • 高速场景:NVMe、GPU、万兆网卡等,PCIe是唯一选择 • 实际项目中经常混合使用:I2C挂传感器,SPI挂Flash,USB接外设 • 驱动裁剪时,先确定哪些总线真的需要,不需要的整个砍掉

驱动裁剪的核心原则

好了,五条总线都过了一遍。最后说说驱动裁剪的通用原则——不管你面对的是哪条总线,这几条都适用。

  1. 砍掉不用的功能:这是最直接的。系统里没有I2C设备?整个I2C子系统都可以删掉。别觉得可惜,代码少就是好。
  2. 静态配置代替动态检测:比如SPI的时钟极性,如果设备固定,就别在运行时去探测。写死在代码里,省掉一堆判断逻辑。
  3. 中断合并:高速总线的中断可能很频繁。把多个小中断合并成一个批量处理,能显著降低CPU负载。
  4. DMA vs PIO:数据量大的场景用DMA,数据量小的场景用PIO。别盲目上DMA——它的初始化开销不小。
  5. 错误处理精简:量产产品里,很多错误处理代码其实永远不会被执行到。可以适当简化,但别完全去掉——调试的时候你会后悔的。
我的经验: 做驱动裁剪,最忌讳的是「一刀切」。我见过有人为了省代码,把I2C的超时重试机制砍掉了,结果产品在高温环境下频繁通信失败。裁剪之前,一定要搞清楚你的硬件工作环境——温度、电压、干扰源,这些都会影响总线的稳定性。