一、IIC简介
IIC(Inter-Integrated Circuit)是一种串行通信协议,也被称为I2C(Inter-IC),它是Philips公司开发的一种多主机、多从机的协议。它可以实现在很短的距离内由多个芯片组成的系统中的彼此之间的高速串行通信,用于连接芯片和外设,如温度传感器、电子罗盘、LCD驱动器等。
IIC通信采用两根线来进行数据传输,分别为串行数据线SDA(Serial Data Line)和串行时钟线SCL(Serial Clock Line),其中SDA传输数据,SCL产生时钟信号来控制数据传输的时序。
IIC通信采用主从模式,一个主控器可以控制多个从设备,而从设备通常只能在主控器的控制下进行数据传输。主控制器负责发送起始信号、控制总线、发送地址和数据等操作,从设备负责接收和处理数据。通信时,主控制器通过设备地址来指定与哪一个从设备进行数据交互。
IIC通信具有传输速度快、线路简单、适用于同轴电缆等优点,常用于短距离的低速数据传输。
二、通讯规则介绍
IIC通讯的基本规则如下:
1. 结束信号:由主机拉低SDA线,在SCL线高电平时,SDA线由低电平转为高电平,产生起始信号。
2. 起始信号:由主机拉高SDA线,在SCL线高电平时,SDA线由高电平转为低电平,产生结束信号。
3. 数据传输:数据按位传输,先传输高位,每个数据位的传输由SCL决定。在SCL线高电平时,SDA线上发送的数据将被视为有效数据。
4. 地址帧:每个IIC设备都有一个唯一的7位地址。在地址帧中,主机发出设备地址,并指出是读操作还是写操作。
5. 读取确认:在发送完每个字节后,接收方必须发送一个确认位,以告知发送方是否已成功接收。
6. 忙确认:如果接收方无法接收字节,则发送无应答信号,表示接收设备忙。
7. 重复起始信号:除了结束信号外,主机还可以在没有停止信号的情况下产生起始信号。
8. 时钟同步:主机产生的时钟信号必须同步于SCL线的电平变化,因为在SCL高电平时传输的状态数据可以被接收,而在SCL低电平时传输的指令则可被接收。
总之,IIC通信需要主控制器和从设备之间遵守一定的规则和时序,以确保数据传输的正确性和稳定性。
三、时序图分析
四、代码分析
// 需要替换的函数
GPIO_WriteBit(); // 引脚电平写入函数
GPIO_ReadInputDataBit(); // 读引脚电平函数
Delay_us(10); // 延迟函数
/**
* @brief 拉高拉低SCL
* @param {uint8_t} BitValue 0/1
* @return {*}
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
Delay_us(10);
}
/**
* @brief : 拉高拉低SDA
* @param {uint8_t} BitValue 0/1
* @return {*}
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
Delay_us(10);
}
/**
* @brief : 读SDA电平
* @return {*}
*/
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
void MyI2C_Init(void)
{
/*使能外设时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
/*结构体配置IIC引脚*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); // 拉高引脚电平
}
/********************************************************************************************************************************************
修改上面的函数就行下面的都是封装好的
IIC通讯原则 发送起始信号 发送一个字节 接收应答标志 发送下一个字节 接收应答标志 发送停止标志
*******************************************************************************************************************************************/
/**
* @brief 发送开始标志
* @return {*}
*/
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
MyI2C_W_SDA(0);
MyI2C_W_SCL(0);
}
/**
* @brief 发送停止标志
* @return {*}
*/
void MyI2C_Stop(void)
{
/*SCL高电平期间SDA由低变高*/
MyI2C_W_SDA(0);
MyI2C_W_SCL(1);
MyI2C_W_SDA(1);
}
/**
* @brief 起始位后SCL为低 可以放数据在SDA上,随后拉高SCL从机读取主机发送的数据,主机在拉低SCL进入下一位的发送
* @param {uint8_t} Byte
* @return {*}
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
MyI2C_W_SDA(Byte & (0x80 >> i));
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
/**
* @brief 发送buffer
* @param {uint8_t} Byte
* @return {*}
*/
void MyI2C_SendBuffer(uint8_t *Buffer, uint32_t len)
{
MyI2C_Start();
for (int i = 0; i < len; i++)
{
MyI2C_SendByte(Buffer[i]);
MyI2C_ReceiveAck();
}
MyI2C_Stop();
}
/**
* @brief : 接收指定从机寄存器地址的buffer
* @param {uint8_t} I2C_Slave_Addr 从机地址
* @param {uint8_t} AR_Addr 寄存器地址
* @param {uint8_t} *Buffer 数据
* @param {uint32_t} len 数据长度
* @return {*}
*/
void MyI2C_RecvBuffer(uint8_t I2C_Slave_Addr, uint8_t AR_Addr, uint8_t *Buffer, uint32_t len)
{
MyI2C_Start();
MyI2C_SendByte(I2C_Slave_Addr);
MyI2C_ReceiveAck();
MyI2C_SendByte(AR_Addr);
MyI2C_ReceiveAck();
MyI2C_Start();
MyI2C_SendByte(I2C_Slave_Addr | 0x01);
MyI2C_ReceiveAck();
for (int i = 0; i < len; i++)
{
Buffer[i] = MyI2C_ReceiveByte();
if (i == len - 1)
{
MyI2C_SendAck(1);
}
else
{
MyI2C_SendAck(0);
}
}
MyI2C_Stop();
}
/**
* @brief 主机先拉高SDA是释放总线的意思,SCL此时为低 ,从机可以 放数据在SDA上,随后主机拉高SCL读取SDA的值在拉低SCL进入下一个周期
* @param {uint8_t} Byte
* @return {*}
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;
MyI2C_W_SDA(1);
for (i = 0; i < 8; i++)
{
MyI2C_W_SCL(1);
if (MyI2C_R_SDA() == 1)
{
Byte |= (0x80 >> i);
}
MyI2C_W_SCL(0);
}
return Byte;
}
/**
* @brief 发送应答位
* @param {uint8_t} Byte 0应带继续接收,1不应答
* @return {*}
*/
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
/**
* @brief 发送应答位
* @param {uint8_t} Byte 0应带继续接收,1不应答
* @return {*}
*/
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;
}
所有函数的声明
void MyI2C_W_SCL(uint8_t BitValue);
void MyI2C_W_SDA(uint8_t BitValue);
uint8_t MyI2C_R_SDA(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
void MyI2C_SendBuffer(uint8_t *Buffer, uint32_t len);
void MyI2C_RecvBuffer(uint8_t I2C_Slave_Addr, uint8_t AR_Addr, uint8_t *Buffer, uint32_t len);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);
五、总结
代码有些细节不够完善,可以根据大体思想自行完善。代码的优势就是,任意IO配置成开漏输出就行,不需要借助硬件帮忙,可移植性强。