STM32读取GY-39光照传感器:MAX44009寄存器与I2C/UART例程详解 简介面向STM32F4开发者的GY-39多传感器读取例程基于MAX44009光照传感器与BMP280温湿度气压传感器演示通过I²C/SPI通信获取环境数据适用于物联网终端、智能家居与小型气象站等场景也适合希望快速上手STM32传感器驱动开发的初学者。资源共324个文件约3.88MB以Keil工程形式组织包含h/c源文件、s启动汇编、o目标文件及d依赖文件等源码覆盖GPIO、USART、I²C初始化以及数据解析和串口输出模块结构清晰可直接编译。已有1890人学习下载工程配套完整可对照阅读初始化配置、通信协议与数据处理流程帮助理解STM32配合GY-39的实际开发方法。通过这套例程读者能掌握传感器时序操作、寄存器读写和工程级代码组织便于迁移到自己的项目中。1. GY-39 与 MAX44009 在 STM32 项目里的第一性问题GY-39 是典型的“一板三传感器”环境检测模块MAX44009 管环境光照BMP280 管气压SHT30 管温湿度三颗传感器通过内部 I2C 总线连在一起对外引出 I2C 或 UART 两种接口。标题里虽然写了例程、GY-39、STM32F4 一串关键词但真正决定能不能读通模块的是 MAX44009 这颗光照芯片。它把照度结果拆成 4 位指数加 12 位尾数存放寄存器用法和常见的光敏电阻 ADC 完全不同直接当普通 16 位数据读出来数值会大得离谱。这一篇就把从接线、I2C 配置、寄存器读取、串口解析到滤波校准的完整路径讲清楚适合做智能灯控、农业大棚辐照度记录、传感器课程设计或者基于 STM32 的毕业设计把光照这一路读通就能直接当传感数据源用。2. GY-39 模块硬件连接、I2C 地址与工作模式选择2.1 引脚定义与 STM32F4 接线表在动手找例程之前先确认拿到的是哪个版本的 GY-39。常见版本引出 6Pin 或 8Pin同一块板上有的引脚被复用到 I2C 和 UART 两用方向以丝印为准。下面这张引脚表按最常见批次整理做硬件连接时逐项对照。引脚方向类型说明VCC输入电源3.3V ~ 5V推荐 3.3V 供电GND输入电源数字地与 STM32 共地SDA双向I2C 数据UART 模式下为模块 TXSCL输入I2C 时钟UART 模式下为模块 RXSM/SDIO输入模式/地址选择部分批次用作 I2C 地址选择或 UART 模式切换INT输出中断MAX44009 中断引脚可选STM32F4 的 I2C 引脚建议用 CubeMX 固定到 I2C1PB6 为 SCLPB7 为 SDA。如果这两组引脚被屏幕占用就换 I2C2PB10/PB11或 I2C3PA8/PC9但改完一定要在 CubeMX 里检查其他外设是否占用了同一引脚避免出现复用冲突。GY-39 的 VCC 接 3.3V不要长时间接 5V板上电平转换电路会把 5V 电平反向串到 SCL/SDA运行时间一长容易拉坏 STM32 的 IO。接线要点是“一线一电源先上电后接信号”。我一般先把 VCC、GND 接好万用表确认模块 VCC 引脚有 3.3V 之后再插 SCL/SDA。热插拔 I2C 总线很容易让从机锁死尤其是已经上电的模块插线瞬间的毛刺可能让 MAX44009 的内部状态机卡住。2.2 I2C 地址默认 0x4A 还是 0x48先做一次总线扫描MAX44009 的 7 位 I2C 地址由 ADDR 引脚决定接地时是 0x4A接 VCC 时是 0x48。问题在于 GY-39 上 ADDR 引脚不一定引出来有些批次的模块还把 ADDR 固定拉高或拉低所以例程里写死哪个地址都可能踩空。最稳妥的做法是上电后扫描一遍总线把所有能应答的地址打印出来再决定代码里用哪个宏。// I2C 地址扫描1 秒执行一次结果通过串口输出 void I2C_Scan(I2C_HandleTypeDef *hi2c) { for (uint8_t addr 0x08; addr 0x78; addr) { // HAL 的 Transmit 会先发 START 地址从机应答则返回 HAL_OK if (HAL_I2C_Master_Transmit(hi2c, addr 1, addr, 0, 10) HAL_OK) { printf(found addr: 0x%02X\r\n, addr); HAL_Delay(10); } } }这里addr 1是把 7 位地址左移成总线 8 位地址因为 HAL 库的 Address 参数要求带读写方向的完整字节Transmit 的第三个参数传addr但长度写 0等于只发送地址不发送数据从机只要有 ACK 就会被识别出来。GY-39 板子上一般会同时出现 0x4AMAX44009和 0x76BMP280若只扫到 0x4A 而找不到 0x76大概率是另一颗芯片被模式引脚切到了 UART需要回过去查 SM 引脚电平。2.3 为什么不能只靠 STM32 内部上拉STM32F4 的 I2C 引脚内部有弱上拉但驱动能力在 400kHz 下明显不足尤其当模块到主控之间有 10cm 以上杜邦线时SCL/SDA 边沿会变缓出现“主机收到 ACK 但数据错位”的怪问题。GY-39 板载的 I2C 上拉电阻大多数批次已经焊好但如果你用的是裸芯片改装必须在 VCC 与 SCL、VCC 与 SDA 之间各加一个 4.7kΩ 上拉电阻具体就是两个电阻分别跨接在 VCC 和 SCL、VCC 和 SDA 之间。上拉电阻不是越大越好10kΩ 在长线场景会把上升沿拖到微秒级100kHz 下勉强能用400kHz 下时序余量基本归零。快速判断方法是用示波器量 SCL 的上升沿正常应小于 1μs超过这个值就换小一号电阻直到边沿干净。3. MAX44009 寄存器结构与 STM32 HAL 最小驱动MAX44009 内部寄存器只有十几个读取光照只需要配置一个寄存器、读两个数据寄存器。先理解寄存器表后面对照着看代码就不会一头雾水。3.1 MAX44009 寄存器表和配置位寄存器地址名称读写默认值关键位说明0x0A中断状态只读0x00bit0 为中断标志0x0B配置读写0x00bit6 手动/自动bit0~2 积分时间0x0C光照高字节只读0x00bit7~4 指数bit3~0 尾数高 4 位0x0D光照低字节只读0x00bit7~0 尾数低 8 位0x0E中断阈值高字节读写0x00阈值格式与 0x0C 相同0x0F中断阈值低字节读写0x00阈值格式与 0x0D 相同配置寄存器 0x0B 平时维持默认值 0x00 即可bit6 为 0 表示自动连续模式芯片每 800ms 自动完成一次光强检测我们只要周期性去读 0x0C/0x0D 就行。只有对功耗特别敏感、需要精确控制采样时间时才把 bit6 置 1 进入手动模式再用 bit0~2 选择 1.5625ms 到 25.6s 的积分时间。初始化代码里显式写一次 0x00 很有必要防止模块被前一个程序改成手动模式后没有再自动更新数据。3.1.1 照度公式为什么寄存器里是“科学计数法”0x0C 和 0x0D 拼起来并不是定点数而是类似科学计数法的结构0x0C 高 4 位是指数 exponent0x0C 低 4 位加 0x0D 的 8 位组成 12 位尾数 mantissa。照度 Lux 的计算按下面这条式子换算Lux 0.72 * mantissa * 2^exponent / 16系数 0.72 来自 MAX44009 的感光特性和数字转换器增益除以 16 是因为 12 位尾数里真正主导数值的是高 8 位低 4 位只提供分辨率直接乘会把结果整体放大 16 倍。打个比方读回 0x0C 0x2A、0x0D 0x01 时exponent 2mantissa 0xA01 2561算出来就是 0.72 * 2561 * 4 / 16 ≈ 461 Lux正好对应室内日光灯环境。如果把这个原始值当普通 16 位整数来算会得到 10241 Lux直接错了一个量级。3.2 基于 STM32 HAL 库的 MAX44009 读取例程#define MAX44009_I2C_ADDR_W 0x94 // 7bit 地址 0x4A 左移一位写方向 #define MAX44009_I2C_ADDR_R 0x95 // 读方向 #define MAX44009_REG_CFG 0x0B #define MAX44009_REG_LUX_H 0x0C HAL_StatusTypeDef MAX44009_Init(I2C_HandleTypeDef *hi2c) { uint8_t cfg 0x00; // 自动连续模式不使能中断 // 先发目标寄存器地址再写配置值一次事务完成 return HAL_I2C_Mem_Write(hi2c, MAX44009_I2C_ADDR_W, MAX44009_REG_CFG, I2C_MEMADD_SIZE_8BIT, cfg, 1, 100); } HAL_StatusTypeDef MAX44009_ReadLux(I2C_HandleTypeDef *hi2c, float *lux) { uint8_t reg MAX44009_REG_LUX_H; uint8_t raw[2]; // 把寄存器地址作为一字节数据发出去再用重复起始条件读两字节 if (HAL_I2C_Master_Transmit(hi2c, MAX44009_I2C_ADDR_W, reg, 1, 100) ! HAL_OK) return HAL_ERROR; if (HAL_I2C_Master_Receive(hi2c, MAX44009_I2C_ADDR_R, raw, 2, 100) ! HAL_OK) return HAL_ERROR; // raw[0] 来自 0x0Craw[1] 来自 0x0D uint8_t exponent raw[0] 4; // 高 4 位指数 uint16_t mantissa ((uint16_t)(raw[0] 0x0F) 8) | raw[1]; // 12 位尾数 if (exponent 15) { *lux 188000.0f; // 超出常见量程直接封顶 } else { *lux 0.72f * mantissa * (float)(1u exponent) / 16.0f; } return HAL_OK; }这个例程拆成两步而不是直接用HAL_I2C_Mem_Read是为了把读写过程拆开看清楚。HAL_I2C_Mem_Read也能做同样的事但一些早期 STM32F4 库版本对这类连续读多字节寄存器的兼容性表现不稳定先 Transmit 寄存器地址、再 Receive 数据是最通用、最不容易踩芯片坑的写法。参数里两个值得调的地方超时 100ms 对应 I2C 总线在 100kHz 下传输几十字节的耗时余量改成 50ms 也能跑但总线被占时要给重试留时间1u exponent在 exponent 等于 14 时已经接近 16384代码里用 if 截断避免了指数过大带来的移位溢出也保证 Lux 不会输出无穷大或负数。3.3 主循环轮询与错误日志打印int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_USART2_UART_Init(); float lux 0.0f; MAX44009_Init(hi2c1); while (1) { if (MAX44009_ReadLux(hi2c1, lux) HAL_OK) { printf(Lux: %.2f\r\n, lux); } else { // I2C 出错时打印错误码不要静默忽略 printf(I2C error: %d\r\n, (int)hi2c1.ErrorCode); MX_I2C1_Init(); // 复位 I2C 外设再重试 } HAL_Delay(500); } }把读失败单独打印出来能区分“传感器没上电”和“I2C 总线锁死”两类问题。MX_I2C1_Init在错误后重新初始化外设是处理总线异常时最简单也最常用的恢复手段更规范的做法是先HAL_I2C_DeInit再MX_I2C1_Init但要注意 DeInit 会把引脚复用配置一起清掉需要接着调用MX_GPIO_Init重新配置引脚否则下次传输仍然失败。4. GY-39 串口模式与自动数据帧解析GY-39 对外除了 I2C还保留了 UART 工作模式。把模块上的模式引脚设置到 UART 一侧并重新上电后模块不再是“寄存器从机”而是主动、周期性地把三路传感器数据通过串口发出来主机只需要一根 RX 引脚就能收数据。这种模式对刚接触 I2C 时序的开发者更友好也是很多 GY-39 stm32 例程包里默认给的方式。需要注意不同批次的 GY-39 固件封装帧格式不一样网上流传的 16 字节、18 字节版本都有所以下面这套解析代码按“帧头 长度 数据 校验和”的通用结构来写实际使用时把宏改成本板卡抓到的值即可。4.1 一帧数据的组织方式拿到模块后先接 USB 转串口抓一帧原始数据按下面几项做标注前两个字节通常是帧头比如 0xAA 0x55 或 0xAA 0xAA第三字节是后续长度从第四个字节开始依次是光照、气压、温度、湿度最后一字节是校验和常见做法是前面所有字节求和取低 8 位。把抓到的帧拆到一张表里解析代码就照着这张表写偏移量。偏移含义长度备注0帧头 01用于同步1帧头 11出现连续 AA AA 时先缓存再判断2长度1后续有效数据长度3~6光照原始值4通常为 float 的字节序7~14气压/温度/湿度8按协议拆15校验和1前 N 字节求和低 8 位校验和规则可以在串口助手里手算验证把一帧里校验和之外的字节全部累加再看低 8 位是否和最后一字节相等。如果不相等就去查协议文档里的另一条规则比如异或校验或 CRC8不要在代码里强行凑一个不成立的公式。4.2 串口状态机收帧代码#define FRAME_HEAD_0 0xAA #define FRAME_HEAD_1 0xAA #define FRAME_MAX_LEN 16 static uint8_t g_frame[FRAME_MAX_LEN]; static uint8_t g_frame_pos; static uint8_t g_frame_state; void GY39_UART_ByteHandler(uint8_t byte) { switch (g_frame_state) { case 0: // 等第一个帧头 if (byte FRAME_HEAD_0) g_frame_state 1; break; case 1: // 等第二个帧头失败则重新同步 g_frame_state (byte FRAME_HEAD_1) ? 2 : 0; break; case 2: // 长度字节先缓存再决定后续数量 g_frame_pos 0; g_frame[g_frame_pos] byte; g_frame_state 3; break; case 3: // 连续收满 FRAME_MAX_LEN-2 个字节最后一个是校验和 g_frame[g_frame_pos] byte; if (g_frame_pos FRAME_MAX_LEN) { uint8_t sum 0; for (int i 0; i g_frame_pos - 1; i) sum g_frame[i]; if (sum g_frame[g_frame_pos - 1]) { GY39_ParseFrame(g_frame, g_frame_pos); } g_frame_state 0; // 无论校验是否通过都回初始态 } break; } }状态机的核心价值在于丢字节后的重新同步。串口是字节流如果程序只在中断里按“接收固定长度”就解析一旦 UART 丢一个字节后面所有帧都会整体错位。这里用状态 0 和状态 1 做双重帧头判断只有连续两个帧头出现才开始缓存中间任何一个字节不对就回到状态 0 重新等帧头保证错一帧最多丢一帧不会连锁错位。FRAME_MAX_LEN必须根据实际帧长调整如果抓到的帧长 18就把宏改成 18同时把长度字节的校验一并加上防止误帧。4.3 用 CubeMX 配置串口中断接收// 在 USART2 全局中断回调里调用 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { GY39_UART_ByteHandler(rx_byte); HAL_UART_Receive_IT(huart2, rx_byte, 1); // 继续接收下一字节 } }HAL_UART_Receive_IT必须在回调里重新启动一次而且我习惯放在回调首行这样即使GY39_UART_ByteHandler里做了较重的解析下一字节也已经挂到硬件上避免串口 FIFO 溢出丢帧。主循环里不要做大数据处理GY-39 的串口帧频率一般在 10Hz 量级解析完把结果放到全局结构体主循环只读这份缓存即可。如果用 DMA 接收把HAL_UART_Receive_DMA的循环模式打开用 IDLE 中断判断一帧结束115200 波特率下能明显降低 CPU 占用但代码复杂度会高一些串口数据量不大时单字节中断足够。5. 数据滤波、量程校准与三个必查错误光传感器数据最容易出现的现象是“读数跳变”这不一定是传感器坏了可能来自荧光灯 100Hz 频闪或人走过遮挡。MAX44009 的积分时间默认 800ms 已经滤掉一部分高频分量但读值仍有 ±5% 左右的波动。常见做法是加滑动平均#define FILTER_LEN 8 static float lux_buf[FILTER_LEN]; static uint8_t idx; static uint8_t filled; float Lux_Filter(float lux_now) { lux_buf[idx] lux_now; if (idx FILTER_LEN) { idx 0; filled 1; } float sum 0; for (int i 0; i FILTER_LEN; i) sum lux_buf[i]; return filled ? sum / FILTER_LEN : sum / (idx 1); }这个滤波器窗口长度取 8在 500ms 的读取周期下对应 4 秒平滑既能消除频闪突变又不至于让手电筒照射这类瞬间反应被完全抹平。做植物补光灯控制时窗口缩短到 4做室内恒照度调节时窗口增加到 16响应和稳定可以自己权衡。Lux 值作为控制量而不是显示量时建议做一次三点校准用标准照度计在暗处、台灯 1m 处、正午窗边三个点记录传感器读数和标准值。按“标准值 ÷ 传感器读值”算出一个修正系数再对系数做一次平均写进代码作为最终增益补偿。这个做法比在算法里堆滤波器更有实际意义因为每块 GY-39 的光窗装配误差会影响 10% 以上的绝对精度。三个必查错误按出现频率排序第一I2C 地址扫描不到 0x4A。先确认 SM 引脚是拉高还是拉低把模块切到 I2C 模式部分 GY-39 批次默认 UART 模式上电后模块只在发串口数据主机当然扫不到。第二读出来永远是 0.00。检查 0x0B 配置寄存器是不是被例程改成了手动模式手动模式下积分时间没设对会读到 0再确认供电高于 1.8VMAX44009 欠压时输出不稳定。第三连续运行一段时间后 I2C 卡死。用逻辑分析仪抓 SCL如果 SCL 一直为低就是从机把总线拉死了软件复位 I2C 外设后仍不行就直接断电重上电这是比任何软件复位都彻底的恢复方式。最后留一个定位技巧无论显示值多离谱先把raw[0]和raw[1]的原始寄存器值一起通过串口打印出来对照数据手册的照度表核对一次。原始值偏大或偏小代表换算问题原始值完全不变才是传感器或接线问题。本文还有配套的精品资源点击获取