再议IIC协议与设计【2】--使用GPIO实现IIC从机通讯源码分析与测试

概述

在本阶段的工作中,需要实现一个由GPIO模拟的I2C从机工程设计,以前只使用GPIO模拟I2C设计过主机,对于从机的设计,还是首次。下面就对本次工作中从机设计思想做详细记录。


开发平台

THK88


从机通信设计框图


图 1

程序设计与分析

硬件平台的寄存器配置

#define WAIT_IIC_SCL_HIGH   while ( !GET_SCL_DAT )  
#define WAIT_IIC_SCL_LOW    while ( GET_SCL_DAT )  
#define WAIT_IIC_SDA_HIGH   while ( !GET_SDA_DAT )  
#define WAIT_IIC_SDA_LOW    while ( GET_SDA_DAT )  
  
  
#define IIC_WAIT_START      WAIT_IIC_SCL_HIGH;  WAIT_IIC_SDA_LOW      
#define IIC_WAIT_STOP       WAIT_IIC_SCL_LOW; SDA_IN; WAIT_IIC_SCL_HIGH; WAIT_IIC_SDA_HIGH   
  
  
#define IIC_SLAVE_SEND_LOW  WAIT_IIC_SCL_LOW; SDA_OUT; SET_SDA_LOW; WAIT_IIC_SCL_HIGH        
#define IIC_SLAVE_SEND_HIGH WAIT_IIC_SCL_LOW; SDA_OUT; SET_SDA_HIGH; WAIT_IIC_SCL_HIGH  
  
  
#define IIC_SLAVE_SEND_ACK  IIC_SLAVE_SEND_LOW  
#define IIC_SLAVE_SEND_NAK  IIC_SLAVE_SEND_HIGH  

初始化

void iic_init(void)  // 完成GPIO作为I2C的初始化 
完成GPIO时钟寄存机配置等功能。


接收地址以及读写命令模块

for(bitcount = 0; bitcount < 7; bitcount ++)  
{  
    WAIT_IIC_SCL_LOW;                     
    WAIT_IIC_SCL_HIGH;  
    iic_slv_addr <<= 1;  //先移位,再读数  
    if(GET_SDA_DAT)  
        iic_slv_addr |= 0x01;  
    else  
        iic_slv_addr |= 0x00;  
}  
iic_slv_addr <<= 1;  
// 读取7位地址  
  
  
WAIT_IIC_SCL_LOW;  
WAIT_IIC_SCL_HIGH;  
if(GET_SDA_DAT)  
    iic_master_rw = 1;  
else  
    iic_master_rw = 0;  
// 读写标志位  
  
  
if (iic_slv_addr == SLAVE_ADDR)  
    IIC_SLAVE_SEND_ACK;    // 地址正确,从机发送ACK信号  
      
    从机接收接口定义以及说明  
    uint8_t L_i2c_rx( uint8_t xdata *pDestBuf, uint16_t *wRecLen);  
      
    pDestBuf:接收数据保存的目的地址 wRecLen:实际接收到的数据的长度

接收数据核心代码分析

