再議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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章