STM32 进阶封神之路(三十一):硬件 IIC 深度实战 ——STM32F103 硬件 IIC 驱动 BH1750 光照传感器(底层架构 + 时序解析 + 代

0 阅读21分钟

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)SCLSCL外接 4.7KΩ 上拉电阻到 3.3V
PB7(IIC1_SDA)SDASDA外接 4.7KΩ 上拉电阻到 3.3V
3.3VVCCVCCBH1750 工作电压 3.3V
GNDGNDGND共地
GNDADDRADDRBH1750 从地址设为 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(单次)120ms1 lx
0x13高分辨率模式 2(单次)120ms0.5 lx
0x23低分辨率模式(单次)16ms4 lx
0x10高分辨率模式 1(连续)120ms1 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_leniic_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 芯片)!