浅谈 STM32 硬件I2C的使用 (中断方式 无DMA 无最高优先级)

使用前请先充分验证该方法及相关代码 2016.02.03

引子

STM32的硬件I2C很多人都对它望而却步。因为很多电工都说,STM32 硬件 I2C有BUG、不稳定、死机等等……最后都使用GPIO模拟I2C。

的确,模拟I2C好用。但是在我看来在一个72M的Cortex-M3的MCU上这样做非常不妥。一般来说I2C是一种慢速总线,就算工作在400kHz的快速模式上,I2C传送每个字节仍需要至少23us——还没有计算地址、起始信号和结束信号的发送。如果使用GPIO模拟的I2C,这23us的CPU时间都在空转中浪费了,而这23us已经可以做不少的事情了,所以在STM32上I2C还是使用硬件为佳——虽然它多多少少有点缺陷。

这篇文章不是给完全没有接触过STM32 硬件I2C的新手看的,看这篇文章之前至少先阅读STM32的参考手册(RM0008)。

转载请注明出处 http://racede.me/talk_about_stm32_i2c_peripheral.html

 

概览

我们先来看一下STM32 I2C硬件的结构

我们可以看见STM32的硬件I2C有两个和数据有关的寄存器“数据寄存器(Data register)”(DR)和“数据移位寄存器(Data shift register)”(DSR),我们的软件写入的是DR, DSR用于I2C数据的移位发送和接收,DR和DSR的数据交换由硬件控制——发送时DSR为空,DR不为空时,硬件自动把DR的数据写进DSR;接收时DR为空,DSR不为空,硬件自动把DSR数据写进DR。连续数据传输时,这样两个寄存器的数据交换使得软件读出和写入DR不会影响I2C总线中的数据接收和发送,使I2C的效率更高,这看起来十分美好,但是正是这个特点在某些情况下会变成电工们的噩梦。原因有二。

1、硬件上,DR和DSR的交换机制存在缺陷。

2、软件上,因为DR和DSR一共能容纳两个字节的数据,导致接收时候NACK的设置有一定的不可预料性。

 

硬件

硬件I2C上的缺陷,新版英文ErrSheet已经写得很清楚,就不引用了,这里只简单说说要点和一些个人总结。

1、EV7, EV7_1, EV6_1, EV6_3, EV2, EV8 和 EV3 必须在当前字节传输前处理完成,不然,有可能会导致数据出错。

这几个事件都涉及到DR和DSR,个人猜测(主要是有个”may be”才敢猜测)可能是读出或者写入DR的同时DSR被填满或清空,导致数据出错。理想情况下“读出或者写入DR的同时DSR被填满或清空”是不可能发生的,中断一来临的时候,CPU马上处理中断请求,读出或者写入DR数据,这时DSR的数据还是“新鲜滚热辣”的,可能连一位都没有接收或发送。但是,在实际使用时,可能有别的中断优先级比I2C的事件中断要高,I2C事件没有及时处理而出现了上述的情况。所以,ST建议把I2C的事件中断设置成最高优先级。

2、产生STOP前DSR必须为空,不然,会导致DSR里的数据左移一位。

这个没什么好说的,就是一个硬件的BUG,保证发送STOP前DSR没有数据就可以了。

3、总线上,开始条件(S)后没有进行数据传输就马上设置停止条件(P),或者S后忘记P会导致硬件I2C不能再次产生S,必须软复位I2C。

这个ST解释成是,STM32严格按照了I2C的标准,S之后没有数据传输是不能P的。其实这点可以体谅,但是,这点如果没有处理好,总线上的错误会导致STM32 I2C陷入瘫痪。

 

软件

由于DR和DSR的存在,编程上需要一些技巧,新版英文ErrSheet和参考手册(RM0008)都有相关的操作介绍(Closing the communication),排除硬件上的缺陷,编程的难点主要在接收时如何可靠地设置NACK上。

