13. I2C通信协议
一、I2C通信协议简介
I2C(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接微控制器以及其外围设备。它是由数据线 SDA 和时钟线 SCL 构成的串行总线,可发送和接收数据,在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送。
I2C 总线有如下特点:
- 总线由数据线 SDA 和时钟线 SCL 构成的串行总线,数据线用来传输数据,时钟线用来同步数据收发。
- 总线上每一个器件都有一个唯一的地址识别,所以我们只需要知道器件的地址,根据时序就可以实现微控制器与器件之间的通信。
- 数据线 SDA 和时钟线 SCL 都是双向线路,都通过一个电流源或上拉电阻连接到正的电压,所以当总线空闲的时候,这两条线路都是高电平。
- 总线上数据的传输速率在标准模式下可达 100kbit/s 在快速模式下可达 400kbit/s,在高速模式下可达 3.4Mbit/s。
- 总线支持设备连接。在使用 I2C 通信总线时,可以有多个具备 I2C 通信能力的设备挂载在上面,同时支持多个主机和多个从机,连接到总线的接口数量只由总线电容 400pF 的限制决定。
I2C 总线挂载多个器件的示意图,如下图所示:
二、I2C总线时序图
#define I2C_SCL_GPIO_PORT GPIOB
#define I2C_SCL_GPIO_PIN GPIO_PIN_8
#define I2C_SCL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define I2C_SDA_GPIO_PORT GPIOB
#define I2C_SDA_GPIO_PIN GPIO_PIN_9
#define I2C_SDA_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define I2C_SCL(x) do{ x ? \
HAL_GPIO_WritePin(I2C_SCL_GPIO_PORT,I2C_SCL_GPIO_PIN, GPIO_PIN_SET):\
HAL_GPIO_WritePin(I2C_SCL_GPIO_PORT,I2C_SCL_GPIO_PIN, GPIO_PIN_RESET);\
}while(0)
#define I2C_SDA(x) do{ x ? \
HAL_GPIO_WritePin(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN, GPIO_PIN_SET):\
HAL_GPIO_WritePin(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN, GPIO_PIN_RESET);\
}while(0)
#define I2C_READ_SDA() HAL_GPIO_ReadPin(I2C_SDA_GPIO_PORT, I2C_SDA_GPIO_PIN)
#define I2C_DELAY() Delay_us(10)
通过宏定义标识符的方式去定义 SCL 和 SDA 两个引脚,同时通过宏定义的方式定义了 I2C_SCL()
和 I2C_SDA()
设置这两个管脚可以输出 0 或者 1,主要还是通过 HAL 库的 GPIO 操作函数实现的。另外方便在 I2C 操作函数中调用读取 SDA 管脚的数据,这里直接宏定义 I2C_READ_SDA
实现。
/**
* @brief 模拟I2C初始化函数
*
*/
void I2C_Simulate_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能I2C的SCL和SDA引脚对应的GPIO时钟
I2C_SCL_GPIO_CLK_ENABLE();
I2C_SDA_GPIO_CLK_ENABLE();
GPIO_InitStruct.Pin = I2C_SCL_GPIO_PIN; // I2C SCL引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出模式
GPIO_InitStruct.Pull = GPIO_PULLUP; // 使用上拉电阻
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 输出速度
HAL_GPIO_Init(I2C_SCL_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = I2C_SDA_GPIO_PIN; // I2C SCL引脚
HAL_GPIO_Init(I2C_SDA_GPIO_PORT, &GPIO_InitStruct);
// 空闲时,I2C总线SCL为高电平,I2C SDA为高电平
I2C_SCL(1);
I2C_SDA(1);
}
开漏模式具有线与特性,即如果有很多开漏模式的引脚连在一起的时候,只有当所有引脚都输出高阻态,电平才为 1,只要有其中一个为低电平时,就等于接地,使得整条线路都为低电平 0。
另外在开漏输出模式下,施密特触发器是打开的,所以 IO 口引脚的电平状态会被采集到输入数据寄存器中,如果对输入数据寄存器进行读访问可以得到 IO 口的状态。也就是说开漏输出模式下,我们可以对 IO 口进行读数据。
①、起始信号
当 SCL 为高电平期间,SDA 由高到低的跳变。起始信号是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传输。
/**
* @brief I2C产生起始信号函数
*
* @note SCL为高电平期间,SDA从高电平往低电平跳变
*/
void I2C_Simulate_Start(void)
{
// 1、释放SDA和SCL,并延迟,空闲状态
I2C_SDA(1);
I2C_SCL(1);
I2C_DELAY();
// 2、拉低SDA,SDA产生下降沿,并延迟
I2C_SDA(0);
I2C_DELAY();
// 3、钳住SCL总线,准备发送数据/接收数据,并延时
I2C_SCL(0);
I2C_DELAY();
}
②、停止信号
当 SCL 为高电平期间,SDA 由低到高的跳变。停止信号也是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
/**
* @brief I2C产生结束信号函数
*
* @note SCL为高电平期间,SDA从低电平往高电平跳变
*/
void I2C_Simulate_Stop(void)
{
// 1、SDA拉低,SCL拉高,并延迟
I2C_SDA(0);
I2C_SCL(1);
I2C_DELAY();
// 2、拉高SDA,产生上升沿,并延迟
I2C_SDA(1);
I2C_DELAY();
}
③、应答信号
发送器每发送一个字节,就在第 9 个时钟脉冲释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK 简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
观察上图标号 ③ 就可以发现,有效应答的要求是从机在第 9 个时钟脉冲之前的低电平期间将 SDA 线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主机,则在它收到最后一个字节后,发送一个 NACK 信号,以通知被控发送器结束数据发送,并释放 SDA 线,以便主机接收器发送一个停止信号。
/**
* @brief 主机检测应答信号
*
* @return uint8_t 0,接收应答成功;1,接收应答失败
*/
uint8_t I2C_Simulate_WaitAck(void)
{
uint8_t waitTime = 0;
uint8_t ack = 0;
// 1、主机释放SDA数据线并延迟,此时外部器件可以拉低SDA线
I2C_SDA(1);
I2C_DELAY();
// 2、主机拉高SCL,此时从机可以返回ACK
I2C_SCL(1);
I2C_DELAY();
// 3、SCL高电平期间主机读取SDA状态,等待应答
while (I2C_READ_SDA())
{
// 4、如果超时的话,就直接产生结束信号,非应答
waitTime++;
if (waitTime > 250)
{
I2C_Simulate_Stop();
ack = 1;
break;
}
}
// 5、SCL=0,结束ACK检查
I2C_SCL(0);
I2C_DELAY();
// 6、返回是否接收到应答信号
return ack;
}
I2C_WaitAck() 函数主要用在写时序中,当启动起始信号,发送完 8bit 数据到从机时,我们就需要等待以及处理接收从机发送过来的响应信号或者非响应信号,一般就是在 I2C_SendOneByte() 函数后面调用。
首先先释放 SDA,把电平拉高,延时等待从机操作 SDA 线,然后主机拉高时钟线并延时,确保有充足的时间让主机接收到从机发出的 SDA 信号,这里使用的是 I2C_READ_SDA 宏定义去读取,根据 I2C 协议,主机读取 SDA 的值为低电平,就表示 “应答信号”;读到 SDA 的值为高电平,就表示 “非应答信号”。在这个等待读取的过程中加入了超时判断,假如超过这个时间没有接收到数据,那么主机直接发出停止信号,跳出循环,返回等于 1 的变量。在正常等待到应答信号后,主机会把 SCL 时钟线拉低并延时,返回是否接收到应答信号。
SDA 为低电平即应答信号,高电平即非应答信号,首先先根据返回 “应答” 或者 “非应答” 两种情况拉低或者拉高 SDA,并延时等待 SDA 电平稳定,然后主机拉高 SCL 线并延时,确保从机能有足够时间去接收 SDA 线上的电平信号。然后主机拉低时钟线并延时,完成这一位数据的传送。最后把 SDA 拉高,呈高阻态,方便后续通信用到。
/**
* @brief 发送应答信号或非应答信号
*
* @param ack 0,发送应答信号;1,发送非应答信号
*/
void I2C_Simulate_SendAck(uint8_t ack)
{
// 1、拉低SDA,表示应答,拉高SDA,表示非应答,并延迟
I2C_SDA(ack);
I2C_DELAY();
// 2、主机拉高SCL线,并延迟,确保从机能有足够时间去接收SDA线上的电平信号
I2C_SCL(1);
I2C_DELAY();
// 3、主机拉低时钟线并延时,完成这一位数据的传送
I2C_SCL(0);
I2C_DELAY();
// 4、释放SDA线,并延迟
I2C_SDA(1);
I2C_DELAY();
}
④、数据有效性
I2C 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在 SCL 的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
在 SCL 低电平期间,从机将数据位依次放到 SDA 总线上(高位先行),然后释放 SCL,主机将在 SCL 高电平期间读取数据位,所以 SCL 高电平期间不允许有数据变化,依次循环上过过程 8 次,即可接收一个字节(主机在接收之前,需要释放 SDA)。
/**
* @brief I2C读取一个字节函数
*
* @param ack 0,发送应答信号,1,发送非应答信号
* @return uint8_t
*/
uint8_t I2C_Simulate_ReadOneByte(uint8_t ack)
{
uint8_t receive = 0;
// 1、主机释放SDA
I2C_SDA(1);
for (uint8_t i = 0; i < 8; i++)
{
// 2、释放SCL,主机将在SCL高电平期间读取数据位
I2C_SCL(1);
I2C_DELAY();
// 3、读取SDA
if (I2C_READ_SDA())
{
receive |= 0x80 >> i;
}
// 4、拉低SCL,从机切换SDA线输出数据
I2C_SCL(0);
I2C_DELAY();
}
// 5、发送应答信号或非应答信号
I2C_Simulate_SendAck(ack);
// 6、返回读取的数据
return receive;
}
首先可以明确的是时钟信号是通过主机发出的,而且接收到的数据大小为 1 字节,但是 I2C 传输的单位是 bit,所以就需要执行 8 次循环,才能把一字节数据接收完整。
首先需要一个变量 receive 存放接收到的数据。在每次循环的开始的时候,在 SCL 高电平期间加入延时,确保有足够的时间能让数据发送并进行处理,使用宏定义 I2C_READ_SDA() 就可以判断读取到的高低电平,假如 SDA 为高电平,会对 0x80 这个数右移当前的循环数,然后这个结果会与 receive 进行或运算,将指定位置 1,如果 SDA 是低电平,则不进行处理,当前位仍为 0。当 SCL 线拉低后,需要加入延时,便于从机切换 SDA 线输出数据。在 8 次循环结束后,我们就获得了 8bit 数据,把它作为返回值返回,然而按照时序图,作为主机就需要发送应答或者非应答信号,去回复从机。
⑤、数据传输
在 I2C 总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在 SCL 串行时钟的配合下,在 SDA 上逐位地串行传送每一位数据。数据位的传输是边沿触发。
数据传输时序图如下:
SCL 低电平期间,主机将数据位依次放到 SDA 总线上(高位先行),然后释放 SCL,从机将在 SCL 高电平期间读取数据位,所以 SCL 高电平期间 SDA 不允许有数据变化,依次循环上述过程 8 次,即可发送一个字节。
/**
* @brief I2C发送一个字节函数
*
* @param data 待发送的数据
*/
void I2C_Simulate_SendOneByte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
// 1、发送数据位的高位
I2C_SDA((data & 0x80) >> 7);
I2C_DELAY();
// 2、释放SCL,从机将在SCL高电平期间读取数据位
I2C_SCL(1);
I2C_DELAY();
// 3、拉低SCL
I2C_SCL(0);
// 4、数据左移一位,用于下次发送
data <<= 1;
}
// 5、发送完成,主机释放SDA线
I2C_SDA(1);
}
在 I2C 的发送函数 I2C_SendOneByte() 中,我们把需要发送的数据作为形参,形参大小为 1 个字节。在 I2C 总线传输中,一个时钟信号就发送一个 bit,所以该函数需要循环八次,模拟八个时钟信号,才能把形参的 8 个位数据都发送出去。这里使用的是形参 data 和 0x80 与运算的方式,判断其最高位的逻辑值,假如为 1 即需要控制 SDA 输出高电平,否则为 0 控制 SDA 输出低电平。
经过第一步的 SDA 高低电平的确定后,接着需要延时,确保 SDA 输出的电平稳定,在 SCL 保持高电平期间,SDA 线上的数据是有效的,此过程也是需要延时,使得从设备能够采集到有效的电平。然后准备下一位的数据,所以这里需要的是把 data 左移一位,等待下一个时钟的到来,从设备进行读取。把上述的操作重复 8 次就可以把 data 的 8 个位数据发送完毕,循环结束后,把 SDA 线拉高,等待接收从设备发送过来的应答信号。
⑥、空闲状态
I2C 总线的 SDA 和 SCL 两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
三、I2C写操作时序图
主机首先在 I2C 总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送从机地址 +0(写操作)组成的 8bit 数据,所有从机接收到该 8bit 数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。注意:I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
四、I2C读操作时序图
主机向从机读取数据的操作,一开始的操作与写操作有点相似,都是由主机发出起始信号,接着发送从机地址 +1(读操作)组成的 8bit 数据,从机接收到数据验证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返回 8bit 数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号,从机才会停止发送数据。
五、硬件I2C的框图
STM32 有专门用于负责协议的 I2C 协议的外设,只要配置外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来,CPU 只要检测外设的状态和访问数据寄存器,就能完成数据收发。这种由硬件外设处理 I2C 协议的方式减轻了 CPU 的工作。
STM32 的 I2C 外设可用作通信的主机及从机,支持 100kBit/s 的速率,支持 7 位、10 位设备地址、支持 DMA 数据传输,并具有数据校验功能。它的 I2C 外设还支持 SMBus2.0 协议。
六、I2C通信过程
使用 I2C 外设通讯时,在通讯的不同阶段它会对 “状态寄存器 (SR1 及 SR2)” 的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。一般,我们会把 STM32 作为主设备。
6.1、主发送器
图中的是 “主发送器” 流程,即作为 I2C 通讯的主机端时,向外发送数据时的过程。主发送器发送流程及事件说明如下:
- 控制产生起始信号 (S),当发生起始信号后,它产生事件 “EV5”,并会对 SR1 寄存器的 “SB” 位置 1,表示起始信号已经发送;
- 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件 “EV6” 及 “EV8”,这时 SR1 寄存器的 “ADDR” 位及 “TXE” 位被置 1,ADDR 为 1 表示地址已经发送,TXE 为 1 表示数据寄存器为空;
- 以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的 “数据寄存器 DR” 写入要发送的数据,这时 TXE 位会被重置 0,表示数据寄存器非空,I2C 外设通过 SDA 信号线一位位把数据发送出去后,又会产生 “EV8” 事件,即 TXE 位被置 1,重复这个过程,就可以发送多个字节数据了;
- 当我们发送数据完成后,控制 I2C 设备产生一个停止信号 (P),这个时候会产生 EV8_2 事件,SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。
假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。
6.2、主接收器
图中的是 “主接收器” 流程,即作为 I2C 通讯的主机端时,接收从机发送过来数据时的过程。主接收器发送流程及事件说明如下:
- 同主发送流程,起始信号 (S) 是由主机端产生的,控制发生起始信号后,它产生事件 “EV5”,并会对 SR1 寄存器的 “SB” 位置 1,表示起始信号已经发送;
- 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件 “EV6” 这时 SR1 寄存器的 “ADDR” 位被置 1,表示地址已经发送。
- 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生 “EV7” 事件,SR1 寄存器的 RXNE 被置 1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控制 I2C 发送应答信号 (ACK) 或非应答信号 (NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输;
- 发送非应答信号后,产生停止信号 (P),结束传输。
七、I2C常用寄存器
7.1、I2C控制寄存器
I2C 控制寄存器 1 的位 10 ACK 应答使能,该位置 0,不产生应答,该位置 1,在接收一个字节后返回应答。
I2C 控制寄存器 1 的位 9 STOP 在置 1 的情况下,产生结束信号。
I2C 控制寄存器 1 的位 8 START 在置 1 的情况下,产生起始信号。
I2C 控制寄存器 1 的位 1 PE 在置 1 的情况下,使能 I2C 外设。
I2C 控制寄存器 2 的位 5:0 FREQ[5:0] 设置 I2C 外设工作的时钟频率,这个时钟频率不是主机与从机间的通信频率。
7.2、I2C数据寄存器
7.3、I2C状态寄存器
I2C 状态寄存器 1 的位 10 AF 检测应答是否成功,该位为 1,表示应答失败,该位为 0,表示应答成功。
I2C 状态寄存器 1 的位 7 TxE 在发送过程中 DR 为空时该位置 1,发送设备地址时不会置位。
I2C 状态寄存器 1 的位 6 RxNE 接收模式下数据寄存器非空时置 1。RxNE 不会在地址阶段置 1。
I2C 状态寄存器 1 的位 1 ADDR 会在成功发送设备地址后置 1。
I2C 状态寄存器 1 的位 9 SBB 会在成功产生开始信号后置 1。
7.4、I2C时钟控制寄存器
该寄存器配置主机与从机通信的时钟频率。
7.5、I2C TRISE寄存器
八、IO引脚复用功能
【1】、I2C1 引脚复用及其重映射功能
功能引脚 | 复用引脚 | 重映射引脚 |
---|---|---|
SCL | PB6 | PB8 |
SDA | PB7 | PB9 |
【2】、IC2C 引脚复用及其重映射功能
功能引脚 | 复用引脚 | 重映射引脚 |
---|---|---|
SCL | PB10 | PF1 |
SDA | PB11 | PF0 |
【3】、I2C3 引脚复用及其重映射功能
功能引脚 | 复用引脚 | 重映射引脚 |
---|---|---|
SCL | PA8 | |
SDA | PC9 |
九、I2C配置步骤
9.1、使能对应的时钟
#define __HAL_RCC_I2C1_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_I2C2_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_I2C3_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C3EN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C3EN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOA_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOB_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOBEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOC_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOCEN);\
UNUSED(tmpreg); \
} while(0U)
#define __HAL_RCC_GPIOF_CLK_ENABLE() do { \
__IO uint32_t tmpreg = 0x00U; \
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
/* Delay after an RCC peripheral clock enabling */ \
tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_GPIOFEN);\
UNUSED(tmpreg); \
} while(0U)
9.2、配置I2C工作参数
要使用一个外设首先要对它进行初始化,I2C 的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_I2C_Init(I2C_HandleTypeDef *hi2c);
形参 hi2c 是 I2C 的句柄,UART_HandleTypeDef 结构体类型,其定义如下:
typedef struct
{
I2C_TypeDef *Instance; // I2C寄存器地址
I2C_InitTypeDef Init; // I2C初始化结构体
uint8_t *pBuffPtr; // I2C传输数据缓冲区
uint16_t XferSize; // I2C传输数据大小
__IO uint16_t XferCount; // I2C传输数据计数器
__IO uint32_t XferOptions; // I2C传输选项
__IO uint32_t PreviousState; // I2C上一次状态
DMA_HandleTypeDef *hdmatx; // IC2发送参数设置(DMA)
DMA_HandleTypeDef *hdmarx; // I2C接收参数设置(DMA)
HAL_LockTypeDef Lock; // 锁对象
__IO HAL_I2C_StateTypeDef State; // I2C传输状态结构体
__IO HAL_I2C_ModeTypeDef Mode; // IC2模式
__IO uint32_t ErrorCode; // I2C错误状态
__IO uint32_t Devaddress; // I2C目标地址
__IO uint32_t Memaddress; // I2C目标内存地址
__IO uint32_t MemaddSize; // I2C目标内存地址大小
__IO uint32_t EventCount; // I2C事件计数
} I2C_HandleTypeDef;
Instance:指向 I2C 寄存器基地址。实际上这个基地址 HAL 库已经定义好了,可以选择范围:I2C1~ I2C3。
#define I2C1 ((I2C_TypeDef *) I2C1_BASE)
#define I2C2 ((I2C_TypeDef *) I2C2_BASE)
#define I2C3 ((I2C_TypeDef *) I2C3_BASE)
Init:I2C 初始化结构体,用于配置通讯参数。
hdmatx,hdmarx:配置 I2C 发送接收数据的 DMA 具体参数。
Lock:对资源操作增加操作 锁保护,可选 HAL_UNLOCKED 或者 HAL_LOCKED 两个参数。
ErrorCode:串口错误操作信息。主要用于存放 I2C 操作的错误信息。
I2C_InitTypeDef 这个结构体类型,该结构体用于配置 I2C 的各个通信参数,具体说明如下:
typedef struct
{
uint32_t ClockSpeed; // SCL时钟频率
uint32_t DutyCycle; // 时钟占空比
uint32_t OwnAddress1; // 本机地址
uint32_t AddressingMode; // 地址模式
uint32_t DualAddressMode; // 双重地址模式
uint32_t OwnAddress2; // 本机地址2
uint32_t GeneralCallMode; // 指定广播呼叫模式
uint32_t NoStretchMode; // 禁止时钟延长模式
} I2C_InitTypeDef;
ClockSpeed:设置 SCL 时钟频率,此值要低于 40 0000。
DutyCycle:时钟周期占空比,可选值如下。
#define I2C_DUTYCYCLE_2 0x00000000U
#define I2C_DUTYCYCLE_16_9 I2C_CCR_DUTY
OwnAddress1:STM32 的 I2C 设备自身地址 1。每个连接到 I2C 总线上的设备都要有一个自己的地址,作为主机也不例外。地址可设置为 7 位或 10 位(受下面 AddressingMode 成员决定),只要该地址是 I2C 总线上唯一的即可。STM32 的 I2C 外设可同时使用两个地址,即同时对两个地址作出响应,这个结构成员 OwnAddress1 配置的是默认的、OAR1 寄存器存储的地址,若需要设置第二个地址寄存器 OAR2,可使用 DualAddressMode 成员使能,然后设置 OwnAddress2 成员即可,OAR2 不支持 10 位地址。
AddressingMode:地址模式,我们可以设定为 7 位地址模式 或 10 位地址模式。这需要根据实际连接到 I2C 总线上设备的地址进行选择,这个成员的配置也影响到 OwnAddress1 成员,只有这里设置成 10 位模式时,OwnAddress1 才支持 10 位地址。
#define I2C_ADDRESSINGMODE_7BIT 0x00004000U
#define I2C_ADDRESSINGMODE_10BIT (I2C_OAR1_ADDMODE | 0x00004000U)
DualAddressMode:双重地址模式。STM32 的 I2C 外设可同时使用两个地址,即同时对两个地址作出响应。
#define I2C_DUALADDRESS_DISABLE 0x00000000U
#define I2C_DUALADDRESS_ENABLE I2C_OAR2_ENDUAL
OwnAddress2:STM32 的 I2C 设备自身地址 2。
#define UART_HWCONTROL_NONE 0x00000000U
#define UART_HWCONTROL_RTS ((uint32_t)USART_CR3_RTSE)
#define UART_HWCONTROL_CTS ((uint32_t)USART_CR3_CTSE)
#define UART_HWCONTROL_RTS_CTS ((uint32_t)(USART_CR3_RTSE | USART_CR3_CTSE))
NoStretchMode:本成员是关于 I2C 禁止时钟延长模式设置,用于在从模式下禁止时钟延长。它在主模式下必须保持关闭。
#define UART_OVERSAMPLING_16 0x00000000U
#define UART_OVERSAMPLING_8 ((uint32_t)USART_CR1_OVER8)
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
typedef enum
{
HAL_OK = 0x00U, // 成功
HAL_ERROR = 0x01U, // 错误
HAL_BUSY = 0x02U, // 忙碌
HAL_TIMEOUT = 0x03U // 超时
} HAL_StatusTypeDef;
9.3、I2C底层初始化
HAL 库中,提供 HAL_GPIO_Init() 函数用于配置 GPIO 功能模式,初始化 GPIO。该函数的声明如下:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
该函数的第一个形参 GPIOx 用来 指定端口号,可选值如下:
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
第二个参数是 GPIO_InitTypeDef 类型的结构体变量,用来 设置 GPIO 的工作模式,其定义如下:
typedef struct
{
uint32_t Pin; // 引脚号
uint32_t Mode; // 模式设置
uint32_t Pull; // 上下拉设置
uint32_t Speed; // 速度设置
uint32_t Alternate; // 复用功能设置
}GPIO_InitTypeDef;
成员 Pin 表示 引脚号,范围:GPIO_PIN_0 到 GPIO_PIN_15。
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
#define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */
#define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */
#define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
#define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
#define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
#define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */
#define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
成员 Mode 是 GPIO 的 模式选择,有以下选择项:
#define GPIO_MODE_AF_OD 0x00000002U // 开漏式复用
成员 Pull 用于 配置上下拉电阻,有以下选择项:
#define GPIO_NOPULL 0x00000000U // 无上下拉
#define GPIO_PULLUP 0x00000001U // 上拉
#define GPIO_PULLDOWN 0x00000002U // 下拉
成员 Speed 用于 配置 GPIO 的速度,有以下选择项:
#define GPIO_SPEED_FREQ_LOW 0x00000000U // 低速
#define GPIO_SPEED_FREQ_MEDIUM 0x00000001U // 中速
#define GPIO_SPEED_FREQ_HIGH 0x00000002U // 高速
#define GPIO_SPEED_FREQ_VERY_HIGH 0x00000003U // 极速
成员 Alternate 用于 配置具体的复用功能,不同的 GPIO 口可以复用的功能不同,具体可参考数据手册。
#define GPIO_AF4_I2C1 ((uint8_t)0x04) /* I2C1 Alternate Function mapping */
#define GPIO_AF4_I2C2 ((uint8_t)0x04) /* I2C2 Alternate Function mapping */
#define GPIO_AF4_I2C3 ((uint8_t)0x04) /* I2C3 Alternate Function mapping */
9.4、I2C发送数据
HAL 库提供 HAL_I2C_Master_Transmit() 函数主机向从机发送数据。其声明如下:
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
形参 hi2c 是 I2C_HandleTypeDef 结构体指针类型的 I2C 句柄。形参 DevAddress 是 从机设备的地址。形参 pData 是 要发送的数据缓冲区的指针。形参 Size 是 要发送的数据大小,以字节为单位。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
HAL 库提供 HAL_I2C_Mem_Write() 函数主机向从机指定内存地址中写入数据。其声明如下:
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)
形参 hi2c 是 I2C_HandleTypeDef 结构体指针类型的 I2C 句柄。形参 DevAddress 是 从机设备地址。形参 MemAddress 是 从机设备的内存地址。形参 MemAddSize 是 从机设备内存地址的长度。形参 pData 是 要写入的数据缓冲区的指针。形参 Size 是 要写入的数据大小,以字节为单位。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
形参 DevAddress 是设备的地址,调用前必须左移;
9.5、I2C读取数据
HAL 库提供了 HAL_I2C_Master_Receive() 函数主机接收从机发来的数据。该函数的声明如下:
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
形参 hi2c 是 I2C_HandleTypeDef 结构体指针类型的 I2C 句柄。形参 DevAddress 是 从机的设备地址。形参 pData 是 要保存数据的缓冲区指针。形参 Size 是 要读取的数据大小,以字节为单位。形参 Timeout 设置 超时时间,以毫秒为单位。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
HAL 库还提供了 HAL_I2C_Mem_Read() 函数主机从指定的指定内存地址中读取数据。该函数的声明如下:
HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)
形参 hi2c 是 I2C_HandleTypeDef 结构体指针类型的 I2C 句柄。形参 DevAddress 是 从机的设备地址。形参 MemAddress 是 从机设备的内存地址。形参 MemAddSize 是 从机设备内存地址的长度。形参 pData 是 要保存数据的缓冲区指针。形参 Size 是 要读取的数据大小,以字节为单位。形参 Timeout 设置 超时时间,以毫秒为单位。
该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功,HAL_ERROR 表示 错误,HAL_BUSY 表示 忙碌,HAL_TIMEOUT 表示 超时。
形参 DevAddress 是设备的地址,调用前必须左移;