【STM32学习笔记】STM32 I2C读写EEPROM(AT24C02)的一些实际问题和Bug


本文中的IDE是VScode,用的EIDE插件调用Keil的底层,所以跟Keil是一样的,只不过外观不一样。做实验用的是野火的霸道板子。下面的问题和细节在我的bilibili视频上会有说明。

我的bilibili视频链接


1.STM32 I2C 通讯失败后Busy位一直为1的问题

相信只要玩过STM32 I2C外设的都有碰到过这个问题,调试的时候经常能看到刚刚初始化完成或者程序一运行Busy位就为1的情况,网上查询了一下,这个问题已经存在很多年了,我也是准大二,刚刚调试完I2C,在学习的很多阶段都遇到过这个问题,我总结出来我碰到过的三种情况


a.程序错误导致的通讯失败,从而进入死循环


原因分析:

对于刚开始写驱动的小白而言,对STM I2C时序图没有深入了解,所以代码容易写错,如果以错误的时序驱动STM32 I2C,那么当把程序烧入到STM32时,程序一瞬间运行卡死,然后导致Busy锁死,锁死之后调试发现还未初始化I2C 的SR2寄存器Busy位就有值,然后尝试复位,但只是按钮复位是不能清除锁死的Busy位,重新上电复位才能清除。上电后又运行错误的程序,然后就进入了无限制的循环。

(PS:有些人可能会说其实不用管这个Busy位继续调试就行,但是作为初学者来说应该要知道Busy位为1这个问题侧面反映了你自己写程序是有问题的,如果程序正确了,Busy也就不为1(除非有其他的情况)。)


解决方案:

把程序调正确。可以看火哥或者正点原子的视频,或者直接看正确的源代码(下面有野火亲测可用代码),如果能够正确运行别人的代码,而自己的代码不好使,那么就是代码的问题;如果都不好使,那么可能是其他两种情况。

野火示例代码


案例:

计划是写WriteByte()函数和WritePage()函数,最后写一个ReadByte()函数验证,用串口输出测试信息。
(PS:这里的WriteByte()是用来写一个字节的,WritePage()是页写,ReadByte()是读多个字节)。

写WriteByte函数没有任何问题,贴图如下。

然后后面也是一口气把WritePage()和ReadByte()函数全部写完,当然程序是有问题的,因为是自己照着时序图写的,没有在意一些细节,调试结果如下。

调试信息提示函数是卡在WritePage();然后进入调试环节,这里可以看到我已进入调试就出现了刚初始化I2C后SR2寄存器为2(Busy位为1)的情况,复位后也是如此,然后我将后来写的两个函数注释掉,就没有发生过这种情况,所以由此推断是程序编写错误所导致的初始化后Busy=1的问题。

把程序调试好后就没有问题了结果如下。


b.硬件错误导致的通讯失败,从而进入死循环


原因分析:

没有分析自己的硬件情况,盲目调试代码。比如:根据Busy位的介绍可以看到,SDA和SCL为低电平时,硬件将置1,这个置1是实时的,也就是这个位会根据你硬件的实际情况实时变化(即使I2C没有使能也会变化)。就比如你的SCL和SDA线突然短路了,那么就会导致这种情况。


解决方案:

可以尝试拿万用表测试一下引脚的电压值,看看是否达到所需要求,当然这个电压范围也要在EEPROM工作范围内,如果元件好使的话就是电路的问题,仔细排查肯定没问题。


案例:

刚把程序调试完,打算把程序升级一下,在追加一个WriteBuffer()函数,但是写完之后烧入第一遍是没问题的,然后用手摸了一下板子,结果发现程序又不行了,我就估计着这个板子可能是有些地方被摸短路了,重新上电程序正常运行,这是第一个地方。后面把这个程序用在I2C2上(板子上的I2C线路默认是接I2C1的),测试了一下发现Busy=1,这次是因为I2C2的引脚没有接入线路,所以无法正常通讯。


c.工作到一半因为各种原因通讯失败,调试的时候出现Busy=1


因为在实际开发生产过程很可能因为人为原因导致这个问题,但是又不可能断电复位,那么Busy位被锁死就非常棘手,所以这里可以借鉴一位优秀博主的解决方案。

解决方案

具体操作流程可以看我视频,方案概括起来就是在初始化I2C之前先利用GPIO IPU模式上拉使其引脚高电平,然后再利用CR1寄存器的第15位SWRST软件复位,这样一来就能在SCL和SDA总线高电平的基础之上复位Busy,最后再重新配置GPIO为开漏继续初始化I2C。
这里可以看到这个软件复位说明中有提到可以复位Busy位。