在只有DSR的MCU上设置NACK是非常简单的,在读出倒数第二个数据前设置一下就可以了,但是个方法在似乎在STM32上行不通,因为STM32有DR和DSR,在倒数第二个数据被接收的时候(RxNE置位),马上设置NACK,理想情况下没有任何问题,NACK也被正确的发送,但是如果有其他更高优先级的中断打断了这个过程,NACK就不能及时设置,导致从器件收到的是ACK没有释放总线……

ST提供的资料上(笔者所见),给电工们的建议。

1、接收2个字节或1个字节时,切换GPIO模式为OD,然后软件下拉SCL引脚,使硬件I2C发生时钟延展,把下一个字节开始传输的时机延后,设置完NACK后,再把GPIO设置回AFOD,但是这只能解决小于两个字节的接收。

2、大于2个字节用DMA,DMA可以说是特效药,“屡试不爽”。不过要注意,接收大于或等于2个字节时才能使用DMA,不然不能产生EOT-1事件导致NACK不能正确发送。

3、设置I2C事件中断为最高优先等级。

 

方案

读到这里你可能会想,硬件有缺陷,软件也得这么“猥琐”,可以说是寸步难行。真的没有其他办法了吗?其实,我们可以把DR和DSR两个当一个用,全部判断BTF,不理会TxE和RxE,用时间来换稳定性,慢点就慢点总比没得用好。

[important]

发送时:

开始,发送写地址,器件应答,清ADDR,一字节数据到写DR,硬件把DR数据写入到DSR,当DSR传输完毕时,DR也为空,BTF置位,这时我们再写一字节数据到DR,如此循环,最后一次BTF置位的时候发送P或者重起始(R)。这样操作,“硬件把DR数据写入到DSR”执行的时间是我们可以预料的,不存在上面提及的冲突问题。

接收时:

1、接收一个字节:按照ST给的方法。开始,发送读地址,器件应答,清ADDR前软件下拉SCL,写完NACK、STOP和DR后软件再释放SCL。RxNE时读DR。

2、接收两个字节:也是按照ST的方法。开始,发送读地址,器件应答,设置POS和ACK,下拉SCL,清ADDR,设置NACK,释放SCL。BTF时,软件拉低SCL,发送STOP,读DR,释放SCL,再读DR。

3、接收两个以上字节:开始,发送读地址,器件应答,直接清ADDR。BTF时,读DR一次。再BTF,再读DR一次,如此循环。倒数第二次BTF时设置NACK(注意DR和DSR各有一字节的数据),读DR一次。再等到最后一次BTF时,软件拉低SCL,发送STOP,读DR,释放SCL,再读DR。

4、请注意在读取SR2到操作其他I2C寄存器期间使用软件产生时钟延展。 2016.02.03  thx:iguesser

[/important]

转载请注明出处 http://racede.me/talk_about_stm32_i2c_peripheral.html

 

干扰

当总线空闲时,无论是SCL的跳变(电平高低高),还是SDA的跳变,都会导致STM32的硬件I2C瘫痪,不能产生下一个S。当总线正在传输数据时,总线上的信号干扰对STM32的硬件I2C来说是致命的。

1、空闲时SDA跳变,会产生一个S和一个P,幸好这个P会产生一个中断,我们可以用一个收到P就软复位硬件I2C的策略。这样能避免空闲时SDA跳变带来的干扰。

2、空闲时SCL跳变,这是一个I2C的错误信号,但是STM32却会认为这是一个S,所以SCL跳变会导致BUSY置位,而且不会像SDA跳变那样会产生一个P中断。如果在单主的情况下,你可以为I2C的S做一个超时,超时了就软复位I2C就可以,当然最简单的方法还是空闲时关闭I2C(PE置零)。在多从机的情况下,只能等待别的主机发送的一个P,或者伺机软复位。

3、传输途中因干扰,产生总线错误(BERR)。单主接收途中出现BERR,可以在关闭硬件I2C后,连续模拟产生9个以上的SCL,在保证SDA为高电平的情况下软复位I2C。

4、传输途中因干扰,导致仲裁丢失(ARLO)。单主时和BERR的处理方法相同。

 

其他

还有什么值得注意的?

