看時序圖寫I2C驅動,教你如何自己手擼非標I2C驅動函數
很多人不知道怎么看著時序圖寫程序,下面結合一個非標準的I2C器件,教大家如何寫一個高效的IO模擬I2C時序。

觀察該時序,具備I2C的開始信號,I2C的結束信號,I2C的應答、非應答、響應應答,以及寫字節和讀字節的基本操作時序。
下面,我們一步一步分析。
1、I2C開始信號
觀察時序圖,在SCLK高電平的狀態下,在SDIO產生一個下降沿是為開始信號。
void I2C_Start()
{ //設置I2C使用的兩個引腳為輸出模式
pinMode(SCLK_PIN, OUTPUT); pinMode(SDIO_PIN, OUTPUT); //在SCL為高電平的時候讓SDA產生一個下降沿是為開始信號
digitalWrite(SDIO_PIN, 1); digitalWrite(SCLK_PIN, 1); digitalWrite(SDIO_PIN, 0);
}上述代碼即先將兩個引腳設置為輸出模式,然后在SCLK為高電平的時候在SDIO引腳輸出一個下降沿。
2、I2C停止信號
觀察時序圖,在SCLK為高電平的時候在SDIO引腳產生一個上升沿是為停止信號。
void I2C_Stop()
{ pinMode(SDIO_PIN, OUTPUT); //在SCL為高電平的時候讓SDA產生一個上升沿是為停止信號
digitalWrite(SDIO_PIN, 0); digitalWrite(SCLK_PIN, 1); digitalWrite(SDIO_PIN, 1);
}這里采用的是Arduino編寫的IO基本操作,你可以替換成任意單片機的IO操作。
由于整個過程SCLK引腳一直是輸出狀態,所以僅在開始信號中對SCLK初始化為輸出模式,而過程中可能會修改SDIO的輸入輸出模式,所以其他的函數開頭都要根據情況對SDIO引腳的模式進行設置。
通過三行代碼實現在SCLK為高電平的時候在SDIO產生一個上升沿,實現停止信號。
3、寫字節操作
接下來,按照時序的順序編寫方便認讀
I2C的讀寫字節是這么定義的:當時鐘線為低電平的時候,允許修改數據線的電平狀態,在時鐘線為高電平的時候讀取數據線的狀態。
因為是寫操作,因此我們要先將時鐘線SCLK拉低,再修改SDIO的值,然后拉高時鐘。拉高后,從機就會從總線上讀取SDIO的狀態,接著一位一位的這么發送。
void I2C_Write(uint8_t dat){
pinMode(SDIO_PIN, OUTPUT); //拉低時鐘線后可修改數據線的狀態
digitalWrite(SCLK_PIN, 0);
for(int i=0;i<8;i++)
{
digitalWrite(SDIO_PIN, (bool)(dat&0x80));
digitalWrite(SCLK_PIN, 1);//在高電平時候送出數據
dat=dat<<1;
digitalWrite(SCLK_PIN, 0);//拉低準備下一個位的數據發送
}
}上述代碼正描述了這一情況:為了保證最后是低電平,這里將SCLK的第一次拉低放到循環外面,這樣可以用最少的執行次數完成一個字節的寫任務;同時,結束完一個字節寫入后時鐘線是低電平狀態(時序圖中寫入的第一個字節為DeviceID,第二個字節為寄存器地址+讀寫位)。
寫完一個字節后,從機會對寫入事件進行應答,這個時候主機可以從總線上讀取應答信號。
4、讀取從機應答引號
應答信號在寫完一個字節的低電平后由從機送出,在時鐘為高電平的時候可以讀取出來,我們注意到寫字節操作后時鐘線已經是低電平了,因此這個時候
只要拉高時鐘線,接下來就可以讀取應答信號,讀取應答信號根據時序圖應該拉低時鐘準備下一個字節的寫入。
bool I2C_RACK(){ bool ack;
pinMode(SDIO_PIN, INPUT);
digitalWrite(SCLK_PIN, 1);//接收應答信號,當時鐘拉高時候,從機送出應答信號
ack = digitalRead(SDIO_PIN);
digitalWrite(SCLK_PIN, 0);//讀取完應答信號后拉低時鐘。
return ack;
}如上代碼所示,即為接收從機應答,拉高時鐘,讀取應答,再拉低,返回應答。如果從機應答了,這里會讀取到一個低電平。
后面就是再寫入一個寄存器+讀寫位的地址,參靠上面的寫入操作。
寫入寄存器地址后,緊跟著又一個接收從機應答信號,然后從機就會送出數據,送出的數據分高字節和低字節,高低字節間要有一個主機發送給從機的應答信號,這樣從機就知道主機收到了數據,就會送出后面的低字節數據。
5、讀字節操作
注意,前面說過,讀寫都是總線在時鐘低電平時候修改數據線,在高電平送出。
因此,主機讀取從機送來的數據仍然是在高電平時候讀取。
uint8_t I2C_Read()
{ uint8_t dat=0;
pinMode(SDIO_PIN, INPUT); for(int i=0;i<8;i++)
{
digitalWrite(SCLK_PIN, 1);//讀取數據時候是在時鐘的高電平狀態讀取
dat=dat<<1; if(digitalRead(SDIO_PIN))
{
dat=dat|1;
}
digitalWrite(SCLK_PIN, 0);//拉低時鐘線準備下一個位的讀取
} return dat;
}操作過程是將SDIO數據線的IO設置為輸入模式,準備讀取,然后拉高時鐘,讀取數據,移位,拉低循環讀取8位數據。
注意,操作完一個字節讀取任務后,時鐘線還是低電平。
讀取完一個字節后,主機要給從機發送一個應答信號,這樣從機會接著發低字節數據。
6、主機發送應答信號給從機
void I2C_ACK()
{pinMode(SDIO_PIN, OUTPUT);digitalWrite(SDIO_PIN, 0);//給從機發送應答信號,即拉低數據線,然后拉高時鐘讓從機讀取該應答digitalWrite(SCLK_PIN, 1);digitalWrite(SCLK_PIN, 0);//執行完應答后拉低時鐘線,準備下一步動作。}拉低數據線,然后在高電平的時候讓從機去讀取,之后拉低時鐘線準備下一步接收動作。
當再接收一個字節后,就讀取完成了,這個時候就是產生一個非應答信號,然后發給總線結束信號,告訴從機一個讀寫周期結束了。
7、主機非應答信號
什么是非應答信號呢?
就是接收完了數據,釋放數據線,不去拉低數據線。
void I2C_NACK()
{ //非應答信號:即主機不再對從機進行應答,主機釋放數據線,即拉高數據線,然后給時鐘一個周期信號(拉高再拉低)
pinMode(SDIO_PIN, OUTPUT); digitalWrite(SDIO_PIN, 1); digitalWrite(SCLK_PIN, 1); digitalWrite(SCLK_PIN, 0);
}將SDIO引腳設置為輸出,拉高數據線,即為釋放數據線,然后拉高拉低時鐘,即在時鐘線產生一個時鐘周期信號。
然后發送結束信號。結束信號在開頭已經講明,即在時鐘線為高電平的狀態下,在數據線產生一個上升沿。
觀察以上代碼沒一個多余重復的操作動作,即完美的視線了時序圖上的所有操作。
接下來就是利用上述的I2C成分進行對寄存器的讀寫操作了。
8、讀寄存器
由于圖中設備的DeviceID 為0x80,即直接寫進來,從機判斷是讀還是寫的字節在寄存器地址。
因此,將寄存器的地址左移一位,在末尾補上是讀(1)還是寫(0)。
uint16_t read_reg(uint8_t reg){
uint16_t dat=0;
reg=(reg<<1)|1;
I2C_Start();
I2C_Write(0x80);
I2C_RACK();
I2C_Write(reg);
I2C_RACK();
dat=I2C_Read();
dat=dat<<8;
I2C_ACK();
dat=dat|I2C_Read();
I2C_NACK();
I2C_Stop();
return dat;}9、寫寄存器操作
void write_reg(uint8_t reg, uint16_t dat){
reg=(reg<<1);
I2C_Start();
I2C_Write(0x80);
I2C_RACK();
I2C_Write(reg);
I2C_RACK();
I2C_Write(dat>>8);
I2C_RACK();
I2C_Write(dat&0xFF);
I2C_NACK();
I2C_Stop();
}最后,對寄存器讀寫函數測試。
void setup()
{ Serial.begin(115200); Serial.println("Hello I2C"); write_reg(0x02,0x2250); Serial.println(read_reg(0x02),HEX); write_reg(0x02,0x2281); Serial.println(read_reg(0x02),HEX);
}void loop()
{
}
讀取的數值與寫入的是一樣的。
最后曬出完整的測試代碼:
#define SCLK_PIN 8#define SDIO_PIN 9void I2C_Start(){ //設置I2C使用的兩個引腳為輸出模式
pinMode(SCLK_PIN, OUTPUT);
pinMode(SDIO_PIN, OUTPUT); //在SCL為高電平的時候讓SDA產生一個下降沿是為開始信號
digitalWrite(SDIO_PIN, 1);
digitalWrite(SCLK_PIN, 1);
digitalWrite(SDIO_PIN, 0);
}void I2C_Stop(){
pinMode(SDIO_PIN, OUTPUT); //在SCL為高電平的時候讓SDA產生一個上升沿是為停止信號
digitalWrite(SDIO_PIN, 0);
digitalWrite(SCLK_PIN, 1);
digitalWrite(SDIO_PIN, 1);
}void I2C_Write(uint8_t dat){
pinMode(SDIO_PIN, OUTPUT); //拉低時鐘線后可修改數據線的狀態
digitalWrite(SCLK_PIN, 0);
for(int i=0;i<8;i++)
{
digitalWrite(SDIO_PIN, (bool)(dat&0x80));
digitalWrite(SCLK_PIN, 1);//在高電平時候送出數據
dat=dat<<1;
digitalWrite(SCLK_PIN, 0);//拉低準備下一個位的數據發送
}
}uint8_t I2C_Read()
{ uint8_t dat=0;
pinMode(SDIO_PIN, INPUT); for(int i=0;i<8;i++)
{
digitalWrite(SCLK_PIN, 1);//讀取數據時候是在時鐘的高電平狀態讀取
dat=dat<<1; if(digitalRead(SDIO_PIN))
{
dat=dat|1;
}
digitalWrite(SCLK_PIN, 0);//拉低時鐘線準備下一個位的讀取
} return dat;
}bool I2C_RACK(){ bool ack;
pinMode(SDIO_PIN, INPUT);
digitalWrite(SCLK_PIN, 1);//接收應答信號,當時鐘拉高時候,從機送出應答信號
ack = digitalRead(SDIO_PIN);
digitalWrite(SCLK_PIN, 0);//讀取完應答信號后拉低時鐘。
return ack;
}void I2C_ACK(){
pinMode(SDIO_PIN, OUTPUT);
digitalWrite(SDIO_PIN, 0);//給從機發送應答信號,即拉低數據線,然后拉高時鐘讓從機讀取該應答
digitalWrite(SCLK_PIN, 1);
digitalWrite(SCLK_PIN, 0);//執行完應答后拉低時鐘線,準備下一步動作。}void I2C_NACK(){ //非應答信號:即主機不再對從機進行應答,主機釋放數據線,即拉高數據線,然后給時鐘一個周期信號(拉高再拉低)
pinMode(SDIO_PIN, OUTPUT);
digitalWrite(SDIO_PIN, 1);
digitalWrite(SCLK_PIN, 1);
digitalWrite(SCLK_PIN, 0);
}uint16_t read_reg(uint8_t reg)
{ uint16_t dat=0;
reg=(reg<<1)|1;
I2C_Start();
I2C_Write(0x80);
I2C_RACK();
I2C_Write(reg);
I2C_RACK();
dat=I2C_Read();
dat=dat<<8;
I2C_ACK();
dat=dat|I2C_Read();
I2C_NACK();
I2C_Stop(); return dat;
}void write_reg(uint8_t reg, uint16_t dat){
reg=(reg<<1);
I2C_Start();
I2C_Write(0x80);
I2C_RACK();
I2C_Write(reg);
I2C_RACK();
I2C_Write(dat>>8);
I2C_RACK();
I2C_Write(dat&0xFF);
I2C_NACK();
I2C_Stop();
}void setup() {
Serial.begin(115200);
Serial.println("Hello I2C");
write_reg(0x02,0x2250);
Serial.println(read_reg(0x02),HEX);
write_reg(0x02,0x2281);
Serial.println(read_reg(0x02),HEX);
}void loop() {
}看完這篇文章,你學會純手工擼IO模擬I2C時序的代碼了嗎?















評論