發布日期:2022-04-28 點擊率:103
Other Parts Discussed in Post: BQ25703A
作者:TI 華北區工程師 Brian Wang 和 Young Hu
相關代碼請點擊以下附件下載:
I2C_Simulation.c I2C_Simulation.h
引言
I2C作為一種簡單的數字通訊方式,僅需要兩根數據線就可以完成近距離主機(Master)與從機(Slave)之間的通訊,節省了MCU引腳以及額外的邏輯芯片,簡化了PCB布板難度,因此得到了廣泛的應用。近年來,TI也推出了越來越多支持I2C通訊功能的芯片,大大簡化了芯片與MCU之間的通訊,方便了系統的設計。
但在實際應用中,針對性能要求較低的應用場合,通常選擇外設較為簡單的低端主控MCU,可能并不具備I2C接口。對于此類應用,可以通過MCU的IO口進行I2C模擬,與被控器件建立通訊,達到發送控制指令、讀取內部寄存器的目的。即使在I2C接口缺失的情況下也能夠充分發揮器件的全部功能。
本文基于C2000提供了一種利用GPIO模擬I2C控制被控芯片的解決方案,并附有完整例程。對于絕大多數采用標準I2C通信協議以及部分采用SMBus的芯片均具有參考意義。基于其它MCU的方案也可參考該例程進行移植。
一、I2C 通訊協議與GPIO模擬
I2C總線由兩條雙向信號線構成,分別為數據線(SDA)以及時鐘線(SCL),分別用電阻進行上拉,以實現高低電平之間的切換,進行設備之間的數據交交換。I2C允許的工作電壓范圍較為寬泛,典型電壓基準為+3.3V或+5V。常見的I2C總線速率分為以下幾種模式:標準模式(100Kbit/s)、快速模式(400Kbit/s)以及高速模式(3.4Mbit/s)等。如圖為典型的I2C連接示意圖:
圖1 I2C連接示意圖
如圖2為典型的I2C通訊幀格式示意圖。一幀完整的數據發送主要包括起始位、地址位、讀/寫位、ACK/NCK位、數據位等。下面對各部分進行簡要的講解,并介紹如何通過C2000進行實現。
圖2 I2C連接示意圖
1.1 起始及結束指令
當某個設備在I2C總線上被配置為主機(Master),該設備可以發送起始及結束信號用來發起或結束一次I2C通信,母線電平示意圖如圖2所示。
起始信號:在SCL為高電平期間,SDA由高電平轉換為低電平。
結束信號:在SCL為高電平期間,SDA由低電平轉換為高電平。
圖3 I2C通訊起始及結束信號
在C2000中,可以通過以下代碼實現起始信號的發送。其中SCL及SDA分別代表用C2000 GPIO模擬的SDA及SCL總線,具體定義請參考例程部分。
void I2C_Start(void)
{
Delay(I2CDelay);
SCL_High(); // Set the SCL
SDA_High(); // Set the SDA
Delay(I2CDelay);
SDA_Low(); // Clear the SDA while SCL is high indicates the start signal
Delay(I2CDelay);
SCL_Low(); // Clear the SCL to get ready to transmit
}
可以參考以下代碼實現結束信號的發送:
void I2C_Finish(void)
{
SDA_Low(); // Clear the SDA
SCL_Low(); // Clear the SCL
Delay(I2CDelay);
SCL_High(); // Set the SCL
Delay(I2CDelay);
SDA_High(); // Set the SDA while SCL is high indicates the finish signal
}
1.2 數據位及地址位
I2C通訊的數據位通常由1-8的數據構成,在主機進行數據的發送以及讀取期間,SCL總線時鐘信號時鐘仍由主機發出,每個SCL高電平期間對應一位數據。在SCL高電平期間,都應該保持SDA上的數據正確,因此在實際的應用中,通常使得SDA的高電平脈寬寬于SCL。
地址位的發送與數據位類似,實際的操作中可以將設備的7位地址位+1位讀寫位作為一個8位字節進行整體的發送。以BQ25703A為例,默認設備地址為0x6B(7bit)。則在進行讀操作時,所要發送的字節為0xD7(1101011b+1b);進行寫操作時,所要發送的整體字節為0xD6 (1101011b+0b)。
數據位及地址位的發送均可參考以下發送一個8位byte的實現方法:
void I2C_Send_Byte(unsigned char txd)
{
int t;
SDA_Output(); // Config SDA GPIO as output
SCL_Low(); // Clear the SCL to get ready to transmit
txd&=0x00FF; // Get the lower 8 bits
for(t=0;t<8;t++)
{
SDA_Data_Register = (txd&0x80)>>7; // Send the LSB
txd<<=1;
Delay(I2CDelay/2);
SCL_High(); // Set the SCL
Delay(I2CDelay);
SCL_Low(); // Clear the SCL
Delay(I2CDelay/2);
}
}
1.3 ACK/NACK指令
Acknowledge(ACK)以及Not Acknowledge(NACK)指令通常發生在一個byte發送結束之后,用于標志一個byte發送的成功或失敗。特別需要注意的是,即使是在ACK時鐘周期期間,SCL總線時鐘信號也是由主機產生的。
ACK: 當一次發送結束,主機釋放SDA總線。若發送成功,從機在第9個時鐘周期內拉低SDA總線,并在整個高電平期間保持。
NACK: 當一次發送結束,主機釋放SDA總線。若發送失敗,在第9個時鐘周期內SDA始終處于高電平。
在通訊中作為主機的MCU通常只需要實現NACK的發送以及ACK信號的等待,具體可參考以下程序:
void I2C_NAck(void)
{
SCL_Low(); // Clear the SCL to get ready to transmit
SDA_Low() ; // Clear the SDA
Delay(I2CDelay);
SCL_High(); // Set the SCL
Delay(I2CDelay);
SCL_Low(); // Clear the SCL
Delay(I2CDelay);
}
Uint16 I2C_Wait_Ack(void)
{
int ErrTime=0;
int ReadAck=0;
SDA_Input(); // Config SDA GPIO as Input
Delay(I2CDelay);
SCL_High(); // Set the SCL and wait for ACK
while(1)
{
ReadAck = SDA_Data_Register ; // Read the input
if(ReadAck)
{
ErrTime++;
if(ErrTime>ErrLimit)
{
//Error handler:Set error flag, retry or stop.
//Define by users
return 1;
}
}
if(ReadAck==0) // Receive a ACK
{
Delay(I2CDelay);
SCL_Low(); // Clear the SCL for Next Transmit
return 0;
}
}
}
基于以上幾個基本的I2C通訊操作,就可以發送一個完整I2C數據幀,實現基本的I2C通訊功能,構建了利用GPIO口模擬I2C進行芯片控制的基礎。
二、I2C模擬器件寄存器寫入與讀取
在構建了基本的I2C通訊功能之后,就可以利用I2C通訊對Slave進行控制或狀態的讀取,其本質就是對Slave的內部寄存器進行讀寫操作。下面以一個典型的帶有I2C功能的8位寄存器芯片為例,介紹如何利用前文的基礎I2C模擬函數對芯片的內部寄存器進行寫入和讀取。
I2C 寫入:要進行一次I2C寫入,MCU首先要發送一個起始位以及一個由7位slave地址位和讀寫位(0b)組成的8位硬件寫地址,而后釋放SDA總線。若地址正確,slave將拉低SDA發送一個ACK。此后,MCU發送寫入寄存器的地址,并等待slave返回的ACK。響應后,MCU發送8位數據,并在收到ACK響應后發送停止位。
圖4 I2C寫入寄存器幀格式
具體實現方法可以參考以下代碼:
void I2C_Write_Register(unsigned char Device, unsigned char Register,unsigned char Value)
{
I2C_Start();
I2C_Send_Byte(Device); //Send the device address
I2C_Wait_Ack(); //Wait for the ack signal
I2C_Send_Byte(Register); //Send the register address
I2C_Wait_Ack(); //Wait for the ack signal
I2C_Send_Byte(Value); //Send register value
I2C_Wait_Ack();
I2C_Finish();
}
I2C讀取:要讀取Slave的內部寄存器,MCU首先要與Slave進行一次通信,告知Slave讀取的目標寄存器,該過程與進行寫入操作類似。MCU首先發送起始位、8位Slave寫地址,并在ACK信號后發送8位的目標寄存器地址。在Slave響應該地址后,MCU重新發送一次起始位,以及8位Slave讀地址(7位地址+1b),ACK響應后MCU釋放SDA總線,并繼續發送SCL時鐘信號讀取SDA上的內容。接收完成后,MCU 發送NACK位以及STOP位結束一次寄存器讀取操作。
圖5 I2C讀取寄存器幀格式
8位Byte的讀方法可以參考以下代碼:
unsigned char I2C_Read_Byte(void)
{
int t,rxData;
unsigned char receive;
SDA_Input();
for(t=0;t<8;t++)
{
SCL_Low(); // Clear the SCL
Delay(I2CDelay);
SCL_High(); // Set the SCL
receive<<=1;
rxData = SDA_Data_Register ;
if(rxData)
{
receive++;
}
Delay(I2CDelay);
}
return receive;
}
寄存器的讀方法可以參考以下代碼:
unsigned char I2C_Read_Register(unsigned char Device_Write,unsigned char Device_Read, unsigned char Register)
{
unsigned char ReadData;
I2C_Start();
I2C_Send_Byte(Device_Write); //Send the device address
I2C_Wait_Ack(); //Wait for the ack signal
I2C_Send_Byte(Register); //Send the register address
I2C_Wait_Ack(); //Wait for the ack signal
I2C_Start();
I2C_Send_Byte(Device_Read); //Send register value
I2C_Wait_Ack();
SDA_High(); // Set the SDA
ReadData = I2C_Read_Byte();
I2C_NAck();
Delay(1);
I2C_Finish();
return ReadData;
}
三、參考例程
本文附帶的例程中包含了完整GPIO模擬I2C通訊的頭文件以及函數,下面對例程中的主要內容進行介紹,以方便讀者理解。
圖6 I2C通訊程序架構
3.1宏定義
1)定義硬件通訊通訊地址及寄存器地址:
#define Device_Address_Write 0xC0
#define Device_Address_Read 0xC1
#define REG_1 0x01
#define REG_2 0x02
#define REG_3 0x03
#define REG_4 0x04
Device_Address_Write | 硬件寫地址:默認地址0x60(7bit)+0b |
Device_Address_Read | 硬件讀地址:默認地址0x60(7bit)+0b |
REG_1 - 4 | 硬件內部寄存器地址 |
表1 硬件讀寫地址及寄存器地址
在調用此代碼時,只需在.h文件依照所用器件實際情況修改硬件地址及各寄存器地址,就可以很方便地調用相關函數。
2)定義I2C通訊速率
#define I2CDelay 1 // Define to configure I2C rate
I2CDelay | I2C通訊時鐘高低電平時間 |
表2 I2C通訊速率
通過改變I2CDelay可以設置I2C通訊時鐘的高低電平持續時間,進而改變I2C的通訊速率。實際應用中,該值可以通過實際測試進行調整,以達到理想的通訊速率。
3)定義IO口動作
#define SDA_High() {GpioDataRegs.GPASET.bit.GPIO7 = 1;EALLOW;GpioCtrlRegs.GPADIR.bit.GPIO7=1;EDIS;}
#define SDA_Low() {GpioDataRegs.GPACLEAR.bit.GPIO7 = 1;EALLOW;GpioCtrlRegs.GPADIR.bit.GPIO7=1;EDIS;}//To clear the SDA line. Disable protection for writing register
#define SDA_Input() {EALLOW;GpioCtrlRegs.GPADIR.bit.GPIO7=0;EDIS;} // SDA DIR=Input
#define SDA_Output(){EALLOW;GpioCtrlRegs.GPADIR.bit.GPIO7=1;EDIS;} // SDA DIR=Output
#define SDA_Data_Register GpioDataRegs.GPADAT.bit.GPIO7
#define SCL_High() {GpioDataRegs.GPASET.bit.GPIO6 = 1;} //Set the SCL line
#define SCL_Low() {GpioDataRegs.GPACLEAR.bit.GPIO6 = 1;} //Clear the SCL line
SDA_High() | 將SDA對應GPIO置1 |
SDA_Low() | 將SDA對應GPIO置1 |
SDA_Input | 將SDA對應GPIO設為輸入狀態 |
SDA_Output | 將SDA對應GPIO設為輸出狀態 |
SDA_Data_Register | SDA對應GPIO數據寄存器 |
SCL_High() | 將SCL對應GPIO置1 |
SCL_Low() | 將SCL對應GPIO置0 |
表3 IO口動作宏定義
將GPIO口的動作以宏定義的形式定義為SDA、SCL的動作,以增強代碼的可讀性。在進行程序移植時,只需要根據單片機實際情況將宏定義內的代碼更換成對應GPIO口動作的代碼,不需要對程序其他部分進行改動。其中EALLOWEDIS語句是TI C2000產品改變GPIO口方向時需要解除相應的保護,請根據具體情況進行改動。
4)定義Delay函數
#define Delay(A) DELAY_US(A)
Delay()函數用于進行程序中SDA、SCL的高低電平延時,在例程中實際被定義成DELAY_US()函數。在移植過程需要根據實際情況修改宏定義,更改成適用用戶MCU的延時函數,不需要對后續程序進行修改。
3.2 I2C通訊功能函數
void I2C_Start(void);
void I2C_Finish(void);
Uint16 I2C_Wait_Ack(void);
void I2C_NAck(void);
void I2C_Send_Byte(unsigned char xtd);
unsigned char I2C_Read_Byte(void);
函數名稱 | 功能描述 |
void I2C_Start(void) | 發送I2C通訊起始信號 |
void I2C_Finish(void) | 發送I2C通訊結束信號 |
Uint16 I2C_Wait_Ack(void) | 等待Ack應答信號,返回接收狀態 |
void I2C_NAck(void) | 發送一個NAck信號,用于寄存器讀取 |
void I2C_Send_Byte(unsigned char xtd) | 發送一個字節 |
unsigned char I2C_Read_Byte(void) | 讀取一個字節 |
void Gpio_setup(void) | GPIO口配置 |
void I2C_Write_Register(unsigned char Device, unsigned char Register, unsigned char value) | I2C 寫寄存器函數 |
void I2C_Read_Register(unsigned char Device_Write, unsigned char Device_Read, unsigned char Register) | I2C 讀寄存器函數 |
表4 I2C通訊函數
四、總結
針對由于MCU缺少I2C接口而不能直接使用I2C與外圍芯片進行通訊的問題,本文給出了使用IO模擬I2C接口的方法。首先,從I2C協議入手對數據幀中各個位的邏輯電平進行了詳細介紹,并給出基于C2000 GPIO的具體實現方法;在此基礎上,以常見的8位I2C通訊Slave為例介紹了內部寄存器的讀取邏輯,并給出了實現方法。最后,針對附帶的參考例程內容進行了介紹,方便讀者參考例程,其它MCU也可以在本例程上進行快速的移植。本文為使用IO模擬I2C需求給出了一種有效的解決方案。
下一篇: PLC、DCS、FCS三大控
上一篇: 基于C2000的軟件串口