☰
STM32驱动MLX90614红外测温:从I2C时序到OLED显示的完整落地
2026/10/14 1:33:29 网站建设 项目流程

简介:基于STM32的MLX90614红外测温显示工程,面向嵌入式初学者与物联网项目开发者,提供完整的非接触式测温与OLED显示方案。项目以STM32F103C8T6为主控,通过I2C接口读取MLX90614红外传感器数据,经SPI驱动OLED屏实时显示环境及物体温度,涵盖底层I2C/SPI驱动、传感器寄存器配置、温度转换与屏幕刷新逻辑,并涉及电源管理和抗干扰等实用考虑。压缩包内共有157个文件,以.c/.h源码文件和Keil工程文件(.uvprojx)为主,包含编译生成的.o/.d/.crf等中间文件及调试配置,整体大小3.05MB,目录结构清晰,可方便导入开发环境查看或直接烧录。目前已有818人学习下载。压缩包内附完整可编译的工程源码,可帮助读者深入理解MLX90614的I2C通信时序、OLED屏幕的SPI控制方式,以及温度数据标定与显示格式处理,亦可作为课程设计、毕业设计或物联网测温产品的快速参考模板。

1. 基于STM32的MLX90614红外测温:从I2C时序到OLED显示的完整落地

用STM32驱动MLX90614红外测温模块并通过OLED显示,是很多开发者入门非接触式测温的首选方案,但这个看起来简单的项目里其实藏着不少坑:MLX90614的I2C时序兼容性问题、SMBus协议与标准I2C的差异、EEPROM校准参数读取顺序、以及OLED显示刷新与温度采样之间的时序冲突。这套资源把完整的工程代码、原理图说明和调试记录打包在一起,能帮你避开大部分新手会踩的坑。适合正在做测温手环、智能家居门禁测温或者医疗辅助设备的开发者,也适合课程设计需要快速出DEMO的学生。我拿到这套资源后第一件事就是看I2C初始化代码和EEPROMM读取逻辑,因为这决定了你后续调试是半小时搞定还是要熬一整晚。

2. 硬件搭建与选型:MLX90614的I2C地址问题和传感器配置

2.1 模块选型与引脚连接:别只看VCC和GND

MLX90614虽然封装小,但内部集成了一颗红外热电堆传感器和一颗信号处理芯片,对外提供SMBus兼容的数字接口。市面上常见的模块有两种:一种是纯传感器芯片需要自己搭外围电路,另一种是集成模块带稳压和滤波电容,后者更适合直接对接STM32。这套资源用的是集成模块,引脚定义是VCC、GND、SCL、SDA四根线,部分模块还会引出SMBus的报警引脚ALM,但标准驱动用不上。

连线要注意三个细节。第一,MLX90614的SMBus接口需要上拉电阻,模块上如果有4.7k到10k的上拉就不要再额外加,否则总线负载过重会导致通信时序变形。第二,STM32的I2C引脚如果复用为开漏输出,必须确认GPIO配置正确,很多人在这里用了推挽输出导致总线拉不低。第三,电源要干净,MLX90614对电源纹波比较敏感,供电不要和电机或继电器共用一个电源,否则温度数据会在目标温度附近跳变。

我一般会把SCL接PB6、SDA接PB7(STM32F103的I2C1引脚),OLED用I2C2或者软件I2C都可以。如果OLED也用硬件I2C,注意两条总线的地址不要冲突,MLX90614的默认7位地址是0x5B,OLED屏幕常见地址是0x3C,分开总线最省心。

2.2 MLX90614的寄存器架构:RAM和EEPROM才是关键

MLX90614内部有两块存储区域需要区分清楚。RAM区存放的是测量结果,地址0x06是红外传感器原始数据(IR),地址0x07是环境温度(Ta),这两个地址读取频率最高。EEPROM区存放的是校准参数和配置字,包括发射率校准值(地址0x24)、传感器ID(地址0x25)以及一些出厂校准参数。

