概述
在本階段的工作中,需要實現一個由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)抓取的主機發送的時鐘和數據信號關係。可以在圖中看出:- 時鐘佔空比並不是50%
- 數據變化並不是在時鐘的高電平或者低電平的中間部位發生變化,而是在時鐘剛發生跳變之後就發生了變化。(圖中測試發現時鐘變化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;
由此修改後,可以保證採樣值正確,接收數據也正常,不會發生數據錯誤。