尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
嵌入式通信总线SPI与I2C实战详解
1. 先从“片内”说起为什么需要片间总线做嵌入式这些年我越来越觉得“总线”这词被用得有点滥。芯片内部的AXI、AHB、APB算是片内总线解决的是CPU核、DMA、存储控制器和外设IP之间怎么传数据的问题而SPI、I2C这类板级总线解决的是芯片与芯片、芯片与模块之间怎么交换数据的问题。标题里那句“从沙子到车辙”其实挺贴切——沙子是硅片车辙是电路板上的走线SPI和I2C就是连接这两端的“毛细血管”。很多刚接触MCU的朋友容易犯一个迷糊片内已经有APB总线和GPIO了为什么还要学SPI、I2C答案很简单APB不可能引出几十根引脚接到外部器件上就算能引出来信号完整性、时序同步、设备寻址这些问题也会把人逼疯。片间总线本质上就是把一组复杂的同步、握手、地址选择逻辑封装成一个标准外设你只需要配置几个寄存器剩下的活让硬件去干。SPI是高速、简单、多线I2C是低速、少线、可挂载量大两者互补几乎覆盖了板级通信90%以上的场景。这篇文章我会结合自己做过的实际项目把SPI和I2C从协议层到电路层、再到调试层的细节都捋一遍。不会只讲“什么是模式0”而是要讲清楚为什么选模式0、片选怎么拉才不出毛刺、I2C的2.2kΩ上拉怎么算出来的、什么时候要电平转换。这些东西不踩几次坑是记不牢的我趁着热乎全写出来给后来人留个宝典。1.1 片内总线与片间总线的分界线严格来说片内总线和片间总线之间并没有一条物理上的硬边界。以常见的Cortex-M内核MCU为例CPU通过AHB总线访问Flash、SRAM和DMA控制器APB1/APB2桥接外设寄存器而SPI和I2C外设的寄存器就挂在APB上。你写一个SPI_DR寄存器数据从CPU核经AHB→APB→SPI移位寄存器最后从MOSI引脚串行出去——这一刻数据才刚刚离开“片内”。理解这条数据通路很重要因为它决定了性能上限。APB总线的最大工作频率通常受限于外设挂载总线的时钟比如GD32F303的APB1最大是60MHzAPB2最大是120MHz。SPI2挂在APB1上SPI1挂在APB2上这意味着SPI1的理论波特率能跑到更高。很多人不看数据手册就直接把SPI时钟配成主频一半结果高频下读取外部Flash出错排查半天才发现是APB分频没算对。想跑高速先查你的SPI挂在哪条总线上然后确认APB分频系数这一步省不掉。另外还有个容易被忽略的点片内总线是并行数据片间总线是串行数据。CPU往SPI_DR写一个16位数据SPI硬件负责把这个16位变成一串比特从MOSI发出去。这个过程如果完全靠CPU手动移位效率极低所以SPI外设内部有移位寄存器和缓冲区甚至还有FIFO。而DMA可以把内存里的数据块直接搬到SPI_TDR不占CPU这就是“SPI DMA”的意义所在。后面我会专门讲DMA的配置时机。1.2 从“点对点”到“一主多从”的设计思路片内通信基本是“一点对多点”总线上所有设备抢仲裁。但片间通信不一样大多数场景是“一主多从”由主机发起所有传输。这种设计哲学根植于物理层SPI靠片选线CS区分从机低电平选中I2C靠设备地址区分从机用ACK应答确认数据是否送达。理解了这一点你就能明白为什么SPI接口那么“简单粗暴”而I2C接口那么“讲究礼仪”。SPI本质上是“主子对话其他人都闭嘴”。主机把CS拉低等于对某个从机说“我只跟你说话”同步时钟SCK一拉数据就按照约定好的顺序在MOSI/MISO上飞来飞去。其他从机的CS没被拉低它们的MISO引脚必须处于高阻态否则总线就打架了。这也是为什么SPI从机设备通常都有三态输出的要求。有些国产芯片的MISO设计不标准CS未选中但引脚不是高阻导致总线上多个设备冲突测出来全是乱码这坑我踩过。I2C则是“所有人听广播点名应答”。主机先发START条件然后发7位地址读写标志总线上的每个从机都拿自己的地址跟收到的地址比较匹配的一方会在第9个时钟周期拉低SDA产生ACK。I2C只有两根线但它靠一套复杂的时序协议换来了“一个主机最多能挂100多个从机理论地址空间128减去保留地址”的好处。代价就是速度上限低标准模式100kbit/s快速模式400kbit/s高速模式也就3.4Mbit/s。相比之下SPI轻松跑几十Mbit/s。所以做数据采集卡、显示刷新优先SPI做传感器、EEPROM这种低速率设备I2C足够。2. SPI四根线撑起的高速通道SPI的全名叫Serial Peripheral Interface串行外设接口。它通常是四根线SCK、MOSI、MISO、CS。某些设备只需要三根线比如只读传感器没有MISO或者只写的DAC没有MOSI回读功能还有一些设备把CS省略掉变成三线模式但那样就得靠同步字来区分帧用起来很别扭。板级设计上四线制还是最稳的选择。SPI是我用过最“舒服”的通信协议因为它的时序极其透明SCK提供节拍在每个时钟沿主机和从机同时移出一位并移入一位天然全双工。没有地址帧、没有应答位想做突发传输直接把CS拉低连续发N个字节想停就拉高CS。这种透明性让SPI非常容易通过示波器抓波形来判断问题——只要看CS、SCK、MOSI三条线是否对齐就能定位八九成的故障。不过太快也不是好事。SCK时钟速率一旦超过从机允许的范围或者时序模式选错数据就全废。2.1 SPI协议核心时钟极性与相位SPI有四种模式由CPOL时钟极性和CPHA时钟相位两个bit决定。CPOL决定空闲时SCK是高还是低CPOL0空闲低第一个边沿是上升沿CPOL1空闲高第一个边沿是下降沿。CPHA决定在第一个边沿采样还是第二个边沿采样CPHA0第一个边沿采样CPHA1第二个边沿采样。这四个组合就是Mode 0~3。你不需要背下“Mode0上升沿采样”这种表那是应试教育的做法。实际选模式只看一件事从机手册的时序图里数据在哪根边沿被锁存。有些传感器特别“皮”数据手册里时序图画得不清楚那就先用示波器看它回传的MISO波形反推它是在上升沿还是下降沿把数据送出来的。比如AD7606这颗16位ADC它有并行和SPI两种接口用SPI模式时官方推荐CPOL0、CPHA1Mode 2。为什么因为AD7606的MISO变化发生在SCK下降沿主机在随后一个上升沿采样最稳。如果你用Mode 0SCK上升沿采样时MISO可能刚好在切换读到的就是毛刺中间的值。还有一点容易翻车不少国产MCU的SPI外设寄存器里CPOL和CPHA的默认值跟标准ST库不一样比如GD32F303的配置位和STM32是兼容的但CH32V307由于内核是RISC-V其寄存器布局有区别你按STM32的库函数写有可能初始化失败。所以任何时候都要去查对应MCU的参考手册确认SPI_CFG寄存器里CKPL和CKPH的含义。我吃过亏一个项目用CH32V307挂OLED按STM32的思路配置SPI模式0结果满屏雪花查了两天才发现CH32V307的CPOL在空闲时的有效电平定义刚好是反的。2.2 硬件片选与软件片选谁更靠谱片选是SPI最容易被忽视但最关键的一环。硬件片选NSS由SPI外设自动控制配置好之后你来发数据前它会自动拉低发送完拉高。软件片选则是用普通GPIO拉低片选然后通过SPI收发数据最后手动拉高。很多初学者喜欢用软件片选因为它直观想什么时候拉就什么时候拉还能实现“中途暂停保持片选有效”这种特殊需求。但其实硬件片选在很多场景更合适尤其是配合DMA。硬件片选的好处是不需要CPU参与减少了GPIO操作带来的延迟。但硬件片选的坑也不少一是它通常在你写入数据寄存器后立刻拉低CS如果你的数据流有间隙CS也会跟着抖动二是一些MCU的硬件NSS在连续传输模式下可能会在字节之间释放CS比如STM32的NSS脉冲模式导致从机误认为传输结束。所以我通常的建议是简单低速、字节间允许间隙的情况下用软件片选高速连续流、配合DMA的场景优先用硬件片选但一定要检查NSS的输出时序是不是你想要的必要时用GPIO模拟NSS配合DMA也行那个其实还是软件片选但可以利用DMA的空闲中断来释放片选。我之前做过一个项目用GD32F303的SPI0跑DMA向TF卡写日志一开始用软件片选在DMA传输完成中断里拉高CS。结果发现TF卡偶尔写错块原因在于DMA传输完成后CPU去中断拉高CS的延迟有几十微秒而TF卡在SPI模式下对CS上升沿的要求很严格必须在特定字节边界后拉高晚一点它就把前面的数据当成下一条命令了。后来改成硬件NSS自动片选CS拉高时机跟发送完成硬件对齐问题立刻消失。所以结论很简单有DMA就尽量用硬件片选。2.3 SPI DMA什么时候才能真正提速很多人一看到“SPI DMA”就热血沸腾觉得只要挂上DMA速度就起飞。实际上DMA只是“搬运工”真正的速度瓶颈在SPI时钟和外设总线。DMA的作用是减少CPU参与字节搬运的开销让你可以把大批数据扔给外设之后去干别的活或者干脆让CPU在内存里准备下一块数据。如果数据量只有几十字节中断收发反而更简单DMA初始化代码的耗时可能比中断累加还多。SPI DMA的典型使用场景是连续的块传输比如一次刷新一个320x240的LCD缓冲区15万字节、向TF卡写一个4KB扇区、从AD7606连续采集几万个16位样品。这时DMA才能体现价值。配置DMA时有三个关键参数容易配错第一外设地址必须是SPI的数据寄存器地址而不是SPI的某个FIFO寄存器否则数据搬不到位第二内存地址要保证4字节对齐特别是用32位DMA搬运16位FIFO数据时如果内存地址不对齐会触发总线错误第三传输方向要分清“从内存读到SPI发送”和“从SPI接收写到内存”两种方向对应的DMA通道不同发展成收发全开的话要开两个DMA通道一个TX、一个RX。还有一个细节DMA搬运数据时CPU写SPI_DR会导致数据交叉错乱因为DMA和CPU同时访问外设寄存器会产生竞争。解决办法是配置好DMA后启动阶段唯一一次写入SPI_DR来触发SPI发送后续数据全部由DMA接管。我习惯的做法是先配置SPI为仅发送模式、使能DMA请求然后写SPI_DR唤醒转移紧接着使能DMA通道形成一个无缝衔接。这个顺序颠倒一下就可能在LED屏上看到第一个字节重复或丢失。2.4 实战AD7606和TF卡的SPI电路细节AD7606是16位8通道同步采样ADC采样率最高200kSPS使用SPI接口时要注意它有两个模式一种是普通的命令读模式主机发一个字节的配置命令然后从机持续输出通道数据另一种是自动循环模式CS拉低后直接连续读所有通道。我常用的是前者因为它更可控。电路连接上AD7606的SCLK最高频率有25MHz但建议开始不要跑太高先设成10MHz测通再往上提因为PCB走线寄生电容会影响MISO边沿尤其当MISO线上还有下拉电阻时边沿会更慢。AD7606的SPI从机模式有一个特点它的MISO在SCLK上升沿变化输出在下降沿被主机采样所以主机必须配置成CPOL1、CPHA1Mode 2也就是SCLK空闲为高、数据在第二个边沿采样。这个细节如果错了读出来的ADC数据会整体错位一位甚至全部出错。另外AD7606的/CS信号最好用GPIO控制而不用硬件NSS因为你要在读取过程中有一次“拉高再拉低”的操作来重新触发下一次转换硬件NSS很难做到这种中间打断的时序。再说TF卡TF卡在SPI模式下是半双工模式的简化主机发送命令从机回应响应之后数据流。TF卡需要一个上电延时还必须先发送至少74个时钟脉冲此时CS为高让它初始化。很多人的TF卡SPI老是初始化失败就是因为省略了这个“喂时钟”的过程。TF卡SPI模式下最大时钟频率通常25MHz但要注意CRC校验命令——有些卡在初始化时如果不带CRCCMD0的CRC字段为0x95会直接拒绝响应。另外TF卡的MISO线上建议加上拉电阻10kΩ左右因为卡内部输出可能是开漏或类似结构加上拉才能保证高电平正确。TF卡的CS拉高和拉低之间主机最好保持时钟空闲避免在CS无效时SCK继续翻转否则卡可能捕捉到杂散时钟沿导致状态错误。调试时我习惯先用低速SPI400kHz跑通初始化再切换到25MHz高速模式。这样能明显区分“电路问题”和“协议问题”省去很多无谓的纠结。3. I2C两根线打天下的“开漏”艺术I2C的发明初衷是“用最少导线连接最多设备”所以它把协议设计得极其精致。两根线SDA和SCL都是开漏输出必须接上拉电阻才能产生高电平。这也是I2C与SPI最根本的差异点之一。SPI的推挽输出直接驱动MOSI和MISO输出能力强速度快而I2C的开漏结构决定了它只能低速、长距离、抗干扰能力有限。但这个“弱”换来的是设备地址化、多主仲裁、时钟拉伸这些高级功能而且任意两个器件之间只有两根公共线扩展起来非常方便。我第一次接触I2C的时候不理解为什么要那么别扭地搞开漏。后来做I2C扩展芯片才明白开漏结构允许不同设备在不同的时间拉低总线而这种“线与”逻辑天然实现了多主仲裁和设备的应答机制。如果I2C也搞成推挽输出两个设备同时驱动总线时一个拉高一个拉低就会短路那才是灾难。开漏就像一个多人共用的电话线谁想说话就拉低线说完就释放总线保持高电平表示没人说话——非常优雅。3.1 开漏输出与推挽输出的本质区别推挽输出Push-Pull就是GPIO内部有两个管子一个接到VCCP-MOS一个接到GNDN-MOS输出高电平时P管导通电流可以灌进负载输出低电平时N管导通电流可以拉出去。这种结构输出阻抗低、速度高但“不能”同时两个输出对C强输出否则一个拉高一个拉低就是短路电流。开漏输出Open-Drain则只有下拉管接到GND高电平完全依赖外部上拉电阻。好处是多个开漏输出可以直接并联在同一条线上任何一个拉低就输出低大家都不拉才输出高。这就构成了“线与”逻辑是I2C总线的基础。坏处是上升沿时间受RC充电速度限制电阻越大上升沿越慢最大通信速率就越低。所以I2C总线上拉电阻的选择是一个经典的工程权衡。选上拉电阻有一条经验公式R_pullup ≤ t_rise / C_bus其中t_rise是总线的最大上升时间要求C_bus是总线电容总和。标准模式100kbit/s允许上升沿最多1微秒快速模式400kbit/s允许最多300纳秒。如果总线上挂的设备多布线长电容可能达到几百pF这时候用4.7kΩ的上拉电阻很容易让上升沿拖到微秒级别导致通信失败。我在一个挂有8个I2C设备、延长线约30cm的板子上初调用4.7kΩ电阻400kHz跑不起来后来换成2.2kΩ才稳定。电容不是凭空算的大概经验每挂一个I2C设备增加5~10pF每厘米PCB走线增加0.5~1pF算个总数再选电阻。3.2 地址、ACK与时钟拉伸I2C的“握手”玄机I2C的通信流程是STARTSCL高时SDA产生下降沿→ 地址字节7位地址读写位→ ACK从机在第9个时钟拉低SDA→ 数据字节ACK/NAK → STOPSCL高时SDA产生上升沿。这个框架很简单但每个环节都有魔鬼细节。地址方面7位地址的0x00是广播呼叫地址0x01是起始字节等特殊用途实际可用地址在0x02~0x7E。有些器件支持可编程地址引脚比如AT24C256EEE的A0/A1/A2三个引脚可以组合出8个不同地址坑的是这三个引脚内部有上拉/下拉陷阱有些封装的A0/A1/A2悬空时默认低电平所以你焊接后以为悬空0实际可能是1或不确定必须仔细读手册。另外地址冲突是I2C最常见的问题设备地址一旦撞车就得换芯片或者用I2C扩展器的地址分配功能所以画板前一定要列个地址表。ACK是I2C可靠性的基石。主机发送完每个字节后从机必须在第9个SCL低电平期间把SDA拉低否则主机认为传输失败。有一个反直觉的点如果是主机读数据当主机不想再收数据了它在最后一个字节时不发ACK而发NAK释放SDA然后发STOP从机才知道“别发了”。如果主机一直发ACK从机会一直接着发导致系统卡在读数据死循环。时钟拉伸Clock Stretching是I2C的一个高级功能从机在需要处理数据时可以把SCL拉低让主机暂停时钟直到从机准备就绪。很多老器件不支持时钟拉伸但有些EEPROM在页写的时候会延长时间用硬件的I2C外设时如果外设不支持自动处理时钟拉伸程序就得自己检测SCL电平来做软件上的等待。CH32V307的I2C硬件支持时钟拉伸但具体要配置相关寄存器不然可能忙着忙那反而出错。3.3 电平转换什么时候需要怎么做很多板级系统里MCU是3.3V但I2C设备可能是5V比如老式EEPROM、铁电存储还是5V供电的。如果直接把3.3V的引脚连到5V的开漏总线上由于开漏输出实际只是“不驱动高电平”只要上拉电阻接到5V两个不同电压域的器件就能安全通信前提是每个器件能够耐受5V的电压3.3V器件引脚常通过内部钳位二极管组成保护但超压会损坏。还有人担心“I2C需要电平转换吗”答案不是绝对的。如果3.3V设备较弱最稳妥的方案是用专用电平转换芯片比如PCA9306或者分立MOS管做的双向电平转换电路。分立方案常用2N7002MOS管方法是用两个MOS管背靠背接Vref1端接3.3VVref2端接5V两边总线各通过上拉电阻到各自电源。原理是一侧拉低时MOS管导通另一侧也被拉低一侧释放时MOS管截止另一侧靠上拉电阻恢复高电平。这个电路便宜且可靠是低速率I2C电平转换的标准做法。要注意的是电平转换会把时序延迟再加一点400kHz下要尽量用低电容MOS管同时两侧上拉电阻值要恰当2.2k~4.7k。如果你用了一个无源上拉电平转换而且在高速模式下出现数据乱码多半是上升沿太慢可以把电阻降到1k以下但要算好电流避免超出总线驱动能力。还有一个容易忽视的问题双向转换电路的MOS管栅极必须接两个电源域低侧的那个电源通常是3.3V接反了会完全不通。3.4 I2C扩展与编码器用小成本解决多设备连接I2C的一大优势是可以扩展。比如你MCU的I2C1只引出两组引脚但板上还有10个设备要挂这时就可以用I2C扩展器如PCF8574/PCF8575把I2C地址转换为并行GPIO。PCF8574占用一个I2C地址提供8个准双向IO每路IO可以输入或输出但要注意它内部是准双向结构输入时外部必须驱动成高或低不能浮空。PCF8574有两个地址引脚可以配置8种不同地址这样一个I2C总线就能挂8个PCF8574变成64路IO性价比很高。I2C编码器则是一个很有趣的应用。有些旋转编码器不并联EC11而用I2C接口芯片如AS5600直接输出12位角度数据。它的I2C地址固定为0x36不支持修改所以一个总线上最多只能挂一个AS5600除非你用地址转换芯片。还有I2C机械编码器模块内部用移位寄存器扫描按键状态读出来的数据格式复杂但接线确实省心——两根线加上电源就能读旋转方向和按键值。I2C扩展和编码器项目里我最常犯的错是忘记考虑“总线地址冲突”。如果设备地址完全固定且不可调那就得规划好哪个设备挂在I2C1哪个挂在I2C2。实在无法避免冲突时可以用TCA9548A这种I2C选择器把总线分成8路每路挂一个冲突设备。虽然多了一个芯片但逻辑清晰调试容易得多。3.5 软件I2C与硬件I2C的选择“软件I2C和硬件I2C的区别”绝对是搜索引擎常客。软件I2C是用GPIO模拟时序手动拉高拉低SCL/SDA并检测状态。硬件I2C则是芯片内部外设只要配置寄存器硬件自己产生START/STOP、时钟、ACK检测等。原则上如果MCU支持硬件I2C优先用硬件I2C因为软件模拟在时序上很脆尤其是在中断频繁的环境里稍有延迟就会被从机判定为超时。但现实是残酷的。很多老款MCU的硬件I2C被吐槽“难用、BUG多”比如STM32F1系列早期I2C外设就有很多坑导致很多工程师宁愿用软件I2C。其实现在的芯片比如GD32F303、CH32V307硬件I2C已经成熟得多只要配置正确稳定性和速度都远胜软件模拟。这里我给个实用建议如果你只是偶尔读写一个EEPROM数据量小、速率无所谓软件I2C完全能用还能减少调试外设配置的时间如果你要连续读传感器、刷OLED或者对时序有要求那就老老实实硬件I2C。软件I2C还有一个用途是“移植友好性”——当你想把一个传感器例程移植到任意MCU上只要手写一对IO模拟函数就能跑通完全不受外设寄存器差异困扰。CH32V307开发板上很多人用I2C驱动OLED就是因为CH32V307的I2C外设配置稍微复杂一点需要设置时序寄存器SCLT/H很多人索性用GPIO模拟I2C省心。这事没有绝对对错我自己的原则是量产项目用硬件I2C原型评估用软件I2C。4. SPI与I2C的实战对比与选型很多人选型的时候纠结“这个设备该用SPI还是I2C”其实只要遵循几条经验就能快速决策速度要求高、数据量大选SPI引脚紧张、挂载设备多、速率低选I2C。但真要落到实做还有一些隐性因素要考虑比如时序复杂度、驱动能力、从机数量、调试难度。我做了个表格方便大家对照着看项目SPII2C引脚数4根SCK/MOSI/MISO/CS2根SCL/SDA传输速率典型10~50Mbit/s标准100k快400k高速3.4M工作模式全双工半双工从机选择硬件片选或GPIO靠地址寻址从机数量受片选引脚数限制地址空间大理论多但受电容限制时序复杂度简单线少但模式多复杂有START/STOP/ACK等数据可靠性无应答机制每字节都有ACK硬件开销片选线多但线路简单上拉电阻、电平转换麻烦调试难度较容易抓波形直观较难时序节奏较多从实际项目角度讲SPI适合LCD/OLED尤其高刷新率屏幕、NOR Flash、TF卡、数据采集芯片AD7606、ADS1256、SPI多轴陀螺仪。I2C适合EEPROM、温度传感器、RTC、光传感器、I2C IO扩展器、I2C SSD1306 OLED不过小屏也可以SPI。如果你的MCU引脚特别紧张比如只有CN32个脚又想挂时钟、温度、EEPROM三个设备I2C一根总线搞定SPI就得占12个脚4*3瞬间就明白怎么选了。还有一点SPI多设备总线的总线上拉问题没有I2C那么明显但SPI的MISO线如果多个从机挂并联就需要保证每个从机未被选中时输出高阻否则会电平冲突。有些芯片手册只写了一行“MISO tri-state when CS is high”但实际要等CS拉高后一小段时间才能释放时序上容易有交叠此时应在MISO上加一个小电阻如100Ω做隔离或者改用片选信号带延迟的译码器来减少串扰。另外GD32F303的SPI和I2C外设在使用DMA时的中断标志位也有差异SPI的DMA传输完成事件是TXE/RXNEI2C则是TXBE/RXNE。习惯了STM32库的人换到GD32要特别注意因为寄存器定义虽然兼容但库函数的命名可能不同。我记得有一次用GD32F303的I2C1配合DMA读MEMS传感器DMA中断里清标志没清对导致第二次读取直接卡死后来改用轮询才稳。这个项目要是加个注释能省后来者半天时间。5. 常见问题与调试技巧实录这一章全是血泪教训。我汇总了SPI和I2C调试中最常见的几个“坑”以及怎么定位、怎么解决完全可以当速查表用。5.1 SPI接口测试的常见坑与定位方法SPI接口测试看起来很直观但最容易出错的地方不是波形而是初始化顺序和模式配置。最常见的坑是“MOSI和MISO接反”。尤其那些排线接口的模块比如SD卡模块经常有人把MOSI接SD卡的MISO上卡当然读不了。接反了的现象能写入读回的响应全是0xFF或0x00。解决办法永远都是先拿逻辑分析仪抓看一眼SDA、MOSI线上到底有没有数据。第二个坑是“时钟速率过高导致误码”。SPI从机的最大时钟频率都有标注比如某LCD模块支持最高30MHz但在建议工作电压下实际只能跑20MHz。如果你设置25MHz显示花屏这不是代码问题是速率问题。处理办法是把SPI时钟降到10MHz测试如果稳定了就逐步往上调。同时检查电源旁路电容是否靠近器件因为电流尖峰会导致器件内部逻辑误判。第三个坑跟“片选时序”有关。当用软件片选时必须先拉低CS延时一小段时间很多从机要求CS低到第一个SCK前沿之间有最小建立时间再开始发SCK。中断里操作时要仔细计算GPIO写寄存器到SPI开始发送之间的延迟。最好在CS拉低后加几个空指令的延时或者用SPI外设的脚本来保证时间。如果CS和SCK同时变换有些从机直接忽略整个传输。SPI DMA调试的坑更多DMA传输完成后SPI可能还处于忙碌状态如果立即拉高片选可能漏掉最后一个字节。解决办法是在DMA传输完成中断里先看SPI的忙碌标志BSY等BSY0后再拉CS。经验值是等几十微秒但最可靠是查标志。另外用DMA收发时如果DMA和SPI的时钟分频不匹配可能收到残段数据建议先用轮询模式跑通再加DMA分步调试永远是最省心的。5.2 I2C读写EEPROM的Verilog实现要点说到“i2c读写eeprom代码 verilog”这是FPGA新手比较喜欢做的练习。Verilog里实现I2C主机我一般拆成这几个状态IDLE、START、SEND_ADDR、WAIT_ACK、SEND_DATA、RECEIVE_DATA、SEND_NACK、STOP。核心是“周期计数器”因为每个位传输需要多个时钟周期比如目标400kHz系统时钟50MHz那么每个SCL周期要125个系统时钟周期。我用一个计数器从0数到124在一个周期的特定点改变SCL和SDA。写EEPROM时最需要注意的是“页写”和“写周期时间”。AT24C系列EEPROM每页通常8字节或16字节具体看型号如果你一次写入超过页大小地址会回卷导致数据覆盖错误。所以一次写数据必须按页边界拆分成多次写事务。每次写事务结束之后EEPROM内部进入写周期通常要等3~5毫秒这期间它不响应任何I2C命令。判断写周期结束的办法一是延时固定时间二是发一个无操作读来探测ACK如果EEPROM返回ACK说明写完。第二种更高效。Verilog实现里还要注意ACK检测在第9个时钟主机的SDA线必须处于输入状态所以要在那个周期把SDA设成高阻然后采样外部值。如果采到低电平说明从机应答若采到高电平则从机没有应答。有很多人的代码在发送完地址后忘了释放SDA导致读状态永远为1。另外I2C起始条件定义为SCL为高时SDA从高到低停止条件是SCL为高时SDA从低到高这两个条件必须严格遵守否则从机状态机就乱了。调试时用逻辑分析仪把SDA/SCL抓到屏幕上看START和STOP位置基本一眼能找到问题。5.3 总线死锁与复位策略I2C有一个经典死锁场景主机在传输过程中被复位比如看门狗而此时从机正处于数据输出状态正把SDA拉低如果主机复位释放总线变成高阻但因SDA仍被从机拉低导致总线一直处于低电平——看起来就像总线“卡死”了。解决这个问题的方法有两种一是硬件上在从机的SDA线上串一个可断开的隔离一般不做二是软件上用“总线恢复时序”主机的SCL线连续输出9个脉冲并在这9个周期内监测SDA当SDA变高后再发一个STOP让总线恢复空闲状态。这个操作相当于强迫从机释放SDA并结束当前通信。很多驱动库里都有这个函数比如BH1750传感器的init里就会先做一次总线恢复。另外一个死锁是软件模拟I2C时由于GPIO方向切换不及时导致SDA被拉低后无法读回高电平。解决办法是检查GPIO配置开漏输出模式下读引脚时要先关闭输出方向或者用输入模式读取。如果你的MCU的GPIO没有真正的开漏输出模式有些只有“推挽输出方向切换”需要额外配置那就用输出寄存器置1和方向寄存器切换来实现开漏。SPI的死锁相对少见但也可能因为CS电平配置错误导致从机认为总线一直忙。比如某些设备的CS是高有效而不是低有效如果你按低有效配置了主机一直选中从机从机可能持续输出数据把自己的缓冲区占满然后默默不理你了。所以永远先确认CS的有效极性别上来就按默认去拉。5.4 I2C读写EEPROM的代码片段参考以Verilog为例这里给一个简洁的I2C写单个字节的状态机片段注意这不是完整可综合工程只为展示关键逻辑。// 简化系统时钟50MHz目标I2C时钟约400kHz每个SCL半周期125个主时钟周期 localparam IDLE0, START1, SEND_BITS2, ACK_BIT3, STOP4; reg [6:0] cnt; // 位计数 reg [125:0] tcnt; // 时间计数 reg scl, sda_out; wire sda_in; always (posedge clk) begin case(state) IDLE: if(start_go) begin stateSTART; tcnt0; end START: begin // SCL高时SDA拉低构成START scl1; sda_out0; if(tcnt124) begin stateSEND_BITS; bit_cnt7; tcnt0; end else tcnttcnt1; end SEND_BITS: begin // 在SCL低电平期间改变SDA高电平期间保持 if(tcnt0) begin scl0; sda_outdata_to_send[bit_cnt]; end if(tcnt62) scl1; // 前半周期后半段拉高SCL if(tcnt124) begin if(bit_cnt0) stateACK_BIT; else begin bit_cntbit_cnt-1; tcnt0; end end else tcnttcnt1; end ACK_BIT: begin sda_out1; // 释放SDA让从机应答 if(tcnt62) scl0; // 下降沿 if(tcnt124) begin stateSTOP; tcnt0; end else tcnttcnt1; end STOP: begin scl0; sda_out0; if(tcnt62) scl1; if(tcnt124) begin sda_out1; stateIDLE; end // SCL高时SDA上升沿产生STOP else tcnttcnt1; end endcase end这个代码只是个骨架真正工程里还需要处理时钟拉伸检测、总线超时、错误状态恢复等。但只要你把START和STOP的时序卡正确后面扩展发送多字节就不难了。6. 写在最后我的几点实战体会我做嵌入式这么多年SPI和I2C算是用得最多的两种片间总线。说几句掏心窝的话给大家当参考。第一别迷信硬件外设也别妖魔化软件模拟。硬件外设速度快但寄存器配置复杂查问题看寄存器状态比扒波形麻烦软件模拟调试灵活但时序不够稳。我现在的习惯是所有外设驱动先跑通软件版本再切换到硬件外设这样既能快速验证系统逻辑又能让最终产品跑在硬件加速上。特别是I2C先软件模拟跑通通信流程再换硬件外设能省不少查手册的时间。第二示波器和逻辑分析仪是总线调试的底气。很多人对着代码发呆三个小时不如下个逻辑分析仪几十块的也能用抓十几秒波形。SPI看CS和SCK、MOSI的对齐关系I2C看START/STOP条件和ACK位。波形一出来很多问题直接就水落石出了。我强烈建议每个嵌入式工程师桌面常备一个8通道以上的逻辑分析仪。第三关于CH32V307这类RISC-V MCU库函数和例程跟STM32很相似但寄存器细节有差异特别是时钟树和外设复用时容易遇到“例程没问题就是跑不对”的怪现象。我测试CH32V307 I2C OLED例程时发现它的I2C配置需要额外设置I2C_CTLR2的FREQ字段如果不设置总线时钟就不是想要的400kHz时序可能错乱。所以遇到问题多翻手册别死盯例程。第四电路设计上一定要把总线电容和上拉电阻算好。I2C的2.2k、4.7k上拉不是固定值要根据总线长度和挂载数量调整。SPI的线长超过10cm就要考虑阻抗匹配信号线尽量短而粗或增加串联电阻阻尼振铃。早年在板级布线时忽略这些后期调试会让你怀疑人生。最后分享一个小技巧调试两个设备通信在主机端先发一串固定的字节如0x55、0xAA、0x01然后用逻辑分析仪看从机回发的数据如果回发的数据不是预期值优先怀疑寄存器配置和硬件连接不要急着查协议分析。等双方数据对齐了再上DMA或中断一次成功的概率会高很多。这些经验是我踩了无数次坑之后才总结出来的希望你少走弯路。
RELATED