很多电工们反映,上电也是一个大问题,I2C一上电就马上BUSY了,第一次的S都不能发送,我是没有遇上这个问题。Google了一下很多都说是初始化顺序的问题。说说我的初始化吧,打开I2C外设的时钟、打开I2C引脚所在的GPIO的时钟、配置 GPIO_AF_OD、 I2C_DeInit、 I2C_Init、 I2C_Cmd,没有什么特别。还有一种可能就是,上电时上电的脉冲干扰了总线,导致某个从设锁死了总线(拉低了SDA)导致的BUSY置位,这个可以用处理BERR的方法,使总线恢复正常。(2012 Jun 6)

[important]

总线上的P产生后最好不要配置CR1的ACK位。STOP发送后配置ACK位——作为主机接收最后一字节时需要发送NACK,同时我们需要响应自己的从机地址,这时需要重新配置ACK为”1″——有可能导致下一次作为主机通信发送地址时,硬件不发送地址而直接发送P——这应该是一个硬件BUG,暂时还没有看见相关资料——具体表现为EV6死循环。推荐的做法是设置P前,软件下拉SCL,设置P,设置ACK,释放SCL,这样总线上的P将在释放SCL后产生。(2012 Jul 3)

[/important]

 

总结

这些都是我调STM32硬件I2C的一些心得。上文提及到的中断接收和发送方法,我用TIM自动更新,产生最高占先优先级的中断,并在中断里停留70us左右,且重装载值是一个素数的情况下,STM32F103VET6 400kHz的I2C跑了近一周没有发现数据错误。

至此,STM32 I2C的问题基本解决,欢迎广大电工们指正、反馈。

转载请注明出处 http://racede.me/talk_about_stm32_i2c_peripheral.html