很多初学者上来就读RAM的0x06和0x07,发现数据不对就怀疑传感器坏了,其实是因为没有先读EEPROM的校准参数。MLX90614的出厂校准数据存储在EEPROM里,芯片上电后会自动加载到内部寄存器,但如果你要计算实际温度,需要用到发射率值(默认是0x95,对应人体测温场景)。有些模块出厂时发射率被改成别的值,读出来的温度会整体偏移,这时候就要通过写EEPROM的0x24地址来校正。

这套资源里的代码直接封装了读取EEPROM和RAM的函数,你不用自己从头写I2C时序,但建议理解一下SMBus的读字流程:先发设备地址加写位,然后发寄存器地址,再重新发送设备地址加读位,最后连续读两个字节加PEC校验字节。这里的PEC就是SMBus特有的CRC-8校验,MLX90614会计算并返回这个校验字节,如果你忽略它也不影响读取数据,但总线在某些情况下可能会出现错位。

3. 核心代码实现:SMBus兼容读取、温度计算和OLED刷新策略

3.1 初始化I2C和GPIO:复用开漏加时钟配置

先看GPIO和I2C的初始化代码,这是整个项目的地基。代码用的是STM32标准外设库,如果你改用HAL库,逻辑是一样的,只是函数名不同。

void I2C_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 使能GPIOB时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 使能I2C1时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // PB6=SCL, PB7=SDA GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度50MHz GPIO_Init(GPIOB, &GPIO_InitStructure); // I2C1配置 I2C_InitTypeDef I2C_InitStructure; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_ClockSpeed = 100000; // 100kHz标准模式 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_OwnAddress2 = 0x00; I2C_Init(I2C1, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }

这段代码的关键点有两个。一是GPIO模式必须配置为复用开漏GPIO_Mode_AF_OD,因为I2C总线协议要求开漏输出配合外部上拉电阻才能实现线与功能,如果你配成推挽输出,SDA线在多个设备通信时会出现短路风险。二是I2C时钟频率设置为100kHz,MLX90614在SMBus模式下支持10kHz到100kHz的时钟频率,虽然它也能跑400kHz快速模式,但官方手册推荐工作在100kHz以下更有保障,尤其是当你的线材比较长或者模块质量一般的时候。

3.2 读取MLX90614 RAM数据的核心函数

读取温度数据是整个项目的核心,这里给出读取RAM地址的完整函数实现。

// 向MLX90614指定寄存器地址发起写命令(用于设置EEPROM等操作) uint8_t MLX90614_WriteReg(uint8_t reg_addr, uint16_t data) { // 1. 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 2. 发送设备地址+写位 I2C_Send7bitAddress(I2C1, MLX90614_ADDR, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 3. 发送寄存器地址 I2C_SendData(I2C1, reg_addr); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 4. 发送数据低字节(SMBus规定先发低字节) I2C_SendData(I2C1, (uint8_t)(data & 0xFF)); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 5. 发送数据高字节 I2C_SendData(I2C1, (uint8_t)((data >> 8) & 0xFF)); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 6. 发送PEC校验字节(CRC-8),启用校验功能时必须发送 uint8_t pec = 0; pec = MLX90614_PEC_Calc(MLX90614_ADDR << 1, reg_addr, data); I2C_SendData(I2C1, pec); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 7. 停止信号 I2C_GenerateSTOP(I2C1, ENABLE); return 0; } // 读取MLX90614指定寄存器地址的16位数据(用于RAM和EEPROM读取) uint16_t MLX90614_ReadReg(uint8_t reg_addr) { uint16_t result = 0; uint8_t data_l = 0, data_h = 0; // 1. 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 2. 发送设备地址+写位,指定要读取的寄存器地址 I2C_Send7bitAddress(I2C1, MLX90614_ADDR, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, reg_addr); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 3. 重新发送起始信号(SMBus读字操作要求restart) I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); // 4. 发送设备地址+读位 I2C_Send7bitAddress(I2C1, MLX90614_ADDR, I2C_Direction_Receiver); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 5. 读取低字节(SMBus数据格式:低字节在前),并发送ACK I2C_AcknowledgeConfig(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data_l = I2C_ReceiveData(I2C1); I2C_AcknowledgeConfig(I2C1, ENABLE); // 继续ACK // 6. 读取高字节,发送NACK表示这是最后一个字节 I2C_AcknowledgeConfig(I2C1, DISABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data_h = I2C_ReceiveData(I2C1); // 7. 读取PEC校验字节(如果启用PEC)——用NACK结束后读取 uint8_t pec = 0; I2C_AcknowledgeConfig(I2C1, DISABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); pec = I2C_ReceiveData(I2C1); // 8. 停止信号 I2C_GenerateSTOP(I2C1, ENABLE); result = (data_h << 8) | data_l; return result; }

