在C2000上使用GPIO模拟I2C通信

作者: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总线,具体定义请参考例程部分。

  1. void I2C_Start(void)  
  2. {  
  3.     Delay(I2CDelay);  
  4.     SCL_High();   // Set the SCL  
  5.     SDA_High();   // Set the SDA  
  6.     Delay(I2CDelay);  
  7.     SDA_Low();    // Clear the SDA while SCL is high indicates the start signal  
  8.     Delay(I2CDelay);  
  9.     SCL_Low();    // Clear the SCL to get ready to transmit 
  10. }  

     可以参考以下代码实现结束信号的发送:

  1. void I2C_Finish(void)  
  2. {  
  3.     SDA_Low();    // Clear the SDA  
  4.     SCL_Low();    // Clear the SCL  
  5.     Delay(I2CDelay);  
  6.     SCL_High();   // Set the SCL  
  7.     Delay(I2CDelay);  
  8.     SDA_High();   // Set the SDA while SCL is high indicates the finish signal  
  9. }  

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的实现方法:

  1. void I2C_Send_Byte(unsigned char txd)  
  2. {  
  3.     int t;  
  4.     SDA_Output();   // Config SDA GPIO as output  
  5.     SCL_Low();      // Clear the SCL to get ready to transmit  
  6.     txd&=0X00FF;    // Get the lower 8 bits  
  7.     for(t=0;t<8;t++)  
  8.         {  
  9.             SDA_Data_Register = (txd&0x80)>>7;  // Send the LSB  
  10.             txd<<=1;  
  11.             Delay(I2CDelay/2);  
  12.            SCL_High();   // Set the SCL  
  13.            Delay(I2CDelay);  
  14.            SCL_Low();    // Clear the SCL  
  15.            Delay(I2CDelay/2);  
  16.         } 
  17. }  

1.3 ACK/NACK指令

     Acknowledge(ACK)以及Not Acknowledge(NACK)指令通常发生在一个byte发送结束之后,用于标志一个byte发送的成功或失败。特别需要注意的是,即使是在ACK时钟周期期间,SCL总线时钟信号也是由主机产生的。

     ACK: 当一次发送结束,主机释放SDA总线。若发送成功,从机在第9个时钟周期内拉低SDA总线,并在整个高电平期间保持。

     NACK: 当一次发送结束,主机释放SDA总线。若发送失败,在第9个时钟周期内SDA始终处于高电平。

     在通讯中作为主机的MCU通常只需要实现NACK的发送以及ACK信号的等待,具体可参考以下程序:

  1. void I2C_NAck(void)  
  2. {  
  3.     SCL_Low();     // Clear the SCL to get ready to transmit  
  4.     SDA_Low() ;    // Clear the SDA  
  5.     Delay(I2CDelay);  
  6.     SCL_High();    // Set the SCL  
  7.     Delay(I2CDelay);  
  8.     SCL_Low();      // Clear the SCL  
  9.     Delay(I2CDelay); 
  10. }  

 

  1. Uint16 I2C_Wait_Ack(void)  
  2. {  
  3.     int ErrTime=0;  
  4.     int ReadAck=0;  
  5.     SDA_Input();  // Config SDA GPIO as Input
  6.     Delay(I2CDelay);  
  7.     SCL_High();   // Set the SCL and wait for ACK  
  8.     while(1)  
  9.     {  
  10.          ReadAck = SDA_Data_Register ;  // Read the input  
  11.          if(ReadAck)  
  12.         {   
  13.              ErrTime++;  
  14.              if(ErrTime>ErrLimit)  
  15.             {  
  16.                  //Error handler:Set error flag, retry or stop.  
  17.                 //Define by users  
  18.                  return 1;  
  19.              }  
  20.         }  
  21.          if(ReadAck==0)  // Receive a ACK
  22.          {  
  23.              Delay(I2CDelay);  
  24.             SCL_Low();   // Clear the SCL for Next Transmit  
  25.              return 0;  
  26.          }  
  27.      } 

   基于以上几个基本的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写入寄存器帧格式

具体实现方法可以参考以下代码:

  1. void I2C_Write_Register(unsigned char Device, unsigned char Register,unsigned char Value)  
  2. {  
  3.         I2C_Start();  
  4.         I2C_Send_Byte(Device); //Send the device address  
  5.         I2C_Wait_Ack();       //Wait for the ack signal  
  6.         I2C_Send_Byte(Register); //Send the register address  
  7.         I2C_Wait_Ack();       //Wait for the ack signal  
  8.         I2C_Send_Byte(Value); //Send register value  
  9.         I2C_Wait_Ack();  
  10.         I2C_Finish(); 
  11. }  

         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的读方法可以参考以下代码:

  1. unsigned char I2C_Read_Byte(void)  
  2. {  
  3.     int t,rxData;  
  4.     unsigned char receive;  
  5.     SDA_Input();  
  6.     for(t=0;t<8;t++)  
  7.     {  
  8.         SCL_Low();   // Clear the SCL  
  9.         Delay(I2CDelay);  
  10.          SCL_High();   // Set the SCL  
  11.          receive<<=1;  
  12.          rxData = SDA_Data_Register ;  
  13.          if(rxData)  
  14.          {  
  15.              receive++;  
  16.          }  
  17.          Delay(I2CDelay);  
  18.      }  
  19.      return receive; 
  20. }

          寄存器的读方法可以参考以下代码:

  1. unsigned char I2C_Read_Register(unsigned char Device_Write,unsigned char Device_Read, unsigned char Register)  
  2. {  
  3.     unsigned char ReadData;  
  4.     I2C_Start();  
  5.     I2C_Send_Byte(Device_Write); //Send the device address  
  6.     I2C_Wait_Ack();             //Wait for the ack signal  
  7.     I2C_Send_Byte(Register);    //Send the register address  
  8.     I2C_Wait_Ack();             //Wait for the ack signal  
  9.     I2C_Start();  
  10.     I2C_Send_Byte(Device_Read); //Send register value  
  11.     I2C_Wait_Ack();  
  12.     SDA_High();                  // Set the SDA  
  13.     ReadData = I2C_Read_Byte();  
  14.     I2C_NAck();  
  15.     Delay(1);  
  16.     I2C_Finish();  
  17.     return ReadData; 
  18. }  

三、参考例程

       本文附带的例程中包含了完整GPIO模拟I2C通讯的头文件以及函数,下面对例程中的主要内容进行介绍,以方便读者理解。

图6 I2C通讯程序架构

 

3.1宏定义

1)定义硬件通讯通讯地址及寄存器地址:

  1. #define Device_Address_Write 0xC0  
  2. #define Device_Address_Read 0xC1   
  3. #define REG_1 0x01  
  4. #define REG_2 0x02  
  5. #define REG_3 0x03  
  6. #define REG_4 0x04  

