资讯详情

STM32驱动BH1750光照传感器:软件I2C通信详解与实战

📅 2026/9/24 15:14:23 | 华诺云谱 👁 阅读
STM32驱动BH1750光照传感器:软件I2C通信详解与实战
要驱动一块BH1750光照传感器本质就是在STM32F103C8T6上跑通I2C通信再按芯片手册把光强寄存器读回来换算成勒克斯。这件事听起来门槛不高但真上手做的时候接线、地址、时序、寄存器配置、数据拼接任何一个环节掉链子读回来的数据都是错的——要么是0要么是乱七八糟的跳变甚至总线直接卡死。这篇文章我按自己实际跑通这个项目的顺序来写从选型思路、原理图细节、寄存器手册怎么看到软件I2C的完整代码怎么一步步写出来最后附上我实测过程中踩过的坑和排查套路。新手照着做半小时内把光照数据从串口打出来是完全可以的有基础的朋友也可以直接跳到代码和问题排查部分看有没有戳中你之前迷惑的地方。1. 项目整体思路与方案选型1.1 为什么是BH1750而不是光敏电阻或光敏二极管做光照检测的方案其实不少。最便宜的是光敏电阻配上电阻分压接ADC电路简单成本极低但缺点也很明显器件一致性差、温度漂移大、输出是非线性的而且不同批次的光敏电阻参数差异很大生产调试很恶心。光敏二极管相对好一些但需要额外的电流转电压电路和运放还要校准对新手不友好。BH1750就不一样了。它内部集成了光敏二极管、运算放大器和ADC直接输出数字量数字接口是I2C片上还带了校正电路。实测下来的好处就是一致性非常好同一个光照条件下不同板子读出来的值相差很小而且光谱响应经过处理比较接近人眼对可见光的感知。对大多数环境光检测、智能家居、自动调节亮度的应用来说这个精度和线性度都够用了。另外一个很关键的点是BH1750的测量范围和分辨率。它的量程是1到65535勒克斯分辨率最低可以到0.5勒克斯。室内灯光大概几百勒克斯晴天户外可以到几万勒克斯这个量程覆盖了绝大多数场景。分辨率到0.5勒克斯是什么概念就是你在昏暗的房间里稍微把手机屏幕亮度调高一点它都能感知到变化这对做自动亮度调节的项目来说非常实用。1.2 STM32F103C8T6最小系统板和资源盘点STM32F103C8T6这块芯片在国产开发板圈子里可以说是“街U”了。Cortex-M3内核主频72MHzFlash是64KBRAM是20KB价格便宜资料多车充、无人机飞控、各种小家电主控板都能看到它的身影。对于驱动BH1750这种低速传感器它的性能严重过剩但好处是库函数成熟出问题搜一下遍地是答案。网上常说的STM32F103C8T6最小系统板就是一颗芯片加晶振、复位电路、稳压电路、USB转串口芯片的成品小板子引脚全部引出来面包板或者杜邦线直接就能用。这里涉及一个关键知识点C8T6有GPIOA到GPIOB共两组GPIO每组16个引脚还有GPIOC的13、14、15三个引脚但不推荐用PC13到PC15做I2C因为这三个引脚在调试模式下有特殊行为而且外部中断和RTC相关功能会牵扯。驱动BH1750一般用PB6、PB7或者PB8、PB9这两个组合正好是芯片上的I2C1和I2C2外设引脚后面如果要切硬件I2C很方便。板子供电要注意一点最小系统板上的3.3V稳压芯片一般能输出几百毫安电流带一个BH1750模块毫无压力。但如果你还同时挂了OLED、舵机、WiFi模块之类的最好用外部稳压供电别都挤在板载LDO上实测下来板载LDO发热严重的时候芯片复位和各种奇怪问题都会冒出来。1.3 通信协议选型软件I2C还是硬件I2CBH1750是I2C接口STM32F103C8T6自带两个硬件I2C外设。但老玩家都知道F1系列的硬件I2C在早期固件库时代确实有一些诡异的bug比如总线忙标志清除不掉、主模式发送时状态机混乱网上“硬件I2C很难用”的传说就是这么来的。虽然实际上多数是配置问题不是芯片物理缺陷但对于新手来说直接上软件I2C反而更稳妥。软件I2C就是用两个GPIO模拟I2C时序拉高拉低、翻转时钟代码你想怎么控制就怎么控制出了问题也好调试。BH1750的I2C时钟频率最高支持400kHz也就是Fast Mode我们软件模拟跑在100kHz左右完全没问题毕竟光照传感器对实时性要求不高。而且软件I2C还有一个大优势任意两个GPIO都能用不绑定硬件外设引脚布线更自由。我这次就采用标准库加软件I2C的方式代码逻辑完全透明每一步时序都能在逻辑分析仪上看清楚。等跑通了这套流程你要是想切到硬件I2C或者HAL库我后面也会给出改写思路。2. 硬件接线与原理解析2.1 BH1750模块的引脚定义与接线清单市面上卖的BH1750模块大多是5V供电兼容的版本板载了稳压电路和I2C电平转换电路这种模块用起来方便。也有一种是纯芯片的贴片小板没有稳压只能接3.3V。买模块的时候看清介绍接线逻辑不一样。常用的模块引脚一般是四个VCC、GND、SCL、SDA部分模块还多一个ADDR引脚。ADDR引脚用来设置I2C设备地址拉低接地时地址是0x23拉高接VCC时地址是0x5C悬空默认内部下拉相当于低电平。没有引出ADDR的模块地址固定是0x23。这一点后面写代码时很关键如果你连续读到的地址应答都是NACK先检查模块上有没有ADDR引脚以及它接了什么电平。接线清单很简短VCC接3.3V如果模块带板载稳压也可以接5V但还是建议3.3V统一减少意外风险GND接GNDSCL接PB6SDA接PB7这里我选了PB6和PB7是因为它们同时是I2C1的SCL和SDA引脚如果你后续想用硬件I2C直接用I2C1外设引脚不用改。如果你想留出PB6和PB7给别的功能那SCL、SDA也可以随便定义到其他GPIO软件I2C对引脚没有任何约束。但要注意如果换用了其他引脚代码里的GPIO初始化函数必须同步修改别只改一半。2.2 I2C上拉电阻为什么不能省略I2C协议的物理层是开漏结构也就是说设备只能主动把SCL和SDA拉低不能主动拉高高电平全靠外部上拉电阻。没有上拉电阻总线根本拉不到高电平通信必然失败。很多BH1750模块板上已经自带了两个4.7k欧姆左右的上拉电阻到VCC所以直接接STM32就能用。但如果你用的是纯芯片或者自己画的PCB那上拉电阻一定要加。常见取值是4.7k欧姆供电3.3V时没问题如果总线长度比较长、设备数量多可以改成2.2k欧姆但功耗会高一点。这里有个容易忽略的坑如果你的BH1750模块是5V供电版本它的上拉电阻可能接到了5V这时SCL和SDA引脚上就会出现高于3.3V的电平。STM32F103C8T6的GPIO引脚大部分是5V容忍的FT类型PB6和PB7正好是FT引脚所以问题不大。但如果不确定自己的引脚是不是5V容忍建议统一用3.3V给模块供电避免长期高压冲击芯片引脚。2.3 供电、滤波与电平匹配的细节BH1750的工作电压范围是2.4V到3.6V内核电路完全可以在3.3V下正常工作。如果你的模块是3.3V版本那VCC接STM32的3.3V输出就行。如果是5V模块VCC接5V也可以因为模块内部有稳压SDA和SCL的电平转换电路也已经处理好了。供电的稳定性直接影响ADC采样的准确度。BH1750内部虽然有稳压但纹波太大会导致测量数据波动。最小系统板的3.3V一般是AMS1117之类的LDO输出的纹波很小直接供电没问题。但如果你是把板子接到电脑USB口供电注意USB口供电质量参差不齐有的劣质HUB输出电压偏低会导致整个系统工作不稳定。我遇到过串口打印乱码、传感器读数异常最后发现是USB延长线压降太大换了一个口就正常了。还有一个细节模块上的VCC和GND之间最好并一个0.1uF的陶瓷电容做去耦很多成品模块上已经有了。如果自己搭电路别忘了。这个电容不是玄学是数字电路稳定工作的基本保障高频噪声会被它滤掉一部分。2.4 面包板接线的坑接触不良和杜邦线氧化标题里提到了“怎么算插入在面包板上”这个确实是个高频问题。STM32F103C8T6最小系统板是双排针设计可以直接插到面包板中间的凹槽里板子一边的排针插在面包板的上半区另一边插在下半区这样就自然把两侧引脚分开了。插的时候注意方向板子上的USB口朝外方便插线。面包板的内部金属簧片用久了或者插过粗的杜邦线会出现接触不良。现象就是代码明明没问题但I2C读取时不时失败。解决方法是换一排面包板孔位或者直接焊转接板。杜邦线也一样母头用久了氧化会导致接触电阻变大特别是SDA这种双向数据线接触电阻大了信号边沿变缓通信就容易出错。排查问题的时候优先换线这能省下大量无用功。3. BH1750驱动原理与数据读取方法3.1 从寄存器视角理解BH1750BH1750内部本质上就是一个光强度转数字的ADC但它没有传统意义上的“寄存器地址”而是通过I2C指令来控制。你给它发一个指令字节它就执行对应的操作比如上电、开始测量、设置测量模式。指令字节分两类。一类是测量指令比如0x10表示高分辨率模式1勒克斯分辨率0x20表示高分辨率模式20.5勒克斯分辨率0x13表示低分辨率模式4勒克斯分辨率。我们要读取光照数据一般的流程是发送上电指令0x01发送测量指令比如0x10等待测量完成这个时间取决于测量模式发起读操作读取2字节数据把2字节拼接右移一位或者除以1.2得到实际勒克斯值这里有一个新手最常见的疑惑“为什么读出来的16位数据要右移一位”答案和分辨率有关。在连续H分辨率模式0x10下数据寄存器的高8位和低8位拼接后每1LSB代表1勒克斯所以不用移位直接换算就是光照值。但在高分辨率模式20x20下1LSB代表0.5勒克斯所以需要右移一位。而如果用了MTreg延长测量时间还要按公式换算这个后面单独讲。3.2 测量模式怎么选分辨率、测量时间与功耗的平衡BH1750的测量模式可以粗分为一次性测量和连续测量两大类。一次性测量就是发一次指令测一次数据读走后芯片自动进入低功耗待机连续测量就是芯片持续测量数据不断刷新你随时可以读到最新数据。从应用场景来看电池供电的便携设备建议用一次性测量。平时芯片休眠需要光照数据时唤醒测一次测完继续休眠功耗极低。需要实时监控光照变化的场景比如智能灯光的自动调节、植物补光控制建议用连续测量省去每次发起测量和等待的时间。分辨率方面普通环境光检测用1勒克斯的H分辨率就够了0.5勒克斯分辨率对低照度有优势但测量时间更长。低分辨率模式虽然速度快但噪声相对大不推荐用。如果你看了数据手册还会发现一个叫“测量时间寄存器”的东西指令是0x40加上一个7位的MTreg值。默认MTreg是69对应的测量时间大约是120毫秒。把MTreg调大测量时间变长灵敏度变高分辨率可以进一步提升把MTreg调小测量时间变短适合快速响应但灵敏度降低。这个寄存器是很多教程不讲的但实际做高精度项目时非常有用。比如你要在很暗的环境下检测极其微弱的光变化就可以把MTreg调到254分辨率能到0.1勒克斯级别但测量时间会拉长到好几秒功耗也相应增加。3.3 光强换算从“寄存器值”到“勒克斯”的完整公式读回来的16位原始数据怎么变成我们熟悉的勒克斯值分两种情况。第一种是用默认MTreg69高分辨率模式H分辨率1勒克斯/位光照值 原始数据16位 / 1.2单位勒克斯。高分辨率模式20.5勒克斯/位光照值 原始数据16位 / 2.4。低分辨率模式4勒克斯/位光照值 原始数据16位 / 0.3。看到这里是不是觉得有点乱其实本质是因为每个模式的LSB权重不一样。H模式下1LSB实际等于1/1.2勒克斯模式2下1LSB等于0.5/1.2勒克斯低分辨率模式下1LSB等于4/1.2勒克斯。所以拿SensorLux做单位直接输出时不处理的话就是原始数据除以1.2。第二种是改了MTreg的情况。假设你把MTreg设成了X测量时间变成了原来默认的X/69倍同样的光照条件下读数也会按比例变化。换算公式是实际勒克斯 原始数据 × 69.0 / X / 1.2这个公式的意思是MTreg变大等效分辨率提高同样的光强下原始数值更大所以要按比例折算回来。我调试的时候把MTreg从69改成138读到的室内照度从300多变成了600多如果不换算就直接以为是光照翻倍了其实只是分辨率变了。最后还有一个小细节当测量值超过量程上限时寄存器会停留在0xFFFF也就是“溢出”状态。这种情况下换算出来的值可能是个很大的数但不代表真实光照应该做溢出处理在代码里判断一下返回一个“超量程”的标志。3.4 软件I2C时序为什么必须严格按照字节格式来I2C协议的时序其实很简单起始条件SCL高电平期间SDA拉低、停止条件SCL高电平期间SDA拉高、数据有效性SCL高电平期间SDA电平稳定、应答信号低电平有效。BH1750的I2C从机地址是7位0x23写方向时地址字节是0x46读方向时是0x47。软件模拟的难点不是“会不会写”而是“节奏控制”。GPIO翻转的速度太快可能会导致信号毛刺太慢效率又低。我一般用一个延时函数控制在100kHz左右的时钟频率也就是每个SCL周期10微秒左右。用逻辑分析仪看波形时SDA和SCL的上升沿、下降沿要干净不能有回勾。在模拟I2C时有一个特别容易被忽视的点读数据时最后一个字节结束时要发送NACK保持SDA高电平然后才发停止条件。如果你在最后一个字节发了ACKBH1750会继续往外推数据总线上就会乱套。这个细节很多入门代码都写错了读出来的数据却碰巧能用但数据稳定性会很差。4. 完整代码实现从零写一个能用的驱动4.1 引脚初始化与基础宏定义代码我采用标准库来写因为标准库的代码逻辑直白适合学习和移植。如果你用的是HAL库加CubeMX思路也是一样的只是把GPIO初始化换成CubeMX图形化配置。先定义引脚相关的宏这样后期换引脚只改这里就行了。#define BH1750_I2C_SCL_GPIO_PORT GPIOB #define BH1750_I2C_SCL_GPIO_PIN GPIO_Pin_6 #define BH1750_I2C_SCL_RCC RCC_APB2Periph_GPIOB #define BH1750_I2C_SDA_GPIO_PORT GPIOB #define BH1750_I2C_SDA_GPIO_PIN GPIO_Pin_7 #define BH1750_I2C_SDA_RCC RCC_APB2Periph_GPIOB #define BH1750_I2C_SCL_HIGH() GPIO_SetBits(BH1750_I2C_SCL_GPIO_PORT, BH1750_I2C_SCL_GPIO_PIN) #define BH1750_I2C_SCL_LOW() GPIO_ResetBits(BH1750_I2C_SCL_GPIO_PORT, BH1750_I2C_SCL_GPIO_PIN) #define BH1750_I2C_SDA_HIGH() GPIO_SetBits(BH1750_I2C_SDA_GPIO_PORT, BH1750_I2C_SDA_GPIO_PIN) #define BH1750_I2C_SDA_LOW() GPIO_ResetBits(BH1750_I2C_SDA_GPIO_PORT, BH1750_I2C_SDA_GPIO_PIN) #define BH1750_I2C_SDA_READ() GPIO_ReadInputDataBit(BH1750_I2C_SDA_GPIO_PORT, BH1750_I2C_SDA_GPIO_PIN)初始化函数里关键是要把SCL和SDA配置为开漏输出。为什么用开漏和I2C总线的电气特性有关设备只能拉低总线高电平靠上拉电阻提供。如果配置成推挽输出万一外部设备也是推挽输出两者同时输出相反电平就可能造成短路式的大电流。开漏模式天然避免了这一点这也是I2C协议的物理层要求。void BH1750_I2C_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(BH1750_I2C_SCL_RCC | BH1750_I2C_SDA_RCC, ENABLE); GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin BH1750_I2C_SCL_GPIO_PIN; GPIO_Init(BH1750_I2C_SCL_GPIO_PORT, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin BH1750_I2C_SDA_GPIO_PIN; GPIO_Init(BH1750_I2C_SDA_GPIO_PORT, GPIO_InitStructure); }我习惯在初始化完成后把SCL和SDA都拉高让总线进入空闲状态同时也能检测一下外部上拉是否正常。如果拉高后读到的还是低电平说明外部上拉电阻缺失或者短路了这也是一种简单的硬件自检方法。4.2 软件I2C时序函数起始、停止、发送字节、接收字节接下来是I2C通信的核心函数每个函数对应I2C协议的一个动作。这部分代码可以直接在逻辑分析仪上对波形验证。static void BH1750_I2C_Delay(void) { // 大约5us延时结合系统时钟72MHz for (uint8_t i 0; i 20; i); } void BH1750_I2C_Start(void) { BH1750_I2C_SDA_HIGH(); BH1750_I2C_SCL_HIGH(); BH1750_I2C_Delay(); BH1750_I2C_SDA_LOW(); BH1750_I2C_Delay(); BH1750_I2C_SCL_LOW(); BH1750_I2C_Delay(); } void BH1750_I2C_Stop(void) { BH1750_I2C_SDA_LOW(); BH1750_I2C_SCL_HIGH(); BH1750_I2C_Delay(); BH1750_I2C_SDA_HIGH(); BH1750_I2C_Delay(); } uint8_t BH1750_I2C_WriteByte(uint8_t data) { uint8_t i; for (i 0; i 8; i) { if (data 0x80) BH1750_I2C_SDA_HIGH(); else BH1750_I2C_SDA_LOW(); data 1; BH1750_I2C_Delay(); BH1750_I2C_SCL_HIGH(); BH1750_I2C_Delay(); BH1750_I2C_SCL_LOW(); BH1750_I2C_Delay(); } // 释放SDA读取从机应答 BH1750_I2C_SDA_HIGH(); BH1750_I2C_Delay(); BH1750_I2C_SCL_HIGH(); BH1750_I2C_Delay(); uint8_t ack (BH1750_I2C_SDA_READ() 0) ? 1 : 0; BH1750_I2C_SCL_LOW(); BH1750_I2C_Delay(); return ack; }发送字节时有一个细节是数据位必须在SCL低电平期间准备好然后在SCL高电平期间保持稳定。如果数据在SCL为高时切换会被从机误判为起始或停止条件。所以代码里先设SDA再拉高SCL这个顺序不能变。读字节函数相对简单但要记住最后一个字节之后要发送非应答信号uint8_t BH1750_I2C_ReadByte(uint8_t ack) { uint8_t i, data 0; // 将SDA引脚切换为输入模式 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin BH1750_I2C_SDA_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(BH1750_I2C_SDA_GPIO_PORT, GPIO_InitStructure); for (i 0; i 8; i) { BH1750_I2C_SCL_HIGH(); BH1750_I2C_Delay(); data 1; if (BH1750_I2C_SDA_READ()) data | 0x01; BH1750_I2C_SCL_LOW(); BH1750_I2C_Delay(); } // 主机应答控制 if (ack) { BH1750_I2C_SDA_LOW(); // ACK } else { BH1750_I2C_SDA_HIGH(); // NACK } BH1750_I2C_SCL_HIGH(); BH1750_I2C_Delay(); BH1750_I2C_SCL_LOW(); BH1750_I2C_Delay(); // 恢复SDA为输出模式 GPIO_InitStructure.GPIO_Pin BH1750_I2C_SDA_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(BH1750_I2C_SDA_GPIO_PORT, GPIO_InitStructure); return data; }读数据时切换SDA为浮空输入写完应答和NACK后恢复开漏输出这个切换操作在软件I2C里是必须的。有些代码简化了这一步把SDA保持为开漏输出直接读其实开漏输出模式下读引脚读的是输出锁存器不是外部电平就会出错。也有用读寄存器替代配置的方法比如用GPIO_ReadInputDataBit之前先改CRL寄存器本质上一样但上面的代码更直观。4.3 BH1750驱动封装写指令、读光强、数据拼接与溢出处理有了底层I2C函数BH1750的驱动就很简单了。先写一个发送单字节指令的函数void BH1750_WriteCmd(uint8_t cmd) { BH1750_I2C_Start(); BH1750_I2C_WriteByte(BH1750_ADDR_WRITE); // 0x46 BH1750_I2C_WriteByte(cmd); BH1750_I2C_Stop(); }再写一个读取2字节光强数据的函数uint16_t BH1750_ReadLightIntensity(void) { uint16_t value; uint8_t msb, lsb; BH1750_I2C_Start(); BH1750_I2C_WriteByte(BH1750_ADDR_READ); // 0x47 msb BH1750_I2C_ReadByte(1); // 第一个字节发送ACK lsb BH1750_I2C_ReadByte(0); // 第二个字节发送NACK BH1750_I2C_Stop(); value (uint16_t)((msb 8) | lsb); if (value 0xFFFF) return 0xFFFF; // 溢出标记 return value; }读取的两个字节中先读的是高8位后读的是低8位因为BH1750的数据寄存器是大端格式。拼接后是一个16位无符号整数范围0到65535。注意如果读回0xFFFF不能当成65535勒克斯应该当成溢出异常处理。在应用层调用时要让MCU先发测量指令然后等一段时间再读。如果连续测量模式下也可以直接读设备内部会自动刷新数据。我建议新手用一次性测量模式逻辑更可控float BH1750_GetLightLux(void) { uint16_t raw; float lux; BH1750_WriteCmd(BH1750_POWER_ON); // 0x01 DELAY_MS(10); // 上电稳定 BH1750_WriteCmd(BH1750_ONE_H_RES_MODE); // 0x20 一次性H分辨率模式20.5lx精度 DELAY_MS(180); // 等待测量完成 raw BH1750_ReadLightIntensity(); if (raw 0xFFFF) return -1.0f; // 溢出错误 // 一次性H分辨率模式2下1 LSB 0.5/1.2 lux也可以直接算 lux (float)raw / 2.4f; return lux; }在0x20模式下测量时间是180毫秒左右所以我延时180毫秒。如果是0x10模式等待120毫秒就够了。这里延时时间宁可稍微长一点不能短不然读数还没有更新你读到的可能是上一次的数据或者中间状态。4.4 主函数和串口打印把光照值显示出来主函数里先初始化系统时钟、串口和I2C引脚然后循环读取光照值通过串口打印出来。我这里用USART1波特率115200PA9是TXPA10是RX。int main(void) { float lux 0; uint8_t buf[64]; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); USART1_Init(115200); BH1750_I2C_GPIO_Init(); DELAY_MS(100); // 上电后先复位一次BH1750 BH1750_WriteCmd(0x07); // RESET指令 while (1) { lux BH1750_GetLightLux(); if (lux 0) { sprintf((char *)buf, BH1750 sensor overflow!\r\n); } else { sprintf((char *)buf, Light: %.1f lux\r\n, lux); } USART1_SendString(buf); DELAY_MS(500); } }开头发送的0x07是复位指令确保芯片从干净状态开始这比只发上电指令更可靠。特别是在芯片内部状态不确定时比如上一次通信中途掉电复位指令能把内部寄存器和状态机复位到默认值。运行到这里把板子连上USB转串口打开串口助手波特率设置115200就能看到光照数据持续打印出来。用手遮挡传感器或打开手机闪光灯照射数值应该明显变化。我实测在普通办公室日光灯下桌面照度大概是300到500勒克斯把手机闪光灯贴着传感器照几秒钟内读数能冲到2000以上用手完全捂住读数会掉到10以下。如果数值变化符合这些规律说明整个链路已经通了。4.5 不改硬件直接切换到CubeMXHAL库的思路如果你习惯用CubeMX软件I2C的思路依然适用。在CubeMX里把PB6和PB7配置为GPIO_Output开漏输出速度High然后在用户代码里实现同样的I2C时序函数底层把GPIO_WritePin和HAL_GPIO_ReadPin三个函数替换掉就行。核心逻辑完全一样不用改驱动层的协议代码。如果要用硬件I2CCubeMX里把PB6配置为I2C1_SCLPB7配置为I2C1_SDA外设选择I2C1速度调到100kHz或者400kHz都行。HAL库的用法更简单HAL_I2C_Master_Transmit(hi2c1, BH1750_ADDR_WRITE, cmd, 1, 100); HAL_I2C_Master_Receive(hi2c1, BH1750_ADDR_READ, data, 2, 100);硬件I2C的时序不用你操心代码也更简洁但前提是你对HAL库的调用机制有一定了解。我之前遇到过一个奇怪问题HAL_I2C_Master_Receive之后第二次调用Master_Transmit会超时后来发现是因为总线状态寄存器HAL_I2C_STATE_BUSY没有正确清除需要在每次通信前后调一下__HAL_I2C_CLEAR_FLAG(hi2c1, I2C_FLAG_AF)之类的操作。如果你从软件I2C切换到硬件I2C遇到问题优先查总线忙标志。5. 数据验证与调试技巧5.1 怎么确认读到的数据是准的很多新手把数据读出来以后看到数值会动就以为成功了。但要确认数据是准的还得做一些简单的对照实验。手机下载一个光照度计App或者如果手头有照度计最好。把手机屏幕朝上放在传感器旁边记录App读数和STM32串口读数对比一下。误差在20%以内都算正常因为拍摄角度、传感器位置、光反射都有影响。如果差别很大检查一下换算公式是不是用错了模式。还有一个简单的验证方法用固定光源加距离变化来测试。比如一个台灯固定高度传感器放在正下方记录不同距离下的读数。理论上光照度和距离的平方成反比点光源前提下。移动距离从20厘米到40厘米读数应该降到原来的四分之一左右。如果距离变了但读数几乎不变可能是传感器被什么东西遮住了或者测量模式设置不对。5.2 数据毛刺与软件滤波处理BH1750内部已经有积分电路直接读出来的数据相对平滑但在光线快速变化时还是会出现跳变。如果你做的是智能灯光控制这类对稳定性敏感的项目建议加一点软件滤波。最简单的是滑动平均滤波取最近5到10次采样求平均。代码就几句话float BH1750_FilteredLux(void) { float sum 0; uint8_t i; for (i 0; i 5; i) { sum BH1750_GetLightLux(); DELAY_MS(50); } return sum / 5; }这种滤波方式能滤掉大部分高频抖动代价是响应变慢了几百毫秒。对光照这种变化通常是缓慢的环境量来说完全是值得的。如果响应速度和稳定性都要兼顾可以改成中值滤波取7次采样排序后取中间值极端毛刺直接排除响应延迟还比滑动平均小。5.3 串口波形和上位机显示调完以后如果想看光照变化的曲线可以把串口数据接到PC上用匿名上位机或者简单的Python脚本绘制实时曲线。这里提一个更轻量的方案用VOFA这个串口调试工具它支持JustFloat协议STM32发4字节float数据它就能画实时曲线几秒钟就能跑起来。// 发送一个float到VOFA启用JustFloat协议 void SendFloatToVofa(float value) { uint8_t buf[4]; memcpy(buf, value, 4); USART1_SendBytes(buf, 4); }上位机上配置好串口号和波特率选择JustFloat协议就能看到光照值随时间变化的折线图。光照数据配上曲线调试效率会高很多。6. 常见问题与排查思路6.1 读不到数据I2C总线上没有应答这是最常见的问题。现象是串口打印的数据为0或者程序卡在等待应答上。排查步骤按顺序来用万用表量一下模块的VCC和GND确认供电正常。模块上的电源指示灯亮了吗确认SCL和SDA有没有接反。这个问题真的经常发生杜邦线颜色混淆一下半天就过去了。确认上拉电阻。如果模块没有板载上拉你在总线上量SCL和SDA的静态电压应该接近3.3V。如果为0V肯定缺上拉。确认地址。模块上如果有ADDR引脚拉低是0x23拉高是0x5C。代码里地址必须对应。用示波器或逻辑分析仪看波形。发送地址字节0x46后9个时钟周期内有没有低电平的应答位如果没有基本上就是物理连接问题或者模块已经损坏。确认GPIO配置为开漏模式不是复用推挽模式。复用推挽虽然能输出但在读应答时无法释放总线会冲突。我之前遇到过最诡异的一种情况是一切看起来都对但就是没有应答。最后发现是面包板上有一个引脚插孔接触不良SDA实际没有接通重新插一下就正常了。6.2 数据恒为0或者某个固定值如果读出来的数据每250毫秒左右变化一下但数值恒为0多半是测量指令没有生效。确认你发的是0x01上电之后跟0x20测量指令并且两条指令之间要有几十毫秒间隔。如果连续发测量指令而不先上电芯片可能处于掉电状态ADC不工作。另一种情况是数据恒为最大值65535或5000多这通常是两个寄存器字节拼接时顺序反了或者是在一次性模式下等待时间太短读到的是上一次残余数据的一半。检查代码里msb和lsb的拼接顺序以及HEX模式下的输出格式。6.3 读数跳变厉害读数在几十到几千之间乱跳排查顺序是供电不稳。模块VCC上如果能看到明显的纹波加一个10uF电解电容并联在VCC和GND上试试。接线太长。I2C走线超过20厘米或者杜邦线在电源线旁边信号被干扰了。把线剪短或者换双绞线。光源本身不稳。节能灯、LED灯都有频闪尤其是低质量驱动器的LED灯频闪能到100Hz。BH1750的测量时间至少120毫秒理论上积分效果会平均掉频闪但如果测量时间刚好和频闪周期共振读数就会波动。这种情况下加大MTreg延长测量时间效果立竿见影。软件时序不稳定。你的延时函数如果被中断打断了I2C时序就不完整。确认I2C读写期间没有开高频率的中断或者把I2C函数放在临界区保护。6.4 换了板子或者换了传感器模块地址对不上市面上BH1750模块的I2C地址不是完全统一的。大多数固定为0x23但有些模块的ADDR引脚被拉高地址就变成0x5C。还有一种情况是模块其实是GY-302芯片是BH1750FVI但PCB上ADDR引脚的默认电平设计不同。换模块后先确认地址再改代码。6.5 关于国产替代和芯片差异最近ST芯片缺货的时候很多板子用的STM32F103C8T6是国产替代型号。大多数替代型号的GPIO、外设寄存器行为完全兼容但有一些细微差异比如I2C外设的电气特性、上拉电阻的默认配置或者ADC的零点偏移。如果同样的代码在国产芯片上读BH1750偶尔失败优先排查I2C时序中的延时是否足够适当增加延时到10微秒即可解决。软件I2C在这方面天然比硬件I2C更抗造因为速度完全由软件控制不受外设差异影响。7. 写在最后的实操心得照例分享几个我自己的习惯。第一BH1750这个传感器虽然便宜但它的测量结果受窗口脏污影响很大。如果传感器要长期放在户外或者灰尘多的环境定期清洁表面的透光窗不然读数会逐渐偏低。第二如果项目里的光照值需要参与控制比如自动调节屏幕亮度千万别只依赖滤波后的平均值。光照传感器很容易被一瞬间的阴影或反射干扰最好再加上突变检测当前值相对上一帧变化超过50%时先不响应连续两到三个周期都维持新值再切换状态。这套防抖逻辑我是在做智能家居面板时总结出来的能省掉很多不合理的亮度跳跃。第三代码里我建议把BH1750的地址、测量指令、超时时间都做成宏定义或者配置文件不要直接散写在逻辑代码里。后续换传感器型号、调整测量模式时只改一个头文件就行了省心很多。最后再分享一个小技巧调试的时候可以把BH1750测量得到的勒克斯值和PWM占空比绑定在一起做成一个小呼吸灯。用手电筒照传感器灯变亮手一遮灯变暗。这个不到50行代码的互动效果非常适合刚学完这个案例的新手作为进阶练习它能把传感器读取、数据处理、PWM输出、实时反馈整个链路串起来比单纯看串口打印有成就感得多。希望这篇教程能帮你把BH1750这个传感器的数据和背后的原理一次吃透。有问题欢迎在下面交流我看到了都会回复。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。