目录
[1.控制寄存器1 I2C_CR1](#1.控制寄存器1 I2C_CR1)
[2.控制寄存器2 I2C_CR2](#2.控制寄存器2 I2C_CR2)
[3.时钟控制寄存器 CCR](#3.时钟控制寄存器 CCR)
[3.2.TRISE 上升沿时间配置寄存器](#3.2.TRISE 上升沿时间配置寄存器)
[4.状态寄存器1 I2C_SR1](#4.状态寄存器1 I2C_SR1)
一、STM32中的IIC硬件外设简介
在STM32中,有专门负责I2C协议的硬件外设,在使用时,仅需配置好对应的I2C外设,就可以实现I2C协议的通讯,这种由I2C外设实现的通讯方式减轻了CPU的工作,且使得软件设计更加简单。
I2C功能框图

STM32F103ZET6芯片上I2C片上外设的资源


二、相关寄存器介绍

1.控制寄存器1 I2C_CR1

- 寄存器中的ACK置1时,并不会马上发出一个ACK信号,而是表示在收到一个字节数据后发出一个ACK信号。
- 寄存器中的STOP/START则不同,置1后会产生开始或停止信号。
- NO STRETCH 控制位:一般情况下,时钟线都是由主设备控制,但是在主设备发送数据的速率太快,从设备无法及时接收时,从设备可以开启通过主动拉低时钟线的方式暂停主机的数据发送,这里的NO,代表不扩展这个功能,如果这一位是0,则是双重否定,代表这个功能默认是打开的。在使用时,保持默认即可。
- SMBUS 可以复用为SMBUS协议模块,默认不开启
- PE 使能I2C模块,注意,这一位需要最后打开

2.控制寄存器2 I2C_CR2

CR2的最后6位用于控制I2C模块的输入时钟频率,可用频率范围:2~36MHz之间。
原因:I2C模块连接到了APB1桥接,最高时钟频率为36MHz


3.时钟控制寄存器 CCR

由于I2C_CR2只是设置了一个最高的输入时钟频率,但是实际通讯中很可能达不到这样的速率,因此需要通过CCR进一步配置时钟的分频系数
F/S 快速/标准模式,默认是标准模式
CCR 11:0 表示分频系数

3.1.CRR分频系数的计算(以标准模式为例)
T_high 代表I2C时钟高电平的总时长,
T_low 代表I2C时钟低电平的总时长;
在标准模式下,要求T_high和T_low的值一致,即高电平占空比为50%。
T_pclk 代表I2C_CR2寄存器中设置的PCLK频率的时钟周期,是I2C模块的基础时钟周期:若I2C_CR2寄存器中设置的PCLK频率为36MHz,则 T_pclk = 1/36MHz = 1/36 μs。
标准模式下,I2C传输速率为100kbps(千比特每秒),即每传输1bit需要10μs(I2C时钟周期=10μs);按50%占空比,T_high = T_low = 10μs / 2 = 5μs。
综上,CRR分频系数的公式为:
T_high = RCC × T_pclk
推导得:
RCC = T_high / T_pclk
代入数值计算:
RCC = 5μs / (1/36 μs) = 5 × 36 = 180
3.2.TRISE 上升沿时间配置寄存器


根据芯片的数据手册可知:在标准 I2C模式下,上升时间最大是1000ns,即1us

因此,在基础时钟频率为36MHz的时候,上升沿所占的周期数为:36个,在此基础上加1为37,因此TRISE的值应设置为37。
以上配置需要在使能PE位之前完成。
4.状态寄存器1 I2C_SR1


三、实现I2C协议
1.产生起始信号
- 通过控制寄存器1 I2C_CR1 实现产生起始信号
- 在操作时,将I2C_CR1寄存器中的START位置1后需要等待真正产生起始信号后再做其他操作。
- 需要通过状态寄存器I2C_SR1中的SB位来判断。
- START位可以由软件清零也可以在发出开始信号后由硬件清零。
- SB默认为0,如果产生了起始信号,SB将被置1
状态寄存器 1(I2C_SR1)

控制寄存器1(I2C_CR1)



2.产生终止信号
通过控制寄存器1 I2C_CR1 实现产生终止信号
将I2C_CR1寄存器中的STOP位置1,置1后并不是马上发出停止信号,而是需要满足以下条件时发出


3.产生ACK信号
I2C_CR1寄存器


如果将ACK置1,意味着开启了应答功能,开启后每当收到一字节数据就会发出一个ACK信号。
4.收发数据
在发送数据时,通过TxE位判断是否有数据正在发送,通过BTF位判断当前在发送的数据是否发送完成



数据接收时,仅判断RXNE并且打开ACK即可


5.使用单独的函数实现发送设备地址
因为底层电路实现时将数据和设备地址的发送进行了区分,在发送数据后需要判断的标志位和发送地址后需要判断的标志位不同:
- 发送数据后由SR1中的BTF标志位判断数据是否发送并收到应答
- 发送地址后由SR1中的ADDR标志位判断地址是否发送并收到应答

四、程序编写
芯片的I2C硬件外设帮我们实现了I2C的协议,我们无需再通过代码操作引脚模仿SCL和SDA来自己构建协议的底层逻辑,仅需通过操作对应的寄存器即可实现I2C的通信。
(一)I2C初始化函数
1.配置时钟,开启GPIO、I2C外设的时钟;
2.配置I2C引脚的工作模式:复用功能开漏输出模式 CNF 11; MODE 11;
3.I2C2外设配置
3.1.硬件工作模式 选择工作在I2C模式、标准模式(默认状态)
3.2.选择输入的时钟频率CR2
3.3.配置CCR,对应数据传输速率100kbps,SCL高电平时间为5us
3.4.配置TRISE,SCL上升沿最大时钟周期数 + 1
3.5.使能I2C2模块
函数实现
cpp
// 初始化
void I2C_Init(void)
{
// 1.打开对应模块的时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
// 2.配置GPIO引脚工作模式,I2C2使用的引脚是:GPIOB10-SCL;GPIOB11-SDA
// 使用I2C硬件实现I2C协议时,引脚需要工作在复用功能开漏输出模式:CNF-11;MODE-11
// 先清零,再设置
GPIOB->CRH &= ~(GPIO_CRH_CNF10 | GPIO_CRH_CNF11 | GPIO_CRH_MODE10 | GPIO_CRH_MODE11);
GPIOB->CRH |= (GPIO_CRH_CNF10 | GPIO_CRH_CNF11 | GPIO_CRH_MODE10 | GPIO_CRH_MODE11);
// 3.设置I2C硬件工作模式
// 3.1.工作在I2C标准模式(默认状态,选配)
I2C2->CR1 &= ~I2C_CR1_SMBUS;
I2C2->CCR &= ~I2C_CCR_FS;
// 3.2.设置I2C的输入频率,频率可选范围是2-36MHz
// 先清零,再赋值
I2C2->CR2 = 0;
I2C2->CR2 |= 36;
// 3.3.设置I2C的CCR分频系数
// 输入频率36MHz,标准模式,传输速率100kbps,SCL的周期为10us,SCL高电平时间为5us
// 综上,在输入频率为36MHz的时候,SCL高电平期间的周期数为5*36=180
// 先清零,再赋值
I2C2->CCR = 0;
I2C2->CCR |= 180;
// 3.4.设置TRISE上升沿时间寄存器,值为上升沿期间的时钟周期数加1
// 根据芯片数据手册,上升沿时间为1us,在时钟周期为36MHz时,上升沿期间的周期数为36个,加1后为37
// 先清零,再赋值
I2C2->TRISE = 0;
I2C2->TRISE |= 37;
// 3.5.使能I2C2
I2C2->CR1 |= I2C_CR1_PE;
}
(二) I2C 产生起始信号
操作CR1寄存器中的START位,置1后检测是否真正产生了起始信号,SB位置1表示成功发送了起始信号。
函数实现
cpp
uint8_t I2C_Start(void)
{
// 将CR1寄存器中的START位置1
I2C2->CR1 |= I2C_CR1_START;
// 等待起始信号发出
uint16_t timeout = 0xffff;
while((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout)
{
timeout--;
}
// SB标志位在发出起始条件时被置1,读取SR1状态寄存器后写数据寄存器的操作将清除该位,在发出起始信号的后续操作一定是写数据寄存器,因此不用特意清除
return timeout? OK : FAIL;
}
(三)I2C产生终止信号
设置接收完数据之后发出停止信号,操作CR1的STOP位,置1表示使能STOP。
函数实现
cpp
// 设置STOP位,使能数据接收完成后发送停止信号
void I2C_Stop(void)
{
// 作为主设备,只负责发送一个停止信号即可,不负责检测
I2C2->CR1 |= I2C_CR1_STOP;
}
(四)I2C产生应答/非应答信号
设置使能应答信号
CR1中ACK位置1时,在收到数据后会回复ACK
CR1中ACK位置0时,在收到数据后会回复NACK
函数实现
cpp
// 设置ACK位,使能接收完一byte后发送ACK信号
void I2C_Send_ACK(void)
{
I2C2->CR1 |= I2C_CR1_ACK;
}
// 设置ACK位,使能接收完一byte后发送NACK信号
void I2C_Send_NACK(void)
{
I2C2->CR1 &= ~I2C_CR1_ACK;
}
(五)I2C发送设备地址
主设备发送从机设备地址,并等待应答
将要发送的地址给到DR
等待应答:SR1_ADDR。
函数实现
cpp
// 发送设备地址,并等待应答
uint8_t I2C_Send_Addr(uint8_t addr)
{
// 首次发送,可以直接发送数据
I2C2->DR = addr;
// 等待应答
uint16_t timeout = 0xffff;
while((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout)
{
timeout--;
}
// ADDR标志位的清零:软件读取SR1后再读取SR2会清零ADDR
(void)I2C2->SR2;
return timeout? OK : FAIL;
}
(六)I2C发送一字节数据
主机发送一个字节的数据,并等待应答
等待上一个字节发送完成
将要发送的数据给到DR
等待应答SR1_BTF
函数实现
cpp
// 发送1字节数据,并等待应答
uint8_t I2C_Send_Byte(uint8_t byte)
{
// 等待DR为空,等待上一个字节发送完成
uint16_t timeout = 0xffff;
while((I2C2->SR1 & I2C_SR1_TXE) == 0 && timeout)
{
timeout--;
}
if(timeout == 0)
{
return FAIL;
}
// 发送数据
I2C2->DR = byte;
// 等待数据发送完成
timeout = 0xffff;
while((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout)
{
timeout--;
}
// BTF标志位的清除条件:软件读取SR1寄存器后对数据寄存器的读写操作将清除该位;或者在传输中发出一个起始或停止信号后由硬件清除该位
// 因此不用特意清除该位
return timeout? OK : FAIL;
}
(七)I2C接收一字节数据
主机接收一个字节的数据(应答与否在I2C中不做判断,在EEPROM中做判断)
函数实现
cpp
// 接收1字节数据
uint8_t I2C_Receive_Byte(void)
{
// 等待DR为满,等待数据接收完成
uint16_t timeout = 0xffff;
while((I2C2->SR1 & I2C_SR1_RXNE) == 0 && timeout)
{
timeout--;
}
return timeout? I2C2->DR : FAIL;
}
(八)EEPROM读写操作
cpp
// 函数初始化
void M24C02_Init(void)
{
I2C_Init();
}
// 写入一字节
void M24C02_Write_Byte(uint8_t byte, uint8_t addr)
{
// 假写阶段
// 1.发送起始信号
I2C_Start();
// 2.发送设备地址+写命令
I2C_Send_Addr(ADDR_W);
// 3.发送数据存储地址
I2C_Send_Byte(addr);
// 真写阶段
// 4.发送写入数据
I2C_Send_Byte(byte);
// 5.发送停止信号
I2C_Stop();
// 6.给芯片一些缓冲时间
Delay_nms(5);
}
// 写入n个字节
void M24C02_Write_Bytes(uint8_t* buffer, uint8_t start_addr, uint8_t n)
{
// 假写阶段
// 1.发送起始信号
I2C_Start();
// 2.发送设备地址+写命令
I2C_Send_Addr(ADDR_W);
// 3.发送数据存入的起始地址
I2C_Send_Byte(start_addr);
// 真写阶段
// 4.循环发送要存入的数据
for(uint8_t i = 0; i < n; i++)
{
I2C_Send_Byte(*(buffer + i));
}
// 5.发送完成后最后发送停止信号
I2C_Stop();
// 6.给芯片一些缓冲时间
Delay_nms(5);
}
// 读取一个字节
uint8_t M24C02_Read_Byte(uint8_t addr)
{
// 假写阶段
// 1.发送开始信号
I2C_Start();
// 2.发送设备地址+写命令
I2C_Send_Addr(ADDR_W);
// 3.发送要读取信息的内存地址
I2C_Send_Byte(addr);
// 真读阶段
// 4.发送开始信号
I2C_Start();
// 5.发送设备地址+读命令
I2C_Send_Addr(ADDR_R);
// 6.在读取数据前先设置使能NACK和STOP
I2C_Send_NACK();
I2C_Stop();
// 7.接收数据
uint8_t byte = I2C_Receive_Byte();
return byte;
}
// 读取n个字节
void M24C02_Read_Bytes(uint8_t* buffer, uint8_t start_addr, uint8_t n)
{
// 假写阶段
// 1.发送开始信号
I2C_Start();
// 2.发送设备地址+写命令
I2C_Send_Addr(ADDR_W);
// 3.发送要读的数据的内存起始地址
I2C_Send_Byte(start_addr);
// 真读阶段
// 4.发送开始信号
I2C_Start();
// 5.发送设备地址+读命令
I2C_Send_Addr(ADDR_R);
// 6.读取数据
for(uint8_t i = 0; i < n; i++)
{
if(i < (n - 1))
{
I2C_Send_ACK(); // 不是最后一个数据,发送ACK
}
else
{
I2C_Send_NACK(); // 最后一个数据,发送NCAK和STOP信号
I2C_Stop();
}
// 读取字节
*(buffer + i) = I2C_Receive_Byte();
}
}