51单片机实战:MLX90614红外测温完整方案与I2C避坑指南
简介面向51单片机学习者的红外温度测温项目源码包以MXL90614热电堆传感器为核心配合LCD1602液晶屏实现非接触式温度采集与实时显示占用空间极小适合课程设计、毕业设计以及个人DIY温度计等多种场景。压缩包共8个文件、整体约8KB内部包含4个C源文件、3个头文件和1个Keil工程文件.uv2C源文件分别实现主控逻辑、LCD1602显示驱动与软件延时头文件负责统一声明接口Keil工程文件可直接导入编译并烧录到单片机运行。已有1075人浏览学习属于实测可用的完整小项目。通过学习这份代码可掌握MXL90614模拟电压信号的读取与温度换算方法、51单片机I/O口操作、LCD1602显示时序控制等关键点理解从传感器数据采集到人机界面输出的完整流程整体代码结构清晰、模块划分合理适合初学者对照实物调试也便于移植到其他51系列平台进行二次开发。1. 把 MLX90614 接上 51 单片机一次搞定非接触测温做嵌入式项目最怕的不是代码报错而是传感器明明在卖家用 Arduino 跑得好好的换到 51 单片机上却死活读不出数据。MLX90614 就是这样一个让人又爱又恨的器件——它用 I2C 通信能直接读出物体表面温度不需要接触目标测量距离几厘米内都很准尤其适合做红外温度测温、热源检测、智能家居这类场景。但它的 I2C 时序要求比较严格51 单片机没有硬件 I2C 控制器全靠 GPIO 模拟时序稍有偏差就读回 0xFF 或者干脆卡死在 ACK 等待上。本文就把我实际调通的这套方案完整拆开从硬件接线、核心代码逐段讲解到 LCD 显示、超温报警再到 I2C 踩坑记录给你一套能在 51 单片机以 STC89C52 为例上稳定跑起来的完整参考。这套项目的适用人群很明确手里有 51 开发板、想接 MLX90614 但卡在通信上的开发者或者想快速做一个非接触测温小系统的电子爱好者。读完你不仅能跑通还能知道怎么校准温度、怎么扩展报警和显示逻辑直接改到自己的产品原型里。2. 硬件架构与接线MLX90614 怎么连上 51 单片机2.1 器件引脚与内部结构MLX90614 是一个红外温度计芯片封装常见的有 TO-39 和 MLX90614ESF-BCC 这种贴片版本。它的核心分两部分红外热电堆传感器负责吸收目标辐射内部还有一个环境温度传感器两者通过芯片内部的信号处理单元输出数字温度值。对外接口就是标准 I2C引脚只有 SCL、SDA、VDD、GND 四个硬件接线上非常省事。需要注意 MLX90614 的供电范围是 3.3V~5V但内部 I2C 引脚是 5V 兼容的所以接在 51 单片机上可以直接共地、直接连接 SCL 和 SDA不需要额外加电平转换芯片。这一点和其他很多 3.3V 传感器不一样省了不少事。不过如果是用 3.3V 供电SDA 上拉电阻建议接到 3.3V避免灌电流问题。2.2 完整接线清单我这套方案用的器件是 STC89C52 单片机、MLX90614 模块、LCD1602 液晶屏、一个有源蜂鸣器、三个按键用于设置报警阈值。实际接线如下MLX90614 引脚51 单片机引脚说明VDDVCC5V模块供电注意看模块是否带稳压GNDGND共地SCLP1.0模拟 I2C 时钟线SDAP1.1模拟 I2C 数据线LCD1602 数据口P0 或 P2我用 P0 口接上拉排阻蜂鸣器P2.5低电平触发超温报警按键P3.0~P3.2设置报警阈值上下限为什么 SCL 和 SDA 选 P1.0 和 P1.1因为 STC89C52 的 P1 口是准双向口内部有上拉模拟 I2C 时不需要额外接上拉电阻也能出波形实测波形比较干净。P0 口是开漏输出接 LCD 必须加 10K 排阻上拉否则显示乱码。2.3 模块选型注意买模块还是买裸芯片市面上常见的 MLX90614 有两种形态一种是裸芯片需要自己画板焊在 PCB 上适合做最终产品另一种是模块板载了稳压、上拉电阻、甚至还有滤波电容适合搭原型。我建议新手先用模块因为模块上已经处理好了电源和上拉你只需要四根杜邦线就能接入面包板。不过模块有一个坑有些商家卖的是 MLX90614ESF-BCC出厂地址是 0x5A但有的批号是 0x5B代码里如果写死 0x5A 会读不到数据后面避坑章会细说。2.4 电源设计一个稳定电源决定成败MLX90614 的红外传感器测量精度受电源纹波影响比较大尤其是当蜂鸣器或者继电器动作时电源电压跌落会造成温度读数跳动。我这里用的 5V 稳压模块单独给传感器供电51 单片机用开发板自带的电源两者共地。如果用一个电源同时带蜂鸣器和传感器建议在 MLX90614 的 VDD 引脚就近加一个 10uF 电解电容和 0.1uF 陶瓷电容去耦这个操作能明显减少读数抖动。3. 核心代码拆解从 I2C 时序到温度读取3.1 模拟 I2C 的底层实现51 单片机没有硬件 I2C 外设所以一切都要靠 GPIO 翻转来模拟时序。MLX90614 的 I2C 时钟频率最高支持 100kHz标准模式51 单片机用软件延时完全可以满足。关键是时序要严格起始条件、停止条件、ACK 应答的建立时间和保持时间不能乱。以下是我用的基础 I2C 驱动代码基于 12MHz 晶振的 STC89C52// I2C 引脚定义 sbit SCL P1^0; sbit SDA P1^1; // 微秒级延时12MHz 下大约 12 个机器周期 void delay_us(unsigned int us) { while(us--) { _nop_(); _nop_(); _nop_(); _nop_(); } } // I2C 起始信号SCL 高电平期间 SDA 由高到低 void I2C_Start(void) { SDA 1; SCL 1; delay_us(5); SDA 0; delay_us(5); SCL 0; } // I2C 停止信号SCL 高电平期间 SDA 由低到高 void I2C_Stop(void) { SDA 0; SCL 1; delay_us(5); SDA 1; delay_us(5); } // 发送一个字节高位在前 void I2C_SendByte(unsigned char dat) { unsigned char i; for(i 0; i 8; i) { SDA (dat 0x80) ? 1 : 0; dat 1; SCL 1; delay_us(5); SCL 0; delay_us(5); } } // 接收一个字节高位在前 unsigned char I2C_RecvByte(void) { unsigned char i, dat 0; SDA 1; // 释放总线 for(i 0; i 8; i) { SCL 1; delay_us(5); dat 1; if(SDA) dat | 0x01; SCL 0; delay_us(5); } return dat; } // 发送应答位ack0 表示应答ack1 表示非应答 void I2C_SendAck(unsigned char ack) { SDA ack; SCL 1; delay_us(5); SCL 0; SDA 1; }这段代码的逻辑说明起始信号和停止信号都是通过 SDA 在 SCL 高电平期间的跳变来定义的而数据位的传输则是在 SCL 高电平期间保持 SDA 稳定低电平时才允许 SDA 变化。delay_us(5)在 12MHz 下大约产生 5 微秒延时满足 I2C 标准模式的最小脉宽要求。参数说明I2C_SendByte传入 7 位地址加读写位如 0x5A 左移一位加 0或传入寄存器地址I2C_RecvByte的返回值是传感器送出的原始数据字节。如果你用的晶振是 11.0592MHz延时时间会略有变化但 5 微秒的余量足够不用修改。3.2 读取 MLX90614 温度的完整流程MLX90614 的寄存器映射中0x07 是红外温度数据寄存器TObject10x06 是环境温度寄存器TAmbient。读取温度的标准流程是发送起始信号 → 发送器件地址写方向→ 发送寄存器地址 → 重复起始信号 → 发送器件地址读方向→ 读取两个字节数据 → 读取 PEC 校验字节 → 停止信号。PEC 是包错误校验码是 CRC-8 计算出的用来保证通信完整。下面是我实际用的温度读取函数// CRC-8 查表法计算 MLX90614 的 PEC unsigned char crc8_table[] { 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D }; unsigned char MLX90614_CRC(unsigned char *dat, unsigned char len) { unsigned char crc 0, i; while(len--) { crc ^ *dat; for(i 0; i 8; i) { if(crc 0x80) crc (crc 1) ^ 0x07; else crc 1; } } return crc; } // 读取 MLX90614 某寄存器的原始温度值 //// 返回温度值单位 0.01℃读取失败返回 0xFFFF unsigned int MLX90614_ReadReg(unsigned char reg) { unsigned char buf[3]; unsigned int temp_raw; unsigned char pec, calc_pec; I2C_Start(); I2C_SendByte(MLX90614_ADDR 1 | 0x00); // 写方向 if(I2C_GetAck() ! 0) { I2C_Stop(); return 0xFFFF; } I2C_SendByte(reg); if(I2C_GetAck() ! 0) { I2C_Stop(); return 0xFFFF; } I2C_Start(); // 重复起始 I2C_SendByte(MLX90614_ADDR 1 | 0x01); // 读方向 if(I2C_GetAck() ! 0) { I2C_Stop(); return 0xFFFF; } buf[0] I2C_RecvByte(); I2C_SendAck(0); // 接收第一个字节后应答 buf[1] I2C_RecvByte(); I2C_SendAck(0); // 接收第二个字节后应答 buf[2] I2C_RecvByte(); // PEC I2C_SendAck(1); // 最后一个字节后非应答 I2C_Stop(); // 校验 PEC计算内容包括地址读方向、寄存器、数据两个字节 calc_pec MLX90614_CRC(buf, 2); pec buf[2]; if(calc_pec ! pec) return 0xFFFE; // 校验失败 temp_raw (unsigned int)(buf[0] | (buf[1] 8)); return temp_raw; } // 转换成实际温度公式温度 原始值 * 0.02 - 273.15 int MLX90614_GetTemp(void) { unsigned int raw MLX90614_ReadReg(0x07); if(raw 0xFFFF || raw 0xFFFE) return -1000; return (int)((float)raw * 0.02 - 273.15); }这里有个细节容易踩坑I2C_GetAck()函数需要你自己在 I2C 底层里实现。它的逻辑是发送完 8 位数据后释放 SDA然后在第 9 个时钟周期读取 SDA 电平低电平表示应答。我贴的底层代码里还没写这个函数实际补全如下unsigned char I2C_GetAck(void) { unsigned char ack; SDA 1; // 释放数据线 SCL 1; delay_us(5); ack SDA; // 读取从设备应答位 SCL 0; delay_us(5); return ack; // 返回 0 表示有应答 }这个函数的位置很关键必须在I2C_SendByte之后调用但是I2C_SendByte里面不能自己释放 SDA否则就无法在同一个字节时序里完成读应答。所以我之前贴的I2C_SendByte里最后没有释放 SDA就是因为释放动作要留给I2C_GetAck来做。如果你把释放动作写在I2C_SendByte内部后面再读 ACK 时 SDA 还是低就会误判为一直应答。3.3 温度计算与误差校准MLX90614 输出的原始值 TObject 是 16 位有符号整数温度计算公式是温度(℃) 原始值 * 0.02 - 273.15。这个公式来自数据手册。0.02 是分辨率系数273.15 是开尔文与摄氏度的差值。很多人直接把这个结果往 LCD 上显示然后发现和红外测温枪差好几度这是因为没有做发射率校准。MLX90614 出厂默认发射率是 1.0但现实的物体表面发射率各不相同皮肤大约 0.98黑色哑光表面约 0.95抛光金属只有 0.1~0.3。如果你的被测目标是金属表面读出来的温度会比真实温度低很多。一个简单校准方法是用已知温度的黑体或者水银温度计对照算出修正偏移量然后在代码里加个偏移值// 全局偏移量单位 0.01℃ int temp_offset 35; // 根据实测校准例如正 0.35℃ int MLX90614_GetTempCalibrated(void) { int temp MLX90614_GetTemp(); if(temp -1000) return temp; return temp temp_offset; }这种线性偏移只适用于目标发射率相对固定的场景比如测人体额温。如果目标材质变化很大更专业的做法是通过修改 MLX90614 内部的发射率寄存器0x04来调整但那个操作更复杂需要写 EEPROM还有可能锁死传感器后续章节我会给一个更安全的二次开发思路。3.4 主循环轮询读取与防卡死设计51 主循环里我采用 200ms 轮询一次温度因为有源蜂鸣器和 LCD 刷新都不需要太高的频率。读取前先检查MLX90614_ReadReg是否返回了 0xFFFF如果是说明 I2C 通信失败这种情况多发生在传感器未插稳或者地址错误。为了不至于一失败就卡死我加了一个简单的超时机制连续失败 5 次就显示错误提示然后重新初始化总线。void main(void) { unsigned char err_count 0; int temp; LCD_Init(); LCD_ShowString(0, 0, Temp:); while(1) { temp MLX90614_GetTempCalibrated(); if(temp -1000 || temp -999) { err_count; if(err_count 5) { LCD_ShowString(0, 1, Sensor Error!); err_count 0; } } else { err_count 0; LCD_ShowTemp(temp); CheckAlarm(temp); // 超温报警 } delay_ms(200); } }这里注意delay_ms我用的是定时器 0 实现的毫秒级延时而不是简单的循环延时因为 200ms 的软延时太浪费 CPU。如果你用while(--ms ! 0)这种软件延时200ms 期间中断里的按键扫描也会受到影响。实际项目里建议用TMOD 0x01; TH0 0x4C; TL0 0x00;这种方式写定时器延时。4. 显示与报警模块把温度变成看得见、听得见的信号4.1 LCD1602 显示温度值LCD1602 是 51 开发板上最常用的字符液晶8 位数据模式需要 11 根引脚4 位模式只需要 7 根。我用的是 4 位模式因为可以省下 GPIO 给其他模块。初始化时序要注意每次写命令都要先判断忙标志否则容易出现显示乱码。void LCD_WriteCmd(unsigned char cmd) { LCD_BusyCheck(); // 等待液晶空闲 LCD_DATA cmd 0xF0; // 高四位 LCD_RS 0; LCD_EN 1; delay_ms(2); LCD_EN 0; LCD_DATA (cmd 4) 0xF0; // 低四位 LCD_EN 1; delay_ms(2); LCD_EN 0; } void LCD_WriteData(unsigned char dat) { LCD_BusyCheck(); LCD_DATA dat 0xF0; LCD_RS 1; LCD_EN 1; delay_ms(2); LCD_EN 0; LCD_DATA (dat 4) 0xF0; LCD_EN 1; delay_ms(2); LCD_EN 0; }显示温度时我需要把 int 类型的温度值单位 0.01℃转换成字符串。因为 51 的 printf 族函数在 Keil 里会占用较多代码空间我不建议在单片机里直接用 sprintf而是手工转换void LCD_ShowTemp(int temp) { unsigned char tb[16]; int t temp; // 处理负温度 if(t 0) { tb[0] -; t -t; } else { tb[0] ; } tb[1] 0 (t / 1000) % 10; tb[2] 0 (t / 100) % 10; tb[3] .; tb[4] 0 (t / 10) % 10; tb[5] 0 t % 10; tb[6] C; tb[7] \0; LCD_ShowString(6, 0, tb); }这段代码把整数拆成 百位、十位、小数位比如 31.25℃ 会被显示成31.2因为我只保留了一位小数。如果你需要保留两位可以调整取模顺序。注意 LCD1602 的显示位置是 16 列 2 行第 0 行第 0 列左上角。4.2 报警阈值与按键交互报警功能是很多测温项目的刚需超过 37.5℃ 就响蜂鸣器。为了让阈值可调我用了三个按键一个进入设置模式一个加一个减。设置模式下 LCD 第二行显示报警阈值按键调节后长按确认键保存。这里有一个细节键扫描要放在定时器中断里做主循环才不会被阻塞。unsigned int alarm_threshold 375; // 37.5℃ void CheckAlarm(int temp) { if(temp (int)alarm_threshold) { BEEP 0; // 蜂鸣器置低 } else { BEEP 1; } }蜂鸣器有源和无源驱动方式不同有源蜂鸣器只要供电就会响所以三极管驱动时BEEP 0是响无源蜂鸣器需要给 PWM 脉冲。如果你用的是无源蜂鸣器得加上一条BEEP !BEEP;在主循环里翻转频率大约 2kHz 左右人耳听着舒服。我在这个项目里选有源蜂鸣器是为了省一个定时器。报警的阈值比较要注意alarm_threshold是 375 代表 37.5℃而MLX90614_GetTempCalibrated返回的是 0.01℃ 的整数所以比较时不能用 int 直接和 375 比要统一单位。4.3 环境温度显示与人体温度关系MLX90614 除了测物体温度内部寄存器 0x06 还存环境温度。环境温度的意义在于判断传感器自身是否过热。如果室温超过 60℃ 或者低于 -40℃芯片精度会明显下降。我在屏幕第二行显示Amb:环境温度方便在调试时定位误差来源。有读者问过红外测温枪测额头 37℃为什么我的 MLX90614 读出来才 34℃这是因为额头表面跟环境有热交换尤其是刚刚从室外走进室内额头表面温度会比体内低。医学上的核心体温和表面温度有差距所以如果做体温检测门禁一般要加一个补偿算法或者让被测人在室内稳定两分钟再测。这是传感器本身的物理特性不是代码 bug。5. 避坑指南MLX90614 51 的常见问题排查5.1 现象读到的温度恒为 -273.15这个值其实是(0xFFFF * 0.02 - 273.15)也就是原始值 65535 转出来是 382℃不对65535*0.021310.71310.7-273.151037.55。我这里说 -273.15 是原始值为 0 的情况。更常见的读回 0xFFFF 时转换结果是65535*0.02-273.151037.55很明显不对。所以很多代码会先判断raw 0xFFFF再报错。原因第一传感器地址不对。MLX90614 默认地址是 0x5A但我买过的几个模块地址是 0x5B。你可以在 I2C 总线上扫描一下看看哪些地址有应答。第二SDA 和 SCL 接反了。这是最常犯的低级错误因为 51 开发板的丝印有可能把 P1.0 和 P1.1 标错。第三I2C 时钟线没上拉。虽然 STC89C52 的 P1 口内部有上拉但强度不够如果线太长或者传感器模块距离远波形会变形建议外部加 4.7K 上拉到 VCC。解决先用示波器或者逻辑分析仪看 SCL 和 SDA 波形。如果没有调试工具可以写一个最简单的 I2C 扫描程序挨个发送地址0x50 到 0x5F 之间看哪个地址能收到 ACK。我的经验是把 SCL 和 SDA 交换一下大概率能解决一半的问题。5.2 现象温度读数跳动超过 2℃原因供电不稳是头号嫌疑。我用过一个 USB 转 5V 的线带 LED 灯板时电压从 5.0V 跌到 4.6V温度读数直接跳了 1℃。MLX90614 内部对 VDD 变化其实有校准机制但噪声会影响 ADC 的参考电压。解决在 VDD 和 GND 之间并联 100nF 陶瓷电容和 10uF 钽电容靠近传感器引脚。另外不要把蜂鸣器和传感器共用一个电源引脚蜂鸣器动作瞬间电流尖峰很大。如果必须共用给蜂鸣器加一个单独的 100uF 电容储能。5.3 现象I2C 偶尔卡死在等待 ACK原因51 的软件 I2C 没有超时机制一旦 SDA 被从设备拉低主设备就会一直等。最常见的是传感器被误触发了复位或者 SDA 线路干扰导致时序错乱。解决给所有 I2C 操作加上超时判断。我实现一个简单的计数器unsigned char I2C_GetAck_Timeout(void) { unsigned char t 0; SDA 1; SCL 1; delay_us(5); while(SDA t 20) { delay_us(1); t; } ack SDA; SCL 0; delay_us(5); return ack; }如果 SDA 在 20 微秒内一直为高即无应答就强制退出避免死循环。这个改动不会影响正常通信因为正常 ACK 在 5 微秒内就会出现。5.4 现象读出来的温度是 255℃原因这是 PEC 校验错误时我的函数返回 0xFFFE而 0xFFFE * 0.02 - 273.15 ≈ 255℃正好对上。说明数据在传输过程中有误码或者你用的模块不是标准的 MLX90614而是国产替代芯片 MLX90614 的兼容版本它们响应时序稍慢需要把延时从 5us 加到 10us。解决先把delay_us(5)改成delay_us(10)试试。如果还不行检查你的 PEC 计算是否正确。MLX90614 的 CRC-8 多项式是 0x07初始值是 0x00我前面贴的查表算法是对的但注意它只计算了地址、寄存器、两个数据字节共 4 个字节如果你把起始位和停止位也算进去就会算错。5.5 现象LCD1602 显示乱码但温度能读出来原因LCD 初始化时序不对没有等上电稳定就发命令或者 4 位模式下发送顺序错误。51 上电后 LCD 需要 15ms 以上的等待时间而我的LCD_Init里第一步delay_ms(15)必须放在第一条不能先写命令再延时。解决严格按照数据手册走上电延时 15ms → 写 0x33 → 延时 5ms → 写 0x32 → 延时 5ms → 写 0x28 → 写 0x0C → 写 0x01 → 写 0x06。如果还乱码检查 P0 口是否接了上拉排阻以及 LCD 的 RS/RW/EN 引脚是否和你的代码定义一致。我见过把 RS 接到 P2.7 但是代码写成 P2.0 的翻车现场。6. 二次开发改造把单点测温升级成多通道和低功耗拿到这套代码后最常见的改造方向有两个一是把单点测温改成多通道比如同时测环境温度、两个物体表面温度二是做成低功耗手持设备。这两个方向我给你一条具体可行的改造路线。先说多通道。MLX90614 的 I2C 地址是 7 位硬件上可以通过 SMBus 协议修改内部的 EEPROM 地址寄存器0x0E来给每个传感器分配不同地址这样同一条 I2C 总线上就能挂多个传感器。但 EEPROM 写入有风险操作不当容易锁死芯片。我一般推荐一个更安全的方法给每个 MLX90614 模块加一个用 GPIO 控制电源的 LDO读哪个就开哪个的电源同一时刻只让一个传感器在线地址冲突就自然解决了。代价是软件上要加一个上电稳定延时——传感器上电后需要 50ms 才能进入稳定读数状态。你可以用一个移位寄存器控制多个 LDO 的开关代码改动量很小。再说低功耗。STC89C52 本身功耗不小但如果你换成 STC15 系列或者 STM8代码基本可以移植。MLX90614 在休眠模式下电流只有 2uA 左右唤醒后测量一次大约需要几十毫秒。我建议的做法是每 10 秒唤醒一次读取温度后立即进入掉电模式用外部中断按键唤醒。这样两节 AAA 电池可以用比较久。注意 MLX90614 没有专门的 sleep 引脚唤醒还是靠 I2C 通信命令所以主控也只能跟着它一起睡、一起醒。如果你要改测量精度算法我强烈建议不要改动 MLX90614 内部的发射率寄存器而是把发射率的影响放在应用层做补偿。你可以在代码里维护一个发射率系数表// 根据目标表面类型选择补偿系数 // 目标表面发射率越低补偿系数越大 float emissivity_table[] { 1.00, // 黑体 0.98, // 皮肤 0.95, // 哑光塑料 0.85, // 混凝土 0.70, // 阳极氧化铝 0.20 // 抛光金属 }; float compensate(float raw_temp, float emissivity) { // 常见做法以黑体辐射为参考补偿目标发射率 return (raw_temp 273.15) / emissivity - 273.15; }这段代码的逻辑是基于普朗克辐射的简化近似物体实际温度与测量温度和发射率的关系是线性的在窄范围温度下近似成立。你在设置菜单里选择表面类型程序自动选发射率系数可以明显减少金属表面的测量误差。最后提醒一个血泪经验无论你怎么改每次上电先跑一遍 I2C 扫描确认传感器在线再进入主循环。自从我加了这段自检就再也没遇到过“怎么都不出数据”的玄学问题。从那以后我每次写 51 I2C 传感器驱动都强制先写一个地址扫描函数确认通信链路正常再往下写业务逻辑——这个习惯帮我省下了无数排查时间希望帮到你。本文还有配套的精品资源点击获取