I2C(Inter-Integrated Circuit)协议是一种用于同步、半双工、串行总线(由SCL时钟线、SDA数据线组成)上的协议。规定了总线空闲状态、起始条件、停止条件、数据有效性、字节格式、响应ACK信号、从设备地址选择、数据方向。有主从机之分,主机master就是掌控SCL时钟信号的一方,并且起始信号和停止信号也由主机发送。
一、协议说明
(1)总线空闲状态:SCL、SDA均为高电平,此时主从设备都不控制总线(主从设备的SDA和SCL引脚为输入或者开漏输出),由外部上拉电阻将总线拉高。
(2)起始条件:SCL在高电平的时候,SDA出现下降沿,如下图:
(3)停止条件:SCL在高电平的时候,SDA出现上升沿,如下图:
(4)数据有效性:什么时候读数据(读SDA电平)有效?在SCL为高电平时,读数据有效。什么时候写数据(改变SDA电平)有效?在SCL为低电平时,写数据有效。读写时序如下图:
(5)字节格式:发送到SDA线上的每个字节必须为8位,首先传输的时字节最高位(MSB)。
(6)响应ACK:为确保发送数据被对方可靠地接收,接收方必须发出ACK信号表示已成功接收数据。这个ACK信号就是接收方在接收完一个字节数据后,SCL的第一个下降沿处拉低SDA。发送方如何接收这个ACK?在发送方发送完一个字节数据后,SCL的第一个高电平处读取SDA线(SDA引脚处于输入状态不会使SDA拉低),如果读出为低,则表示接收方成功接收到数据。在发送方读取完ACK后,接收方需要释放SDA,即在SCL的第二个下降沿处将SDA设置为开漏输出,释放SDA总线。
(7)从设备地址选择和数据方向:在发送起始信号之后,紧接着发送的一个字节,其前7位是从设备的地址,最后一位表示数据传输的方向,0表示写(即接下来由主机发送数据),1表示读(即接下来由主机接收数据)。
二、C代码实现
下面模拟的时I2C master主机的时序。SCL信号由主机发出。
(1)模拟I2C使用到的IO口配置和延时函数,与平台相关(以STM8L为例)
//SCL引脚配置为输出
#define SCL_SET_OUTPUT() GPIO_Init(I2C_SCL_GROUP, I2C_SCL_PIN, GPIO_Mode_Out_OD_HiZ_Fast)
//SCL引脚配置为输入
#define SCL_SET_INPUT() GPIO_Init(I2C_SCL_GROUP, I2C_SCL_PIN, GPIO_Mode_In_FL_No_IT)
//SDA引脚配置为输出
#define SDA_SET_OUTPUT() GPIO_Init(I2C_SDA_GROUP, I2C_SDA_PIN, GPIO_Mode_Out_OD_HiZ_Fast)
//SDA引脚配置为输入
#define SDA_SET_INPUT() GPIO_Init(I2C_SDA_GROUP, I2C_SDA_PIN, GPIO_Mode_In_FL_No_IT)
//读SCL引脚电平
#define SCL_READ() GPIO_ReadInputDataBit(I2C_SCL_GROUP, I2C_SCL_PIN)
//读SDA引脚电平
#define SDA_READ() GPIO_ReadInputDataBit(I2C_SDA_GROUP, I2C_SDA_PIN)
//SCL引脚拉高
#define SCL_WRITE_HIGH() GPIO_SetBits(I2C_SCL_GROUP, I2C_SCL_PIN)
//SCL引脚拉低
#define SCL_WRITE_LOW() GPIO_ResetBits(I2C_SCL_GROUP, I2C_SCL_PIN)
//SDA引脚拉高
#define SDA_WRITE_HIGH() GPIO_SetBits(I2C_SDA_GROUP, I2C_SDA_PIN)
//SDA引脚拉低
#define SDA_WRITE_LOW() GPIO_ResetBits(I2C_SDA_GROUP, I2C_SDA_PIN)
//延时的目的是I2C设备由上升沿和下降沿时间,延时周期一般大于1/400KHz即可。该延时也决定了数据传输的速率
void delay_us(uint8_t delay_time)
{
while(delay_time--)
{
nop();
}
}
(2)起始信号
void i2c_start(void)
{
//对SCL和SDA进行输出配置
SDA_SET_OUTPUT();
delay_us(DELAY_CNT);
//拉高SCL和SDA
SDA_WRITE_HIGH();
delay_us(DELAY_CNT);
SCL_WRITE_HIGH();
delay_us(DELAY_CNT);
//在SCL高电平时,SDA出现下降沿,即触发开始信号
SDA_WRITE_LOW();
delay_us(DELAY_CNT);
}
(3)停止信号
void i2c_stop(void)
{
//对SCL和SDA进行输出配置
SDA_SET_OUTPUT();
delay_us(DELAY_CNT);
//拉高SCL,拉低SDA
SDA_WRITE_LOW();
delay_us(DELAY_CNT);
SCL_WRITE_HIGH();
delay_us(DELAY_CNT);
//在SCL高电平时,SDA出现上升沿,即触发停止信号
SDA_WRITE_HIGH();
delay_us(DELAY_CNT);
}
(4)发送数据
/*
* 返回值 0:没有收到接收方的ACK信号(此时需要重发数据或者停止传输)
* 1:收到了接收方的ACK信号
*/
void i2c_send(uint8_t send_data)
{
int8_t send_data_index = 0;
for(send_data_index = 7; send_data_index >= 0; send_data_index--)
{
//拉低SCL,改变SDA
SCL_WRITE_LOW();
delay_us(DELAY_CNT);
if(send_data_index == 7)
{
SDA_SET_OUTPUT();
delay_us(DELAY_CNT);
}
if(send_data >> send_data_index & 0x01)
{
SDA_WRITE_HIGH();
delay_us(DELAY_CNT);
}
else
{
SDA_WRITE_LOW();
delay_us(DELAY_CNT);
}
//拉高SCL,保持SDA稳定,供从设备读取
SCL_WRITE_HIGH();
delay_us(DELAY_CNT);
}
//拉低SCL,触发从设备ACK
SCL_WRITE_LOW();
delay_us(DELAY_CNT);
}
(5)等待ACK信号
uint8_t i2c_wait_ack(void)
{
uint8_t ack = 0;
SDA_SET_INPUT();
delay_us(DELAY_CNT);
//读SDA,检测从设备是否回应
SCL_WRITE_HIGH();
delay_us(DELAY_CNT);
ack = (SDA_READ() != RESET) ? 0 : 1;
SCL_WRITE_LOW();
delay_us(DELAY_CNT);
return ack;
}
(6)接收数据
void i2c_receive(uint8_t *p_receive_buff)
{
int8_t send_data_index = 0;
for(send_data_index = 7; send_data_index >= 0; send_data_index--)
{
//拉低SCL,供从设备改变SDA
SCL_WRITE_LOW();
delay_us(DELAY_CNT);
if(send_data_index == 7)
{
SDA_SET_INPUT();
delay_us(DELAY_CNT);
}
//拉高SCL,读取SDA
SCL_WRITE_HIGH();
delay_us(DELAY_CNT);
if(SDA_READ() != RESET)
{
(*p_receive_buff) |= (0x01 << send_data_index);
}
else
{
(*p_receive_buff) &= ~(0x01 << send_data_index);
}
}
}
(7)发送ACK信号
void i2c_send_ack(void)
{
//读完数据,在第九个时钟(即第八个时钟的下降沿)发出ACK信号
SCL_WRITE_LOW();
delay_us(DELAY_CNT);
SDA_SET_OUTPUT();
delay_us(DELAY_CNT);
SDA_WRITE_LOW();
delay_us(DELAY_CNT);
//释放总线
SCL_WRITE_HIGH();
delay_us(DELAY_CNT);
}
(8)发送NACK信号
void i2c_send_no_ack(void)
{
//读完数据,在第九个时钟(即第八个时钟的下降沿)发出ACK信号
SCL_WRITE_LOW();
delay_us(DELAY_CNT);
SDA_WRITE_HIGH();
delay_us(DELAY_CNT);
//释放总线
SCL_WRITE_HIGH();
delay_us(DELAY_CNT);
}
在调试过程中有一个需要注意的地方:
①起始信号和停止信号的模拟,不要随便加入SCL的下降沿,因为很有可能在接收数据之后发送一个停止信号,如果此时停止信号中一开始出现SCL下降沿,会是从设备误认为还要发送数据,很有可能将SDA拉低,一旦从设备拉低了SDA,停止信号在拉高SDA时会失败,导致停止信号失效;
②在SCL高电平时,不要切换SDA的输入输出模式,因为很有可能切换SDA输入输出时,SDA引脚上会输出抖动的电平信号,而这时SCL是高电平,所以有可能会误触发起始或停止信号;
③输出要配置成开漏输出。
以上就是I2C协议软件模拟的C代码实现,下一篇将介绍软件模拟I2C的具体应用,以SHT20温湿度传感器为例。