这段代码的逻辑说明:SMBus读字操作和标准I2C读操作的最大区别在于第二步和第三步之间的restart信号。SMBus在写完寄存器地址后必须重新发送起始信号(repeated START),然后切换为读模式,这样才能读取指定寄存器的内容。如果你用标准I2C的连续读函数,比如HAL库的HAL_I2C_Mem_Read,它内部可能不会严格按照SMBus的时序要求来实现,导致读回来的数据错位。

参数说明:MLX90614_ADDR是7位设备地址0x5B,但在发送时要左移一位变成8位,最低位是读写标志。reg_addr是要读取的寄存器地址,读温度时传0x06,读环境温度时传0x07,读发射率时传0x24。PEC校验字节可以忽略不读,但如果你的总线干扰比较大,建议启用校验,具体的CRC-8算法函数在资源包里有现成实现。

3.3 温度计算:原始数据到摄氏度的转换公式

读取到的16位原始数据只是ADC数值,需要转换成实际温度。MLX90614内部已经做了线性化处理,所以转换公式很简单,但一定要分清读取的是哪个寄存器。

// 将MLX90614原始数据转换为摄氏度 float MLX90614_GetTemp(uint8_t reg_addr) { uint16_t raw = MLX90614_ReadReg(reg_addr); // 温度计算公式:温度(°C) = 原始值 * 0.02 - 273.15 // 原始值是开尔文温度乘以50后的结果 return (float)raw * 0.02f - 273.15f; } // 读取目标物体温度(红外传感器温度) float MLX90614_GetObjectTemp(void) { return MLX90614_GetTemp(0x06); } // 读取环境温度(传感器自身附近的环境温度) float MLX90614_GetAmbientTemp(void) { return MLX90614_GetTemp(0x07); }

这个转换公式的原理是MLX90614内部以0.02摄氏度的分辨率表示温度值,且内部以开尔文为单位存储。举个例子,如果读取到的原始值是0x0B91(即2961),那么温度就是2961×0.02-273.15=13.07摄氏度。这个公式是固定的,不需要额外校准。

需要注意的一个坑是:如果你用手直接捏住传感器外壳或者传感器靠近发热元件,读取的环境温度会比实际环境高好几度,这时候目标温度的计算也会跟着偏移。所以传感器的安装位置要远离主控芯片和电源模块,必要时加一个隔热垫。

3.4 OLED显示刷新策略:温度采样和显示不能互相阻塞

OLED显示如果用I2C接口,在刷新一屏数据时需要几百毫秒的时间,如果在这期间去读取MLX90614的温度,IRQ时序会被拉长,导致温度数据读取失败或数据跳变。这套资源给出的方案是分时复用:温度采样和OLED刷新放在同一个循环里,但用简单的状态机控制。