Device_Address_Write

硬件写地址:默认地址0x60(7bit)+0b

Device_Address_Read

硬件读地址:默认地址0x60(7bit)+0b

REG_1 - 4

硬件内部寄存器地址

   表1 硬件读写地址及寄存器地址

     在调用此代码时,只需在.h文件依照所用器件实际情况修改硬件地址及各寄存器地址,就可以很方便地调用相关函数。

2)定义I2C通讯速率

  1. #define I2CDelay 1    // Define to configure I2C rate  

I2CDelay

I2C通讯时钟高低电平时间

       表2 I2C通讯速率

     通过改变I2CDelay可以设置I2C通讯时钟的高低电平持续时间,进而改变I2C的通讯速率。实际应用中,该值可以通过实际测试进行调整,以达到理想的通讯速率。

3)定义IO口动作

  1. #define SDA_High() {GpioDataRegs.GPASET.bit.GPIO7 = 1;EALLOW;GpioCtrlRegs.GPADIR.bit.GPIO7=1;EDIS;} 
  2. #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  
  3. #define SDA_Input() {EALLOW;GpioCtrlRegs.GPADIR.bit.GPIO7=0;EDIS;}  // SDA DIR=Input  
  4. #define SDA_Output(){EALLOW;GpioCtrlRegs.GPADIR.bit.GPIO7=1;EDIS;}  // SDA DIR=Output  
  5. #define SDA_Data_Register GpioDataRegs.GPADAT.bit.GPIO7  
  6. #define SCL_High() {GpioDataRegs.GPASET.bit.GPIO6 = 1;}     //Set the SCL line  
  7. #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口动作的代码,不需要对程序其他部分进行改动。其中EALLOW\EDIS语句是TI C2000产品改变GPIO口方向时需要解除相应的保护,请根据具体情况进行改动。

4)定义Delay函数

  1. #define Delay(A) DELAY_US(A)  

       Delay()函数用于进行程序中SDA、SCL的高低电平延时,在例程中实际被定义成DELAY_US()函数。在移植过程需要根据实际情况修改宏定义,更改成适用用户MCU的延时函数,不需要对后续程序进行修改。

3.2 I2C通讯功能函数

  1. void I2C_Start(void);  
  2. void I2C_Finish(void);  
  3. Uint16 I2C_Wait_Ack(void);  
  4. void I2C_NAck(void);  
  5. void I2C_Send_Byte(unsigned char xtd);  
  6. 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需求给出了一种有效的解决方案。