ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32 I2C实战:MLX90614红外测温与OLED显示驱动详解

STM32 I2C实战:MLX90614红外测温与OLED显示驱动详解

简介:这份资源面向正在学习STM32嵌入式开发的初学者与进阶者,围绕MLX90614红外测温传感器与OLED显示屏的联动应用展开,帮助读者掌握非接触式温度采集与实时显示这一典型项目场景。压缩包共191个文件,约6.36MB,以c源文件与h头文件为核心,配合Keil工程所需的uvprojx、uvoptx配置、编译生成的o、crf、axf、hex等中间与目标文件,以及sct分散加载、map映射、lst清单等辅助文件,完整保留了从源码到可执行固件的工程结构。项目涉及STM32微控制器编程、SMBUS/IIC通信协议、MLX90614数据解析与OLED驱动显示等关键知识点,读者可据此理解传感器通信、温度换算与屏幕刷新的完整链路。目前已有2145人学习下载,适合作为嵌入式接口设计与数据处理练习的参考工程。

1. 从一颗红外测温芯片说起:为什么 MLX90614 配 OLED 是嵌入式入门的黄金组合

如果你手头有一块 STM32 最小系统板,想找一个既能练 I2C 通信、又能练 OLED 驱动、还能做出实物效果的练手项目,MLX90614 加 OLED 显示温度这个组合几乎是量身定做的。MLX90614 是 Melexis 出的红外测温芯片,出厂校准、非接触测量,通过 SMBus(兼容 I2C)输出物体温度和环境温度两个值,精度在人体测温区间能做到 ±0.5℃ 左右。OLED 用 SSD1306 驱动的 0.96 寸模块最常见,128×64 分辨率,I2C 接口,和 MLX90614 可以挂在同一条总线上。

这个项目的核心链路很短:STM32 通过 I2C 读 MLX90614 的 RAM 寄存器,拿到 16 位原始数据,换算成摄氏度,再通过 I2C 写到 SSD1306 的显存,刷新屏幕。链路短意味着调试路径清晰,出问题容易定位。但短不代表没坑——I2C 地址冲突、PEC 校验、温度换算的 0.02 系数、OLED 页寻址模式,每一个都能让新手卡半天。下面按实际动手顺序拆开讲。

2. 硬件选型与 I2C 总线规划:先把地址和上拉电阻定下来

2.1 MLX90614 的型号差异与地址规则

MLX90614 常见后缀有 A、B、C、D 等,区别在视场角和测温范围。做人体测温选 BAA 或 DAA 居多,视场角 90° 或 35°。关键参数是出厂固定的 I2C 从机地址:默认 0x5A(7 位地址),部分型号可编程为 0x5B 到 0x5F。SSD1306 的 I2C 地址通常是 0x3C 或 0x3D,由模块背面电阻跳线决定。两个地址不冲突,可以挂同一条 I2C 总线。

我一般会先用扫描程序确认总线上实际存在的地址,再写业务代码。这一步能省掉后面大量“为什么读不到数据”的排查时间。

// I2C 地址扫描,基于 HAL 库,适用于 STM32F1/F4 系列 #include "stm32f1xx_hal.h" extern I2C_HandleTypeDef hi2c1; void I2C_Scan(void) { uint8_t addr; HAL_UART_Transmit(&huart1, (uint8_t*)"Scanning I2C bus...\r\n", 22, 100); for (addr = 1; addr < 128; addr++) { // HAL 的地址参数需要左移一位,第 8 位是读写位 if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 2, 10) == HAL_OK) { // 打印找到的地址,实际项目里换成 printf 或 OLED 显示 char buf[32]; sprintf(buf, "Found: 0x%02X\r\n", addr); HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 100); } } }

这段代码的逻辑是逐个尝试 7 位地址,用HAL_I2C_IsDeviceReady发一个空写操作看从机是否应答。参数addr << 1是因为 HAL 库要求传入包含读写位的 8 位地址格式。2是重试次数,10是超时毫秒数。正常应该扫到 0x3C(OLED)和 0x5A(MLX90614)。如果只扫到一个,先查另一个模块的供电和焊接。

