STM32HAL库 -- 9.IIC通信 软件IIC与硬件IIC驱动0.96寸OLED屏幕
目录
1.简介
2.IIC通信协议
2.1IIC简介
2.2IIC总线结构
2.3IIC的性能参数
2.4IIC基本时序单元
2.4.1起始条件
2.4.2停止条件
2.4.3发送或接收1字节数据
2.4.4应答信号
2.5IIC时序
2.5.1地址帧
2.5.2重复起始条件
2.5.3IIC通信时序示例 -- 指定地址写数据
2.5.4IIC通信时序示例 -- 当前地址读数据
2.5.5IIC通信时序示例 -- 指定地址读
编辑
3.软件IIC和硬件IIC
3.1软件IIC
3.2硬件IIC
4.软件IIC代码
4.1硬件连接
4.2程序编写
4.2.1宏定义
4.2.2GPIO的初始化
4.2.3起始条件
4.2.4停止条件
4.2.5检测应答信号
4.2.6发送应答信号和非应答信号
4.3测试代码
5.硬件IIC代码
1.简介
这个文章会介绍IIC通信,然后编写IIC的代码。
作者使用的开发板是正点原子的精英版,写文章用于记录学习和经验分享。
2.IIC通信协议
2.1IIC简介
IIC(Inter-Integrated Circuit)通信协议,是一种由Philips公司(现为NXP Semiconductors)在1982年推出的多主机的、双向的、同步的串行通信协议。它主要用于集成电路之间的短距离通信,广泛应用于嵌入式系统和微控制器之间的通信。
2.2IIC总线结构
IIC 总线由两条信号线组成:
信号线 | 含义 | 功能 |
---|---|---|
SDA | Serial Data Line | 数据传输线,用于主从设备之间发送和接收数据 |
SCL | Serial Clock Line | 时钟线,由主设备生成,控制数据传输的时序 |
开漏输出结构:IIC 的 SDA 和 SCL 引脚通常为开漏输出(Open-drain 或 Open-collector) ,这意味着它们只能将信号拉低(0V),不能主动驱动高电平(VCC)。
上拉电阻:为了使信号恢复到高电平,必须在总线上添加外部上拉电阻(Pull-up Resistor) 连接到 VCC。典型值:1kΩ ~ 10kΩ(取决于总线速度和负载),高速模式下需要更小阻值以加快上升沿时间。
总线拓扑结构:单主多从结构,即一个主设备控制多个从设备。多主多从结构,即支持多个主设备竞争总线使用权,通过仲裁机制避免冲突。
2.3IIC的性能参数
以下是标准 I²C 协议的主要性能参数:
参数 | 描述 |
---|---|
通信类型 | 同步串行通信 |
数据线数量 | 2 条:SDA(数据线)、SCL(时钟线) |
通信方向 | 半双工 |
通信模式 | 主从模式(支持多主多从) |
数据单位 | 一个字节 = 8 位数据位(MSB先传)每个字节后必须有一个 ACK/NACK 应答位 |
最大传输速率 | 标准模式下为 100 kbps,快速模式下为 400 kbps,高速模式下可达 3.4 Mbps |
电压电平 | 通常为 5V 或 3.3V,但可支持更低的电压(如 1.8V) |
总线拓扑结构 | 开漏输出 + 上拉电阻 |
地址位宽 | 支持 7 位或 10 位寻址方式 |
最大从设备数量 | 总线上可挂接的设备数量受总线的最大电容400pF限制。一般不超过8个。 |
2.4IIC基本时序单元
2.4.1起始条件
起始条件(START Condition)。当 SCL 为高电平时,SDA 由高变低。通知所有从设备一次通信即将开始。
特点是只有主设备可以发出起始信号。所有从设备进入监听状态。
2.4.2停止条件
停止条件(STOP Condition)。当SCL 为高电平时,SDA 由低变高。表示本次通信结束。
2.4.3发送或接收1字节数据
SCL低电平期间,发送端(主机或从机)将数据位依次放到SDA线上(高位先行),然后主机(或具有时钟控制权的一方)主动将 SCL 置为高电平,接收方(从机或主机)将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送或接收一个字节。
2.4.4应答信号
每次 IIC 总线上传输一个字节(8位)后,接收方必须返回一个应答信号(ACK/NACK) ,作为对发送方的回应。发送方在此期间释放 SDA,允许接收方控制该信号线,并且发送方采样其状态以判断是 ACK 还是 NACK。
应答信号占据第9个时钟周期(SCL周期),紧跟在每个字节之后。
类型 | 含义 | SDA 状态 |
---|---|---|
ACK(Acknowledgment) | 接收成功 | SDA = 低电平 |
NACK(Not Acknowledgment) | 接收失败或未准备好 | SDA = 高电平 |
STOP信号之前的应答信号:
操作类型 | 是否应在 STOP 前发送 NACK? | 说明 |
---|---|---|
读操作 | ✅ 是 | 最后一个字节应答为 NACK |
写操作 | ❌ 否 | 最后一个字节应答为 ACK 即可 |
2.5IIC时序
一个完整的 IIC 时序 包含多个关键信号阶段:起始条件、地址帧传输、应答/非应答(ACK/NACK)、数据帧传输、重复起始条件(可选)、停止条件等。这些阶段构成了主设备与从设备之间的一次完整通信过程。
起始条件、应答/非应答、数据帧传输和停止条件在之前一节已经讲过。
2.5.1地址帧
地址帧是一个 8位的数据字节,用于标识将要通信的从设备地址和数据传输方向(读/写)。
地址帧结构(7位地址模式下):
位号 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
内容 | A6 | A5 | A4 | A3 | A2 | A1 | A0 | R/W# |
A6~A0(共7位) :表示从设备的地址。
R/W#(第0位) :表示数据传输方向。0:写操作(主设备向从设备发送数据);1:读操作(主设备从从设备读取数据)。
当主机发出起始条件之后紧跟着发送地址帧,此时所有从设备监听总线,比对收到的地址是否与自身匹配。如果匹配,则该从设备开始响应后续通信(如返回 ACK),并准备接收或发送数据。如果不匹配,从设备将继续监听下一次 START 条件。
2.5.2重复起始条件
重复起始条件是指在一次 IIC 通信过程中,主设备在没有发出 STOP 条件的前提下,再次发出一个 START 条件,从而开启一个新的地址帧传输周期。这是 I²C 协议正式支持的标准操作之一。
作用 | 描述 |
---|---|
切换读写方向 | 在连续读写操作中,用于从“写”切换为“读” |
访问多个从设备 | 在不释放总线的前提下,访问不同的从设备 |
提高通信效率 | 避免多次发送 START 和 STOP,减少通信开销 |
使用场景1:主设备先写地址和寄存器,然后切换为读数据
[START] → [地址+写] → [ACK] → [寄存器地址] → [ACK] → [Repeated START] → [地址+读] → [ACK] → [读取数据字节] → [NACK] → [STOP]
使用场景2:主设备访问多个从设备而不释放总线
[START] → [地址A+写] → [ACK] → [数据1] → [ACK] → [Repeated START] → [地址B+写] → [ACK] → [数据2] → [ACK] → [STOP]
2.5.3IIC通信时序示例 -- 指定地址写数据
[START] → [0xD0(从设备地址)+0(写)] → [ACK] → [0x19(寄存器地址)] → [ACK] → [0xAA(数据)] → [ACK] → [STOP]
2.5.4IIC通信时序示例 -- 当前地址读数据
“当前地址读数据”是指主设备在不发送新的地址帧(如寄存器地址)的情况下,直接从从设备的当前地址指针位置开始读取数据。
在许多 IIC 从设备(如 EEPROM、ADC、传感器等)中,内部都有一个 地址指针寄存器(Address Pointer Register) ,用于指示下一次读或写操作的目标地址。 例如:主机写入地址 0x05 并写入一个字节后,下次写入将发生在地址 0x06。
某些设备(如某些传感器)每次通信都需要重新发送地址。即使进行了写操作,地址指针也不会改变。OLED 显示屏 SSD1306(通常需要每次都写显存地址)。
当主设备执行以下操作时,地址指针会发生变化:
操作类型 | 地址指针行为 |
---|---|
写地址帧 + 数据帧 | 设置地址指针为指定值 |
写数据(带地址) | 设置地址指针,并写入数据后指针自动递增 |
读数据 | 从当前地址读取数据后,地址指针通常自动递增 |
[START] → [0xD0(从设备地址)+1(读)] → [ACK] → [0x0F(数据)] → [NACK] → [STOP]
2.5.5IIC通信时序示例 -- 指定地址读
[START] → [0xD0(从设备地址)+0(写)] → [ACK] → [0x19(寄存器地址)] → [ACK] → [Repeated START] → [0xD0(从设备地址)+1(读)] → [ACK] → [0xAA(数据)] → [NACK] → [STOP]
3.软件IIC和硬件IIC
3.1软件IIC
在嵌入式系统中,软件 IIC(Software IIC) 是一种通过 通用输入输出引脚(GPIO)模拟 IIC 总线协议 的方式。通过编写函数控制 SDA 和 SCL 的电平变化,并严格遵循 IIC 协议的时序规范,即可实现数据的发送和接收。
信号线 | GPIO 功能 |
---|---|
SDA(Serial Data Line) | 数据线,双向传输 |
SCL(Serial Clock Line) | 时钟线,主设备控制 |
软件IIC的优缺点:
优点 | 描述 |
---|---|
灵活 | 可使用任意 GPIO 引脚作为 SDA 和 SCL |
无需硬件支持 | 即使没有硬件 I²C 模块也可通信 |
便于调试 | 可插入打印语句观察每个步骤 |
兼容性强 | 支持各种平台(如 STM32、ESP32、Arduino、51、AVR 等) |
缺点 | 描述 |
---|---|
速度慢 | 依赖 CPU 轮询,难以达到高速模式(>400kbps) |
占用 CPU 资源 | 实时性要求高时影响性能 |
代码复杂度高 | 需处理 START/STOP、ACK/NACK、地址帧、数据帧等 |
抗干扰能力差 | 容易受中断或延时影响导致通信失败 |
3.2硬件IIC
硬件 IIC(Inter-Integrated Circuit) 是指由微控制器(MCU)或系统芯片(SoC)内部集成的专用外设模块,用于实现 高速、稳定、自动化的 IIC 总线通信,能够自动处理 I²C 协议的起始条件、地址帧、数据传输、ACK/NACK 应答、停止条件等操作。
4.软件IIC代码
使用软件IIC的方式,操作oled屏幕。主要验证软件IIC的功能,对oled屏幕的驱动,不进行说明。
4.1硬件连接
选用两个GPIO接口当作SCL、SDA。另外两根线分别接VCC、GND即可。
作者选择的SCL 接在PD13、SDA接在PD15。
4.2程序编写
4.2.1宏定义
/*********宏定义********/
#define OLED_SLAVE_ADDRESS ((unsigned char)(0x78))#define OLED_IIC_SCL_CLK_ENABLE() __HAL_RCC_GPIOD_CLK_ENABLE()
#define OLED_IIC_SCL_PORT GPIOD
#define OLED_IIC_SCL_PIN GPIO_PIN_13#define OLED_IIC_SDA_CLK_ENABLE() __HAL_RCC_GPIOD_CLK_ENABLE()
#define OLED_IIC_SDA_PORT GPIOD
#define OLED_IIC_SDA_PIN GPIO_PIN_15#define OLED_WRITE_SCL(x) do{HAL_GPIO_WritePin(OLED_IIC_SCL_PORT, OLED_IIC_SCL_PIN, (GPIO_PinState)(x));}while(0)
#define OLED_WRITE_SDA(x) do{HAL_GPIO_WritePin(OLED_IIC_SDA_PORT, OLED_IIC_SDA_PIN, (GPIO_PinState)(x));}while(0)
定义了这个设备的从机地址,连接的端口和引脚,还有两个宏函数。
OLED_WRITE_SCL(x)。当x为1时,即把SCL拉高,写0就拉低。SDA也同理。这样编写方便后续代码的使用。
4.2.2GPIO的初始化
首先打开时钟,然后配置两个GPIO引脚,注意,要配置成上拉电阻模式和开漏输出模式。速度,作者使用的低速,可以正常使用。写入初始值,都是高电平就行。
/*** @brief OLED引脚初始化函数* @param 无* @retval 无*/
void oled_gpio_init(void)
{/* 1.打开时钟 */OLED_IIC_SCL_CLK_ENABLE();OLED_IIC_SDA_CLK_ENABLE();/* 2.配置GPIO */GPIO_InitTypeDef gpio_initure = {0};gpio_initure.Pin = OLED_IIC_SCL_PIN; //SCL线gpio_initure.Mode = GPIO_MODE_OUTPUT_OD; //开漏输出模式gpio_initure.Speed = GPIO_SPEED_FREQ_LOW; //低速gpio_initure.Pull = GPIO_PULLUP; //上拉电阻HAL_GPIO_Init(OLED_IIC_SCL_PORT, &gpio_initure);gpio_initure.Pin = OLED_IIC_SDA_PIN; //SDA线HAL_GPIO_Init(OLED_IIC_SDA_PORT, &gpio_initure);/* 3.写入初始值 */HAL_GPIO_WritePin(OLED_IIC_SCL_PORT, OLED_IIC_SCL_PIN, GPIO_PIN_SET);HAL_GPIO_WritePin(OLED_IIC_SDA_PORT, OLED_IIC_SDA_PIN, GPIO_PIN_SET);
}
4.2.3起始条件
对应之前介绍的起始条件的时序图,编写代码就好。在SCL为高电平的时候,SDA从高电平置为低电平。这里也不用延时,因为每条代码在执行时也是要花费时间的,所以是有电平变化的过程的。无需担心。
/*** @brief OLED的IIC开始信号* @param 无* @retval 无*/
void oled_iic_start(void)
{OLED_WRITE_SCL(1); // 1. 拉高 SCL 时钟线OLED_WRITE_SDA(1); // 2. 拉高 SDA 数据线OLED_WRITE_SDA(0); // 3. 拉低 SDA 数据线(产生下降沿)OLED_WRITE_SCL(0); // 4. 拉低 SCL 时钟线,释放总线以便后续操作
}
4.2.4停止条件
拉高SCL,然后SDA从低电平置为高电平。
/*** @brief OLED的IIC停止信号* @param 无* @retval 无*/
void oled_iic_stop(void)
{OLED_WRITE_SCL(1); // 1. 拉高 SCL 时钟线OLED_WRITE_SDA(0); // 2. 拉低 SDA 数据线OLED_WRITE_SDA(1); // 3. 拉高 SDA 数据线(产生上升沿)
}
4.2.5检测应答信号
当发送方发送了1字节数据之后,接受方要做出应答,发送方检测应答信号,就知道通信是否正常。
/*** @brief 检测从设备是否发送 ACK 应答* @param 无* @retval 0 表示收到 ACK,1 表示收到 NACK*/
unsigned char oled_iic_check_ack(void)
{unsigned char ack;OLED_WRITE_SDA(1); // 释放 SDA,使其可以被从设备拉低OLED_WRITE_SCL(1); // 拉高 SCL,进入第 9 个时钟周期ack = OLED_READ_SDA(); // 读取 SDA 状态OLED_WRITE_SCL(0); // 拉低 SCL,完成 ACK 检测return ack; // 返回 0 表示 ACK,1 表示 NACK
}
4.2.6发送应答信号和非应答信号
当主机接收到1字节数据时要发送一个应答信号,告知从机,我收到了数据。当主设备要停止读取时,就发一个非应答信号,然后发送STOP结束这一次通信。
/*** @brief 主设备发送 ACK 应答(继续读取下一个字节)* @param 无* @retval 无*/
void oled_iic_send_ack(void)
{OLED_WRITE_SDA(0); // 拉低 SDA,表示 ACKOLED_WRITE_SCL(1); // 拉高 SCLOLED_WRITE_SCL(0); // 拉低 SCL,结束 ACKOLED_WRITE_SDA(1); // 释放 SDA(为下一次通信做准备)
}/*** @brief 主设备发送 NACK 应答(停止读取)* @param 无* @retval 无*/
void oled_iic_send_nack(void)
{OLED_WRITE_SDA(1); // 拉高 SDA,表示 NACKOLED_WRITE_SCL(1); // 拉高 SCLOLED_WRITE_SCL(0); // 拉低 SCL,结束 NACKOLED_WRITE_SDA(1); // 释放 SDA
}
4.3测试代码
上面的程序代码不完整,但是经过作者的测试,可以成功驱动oled屏幕。我会附上完整的代码供学习参考。
5.硬件IIC代码
正在制作中,敬请期待.......