相关推荐

PCIe配置空间与BAR空间详解:从枚举原理到掉卡排错实战

PCIe配置空间与BAR空间详解:从枚举原理到掉卡排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/26 1:09:03
VPLC+数字孪生:工业PLC调试从数月压缩至数小时的核心实践

VPLC+数字孪生:工业PLC调试从数月压缩至数小时的核心实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/26 1:09:03
ESP32应用商店实战:WebAssembly模块动态加载与OTA安全

ESP32应用商店实战:WebAssembly模块动态加载与OTA安全

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/26 1:09:03
MORE NEWS

更多资讯

📰

5个信号识别垃圾账号:anti-slop用户检查如何过滤AI slop贡献者

【免费下载链接】anti-slop A GitHub action that detects and automatically closes low-quality and AI slop PRs. 项目地址: https://gitcode.com/gh_mirrors/an/anti-slop 点击查看 免费下载 开源维护者正被大量低质量 PR 淹没,其中不少是 AI 自动生…

📰

cursor-free-vip 使用教程:三步重置 Cursor 机器 ID,恢复 AI 助手试用额度

cursor-free-vip 使用教程:三步重置 Cursor 机器 ID,恢复 AI 助手试用额度 【免费下载链接】cursor-free-vip [Support 0.45](Multi Language 多语言)自动注册 Cursor Ai ,自动重置机器ID , 免费升级使用Pr…