2.STM32 I2C 在调试的时候明明已经GenerateSTART结果SB还是为0

原因分析:

STM32的这个位自带检测功能,也就是说这个位只有在==正确发送完==后才会置位,即使发送完成但是没有发送正确也是不置位的。就例如我给SCL线拉低了,这个时候就不满足起始条件的要求。


解决方案:

如果是硬件问题,可以拿万用表测一下引脚,看看是否满足电位需求,一般来说硬件没有问题了这个位是没问题的。


案例:

将开发板上的PB6(I2C1 SCL引脚)接入3.3V或者5V,程序就会出错,调试结果如下:

通过Debug我们也可以看到此时SR1寄存器的值为0,也就说明了SCL的电压低于所需的高电平标准,此时无法正确产生起始信号。


3.火哥视频中所提到的问题(不能用CheckEvent(),可以用getFlagStatus())

原因分析:

首先说明一点:
下面的结论大部分讨论的是发送的情况(主机当发送端的情况),EV6事件是这个问题的关键,EV6事件检测5个位,分别为Busy,TRA,MSL,TXE,ADDR。所以下面的结论都会围绕这5个位进行说明。还有下面的图片不是直接用的固件库函数,是经过自己封装过的函数,效果与固件库一致。

这里先提出四条结论:
==1.由于STM32 I2C发送速率(Max = 400K)远小于单片机本身运行的速率(72M),所以从发送(接收)完成到检测到EV6,EV8事件这段时间可以运行很多行代码,在主频为72M下I2C传输速率为400K情况下测出,要经过刚好8条CheckEvent()才会出现一次发送成功。==
==2.TXE和TRA以及ADDR位在地址发送成功并成功响应条件下是同时置位的。并且TXE和TRA状态时刻相同(至少我测出来是这样的)。==
==3.TXE和TRA会在起始条件或者停止条件后由硬件自动清除。==
==4.TXE和TRA的置位会受到从机应答的影响,过程为:主机先发送数据,发送结束后先等待从机的应答,如果从机应答,那么将TXE,TRA,ADDR置1,否则置0。==


以下是关于结论说明


结论1:

不难看出这一条结论说明当写入DR寄存器后仅仅只是写入了DR寄存器,数据也可能到达了移位寄存器,但是数据还没有完全发送出去,这需要好几次CheckEvent()执行完毕后才可能检测出EV6,或者EV8事件。所以从这条可以得出在使用SendData()后或者使用Send7bitAddress()后一定要跟一个检测事件循环。


结论2:

经过测试可以得出在使用Send7bitAddress()后这些位同时置位的。视频中有相应说明,下面也有相关配图。
在使用Send7bitAddress()前:
可以看到SR1=1,SR2=3,这里的TRA和TXE还有ADDR都为0,SR1=1是刚发送了起始条件,SB=1。


在使用Send7bitAddress()后:
这里的SR1 = 130,SR2 = 7,说明了TRA和TXE还有ADDR都为1。当然这是在调试断点的时候的,已经过了很长时间,两个断点之间的时间足以让EEPROM响应,所以这两组数据说服力不强。


所以我又重新做了一个实验,通过改写固件库中的CheckEvent()函数,往里面加了几行代码,使得每次寄存器的值都能够被记录到两个数组中。
贴图如下:


由于前面有一个EV5事件的检测,所以会有几组数据是EV5的数据,而我们所需要的是EV6事件的数据,所以可以先在检测EV6前断个点,最好是在Send7bitAddress()之前断点,得到如下结果。

SR1寄存器:

**SR2寄存器:** **可以看到都是4组数据,然后我们只需从第5组数据开始分析数据即可(EV5事件结束后就是EV6)。 继续往下运行得: SR1寄存器:** **SR2寄存器:** **刚好SR1寄存器的8个0对应SR2寄存器8个3,SR1寄存器最后一个130对应最后一个7。本次的测试没有将发送地址和检测EV6事件断开,是直接运行到底的。这也就很好的说明了结论1,并且最后一组数据说明了ADDR和TXE和TRA是同时置位的。这个也间接说明了结论4的正确性,不难猜出这三个位是有关联的。**

结论3:

这一条是对于结论2的理论说明,先来看一下各个位的说明
TXE位:

TRA位:

图上也说明了起始条件或停止条件都会清除这两个位,而且也可以看到TRA的值是根据RW来的,所以如果是发送情况的话这两个位是相同的。


结论4(重点!!!):

这里可以沿用结论2的实验,操作如下:
在Send7bitAddress()的前面先断一个点,然后给SCL接一个5V(或者3.3V)导致SCL为低电平,在开始往下运行,然后按暂停键,查看寄存器的值。
结果如下:

SR1寄存器:

SR2寄存器:

还是从第5个数据开始分析,不难看出现处于总线出错状态,所以BERR位置1,也就是SR1=256。从这里也可以看出TXE和ADDR以及TRA位都为0。对于TXE和TRA的一般认识都是I2C模块发送完毕后就会置位,但是这里并没有置位,因为总线出错的缘故,所以导致应答失败,进而没有置位TXE和TRA。
(有些杠精如果要说我这个AF应答失败位为啥没有置位,那么你去视频里看细节或者下面对于火哥问题的具体分析过程,这里就不细说,因为实际调Bug的时候没有这么顺利,也不是做这些实验调出来的,这些实验只能一部分说明,不能完全说明)。


回归问题

首先先将还未经过修改的代码贴出来
(PS:强调一下,这个代码经过个人封装,跟野火的不一样,抄了也没用,自己看懂了再去改)

大概说明一下程序,首先,程序运行一次WriteByte(),然后再运行这个CheckAT24C02Busy函数(火哥的那个是WaitForWriteEnd(),一样的)。这个时候EEPROM正在忙碌,所以程序会整体卡在这部分代码里。

1.第一点

根据上述结论1和结论3程序运行完Send7bitAddress()后,只是经过一次CheckEvent()判断EV6事件,而新一轮循环又开始产生START信号,所以一次CheckEvent()的时间完全不足以置位TXE和TRA,其次有START信号就会清除TXE和TRA位,所以EV6永远不可能检测的出来。

2.第二点

即使可能发送出去了,因为EV5事件检测时需要读SR1和SR2寄存器,并且Send7bitAddress()函数会写入DR寄存器,这样就会清除ADDR位。这样的话5个位有3个位丢失了。


针对前两点的临时解决方案

1. 先把EV5事件的CheckEvent()改为getFlag(),用于检测SB位,这样的话只读取了SR1寄存器,不是两个寄存器都读,也就不会清除ADDR位。

2. 在Send7bitAddress()后面,CheckEvent()前面加一个While,用于检测TXE位,理论上可以保证有时间发送完成地址。(后面会说明这个地方是错误的)。

得到如下结果:


按下F5调试,你会发现程序会卡死在检测TXE位上,为什么???

细节解释

根据最重要的结论4可知:由于EEPROM处于忙碌状态,在Send7bitAddress()后会处于应答失败的情况,也就是AF=1,这样一来TXE和TRA就不能置位,所以这样检验是徒劳的,只会让程序卡死。

处理方案

不能光检测TXE位,应当AF和TXE同时检测,从上面的结论可以推导出来,如果应答成功,则AF=0,TXE=1,应答失败,则AF=1,TXE=0。所以可以同时检测两个位,结果如下:

说明一下细节:
如果应答成功,那么这个检测AF和TXE的While()肯定能够满足地址有足够的发送时间。
如果应答失败,那么这个也可以有足够的时间,因为AF的置位是要等发送完成并且花一定的时间去等待应答,肯定够了。
所以就是要么AF=1,要么TXE=1,毕竟这两个位是有关系的。

最后要注意的是,应答失败的循环肯定有好几次,那么对于用于检测
AF和TXE循环肯定是有影响的,毕竟AF位又不会自动清除,所以在每遍循环都要软件清除AF位。

修改后代码的最终成品:


4.后记

对于修改成功的代码可以往里面加入超时处理和用于输出调试信息的代码。整个过程来讲并没有这篇博客写的这么简单,前前后后也是经过反复测试和猜想的,这段过程给我最大的感受就是有些可能不是硬件上Bug,只能说是缺陷,只不过不知道而已,就比如这个SB位和TXE位都设计的过于智能,能够检测过程是否成功,成功再置位。这些东西在文档中没有说明,都得靠自己去摸索。再怎么Bug也是可以避过的,大不了用软件I2C(实际开发项目的时候是不建议这么做的,少了很多功能)。加油吧,打工人。





==如果文章写的有问题的话,还请各位大佬指点==





原创的文章,写作不易,引用的时候记得给个链接!!!!!!!!!!!!!