2.2 上拉电阻与总线电容:4.7k 不是万能值

I2C 总线必须加上拉电阻,常见模块自带上拉,但多个模块并联时等效阻值会变小。标准模式 100kHz 下,4.7kΩ 是常见值;快速模式 400kHz 下,如果总线电容超过 200pF,4.7k 可能导致上升沿变缓,通信失败。我的经验是:先看模块是否自带上拉,如果两个模块都带 4.7k,并联后等效 2.35k,对 3.3V 系统来说灌电流约 1.4mA,在 STM32 的 20mA 引脚能力内,通常没问题。但如果通信不稳定,示波器看 SCL 上升沿,超过 1μs 就要减小上拉阻值或降低速率。

STM32 的 I2C 外设配置里,时钟速率建议先设 100kHz 跑通,再尝试 400kHz。很多 MLX90614 模块在 400kHz 下也能工作,但长杜邦线场景下 100kHz 更稳。

3. 读温度:MLX90614 寄存器解析与 0.02 系数换算

3.1 关键寄存器与读取时序

MLX90614 的 RAM 里,0x06 是环境温度(Ta),0x07 是物体温度(Tobj1)。每个寄存器 16 位,低字节在前。读取流程是:发送从机地址加写位,发送寄存器地址,重复起始条件,发送从机地址加读位,读两个字节,发送 NACK 和 STOP。

用 HAL 库的HAL_I2C_Mem_Read可以一步完成:

// 读取 MLX90614 物体温度,返回摄氏度 #define MLX90614_ADDR (0x5A << 1) // HAL 需要 8 位地址格式 #define MLX90614_TOBJ1 0x07 float MLX90614_ReadTemp(void) { uint8_t buf[2]; // 从 0x07 寄存器读 2 字节,超时 100ms if (HAL_I2C_Mem_Read(&hi2c1, MLX90614_ADDR, MLX90614_TOBJ1, I2C_MEMADD_SIZE_8BIT, buf, 2, 100) != HAL_OK) { return -273.15f; // 读取失败返回绝对零度作为错误标志 } // 低字节在前,拼成 16 位 uint16_t raw = (uint16_t)(buf[1] << 8) | buf[0]; // 0.02 是 MLX90614 的固定换算系数,单位 K float tempK = raw * 0.02f; // 开尔文转摄氏度 return tempK - 273.15f; }

逻辑说明:HAL_I2C_Mem_Read内部会处理寄存器地址发送和重复起始条件。参数I2C_MEMADD_SIZE_8BIT表示寄存器地址是 8 位。buf[1] << 8 | buf[0]是因为 MLX90614 输出小端格式。0.02 这个系数来自芯片手册,原始值乘以 0.02 得到开尔文温度。返回 -273.15 作为错误标志,因为实际测量不可能到这个值。

3.2 PEC 校验:要不要开,什么时候开

MLX90614 支持 SMBus 的 PEC(Packet Error Checking),在数据后附加一个 CRC-8 字节。长线或干扰环境下建议开启,但 HAL 库的HAL_I2C_Mem_Read不直接支持 PEC 读取,需要手动实现时序或用HAL_I2C_Master_Receive配合手动发送寄存器地址。

我的做法是:原型阶段先不开 PEC,跑通温度读取;如果现场出现偶发跳变到 -273 或异常值,再开 PEC。开 PEC 后每次读 3 个字节,前两字节是数据,第三字节是 CRC。CRC 计算多项式是 x^8 + x^2 + x + 1,初始值 0。

// MLX90614 PEC CRC-8 计算 uint8_t MLX90614_CRC8(uint8_t *data, uint8_t len) { uint8_t crc = 0; for (uint8_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t bit = 0; bit < 8; bit++) { if (crc & 0x80) { crc = (crc << 1) ^ 0x07; } else { crc <<= 1; } } } return crc; }

