内部集成电路(Inter Integrated circuit )的简称叫做IIC,是一种简单的、半双工同步通信的串行通信接口,IIC总线是上世纪80年代(1982年)由飞利浦公司设计出来,当时的目的是为了给MCU和外围芯片提供更简单的交互方式。
IIC总线只需要两根引脚就可以实现通信,一根是数据线SDA,另一根是时钟线SCL,所有通过IIC接口通信的外围器件都挂载在IIC总线上,通过这种机制就可以实现多机通信。
可以看到,外围器件的时钟线和数据线都是挂载在IIC总线(由主控芯片提供),并且在空闲状态下所有器件的时钟线SCL和数据线SDA都被总线的上拉电阻拉高,这样就可以把SDA引脚和SCL引脚设置为开漏模式即可,好处是防止短路。
每个挂载在IIC总线上的外围器件都有独立的器件地址,主机发送开始信号后,只需要发送想要通信的设备的地址,如果设备收到地址并且匹配正确,则开始进行单独通信。
IIC总线支持不同的通信速率,但是一般常用的标准速率100KHZ,但是有的外围器件可以支持高达400KHZ的通信速率,而由于IIC总线是半双工通信,所以同一时刻只能接收或者发送,也就是说,IIC总线一般是为了控制,不适合作为大量数据传输的接口。
接口可以下述4种模式中的一种运行:
默认状态下工作于从模式 。 接口在生成起始条件后自动地从从模式切换到主模式 (谁先发送开始信号,谁就作为主机)。当仲裁丢失或产生停止信号时,则从主模式切换到从模式,从而实现多主模式功能。
通信流
可以看到,在建立通信的时候主机需要发送 开始信号 ,紧接着主机需要发出从器件的 设备地址 (7bit+1bit),从设备的物理地址是7bit,但是由于只有一根数据线,就需要说清楚数据的传输方向,数据的传输方向通过从设备的地址最低位进行表示(最低位是0,表示写操作,最低位是1,表示读操作),IIC总线提供了 应答机制 ,也就是说从机收到了1个字节的数据之后,会在第九个脉冲发送给主机一个应答信号(1bit),如果主机收到从机的应答信号,则主机可以继续发送数据,反之,如果主机没有收到从机发送的应答信号,那主机就不应该继续发送数据,而是应该主动发出一个 停止信号 ,表示停止通信。
// ---------- software_iic.h ----------
#ifndef __SOFTWARE_IIC_H__
#define __SOFTWARE_IIC_H__
#include "main.h"
#include "tim.h"
#include "gpio.h"
#define DLY_TIM_Handle (&htim1)
// SCL: PB10, SDA: PB11
#define IIC_SCL_PORT GPIOB
#define IIC_SCL_PIN GPIO_PIN_10
#define IIC_SDA_PORT GPIOB
#define IIC_SDA_PIN GPIO_PIN_11
#define IIC_SDA_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define IIC_SCL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define IIC_SCL_WRITE_UP() HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET)
#define IIC_SCL_WRITE_DOWN() HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_RESET)
#define IIC_SDA_WRITE_UP() HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_SET)
#define IIC_SDA_WRITE_DOWN() HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_RESET)
#define IIC_SDA_READ() HAL_GPIO_ReadPin(IIC_SDA_PORT, IIC_SDA_PIN)
void delay_us(uint16_t nus);
void IIC_Init(void);
void IIC_SDA_OutputMode(void);
void IIC_SDA_InputMode(void);
void IIC_StartSignal(void);
void IIC_StopSignal(void);
void IIC_SendBytes(uint8_t data);
uint8_t IIC_ReadBytes(void);
uint8_t IIC_WaitACK(void);
void IIC_MasterACK(uint8_t ack);
#endif
// ---------- software_iic.c ----------
void IIC_Init(void)
{
// 初始化SCL和SDA为开漏输出
GPIO_InitTypeDef GPIO_InitStruct = {0};
IIC_SDA_GPIO_CLK_ENABLE();
IIC_SCL_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = IIC_SCL_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(IIC_SCL_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = IIC_SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(IIC_SDA_PORT, &GPIO_InitStruct);
// 初始化SCL和SDA为高电平
IIC_SCL_WRITE_UP();
IIC_SDA_WRITE_UP();
}
// SDA输出模式
void IIC_SDA_OutputMode(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
IIC_SDA_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = IIC_SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(IIC_SDA_PORT, &GPIO_InitStruct);
}
// SDA输入模式
void IIC_SDA_InputMode(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
IIC_SDA_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = IIC_SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(IIC_SDA_PORT, &GPIO_InitStruct);
}
开始信号由主机发出,表示打算和所有的从器件进行通信,IIC总线规定在SCL时钟线保持高电平期间,把SDA数据线拉低,表示开始信号。
// IIC开始信号
void IIC_StartSignal(void)
{
IIC_SDA_OutputMode(); // 设置SDA为输出模式
// 确保SCL和SDA都是高电平
IIC_SCL_WRITE_UP();
IIC_SDA_WRITE_UP();
// 拉低SDA,产生一个下降沿
// 一般常用的IIC总线标准速率为100kHz,即每个时钟周期为10us,故SDA低电平应持续5us
IIC_SDA_WRITE_DOWN(); // SDA拉低
delay_us(6); // 为了保证兼容性,这里延时6us
// 拉低SCL,表示准备通信
IIC_SCL_WRITE_DOWN(); // SCL拉低
}
如何实现微秒级的延时可以参考下文
STM32基于HAL库实现微秒延时
2.4 停止信号
停止信号由主机发出,表示不打算和从器件继续通信,IIC总线规定在SCL时钟线保持高电平期间,把SDA数据线拉高,表示停止信号。
// IIC停止信号
void IIC_StopSignal(void)
{
IIC_SDA_OutputMode(); // 设置SDA为输出模式
// 确保SCL和SDA都是低电平
IIC_SCL_WRITE_DOWN();
IIC_SDA_WRITE_DOWN();
// 拉高SCL,产生一个上升沿
// 一般常用的IIC总线标准速率为100kHz,即每个时钟周期为10us,故SCL高电平应持续5us
IIC_SCL_WRITE_UP();
delay_us(5);
IIC_SDA_WRITE_UP(); // 拉高SDA,表示通信结束
delay_us(5); // 确保SDA的电平可以被其他器件检测到
}
在主机发送开始信号后,就可以发送数据或者地址,IIC总线规定数据的收发都是 MSB (高位先出),由于只有一个数据线,所以IIC采用串行方式把数据的每个bit位发出去。
由于SCL提供的脉冲周期是有规律的,所以IIC总线规定只能在SCL脉冲周期的高电平期间进行数据的读取或者写入,在SCL脉冲周期的低电平期间可以进行数据的修改。
// 主机发送数据
void IIC_SendBytes(uint8_t Data)
{
uint8_t i = 0;
IIC_SDA_OutputMode(); // 设置SDA为输出模式
// 确保SCL和SDA都是低电平
IIC_SCL_WRITE_DOWN();
IIC_SDA_WRITE_DOWN();
// 开始发送8位数据
for (i = 0; i < 8; i++)
{
// SCL低电平期间主机准备数据
if (Data & (1 < < (7 - i))) // 判断数据的第7-i位是否为1
{
IIC_SDA_WRITE_UP(); // 如果为1,SDA拉高
}
else
{
IIC_SDA_WRITE_DOWN();// 如果为0,SDA拉低
}
delay_us(5); // 至此,数据准备完毕
// 拉高SCL,主机发送数据
IIC_SCL_WRITE_UP();
delay_us(5); // 至此,数据发送完毕
// 拉低SCL,准备发送下一个数据
IIC_SCL_WRITE_DOWN();
delay_us(5);
}
}
在主机发送开始信号后,就可以发送数据或者地址,IIC总线规定数据的收发都是MSB(高位先出),由于只有一个数据线,所以IIC采用串行方式把数据的每个bit位发出去。
由于SCL提供的脉冲周期是有规律的,所以IIC总线规定只能在SCL脉冲周期的高电平期间进行数据的读取或者写入,在SCL脉冲周期的低电平期间可以进行数据的修改。
// 主机接收数据
uint8_t IIC_ReadBytes(void)
{
uint8_t i = 0;
uint8_t Data = 0; // 用于存储接收到的数据
IIC_SDA_InputMode(); // 设置SDA为输入模式
IIC_SCL_WRITE_DOWN(); // 确保SCL为低电平
// 开始接收8位数据
for (i = 0; i < 8; i++)
{
// 拉高SCL,主机准备接收数据
IIC_SCL_WRITE_UP();
delay_us(5); // 至此,从机数据准备完毕,主机开始接收
if (IIC_SDA_READ() == 1) // 主机收到1
{
Data |= (1 < < (7 - i)); // 将收到的1存储到Data的第7-i位
}
/* 由于Data初始化为0000 0000,所以不需要else语句
else // 收到0
{
Data &= ~(1 < < (7 - i)); // 将收到的0存储到Data的第7-i位
}
*/
// 拉低SCL,主机准备接收下一个数据
IIC_SCL_WRITE_DOWN();
delay_us(5);
}
return Data; // 返回接收到的数据
}
IIC总线增加了应答机制,在主机发送一个字节数据之后,从机在第9个脉冲周期进行应答,如果SDA为0,则表示应答,如果SDA=1,则表示无应答,如果从机没有应答,则主机应该发送停止信号,表示停止通信。这里分为两种情况:
第一种:主机发送数据,从机进行应答
// 主机发送数据,从机进行应答
uint8_t IIC_WaitACK(void)
{
uint8_t ack;
IIC_SDA_InputMode(); // 设置SDA为输入模式
IIC_SCL_WRITE_DOWN(); // 确保SCL是低电平
delay_us(5);
IIC_SCL_WRITE_UP(); // 拉高SCL,主机准备接收从机的应答信号
delay_us(5); // 至此,从机应答信号准备完毕,主机开始接收
// 如果从机应答信号为0,表示从机接收到数据
if (IIC_SDA_READ() == 0)
{
ack = 0;
}
else // 如果从机应答信号为1,表示从机没有接收到数据
{
ack = 1;
}
IIC_SCL_WRITE_DOWN(); // 拉低SCL,主机忽略数据
delay_us(5);
return ack; // 返回从机的应答信号
}
第二种:从机发送数据,主机进行应答
// 从机发送数据,主机进行应答,0表示应答,1表示不应答
void IIC_MasterACK(uint8_t ack)
{
IIC_SDA_OutputMode(); // 设置SDA为输出模式
// 确保SCL和SDA都是低电平
IIC_SCL_WRITE_DOWN();
IIC_SDA_WRITE_DOWN();
if (ack == 0) // 如果ack为0,表示主机应答
{
IIC_SDA_WRITE_DOWN(); // SDA拉低
}
else // 如果ack为1,表示主机不应答
{
IIC_SDA_WRITE_UP(); // SDA拉高
}
delay_us(5); // 至此,应答信号准备完毕
// 拉高SCL,主机发出应答信号
IIC_SCL_WRITE_UP();
delay_us(5); // 至此,应答信号发送完毕
// 拉低SCL,从机忽略数据
IIC_SCL_WRITE_DOWN();
delay_us(5);
}
全部0条评论
快来发表一下你的评论吧 !