资讯动态

STM32F103驱动BH1750光照传感器:I2C配置、测量模式与避坑指南

发布时间:2026/10/16 17:17:48 来源:尧图企业网站定制
简介这份资源面向嵌入式开发初学者与需要快速搭建光照采集方案的工程师围绕STM32F103微控制器驱动BH1750光照强度传感器GY-30模块展开解决环境光测量与TFTLCD实时显示的实际问题。压缩包共140个文件约4.53MB以37个h头文件、34个c源文件为核心配合16个o目标文件、16个d依赖文件及uvprojx、uvoptx等Keil工程配置另有hex、axf固件与map映射文件覆盖从源码到可烧录程序的完整链路。内容涉及I²C总线通信、BH1750多种测量模式选择、光照数据解析与单位换算以及TFTLCD初始化、坐标设置与界面布局等关键环节适合智能家居、健康监测与智能照明等场景参考。目前已有4833人学习下载读者可借此理解微控制器编程、传感器驱动、总线通信、数据处理与图形显示的完整流程并对照工程结构排查配置与显示问题。1. 从一块 GY-30 模块说起为什么 STM32F103 配 BH1750 是光照采集的稳妥组合手头拿到一块 GY-30 模块正面是 BH1750FVI 芯片背面是几个电阻和电容五根引脚分别是 VCC、GND、SCL、SDA、ADDR。很多人第一次上电就卡住串口打印出来永远是 0 或者 65535换个光照环境也没反应。问题往往不在传感器本身而在 STM32F103 的 I2C 配置和 BH1750 的命令时序没对上。BH1750 是数字式环境光传感器内部集成了光电二极管和 16 位 ADC直接输出勒克斯lx值省掉了外部运放和标定环节。GY-30 是它的常见 breakout 板带上拉电阻和滤波电容拿来就能接。STM32F103 这边用硬件 I2C 或者软件模拟 I2C 都能驱动前者省 CPU后者引脚灵活。这套组合适合做光照监测节点、屏幕背光自动调节、农业大棚补光控制这类场景成本低资料多但坑也集中——I2C 地址、测量模式、时序延时任何一个没处理对读出来的就是废数据。下面按实际调试顺序把配置、读写、换算和排错一次讲透。2. 硬件连接与 I2C 地址选择ADDR 引脚决定的那一位2.1 GY-30 模块引脚与 STM32F103 的对接GY-30 模块通常引出 VCC、GND、SCL、SDA、ADDR 五个脚。VCC 接 3.3VGND 共地SCL 和 SDA 分别接 STM32F103 的 I2C 时钟线和数据线。以 STM32F103C8T6 为例I2C1 的默认引脚是 PB6SCL和 PB7SDAI2C2 是 PB10SCL和 PB11SDA。如果这两个硬件 I2C 被其他外设占了可以用软件模拟 I2C 挪到任意 GPIO 上代价是时序靠延时循环撑高速下容易丢步。ADDR 引脚是地址选择位。BH1750 的 7 位从机地址是0x23或0x5C取决于 ADDR 接低还是接高。GY-30 板上一般把 ADDR 悬空或下拉默认地址是0x23。写驱动时8 位地址形式是0x46写和0x47读这是 7 位地址左移一位再补读写位的结果。很多人直接拿0x23当 8 位地址用HAL 库的HAL_I2C_Master_Transmit第一个参数填0x23结果一直返回 HAL_ERROR就是这里错了。提示用逻辑分析仪或者示波器抓一下 SCL/SDA 波形确认从机地址字节发出去之后有没有 ACK。没有 ACK 就是地址错或者硬件没接好。硬件上拉电阻方面GY-30 模块自带上拉但如果你把线拉得很长或者总线上挂了多个 I2C 设备建议在 STM32 侧再并 4.7kΩ 上拉到 3.3V。上拉太弱会导致上升沿变缓400kHz 下波形变成三角波通信随机失败。2.2 硬件 I2C 与软件模拟 I2C 的选型对比STM32F103 的硬件 I2C 外设早年有“卡死”的传闻实际是错误处理没写全总线锁死之后没有恢复机制。如果项目对实时性要求不高软件模拟 I2C 反而更可控出问题容易定位。两种方式的取舍可以看下面这张表。对比项硬件 I2C软件模拟 I2CCPU 占用低DMA 可进一步降低高每个位都要循环延时引脚灵活性固定引脚重映射有限任意 GPIO最高速率400kHzF103 标准通常 100kHz 以内稳定调试难度状态寄存器多出错码不直观逻辑清晰示波器一看就懂多设备冲突总线仲裁硬件处理需自行保证时序我一般会在快速验证阶段用软件模拟确认传感器能正常读之后再决定要不要换硬件 I2C 省 CPU。如果系统里 I2C 设备多、通信频繁硬件 I2C 配合中断或 DMA 更合适。2.3 初始化代码GPIO 与 I2C 外设配置以 HAL 库硬件 I2C1 为例初始化代码如下。这里假设系统时钟 72MHzI2C 时钟 400kHz。/* i2c1_init.c - STM32F103 HAL 库 I2C1 初始化 */ #include stm32f1xx_hal.h I2C_HandleTypeDef hi2c1; void I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; /* 400kHz 快速模式 */ hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; /* 占空比 2:1 */ hi2c1.Init.OwnAddress1 0; /* 主机模式自身地址无关 */ hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 0; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { /* 初始化失败检查时钟使能和引脚配置 */ Error_Handler(); } } void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c) { GPIO_InitTypeDef GPIO_InitStruct {0}; if (hi2c-Instance I2C1) { __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_I2C1_CLK_ENABLE(); /* PB6SCL, PB7SDA复用开漏 */ GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_OD; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); } }ClockSpeed设 400000 对应快速模式BH1750 支持到 400kHz没问题。DutyCycle在快速模式下才有效选I2C_DUTYCYCLE_2是 Tlow/Thigh 2符合大多数传感器时序。GPIO_MODE_AF_OD是复用开漏I2C 必须用开漏模式否则总线会短路。Pull设GPIO_PULLUP启用内部上拉但内部上拉约 40kΩ偏弱外部最好再并 4.7kΩ。如果初始化后HAL_I2C_IsDeviceReady一直返回 HAL_BUSY先查 GPIO 时钟和 I2C 时钟是否都使能了再查引脚有没有被其他外设占用。F103 的 I2C1 和 USB 有引脚冲突同时用要重映射。3. BH1750 命令集与测量模式连续测量和单次测量的取舍3.1 命令字节解析上电、复位、测量模式BH1750 的通信很简单主机发一个命令字节传感器执行对应操作。命令分几类上电Power On0x01、断电Power Off0x00、复位Reset0x07、连续测量模式、单次测量模式。连续测量模式下传感器以设定周期自动更新数据主机随时读单次测量模式下发一次命令测一次测完自动进入断电状态省电。连续测量模式又分高分辨率、高分辨率模式2、低分辨率三种。高分辨率0x10是 1lx 精度测量时间约 120ms高分辨率模式20x11是 0.5lx 精度测量时间约 120ms但受噪声影响更大低分辨率0x13是 4lx 精度测量时间约 16ms。单次测量对应0x20、0x21、0x23。选哪个模式看场景。电池供电的光照节点用单次测量测完就断电平均功耗能压到微安级。需要连续监测的用连续高分辨率模式读之前不用反复发命令。注意连续模式下第一次读数要等测量周期过去否则读到的是旧值或者 0。3.2 写命令与读数据的完整时序写命令用HAL_I2C_Master_Transmit读数据用HAL_I2C_Master_Receive。BH1750 的数据是两个字节高字节在前低字节在后合起来是 16 位无符号数。原始值和勒克斯的换算关系是lux raw / 1.2。这是芯片手册给的典型值实际会有 ±20% 左右的偏差高精度场景需要自己标定。/* bh1750.c - BH1750 读写与换算 */ #include stm32f1xx_hal.h #define BH1750_ADDR_W 0x46 /* 7位地址0x23左移一位写 */ #define BH1750_ADDR_R 0x47 /* 读 */ extern I2C_HandleTypeDef hi2c1; /* 发送单字节命令 */ uint8_t BH1750_WriteCmd(uint8_t cmd) { return HAL_I2C_Master_Transmit(hi2c1, BH1750_ADDR_W, cmd, 1, 100); } /* 初始化上电 - 复位 - 连续高分辨率模式 */ uint8_t BH1750_Init(void) { if (BH1750_WriteCmd(0x01) ! HAL_OK) return 1; /* 上电 */ HAL_Delay(10); if (BH1750_WriteCmd(0x07) ! HAL_OK) return 2; /* 复位 */ HAL_Delay(10); if (BH1750_WriteCmd(0x10) ! HAL_OK) return 3; /* 连续高分辨率 */ HAL_Delay(180); /* 等第一次测量完成120ms 留余量 */ return 0; } /* 读取原始值并换算成 lux */ float BH1750_ReadLux(void) { uint8_t buf[2] {0}; uint16_t raw 0; if (HAL_I2C_Master_Receive(hi2c1, BH1750_ADDR_R, buf, 2, 100) ! HAL_OK) { return -1.0f; /* 读失败返回负值调用方判断 */ } raw (uint16_t)(buf[0] 8) | buf[1]; return raw / 1.2f; }BH1750_WriteCmd里超时设 100msI2C 400kHz 下传一个字节远小于这个值超时说明总线异常。BH1750_Init里上电后延时 10ms 等内部稳压复位后延时 10ms 等寄存器清零发连续测量命令后延时 180ms 等第一次转换完成。这个 180ms 是血泪经验手册写 120ms 典型值但不同批次模块有差异留足余量才不会读到 0。BH1750_ReadLux里读失败返回 -1.0f调用方根据这个判断传感器是否掉线。注意HAL_I2C_Master_Receive的地址参数要用读地址0x47用写地址会一直等不到数据。3.3 测量模式切换与功耗控制如果项目是电池供电连续测量模式一直开着BH1750 工作电流约 0.12mA不算大但加上 STM32 和其他外设整体功耗还是要算。更省电的做法是单次测量需要读数时发0x20延时 180ms读两个字节然后发0x00断电。这样传感器大部分时间在断电状态电流降到 0.01μA 级别。/* 单次测量 断电适合低功耗场景 */ float BH1750_ReadOnce(void) { uint8_t buf[2] {0}; uint16_t raw 0; BH1750_WriteCmd(0x01); /* 上电 */ HAL_Delay(10); BH1750_WriteCmd(0x20); /* 单次高分辨率 */ HAL_Delay(180); if (HAL_I2C_Master_Receive(hi2c1, BH1750_ADDR_R, buf, 2, 100) ! HAL_OK) { BH1750_WriteCmd(0x00); return -1.0f; } raw (uint16_t)(buf[0] 8) | buf[1]; BH1750_WriteCmd(0x00); /* 断电 */ return raw / 1.2f; }单次测量每次都要上电、延时、测量、断电整个周期约 200ms。如果每秒采一次平均电流很小。注意单次测量模式下发完测量命令后传感器不会自动断电需要手动发0x00否则它一直保持上电状态省电效果打折。4. 避坑与排查读数为 0、65535 和跳变的处理4.1 读数恒为 0地址错、模式没发、延时不够现象串口打印 lux 一直是 0.0换光照环境无变化。原因有三种常见情况。一是 I2C 地址用错把 7 位地址0x23直接传给 HAL 库实际应该传0x46写地址。二是初始化时没发测量模式命令只发了上电和复位传感器没有启动转换。三是延时不够发完连续测量命令后立刻读转换还没完成读到的是 0。解决用HAL_I2C_IsDeviceReady确认地址能通返回 HAL_OK 说明地址对。检查初始化流程里有没有0x10或0x20命令。把延时从 120ms 加到 180ms 再试。4.2 读数恒为 65535数据线没接好或上拉缺失现象读出来 raw 值一直是 65535lux 约 54613。原因SDA 线虚焊或者没接I2C 读的时候数据线被上拉电阻拉高读回来全是 1。或者上拉电阻没接开漏输出无法拉高波形异常。解决万用表测 SDA 对 VCC 的电压空闲时应该是 3.3V。如果只有 1V 左右上拉不够。补 4.7kΩ 上拉到 3.3V。检查杜邦线有没有断芯换一根。4.3 读数跳变严重电源噪声或测量模式选错现象lux 值在几十到几千之间乱跳没有规律。原因VCC 电源噪声大开关电源纹波耦合到传感器。或者用了高分辨率模式20x110.5lx 精度对噪声更敏感。另外如果传感器和电机、继电器共用电源干扰更明显。解决VCC 并 0.1μF 和 10μF 电容滤波。换回高分辨率模式0x10。传感器电源和数字电路电源分开走线单点接地。软件上做滑动平均滤波连续读 5 次去掉最大最小再平均。4.4 I2C 总线锁死时钟被拉低不释放现象程序跑一段时间后 I2C 通信全部失败HAL_I2C_Master_Transmit返回 HAL_BUSY 或 HAL_TIMEOUT。原因从机在传输过程中被复位或者电源抖动把 SCL 或 SDA 拉低不放主机等不到总线空闲。F103 的硬件 I2C 遇到这种情况容易卡死。解决在 I2C 初始化前加总线恢复函数手动发 9 个时钟脉冲让从机释放总线。具体做法是把 SCL 配成推挽输出SDA 配成输入循环翻转 SCL 九次然后发 STOP 条件。恢复后再重新初始化 I2C 外设。/* i2c_bus_recovery.c - 总线恢复在 I2C 初始化前调用 */ void I2C_BusRecovery(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); /* SCL 推挽输出SDA 输入上拉 */ gpio.Pin GPIO_PIN_6; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, gpio); gpio.Pin GPIO_PIN_7; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOB, gpio); /* 发 9 个时钟 */ for (int i 0; i 9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); } /* 发 STOPSDA 低 - SCL 高 - SDA 高 */ gpio.Pin GPIO_PIN_7; gpio.Mode GPIO_MODE_OUTPUT_OD; HAL_GPIO_Init(GPIOB, gpio); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); HAL_Delay(1); }这个恢复函数在每次 I2C 初始化之前调用一次能解决大部分总线锁死。注意恢复完之后要重新配置 I2C 引脚为复用开漏再调HAL_I2C_Init。4.5 多传感器共用总线时的地址冲突现象总线上挂两个 GY-30只有一个能读另一个没反应。原因两个模块 ADDR 都接低地址都是0x23冲突了。解决把其中一个模块的 ADDR 接高地址变成0x5C写地址0xB8读地址0xB9。驱动里把地址做成参数传入不要写死。如果模块 ADDR 已经焊死可以用 I2C 多路复用器扩展但成本上去了不如直接改硬件。5. 进阶技巧用定时器触发单次测量做低功耗采集5.1 定时器 单次测量的低功耗框架前面单次测量是软件延时CPU 空转等 180ms功耗没省到极致。更好的做法是用定时器中断触发测量主循环进睡眠测完读数据再睡。以 TIM2 为例配置成 1 秒周期中断中断里发单次测量命令启动一个 200ms 的定时器第二个中断里读数据。/* low_power_bh1750.c - 定时器触发的低功耗采集 */ #include stm32f1xx_hal.h extern I2C_HandleTypeDef hi2c1; TIM_HandleTypeDef htim2; TIM_HandleTypeDef htim3; volatile uint8_t measure_flag 0; volatile float lux_value 0.0f; /* TIM2 中断1秒周期发测量命令 */ void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); BH1750_WriteCmd(0x01); /* 上电 */ BH1750_WriteCmd(0x20); /* 单次高分辨率 */ __HAL_TIM_SET_COUNTER(htim3, 0); HAL_TIM_Base_Start_IT(htim3); /* 启动200ms定时 */ } } /* TIM3 中断200ms 后读数据 */ void TIM3_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim3, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(htim3, TIM_FLAG_UPDATE); HAL_TIM_Base_Stop_IT(htim3); uint8_t buf[2] {0}; if (HAL_I2C_Master_Receive(hi2c1, 0x47, buf, 2, 100) HAL_OK) { uint16_t raw (uint16_t)(buf[0] 8) | buf[1]; lux_value raw / 1.2f; } BH1750_WriteCmd(0x00); /* 断电 */ measure_flag 1; } }TIM2 周期 1 秒中断里发上电和单次测量命令同时启动 TIM3。TIM3 周期 200ms中断里读数据、断电、置标志。主循环检测measure_flag处理数据后进__WFI()睡眠。这样 CPU 大部分时间在睡平均电流能压到毫安级以下。5.2 数据校验与滑动平均滤波BH1750 原始值偶尔会有尖峰直接换算成 lux 显示会跳。简单有效的办法是滑动平均存最近 8 次读数去掉最大最小再平均。代码不复杂但能明显改善显示稳定性。/* 滑动平均滤波窗口8去掉最大最小 */ float filter_lux(float new_val) { static float buf[8] {0}; static uint8_t idx 0; static uint8_t cnt 0; float sum 0, max -1, min 999999; buf[idx] new_val; idx (idx 1) % 8; if (cnt 8) cnt; for (uint8_t i 0; i cnt; i) { sum buf[i]; if (buf[i] max) max buf[i]; if (buf[i] min) min buf[i]; } if (cnt 3) return new_val; /* 样本太少直接返回 */ return (sum - max - min) / (cnt - 2); }窗口大小 8 是经验值采样周期 1 秒的话响应延迟约 8 秒对光照这种缓变量够用。如果要做光控开关响应要快窗口降到 3 或者 4。5.3 标定用标准照度计修正系数BH1750 出厂标定是在特定光源下做的换 LED 或者荧光灯读数会有偏差。要求不高的话raw / 1.2直接用。要求高的话拿一个标准照度计对比算修正系数。光源类型典型修正系数说明白炽灯1.00接近标定条件白光 LED1.05 ~ 1.15光谱偏蓝读数偏低荧光灯0.95 ~ 1.05有频闪建议多次平均自然光1.00 ~ 1.10随色温变化标定步骤把传感器和标准照度计放同一平面记录 5 个不同光照下的 raw 值和标准 lux 值算标准lux / (raw/1.2)的平均值作为修正系数写进代码。注意标定要在稳定光源下做避免阳光直射和阴影。从那以后我每次接新传感器都先拿逻辑分析仪抓一遍 I2C 波形确认地址和 ACK 再写驱动省得在代码里来回猜。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