我的工程里要用到iic總線擴展rom,stm32是有硬件iic的,但是,網上有很多人說這個硬件iic有漏洞,甚至於有bug。http://bbs.21ic.com/icview-184741-1-1.html http://blog.gkong.com/more.asp?name=zjcsharp&id=112878。《例說stm32》的表述是:“非常復雜,不太好用”。那么我判斷這個硬件iic可能確實有不足,因此選擇直接用軟件模擬出iic。
在做的過程中,遇到幾個問題,記錄下來。
1、引腳的模式與配置
iic的兩個引腳SDA與SCL都要求既能輸出又能輸入。這對stm32來說問題不大,由參考手冊給出的圖來看,引腳是始終連着IDR寄存器的,另外“輸出配置”一節還特意講到,“在開漏模式時,對輸入數據寄存器的讀訪問可得到I/O狀態”。所以,模式的問題很好解決。
SDA線是由不同的器件分時控制的,這就造成一個問題:當一個器件主動置高或者置低時,若另一個器件若發出相反的電平,會短路。
這就決定將引腳配置成推挽,有很多麻煩事。alientek就是這么做的,他在主機(單片機)控制SDA線時,將其SDA引腳配置成推挽輸出;從機(EEPROM)控制SDA線時,將單片機的引腳配置成上拉/下拉輸入,用頻繁的配置切換來避免這個問題。
我覺得這么做太麻煩,stm32有一個開漏的配置,它與推挽有點像,但也不完全一樣,手冊里這么說:
開漏模式:輸出寄存器上的’0’激活N-MOS,而輸出寄存器上的’1’將端口置於高阻狀態(P-MOS從不被激活)。
推挽模式:輸出寄存器上的’0’激活N-MOS,而輸出寄存器上的’1’將激活P-MOS。