参数说明:data是包含从机地址写、寄存器地址、从机地址读、低字节、高字节的完整序列,len通常为 5。计算出的 CRC 与读回的第三字节比较,一致才认为数据有效。这个校验在电磁干扰强的场合能过滤掉大部分坏数据。

4. OLED 显示驱动:SSD1306 显存布局与温度刷新策略

4.1 SSD1306 的页寻址与显存映射

0.96 寸 OLED 模块用 SSD1306 驱动,128×64 像素,显存按页组织,共 8 页,每页 8 行,每页 128 字节。页寻址模式下,写入一个字节控制一列 8 个像素。显示字符需要字库,常用 6×8 或 8×16 点阵。

初始化序列不能省,尤其是电荷泵使能命令 0x8D 后跟 0x14,否则屏幕不亮。下面是一个最小初始化加温度显示的流程:

// SSD1306 初始化关键命令序列 void SSD1306_Init(void) { uint8_t cmds[] = { 0xAE, // 关闭显示 0xD5, 0x80, // 时钟分频 0xA8, 0x3F, // 多路复用比 64 0xD3, 0x00, // 显示偏移 0 0x40, // 起始行 0 0x8D, 0x14, // 电荷泵使能,必须 0x20, 0x00, // 水平寻址模式 0xA1, // 段重映射 0xC8, // 扫描方向 0xDA, 0x12, // COM 引脚配置 0x81, 0xCF, // 对比度 0xD9, 0xF1, // 预充电 0xDB, 0x40, // VCOMH 0xA4, // 显示跟随 RAM 0xA6, // 正常显示 0xAF // 开启显示 }; for (uint8_t i = 0; i < sizeof(cmds); i++) { uint8_t buf[2] = {0x00, cmds[i]}; // 0x00 是命令模式控制字节 HAL_I2C_Master_Transmit(&hi2c1, 0x3C << 1, buf, 2, 100); } }

逻辑说明:每个命令前加 0x00 控制字节表示命令模式,0x40 表示数据模式。0x8D, 0x14是电荷泵使能,漏掉这个屏幕永远不亮,这是新手最常见的翻车点之一。初始化完成后,用0x40控制字节加显存数据批量写入。

4.2 温度刷新:局部刷新与全屏刷新的取舍

温度值变化频繁,如果每次全屏刷新 1024 字节,I2C 在 100kHz 下需要约 100ms,屏幕会闪。更好的做法是局部刷新:只更新温度数字所在的页和列。比如温度显示在第二页,只写第二页的 128 字节中的相关列。

我一般会维护一个显存缓冲区uint8_t oled_buf[8][128],温度更新时只改对应页的字节,然后只发送那一页。这样刷新时间降到十几毫秒,视觉上无闪烁。

// 局部刷新第二页(温度显示区) void OLED_RefreshPage(uint8_t page) { // 设置页地址 uint8_t cmd[] = {0x00, 0xB0 + page, 0x00, 0x10}; HAL_I2C_Master_Transmit(&hi2c1, 0x3C << 1, cmd, 4, 100); // 发送该页 128 字节显存,控制字节 0x40 uint8_t data[129]; data[0] = 0x40; memcpy(&data[1], oled_buf[page], 128); HAL_I2C_Master_Transmit(&hi2c1, 0x3C << 1, data, 129, 200); }

参数说明:0xB0 + page设置页起始地址,0x00和0x10设置列地址低 4 位和高 4 位为 0。data[0] = 0x40是数据模式控制字节。一次传输 129 字节,在 100kHz 下约 12ms,可以接受。

5. 避坑与排查:I2C 读不到、温度跳变、OLED 不亮怎么查

5.1 现象:扫描不到 MLX90614 地址

原因:供电电压不对。MLX90614 工作电压 3.0V 到 3.6V,5V 会损坏或不应答。或者 SDA/SCL 接反,或者模块没有上拉电阻。

解决:万用表量 VCC 对 GND 电压,确认在 3.3V。用扫描程序确认。如果模块自带稳压,确认输入电压范围。接反的话,交换 SDA 和 SCL 再扫。

