ARTICLE DETAIL

资讯详情

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

DSP硬件I2C外设配置实战:告别GPIO软件模拟,稳定时序与高可靠通讯

DSP硬件I2C外设配置实战:告别GPIO软件模拟,稳定时序与高可靠通讯 做DSP开发的朋友应该都有过这种经历项目里要挂个传感器、EEPROM或者OLED屏I2C是最常用的低速总线之一。很多人图省事直接拿两个GPIO口用软件模拟时序我最早也是这么干的但后来在产线上被折腾得够呛——中断一多时序就飘主频一变延时全乱多挂几个从设备更是头疼。这篇文章我就聊聊怎么在DSP上老老实实把硬件I2C外设配起来彻底告别IO口软件模拟时序那种“能用但不稳”的方案。以下以TI C2000系列TMS320F28035为例其它DSP的I2C外设结构大同小异思路完全通用。1. 软件模拟I2C走过的弯路时序、CPU与扩展性1.1 用GPIO模拟I2C最大的问题不是“会不会”而是“稳不稳”软件模拟I2C的经典写法并不复杂就是找两个GPIO口一个当SCL一个当SDA然后按照起始条件、数据位、应答位、停止条件的时序图去拉电平。问题在于这套时序完全依赖CPU的指令周期去延时。主频是60MHz的时候你写一个DELAY_US(5)刚好凑出100kHz的SCL等哪天项目升级换了更高主频的芯片或者系统里使能了中断这段延时就会变。我自己踩过最典型的一个坑在一个温湿度采集板上用GPIO模拟I2C读SHT30单跑的时候一切正常后来把无线发送的中断打开SCL的高电平时间被中断服务程序拉长出好几微秒。逻辑分析仪上一看整个时序波形宽窄不一从设备偶尔就回一个NACK数据读出来偶尔跳变几个字节。查了好几天才定位到是中断打扰了延时循环。所以软件模拟方案的第一个瓶颈就是CPU一旦被抢占时序稳定性没保证。硬件I2C外设则完全不同SCL的每个高低电平都是内部计数器生成的中断来了它照样按计数器跑CPU就算进中断服务程序I2C总线上的波形也不会跟着变形。1.2 什么场景下软件模拟依然够用也不是说软件模拟一无是处。如果你只是临时调试或者板上只有一个固定的从设备总线速率不要求精确也不跑什么实时任务那GPIO模拟I2C确实简单直观出问题也好定位。再加上很多MCU的GPIO可以直接配置成开漏输出外部加上拉电阻配合延时翻转也能驱动I2C设备。但一旦你的系统上了RTOS、开了定时器中断、接了多个I2C从设备或者需要快速连续读取一批数据软件模拟就会变成麻烦制造者。读一个传感器还行要是一次读256字节的EEPROMCPU就全程空转在那里翻电平什么正事都干不了。这也是后面我下定决心在DSP上把硬件I2C外设调通的原因。1.3 硬件外设到底带来了什么改变DSP内部的I2C外设本质上是一个独立于CPU的总线控制器。它自己负责产生起始条件、发送地址、发送数据、检测应答、产生停止条件这些底层操作。CPU只需要配置好寄存器然后往发送数据寄存器里填字节或者从接收数据寄存器里取字节就行。这种设计带来的实际好处有三点第一SCL时钟由硬件定时器产生速率稳定不受CPU负载影响第二CPU在数据发送间隙可以被解放出来处理别的事配合中断甚至可以做到发送一批数据期间CPU完全不干预第三硬件外设自带总线仲裁和时钟同步逻辑如果总线上挂了多个主设备也不会因为软件时序来不及而冲突。这几条恰恰都是软件模拟方案给不了的。2. DSP硬件I2C外设的结构、时钟域与引脚复用2.1 I2C模块的内部组成以TI C2000系列为例I2C外设内部大致包含几个部分发送移位寄存器、接收移位寄存器、字节计数寄存器、时钟分频链路、状态机控制逻辑以及中断和FIFO缓冲具体芯片可能配置不同比如F28035带有16字节的FIFO。数据发送的过程可以简单理解成CPU把要发的字节写到I2CDXR发送数据寄存器硬件自动把数据搬到移位寄存器里然后在SCL时钟的配合下一位一位送到SDA线上同时从机发回来的数据会一位一位收进接收移位寄存器凑够一个字节以后再搬到I2CDRR接收数据寄存器CPU去读就行了。这里要特别留意的是“硬件自动搬运”这几个字。软件模拟I2C时每一位都是CPU亲自拉高的拉低硬件I2C则是移位寄存器在时钟沿自动工作。所以CPU发送一个字节之后不用盯着每一个时钟沿只需要等一个状态标志位从0变1表示这个字节已经发完就可以继续填下一个。这一个差异直接决定了CPU利用率的天壤之别。2.2 时钟如何从系统时钟一步步分频到SCLI2C外设的时钟不是直接等于系统时钟的中间经历了两级分频。以F28035为例系统时钟SYSCLKOUT首先经过I2CPSC寄存器预分频得到I2C模块时钟然后这个模块时钟再经过I2CCLKH高电平周期和I2CCLKL低电平周期两个寄存器的分频才得到最终的SCL时钟。用一个简化的公式表示就是I2C模块时钟 SYSCLKOUT / (I2CPSC 1) SCL周期 I2C模块时钟周期 × (I2CCLKH I2CCLKL 修正值)修正值跟具体芯片的内部电路延迟有关不同型号略有差异精确值要去数据手册里查时序参数表。但理解这个分频逻辑非常重要因为很多人配置I2C速率不对就是没搞明白系统时钟、模块时钟、SCL时钟这三者之间的关系配了一通之后发现SCL只有十几kHz或者飙到几MHz。我实测下来F28035工作在60MHz系统时钟下想让SCL跑到100kHz先把I2CPSC设为9让模块时钟落到6MHz再让I2CCLKH和I2CCLKL都设为30左右出来的SCL就非常接近100kHz了。如果目标是400kHz可以把系统时钟设成80MHz预分频设为4模块时钟16MHz高低电平寄存器设成约20同时确认外部上拉电阻阻值不能太大。2.3 GPIO复用配置容易出错的地方DSP的引脚大多是复用的I2C的SDA和SCL也不例外。F28035上I2C-A模块的SDA和SCL可以映射到GPIO28/29也可以映射到GPIO32/33具体由GPIO MUX寄存器决定。这里容易出问题的点在于很多人配置完I2C模块的寄存器却忘了把引脚复用切到I2C功能结果SCL和SDA两个引脚还处于GPIO模式电平根本不受I2C外设控制。表现出来的现象就是代码跑起来了寄存器状态看着都对但示波器上SDA和SCL就是没有波形。另外还有一个细节配置GPIO复用的时候最好先把引脚设为高阻或开漏状态再切换到I2C功能。如果引脚状态的切换顺序不对有可能在初始化瞬间把总线拉死导致后面I2C外设怎么发起始条件都失败。3. 寄存器级配置从零初始化硬件I2C3.1 先使能外设时钟配置GPIO引脚初始化的第一步是打开I2C外设的时钟。以F28035为例外设时钟控制寄存器里有一个I2CA的时钟使能位把它置1以后I2C模块才开始工作。很多人漏了这一步后续所有寄存器写入都是白写。然后是GPIO复用配置。比如用GPIO28/29作I2C引脚就需要在对应的GPAMUX寄存器里把这两个引脚的功能位改成I2C功能同时确认GPIO方向寄存器不受影响。I2C引脚由外设控制方向寄存器一般不用刻意设置。代码顺序上强烈建议先配置GPIO复用再使能I2C外设时钟。我一开始恰好反过来了先开时钟后配引脚复用结果外设已经上电而引脚还在普通IO状态总线上出现了短暂的低电平毛刺偶尔会把挂着的从设备搞懵。虽然不是什么严重故障但排查起来相当迷惑。3.2 计算并写入预分频与时序寄存器这是一个需要动笔算的步骤。先确定系统时钟频率然后看数据手册确认I2C模块时钟允许的范围。以F28035为例配置100kHz SCL可以按下面的参数设置SYSCLKOUT 60MHzI2CPSC 9得到模块时钟 60 / (9 1) 6MHzI2CCLKH 30I2CCLKL 30实测SCL约98kHz满足100kHz常规需求配置代码大致是这样// 以TMS320F28035为例配置I2C-A 100kHz主模式 void I2CA_Init(void) { // 1. 使能I2C-A外设时钟PCLKCR0寄存器对应位 ClkCfgRegs.PCLKCR0.bit.I2CAENCLK 1; // 2. 配置GPIO28/29为I2C功能具体位段以数据手册GPIO MUX表为准 GpioCtrlRegs.GPAMUX2.bit.GPIO28 1; // SDA GpioCtrlRegs.GPAMUX2.bit.GPIO29 1; // SCL // 3. 复位I2C模块配置寄存器 I2caRegs.I2CMDR.bit.IRS 0; // 4. 设置预分频模块时钟 SYSCLKOUT / (I2CPSC 1) I2caRegs.I2CPSC 9; // 5. 设置SCL高低电平周期 I2caRegs.I2CCLKL 30; I2caRegs.I2CCLKH 30; // 6. 配置为主模式7位地址释放总线IRS1 I2caRegs.I2CMDR.all 0x0000; I2caRegs.I2CMDR.bit.MST 1; // 主模式 I2caRegs.I2CMDR.bit.IRS 1; // 模块退出复位 }这里特别说一下I2CMDR寄存器里的IRS位。这个位是I2C模块的复位控制配置外部寄存器的时候先把它清零让模块处于复位状态等时钟分频、地址模式这些参数全部写完之后再把它置1模块才开始正常运行。跳过清零这一步很有可能你前脚写的分频值后脚被模块内部逻辑覆盖掉时序完全不对。3.3 主从模式、地址模式与运行控制I2C总线有多种工作模式主发送、主接收、从发送、从接收。DSP的I2C外设这四个模式都支持但平时用得最多的还是主模式。配置主模式需要设置I2CMDR的MST位同时确认是否使用10位地址。大部分小器件都是7位地址模式所以A位扩展地址默认保持0就行。还有一个容易被忽略的是数字回环模式DLB这个模式可以让发送和接收在芯片内部短接相当于自己跟自己通讯。调试的时候很有用比如你怀疑外部电路有问题可以先开启这个模式确认I2C模块本身能不能正常收发。但正常工作之前一定要把它关掉不然数据根本送不到外部总线上。3.4 一个容易忽略的坑外设时钟与GPIO配置顺序我在3.1提到配置顺序这里再展开一下。GPIO复用配置等于先告诉芯片“这两个引脚现在由I2C外设接管”然后给I2C外设送电。如果送电之后才切复用引脚在外设上电瞬间可能仍处于默认的GPIO状态而这个状态如果是输出低电平就会在SDA或SCL上产生一个不该有的低脉冲。最好的习惯是先把引脚设成输入或高阻再配置复用功能最后使能外设时钟。对于I2C这种开漏总线引脚初始化为高阻状态最安全因为外部上拉电阻会把电平拉到高不会产生干扰。4. 主模式下的数据收发寄存器轮询与中断两种方式4.1 主发送完整流程配置好I2C模块之后发送一个字节给从设备的流程可以拆成几步。第一步在I2CSAR寄存器里写入从设备地址。注意这个地址是7位地址数据手册里给的一般是裸地址不需要左移一位硬件会自动帮你把读写位拼上去。第二步把要发送的字节数写入I2CCNT寄存器。这里有个非常实用的细节I2CCNT是8位寄存器最大值只有255。也就是说单次I2C事务最多只能发255个字节。如果你要一次写256字节以上必须拆成多次事务中间加重复起始条件或者停止条件。第三步把I2CMDR里的STT位置1启动起始条件并开始发送。如果是多字节数据可以先把第一个字节写入I2CDXR然后等状态寄存器里发送就绪标志置位后再填下一个字节。第四步全部数据发送完毕后把STP位置1产生停止条件。这一步看起来简单但实际轮询判断时要注意状态标志位的确切含义。发送就绪标志和整个事务完成标志不是一回事。发送就绪只代表当前这个字节已经移出不代表从设备已经正确应答。要从状态寄存器里找到“无应答”或“总线忙”的状态位才能真正判断数据发没发成功。4.2 主接收完整流程与“最后一个字节”的玄机从设备读数据比发送稍复杂一点。流程是先发起始条件发从设备地址读方向然后连续接收字节最后发停止条件。这里有一个特别容易踩的坑接收模式下倒数第二个字节接收完成后主设备必须让从设备知道“下一个字节是最后一个”方法是在接收倒数第二个字节时设置一个“发送NACK”的标志。如果没有这个NACK很多从设备会以为你还要继续读停在总线上不释放。我最早在调AT24C02 EEPROM的时候就是读出来的数据最后一个字节不对要么是重复的旧值要么直接多出来一个0xFF。后来查资料才明白I2C协议里规定主接收方读完最后一个字节后要回复一个NACK从设备才会认为事务结束并释放SDA线。硬件I2C外设通常在寄存器里提供了相应的控制位你需要在正确的时间点去置这个位顺序错了还是不行。4.3 中断方式与字节计数的配合如果你的系统里有其他实时任务用轮询方式读I2C会占用大量CPU时间这时候可以切换成中断方式。配置思路是把I2C模块的接收FIFO或发送FIFO事件映射到中断控制器然后在中断服务程序里处理数据。F28035的I2C外设带有FIFO功能发送和接收各有16字节深度。有了FIFO之后CPU可以一下子把多个字节填进发送FIFO硬件自己按顺序往外发接收的时候也可以攒够一批数据再触发中断减少打断CPU的次数。使用FIFO时要注意中断标志的判断。发送FIFO有“水位线”设置你可以决定还剩几个字节时产生中断接收FIFO同理可以设置攒够几个字节再通知CPU。调试的时候建议先把水位线设成1跑通了以后再根据实际需要调深一上来就大深度容易出一些难以定位的时序问题。4.4 一个经典场景读写AT24C02 EEPROM为了把上面的流程串起来我拿最常见的AT24C02举例。这个芯片容量2Kbit也就是256字节I2C地址一般是0xA07位地址是0x50。读单个字节的时序是发送起始条件发送从设备地址写方向0xA0发送要读取的字节地址发送重复起始条件注意不是停止再开始发送从设备地址读方向0xA1读取一个字节发送NACK发送停止条件第4步这是很容易写错的地方。有些人图省事在第3步之后先发停止条件再重新发起始条件虽然大多数从设备也能接受但严格来说I2C协议里提供了重复起始条件在同一个事务内切换读写方向应该优先使用它。DSP的硬件I2C外设对这个场景的支持方式是在需要产生重复起始时先读到接收寄存器最后一个字节然后重新置STT位硬件会在当前事务内部插入一个重复起始条件而不是先停再启。我建议初学的朋友一定要先用逻辑分析仪把这段波形抓出来看一眼理解了重复起始和“停止再开始”的区别后面再调更复杂的传感器就顺了。5. 波形级验证示波器、逻辑分析仪与故障排查5.1 想确认时序对不对别靠猜调试I2C最忌讳的就是不抓波形只看代码。代码逻辑上完全没问题但可能外部上拉电阻虚焊、从设备地址搞错、引脚复用没切对这些光看寄存器是看不出来的。工具上逻辑分析仪比示波器好用得多。I2C协议分析这种中低速信号一个几十块钱的8通道逻辑分析仪配合PC端软件就能自动解析出地址、数据、ACK/NACK一眼就能看出是哪一步出了问题。示波器适合看模拟波形质量比如边沿的上升时间、低电平是0V还是0.5V、有没有振铃这些逻辑分析仪看不出来。抓波形时的关键节点起始条件SDA先拉低SCL保持高地址字节前7位是从设备地址第8位是读写方向应答位第9个SCL时钟SDA被从设备拉低表示ACK停止条件SDA在SCL高电平期间拉高如果这些节点都能一一对上基本可以判定I2C通讯本身是通的。剩下的就是业务逻辑层面的数据解析了。5.2 常见故障及排查链路I2C调试中问题最多的几个现象我整理过一个排查顺序表现象大概率原因排查方向SDA/SCL完全没有波形外设时钟没开、GPIO复用没配、模块复位未释放先查寄存器配置顺序再查引脚复用SCL有波形但SDA一直高从设备地址错误、上拉电阻问题、从设备未上电抓波形看ACK位检查从机地址和供电数据读出来全是0xFF上拉电阻虚焊或阻值过大、从设备未应答检查总线上拉检查地址排除从机故障数据读出来全0x00SDA对地短路、从设备地址写成了写方向断电测SDA对地电阻重新核对方向位一次能读到部分数据后面卡住最后一个字节NACK没设置、字节计数超过FIFO深度检查接收最后一个字节的处理减少单次长度这里面有个经验看到数据全0xFF先别怀疑DSP配置多半是外部上拉出了问题。I2C总线是开漏结构高电平全靠上拉电阻提供。上拉电阻没焊好或者虚焊SDA就永远停留在低电平或者随机状态读出来的数据极容易全是0xFF。5.3 总线卡死后的复位技巧I2C总线卡死的现象很典型SCL有波形但SDA一直保持低电平怎么发起始条件都发不出去。这种情况通常是总线进入了一种“死锁”状态从设备可能正处于某个中间状态一直在等主设备的下一个时钟边沿而主设备因为检测到总线忙不愿意继续发时钟。通用的解锁办法是这样的把I2C的SCL引脚临时配置成普通GPIO输出手动输出9个时钟脉冲同时把SDA保持在高电平。9个脉冲能让大部分从设备从错误的中间状态恢复出来释放SDA。要注意的是在切换SCL工作模式之前先记住原配置解锁完之后再切回来。我一开始遇到总线死锁第一反应是给DSP断电重启这在开发阶段还凑合但到了现场就非常狼狈。后来在初始化代码里加了一个“总线恢复”函数每次I2C通讯失败超过一定次数就自动调用。效果非常好至少省掉了一半的现场复位操作。6. 实测数据硬件I2C与软件模拟的关键指标对比6.1 时序抖动与误码率我在同一块DSP板上做过一组对比实验分别用GPIO软件模拟和硬件I2C外设去读同一个SHT30温湿度传感器SCL目标速率都设成100kHz连续读1000次统计时序抖动和误码指标GPIO软件模拟硬件I2C外设SCL高电平最大抖动±10us以上几乎无抖动1000次读取误码次数3次左右0次开中断后波形变化明显变形无变化单字节发送CPU占用全程占用约等于写入寄存器的时间这个结果很直观。没有中断干扰的时候软件模拟也能工作但一旦系统复杂起来时序抖动对通讯可靠性的影响就会凸显。硬件I2C外设把时序交给了独立的计数器遇到中断只影响CPU执行代码不影响总线波形这一点在真实项目里价值极高。6.2 CPU占用与代码复杂度软件模拟I2C的代码写起来很直观就是一堆延时和电平翻转但代价是CPU被长时间占住。发送一个字节哪怕SCL是100kHz也至少要持续80us左右的总线时间这期间如果用的是纯软件延时CPU基本干不了别的事。硬件I2C外设配合FIFO之后CPU把一个字节填进发送FIFO然后就可以去忙别的了硬件自己会把字节发完。如果是中断模式CPU甚至只需要在FIFO需要补充数据的时候才短暂介入。对于一个需要同时采集多路传感器、跑通信协议栈的系统来说这个差别是决定性的。6.3 多从设备扩展与总线仲裁软件模拟I2C挂多个从设备时最大的问题是每个设备的地址、速率、时序要求都得靠代码去适配一旦某个设备时钟延展时间过长软件延时逻辑可能直接卡死。硬件外设对时钟延展有自动处理机制从设备拉低SCL要求主设备等待的时候硬件会先暂停SCL等从设备释放之后再继续CPU不用管这些细节。另外I2C总线上如果出现两个主设备同时发送数据硬件外设通过比较SDA电平和内部发送值来自动仲裁仲裁失败的主设备会自动停手等总线空闲再重试。这在多主系统中非常重要也是软件模拟方案极难实现的功能之一。虽然大部分DSP应用是单主模式但了解这一点对理解I2C协议的底层机制帮助很大。6.4 什么情况下还可以继续用软件模拟话说回来硬件I2C外设也不是万能的。有些DSP型号的硬件I2C外设在极低速率下有分频精度问题比如想跑个10kHz的慢速总线去兼容某个老传感器反而GPIO模拟更灵活想设多少就设多少。再者如果只是开发初期快速验证一下传感器能不能出数软件模拟确实比写一堆初始化代码快得多。我的建议是把两条路都保留在代码库里正式功能走硬件I2C临时调试工具走软件模拟。两个模块互不干扰调试的时候还能互相印证算是比较实用的工程习惯。最后再分享一个我自己的小经验配置硬件I2C外设的时候先把SCL和SDA都确认在高电平然后把从设备地址、方向位、寄存器地址这三项逐项在逻辑分析仪上核对一遍最后再看数据。我见过太多人一上来就怀疑代码逻辑查了半天寄存器最后发现示波器探针压根没夹好。先看波形再改代码这个顺序能帮你省下大半天的排查时间。
返回列表