這樣一來,問題就很好解決了:當單片機的SDA引腳置低時,SDA線被拉低,當單片機的SDA引腳置高時,實際上引腳是浮空的,SDA線通過上拉電阻被VCC拉高(iic的兩條線都要通過上拉電阻接到VCC,典型接法),這樣就不會出現短路的狀況。很巧妙。
2、邏輯與時序
邏輯與時序的問題iic相關手冊,尤其是EEPROM的手冊上都有詳細的介紹,這里主要從編程的角度來討論這個問題。
先附上代碼:
void IicStart()
{
SDA_H;
SCL_H;
delay_us(5);
SDA_L;
delay_us(5);
SCL_L; //
delay_us(2); //
SDA_H;
delay_us(2);
}
void IicStop()
{
SDA_L;
SCL_L;
delay_us(2);
SCL_H;
delay_us(2);
SDA_H; //
delay_us(2);
SCL_L;
delay_us(2); //
}
void IicAck() //
{
SCL_L;
SDA_L;
delay_us(5);
SCL_H;
delay_us(5); //
SCL_L;
delay_us(2);
SDA_H;
delay_us(2);
}
void IicNack() //
{
SCL_L;
SDA_H;
delay_us(5);
SCL_H;
delay_us(5); //
SCL_L;
delay_us(2);
SDA_H;
delay_us(2);
}
void IicWaiteAck()
{
SCL_L;
SDA_H;
delay_us(5);
SCL_H;
delay_us(5);
SCL_L;
delay_us(2);
}
void IicSendByte(u8 temp)
{
u8 i;
for(i=0;i<8;i++)
{
SCL_L;
delay_us(5);
if(temp&0x80) //MSB在前
SDA_H;
else
SDA_L;
SCL_H;
delay_us(5);
SCL_L;
temp<<=1;
}
delay_us(2);
SDA_H;
delay_us(2);
}
u8 IicReceiveByte()
{
u8 i,temp=0;
delay_us(2);
SDA_H; //
delay_us(2);
for(i=0;i<8;i++)
{
temp<<=1;
SCL_L;
delay_us(5);
SCL_H;
delay_us(2);
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7))
temp=temp|0x01;
else
temp=temp&0xFE;
}
SCL_L;
delay_us(2);
return temp;
}
發送8個字節:
IicStart();
IicSendByte(0xA0); //尋址,發送寫命令
IicWaiteAck();
IicSendByte(0x00); //字節地址,24LC02里首先寫入的是指針,指向0x00字節單元
IicWaiteAck();
for(i=0;i<8;i++)
{
IicSendByte(0xa0+i); //寫完了指針就是按指針的指向寫數據
IicWaiteAck();
}
IicStop();
delay_ms(4);
接收字節:
IicStart();
IicSendByte(0xA0); //尋址,發送寫命令(先要寫指針)
IicWaiteAck();
IicSendByte(0x00); //寫指針
IicWaiteAck();
IicStart(); //再次發出起始條件
IicSendByte(0xA1); //這次是尋址和讀命令
for(i=0;i<2;i++) //24LC會根據寫好的指針,將相應數據發送在SDA上
{
IicAck();
*((u8 *)(&lxj)+i)=IicReceiveByte();
}
IicNack();
IicStop();
在發送START,ACK,NACK,STOP,發送一個字節時,以及接收ACK,接收字節時,每個函數返回,都保證SCL為低,即一個信號位結束;並且SDA為高(浮空),以保證SDA線可以被其他器件控制。
另外還要注意電平高低之間要有適當的延時。我這里微秒級的延時是為了產生有效的iic信號,這些延時是我隨意加進去的,可以用,但不知道能不能減少一點;毫秒級的延時是為了等待24LC02將數據從緩沖區寫入ROM,這個時間與寫入的字節數無關,至少要3ms,否則出錯。
24LC02每次最多寫入8個字節,讀出沒有限制。
3、EEPROM的頁
24LC02有頁的概念,在寫時,先寫入緩沖區,緩沖區大小是一頁(8字節,64位),在頁寫時,先寫入緩沖區,待總線上出現STOP信號時,將緩沖區寫到相應的ROM中。這就要求:不能跨頁寫。以下是網上摘錄的一點,我覺得自己對這個東西的理解還不全面,網上的資料不多。
“AT24CXX系列的EEPROM為了提高寫效率,提供了頁寫功能,內部有個一頁大小的寫緩沖RAM,地址范圍當然就是從00到一頁大小,發生寫操作時,開始送入的地址對應的頁被選中,並將其內容映像到緩沖RAM,數據從低端地址對應的緩沖RAM地址開始修改,超過這個地址范圍就回到00,寫完后,就會把開始確定的EEPROM頁擦除,再把一整頁RAM數據寫入。所有寫數據都發生在開始寫地址時確定的頁上。
如頁容量為128,一頁都是從00開始按128字節分成一個個的頁,0頁就是0~7F,1頁就是80~FF,類推,邊界就是128字節的整數倍地址。頁RAM的地址范圍為7位00~7F,寫入時高端地址就是頁號。發生寫操作,開始送入的地址對應的頁被鎖存,后續不論寫多少,都在這個頁中,只是一個頁內的地址進行加一,超過就歸零開始。從F0開始寫32個字節,那么開始送入的地址為F0,就會鎖定在1號頁(第2個頁)上,底端7位頁內部地址開始從70H開始寫,到達7F時回到00再到10H,也就是寫在了F0~FF,80~8F。也就是,從01開始寫也只能到7F,再往80寫就跑到00上去了,這就是寫操作的翻卷,datasheet上都有說明。就是從邊界前寫兩個字節也要分兩次寫。頁是絕對的,按整頁大小排列,不是從開始寫入的地址開始算。
讀沒有頁的問題,可以從任意地址開始讀取任意大小數據,只是超過整個存儲器容量時地址才回卷。但一次性訪問的數據長度也不要太大。
所以分頁的存儲器要做好存儲器管理,盡量同時讀寫的數據放在一個頁上。
”
4、其他
在我這個項目里,iic是不常用的,當電網發生故障時,才做記錄。所以,引腳無需長期配置在開漏輸出,根據st的官方推薦,在不使用iic時,將引腳配置成浮空輸入。