// 主循环:采样温度 + OLED显示 int main(void) { // 初始化系统、GPIO、I2C、OLED等 SystemInit(); I2C_GPIO_Init(); OLED_Init(); uint16_t sample_count = 0; float obj_temp = 0.0f; char display_buf[16]; while(1) { // 每10次循环采样一次温度,其余时间用于OLED刷新 if(sample_count % 10 == 0) { obj_temp = MLX90614_GetObjectTemp(); // 格式化显示字符串 sprintf(display_buf, "Obj: %.2f C", obj_temp); } // OLED每100ms刷新一次 if(sample_count % 5 == 0) { OLED_Clear(); OLED_ShowString(0, 0, display_buf); // 刷新环境温度 float amb_temp = MLX90614_GetAmbientTemp(); sprintf(display_buf, "Amb: %.2f C", amb_temp); OLED_ShowString(0, 2, display_buf); OLED_Refresh(); } delay_ms(10); sample_count++; if(sample_count > 100) sample_count = 0; } }

这种分时刷新策略的目的是避免长时间占用I2C总线。OLED刷新一屏通常需要发送1024字节的数据(128×64分辨率),在100kHz的I2C时钟下大约需要100ms,这段时间内如果连续读取MLX90614的多个寄存器,时间会被拉长到200ms以上,实际测温的实时性就变差了。所以采样间隔选10次循环一次(100ms),显示刷新选5次循环一次(50ms),两者交错进行。

4. 避坑指南:MLX90614调试中五个最容易踩的坑

4.1 I2C总线卡死,程序卡在等待事件标志

现象:程序运行一段时间后卡在while(!I2C_CheckEvent(...))这个循环里,CPU占用率100%,看门狗超时复位。

原因:MLX90614在异常掉电或者总线干扰时,内部状态机可能进入错误状态,不再响应主机的通信请求。更常见的情况是I2C总线上有毛刺,导致从设备在收到一个不完整的字节后把SDA线拉死。

解决:在每次I2C通信失败后,主动产生一个停止信号来复位总线状态,或者连续发送9个SCL时钟脉冲让从设备释放SDA线。这套资源里提供了一个I2C_Bus_Reset函数,可以在通信超时后调用。另外,建议在初始化I2C前先对SCL和SDA引脚做一次软复位,拉低SDA再拉高SCL,发送GPIO模拟的STOP信号。

4.2 读到的温度数据恒定不变或始终是-273.15

现象:不管目标物体是手还是冰水,读到的温度始终不变,或者一直显示-273.15。

原因:-273.15是绝对零度的摄氏表示,出现这个值说明读取到的原始数据是0x0000,也就是说MLX90614根本没有响应或者读取的寄存器不包含有效数据。常见原因有两个,一是I2C设备地址不对,向错误地址发起了读操作;二是在使用I2C_Mem_Read之类的库函数时,寄存器地址被当成了内存地址,导致从设备的地址指针指向无效区域。

解决:先用I2C扫描程序确认MLX90614的实际设备地址。在总线上搜索0x00到0x7F的所有地址,看看哪个地址有ACK响应。如果扫描到0x5B以外的地址,就要在代码的MLX90614_ADDR宏中改过来。另外,检查I2C读取函数的第二个参数是否传入了正确的寄存器地址,比如读取温度应该传0x06而不是0x07。

4.3 读取温度数据时好时坏,加上OLED显示后更严重

现象:单独读温度很稳定,但接上OLED后温度数据偶尔出现跳变,比如从36度突然变成5度再跳回来。

原因:OLED刷新时占用了大量总线带宽,如果OLED和MLX90614挂在同一条I2C总线上,OLED刷新期间MLX90614的通信时序被拉长。MLX90614的SMBus协议有一个超时机制,如果从启动到停止的时间超过某个阈值,从设备会放弃本次通信,返回无效数据或产生NACK。

解决:最简单的方案是让OLED和MLX90614分用两条I2C总线,STM32F103有两个硬件I2C控制器,OLED接I2C1,MLX90614接I2C2,互不干扰。如果只能用一条总线,那就把OLED刷新频率降低,比如从50Hz降到20Hz,同时把温度采样放在OLED刷新间隙里,比如OLED刷新完一屏后延迟2ms再触发温度采样。