📰

ComfyUI 中文工作流合集:18 个现成模板,十分钟跑通第一张图

ComfyUI 中文工作流合集:18 个现成模板,十分钟跑通第一张图 【免费下载链接】ComfyUI-Workflows-ZHO 我的 ComfyUI 工作流合集 | My ComfyUI workflows collection 项目地址: https://gitcode.com/GitHub_Trending/co/ComfyUI-Workflows-ZHO Comf…

📰

G-Helper 华硕笔记本调校三步配好

G-Helper 华硕笔记本调校三步配好 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenbook, Expertbook, ROG Ally, and more. …

📰

YimMenu 实战指南:GTA5 开源菜单从 CMake 编译到 Lua 脚本扩展

YimMenu 实战指南:GTA5 开源菜单从 CMake 编译到 Lua 脚本扩展 【免费下载链接】YimMenu YimMenu, a GTA V menu protecting against a wide ranges of the public crashes and improving the overall experience. 项目地址: https://gitcode.com/GitHub_Trending…

📰

中科晟达注塑机 MES 系统:从车间到数字化的智能制造实践

1. 引言 注塑成型是制造业中应用最广泛的工艺之一,从汽车零部件、家电外壳到医疗器械、精密电子,几乎无处不在。然而,传统注塑车间长期面临设备状态不透明、生产计划靠经验、质量追溯困难、模具与工艺参数管理粗放等痛点。随着智能制造与工业…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