5.2 现象:温度读数始终为 -273.15 或固定值

原因:HAL_I2C_Mem_Read返回错误,或者寄存器地址写错。MLX90614 的物体温度是 0x07,环境温度是 0x06,写错会读到其他数据。

解决:在读取函数里加错误打印,看 HAL 返回的HAL_I2C_ERROR_AF还是HAL_I2C_ERROR_TIMEOUT。AF 是从机无应答,查地址和接线;TIMEOUT 是总线被拉死,查上拉和短路。确认寄存器地址是 0x07。

5.3 现象:温度值偶尔跳到 1000℃ 以上

原因:原始数据拼接字节序错误,或者 PEC 未开启导致坏数据被采纳。

解决:确认buf[1] << 8 | buf[0]的顺序。MLX90614 是小端,低字节在前。如果拼反,原始值会大 256 倍。开启 PEC 校验,过滤坏帧。

5.4 现象:OLED 不亮但 I2C 能扫到地址

原因:初始化序列里电荷泵命令缺失,或者对比度设得太低,或者0xAF开启显示命令没发。

解决:对照初始化序列逐条检查,重点看0x8D, 0x14和0xAF。用逻辑分析仪抓 I2C 波形,确认命令字节正确送达。

5.5 现象:温度刷新时屏幕闪烁

原因:全屏刷新耗时太长,或者刷新频率过高。

解决:改用局部刷新,只更新温度所在页。刷新周期控制在 200ms 以上,给 I2C 总线留出空闲时间。如果还闪,检查电源是否稳定,OLED 在刷新时电流波动可能导致电压跌落。

6. 进阶技巧:用定时器触发单次测温与温度趋势显示

跑通基础显示后,可以加两个实用功能。一是用定时器触发 MLX90614 的单次测量模式,降低功耗和自发热。MLX90614 默认连续测量,写 0x00 到 0x04 寄存器可以进入睡眠,写 0x01 触发单次。二是把最近几次温度值存成环形缓冲区,在 OLED 上画一个小趋势图,直观看到温度变化。

// 单次测温触发与读取 void MLX90614_TriggerSingle(void) { uint8_t cmd = 0x01; // 写 0x04 寄存器触发单次测量 HAL_I2C_Mem_Write(&hi2c1, MLX90614_ADDR, 0x04, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 100); } // 温度趋势环形缓冲区 #define TREND_LEN 32 float trend_buf[TREND_LEN]; uint8_t trend_idx = 0; void Trend_Update(float temp) { trend_buf[trend_idx] = temp; trend_idx = (trend_idx + 1) % TREND_LEN; } // 在 OLED 底部画趋势折线,简化版 void Trend_Draw(void) { for (uint8_t i = 0; i < TREND_LEN - 1; i++) { uint8_t idx1 = (trend_idx + i) % TREND_LEN; uint8_t idx2 = (trend_idx + i + 1) % TREND_LEN; // 把温度映射到 0-15 像素高度,画在第七页 uint8_t y1 = (uint8_t)((trend_buf[idx1] - 20.0f) * 1.5f); uint8_t y2 = (uint8_t)((trend_buf[idx2] - 20.0f) * 1.5f); // 画线函数需要自己实现,这里只示意映射逻辑 OLED_DrawLine(i * 4, 56 - y1, (i + 1) * 4, 56 - y2); } }

参数说明:0x04寄存器写0x01触发单次测量,测量完成后自动回到睡眠。趋势缓冲区长度 32,映射时假设温度范围 20℃ 到 30℃,乘以 1.5 映射到 0 到 15 像素。实际使用要根据量程调整系数。

我自己的习惯是:每次改完 I2C 时序或显示逻辑,先用逻辑分析仪抓一段波形存下来,对比正常和异常时的差异。这个黑匣子比反复烧录快得多。另外,MLX90614 的视场角决定了测量区域,测额头时距离 3 到 5 厘米比较合适,太远会混入背景温度。这些细节看起来小,但决定了读数是否可信。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进