文章目录
一、I2C总线结构
特点:
1、I2C是一种半双工的同步传输协议,通过SCL和SDA两线进行主从机的数据交互。
2、存在“线与”机制。多主机时,总线具有“线与”的逻辑功能,只要有一个节点发送低电平,总线上就表现低电平的状态。
3、存在SDA回读机制。总线被启动后,多个主机在每发送一个数据位时都要对自己的输出电平进行检测,只要检测的电平与自己发出的电平一致,就会继续占用总线。
4、上拉电阻存在的意义:确保总线在空闲时为高电平
5、SCL和SDA需要设置为开漏口。设置为开漏口的目的:防止两个设备一个拉高,一个拉低,造成短路的现象。当设置为开漏口时,不存在P管,输出高电平的能力来源于VCC,这样总线上的电路就不会存在短路的风险。
二、根据I2C的时序进行代码的构建
主机发送数据给从机
1、主机发送数据时可以看成5个步骤,发送起始信号,发送地址码及读写位,接收返回信号、发送数据、发送停止位。
①主机发送起始信号。
②主机发送从设备地址(7bit) + 读写位(最低位)。0表示主机写,1表示主机读。
③主机接收从机的应答信号(ACK/nACK)。
④主机发送数据,发送完成后若接收到ACK信号则继续发送下一帧数据。
⑤主机接收到nACK信号后,发送停止信号。
**
根据上述的步骤,在程序中定义相关的状态。SCL的时序由定时器产生。
typedef enum{ //i2c对应的状态
S_Start = 0, //起始
S_Radio_Address, //广播地址
S_WriteReadBit, //发送读写位
S_WriteData, //发送数据
S_Read_Ack, //读取应答信号
//S_ReadData, //读取数据 只针对主机发送,暂时可以忽略该成员
//S_Write_Ack, //发送应答信号 只针对主机发送,暂时可以忽略该成员
S_Stop, //停止信号
S_Error, //通讯过程中出现异常的处理标志
}Master_Step;
uint8_t S_Master_Step; //用于表示当前I2C的状态
1、起始信号
初始状态:S_Master_Step == S_Start,当SCL为高电平时,拉低SDA,产生起始信号。起始信号产生后S_Master_Step = S_Radio_Address。准备广播地址。
if(S_Master_Step == Step_Start)
{
//I2C开始通讯
if(Master_SCL == 1 && Master_SDA == 1) //SCL为高,SDA空闲
{
Master_SDA = 0; //SDA拉低
Master_SCL_TIMEn //SCL时序由定时器产生
S_Master_Step = Step_Radio_Address; //主机准备发送地址
}
}
2、地址广播
当S_Master_Step == S_Radio_Address时,进行地址广播。
可以看成主机发送一串数据给从机。
在SCL高电平期间
在SCL为高电平期间,SDA上的数据必须保持稳定,所以程序中不做处理。
case Step_Radio_Address: //地址广播
break;
在SCL低电平期间
case Step_Radio_Address: //广播地址
if(S_Master_BitCnt < 7)
{
S_Master_Buf = (Master_Message.Address) << S_Master_BitCnt; //高位先行
S_Master_BitCnt++;
if(S_Master_Buf & 0x80) //发送地址
{
Master_SDA = 1;
}
else
{
Master_SDA = 0;
}
}
else //最低位为读写位
{
S_Master_BitCnt = 0;
S_Master_Step = Step_Read_Ack; //master准备接收应答信号。
if(S_Master_Buf & 0x80) //LSB为读写位
{
U_Master_Status = Status_ReadData; // 最低位为1表示:主机读取数据
}
else
{
U_Master_Status = Status_SendData; // 最低位为0表示:主机写数据
}
}
break;
3、主机接收应答信号
注意点:
①在接收应答信号的前一个CLK信号的低电平时间内,将SDA端口切换为输入态,用于接收从机的ACK/nACK信号。
②主机发送完地址+读写位,接收从机的应答信号后,在SCL高电平期间,识别主机将要做发送动作还是接收动作。
③若主机处于发送状态,且接收到从机返回的ACK信号,在SCL高电平期间,准备下一帧要发送的数据。此时S_Master_Step的状态指向Step_Write_Data(写数据)。
④当数据发送完成时,将S_Master_Step的状态切换为Step_Stop。
在SCL高电平期间
case Step_Read_Ack:
if(Master_SDA == ACK) //主机读取到ACK信号
{
if(U_Master_Status == Status_SendData) // 主机写数据
{
S_Master_Step = Step_Write_Data; // 状态指向写数据
Master_SDA_OUT //SDA端口设置为输出
if(S_Master_ByteCnt < Master_Message.WriteSize) //识别主机当前发送第几帧数据
{
S_Master_Buf = *(Master_Message.WriteData+S_Master_ByteCnt); //将要发送的byte存放到S_Master_Buf
S_Master_ByteCnt++; // 发送的byte数目加1
}
else
{
//主机数据发送完毕,准备发送停止信号,将状态切换为 Step_Stop
S_Master_ByteCnt = 0;
S_Master_Step = Step_Stop;
}
}
/* 注释代码为主机读取部分,暂时可以忽略 */
//else if(U_Master_Status == Status_ReadData) //主机读取数据
// {
// //主机读取数据
// {
// Master_SDA_IN //SDA端口切换为输入态
// Master_Message.ReadData = Master_ReadDate; //数组的地址存放到结构体中
// S_Master_BitCnt = 0;
// S_Master_Step = Step_Read_Data; //状态指向读数据
// }
// }
}
else
{
//若在主机未发送完成,产生了nAck信号,可以查看S_Master_ByteCnt变量,得知主机发送到第几个byte
S_Master_Step = Step_Error; // 故障排查
}
break;
在SCL低电平期间
case Step_Read_Ack: //主机读取到Ack信号
if(!S_Master_BitCnt)
{
Master_SDA_IN //当主机发送完数据后,SDA立刻切换为输入态,准备读取从机发送的ACK信号
}
break;
4、主机发送数据
注意点:
① 在SCL高电平期间,需要保持数据的稳定性,禁止操作SDA端口。可在高电平期间对发送bit进行计数,当发送1个byte后,将S_Master_Step的状态切换Step_Read_Ack(读取ACK/nACK信号)。
② 在SCL低电平期间,传输数据,改变SDA端口的电平(高位先行)。
在SCL高电平期间
case Step_Write_Data: //主机发送数据
S_Master_BitCnt++; //记录发送bit的个数
if(S_Master_BitCnt == 8)
{
S_Master_BitCnt = 0;
S_Master_Step = Step_Read_Ack; //8bit数据发送完成后,接收Slave的ack信号
}
break;
在SCL低电平期间
case Step_Write_Data: //主机发送数据
if(S_Master_BitCnt < 8) //逐个bit发送
{
S_Master_Buf <<= S_Master_BitCnt; //移位
if(S_Master_Buf & Master_MSB) //高位先行
{
Master_SDA = 1;
}
else
{
Master_SDA = 0;
}
}
break;
5、主机发送停止信号
if(S_Master_Step == Step_Stop)
{
//I2C结束通讯
Master_SCL_TIMDis //关闭定时器,不再为SCL产生CLK
Master_SDA_OUT //SDA端口设置为输出
Master_SCL = 1; //拉高SCL端口
Master_SDA = 1; //拉高SDA端口
}
主机向从机读取数据
1、主机读取数据时有6个步骤,发送起始信号,发送地址码及读写位、接收应答信号,接收数据、发送返回信号、发送停止位。
①主机发送起始信号。
②主机发送从设备地址(7bit) + 读写位(最低位)。0表示主机写,1表示主机读。
③主机接收从机的应答信号(ACK/nACK)。
④主机接收数据,接收完成后,根据需求对从机回复应答信号,若主机回复ACK信号,则继续接收下一帧数据。
⑤当主机回复nACK信号时,下一个时钟,主机产生停止信号。
根据上述的步骤,在程序中定义相关的状态。
typedef enum{ //i2c对应的状态
S_Start = 0, //起始
S_Radio_Address, //广播地址
S_WriteReadBit, //发送读写位
S_WriteData, //发送数据
S_Read_Ack, //读取应答信号
S_ReadData, //读取数据
S_Write_Ack, //发送应答信号
S_Stop, //停止信号
S_Error, //通讯过程中出现异常的处理标志
}Master_Step;
uint8_t S_Master_Step; //用于表示当前I2C的状态
1、起始信号
S_Master_Step == S_Start,当SCL为高电平时,拉低SDA,产生起始信号。起始信号产生后S_Master_Step = S_Radio_Address。准备广播地址。
if(S_Master_Step == Step_Start)
{
//I2C开始通讯
if(Master_SCL == 1 && Master_SDA == 1) //SCL为高,SDA空闲
{
Master_SDA = 0; //SDA拉低
Master_SCL_TIMEn //SCL时序由定时器产生
S_Master_Step = Step_Radio_Address; //主机准备发送地址
}
}
2、地址广播
当S_Master_Step == S_Radio_Address时,进行地址广播。
在SCL高电平期间
在SCL为高电平期间,SDA上的数据必须保持稳定,所以程序中不做处理。
case Step_Radio_Address: //地址广播
break;
在SCL低电平期间
case Step_Radio_Address: //广播地址
if(S_Master_BitCnt < 7)
{
S_Master_Buf = (Master_Message.Address) << S_Master_BitCnt; //高位先行
S_Master_BitCnt++;
if(S_Master_Buf & 0x80) //发送地址
{
Master_SDA = 1;
}
else
{
Master_SDA = 0;
}
}
else //最低位为读写位
{
S_Master_BitCnt = 0;
S_Master_Step = Step_Read_Ack; //master准备接收应答信号。
if(S_Master_Buf & 0x80) //LSB为读写位
{
U_Master_Status = Status_ReadData; // 最低位为1表示:主机读取数据
}
else
{
U_Master_Status = Status_SendData; // 最低位为0表示:主机写数据
}
}
break;
3、接收应答信号
注意点:
①在接收应答信号的前一个CLK信号的低电平时间内,将SDA端口切换为输入态,用于接收从机的ACK/nACK信号。
②主机发送完地址+读写位,接收从机的应答信号后,在SCL高电平期间,识别主机将要做发送动作还是接收动作。
③若主机处于读取状态,且接收到从机返回的ACK信号,在SCL高电平期间,将SDA端口的状态切换为输入态,准备接收数据,此时S_Master_Step的状态指向Step_Read_Data(读数据)。
在SCL为高电平期间
case Step_Read_Ack:
if(Master_SDA == ACK) //主机读取到ACK信号
{
/* 注释代码为主机发送部分,暂时可以忽略 */
// if(U_Master_Status == Status_SendData) // 主机写数据
// {
// S_Master_Step = Step_Write_Data; // 状态指向写数据
// Master_SDA_OUT //SDA端口设置为输出
// if(S_Master_ByteCnt < Master_Message.WriteSize) //识别主机当前发送第几帧数据
// {
// S_Master_Buf = *(Master_Message.WriteData+S_Master_ByteCnt); // Mastert_WriteDate: 数组地址, S_Master_ByteCnt:偏移量
// S_Master_ByteCnt++;
// }
// else
// {
// //主机数据发送完毕,准备发送停止信号,将状态切换为 Step_Stop
// S_Master_ByteCnt = 0;
// S_Master_Step = Step_Stop;
// }
// }
/* else */
if(U_Master_Status == Status_ReadData) //主机读取数据
{
//主机读取数据
Master_SDA_IN //SDA端口切换为输入态
Master_Message.ReadData = Master_ReadDate; //数组的地址存放到结构体中
S_Master_BitCnt = 0;
S_Master_Step = Step_Read_Data; //状态指向读数据
}
}
else
{
//若在主机未发送完成,产生了nAck信号,可以查看S_Master_ByteCnt变量,得知主机发送到第几个byte
S_Master_Step = Step_Error; // 故障排查
}
break;
在SCL为低电平期间
case Step_Read_Ack: //主机读取到Ack信号
if(!S_Master_BitCnt)
{
Master_SDA_IN //当主机发送完数据后,SDA立刻切换为输入态,准备读取从机发送的ACK信号
}
break;
4、接收数据
①在SCL为高电平期间,SDA上的数据有效,此时主机读取SDA端口的数据。
②在SCL为低电平时,主机作为接收方,禁止操作SDA端口。可在低电平期间对接收bit进行计数,当接收1个byt后,将S_Master_Step的状态切换为Step_Write_Ack(发送ACK/nACK信号)
在SCL为高电平期间
case Step_Read_Data: //master read Data
if(S_Master_BitCnt < 8) //逐个接收bit
{
if(Master_SDA)
{
*(Master_Message.ReadData+S_Master_ByteCnt) |= 0x01; //接收数据
}
*(Master_Message.ReadData+S_Master_ByteCnt) <<= S_Master_BitCnt; //移位
S_Master_BitCnt++; // 记录接收bit的个数
}
break;
在SCL为低电平期间
case Step_Read_Data: //master read Data
if(S_Master_BitCnt == 8) //完成1个byte的接收
{
S_Master_BitCnt = 0;
S_Master_ByteCnt++; //接收的Byte数加1
S_Master_Step = Step_Write_Ack; //主机准备产生Ack信号
}
if(S_Master_ByteCnt==Master_Message.ReadSize) //主机完成接收(Master_Message.ReadSize:主机接收的长度)
{
S_Master_ByteCnt = 0;
F_Master_nAck = 1;
//接收数据完成,返回nAck信号
}
break;
5、发送ACK/nACK信号
①在SCL为低电平期间,主机切换SDA端口的电平,发送ACK信号或者nACK信号(9bit)。
②在SCL为高电平期间,若SDA端口输出低电平,继续接收下一帧数据,则将S_Master_Step的状态指向Step_Read_Data(接收数据)。若SDA端口输出高电平,则将S_Master_Step的状态指向Step_Stop(停止信号)。
在SCL为高电平期间
case Step_Write_Ack: //master send ack
Master_SDA_OUT //SDA端口切换为输出态
if(F_Master_Ack)
{
//此时主机以产生Ack信号,将SDA端口的状态切换为输入
Master_SDA_IN //上拉电阻的存在,SDA端口切换为输入态后,SDA线上的电平仍为高电平。
F_Master_Ack = 0;
S_Master_Step = Step_Read_Data; // 继续发送数据
}
if(F_Master_nAck)
{
S_Master_Step = Step_Stop; //产生停止信号
}
break;
在SCL为低电平期间
case Step_Write_Ack: //master send ack
if(F_Master_nAck) //产生nAck标志
{
F_Master_nAck = 0;
Master_SDA = 1; //nAck
}
else
{
Master_SDA = 0; //Ack
F_Master_Ack = 1;
}
break;
6、停止信号
if(S_Master_Step == Step_Stop)
{
//I2C结束通讯
Master_SCL_TIMDis //关闭定时器,不再为SCL产生CLK
Master_SDA_OUT //SDA端口设置为输出
Master_SCL = 1; //拉高SCL端口
Master_SDA = 1; //拉高SDA端口
}