while(!recFinish)  
{  
    for(bitcount=0; bitcount<8; bitcount++)  
    {  
        while(GET_SCL_DAT);  
        SDA_IN;  
        while(!GET_SCL_DAT);  
  
  
    r0 = GET_SDA_DAT;  
    while(GET_SCL_DAT)  
    {  
        r1 = GET_SDA_DAT;  
        if((r0 == 0) && (r1 == 1))  
        {  
            recFinish = 1;  
            return 0;  
        }  
    }  
  
  
    rxbyte <<= 1;  
    if(r1)  
        rxbyte |= 0x01;  
    else  
        rxbyte |= 0x00;   
}  
buf[len] = rxbyte;  
len++;  
IIC_SLAVE_SEND_ACK;  

在从机接收完地址以后,如果读写标志位是写(L),接下来,从机就会接收数据,接收数据完成的标志是从机接收到了STOP信号即从机发完ACK后的第一个SCL高电平时检测是否有SDA从H跳转到L如果发生了,接收程序结束,如果没有发生跳变,继续接收数据的bit7,bit6,……bit0.


图 2

根据上图2,在时钟节拍①中从机发送ACK信号以后,因为GPIO的采样频率远大于I2C的时钟频率(设计中使用100K),所以需要在发送ACK后,要等待SCL变为L,在上面的代码中体现在c点;在时钟节拍②中,SDA可能会发生变化。最重要的是需要在时钟节拍③内采样SDA,判断是否有变化(即从f到g中连续采样SDA),如果发生了SDA从H到L,那么就认为接收到了STOP信号,跳出接收数据的函数;如果发生SDA从L到H,会被认为是一个异常的信号(START信号,但不应该在此时出现,上段代码中没有处理此异常情况,请注意),同样也需要跳出接收数据的函数;如果SDA没有发生任何变化,同时等待到SCL发生由H到L的变化,则意味着接收到了下一字节的bit7……


那么程序设计最核心的问题就变为怎样判断在时钟节拍③中SDA发生了变化,并且发生了怎样的变化。设计的思想为:在f点(上升沿)后取第一个SDA的采样值r0,遍历时钟节拍③直到g点(下降沿),连续采样SDA的下一个采样值r1,当在时钟节拍③内,只要发生了r1 != r0的时候,马上跳出接收数据程序。但如果r1 === r0,在检测到h点的时候,才把r1赋值给接收字节rxbyte.


以上采样能够成功的一个前提是:SDA在SCL每一时钟节拍的变化能够被采样到。在此假设GPIO的时钟为2MHz,SCL的传输速度为100KHz,时序关系如下图所示 : 


图 3


上图是比较理想的SCL的时钟周期信号,在每半个SCL的时钟周期中,有10个采样点,这样确保了SCL上升沿后的第一个GPIO采样到了r0=0,也能在后5个采样点中采到了r1=1,在这种情况下,不会发生任何错误。但是,实际情况并非如此,在GPIO模拟I2C的SCL信号中,占空比并不一定是50%如果此时SDA的变化在GPIO第一个采样沿之前就发生了变化,那么就无法采样到正确的电平变化信号,发生通讯错误。

所以,以上设计方法能够成功的前提为:SDA并不会在紧靠着SCL的上升沿或者下降沿而变化。也就是说SDA的任何变化,都能被GPIO的时钟采样到。

从机发送数据接口以及定义

uint8_t L_i2c_tx(const uint8_t xdata * pSendBuf, uint16_t wSendLen);

pSendBuf:发送数据缓存地址 wSendLen:发送数据的长度


发送数据程序及分析

for(bytecount = 0; bytecount < len; bytecount ++)  
{          
    txmask = 0x80;  
    txbyte = buf[bytecount];  
    for(bitcount = 0; bitcount < 8; bitcount ++)  
    {  
        WAIT_IIC_SCL_LOW;  
        SDA_OUT;  
        if ( txbyte & txmask )  
            SET_SDA_HIGH;     
        else      
            SET_SDA_LOW;    
        WAIT_IIC_SCL_HIGH;  
        txmask = txmask >> 1;         
    }  
  
  
    WAIT_IIC_SCL_LOW;                                                 
    SDA_IN;  
    WAIT_IIC_SCL_HIGH;  
    if ( GET_SDA_DAT )    
        break;  
}  
return (bytecount);  

在发送数据的时候需要注意:数据要在SCL的低电平时更新(SDA跳变),在SCL为高电平的时候保持不变。一个Byte发送完成后,需要等待主机发送的ACK信号,从机接收到ACK信号后根据需要发送的字节数,判断是否继续发送,还是要等待主机发送的STOP命令。

验证测试

在以上的分析中,实现了GPIO模拟I2C从机的初步设计,但在实际的测试过程中,发现了数据传输错误的问题,为了解决这个问题,在下节中,会根据实际测试情况对上面的代码做一些修改。

问题描述 

主从机传输主要发生在从机接收数据部分,下图4是使用示波器(500MHz 2.5GS/s)抓取的主机发送的时钟和数据信号关系。可以在图中看出: 
  1. 时钟占空比并不是50% 
  2. 数据变化并不是在时钟的高电平或者低电平的中间部位发生变化,而是在时钟刚发生跳变之后就发生了变化。(图中测试发现时钟变化150ns之后数据信号就发生了变化) 


图 4


根据上篇文章的程序(见下)

while(!recFinish)
{
    for(bitcount = 0; bitcount < 8; bitcount ++)
    {
        while(GET_SCL_DAT);
        SDA_IN;
        while(!GET_SCL_DAT);

        r0 = GET_SDA_DAT;
        while(GET_SCL_DAT)
        {
            r1 = GET_SDA_DAT;
            if((r0 == 0) && (r1 == 1))
            {
                recFinish = 1;
                return 1;
            }
        }   
        rxbyte <<= 1;
        if(r1)
            rxbyte |= 0x01;
        else
            rxbyte |= 0x00; 
    }
    buf[(*len)++] = rxbyte;
    IIC_SLAVE_SEND_ACK;
}
return 0;

在r1位置的极限情况下,r1会采样到SCL刚刚变化为低的值,这样就会发生错误的采样,在实际测试中发生了下列错误的采样(主机将数据写入从机,并从从机中读取以验证)。可以看到一些规律:每一个Byte只会发生一次错误,所有的错误都是高位没有采样到。原因就发生才r1采样的时候采样错误。为了改正这种错误,就需要重新确定采样位置,以保证采样到正确的值。



图 5


对于下面的代码片段:

 if(r1)
            rxbyte |= 0x01;
        else
            rxbyte |= 0x00;

有两种修改方法:一种是在取采样值的时候,取r0,这样避免在取r1的时候出现数据跳变,即上面的代码修改为:

if(r0)
            rxbyte |= 0x01;
        else
            rxbyte |= 0x00;

但这种修改方法还是有不完善的地方,因为r0也有可能发生跳变,也有可能是一个不稳定的采样点,那么最理想的状态是采样图4中r2的值。r1可能在SCL发生跳变的瞬间也发生数据的跳变,但是r2一定是在SCL为高的时候的采样点,是不会发生数据的跳变的。

第二种代码可修改如下:

while(!recFinish)
{
    for(bitcount = 0; bitcount < 8; bitcount ++)
    {
        while(GET_SCL_DAT);
        SDA_IN;
        while(!GET_SCL_DAT);

        r0 = GET_SDA_DAT;
        r1 = r0;
        while(GET_SCL_DAT)
        {
            r2 = r1;
            r1 = GET_SDA_DAT;
            if((r0 == 0) && (r2 == 1))
            {
                recFinish = 1;
                return 1;
            }
        }

        rxbyte <<= 1;
        if(r2)
            rxbyte |= 0x01;
        else
            rxbyte |= 0x00; 
    }
    buf[(*len)++] = rxbyte;
    IIC_SLAVE_SEND_ACK;
}
return 0;

由此修改后,可以保证采样值正确,接收数据也正常,不会发生数据错误。

发布了26 篇原创文章 · 获赞 106 · 访问量 8万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章