STM32 进阶封神之路(三十一):硬件 IIC 深度实战 ——STM32F103 硬件 IIC 驱动 BH1750 光照传感器(底层架构 + 时序解析 + 代码落地)
上一篇我们掌握了软件模拟 IIC 的实现逻辑与 BH1750 光照采集,这一篇聚焦 STM32 的硬件 IIC 外设—— 相比软件模拟,硬件 IIC 由芯片内部硬件电路自动处理时序,无需手动编写延时和电平翻转代码,不仅开发效率更高,还能解放 CPU 资源,适合对通信稳定性和效率有要求的工业场景。
本文基于参考文档中的硬件 IIC 核心知识点,从 STM32F103 硬件 IIC 架构、外设配置、通信时序、BH1750 驱动实现,到软件 / 硬件 IIC 对比、实战排障,全程超详细拆解,帮你彻底掌握硬件 IIC 的底层原理与工程应用!
一、硬件 IIC 核心认知:为什么工业场景首选硬件 IIC?
1. 硬件 IIC 与软件 IIC 核心差异
软件 IIC 是通过 GPIO 模拟 IIC 时序,而硬件 IIC 是 STM32 内置的专用通信外设,两者核心差异如下:
表格
| 对比维度 | 软件 IIC | 硬件 IIC |
|---|---|---|
| 实现方式 | GPIO 电平翻转 + 延时模拟时序 | 硬件电路自动生成时序 |
| CPU 占用 | 高(需 CPU 全程参与) | 低(配置后自动传输,仅需中断 / 查询结果) |
| 时序精度 | 依赖延时函数,精度较低 | 硬件生成,时序严格符合 IIC 协议 |
| 通信速率 | 最高可达 400Kbps(受延时限制) | 支持标准模式(100Kbps)、快速模式(400Kbps),部分型号支持高速模式(3.4Mbps) |
| 代码复杂度 | 低(仅需封装 6 个核心时序函数) | 中(需配置外设寄存器、处理中断 / 状态机) |
| 引脚限制 | 无(任意 GPIO 均可) | 有(必须使用指定的 IIC 功能引脚) |
| 多主设备支持 | 需手动处理总线冲突 | 硬件支持总线仲裁,天然支持多主设备 |
2. STM32F103 硬件 IIC 核心资源(参考文档知识点)
STM32F103 系列内置 2 个硬件 IIC 外设(IIC1 和 IIC2),核心资源如下:
- IIC1 引脚:PB6(SCL)、PB7(SDA)(复用功能);
- IIC2 引脚:PB10(SCL)、PB11(SDA)(复用功能);
- 支持 7 位 / 10 位从地址;
- 支持主发送、主接收、从发送、从接收四种模式;
- 内置校验机制,支持应答信号自动生成与检测;
- 支持中断模式和 DMA 模式传输,适配不同场景。
3. 硬件 IIC 核心架构(底层逻辑)
STM32 硬件 IIC 外设的核心架构由以下模块组成(参考文档框图知识点):
- 时钟控制模块:由 APB1 时钟(36MHz)分频生成 IIC 总线时钟(SCL),分频系数可配置;
- 数据移位寄存器:负责数据的串并转换(发送时并行转串行,接收时串行转并行);
- 控制模块:处理 IIC 协议的起始 / 停止信号、应答信号、总线仲裁;
- 状态寄存器:实时反馈通信状态(如发送完成、接收完成、应答检测、总线忙等);
- 中断控制模块:支持传输完成、应答错误等中断,实现异步通信。
二、STM32F103 硬件 IIC 外设配置(底层寄存器 + 库函数)
硬件 IIC 的配置核心是 “时钟使能→GPIO 复用配置→IIC 参数配置→IIC 使能”,以下基于 IIC1(PB6/SCL、PB7/SDA)详细拆解配置流程。
1. 硬件接线(与软件 IIC 兼容)
表格
| STM32 引脚 | IIC 功能 | BH1750 引脚 | 备注 |
|---|---|---|---|
| PB6(IIC1_SCL) | SCL | SCL | 外接 4.7KΩ 上拉电阻到 3.3V |
| PB7(IIC1_SDA) | SDA | SDA | 外接 4.7KΩ 上拉电阻到 3.3V |
| 3.3V | VCC | VCC | BH1750 工作电压 3.3V |
| GND | GND | GND | 共地 |
| GND | ADDR | ADDR | BH1750 从地址设为 0x46(ADDR=GND) |
2. 库函数配置步骤(STM32 标准库)
(1)使能时钟(GPIO+IIC)
c
运行
#include "stm32f10x.h"
#include "delay.h"
// 硬件IIC1引脚定义
#define IIC1_SCL_PIN GPIO_Pin_6
#define IIC1_SDA_PIN GPIO_Pin_7
#define IIC1_GPIO_PORT GPIOB
#define IIC1_GPIO_CLK RCC_APB2Periph_GPIOB
#define IIC1_CLK RCC_APB1Periph_I2C1
// BH1750从地址(ADDR=GND)
#define BH1750_SLAVE_ADDR 0x46
void IIC1_Hardware_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
I2C_InitTypeDef I2C_InitStruct;
// 1. 使能GPIO和IIC1时钟
RCC_APB2PeriphClockCmd(IIC1_GPIO_CLK, ENABLE); // GPIO时钟(APB2)
RCC_APB1PeriphClockCmd(IIC1_CLK, ENABLE); // IIC1时钟(APB1,36MHz)
// 2. 配置GPIO为复用开漏输出(IIC协议要求)
GPIO_InitStruct.GPIO_Pin = IIC1_SCL_PIN | IIC1_SDA_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏输出(关键!)
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(IIC1_GPIO_PORT, &GPIO_InitStruct);
// 3. 配置IIC1参数
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; // IIC模式
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; // 占空比2(快速模式下为16/9)
I2C_InitStruct.I2C_OwnAddress1 = 0x00; // 主机地址(无需设置,仅从机模式需要)
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; // 使能应答
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 7位地址模式
I2C_InitStruct.I2C_ClockSpeed = 100000; // 通信速率100Kbps(标准模式)
// 4. 初始化IIC1
I2C_Init(I2C1, &I2C_InitStruct);
// 5. 使能IIC1
I2C_Cmd(I2C1, ENABLE);
// 6. 使能应答(默认已使能,再次确认)
I2C_AcknowledgeConfig(I2C1, ENABLE);
}
(2)关键配置说明
- GPIO 模式:必须配置为
GPIO_Mode_AF_OD(复用开漏输出),这是 IIC 协议的核心要求 —— 开漏输出支持 “线与” 特性,配合上拉电阻实现总线空闲时的高电平; - 时钟速率:标准模式(100Kbps)分频系数 = APB1 时钟(36MHz)/(2× 时钟速率)=36MHz/(2×100Kbps)=180,硬件自动计算分频系数;
- 应答使能:
I2C_Ack_Enable使能后,接收数据时会自动发送应答信号,无需手动处理; - 占空比:标准模式下使用
I2C_DutyCycle_2(占空比 50%),快速模式(400Kbps)使用I2C_DutyCycle_16_9(16/9 占空比)。
3. 硬件 IIC 核心通信函数实现
基于硬件 IIC 的状态机,封装发送 / 接收、寄存器读写等通用函数,适配 BH1750 等 IIC 设备。
(1)等待 IIC 总线空闲
c
运行
// 等待IIC总线空闲,超时返回0,成功返回1
uint8_t IIC1_Wait_Idle(void) {
uint32_t timeout = 10000;
while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)) {
if (--timeout == 0) {
printf("IIC总线忙超时!\r\n");
return 0;
}
}
return 1;
}
(2)发送起始信号
c
运行
// 发送IIC起始信号,返回1=成功,0=失败
uint8_t IIC1_Send_Start(void) {
if (!IIC1_Wait_Idle()) return 0;
// 发送起始信号
I2C_GenerateSTART(I2C1, ENABLE);
// 等待起始信号发送完成(EV5事件:起始信号已发送)
uint32_t timeout = 10000;
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) {
if (--timeout == 0) {
printf("起始信号发送超时!\r\n");
return 0;
}
}
return 1;
}
(3)发送停止信号
c
运行
// 发送IIC停止信号
void IIC1_Send_Stop(void) {
// 发送停止信号
I2C_GenerateSTOP(I2C1, ENABLE);
// 等待停止信号发送完成
uint32_t timeout = 10000;
while (!I2C_GetFlagStatus(I2C1, I2C_FLAG_STOPF)) {
if (--timeout == 0) {
printf("停止信号发送超时!\r\n");
break;
}
}
}
(4)发送从设备地址 + 读写命令
c
运行
// 发送从设备地址+读写命令,addr=7位从地址,read=1读,0写,返回1=成功
uint8_t IIC1_Send_Addr(uint8_t addr, uint8_t read) {
// 拼接地址+读写位(第0位为读写位,1=读,0=写)
uint8_t addr_byte = (addr << 1) | (read ? 0x01 : 0x00);
// 发送地址字节
I2C_SendData(I2C1, addr_byte);
// 等待地址发送完成并收到应答(EV6事件:地址已发送,收到应答)
uint32_t timeout = 10000;
if (read) {
// 读模式:等待EV6事件(I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) {
if (--timeout == 0) {
printf("读地址应答超时!\r\n");
return 0;
}
}
} else {
// 写模式:等待EV6事件(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {
if (--timeout == 0) {
printf("写地址应答超时!\r\n");
return 0;
}
}
}
return 1;
}
(5)发送 1 字节数据
c
运行
// 发送1字节数据,data=要发送的数据,返回1=成功
uint8_t IIC1_Send_Byte(uint8_t data) {
// 发送数据
I2C_SendData(I2C1, data);
// 等待数据发送完成并收到应答(EV8_2事件:数据已发送,收到应答)
uint32_t timeout = 10000;
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {
if (--timeout == 0) {
printf("数据发送超时!\r\n");
return 0;
}
}
return 1;
}
(6)接收 1 字节数据
c
运行
// 接收1字节数据,ack=1发送应答,0发送非应答,返回接收的数据
uint8_t IIC1_Receive_Byte(uint8_t ack) {
uint8_t data = 0;
// 配置应答模式
I2C_AcknowledgeConfig(I2C1, ack ? ENABLE : DISABLE);
// 等待数据接收完成(EV7事件:数据已接收)
uint32_t timeout = 10000;
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) {
if (--timeout == 0) {
printf("数据接收超时!\r\n");
return 0;
}
}
// 读取数据
data = I2C_ReceiveData(I2C1);
// 最后一字节发送非应答后,等待非应答生效
if (!ack) {
I2C_AcknowledgeConfig(I2C1, ENABLE); // 恢复应答使能,供下次使用
}
return data;
}
(7)通用读写 API(适配 IIC 设备寄存器操作)
c
运行
// 向IIC从设备指定寄存器写入1字节数据
uint8_t IIC1_Write_Reg(uint8_t slave_addr, uint8_t reg_addr, uint8_t data) {
// 1. 发送起始信号
if (!IIC1_Send_Start()) return 0;
// 2. 发送从设备地址+写命令
if (!IIC1_Send_Addr(slave_addr, 0)) {
IIC1_Send_Stop();
return 0;
}
// 3. 发送寄存器地址
if (!IIC1_Send_Byte(reg_addr)) {
IIC1_Send_Stop();
return 0;
}
// 4. 发送数据
if (!IIC1_Send_Byte(data)) {
IIC1_Send_Stop();
return 0;
}
// 5. 发送停止信号
IIC1_Send_Stop();
delay_ms(1);
return 1;
}
// 从IIC从设备指定寄存器读取len字节数据
uint8_t IIC1_Read_Reg(uint8_t slave_addr, uint8_t reg_addr, uint8_t len, uint8_t *buf) {
if (len == 0 || buf == NULL) return 0;
// 1. 发送起始信号
if (!IIC1_Send_Start()) return 0;
// 2. 发送从设备地址+写命令(先写寄存器地址)
if (!IIC1_Send_Addr(slave_addr, 0)) {
IIC1_Send_Stop();
return 0;
}
// 3. 发送寄存器地址
if (!IIC1_Send_Byte(reg_addr)) {
IIC1_Send_Stop();
return 0;
}
// 4. 重新发送起始信号(切换为读模式)
if (!IIC1_Send_Start()) {
IIC1_Send_Stop();
return 0;
}
// 5. 发送从设备地址+读命令
if (!IIC1_Send_Addr(slave_addr, 1)) {
IIC1_Send_Stop();
return 0;
}
// 6. 读取len字节数据
for (uint8_t i = 0; i < len; i++) {
buf[i] = IIC1_Receive_Byte(i != len - 1); // 最后一字节发送非应答
}
// 7. 发送停止信号
IIC1_Send_Stop();
delay_ms(1);
return 1;
}
三、硬件 IIC 驱动 BH1750 光照传感器(实战落地)
基于上述硬件 IIC 通用 API,驱动 BH1750 实现光照强度采集,步骤与软件 IIC 一致,但通信效率和稳定性显著提升。
1. BH1750 核心指令(参考文档指令集合)
表格
| 指令代码 | 功能描述 | 测量时间 | 分辨率 |
|---|---|---|---|
| 0x00 | 断电模式 | - | - |
| 0x01 | 上电模式 | - | - |
| 0x03 | 高分辨率模式 1(单次) | 120ms | 1 lx |
| 0x13 | 高分辨率模式 2(单次) | 120ms | 0.5 lx |
| 0x23 | 低分辨率模式(单次) | 16ms | 4 lx |
| 0x10 | 高分辨率模式 1(连续) | 120ms | 1 lx |
2. BH1750 硬件 IIC 驱动代码
c
运行
#include "bh1750_hardware.h"
#include "iic_hardware.h"
#include "delay.h"
// BH1750指令定义
#define BH1750_POWER_DOWN 0x00
#define BH1750_POWER_ON 0x01
#define BH1750_HIGH_RES1 0x03
#define BH1750_HIGH_RES2 0x13
#define BH1750_LOW_RES 0x23
// BH1750初始化
uint8_t BH1750_Hardware_Init(void) {
// 初始化硬件IIC1
IIC1_Hardware_Init();
delay_ms(10);
// 1. 发送上电指令
if (!IIC1_Write_Reg(BH1750_SLAVE_ADDR, 0x00, BH1750_POWER_ON)) {
printf("BH1750上电失败!\r\n");
return 0;
}
delay_ms(10);
// 2. 设置高分辨率模式1(单次测量)
if (!IIC1_Write_Reg(BH1750_SLAVE_ADDR, 0x00, BH1750_HIGH_RES1)) {
printf("BH1750模式设置失败!\r\n");
return 0;
}
delay_ms(10);
printf("BH1750硬件IIC初始化成功!\r\n");
return 1;
}
// 读取光照强度(单位:lx)
float BH1750_Hardware_Read_Lux(void) {
uint8_t data_buf[2];
uint16_t raw_data = 0;
float lux = 0.0;
// 等待测量完成(高分辨率模式需120ms)
delay_ms(120);
// 读取2字节数据(BH1750数据寄存器地址为0x00)
if (!IIC1_Read_Reg(BH1750_SLAVE_ADDR, 0x00, 2, data_buf)) {
printf("光照数据读取失败!\r\n");
return 0.0;
}
// 拼接16位数据(高字节在前,低字节在后)
raw_data = (data_buf[0] << 8) | data_buf[1];
// 计算光照强度(高分辨率模式1:lux = 原始数据 / 1.2)
lux = raw_data / 1.2;
// 测量完成后断电,降低功耗
IIC1_Write_Reg(BH1750_SLAVE_ADDR, 0x00, BH1750_POWER_DOWN);
return lux;
}
3. 主函数整合与测试
c
运行
#include "stm32f10x.h"
#include "usart.h"
#include "bh1750_hardware.h"
#include "delay.h"
int main(void) {
float lux_value = 0.0;
// 初始化系统时钟(72MHz)
SystemInit();
// 初始化串口1(115200bps,打印数据)
USART1_Init(115200);
// 初始化BH1750(硬件IIC驱动)
if (!BH1750_Hardware_Init()) {
while (1) {
delay_ms(1000);
}
}
printf("STM32硬件IIC+BH1750光照采集系统\r\n");
printf("=======================================\r\n\r\n");
while (1) {
// 读取光照强度
lux_value = BH1750_Hardware_Read_Lux();
// 打印结果
printf("当前光照强度:%.1f lx\r\n", lux_value);
// 光照状态分级
if (lux_value < 10) {
printf("光照状态:极弱(夜间环境)\r\n");
} else if (lux_value < 100) {
printf("光照状态:弱(室内昏暗)\r\n");
} else if (lux_value < 1000) {
printf("光照状态:中等(室内正常)\r\n");
} else if (lux_value < 10000) {
printf("光照状态:强(室外阴天)\r\n");
} else {
printf("光照状态:极强(室外晴天)\r\n");
}
printf("---------------------------------------\r\n\r\n");
delay_ms(2000); // 每2秒采集一次
}
}
4. 测试效果(串口打印)
plaintext
STM32硬件IIC+BH1750光照采集系统
=======================================
BH1750硬件IIC初始化成功!
当前光照强度:456.8 lx
光照状态:中等(室内正常)
---------------------------------------
当前光照强度:9230.5 lx
光照状态:强(室外阴天)
---------------------------------------
当前光照强度:13500.2 lx
光照状态:极强(室外晴天)
---------------------------------------
四、硬件 IIC 中断模式实现(解放 CPU)
上述代码采用查询模式,CPU 需等待通信完成,适合简单场景。对于复杂系统,推荐使用中断模式,实现异步通信,解放 CPU 资源。
1. 中断模式配置(添加中断初始化)
c
运行
// 硬件IIC1中断初始化
void IIC1_Hardware_IT_Init(void) {
NVIC_InitTypeDef NVIC_InitStruct;
// 使能IIC1事件中断和错误中断
I2C_ITConfig(I2C1, I2C_IT_EVT, ENABLE); // 事件中断(如发送完成、接收完成)
I2C_ITConfig(I2C1, I2C_IT_ERR, ENABLE); // 错误中断(如应答错误、总线错误)
// 配置NVIC中断优先级
NVIC_InitStruct.NVIC_IRQChannel = I2C1_EV_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
NVIC_InitStruct.NVIC_IRQChannel = I2C1_ER_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);
}
2. 中断服务函数(处理通信事件)
c
运行
// 全局变量(中断与主函数共享)
uint8_t iic_tx_buf[16]; // 发送缓冲区
uint8_t iic_rx_buf[16]; // 接收缓冲区
uint8_t iic_tx_len = 0; // 发送长度
uint8_t iic_rx_len = 0; // 接收长度
uint8_t iic_tx_cnt = 0; // 发送计数
uint8_t iic_rx_cnt = 0; // 接收计数
uint8_t iic_state = 0; // 通信状态:0=空闲,1=发送中,2=接收中
// IIC1事件中断服务函数
void I2C1_EV_IRQHandler(void) {
switch (I2C_GetLastEvent(I2C1)) {
// 主模式下发送起始信号完成(EV5)
case I2C_EVENT_MASTER_MODE_SELECT:
if (iic_state == 1) {
// 发送从设备地址+写命令
I2C_SendData(I2C1, (BH1750_SLAVE_ADDR << 1) | 0x00);
} else if (iic_state == 2) {
// 发送从设备地址+读命令
I2C_SendData(I2C1, (BH1750_SLAVE_ADDR << 1) | 0x01);
}
break;
// 主发送模式地址发送完成(EV6)
case I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED:
// 发送寄存器地址(BH1750数据寄存器地址0x00)
I2C_SendData(I2C1, 0x00);
break;
// 主发送模式数据发送完成(EV8_2)
case I2C_EVENT_MASTER_BYTE_TRANSMITTED:
if (iic_tx_cnt < iic_tx_len) {
// 继续发送下一字节
I2C_SendData(I2C1, iic_tx_buf[iic_tx_cnt++]);
} else {
if (iic_state == 1) {
// 发送完成,发送停止信号
I2C_GenerateSTOP(I2C1, ENABLE);
iic_state = 0; // 恢复空闲状态
} else if (iic_state == 2) {
// 写寄存器完成,重新发送起始信号切换为读模式
I2C_GenerateSTART(I2C1, ENABLE);
}
}
break;
// 主接收模式地址发送完成(EV6)
case I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED:
if (iic_rx_len == 1) {
// 仅接收1字节,提前禁用应答
I2C_AcknowledgeConfig(I2C1, DISABLE);
}
break;
// 主接收模式数据接收完成(EV7)
case I2C_EVENT_MASTER_BYTE_RECEIVED:
iic_rx_buf[iic_rx_cnt++] = I2C_ReceiveData(I2C1);
if (iic_rx_cnt < iic_rx_len) {
if (iic_rx_cnt == iic_rx_len - 1) {
// 最后一字节前禁用应答
I2C_AcknowledgeConfig(I2C1, DISABLE);
}
} else {
// 接收完成,发送停止信号
I2C_GenerateSTOP(I2C1, ENABLE);
I2C_AcknowledgeConfig(I2C1, ENABLE); // 恢复应答
iic_state = 0; // 恢复空闲状态
}
break;
default:
break;
}
}
// IIC1错误中断服务函数
void I2C1_ER_IRQHandler(void) {
// 应答错误
if (I2C_GetITStatus(I2C1, I2C_IT_AF)) {
I2C_ClearITPendingBit(I2C1, I2C_IT_AF);
printf("IIC应答错误!\r\n");
}
// 总线错误
if (I2C_GetITStatus(I2C1, I2C_IT_BERR)) {
I2C_ClearITPendingBit(I2C1, I2C_IT_BERR);
printf("IIC总线错误!\r\n");
}
// 停止信号错误
if (I2C_GetITStatus(I2C1, I2C_IT_STOPF)) {
I2C_ClearITPendingBit(I2C1, I2C_IT_STOPF);
}
// 重置IIC状态
I2C_Cmd(I2C1, DISABLE);
I2C_Cmd(I2C1, ENABLE);
iic_state = 0;
}
3. 中断模式下 BH1750 数据读取
c
运行
// 中断模式发送BH1750指令
void BH1750_IT_Send_Cmd(uint8_t cmd) {
while (iic_state != 0); // 等待空闲
iic_state = 1; // 标记为发送中
iic_tx_len = 1; // 发送长度1字节(指令)
iic_tx_cnt = 0; // 发送计数清零
iic_tx_buf[0] = cmd; // 指令存入发送缓冲区
// 发送起始信号
I2C_GenerateSTART(I2C1, ENABLE);
}
// 中断模式读取BH1750数据
void BH1750_IT_Read_Data(void) {
while (iic_state != 0); // 等待空闲
iic_state = 2; // 标记为接收中
iic_rx_len = 2; // 接收长度2字节
iic_rx_cnt = 0; // 接收计数清零
// 发送起始信号
I2C_GenerateSTART(I2C1, ENABLE);
}
// 主函数中使用中断模式
int main(void) {
// ... 其他初始化 ...
IIC1_Hardware_IT_Init(); // 初始化IIC中断
while (1) {
// 上电+发送测量指令
BH1750_IT_Send_Cmd(BH1750_POWER_ON);
delay_ms(10);
BH1750_IT_Send_Cmd(BH1750_HIGH_RES1);
delay_ms(120);
// 读取数据
BH1750_IT_Read_Data();
while (iic_state != 0); // 等待接收完成
// 计算光照强度
uint16_t raw_data = (iic_rx_buf[0] << 8) | iic_rx_buf[1];
float lux = raw_data / 1.2;
printf("中断模式光照强度:%.1f lx\r\n", lux);
// 断电
BH1750_IT_Send_Cmd(BH1750_POWER_DOWN);
delay_ms(2000);
}
}
五、硬件 IIC 常见问题与避坑指南(参考文档注意事项)
1. 总线忙超时(I2C_FLAG_BUSY 一直为 1)
- 原因 1:GPIO 模式配置错误(未使用复用开漏输出);解决:确保 GPIO 模式为
GPIO_Mode_AF_OD,而非推挽输出或普通开漏输出; - 原因 2:上拉电阻缺失或阻值过大;解决:SCL 和 SDA 必须外接 4.7KΩ~10KΩ 上拉电阻,否则总线无法稳定在高电平;
- 原因 3:通信异常导致总线未释放(如未发送停止信号);解决:出现忙超时后,可通过 “禁用 IIC→重新使能 IIC” 重置总线;
- 原因 4:多主设备冲突;解决:确保总线上仅一个主设备,或使用硬件 IIC 的总线仲裁功能。
2. 应答错误(I2C_IT_AF 中断触发)
- 原因 1:从设备地址错误(ADDR 引脚接线与代码不一致);解决:ADDR=GND 时地址为 0x46,ADDR=VCC 时为 0x23,确保代码与接线一致;
- 原因 2:从设备未上电或接线错误;解决:检查 BH1750 供电(3.3V)和 SCL/SDA 接线,确保无接反;
- 原因 3:发送数据过快(从设备未准备好);解决:在发送指令后添加适当延时(如 10ms),给从设备足够的响应时间;
- 原因 4:IIC 时钟速率过高;解决:将
I2C_ClockSpeed从 400Kbps 改为 100Kbps,提升兼容性。
3. 数据读取错误(固定值或乱码)
-
原因 1:数据拼接顺序错误;解决:BH1750 数据为高字节在前、低字节在后,确保
raw_data = (data_buf[0] << 8) | data_buf[1]; -
原因 2:测量时间不足;解决:高分辨率模式必须等待≥120ms,低分辨率模式≥16ms,否则数据未更新;
-
原因 3:中断模式下缓冲区溢出或计数错误;解决:确保
iic_tx_len和iic_rx_len正确设置,中断服务函数中计数逻辑无误; -
原因 4:未禁用 JTAG 功能(PB6/PB7 为 JTAG 引脚);解决:若使用 STM32F103 的 PB6/PB7 作为 IIC 引脚,需禁用 JTAG 功能,释放引脚:
c
运行
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); // 禁用JTAG,保留SWD
4. 硬件 IIC 与软件 IIC 共存冲突
- 原因:两者使用同一组 GPIO 引脚,或总线电平冲突;解决:若需共存,使用不同的 GPIO 引脚(如硬件 IIC1 用 PB6/PB7,软件 IIC 用其他 GPIO),或在切换使用时重新配置 GPIO 模式。
六、软件 IIC 与硬件 IIC 选型指南(实战建议)
1. 优先选择软件 IIC 的场景
- 快速原型开发(无需关注引脚限制,任意 GPIO 均可);
- 低速率、低要求的简单场景(如偶尔读取传感器数据);
- 总线上设备较少(≤2 个),无多主设备需求;
- 对代码移植性要求高(需适配不同 STM32 型号或其他 MCU)。
2. 优先选择硬件 IIC 的场景
- 工业级产品(对时序精度和稳定性要求高);
- 高速通信场景(需 400Kbps 快速模式);
- 多主设备或多从设备场景(硬件仲裁更可靠);
- 复杂系统(需解放 CPU,处理其他任务);
- 批量数据传输(如 IIC EEPROM 读写、OLED 屏幕显示)。
七、总结:硬件 IIC 核心要点与进阶方向
1. 核心要点回顾
- 硬件 IIC 本质:STM32 内置外设自动生成 IIC 时序,核心是 “GPIO 复用配置 + IIC 参数配置 + 状态机 / 中断处理”;
- 关键配置:GPIO 必须为
GPIO_Mode_AF_OD,时钟速率匹配从设备,应答使能; - 通信流程:起始信号→地址 + 读写命令→数据传输→停止信号,硬件自动处理应答和时序;
- 避坑关键:上拉电阻不可少、地址匹配接线、测量时间充足、禁用 JTAG 释放引脚。
2. 进阶学习方向
- DMA 模式:硬件 IIC 支持 DMA 传输,进一步降低 CPU 占用,适合大批量数据传输;
- 多从设备通信:在同一 IIC 总线上挂载多个设备(如 BH1750+AT24C02+OLED),通过从地址区分;
- 低功耗优化:结合 STM32 低功耗模式,实现传感器定时唤醒采集,硬件 IIC 中断唤醒 CPU;
- 故障诊断:使用逻辑分析仪抓取硬件 IIC 波形,对比协议时序,排查通信异常;
- 其他型号适配:学习 STM32F4/F7/H7 系列硬件 IIC 配置,掌握不同系列的差异。
掌握硬件 IIC 后,你已具备工业级 IIC 通信的开发能力,可轻松驱动各类 IIC 外设(如 MPU6050、BME280、AT24C02、OLED 等)。下一篇我们将学习 STM32 的 SPI 通信协议,聚焦高速数据传输场景(如 SPI 闪存、触摸屏、ADC 芯片)!