浅谈 STM32 硬件I2C的使用 (中断方式 无DMA 无最高优先级)》上有34个想法

      • 我的应用中,两个32,一个是主,一个是从
        正常状态,主读取从的数据
        突发状态,主写数据给从
        每次通信的数据均〉2字节

        现在遇到的问题是:总线运行一段时间以后就会莫名的被拉低,事件中断中会遇到很多类库没有定义的事件代码

        如果是我的这种应用,请问32的收发各采用什么模式比较好?中断级别该如何设置?

  1. 楼主的文章很好很强大,看完后,我觉得I2C发送数据 不需要对每个事件处理一个中断,按照楼主所说需要设置BTF中断即可,(好比我们的串口发送中断,我们只管发送一个数据,然后等待串口发送空中断,就OK了)。如果对事件中断的各个细节都进行处理,感觉没有必要:开始发送起始,收到SB中断,发送地址,收到ADDR中断。。。 因为从器件可能不存在或无法回复,我们只需要用一个超时保护机制来恢复超时后的处理,至于发送起始,ADDR这些步骤,软延迟就可以了(有可能SB就没有发生我们也忽略,忽视过程中的反馈,当收到应答脉冲时,如果TxE被置位并且在上一次数据发送结束之前没有写
    数据字节到DR,则BTF被置位,接口等待BTF被清除。硬件I2C的应答机制由STM32完成了!我们清0超时保护,放入下一个数据,然后继续~~~~
    从UCOSII的角度,每次进中断都要保护现场,所以减少这种SB,ADDR进中断的时间,只等最后一个BTF的方式+超时保护100uf(假设时钟为200KHZ),会很有方便性。当中是否要处理BERR,我还没考虑好。接收也貌似可以用这个方式,只是主设备在从从设备接收到的最后一个字节后发送一个NACK,从设备接收到NACK后,释放对SCL和SDA线的控制。主设备就可以发送一个停止/重起始(Stop/Re-Start)条件。我们需要知道接收多少个字节。

  2. 之前理解错误,I2C的事件中断只要开启,SB,ADDR只要有事件发生就会进中断,不能单独关闭SB,ADDR中断,因此我们需要在发送完ADDR后,会接到应答或NOACK,如果应答了,读SR1然后读SR2将清除该事件,再开启事件中断,这个时候,由于移位寄存器为空,我们的BTF中断产生!
    就可以进I2C1_EV_IRQHandler() 像串口发送中断一样,写入DR寄存器将清除该事件。然后等待第1个数据的ACK或NOACK。
    应答错误(AF)
    当接口检测到一个无应答位时,产生此错误。此时:

  3. 很强,开始一直怀疑是器件问题,或者异常断电,看来先要解决i2c本身的问题,跳了很长时间 55555 能发个代码吗?

  4. 你好,我按照你的思路写了一个程序,目前单字节和大于2的多字节接收都正常了。只有2个字节接收不正常,表现是总线上出了2个字节(都有ACK),但ACK后SDA就一直为低,SCL高。
    在中断的接收部分程序是:
    //———————————j接收———————————————–
    case 11:
    if(I2C_GetFlagStatus(I2C2, I2C_FLAG_SB))//ev5
    {
    I2C_Send7bitAddress(I2C2, i2c_slave_id,2C_Direction_Receiver);
    i2c_step=12;
    }
    break;
    case 12:
    if(I2C_GetFlagStatus(I2C2, I2C_FLAG_ADDR))//ev6
    {
    if(!I2C_GetFlagStatus(I2C2, I2C_FLAG_TRA))
    {
    i2c_step=13;
    i2c_data_len–;
    if(i2c_data_len==0)
    {
    I2C_AcknowledgeConfig(I2C2, DISABLE);
    I2C_GenerateSTOP(I2C2, ENABLE);
    }
    }
    }
    break;
    case 13:
    if(I2C_GetFlagStatus(I2C2, I2C_FLAG_BTF))//ev7
    {
    if(i2c_data_len==2)
    {
    i2c_step=13;
    I2C_AcknowledgeConfig(I2C2, DISABLE);
    }
    if(i2c_data_len==1)
    {
    I2C_AcknowledgeConfig(I2C2, DISABLE);
    I2C_GenerateSTOP(I2C2, ENABLE);
    *i2c_recvdata= I2C_ReceiveData(I2C2);
    i2c_recvdata++;
    *i2c_recvdata= I2C_ReceiveData(I2C2);
    i2c_step=21;
    }
    if(i2c_data_len==0)
    {
    i2c_step=21;
    I2C_AcknowledgeConfig(I2C2, ENABLE);
    }
    else
    i2c_data_len–;
    *i2c_recvdata= I2C_ReceiveData(I2C2);
    i2c_recvdata++;
    }
    break;
    } //end of switch

  5. 感觉就是如果ADDR(EV6)后如果设置了nack,那么后面收到数据就一定会NACK。但要是等收到数据BTF(ev7)后再设置nack,那就要隔一个数据才会NACK。所以2个以上的数据要提前:i2c_data_len==2时就设置,==1时才设置STOP。

  6. 而且:
    if(i2c_data_len==1)
    {
    I2C_AcknowledgeConfig(I2C2, DISABLE);
    I2C_GenerateSTOP(I2C2, ENABLE);
    *i2c_recvdata= I2C_ReceiveData(I2C2);
    i2c_recvdata++;
    *i2c_recvdata= I2C_ReceiveData(I2C2);
    i2c_step=21;
    }
    这里连着读取就可以把最后一个数也取回来,压根不用等。
    我I2C的速度是300Khz,STM32跑满72Mhz

    • 关键在于要在接到器件地址ACK的中断时设置STM32的ACK(这个ACK是为第一个要接收的字节设置的)。设置ACK后拉低SCL并清空ADDR(这时STM32的硬件会马上开始第一个字节的接收,第一个字节的ACK已经设置,但碍于SCL拉低而出现时钟延展,硬件第一个字节的接收不能马上进行),紧接着设置NACK(这时设置的是第二个字节的ACK位),然后释放SCL,这两个字节的传输就会马上连续进行,线上收到的应该是第一个字节是ACK,第二个字节是NACK。至于POS的作用我已经记得不太清楚了,简言之就是要在时钟延展之前设置第一字节的ACK,时钟延展之后设置第二字节的NACK。

  7. Pingback引用通告: 读到一篇关于I2C的好帖子,分享之 - 合智社区

    • 在DR被读空时,如DSR不为空,数据会被硬件转移到DR,以准备被程序读出。当DSR的数据发送完毕,如DR不为空,则会自动将DR中的数据装载到DSR,以准备下一个字节的发送。

  8. 前辈,您好。一字节和两字节的接收我已经实现了。目前卡在多字节接收上,程序是按照您描述来写的。
    倒数第二个BTF先设置NACK,然后读DR。
    在最后一个BTF中,先把引脚设置OD模式,然后STOP,读一次DR,释放SCL,再读一次DR。
    目前是最后一个DR出现问题。问题表现在DR的数据自动向左移位,并且BERR置位。
    假设我的待接收的数据为4个字节
    0x11 0x22 0x33 0x44

    实际上我接收到的是
    0x11 0x22 0x33 0x89

    在第四个字节中数据出现移位,并且低位置1。我感觉最后一个位是NACK造成的。麻烦您在百忙之中为我解答,谢谢。

    case RECEIVING:
    {
    if (I2C_GetFlagStatus(I2C1, I2C_FLAG_BTF))
    {
    if (I2cReadByteCnt == 0) // 第一次BTF 这里只接收4 个字节所以产生3次BTF
    {
    *I2cReadBuf = I2C_ReceiveData(I2C1);
    I2cReadBuf++;
    I2cReadByteCnt++;
    }
    else if (I2cReadByteCnt == 1) // 倒数第二个BTF
    {

    I2C_AcknowledgeConfig(I2C1, DISABLE);
    *I2cReadBuf = I2C_ReceiveData(I2C1);
    I2cReadBuf++;
    I2cReadByteCnt++;

    }
    else if (I2cReadByteCnt == 2) // 最后一个BTF
    {
    GPIOB->CRL &= (uint32_t)0x00FFFFFF; // 将PB6、PB7的引脚复用模式清除,变成通用OD模式
    GPIOB->CRL |= (uint32_t)0x77000000; // AF_OD -> Out_OD
    GPIOB->BRR = GPIO_Pin_6; // 并且拉低SCL

    I2C_GenerateSTOP(I2C1, ENABLE);
    *I2cReadBuf = I2C_ReceiveData(I2C1);
    I2cReadBuf++;

    GPIOB->CRL &= (uint32_t)0x00FFFFFF; // 将PB6,PB7引脚模式变回复用模式
    GPIOB->CRL |= (uint32_t)0xFF000000; // 恢复I2C引脚复用模式

    *I2cReadBuf = I2C_ReceiveData(I2C1);

    I2cStatusStep = RECEIVED;

    }
    }
    }
    break;

      • 前辈,BTF被置位的时候SCL已经被自动拉低。
        然而我这时候从AF_OD模式切换到Out_OD模式,
        再拉低,这时候就会发现,SCL会有一个很短暂的
        高脉冲,时间很短。处理完关键部分后再恢复AF_OD
        模式SCL又被拉低了一次,这就造成最后一个字节后面
        跟着一个高脉冲,导致BERR,总线关闭错误,并且导致
        DSR多进行了一次唯一。所以才会造成0x44先位移一位
        变成0x88,然后再有一个高脉冲0x89

        我阅读了数据手册和网上的参考资料,也没发现引脚从
        复用模式变成通用模式会主动拉高引脚。
        我用的是寄存器写的。

        // 将PB6、PB7的引脚复用模式清除,变成通用OD模式
        GPIOB->CRL &= (uint32_t)0x00FFFFFF;
        GPIOB->CRL |= (uint32_t)0×77000000; // AF_OD -> Out_OD
        // 并且拉低SCL
        GPIOB->BRR = GPIO_Pin_6;

        调了一周的STM32的I2C,总结出了一些经验,那就是
        必须要等到事件完成后(包括数据接收,STOP,NACK)
        再接收下一个字节。解这个锁的两个很关键钥匙钥匙是

        1.强制拉低SCL 2.BTF

        1.在某些事件中需要一直强制拉低SCL,一直等到事件结束
        再继续接收下一个字节。只要响应中断,就把SCL强制拉低
        直到事件结束才释放。

        2.为何要BTF,因为在接收到数据后你需要硬件自动拉低
        SCL,直到用户读取DR再开始下一个字节的接收。
        如果采用读RxNE,高中断抢占时间很长,而I2C又字节接收数据,
        造成数据一直被覆盖,所以会造成必须要及时读出,否则出错
        这也是您为何只用BTF的原因。

        目前第二把钥匙我已经搞定了,在关键部分加上锁,没有采用
        强制拉低SCL,这个有一个很致命的缺点,那就是读DR后,
        I2C又自动传输,如果高中断长时间不响应,那么还是会造成
        数据丢失。

        第一把钥匙使用后会产生一个高脉冲,使BERR置位

        所以请前辈解答如何切换引脚模式才是正确的。谢谢

  9. 由于STM32硬件I2C设计的时候有BUG,或者是其他原因。结果造成某些事件只要清这个事件或者STOP后就自动开始下一个字节的接收。而我们需要的是直到软件结束才开始进行下一个数据的接收。

    • 你的方向是正确的,BUG大多都出在DSR和DR的交换中,如何使每个事件出现在我们可以预料且可接受的位置这是使用的关键。关于强制拉低SCL时产生的毛刺我调试中没有遇到,以下是我使用的方法,请参考。FYI and thanks your shares.

      /*拉低SCL 软件产生时钟延展*/
      void I2CPool_PullDownSCL(I2CPool_TypeDef * pI2CPool)
      {
      pI2CPool->pI2CGPIOx->BRR = pI2CPool->I2CGPIOx_SCL;/*设置SCL对应的引脚为低电平*/
      *pI2CPool->pI2CGPIOxCR &= ~pI2CPool->I2CGPIOxCR_Mask;/*设置SCL对应引脚为开漏输出模式*/
      }

      /*释放SCL*/
      void I2CPool_ReleaseSCL(I2CPool_TypeDef * pI2CPool)
      {
      *pI2CPool->pI2CGPIOxCR |= pI2CPool->I2CGPIOxCR_Mask;/*设置SCL对应引脚为复用开漏输出模式*/
      }

  10. 昨天花了一整天时间调试硬件I2C,才定位到I2C硬件时序的问题。

    用STM32F429的I2C驱动温湿度芯片HTU21D,出问题的点是等待HTU测量完成后读取数据的点。
    需要读取3个字节的数据。在执行完
    I2C_GenerateSTART(HTU21D_I2C, ENABLE);
    I2C_Send7bitAddress(HTU21D_I2C, HTU21D_ADDR, I2C_Direction_Receiver);
    之后,示波器上看到HTU21D立即返回了2个字节,两个字节都有STM32的ACK,但是在执行
    I2C_ReceiveData(HTU21D_I2C)
    去读取DR上的数据的时候,HTU21D又发过来第三个字节。

    在F429的手册上有这样一句话:
    SCL stretched low until the ADDR flag is cleared
    我原来的理解是芯片会自己保持SCL为低,但是信号看起来不是这样的。
    应该就是博主说的需要使用软件延展的问题了,但是需要在中断中立即响应,否则HTU21D就一口气把数据发完了,STM32的主流程还跟不上

  11. 你好,我的I2C的问题是这样的,我的设备是从机模式,之前用的STM8s003的I2C与无人机通信的,正常。现在更新板子用STM32f030k6代替之前的单片机,程序是直接移植过来的,现在调试时发现单片机接收到的数据不对,比如正确的为0x11,0x23,0x34….现在的数据为正确的数据左移一位且数据中间多一个0XFF,不知道什么情况。

  12. 分析的很棒!看了教程,依旧花了很长的时间写代码。把代码分享出来,供大家参考:https://github.com/meng4036/stm32_i2c

小申进行回复 取消回复

电子邮件地址不会被公开。 必填项已用*标注