一、I2C协议简介
I2C通讯协议 (Inter-Integrated Circuit,读作I平方C、I方C) 是由Phiilps公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要USART、CAN等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路间的通讯。
1. 物理层
(感谢野火的PPT,一部分内容参考了野火)如下图所示即为I2C的物理层:
下面来简要介绍物理层需要了解的知识点:
- 总线: 多个设备共用的信号线,有两条:一条双向串行数据线 (SDA),一条串行时钟线 (SCL)。
- 主机和从机: 总线上挂载着多个通讯主机和通讯从机,每个连接到总线的从机设备都有独一无二的通讯地址,主机通过这些地址对从机设备进行访问。一般来说,总线上挂载着五六个从机和一个主机就够用了。
- 通讯: 当主机与从机正在进行通讯的时候,从机设备输出低电平,将总线拉成低电平,其他设备输出高阻态,不能参与通讯。
- 上拉电阻: 两条总线均通过上拉电阻连接到电源。当所有从机设备空闲时,这些设备会输出高阻态,由上拉电阻把总线拉成高电平,这些设备就相当于与总线断开了。
如果不是用高阻态表示高电平而是用接地表示,那么当一个设备通讯时,这个设备接电源,整个总线通过上拉电阻也接了电源,其他未通讯设备接地,就把其它设备短路掉了。
- 仲裁: 当有多个设备想跟主机通讯时,为防止数据冲突,会采用仲裁的方式(类似DMA)决定由哪个设备占用总线。在通讯分点已经说明了一次通讯只能有一个主机和一个从机。
- 传输模式: 标准模式传输速率为 100kbit/s,快速模式为 400kbit/s,高速模式下可达 3.4Mbit/s,但目前大多I2C设备尚不支持高速模式,一般情况下快速模式就已经够了。
- 注意: 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。
2. 协议层
这个部分的内容相当重要,它是我们后面写代码的基础。
(1)主机写数据到从机
一个数据包的组成如下:
- 图例: 条纹框:数据由主机传输到从机;白色框:数据由从机传输到主机。
- S(传输开始信号): 主机向总线广播,说,我要开始通讯了!至于向谁通讯呢?待会再说。
- SLAVE_ADDRESS(从机地址): 主机现在说,我要访问这个地址所在的设备!I2C总线上的每个设备都有自己的独立地址,主机发起通讯时,通过SDA信号线发送设备地址(SLAVE_ADDRESS)来查找从机。设备地址可以是7位或10位。
- R/W(读写位): 接下来主机表示,我的访问操作是向这个设备写入数据,R/W置为0,表示我要写入!这个设备,你听到了吗?听到请回答!因此实际上主机发送了一个位数为8或11的数据。
- A(应答位): 设备发送应答信号(ACK,Acknowledged)给主机,表示,主机我收到了,我可以工作,你说!
- DATA(数据): 主机说,那好我们开始吧!开始发送数据,一共n字节。发完以后,主机问,设备你收到了吗?设备说,收到(ACK)!现在写入数据,并发送一个应答信号给主机。如此往复,便实现了写的过程。
- A(应答位): 最后一次数据写入后,设备说,够了够了,不要再写了,并发送一个非应答信号(NACK,Not Acknowledged)给主机。
- P(停止位): 主机发送一位停止位告诉设备,我停止访问了!于是设备重新拉回高阻态。
(2)主机由从机读数据
一个数据包的组成如下:
- 图例: 条纹框:数据由主机传输到从机;白色框:数据由从机传输到主机。
- S(传输开始信号): 主机向总线广播,说,我要开始通讯了!至于向谁通讯呢?待会再说。
- SLAVE_ADDRESS(从机地址): 主机现在说,我要访问这个地址所在的设备!这个地址是7位或10位长。
- R/W(读写位): 接下来主机表示,我的访问操作是向这个设备写入数据,R/W置为1,表示我要读数据!这个设备,你听到了吗?听到请回答!因此实际上主机发送了一个位数为8或11的数据。
- A(应答位): 设备发送应答信号(ACK,Acknowledged)给主机,表示,主机我收到了,我可以工作了!
- DATA(数据): 设备接着开始发送数据给主机,一共n字节。发完以后,设备问,主机你收到了吗?主机说,收到(ACK)!如此往复,便实现了读的过程。
- A(应答位): 最后一次读数据后,主机说,我不想再读了,并发送一个非应答信号(NACK,Not Acknowledged)给设备。
- P(停止位): 主机发送一位停止位告诉设备,我停止访问了!于是设备重新拉回高阻态。
(3)读和写交替进行
这个就是以上读和写的复合格式,关键在于主机的读写位,主机想要读就用读的格式,主机想要写就用写的格式。
(4)信号和时钟的配合
通讯起始和停止
- 当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。
- 当 SCL 是高电平时 SDA 线由低电平向高电平切换,表示通讯的停止。
- 起始和停止信号一般由主机产生。
数据有效性
SDA数据线在SCL的每个时钟周期传输一位数据。
- 当SCL为高电平时,SDA表示的数据有效,即此时的SDA为稳定高电平时表示数据“1”,为稳定低电平时表示数据“0”。
- 当SCL为低电平时,SDA的数据无效,一般在这个时候SDA趁机(你可以这么理解)进行电平切换,为下一次表示数据做好准备。
响应
传输时主机产生时钟,在第9个时钟时,数据发送端会释放SDA的控制权,由数据接收端控制SDA,若SDA为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。
二、STM32中的I2C总线
首先声明一下,STM32的硬件I2C是有缺陷的,因此我们基本都是用软件模拟I2C! 但是下面这些内容还是要了解一下,初学不必深入。
1. I2C框图
本部分可参考STM32中文参考手册第24章I2C接口。
下面将框图分为四个部分做简要介绍:
(1)通讯引脚
STM32芯片有两个I2C外设,它们的I2C通讯信号引出到不同的GPIO引脚上,使用时必须配置到这些指定的引脚。
SCL、SDA引脚和编号对应关系如下(可参考电路原理图):
引脚 | I2C1 | I2C2 |
---|---|---|
SCL | PB5(默认)/PB8(重映射) | PB10 |
SDA | PB6(默认)/PB9(重映射) | PB11 |
需要注意的是:在 SMBus 模式下, SMBALERT 是可选信号。如果禁止了 SMBus ,则不能使用该信号。
SMBus (System Management Bus,系统管理总线) 是1995年由Intel提出的,应用于移动PC和桌面PC系统中的低速率通讯。希望通过一条廉价并且功能强大的总线(由两条线组成),来控制主板上的设备并收集相应的信息。SMBus 为系统和电源管理这样的任务提供了一条控制总线,使用 SMBus 的系统,设备之间发送和接收消息都是通过 SMBus,而不是使用单独的控制线,这样可以节省设备的管脚数。
(2)时钟控制逻辑
本部分可参考STM32中文参考手册24.6.8时钟控制寄存器(I2C_CCR)。
I2C时钟是由时钟控制寄存器控制的。寄存器位15可设置标准模式或快速模式,位14可设置快速模式下的占空比(THIGH/TLOW)。位11-0设置SCL时钟的配置因子CCR。CCR的计算过程了解即可(PCLK1 = APB1,默认36MHz):
(3)数据控制逻辑
SDA信号主要连接到数据移位寄存器上,数据移位寄存器的数据
来源及目标是数据寄存器(DR)、地址寄存器(OAR)、PEC寄存器以及SDA数据线。(感觉与USART工作原理比较类似,也是一位一位的发送、接收。)
(4)整体控制逻辑
本部分可参考STM32中文参考手册24.6.1 控制寄存器 1(I2C_CR1)和24.6.2 控制寄存器 2(I2C_CR2)以及24.6.6 状态寄存器 1(I2C_SR1)和24.6.7 状态寄存器 2 (I2C_SR2)。
整体控制逻辑负责协调整个I2C外设,控制逻辑的工作模式根据我们配置的控制寄存器(CR1/CR2)的参数而改变。在外设工作时,控制逻辑会根据外设的工作状态修改状态寄存器(SR1
和SR2),只要读取这些寄存器相关的寄存器位,就可以了解I2C的工作状态。需要注意的寄存器几个位:
CR1寄存器:位10(ACK)、位9(STOP)、位8(START)。
SR1寄存器:位7(TxE:数据寄存器为空(发送时) (Data register empty (transmitters)) )、位6(RxNE:数据寄存器非空(接收时) (Data register not empty (receivers)) )、位1(ADDR:地址已被发送(主模式)/地址匹配(从模式) (Address sent (master mode)/matched
(slave mode)))、位0(SB:起始位(主模式) (Start bit (Master mode)) )。
SR2寄存器:位2(TRA:发送/接收 (Transmitter/receiver) )、位1(BUSY:总线忙 (Bus busy))。
2. STM32的I2C通讯过程
本部分可参考STM32中文参考手册24.3.3 I2C主模式,同时文字内容借鉴了野火PPT。本部分比较重要,也是我们写I2C代码的基础。
(1)主发送器通讯过程
如图所示,上面一行是控制寄存器要发送接收的内容,下面一行是状态寄存器标志的内容,库函数就是通过检测这些状态寄存器来判断这些事件是否已完成。
- 当发生起始信号(S)后,产生事件EV5,并会对SR1寄存器的SB位置1,表示起始信号已经发送。
- 发送设备地址并等待应答信号,若有从机应答,则产生事件EV6及EV8,这时SR1寄存器的ADDR位及TXE位置1,ADDR为1表示地址已经发送,TXE为1表示数据寄存器为空。
- 往DR寄存器写入要发送的数据,这时SR1寄存器的TXE位置0,表示数据寄存器非空,I2C外设通过SDA信号线一位位把数据发送出去后,又会产生EV8事件,即TXE位置1,重复这个过程,可以发送多个字节数据。
- 发送数据完成后,控制I2C设备产生一个停止信号(P),这个时候会产生EV2事件,SR1的TXE位及BTF位都置1,表示通讯结束。
(2)主接收器通讯过程
如图所示,上面一行是控制寄存器要发送接收的内容,下面一行是状态寄存器标志的内容,库函数就是通过检测这些状态寄存器来判断这些事件是否已完成。
- 起始信号(S)是由主机端产生的,控制发生起始信号后,产生事件EV5,并会对SR1寄存器的SB位置1,表示起始信号已经发送。
- 发送设备地址并等待应答信号,若有从机应答,则产生事件EV6,这时SR1寄存器的ADDR位置1,表示地址已经发送。
- 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生EV7事件,SR1寄存器的RXNE置1,表示接收数据寄存器非空,读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时可以控制I2C发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输。
- 发送非应答信号后,产生停止信号(P),结束传输。
3. I2C的结构体定义和库函数
位于头文件stm32f10x_i2c.h,结构体定义如下:
typedef struct
{
uint32_t I2C_ClockSpeed; /*!< 设置SCL时钟频率*/
uint16_t I2C_Mode; /*!< 设置工作模式*/
uint16_t I2C_DutyCycle; /*!< 设置时钟占空比*/
uint16_t I2C_OwnAddress1; /*!< 指定自身I2C设备地址 */
uint16_t I2C_Ack; /*!< 响应使能或关闭响应使能*/
uint16_t I2C_AcknowledgedAddress; /*!< 指定地址长度(7或10位)*/
}I2C_InitTypeDef;
这里着重说明一下I2C_OwnAddress1
,这个是配置I2C设备自己的地址,对于STM32主机设备,可以不用关心这个地址位,但是如果是两个MCU进行通讯的话,是必须要进行配置的。这个地址是7位还是10位,取决于I2C_AcknowledgedAddress
,只有它设置为10位模式,I2C_OwnAddress1
才能使用10位地址。
部分常用库函数如下:
//初始化
void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
//产生起始条件、终止条件、使能应答、设置设备的第二个地址
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);
//发送数据、接收数据、发送地址、读取I2C的寄存器
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
uint16_t I2C_ReadRegister(I2C_TypeDef* I2Cx, uint8_t I2C_Register);
//清除标志位、获得标志位(重要)、标志位中断
void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
配置流程如下:
#ifndef __I2C_H
#define __I2C_H
#include "stm32f10x.h"
/****** 选择使用:I2C1 ******/
/****** 宏定义区 ******/
#define I2C_CLK_SPEED 400000
/* STM32设备地址可随意定义,只要与EEPROM地址不重合即可 */
#define STM32_ADDR 0x5F
#define EEPROM_ADDR 0xA0
#define SCL_PORT_CLK RCC_APB2Periph_GPIOB
#define SDA_PORT_CLK RCC_APB2Periph_GPIOB
#define I2Cx_CLK RCC_APB1Periph_I2C1
#define I2Cx I2C1
#define SCL_GPIO_PORT GPIOB
#define SDA_GPIO_PORT GPIOB
#define SCL_GPIO_PIN GPIO_Pin_6
#define SDA_GPIO_PIN GPIO_Pin_7
/****** 函数声明区 ******/
void I2C_Config(void);
#endif /* __I2C_H */
/**
* @brief I2C初始化配置
* @param 无
* @retval 无
*/
void I2C_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
I2C_InitTypeDef I2C_InitStructure;
/* 开启SCL和SDA时钟 */
RCC_APB1PeriphClockCmd(SCL_PORT_CLK | SDA_PORT_CLK, ENABLE);
RCC_APB1PeriphClockCmd(I2Cx_CLK, ENABLE);
/* 配置SCL对应的GPIO */
GPIO_InitStructure.GPIO_Pin = SCL_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SCL_GPIO_PORT, &GPIO_InitStructure);
/* 配置SDA对应的GPIO */
GPIO_InitStructure.GPIO_Pin = SDA_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SDA_GPIO_PORT, &GPIO_InitStructure);
/* 配置I2C */
I2C_InitStructure.I2C_ClockSpeed = I2C_CLK_SPEED;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = STM32_ADDR;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_Init(I2Cx, &I2C_InitStructure);
I2C_Cmd(I2Cx, ENABLE);
}
未完待续···