系列文章目录
【STM32】| 01——常用外设 | USART
【STM32】| 02——常用外设 | I2C
失败了也挺可爱,成功了就超帅。 |
本文详细介绍 I2C协议及 MCU I2C配置使用
I2C是一种常用的串行通信总线,由串行数据线SDA 和串线时钟线SCL组成。I2C是一种多主机控制总线,由飞利浦公司为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I2C 通讯协议(Inter-Integrated Circuit)是由于它引脚少,硬件实现简单,可扩展性强,不需要外部收发设备,被广泛地使用在系统内多个集成电路(IC)间的通讯。
I2C支持 0KHZ-5MHZ设备通信(hz相当于bps)。有如下几种模式
普通模式 ——100kHz
快速模式——400kHz
快速模式——1MHz
高速模式——3.4MHz
超高速模式——5MHz
我们常用400KHZ 在此基础上 Inter提出了SMBUS系统总线管理 该规范限制了通信速率10K-100KHZ.
I2C是一种主从通信 支持多主多从的总线。
如图 可以看到索引 I2C 设备都通过 SDA/SCL 连接到总线 总线接有上拉电阻(后面讲原因)
I2C特性如下:
SCL和SDA都是双向的通过上拉电阻连接电源 总线在空闲时都输出高电平 总线具有 线与功能。
该协议约定了通信的起始、停止信号以及数据有效性、响应、仲裁同步、地址广播等。
起始信号:SCL高电平时,SDA由高电平转换为低电平
停止信号:SCL高电平时,SDA由低电平转换为高电平
起始信号和停止信号一般都是主机发出,当有起始信号时,总线就会处于被占用状态,当有停止信号时,总线处于空闲状态。
SDA数据线在 SCL 的每个时钟周期传输一位数据。传输时, SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL为低电平时, SDA 的数据无效,一般在这个时候 SDA 进行电平切换(数据位切换高/低),为下一位要传输的数据做好准备(即下一位要传1 SDA切换为高电平反之低平)。
SDA 线上的每个字节必须为 8 位 每次传输可以发送的字节数量不受限制 每个字节后必须跟一个响应位 首先传输的是数据的最高位 MSB 如果从机要完成一些其他功能后(例如一个内部中断服务程序) 才能接收或发送下一个完整的数据字节 可以使时钟线 SCL 保持低电平迫使主机进入等待状态 当从机准备好接收下一个数据字节并释放时钟线 SCL 后 数据传输继续。
MSB:代表高位
如图所示,为一个7位地址,I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS)来查找从机。 I2C 协议规定设备地址可以是 7 位或 10 位。
设备地址后面的一个数据位R/W用来表示数据传输方向,数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据
I2C的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。
数据传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,若 SDA 为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。
作为数据接收端时,当设备(主/从机)接收到 I2C 传输的一个字节数据或地址后,
若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下
一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接
收到该信号后会产生一个停止信号,结束信号传输。
总线上的设备可以抽象为节点。在多主通信中,总线上会有很多节点,它们都有自己的寻址地址,可以作为从节点被别的节点访问,同时它们都可以作为主节点向其他的节点发送控制字节和传送数据。但是如果有两个或两个以上的节点都向总线上发送请求时,这样就形成了冲突。要解决这种冲突,就要进行同步/仲裁,这就是I 2C总线上的同步/仲裁。
同步指:SCL同步
仲裁指:SDK仲裁
SCL同步是由于总线具有线“与”的逻辑功能。
1、只要有一个节点发送低电平时,总线上就表现为低电平。
2、当所有节点都发送高电平时,总线上就表现为高电平。
SDA线的仲裁也是建立在总线具有线“与”逻辑功能上的。节点在发送1位数据后,比较SDA线上所呈现的数据与自己发送的是否一致。是,继续发送;否则,退出竞争。SDA线的仲裁可以保证I2C总线系统在多个主节点同时企图控制总线时通信正常进行并且数据不丢失。总线系统通过仲裁只允许一个主节点可以继续占据总线。
上图是以两个节点为例的仲裁过程。DATA1和DATA2分别是主节点向总线所发送的数据信号,SDA为总线上所呈现的数据信号,SCL是总线上所呈现的时钟信号。
当主节点1、2同时发送起始信号时,两个主节点都发送了高电平信号。这时总线上呈现的信号为高电平,两个主节点都检测到总线上的信号与自己发送的信号相同,继续发送数据。
第2个时钟周期,2个主节点都发送低电平信号,在总线上呈现的信号为低电平,仍继续发送数据。
在第3个时钟周期,主节点1发送高电平信号,而主节点2发送低电平信号。根据总线的线“与”的逻辑功能,总线上的信号为低电平,这时主节点1检测到总线上的数据和自己所发送的数据不一样,就断开数据的输出级,转为从机接收状态。这样主节点2就赢得了总线,而且数据没有丢失,即总线的数据与主节点2所发送的数据一样,而主节点1在转为从节点后继续接收数据,同样也没有丢掉SDA线上的数据。因此在仲裁过程中数据没有丢失。
总结:SDA仲裁和SCL时钟同步处理过程没有先后关系,而是同时进行的。
下面通过主机到从机的读写操作进行介绍
主机产生起始信号后,所有从机就开始等待主机紧接下来 广播 的从机地址信号
(SLAVE_ADDRESS)。 在 I2C 总线上,每个设备的地址都是唯一的, 当主机广播的地址与
某个设备地址相同时,这个设备就被选中了,没被选中的设备将不回接受之后的数据信号。
根据 I2C 协议,从机地址可以是 7 位或 10 位。
在地址位之后,是(RW位)传输方向的选择位,该位为 0 时,表示后面的数据传输方向是由主机传输至从机,即主机向从机写数据。该位为 1 时,则相反,即主机由从机读数据。
从机接收到匹配的地址后,主机或从机会返回一个应答(ACK)或非应答(NACK)信号,
只有接收到应答信号后,主机才能继续发送或接收数据。
斜线:主机传输至从机 S :传输开始信号 SLAVE_ADDRESS: 从机地址
空白:从机传输至主机 A/A:应答(ACK)或非应答(NACK)信号
I2C主机设备 向一个具有7位地址的I2C从机设备写入N个字节数据的数据帧格式:主机先发送开始信号+7位地址+1位R/W位+响应位(从机响应ACK)继续传输+[N字节数据+从机ACK]
如果从机不想接收了 回应NACK 停止传输 主机发送停止信号
斜线:主机传输至从机 S :传输开始信号 SLAVE_ADDRESS: 从机地址
空白:从机传输至主机 A/A:应答(ACK)或非应答(NACK)信号 DATA数据
若数据方向位配置为“1读数据”方向, 即如图所示, 主机发送起始位+广播完地址,等待接收到从机应答信号后, 从机开始向主机返回数据(DATA),数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号(ACK),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输。
除了单独的读和写, I2C 通讯更常用的是复合格式,即如图所示,
该传输过程有两次起始信号(S)。一般在第一次传输中,主机通过 SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址(注意区分它与SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机,主机要读写从机的地址,第二次则是读写的实际内容。
10为地址用作扩展 大多数都是7位地址 暂时不详细说啦 后面遇到了在填充
各种信号MCU大差不差 这里以stm32说明
stm32 i2c外设它提供多主机功能,控制所有I2C总线特定的时序、协议、仲裁和定时。支持标准和快速两种模式,与SMBus 2.0兼容。具备状态错误检测标志及中断、可DMA等特点
详细功能、寄存器等描述 看参考手册
四种模式
● 从发送器模式
● 从接收器模式
● 主发送器模式
● 主接收器模式
一般我们i2c与其他设备通信 所以 I2C是一个工具 配置好I2C 发送/接收后 还需要根据从设备做一些具体操作
旧============================================~~
我这里有个 I2C接口的 温湿度传感器 AHT10 这里用这个演示
先配置单片机 I2C 读写
修改===========================================
这里用OLED来演示 I2C通信 AHT10不知道什么原因 发送设备地址不响应
硬件I2C指 MCU自带的I2C外设 有固定引脚
HAL库提供了很多接口 作为主机的收发 作为从机的收发 存储设备读写 以及对应的3种方式(阻塞、中断、DMA) 这里不全部介绍了 只说下用到的
I2C1 默认功能引脚是 PB6/7 它会自动配置我们不用管
从设备地址 根据从设备数据手册 看查 我这里AHT10传感器的 设备地址为 0X38
因为我们要让OLED显示我们想要的内容 所以要给OLED写命令/数据给它 OLED如何配置这些最后在说 暂时只演示 收发接口
这里我们用主机的收发API
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
可以看到信号正常发出去了 只是没有应答 如果设备地址正确 从设备会响应
设备地址正确 从设备响应 就可以通信了 可以看到写的数据也可以写进去了
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
和阻塞使用方法一样 只是调中断的接口
HAL_StatusTypeDef HAL_I2C_Master_Transmit_IT(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
如果传输完成调用发送完成回调函数
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
/* 如果传输完成会进入这里 */
if(hi2c1.Instance==I2C1)
{
}
}
HAL_StatusTypeDef HAL_I2C_Master_Receive_IT(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
如果接收完成调用接收完成回调函数
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
/* 如果接收完成会进入这里 */
if(hi2c1.Instance==I2C1)
{
}
}
和阻塞使用方法一样 只是调中断的接口
HAL_StatusTypeDef HAL_I2C_Master_Transmit_DMA(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
如果传输完成调用发送完成回调函数
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
/* 如果传输完成会进入这里 */
if(hi2c1.Instance==I2C1)
{
}
}
HAL_StatusTypeDef HAL_I2C_Master_Receive_DMA(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
如果接收完成调用接收完成回调函数
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
/* 如果接收完成会进入这里 */
if(hi2c1.Instance==I2C1)
{
}
}
接口很多大差不差
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout);
// 一般用作写EEPROM 存储设备方便
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef*hi2c,
uint16_t DevAddress,
uint16_t MemAddress,// 相当于从设备寄存器地址/命令
uint16_t MemAddSize,
uint8_t *pData, // 要写的数据
uint16_t Size,
uint32_t Timeout)
#define dev_addr 0x78
/* 第一个数据 比如从机的某个寄存器/命令 */
/* 第2/3个数据 要写入的数据 */
uint8_t data[3]={0x31,0x10,0x11};
/* 以下两个等价 */
HAL_I2C_Master_Transmit(&hi2c1,dev_addr,data,3,0xffff);
HAL_I2C_Mem_Write(&hi2c1,dev_addr,data[0],1,&data[1],2,0xffff);
使用IO口去模拟 I2C 不需要固定引脚 灵活
根据I2C协议 实现I2C读写功能
/* 1、定义引脚并配置IO模式 */
#define I2C_WR 0 /* 写控制bit */
#define I2C_RD 1 /* 读控制bit */
#define I2C_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define I2C_GPIO_PORT GPIOB
#define I2C_SCL_PIN GPIO_PIN_6
#define I2C_SDA_PIN GPIO_PIN_7
#define I2C_SCL_HIGH() HAL_GPIO_WritePin(I2C_GPIO_PORT,I2C_SCL_PIN,GPIO_PIN_SET) // 输出高电平
#define I2C_SCL_LOW() HAL_GPIO_WritePin(I2C_GPIO_PORT,I2C_SCL_PIN,GPIO_PIN_RESET) // 输出低电平
#define I2C_SDA_HIGH() HAL_GPIO_WritePin(I2C_GPIO_PORT,I2C_SDA_PIN,GPIO_PIN_SET) // 输出高电平
#define I2C_SDA_LOW() HAL_GPIO_WritePin(I2C_GPIO_PORT,I2C_SDA_PIN,GPIO_PIN_RESET) // 输出低电平
#define I2C_SDA_READ() HAL_GPIO_ReadPin(I2C_GPIO_PORT,I2C_SDA_PIN)
void sw_i2c_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
/* 打开GPIO时钟 */
I2C_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = I2C_SCL_PIN|I2C_SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct);
}
/* 2、I2C起始信号/停止信号 */
static void I2C_Delay(void)
{
uint8_t i;
for (i = 0; i < 10; i++);
}
void I2C_Start(void)
{
/* SCL高电平时 SDA由高变低 */
I2C_SDA_HIGH();
I2C_SCL_HIGH();
I2C_Delay();
I2C_SDA_LOW();
I2C_Delay();
I2C_SCL_LOW();
I2C_Delay();
}
void I2C_Stop(void)
{
/* SCL高电平时,SDA由低变高 */
I2C_SDA_LOW();
I2C_SCL_HIGH();
I2C_Delay();
I2C_SDA_HIGH();
}
/* 3、I2C应答信号/不应答/等待检测应答信号 */
void I2C_Ack(void)
{
I2C_SDA_LOW(); /* CPU驱动SDA = 0 */
I2C_Delay();
I2C_SCL_HIGH(); /* CPU产生1个时钟 */
I2C_Delay();
I2C_SCL_LOW();
I2C_Delay();
I2C_SDA_HIGH(); /* CPU释放SDA总线 */
}
void I2C_NAck(void)
{
I2C_SDA_HIGH(); /* CPU驱动SDA = 1 */
I2C_Delay();
I2C_SCL_HIGH(); /* CPU产生1个时钟 */
I2C_Delay();
I2C_SCL_LOW();
I2C_Delay();
}
uint8_t I2C_WaitAck(void)
{
uint8_t re;
I2C_SDA_HIGH(); /* CPU释放SDA总线 */
I2C_Delay();
I2C_SCL_HIGH(); /* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
I2C_Delay();
if (I2C_SDA_READ()) /* CPU读取SDA口线状态 */
{
re = 1;
}
else
{
re = 0;
}
I2C_SCL_LOW();
I2C_Delay();
return re;
}
/* 4、写/读一个字节数据 */
void I2C_Write_One_Byte(uint8_t Byte)
{
uint8_t i;
/* 先发送字节的高位bit7 */
for (i = 0; i < 8; i++)
{
if (Byte & 0x80)
{
I2C_SDA_HIGH();
}
else
{
I2C_SDA_LOW();
}
I2C_Delay();
I2C_SCL_HIGH();
I2C_Delay();
I2C_SCL_LOW();
if (i == 7)
{
I2C_SDA_HIGH(); // 释放总线
}
Byte <<= 1; /* 左移一个bit */
I2C_Delay();
}
}
uint8_t I2C_Read_One_Byte(void)
{
uint8_t i;
uint8_t value;
/* 读到第1个bit为数据的bit7 */
value = 0;
for (i = 0; i < 8; i++)
{
value <<= 1;
I2C_SCL_HIGH();
I2C_Delay();
if (I2C_SDA_READ())
{
value++;
}
I2C_SCL_LOW();
I2C_Delay();
}
return value;
}