4.4 发射率校准错误导致体温测量偏低

现象:用MLX90614测量人体额头温度,读数是33度,额温枪是36.5度,差了3度以上。

原因:MLX90614出厂默认发射率校准值是0x95(即0.95),这是针对黑体辐射源的通用值。但人体皮肤的有效发射率通常高于0.95,部分人体部位接近0.98。当被测物体发射率高于传感器的预设值,传感器会低估实际温度。

解决:通过写EEPROM的地址0x24来修改发射率。写入公式是发射率值乘以200,比如0.98对应的写入值是196(0xC4)。修改之后要重新上电才能生效,而且EEPROM的写入次数有限,不要频繁改写。另外,如果只是临时做个演示,不改EEPROM也可以,在读取温度后加一个软件补偿,比如在最终结果上加上1到2度。

4.5 传感器刚上电时温度数据缓慢漂移

现象:刚上电的几分钟内,显示的温度逐渐上升,大约5分钟后才稳定。

原因:MLX90614内部有一个热质量,上电瞬间传感器自身温升还没建立稳定,热电堆传感器读到的红外辐射能量包含了一部分传感器自身的热辐射。同时,OLED屏幕发热和主控芯片发热也会影响传感器附近的环境温度。

解决:上电后先让系统预热2分钟再开始采样显示,或者在前100次采样中丢弃前20个数据点。更彻底的做法是读环境温度寄存器0x07,然后与目标温度做差分计算。MLX90614手册里也有提到,传感器靠近电路板热源时数据会偏高0.5到1度,安装时尽量让传感器探头伸出PCB边缘。

5. 进阶校准技巧:发射率修正和温度补偿让数据更可信

如果你不满足于"能出数",想让你的测温设备达到额温枪级别的准确度,这一步值得研究。核心技巧就两个:发射率修正和两点校准。

先看发射率修正。前面提到人体发射率约0.98,但不同皮肤状态和测量角度会有差异。更严谨的做法是用黑体炉校准来确定实际发射率值。操作方法是:找一台有温度显示的黑体炉,设定一个目标温度比如37度,等黑体炉稳定后,用你的设备读取目标温度T_measured,然后调整发射率值重新测量,直到读数稳定在37.0度。记录此时EEPROM里的发射率值,这个就是你的设备针对当前应用场景的准值。

如果找不到黑体炉,也可以用冰水混合物和沸水做两点校准。冰水混合物在标准大气压下是0度,沸水是100度。把MLX90614分别对准这两种目标读取温度,记录偏差ΔT1和ΔT2。然后做一个线性补偿:

// 线性温度补偿示例 float compensated_temp = measured_temp + slope * (measured_temp - reference_temp) + offset;

其中slope和offset通过上述两组校准点计算。比如冰水混合物测到2度,沸水测到97度,那么偏差分别是+2度和-3度,slope = (-3-2)/(100-0) = -0.05,offset = 2。这样在50度时补偿量为-0.5度。这个补偿方法不依赖发射率设置,直接在软件层做修正,适合快速现场校准。

这套资源里附带了一个校准工具代码,读取EEPROM发射率参数、写入指定值、以及验证温度读取的完整流程。实际项目中我一般会把校准结果保存在EEPROM的备用区域,产品模式下自动加载校准参数,而不是每次上电都重新校准。

另外关于显示界面的一个细节建议:如果你在OLED上同时显示目标温度和环境温度,建议把环境温度字体缩小放在角落,目标温度用大字号居中显示,因为使用者关心的永远是目标温度,环境温度数字太大反而容易分散注意力。最后说一个我自己的习惯:每次改完代码烧录后,我会用固定距离(比如10cm)对准同一个目标连续读取30次温度,看最大值和最小值的差值,如果超过0.5度说明滤波没做好,需要增加滑动平均窗口。从那以后我每次做测温项目都强制走一遍这个流程,能提前暴露很多通信和供电问题。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询