十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SPI、I2C、UART怎么选?从机制到Verilog实现全解析

SPI、I2C、UART怎么选?从机制到Verilog实现全解析 在论坛里刷到SPI、I2C、UART怎么选这个问题少说也有几十次了。每次看到下面一堆回答不是I2C省线就是SPI快再要么就是UART简单。话都没错但对提问的人来说毫无帮助——因为他真正想知道的是我的项目到底该用哪个。作为一个把三种协议在MCU和FPGA上都写过、还用Verilog手撸过UART和I2C控制器的人这篇我想先从协议的工作机制讲起再给一张可以直接打印对照的选型表最后附上RTL实现思路和实际踩过的坑。不管你是刚开始做方案选型还是准备用Verilog写协议控制器这篇都值得完整看完。1. 先把三种协议的性格摸清楚它们各自擅长什么1.1 UART没有时钟线的异步点对点聊天UART最核心的特点是没有时钟线。收发双方各按自己的时钟工作靠事先约定波特率来同步。可以理解成两个人用对讲机通话提前说好语速只要双方节奏差得不太离谱对话就能继续。它的数据帧由起始位、数据位、可选的校验位和停止位组成平时空闲线为高电平发送端先拉低一个位宽告诉对方我开始发了。典型帧格式1位起始位低电平5~8位数据位通常8位0或1位校验位奇偶校验可选1~2位停止位高电平UART是点对点的只能两个设备之间通信没有地址概念。TX/RX各一根线全双工理论上可以同时收和发。速率用波特率表示常见9600、115200、460800实际工程里推到几Mbps也能跑但速率越高对时钟精度和线路质量要求越高。用示波器看UART波形起始位下降沿是非常明显的开始标记熟悉它对后面调试帮助很大。UART最大的优势是简单和通用几乎所有MCU都带UART外设几乎所有调试工具都支持串口调试。缺点是只能点对点、速率相对有限、没有总线仲裁机制多设备通信基本得靠协议栈自己搭。1.2 I2C两根线挂一堆设备的总线老手I2C只用SCL时钟和SDA数据两根线所有设备并联在总线上。它的物理层采用开漏输出加外部上拉电阻实现线与逻辑任何设备都能把总线拉低但谁都不能主动推高。主机产生SCL时钟通过发送7位或10位地址来点名某个从机。被点名的设备以ACK应答然后双方按地址、寄存器地址、数据这样的顺序交换信息。I2C最迷人的地方是挂载能力一条总线上挂十几个传感器很常见每个设备靠地址区分不需要额外的片选线PCB走线也省。代价是协议状态机比UART复杂速度上限也低——标准模式100kbps快速模式400kbps快速模式1Mbps高速模式3.4Mbps。绝大多数传感器和EEPROM场景下400k已经绰绰有余。I2C还有一个容易被忽略的特性它天然支持多主机。多个主机可以分时占用总线靠仲裁机制解决冲突。虽然实际项目里真正用到多主机的场景不多但这让I2C在系统灵活性上明显优于SPI。1.3 SPI全双工、四根线、直来直去的专用通道SPI是三种协议里速度最猛、协议最透明的。标准接法四根线SCLK时钟、MOSI主机输出从机输入、MISO主机输入从机输出、CS片选。每个从机独占一根CS线主机靠拉低对应CS来选择通信对象没有地址、没有应答从机不回ACK。SPI的核心机制是移位寄存器主从双方各有一个移位寄存器每个时钟周期交换一位数据。主机往MOSI上推出一位的同时从机也在MISO上推回一位所以SPI天然全双工。很多新人以为SPI是主机发完、从机再回理解偏差会导致读数据时序搞错。实际上它是边发边收一次传输N个时钟周期就交换了N位数据。SPI速率可以做到几十Mbps甚至上百Mbps非常适合SD卡、Flash、屏幕、高速ADC和FPGA配置芯片。但多从机时每加一个设备就要多一根CS线IO占用随设备数量线性增长。SPI还有4种模式由CPOL时钟极性和CPHA时钟相位组合决定决定了时钟空闲电平和数据采样沿新人常在模式选择上翻车。2. 一张表看全六项关键参数直接对比2.1 核心参数对照表对比项UARTI2CSPI接线数量2根TX/RX另加共地2根SCL/SDA4根起步每个从机再加一根CS同步方式异步无时钟线同步主机产生SCL同步主机产生SCLK传输方向全双工半双工全双工设备寻址无仅点对点7位/10位地址无地址靠片选典型速率9600bps~几Mbps100k/400k/1M/3.4Mbps数十Mbps可上百Mbps协议复杂度低中高低到中总线挂载能力仅两设备一条总线可挂上百设备一个主机可接多从机每从机一根CS典型应用调试串口、GPS模块、蓝牙透传温湿度传感器、EEPROM、RTCFlash、SD卡、显示屏、高速ADC这张表基本回答了一半的选型问题要速度选SPI要省IO选I2C要通用简单选UART。但真拿到项目里还得看下面几个容易被表格掩盖的细节。2.2 表格背后容易忽略的三个点第一UART的全双工是物理能力但很多场景根本没用到。调试串口绝大多数时间只是主机在往外面打日志偶尔收一条命令双向同时满载的情况很少。如果你只是需要打印调试信息别因为SPI快就把调试接口设计成SPI那会给上位机工具链带来一堆不必要的麻烦。第二I2C的速率标称值不是白来的。100k还是400k取决于总线上拉电阻、挂载设备数量、走线长度和寄生电容。这个在第五章单独展开讲这里先记住一个结论I2C标称速率是有前提条件的不是换个电阻就能随便往上拔。第三SPI的速率上限基本不由协议本身决定而是由主从双方IO能力、PCB走线质量和信号完整性决定。同样是SPI接口板内走线跑50M没问题用杜邦线飞线可能10M就花屏、丢数据。所以看别人Demo跑多快没意义要结合自己板子实际情况。2.3 距离是表格里写不出来的变量没有哪张选型表能直接给出通信距离因为距离和速率、电平标准、线材质量强相关。经验值是板内通信三种协议几十厘米内都没问题如果要用线缆引出超过1米原生的3.3V/5V TTL电平不管是I2C还是SPI都容易出问题UART通常会转成RS232或RS485电平来延距离。选型时务必先想清楚设备是放在同一块板上还是分体部署这一步想错后面全是坑。3. 选型决策路径别问哪个好先回答这四个问题3.1 四个问题直接缩小选项范围选型不是凭喜好押注而是从需求反推。我每次做方案都会先问自己四个问题总线上要挂几个设备如果需要挂3个以上传感器且不想每个都占一组IOI2C靠地址寻址的优势立刻体现。如果是单设备高吞吐SPI更合适。数据量多大、多久传一次每秒读一次温湿度I2C的400k完全够用如果是连续采样的高速ADC想都不要想直接SPI。需要双向同时收发吗全双工场景比如通信过程中要一边下发指令一边实时接收响应UART和SPI没问题I2C半双工就要做方向切换时序上多一道麻烦。从机芯片支持什么接口这条经常被忽略。很多传感器和模块只有I2C版本或只有SPI版本器件本身已经把答案定死了纠结选型没有意义跟着器件走最省事。回答完这四个问题剩下的选择空间就很小了。再往细里说如果两种协议都满足需求我优先选自己最熟、调试工具链最顺手的那个——工程里稳定可控比参数好看重要得多。3.2 典型场景的选型实例具体场景推荐协议决策理由板载温湿度传感器每秒读一次I2C挂载简单、速率够用、省IO板载SPI Flash频繁写日志SPI吞吐量高适合块写入外接GPS模块、蓝牙透传模块UART这些模块普遍只出UART接口4路高速ADC做采集前端SPI高速采样每从机独立CS一块板上挂6颗不同型号传感器I2C地址寻址省IO省走线长距离传输跨板、跨机箱UARTRS485差分电平抗干扰、距离远3.3 真实项目里协议几乎都是混用的很多新人有个误区觉得一个项目里要统一用一种协议。实际上真实产品里三种协议经常共存MCU用I2C挂传感器用SPI连Flash用UART做调试串口和上位机通信各自守在自己最合适的岗位上。FPGA工程更明显配置芯片走SPI传感器走I2C与MCU之间走UART或自研并行接口都很常见。所以怎么选的正确理解不是让你押注单一协议而是搞清楚每种协议在系统里的角色学会让它们各司其职。你不需要一个万能的协议你需要的是在合适的位置用合适的接口。4. Verilog实现思路从协议时序到可综合RTL4.1 UART发送最典型的FSM入门案例用Verilog写UART本质就是写一个波特率分频器和发送状态机。系统时钟50MHz目标波特率115200最简单的做法是先做分频分频系数 50_000_000 / 115200 ≈ 434。注意UART接收常用16倍过采样来提高抗干扰能力但发送端只需要1倍波特率逐位输出就行不需要过采样。发送状态机分成四段空闲、起始位、数据位、停止位。空闲状态TX线保持高电平检测到发送使能后拉低一bit时间发出起始位随后按位输出8位数据低位在前最后输出停止位并回到空闲。核心代码逻辑// 波特率时钟节拍 wire baud_tick (div_cnt DIV_MAX); always (posedge clk) begin if (rst) begin txd 1b1; state IDLE; bit_cnt 3d0; end else if (baud_tick) begin case (state) IDLE: begin txd 1b1; if (send_en) begin txd 1b0; // 起始位 state START; end end START: begin txd data[0]; state DATA; end DATA: begin txd data[bit_cnt]; if (bit_cnt 3d7) begin state STOP; end else begin bit_cnt bit_cnt 1b1; end end STOP: begin txd 1b1; // 停止位 state IDLE; end endcase end end接收端比发送端稍微麻烦一点因为接收方不知道对方何时发数据。常用做法是用16倍波特率时钟采样检测到TX线下降沿时认为是起始位开始然后从起始位中点开始每隔16个采样周期在每位的中点采一次数据避开边沿抖动。中点采样是UART接收可靠的关键写接收机时重点研究这一块。4.2 SPI主机移位寄存器是核心思维SPI主机在RTL里反而比UART简单因为它有时钟线不需要在接收端做复杂的过采样。主机要做三件事产生SCLK、控制CS、通过移位寄存器收发数据。以模式0为例空闲时SCLK为低数据在SCLK上升沿输出发送在下降沿采样输入接收一次传输8位就交换一字节。核心代码逻辑// 分频产生 SCLK模式0空闲低电平 always (posedge clk) begin if (div_cnt DIV_MAX) begin div_cnt 0; sclk ~sclk; if (sclk 1b0) begin // SCLK即将上升沿先放数据 mosi tx_reg[7]; tx_reg {tx_reg[6:0], 1b0}; rx_reg {rx_reg[6:0], miso}; // 下降沿采样 end end else begin div_cnt div_cnt 1b1; end end这段代码的精髓是发送移位和接收移位合并成一个状态机的每一次跳转主机每次在上升沿前把最高位放到MOSI上在下降沿把MISO采进来放到移位寄存器最低位16个SCLK周期后完成一字节交换。真正写总线级模块时还要照顾CS的建立时间和释放时间大部分从机要求CS拉低后至少几百纳秒再开始SCLK最后一个SCLK结束后CS还需要保持一段时间再拉高这些时序要求要严格对照从机数据手册。4.3 I2C用三态门模拟开漏输出I2C在FPGA里的核心难点不是状态机而是物理层的开漏建模。FPGA引脚没有真正的开漏模式需要用三态门模拟需要输出低电平时把IO驱动为0需要释放总线时把IO置为高阻由外部上拉电阻把电平拉高。wire sda_in; reg sda_out_en; reg sda_out; assign sda sda_out_en ? sda_out : 1bz; assign sda_in sda;当sda_out_en为1且sda_out为0时SDA被拉低当sda_out_en为0时SDA呈高阻由上拉电阻决定电平。SCL也要这样处理。I2C的状态机需要依次处理起始条件SCL高电平期间SDA由高变低、停止条件SCL高电平期间SDA由低变高、发送7位地址加读写位、释放SDA等待从机ACK、按字节收发数据、每字节结束后在SCL低电平期间完成SDA切换。写I2C时最容易犯的错是SDA切换时机不对。协议铁律是SCL高电平期间SDA必须保持稳定数据变化只能发生在SCL低电平期间。一旦SDA在SCL高电平期间变化对端会把它误判成起始或停止条件整个通信直接乱掉。写RTL的时候要把SCL低电平期间更新SDA写进状态机的每个状态转换里。4.4 仿真验证和常见RTL误区建议先搭testbench仿真看波形再上板验证。UART重点看起始位检测是否误触发、数据位是否在中点采样SPI重点看CS与SCLK时序是否满足从机手册I2C重点看SDA是否只在SCL低电平期间变化、ACK窗口采样是否正确。仿真看不出问题就上板大概率要在示波器前面坐很久。常见RTL错误我列一下UART波特率分频四舍五入导致误差积累通信偶尔错一帧SPI的CPOL/CPHA模式选错读回来数据全是乱的先查SCLK空闲电平和采样沿I2C的三态门没做高阻释放总线一直被某个设备拉低其他设备完全无法通信状态机缺少default分支非法状态进去后出不来整个控制器假死复位信号不同步异步复位释放时刚好撞上协议时序沿出现概率极低的偶发错误5. 实际项目踩坑实录这三个问题几乎人人都遇到5.1 I2C上拉电阻为什么小了反而不通信不少人在I2C通信异常时第一反应是查时序、查地址折腾半天最后发现是上拉电阻选得不对。I2C总线是开漏加外部上拉上拉电阻值必须和总线电容、通信速率匹配。电阻太小低电平灌电流过大器件可能拉不低总线或者超过最大灌电流规格电阻太大RC上升沿太缓在设定速率下信号还没越过高电平阈值就被采样通信直接失败。这也就是上拉电阻小了不通信的根本原因。经验取值标准模式100k用4.7k到10k比较稳妥快速模式400k用2.2k到4.7k总线上设备多、走线长时往2.2k方向选上电后用示波器看SCL、SDA上升沿如果上升沿明显圆润、超过周期的三分之一就减小电阻5.2 SPI片选硬件片选和软件片选之争MCU的SPI外设通常会提供硬件NSS引脚FPGA工程里也有人直接用普通GPIO做CS。硬件片选的好处是自动和传输同步不用软件干预缺点是多从机时IO不够用而且某些MCU的硬件NSS行为很死板从机切换之间的总线间隔不可控。软件片选GPIO手动控制更灵活但要注意三个时序点CS拉低到第一个SCLK之间的延时、最后一个SCLK到CS拉高的延时、两次传输之间CS必须拉高至少几个时钟周期。很多从机尤其Flash要求字节间CS必须释放否则状态机复位不完整。我自己写FPGA时几乎都用软件片选思路在状态机里显式控制CS的拉低和拉高对时序完全掌控。这种自己管片选的做法虽然多写几行代码但排查问题时会省很多时间。5.3 UART波特率容差分频误差是隐形杀手UART是异步通信双方各自用自己的时钟波特率必须匹配。MCU用PLL通常能精确分频出常用波特率但FPGA经常遇到系统时钟无法整分频出目标波特率的情况。比如50MHz分频产生115200分频系数434.027取434后实际波特率和理论值误差不到0.01%完全没问题。但如果时钟是12MHz分频系数104.166取104后误差大概0.16%依然在安全范围。真正危险的是系统时钟很低、分频误差超过2%的情况再加上对端设备本身的时钟偏差总误差可能突破容限导致偶尔错一帧但又找不到明显原因。排查方法很简单计算双方实际波特率的总误差保持在±2%以内最好控制到±1%。UART通信出现间歇性丢字、错字且硬件连接没问题时先怀疑波特率而不是怀疑代码。6. 一点个人的选型体会最后说点这几次项目下来最深的感受协议不是越新越快越好而是和你的器件、时钟资源、调试手段匹配才最好。我现在的习惯是UART做调试串口和上位机通信、I2C挂各类传感器、SPI接Flash和高带宽外设每种协议都用在自己最合适的岗位上不折腾、不炫技。如果你刚开始在FPGA里手写通信协议建议按UART到SPI再到I2C的顺序练手先从带状态机但只有两根线的UART入手再接触有时钟线的SPI最后挑战开漏三态和ACK时序的I2C。这三个协议各写一遍对接口设计和时序约束的理解比背十遍协议文档都管用。
返回列表