STM32单片机学习(26) —— 硬件I2C的实现
文章目录
硬件I2C驱动OLED实验
所谓硬件I2C,指的就是利用单片机内部自带的I2C硬件来完成I2C通信的过程,关于I2C通信的过程,我们已经学过了。
所以基本上所有原理性的东西我们都已经了解了,使用硬件I2C无非就是调用外设标准库函数,基于单片机的I2C外设来实现I2C通信的过程。

由于我们也已经使用过软件I2C了,所以对我们而言,硬件I2C的使用难度并不会很大,最多就是调用标准外设库函数的过程会比较繁琐。
我们仍然以一个实验案例来讲解学习硬件I2C,并且在这里我们将I2C和串口两种通信结合起来。
于是实验的电路接线图,就如下图所示:

学习I2C,顺带再复习一下串口通信。当然单片机USART外设接收数据的方式也要使用中断,而不是一般的轮询方式
单片机I2C外设的系统框图
使用引脚I2C通信,就需要使用单片机的I2C硬件外设。
其系统框图如下图所示:

这个框图将在后续的内容讲解中起到重要作用。
实验目的
实验目的:
在PC端,使用串口调试工具向STM32单片机发送指令:
- 发送指令’0’,表示关闭OLED屏幕,同时单片机向PC端回复消息:“OLED-TurnOff: Success”
- 发送指令’1’,表示开启OLED屏幕并点亮所有像素,同时单片机向PC端回复消息:“OLED-TurnOn: Success”
- 发送指令’2’,表示询问OLED屏幕当前点亮状态,并回复PC端。比如:“OLED-Status: OFF"或者"OLED-Status: ON”
所以本质上和软件I2C实现的功能是差不多的,只不过把串口通信模块加进来了。
当然这里我们继续使用模块化编程的思想,我们先来实现简单的模块。
USART模块实现
串口通信我们已经学过了,像上述实验目的中的串口通信功能对我们而言,已经可以说是手拿把掐了。
在工程的"Tools"目录下新建两个文件:“USART1.c"和"USART1.h”,然后把它们加入Keil5软件的Tools Group组当中。
串口通信模块,我们只需要实现下列两个功能就可以了:
- 配置USART1外设。包括: 初始化引脚,初始化USART1外设,开启RXNE标志位中断以及初始化NVIC。
- 单片机向PC端发送字符串。
注:单片机接收PC端发送的控制指令,没有必要单独作为一个函数去定义,可以直接在中断服务程序当中接收数据。
// USART.h
#ifndef __USART_H
#define __USART_H
#include "stm32f10x.h"
#include "./Delay.h"
void init_USART1(void);
void SendString(char *data);
#endif
#include "./USART.h"
#define RX GPIO_Pin_10
#define TX GPIO_Pin_9
#define USART_PORT GPIOA
// 初始化USART1 引脚
static void init_USART_PIN(void){
// 开时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
// 初始化Rx引脚PA10
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStruct.GPIO_Pin = RX;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 初始化Tx引脚PA9
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = TX;
GPIO_Init(USART_PORT, &GPIO_InitStruct);
}
// 初始化USART1外设
static void init_USART(void){
// 初始化USART引脚
init_USART_PIN();
// 开时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
USART_InitTypeDef USART_InitStruct;
// 设置波特率
USART_InitStruct.USART_BaudRate = 115200;
// 不设置硬件控制流
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
// 设置读写模式
USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
// 无校验位
USART_InitStruct.USART_Parity = USART_Parity_No ;
// 设置一位停止位
USART_InitStruct.USART_StopBits = USART_StopBits_1 ;
// 传输长度为8b
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
// 初始化USART1
USART_Init(USART1, &USART_InitStruct);
// 写使能
USART_Cmd(USART1, ENABLE);
}
// 初始化NVIC
static void init_NVIC(void){
// 初始化NVIC之前先设置中断分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn ;
// 开启NVIC
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
// 设置中断优先级
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
// 设置子优先级
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
// 初始化
NVIC_Init(&NVIC_InitStruct);
}
// 设置中断源
void USART1_RXNEConfig(void){
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}
void init_USART1(void){
init_USART_PIN();
init_USART();
init_NVIC();
USART1_RXNEConfig();
}
// 发送字符串
void SendString(char *data){
while(*data) {
// 发送寄存器为空时,发送一个字节
if(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == SET) USART_SendData(USART1, *data++);
}
}
实现分析:OLED模块
在工程的"Tools"目录下新建两个文件:“OLED.c"和"OLED.h”,然后把它们加入Keil5软件的Tools group组当中。
首先硬件IC2在实现时,直接使用了STM32的I2C外设,采用的编程手段是标准外设库函数实现。
所以我们没有必要再去弄一个"I2C"模块,可以直接在OLED模块中调用标准库函数,实现我们需要的功能。
OLED模块需要的功能一共有四个,"OLED.h"的参考代码如下:
#ifndef __OLED_H
#define __OLED_H
#include "stm32f10x.h"
#include "../tools/Delay.h"
typedef enum{
SUCCESS,
FAIL
}RES;
// 初始化OLED屏幕
void init_OLED(void);
// 点亮OLED
RES OLED_On(void);
// 熄灭OLED
RES OLED_Off(void);
// 读取OLED开关状态
RES OLED_ReadStatus(uint8_t *OLED_StatusPtr);
#endif
注意下方三个涉及到和OLED屏幕通信的函数,都是具有返回值的。
这是因为我们通过标准库来实现I2C通信时,可以通过I2C外设内置的各种标志位进行错误校验和处理,所以这几个函数我们使用枚举类型给定了RES类型的返回值。
下面看一下具体的每一个函数的实现。
引脚和I2C外设初始化
在上述实验中,STM32单片机中我们使用的I2C引脚是:
- SCL — PB10
- SDA — PB11
在一般情况下,PB10和PB11两个引脚都只是一般的、普通IO引脚。那么这两个引脚可以作为实现硬件I2C通信的引脚吗?
我们可以通过查表来获取这两个引脚的复用和重定义功能,以确定如何初始化这两个引脚。
具体的引脚定义表可以参考之前的文档




通过查表,我们可以得出下列信息,STM32F103C8T6共有下列三对可用为I2C通信的引脚:
假如使用引脚的复用功能的话:
第一对:
- PB6的复用功能是: I2C1_SCL,即I2C1外设的SCL引脚。
- PB7的复用功能是: I2C1_SDA,即I2C1外设的SDA引脚。
第二对:
- PB10的复用功能是: I2C2_SCL,即I2C2外设的SCL引脚。
- PB11的复用功能是: I2C2_SDA,即I2C2外设的SDA引脚。
根据我们选择的接线方式,我们使用的I2C硬件外设是I2C2,PB10作为SCL引脚,PB11作为SDA引脚。
当然,这两个引脚都需要设置为复用开漏输出模式。
第三对:
- PB8的重定义功能是: I2C1_SCL,即I2C1外设的SCL引脚。
- PB9的重定义功能是: I2C1_SDA,即I2C1外设的SDA引脚。
总之,有三对实现硬件I2C的引脚可以选择,但我们选择其中的第二对。
在这里需要注意,先看下图

我们首先要初始化GPIOB外设,它挂载在APB2,而I2C1/I2C2则是挂载在APB1,所以在初始化开时钟时要注意这一点
void init_OLED(void){
// 初始化引脚
init_I2C_Pin();
// 复位I2C外设
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C2, ENABLE); // 复位施加信号,类似于按下RESET按键
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C2, DISABLE);// 复位释放信号,类似于释放RESET按键
// 开时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
I2C_InitTypeDef I2C_InitStruct;
// 设置ACK
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
// 指定设备地址的长度,即使用 7 位还是 10 位地址
// 我们学习的是7位字节,即传输1Byte(7 + 1) 7位地址和1位读写
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
// 设置时钟频率,标准模式100kHz,快速模式400kHz
// 我们使用硬件实现I2C可以选择快速模式400kHz
I2C_InitStruct.I2C_ClockSpeed = 400000;
// 在快速模式(Fast Mode)下,设置 SCL 时钟信号的占空比
// 就是设置SCL 信号高电平和低电平持续时间的比例可以设置2:1也可以设置16:9 差别不大
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
// 设置标准I2C协议模式
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
// 设置 STM32 自身作为 I2C 设备时的第一个地址。
// 我们作为主机,故不需要设置
//I2C_InitStruct.I2C_OwnAddress1 = ;
I2C_Init(I2C2, &I2C_InitStruct);
// 使能开启
I2C_Cmd(I2C2, ENABLE);
}
这里有三处我们之前没有学过的内容,这里简单说一下。
I2C外设复位操作
I2C外设的复位:
代码中出现的下面两行代码:
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C2, ENABLE); // 施加复位信号
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C2, DISABLE); // 释放复位信号
它们的作用就是复位I2C外设,类似最小系统板上的复位按钮,它的主要作用是:
- 清除 I2C2 可能存在的错误状态(如 BUSY、ARBLOST 等)。
- 确保 I2C2 处于一个已知的初始状态,避免软件初始化后仍然异常。
- 重新初始化 I2C2 的内部寄存器,避免上一次的残留配置影响新初始化。
如果I2C外设之前被启动过,而且存在一些错误没有被清除,直接调用I2C_Init函数初始化它有可能无法重新工作。
所以利用硬件复位操作,让I2C外设彻底清空并重新开始工作是初始化I2C外设的常见做法.
但是一定要注意,该操作会清空重置I2C外设的一切配置,所以应该在I2C_Init函数初始化之前调用。
由于I2C外设的特殊性,所以在初始化I2C外设之前初始化它,是一个推荐的做法!
注(了解):
为什么I2C具有独特的复位机制,而其它的通信外设(USART、SPI等)往往不需要呢?
简单来说:
- I2C外设设计更复杂,实现的功能也更多。比如I2C外设实际支持多主多从模式,这使得I2C外设在通信时更容易出错导致外设卡死,从而就需要复位后再使用。
- I2C通信过程中,严格依赖双方的"互动"流程。比如单片机给从机设备发送数据,从机断开连接无响应,此时单片机的I2C外设就会卡死,这时如果不复位I2C外设就直接初始化使用是不行的。
再通俗点说:“单片机的I2C外设,是一个精密复杂的仪器,一旦某个流程没有完成它就会卡死在原地,需要重启(复位)它才会重新开始工作”。
I2C_Init函数和I2C_Cmd函数
I2C_Init函数和I2C_Cmd函数:
这一对函数同时都要调用,用于初始化I2C外设。类似的操作,我们之前也见过,和初始化USART外设是一样的。
通俗点说这两个函数的作用是:
- I2C_Init函数用于配置I2C外设。
- I2C_Cmd函数相当于一个控制此外设的总开关,用于开启和关闭此I2C外设。
下面来简单介绍一下这两个函数:
I2C_Cmd函数:
其函数声明如下:
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
其传参的选择十分简单,如下所示:
| 参数 | 类型 | 说明 |
|---|---|---|
I2Cx |
I2C_TypeDef* |
指定要控制的 I2C 外设,取值范围:I2C1 或 I2C2(STM32F103) |
NewState |
FunctionalState |
开启或禁用 I2C 外设,取值:ENABLE(开启) 或 DISABLE(禁用)。 |
I2C_Init函数:
I2C_Init 是 STM32 标准外设库提供的一个函数,用于初始化和配置 I2C 外设,包括时钟速度、地址模式、应答模式等。
其函数声明如下:
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
其第一个参数很简单,传参想要初始化的I2C外设,对于STM32F103系列来说就只有两个I2C外设,即I2C1 或 I2C2
其核心参数是第二个参数,和以往学习的所有Init函数一样,需要传入一个I2C_InitTypeDef类型的结构图对象指针。
这个结构体的类型定义如下:
typedef struct{
uint32_t I2C_ClockSpeed; // 设置 I2C 时钟频率(单位:Hz)
uint16_t I2C_Mode; // 设置 I2C 工作模式
uint16_t I2C_DutyCycle; // 配置 I2C 时钟的占空比
uint16_t I2C_OwnAddress1; // 配置设备自身的 I2C 地址
uint16_t I2C_Ack; // 启用或禁用 I2C 硬件的应答(ACK)
uint16_t I2C_AcknowledgedAddress; // 配置 I2C 的应答地址模式
} I2C_InitTypeDef;
逐一来解释一下这些成员:
I2C_ClockSpeed成员:
该成员需要传参一个32位无符号整数,用于设置 I2C 的时钟频率。
I2C的时钟频率是衡量I2C通信数据传输速率的核心要素,在上面软I2C的实现中我们已经讲了这个概念。
I2C协议当中的规定了典型的三种时钟线频率,如下表所示:
| 模式 | 最大 SCL 频率 | SCL 一个时钟周期 | 理论最大传输速率(单位:千字节每秒) |
|---|---|---|---|
| 标准模式 (SM) | 100 kHz (100,000 Hz) | 10 μs | 11.1KB/s |
| 快速模式 (FM) | 400 kHz (400,000 Hz) | 2.5 μs | 44.4KB/s |
| 高速模式 (HS) | 1 MHz (1,000,000 Hz) | 1 μs | 111.1KB/s |
我们使用的STM32F10x系列芯片最大只支持快速模式,也就是只能选择标准模式和快速模式,高速模式乃至于更高速度的模式只有更高性能、更贵的芯片才支持(比如F4系列)。
在实际传参时,我们可以直接手动传参400000这个整数,来表示选择使用I2C通信的快速模式,即SCL线时钟频率是400kHz。
既然选择使用硬件I2C,就要发挥其通信速率高的优势,而且快速模式大多数从机设备也都是支持的,通常都不会担心兼容性问题。
I2C_Mode成员:
用于设置I2C外设的工作模式。
标准库中给定了三个可以选择的取值:
#define I2C_Mode_I2C ((uint16_t)0x0000)
#define I2C_Mode_SMBusDevice ((uint16_t)0x0002)
#define I2C_Mode_SMBusHost ((uint16_t)0x000A)
I2C_Mode_I2C:最常用的 I2C 模式,也是标准的 I2C 模式,通常用于正常的 I2C 数据传输。
I2C_Mode_SMBusDevice:SMBus 从机模式。
I2C_Mode_SMBusHost:SMBus 主机模式。
SMBus(系统管理总线)是一种在 I2C 基础上进行扩展的协议,当前我们就选择使用标准I2C通信模式即可。
所以此成员只需要直接设置成I2C_Mode_I2C即可。
I2C_DutyCycle成员:
I2C_DutyCycle 成员是用于设置 I2C 通信中SCL时钟周期的占空比 的一个配置项。
**所谓占空比,是描述1个时钟周期内高电平与低电平时间比例的参数。**也就是说占空比影响 SCL 时钟信号线上的高电平和低电平的持续时间比。
占空比有啥用呢?
在一个时钟周期内低电平的比例越大,高电平比例就越小:
- 此时发送端准备数据的时间就会更长,通信抗干扰的能力会更强一些。
- 但由于高电平的时间占比更小了,工作时间变短,接收端采集数据的响应速度就会变短。这就要求接收端需要更快响应数据接收。
在一个时钟周期内高电平的比例越大,低电平比例就越小:
- 此时接收端采集数据的响应时间就变长了,对接收端设备的要求就会降低。
- 但发送端发送数据的准备时间更短,通信就更容易受到干扰。
注意:占空比和通信速率没什么关系,只要SCL信号线频率固定,时钟周期就是固定的数量,高低电平的比例不影响通信速率。
SPL标准库为程序员配置I2C时,提供了两个占空比选项:
I2C_DutyCycle_16_9表示"低电平:高电平"的比值为 16:9:- 高电平占比大概36%
- 低电平占比大概64%
I2C_DutyCycle_2表示"低电平:高电平"的比值为 2:1:- 高电平占比大概33%
- 低电平占比大概67%
所以这两个占空比其实差距不大,相对而言,I2C_DutyCycle_2的低电平占比更高,通信的抗干扰能够更好,支持这种占空比的从机设备也更多,是硬件I2C通信更常见的选择。
当你选择使用I2C通信的快速模式时,占空比属于必须要配置的一个成员属性,不能省略!!!
I2C_OwnAddress1成员:
I2C通信是主从模式的,一般情况下单片机都被视为主机,但I2C也支持将单片机作为从机使用。
若选择单片机作为从机的模式,为了让外部主机寻址到该从机单片机,就需要提供此单片机的地址。
这个参数就用于表示这个地址。
但我们不会选择将单片机作为从机使用,所以该成员可以忽略,不进行配置。
I2C_Ack成员:
I2C_Ack用于配置单片机作为接收端时(也就是单片机读从机数据时),是否向发送端回复ACK。
它的传参选项有两个:
I2C_Ack_Enable:启用应答,单片机收到数据后会发送 ACK,表示数据成功接收。I2C_Ack_Disable:禁用应答,单片机收到数据后会直接发送 NACK。
如果你希望开启主机应答就配置开始,当然也可以选择禁用主机应答。
主机应答通常是开启的,所以我们可以直接给该成员配置为I2C_Ack_Enable取值。
I2C_AcknowledgedAddress成员:
配置单片机在作为从机时,其从机设备地址的长度是7位还是10位。
但我们不会选择将单片机作为从机使用,所以该成员可以忽略,不进行配置。
核心实现:主机发送多个控制指令
在单片机与OLED屏幕进行I2C通信的过程中,单片机需要向OLED从机发送多个控制指令,从而实现控制OLED的功能。
这里需要实现的核心函数就是:
static int8_t hOLED_I2CWriteCmds(const uint8_t *Cmds, uint8_t Length)
该函数我们在软I2C中也实现过类似的,现在我们改用STM32标准外设库来实现这个函数。
I2C外设状态标志位
使用硬件I2C实现通信的过程中,不得不提的一个概念就是——I2C外设状态标志位。
利用单片机的USART外设实现串口通信时,我们是通过USART外设标志位来完成通信过程的,那么I2C外设也不例外。
STM32的I2C外设中,存在两个用于存储表示当前I2C通信状态、硬件状态的寄存器:
- SR1,即Status Register1
- SR2,即Status Register2
通过这些状态标志位,用户可以了解 I2C 通信的进展,检查错误状态,或者确保数据传输的每个步骤正确执行。
以下是 STM32 的 I2C 外设常用状态标志位的表格描述:
| 标志位宏定义 | 英文词组含义 | 标志位含义 | 标志位置为 1 时的条件 | 用途 |
|---|---|---|---|---|
I2C_FLAG_BUSY |
BUSY flag |
总线忙碌标志 | 当 I2C 总线正在传输数据时,标志位置为 1 | 用于判断总线是否空闲,确保在总线空闲时开始通信 |
I2C_FLAG_SB |
START Bit flag |
起始位发送完成标志 | 起始位(START)发送完成时,标志位置为 1 | 用于判断起始信号是否成功发送,确保后续步骤可进行 |
I2C_FLAG_ADDR |
Address flag |
从机地址匹配成功标志 | 主机地址与从机地址匹配时,标志位置为 1 | 确保主从机地址匹配完成,可以进入数据传输阶段 |
I2C_FLAG_AF |
Acknowledge Failure flag |
主机收到从机NACK标志 | 从机发送 NACK时,标志位置为 1 | 用于检测 ACK 错误,通常表示接收失败或通信中断 |
I2C_FLAG_TXE |
Transmit Empty flag |
发送数据寄存器空标志 | 发送寄存器为空时,,标志位置为 1 | 用于判断是否可以向数据寄存器写入新的数据**(不能用于发地址)** |
I2C_FLAG_RXNE |
Receive Not Empty flag |
接收数据寄存器非空标志 | 接收寄存器中有数据时,标志位置为 1 | 用于判断接收寄存器是否有数据,确保可以读取数据 |
I2C_FLAG_BTF |
Byte Transfer Finished flag |
字节传输完成标志 | 当前字节传输完成,标志位置为 1 | 用于判断发送和移位寄存器都为空,确保数据传输已成功结束 |
I2C_FLAG_STOPF |
STOP flag |
停止位发送完成标志 | 停止位已发送完成时,标志位置为 1 | 用于判断通信是否完全结束,确认 I2C 总线空闲 |
那么如何来获取和清零这些状态标志位呢?
这就需要使用两个标准库函数了,即I2C_GetFlagStatus函数和I2C_ClearFlag函数。
I2C_GetFlagStatus函数: 用于获取某个I2C外设的某个状态标志位的取值。
其函数声明如下:
FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
函数的两个形参:
I2Cx: 指示要获取标志位的IC2外设名,可以传参I2C1或者I2C2I2C_FLAG: 要检查的状态标志位。状态标志位可以是各种 I2C 状态寄存器中的标志。例如:I2C_FLAG_SB、I2C_FLAG_ADDR、I2C_FLAG_RXNE等,具体的内容可以参考上面表格的第一列。
函数的返回值:
- 函数返回
SET也就是1,表示标志位已被设置,状态标志位的条件已经满足。 - 函数返回
RESET也就是0,表示标志位已被清零,状态标志位的条件未满足。
**I2C_ClearFlag函数:**用于清零某个I2C外设的某个状态标志位。
其函数声明如下:
void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
这个函数没有返回值,形参的使用和上面的函数完全一致,不再赘述。
了解了I2C外设的状态标志位概念后,下面我们就按照软I2C的实现思路,来一一找到我们需要的库函数:
- 主机发送起始位信号
- 主机发送寻址字节
- 主机读取从机发送的ACK确认
- 主机向从机发送多个字节的控制字节和其它控制指令。
- 主机读取从机发送的ACK确认
- …
- 主机发送停止位信号
起始位发送函数
若想实现主机发送起始位信号,需要调用:I2C_GenerateSTART函数。
这个函数非常简单,就表示主机发送起始位信号,其函数形参如下:
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
第一个参数指示生成起始信号的I2C外设,可以传参 I2C1或者I2C2
第二个参数我们也很熟悉,它可以传参以下枚举类型:
typedef enum {DISABLE = 0, ENABLE = !DISABLE} FunctionalState;
传参**ENABLE**即表示主机发送了一个起始位信号。
调用完这个函数后,不要着急继续发送寻址字节,标志位中的I2C_FLAG_SB用于指示发送起始信号完成。
该函数的调用表示通信开始,建议采用以下方式来调用这个函数:
调用该函数之前,先检查总线是否忙碌,若总线处于忙碌状态,即总线正在传输数据,则无法开始新的通信数据传输。
所以需要等待总线空闲:
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BUSY) == SET); // 等待 I2C2 外设总线空闲,也就是等待BUSY标志位复位
除此之外,在调用此函数发送起始信号后,还需要调用:
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_SB) == RESET); // 等待I2C2外设作为主机, 发送完毕起始位
用于等待SB标志位置位,即表示主机发送起始信号结束。
上面两个状态标志位处理加在一起,所以在硬件I2C实现主机发送起始位的推荐方式是:
// 等待 I2C2 外设总线空闲
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BUSY) == SET);
// 主机发送起始位信号
I2C_GenerateSTART(I2C2, ENABLE);
// 等待确认起始位发送完成
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_SB) == RESET);
不要忘记使用I2C外设寄存器的状态标志位!依靠状态标志位指示通信状态,完成通信流程,是硬件I2C和软I2C非常显著的区别。
BUSY状态标志位的置位复位机制
I2C_FLAG_BUSY(BUSY 总线忙标志位)全程由硬件自动置位与自动清除,不需要也不允许软件手动干预。
参考手册中是这样描述的:
图 4.3-1 BUSY状态标志位的机制-图
举一些例子:
- 主机发送起始位成功(START 成功),BUSY状态标志位就会自动置位,表示总线处于忙碌通信状态…
- 通信结束,主机发送停止位,两条总线都回归高电平,总线进入空闲状态,BUSY标志位自动复位。
所以我们完全不需要操作BUSY标志位的置位和复位问题,可以放心的使用这个标志位。
除了BUSY状态标志位,我们还使用了SB状态标志位,那么它的置位与复位是如何处理的呢?
这一点,我们放到下面的小节来进行讲解。
停止位发送函数
起始位和停止位发送函数的名字非常类似,所以我们直接一起看。
只需要把I2C_GenerateSTART函数名,改成I2C_GenerateSTOP即可表示主机发送停止位。
其函数声明如下:
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
停止位发送函数在发送时,通常不需要对标志位进行处理判断,可以直接调用表示结束通信。具体代码这里就不再赘述了。
I2C_Send7bitAddress函数
主机发送起始位完成后,接下来就轮到主机发送寻址字节了。
在实现软件I2C时,我们没有专门编写函数实现主机发送寻址字节,而是直接将寻址字节、普通数据字节的发送共用了一个函数。
但是硬件I2C的SPL标准外设库,却将这两个发送操作分开了:
- I2C_Send7bitAddress函数,专门用于表示主机发送寻址字节。
- I2C_SendData函数,专门用于表示主机发送普通数据字节。
注意这两个函数不要混用,既然使用SPL标准外设库,操作单片机的I2C外设实现I2C通信,就需要严格遵守SPL库设计好的通信流程,这是由底层硬件的控制逻辑决定的,不像软件 I2C 可以自由处理。
那么该如何使用I2C_Send7bitAddress函数,完成主机寻址字节的发送呢?
此函数声明如下:
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t Direction);
一共有三个参数:
第一个参数I2Cx:
这个参数非常简单,用于指定哪个 I2C 外设发送寻址字节,传参取值比如I2C1 或 I2C2。
第二个参数Address:
第二个参数是该函数在调用传参时最需要注意的。
应该来说,这是ST公司设计I2C外设的SPL标准库时,非常坑爹的一个参数设计!!!
SPL库源码中的注释如下:
Address: specifies the slave address which will be transmitted
翻译成中文,意为:
Address: 指定了将要传输的从机地址。
这个注释简直就是一句彻头彻尾的废话,因为它根本就没有说清楚。现在Address的传参可以有以下可能:
- 像软I2C一样,传入一个带有7位从机地址,加上一位读写标志位的寻址字节。
- 直接传参7位从机地址,但是最高位补一个0
- 直接传参7位从机地址,但是最低位补一个0
- …
那么到底该如何传参呢?
这就需要结合这个函数的源码来分析了,分析过程如下图所示:

经过这样一段分析,你知道Address参数应该如何传参了嘛?
很简单:
只需要用7位从机地址,然后在末尾低位随便加一个0或者1都可以!!!
注意仍然要传参1个字节寻址字节,只不过最低位的0或1随便填!!!
举个例子:
OLED从机设备的7位从机地址是:
011 1100
在实际传参Address时,可以传参:
0111 1000:0x78
0111 1001:0x79
为什么呢?
因为I2C_Send7bitAddress函数在实现时,最终发送的寻址字节的最低位是由参数Direction来确定的,不管之前是0还是1,最终都能根据Direction参数的传入获得一个正确读写标志位。
所以程序员只需要保证高7位的从机地址传参正确即可,至于最低位实际上是无所谓的。
第三个参数Direction:
参数名的含义是方向,实际上这个参数就表示寻址字节当中的读写标志位,该参数的取值通过查看注释可以得知有两个:
- I2C_Direction_Transmitter:表示单片机主机作为发送端,发送数据给从机,也就是读写当中的写。
- I2C_Direction_Receiver:表示单片机主机作为接收端,从机发送数据给主机,也就是读写当中的读。
这个参数的传参还是非常简单。
现在如果你想向从机OLED屏幕发送数据,你只需要这样调用函数,发送寻址字节:
I2C_Send7bitAddress(I2C2, 0x78, I2C_Direction_Transmitter); // 这里也可以传参Ox79
现在如果你想读从机OLED屏幕发送的状态字节,你只需要这样调用函数,发送寻址字节:
I2C_Send7bitAddress(I2C2, 0x79, I2C_Direction_Receiver); // 这里也可以传参Ox78
注意,寻址字节一定要在发送起始位完成,并且成功后,才可以调用此函数发送。
以上。
SB状态标志位的置位复位机制
现在已经完成了寻址字节的发送,我们先不考虑后续的处理(比如从机应答处理),我们先来考虑一下:SB状态标志位的置位复位机制。
SB标志位,用于指示主机发送起始位完成,它的置位和复位机制,参考手册中是这么描述的:
图 4.6-1 SB状态标志位的机制-图
所以当主机发送完成起始位后,硬件会自动置位 SB,表明主机发送起始位完成、通信已经开始了,接下来主机可以发送寻址字节了。
所以SB标志位的置位由硬件自动置位,那么它什么时候复位呢?
它复位依赖于软件自动复位:
软件读取SR1寄存器后,写数据寄存器的操作将清除该位。
这样的操作如何实现呢?
我们实现的代码中是这么写的:
// 等待 I2C2 外设总线空闲
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BUSY) == SET);
// 主机发送起始位信号
I2C_GenerateSTART(I2C2, ENABLE);
// 等待确认起始位发送完成
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_SB) == RESET);
// 主机发送寻址字节
I2C_Send7bitAddress(I2C2, 0x78, I2C_Direction_Transmitter);
// 代码执行到这里,依赖于软件自动复位,SB标志位复位
其中:
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_SB) == RESET);用于获取SB状态标志位的状态,而SB标志位就存储在SR1寄存器当中,这样就完成了读SR1寄存器。I2C_Send7bitAddress(I2C2, 0x79, I2C_Direction_Transmitter);用于主机发送寻址字节给从机,这个过程中显然写了(发送)数据寄存器。
于是这样在"无形中",就完成了SB状态标志位的软件自动复位。
代码写到这里,我们使用的两个状态标志位:SB和BUSY,置位和复位都可以算得上自动管理,程序员不需要额外考虑手动复位问题。
主机发送寻址字节后的处理
主机发送寻址字节后,从机如果收到会应答ACK,主机则需要读这个从机ACK,从而确定寻址是否成功。
这个操作在我们自己实现的软件I2C当中,非常简单,但硬件I2C关于主机发送寻址字节后的处理就麻烦多了。
首先,主机发送寻址字节后的处理,仍然依赖标志位完成,这里就需要用到两个标志位了:
- ADDR标志位,宏定义为
I2C_FLAG_ADDR,复位时表示寻址字节尚未发送完成,置位时表示寻址成功,即主机收到从机应答。 - AF标志位,宏定义为
I2C_FLAG_AF,复位状态表示应答没有失败,置位时表示未收到从机应答,应答失败,即从机无应答。
很明显这两个标志位是互斥的,而且寻址字节发送后一定会有一个标志位置位:
- 一旦寻址成功,ADDR标志位置位,AF标志位一定不会置位。
- 如果寻址失败,AF标志位置位,ADD标志位则一定不会置位。
所以我们可以写一个循环,来等待这两个标志位中的某一个置位:
// 等待AF和ADDR两个标志位的其中一个置位,也就是等待寻址成功亦或者失败
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_ADDR) == RESET
&& I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == RESET
);
// 代码执行到这里说明两个标志位一定有一个置位了,接下来进行处理
寻址成功后的处理:
寻址成功后,ADDR标志位会被置位,按照ST公司的I2C外设设计,当ADDR标志置为1后,总线会等待手动复位此标志位,否则I2C外设将始终等待无法进行后续的数据传输工作。
如何复位ADDR标志位呢?
ST公司关于复位ADDR标志位的设计比较繁琐,要求必须使用I2C_ReadRegister函数先读SR1寄存器,后读SR2寄存器才能够完全清除ADDR标志位。
具体的函数调用代码就是:
// 如果ADDR标志位置位,寻址成功
if (I2C_GetFlagStatus(I2C2, I2C_FLAG_ADDR) == SET) {
// 复位 ADDR 状态标志位
I2C_ReadRegister(I2C2, I2C_Register_SR1); // 读取 SR1 寄存器
I2C_ReadRegister(I2C2, I2C_Register_SR2); // 读取 SR2 寄存器
}
ADDR 标志位被ST公司设计为必须依赖读SR1和SR2两个硬件寄存器才能被复位(清零),无法通过手动调用函数软件复位!
一定要注意,寻址成功后,要及时按照手册中的说明来复位ADDR标志位,如果忘记了复位,那么后续通信将无法继续进行。
寻址失败后的处理:
寻址失败后,AF标志位会被置位,表示应答失败。
ST公司在设计I2C外设的AF标志位时,做了这样的设定:
- AF标志位一旦置位,必须由程序员手动调用函数,手动软件复位!!
- AF标志位一旦置位,I2C通信就一定无法继续,通信将处于卡死状态!!
- 如果不进行AF标志位的手动软件复位,那么AF标志位将持续保持置位状态,乃至于影响下一次I2C通信过程!!!
AF标志位的这种强制设定,使得程序员不能忽视通信过程中的应答错误,避免了通信在"带病、异常"的情况下继续强行进行,也能够使得程序员必须专注于解决通信中的问题,从而实现更健壮、更可用的通信。
当然,这么设计也会有一些缺点:
AF标志位如果忘记了手动复位,将会始终保持置位,影响后续的通信,这也是我们初始化I2C外设时,最好先复位外设的原因。
基于上述描述,一旦寻址失败,我们应该仅需下述处理:
// 如果AF标志位置位,在这里表示寻址失败
if (I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == SET) {
// 手动软件复位AF标志位
I2C_ClearFlag(I2C2, I2C_FLAG_AF);
// 直接发送停止位, 表示终止通信
I2C_GenerateSTOP(I2C2, ENABLE);
// 可以继续其它处理,这里函数返回-1表示寻址失败
return -1;
}
到这里,我们就完成了寻址字节发送后的处理,相信你已经感受到了硬件I2C使用的繁琐和不便,不过这也没什么办法,别人怎么设计,我们就必须怎么用。
I2C_SendData函数
寻址成功后,下面主机向从机发送普通数据字节,这时就需要使用函数:I2C_SendData函数。
该函数的声明如下:
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
这个函数的调用十分的简单,第一个参数就表示主机的I2Cx外设,第二个参数表示向从机发送的1个字节的数据Data。
主机发送普通字节数据后的处理
在主机发送数据到从机的过程中,除了第一个字节的寻址字节外,其余要发送的数据都是普通的字节数据。
发送普通字节后,应该进行什么处理呢?
肯定和发送寻址字节的处理不同。
主机发送普通字节后,有以下两种可能性:
- 若从机应答ACK,表示主机发送数据成功:
I2C_FLAG_TXE和I2C_FLAG_BTF这两个标志位都会被置为1- 其中TXE表示发送数据寄存器为空,BTF表示数据传输已完成
- 我们可以使用BTF这个标志位,是否置1,来判断1个字节的数据是否发送完成。
- 若从机无应答,也就是应答NACK,则表示从机拒收,主机发送数据失败:
I2C_FLAG_AF会被置1,表示ACK失败,从机发送NACK非确认应答。- 注意,不要忘记手动复位AF标志位。
主机发送普通字节数据后的处理逻辑是这样的:
等待BTF标志位为空,即等待主机发送全部数据,然后再判断AF标志位处理从机ACK。
参考的处理方式如下:
// 主机发送非寻址字节数据后的处理逻辑(重点)
// 等待此1个字节的数据发送完成
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BTF) == RESET);
// 再检查AF标志位是否置1, 判断从机是否应当成功
if (I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == SET) {
// 从机无应答, 拒收此数据
I2C_GenerateSTOP(I2C2, ENABLE); // 停止通信
I2C_ClearFlag(I2C2, I2C_FLAG_AF); // 手动清零AF标志位
return -2;
}
注意:
在上面的操作中,我们手动清零了AF标志位,但并没有手动清零TXE和BTF标志位。
这是因为:根据ST公司的I2C外设设计,这两个标志位的置位和复位都可以认为是硬件/软件自动完成的,不需要程序员手动操作!
参考实现代码
//1. 主机发送起始位信号
//2. 主机发送寻址字节
//3. 主机读取从机发送的ACK确认
//4. 主机向从机发送多个字节的控制字节和其它控制指令。
//5. 主机读取从机发送的ACK确认
//6. .....
//7. 主机发送停止位信号
static int8_t OLED_I2CWriteCmds(const uint8_t *Cmds, uint8_t Length){
// 1. 等待 I2C 总线空闲(BUSY 位复位)
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BUSY) == SET);
// 2. 发送起始信号(START),开启通信
I2C_GenerateSTART(I2C2, ENABLE);
// 3. 等待起始条件发送完成(SB 位置位)
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_SB) == RESET);
// 4. 发送寻址字节(由第三个参数确定为写模式)
I2C_Send7bitAddress(I2C2, 0x78, I2C_Direction_Transmitter);
// 5. 等待寻址阶段结束:要么收到应答(ADDR),要么发生应答失败(AF)
// 注意这两个标志位最多有一个置位, 也必然有一个置位
// 因为寻址成功和失败总会发生一个, 也只会发生一个
while (
I2C_GetFlagStatus(I2C2, I2C_FLAG_ADDR) == RESET &&
I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == RESET
);
// 6. 检查是否发生寻址失败(AF),如果失败,发送停止位退出通信
// 但是不要忘记手动软件清除AF标志位
if (I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == SET) {
I2C_ClearFlag(I2C2, I2C_FLAG_AF); // 清除 AF 标志
I2C_GenerateSTOP(I2C2, ENABLE); // 发送 STOP 释放总线
return -1;
}
// 7. 读取 SR1 和 SR2 两个寄存器,手动复位 ADDR 标志,使得通信能够继续进行
I2C_ReadRegister(I2C2, I2C_Register_SR1);
I2C_ReadRegister(I2C2, I2C_Register_SR2);
// 8. 循环发送控制指令数组中的每个字节
for (uint8_t i = 0; i < Length; i++) {
I2C_SendData(I2C2, Cmds[i]); // 写入发送数据寄存器发送
// 等待当前字节发送完成(移位寄存器与数据寄存器都空,BTF 置位)
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BTF) == RESET);
// 检查是否收到从机的 NACK
if (I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == SET) {
I2C_ClearFlag(I2C2, I2C_FLAG_AF); // 清除 AF
I2C_GenerateSTOP(I2C2, ENABLE); // 通信失败,发送 STOP
return -2;
}
}
// 9. 所有数据发送完毕,发送 STOP 终止通信
I2C_GenerateSTOP(I2C2, ENABLE);
return 1;
}
点亮和熄灭OLED
int8_t OLED_On(void){
uint8_t OLED_TurnOnCmds[] = {
0x00, // 下面的指令是命令流
0x8D, 0x14, // 开启电荷泵
0xAF, // 打开屏幕
0xA5 // 让屏幕全亮,点亮所有像素点
};
return OLED_I2CWriteCmds(OLED_TurnOnCmds, sizeof(OLED_TurnOnCmds));
}
int8_t OLED_Off(void){
uint8_t OLED_TurnOffCmds[] = {
0x00, // 下面的指令是命令流
0xA4, // 普通显示模式
0xAE, // 关闭 OLED 显示
0x8D, 0x10 // 关闭电荷泵
};
return OLED_I2CWriteCmds(OLED_TurnOffCmds, sizeof(OLED_TurnOffCmds));
}
核心实现:读取OLED状态字节
现在只剩下最后一个核心函数没有实现了,它就是:
int8_t OLED_ReadStatus(uint8_t *oled_flag);
有了上面的基础上,这个函数实现起来也非常的容易。
这里我们再来学习几个函数:
I2C_ReceiveData函数:
有上面的I2C_SendData函数,就有相应的接收函数。此函数的声明如下:
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
此函数的形参只需要填入要接收数据的外设,可以是I2C1 或 I2C2。
它的返回值是一个8位的无符号数,也就是返回接收到的1个字节数据。
这个函数非常简单,但需要注意:最好在I2C_FLAG_RXNE接收数据寄存器非空标志位置为1时再调用,也就是有数据了再去读数据。
所以此函数的一般调用方式如下:
// 等待接收数据
while (I2C_GetFlagStatus(OLED_I2Cx, I2C_FLAG_RXNE) == RESET); // 等待接收缓冲区非空
// 读取数据
uint8_t status = I2C_ReceiveData(OLED_I2Cx); // 读取 OLED 状态数据
紧接着,我们还需要知道主机如何发送NACK给从机,表示主机不再希望接收从机数据。
这就需要使用函数I2C_AcknowledgeConfig了。
I2C_AcknowledgeConfig函数:
在硬件I2C当中,主机发送ACK还是NACK是自动的,而且是在配置初始化I2C外设时就决定了。
就是调用I2C_Init函数时,I2C_Ack成员的设置。
在前面我们已经将该成员设置为I2C_Ack_Enable,这表示:主机收到从机数据时回复ACK给从机。
而我们现在不需要主机发送ACK了,所以只需要改一下这个配置即可。
I2C_AcknowledgeConfig函数的声明如下:
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
调用它的参数格式如下:
| 参数 | 类型 | 说明 |
|---|---|---|
I2Cx |
I2C_TypeDef* |
选择 I2C 外设(如 I2C1 或 I2C2) |
NewState |
FunctionalState |
ENABLE(发送 ACK)或 DISABLE(禁用 ACK,发送NACK) |
所以只需要调用:
I2C_AcknowledgeConfig(OLED_I2Cx, DISABLE); // 禁用ACK, 主机发送NACK
即可让主机在接收到数据后,发送NACK。
整体的参考实现代码如下:
int8_t OLED_ReadStatus(uint8_t *OLED_StatusPtr){
// 1. 等待 I2C 总线空闲
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BUSY) == SET);
// 2. 主机发送起始位
I2C_GenerateSTART(I2C2, ENABLE);
// 3. 等待起始位发送完成
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_SB) == RESET);
// 4. 主机发送寻址字节
I2C_Send7bitAddress(I2C2, 0x78, I2C_Direction_Receiver); // 读操作
// 5. 等待寻址响应(ADDR 或 AF 至少有一个置位)
while (
I2C_GetFlagStatus(I2C2, I2C_FLAG_ADDR) == RESET &&
I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == RESET
);
// 6. 如果发生应答失败(AF),发送 STOP 并返回失败
// 不要忘记手动软件复位AF标志位
if (I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == SET) {
I2C_ClearFlag(I2C2, I2C_FLAG_AF);
I2C_GenerateSTOP(I2C2, ENABLE);
return 0;
}
// 7. 读取 SR1 和 SR2 寄存器以复位 ADDR标志位,以进行通信后续流程
I2C_ReadRegister(I2C2, I2C_Register_SR1);
I2C_ReadRegister(I2C2, I2C_Register_SR2);
// 寻址成功了,接下来从机OLED就会发送状态字节从主机
// 单片机的I2C外设中的接收数据寄存器会自动接收这个状态字节
// 这个接收过程不需要程序员手动完成
// 程序员只需要等待接收数据寄存器非空,然后调用函数把数据读出来即可
// 8. 从机发送状态字节后,主机就不再需要从机发送了
// 于是直接禁用主机 ACK,主机接收数据后会给从机发送NACK
I2C_AcknowledgeConfig(I2C2, DISABLE);
// 9. 等待接收寄存器非空(RXNE 置位)
while (I2C_GetFlagStatus(I2C2, I2C_FLAG_RXNE) == RESET);
// 10. 读取接收到的状态字节
uint8_t ReceiveByte = I2C_ReceiveData(I2C2);
*OLED_StatusPtr = ((ReceiveByte >> 6) & 0x01); // 解析状态位
// 11. 发送 STOP 结束通信
I2C_GenerateSTOP(I2C2, ENABLE);
return 1;
}
USART中断处理函数
为了实现外部中断的处理函数,需要声明以下函数:
// 开启屏幕并发送开启成功与否给PC端
void OLED_HandleOn(void);
// 关闭屏幕并发送关闭成功与否给PC端
void OLED_HandleOff(void);
// 读取屏幕开启状态并把这个状态发送给PC端
void OLED_HandleStatus(void);
// USART外设全局中断处理函数, 在这里我们处理RXNE标志位中断
void USART1_IRQHandler(void);
它们的实现也非常简单,如下所示:
// 打开OLED的处理函数
void OLED_HandleOn(void) {
if (OLED_On() == 0) {
SendString("OLED-TurnOn: Success");
} else {
SendString("OLED-TurnOff: Failed");
}
}
// 关闭OLED的处理函数
void OLED_HandleOff(void) {
if (OLED_Off() == 0) {
SendString("OLED-TurnOFF: Success");
} else {
SendString("OLED-TurnOFF: Failed");
}
}
// 查询OLED状态的处理函数
void OLED_HandleStatus(void) {
uint8_t OLED_Status;
if (OLED_ReadStatus(&OLED_Status) == 0) {
if (OLED_Status == 0) {
SendString("OLED-Status: ON");
} else {
SendString("OLED-Status: OFF");
}
} else {
SendString("OLED-Status: Failed to read");
}
}
// USART外设全局中断处理函数, 在这里我们处理RXNE标志位中断
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
uint8_t receivedData = USART_ReceiveData(USART1); // 读取接收数据
// 根据命令执行相应操作
switch (receivedData) {
case '1': // 打开OLED
OLED_HandleOn();
break;
case '0': // 关闭OLED
OLED_HandleOff();
break;
case '2': // 查询OLED状态
OLED_HandleStatus();
break;
default:
SendString("Command Error: Invalid command");
break;
}
}
}
因为文件比较多,整个项目我就不在这里展示了可以去我的git仓库clone一下
git clone https://gitee.com/xingyexiakong/cnblog.git
I2C通信的优缺点总结
至此,我们已经掌握了 I2C 通信的原理与基本用法。最后,我们来系统地总结一下 I2C 的优势与局限。
首先来看一下I2C通信的优势:
- 只需两根信号线(SCL、SDA),节省引脚、简化布线,适合板载多设备芯片之间的通信;
- 支持多主机与多从机架构,具有高度灵活性;
- 软件和硬件实现皆可,可以根据不同的需求,选择不同的实现;
- 功能完善强大,支持寻址,应答,主机切换,主从数据发送等功能;
- 成本低廉,无需专用通信器件,仅依赖通用 IO 与上拉电阻即可构建完整通信系统,性价比极高。
I2C的劣势如下:
- 通信距离有限,由于总线结构为开漏+上拉电阻,抗干扰能力弱,不适合长距离数据传输。
- 通信速度相对较低,上拉电阻导致 SDA/SCL 上升沿缓慢,严重制约通信速率,典型速率为 100kHz ~ 400kHz。
- 功耗略高,相比推挽结构,I²C 持续存在上拉通路,产生静态功耗。
- 如果选择多主机模式,容易出现总线冲突,通信过程比较复杂,出错几率高。
- 由于I2C通信协议的复杂性,再加上STM32部分单片机在I2C外设设计上的固有缺陷,使得硬件I2C在不在乎性能时反而不如软件I2C。
I²C 通信协议正是为了以最少的引脚、最低的成本,提供多设备互联的能力而设计的。
它的确做到了“小投入,强功能”的设计目标。
I2C是一种性价比极高、强大且灵活的通信协议,是嵌入式系统中必学且最常用的通信方式之一。
更多推荐


所有评论(0)