
简介C51_MXL90614是一套基于C51单片机与MLX90614红外热电偶传感器通信的参考工程适合嵌入式初学者和需要实现非接触测温的开发者使用。资源围绕I2C总线通信展开覆盖传感器初始化、温度数据读取、寄存器配置及常见通信时序处理并结合LCD1602显示等周边模块构成较完整的测温应用示例。包内共50个文件包含C语言源码.c、头文件.h、编译生成文件.lst、.obj、.hex、.map、.m51、工程备份文件.bak、.uvproj、.uvopt以及MLX90614中文数据手册PDF便于对照芯片手册理解代码实现压缩包约1.53MB结构清晰适合在Keil C51环境下直接打开工程运行查看。已有235人浏览学习。通过研读项目中的初始化序列、I2C读写函数和主程序流程读者能够掌握MLX90614的基本驱动方法并据此移植到其他8051系列项目中。1. 先把 MXL90614 和 C51 之间的 I2C 时序对齐再谈读温度C51 和 MXL90614 通讯表面是 I2C实际是 SMBus最大坑在时序。标题里的 MXL 是常见笔误官方型号是 Melexis 的 MLX90614。这颗红外温度传感器的测量距离可以做到几十厘米但很多人在 C51 上移植参考代码时发现12MHz 晶振下能正常读换到 24MHz 就返回 0xFFFF。原因很简单——模拟 I2C 的延时写死了没有跟随主频变化。下面先解释为什么 MLX90614 不能按普通 I2C 处理再给出 Keil5 下可编译通过的 C51 通讯代码随后是波形排查和 CRC 校验最后把参考代码改成能复用的温度采样模块。无论做红外测温枪、配电柜温度监测还是黑体炉前级采样这套思路都能直接落地。2. 看懂 MLX90614 的数据手册与寄存器才能定 C51 的通讯参数2.1 芯片地址与 RAM/EEPROM 分区不要直接读 0x01MLX90614 的从机地址默认是 0x5A拼上读写位之后写地址是 0xB4读地址是 0xB5。这个地址保存在 EEPROM 的 0x0E 地址上电后自动载入。参考代码通常写#define MLX90614_ADDR 0x5A发送时左移一位。如果改过从机地址就不能再用默认值需要从 EEPROM 回读。数据手册把寄存器分成 RAM 和 EEPROM 两区。RAM 地址 0x01 是物体温度 To0x07 是环境温度 Ta0x00 是红外原始数据的高字节。很多人拿着别家温度传感器的经验想读 0x00 来判断数据就绪结果读到的是无符号整数的一部分根本不是状态位。更不要在 0x01 上做读改写MLX90614 的 RAM 只支持读不支持单独的写命令。2.2 读温度的三步握手写指针、读数据、CRC校验MLX90614 不能像 AT24C32 一样直接发读地址。要读某个 RAM 寄存器必须先发起一个写事务把目标地址写进去然后再用重复起始条件切到读模式。很多参考代码写成send(0xB4); send(0x01); restart(); send(0xB5); read漏掉 restart 的话从机根本不会把数据放到总线上。从机返回三字节低字节、高字节、CRC 校验字节。CRC 是 SMBus 的 PEC多项式 0x07初始值 0x00。忽略校验时偶尔也能读到正确温度但总线一旦有噪声可能把 0xFF00 当成真实值。下面的时序骨架展示了一次完整读操作start(); send_byte(0xB4); // 从机地址 写 check_ack(); send_byte(0x01); // RAM地址指向To check_ack(); restart(); send_byte(0xB5); // 从机地址 读 check_ack(); data_lo read_byte(1); // 读低字节主机回ACK data_hi read_byte(1); // 读高字节主机回ACK pec read_byte(0); // 读CRC主机回NAK stop();逻辑说明read_byte的参数表示主机收到该字节后发 ACK 还是 NAK。最后一个字节必须回 NAK从机据此释放 SDA下一次起始条件才能正常产生。如果最后也回 ACK从机会继续拉低 SDA主控的停止条件会被从机钳位表现为连续读几次后程序卡死在等待应答。2.3 关键参数表时钟、上拉、CRC、从机地址C51 工程的延时和上拉电阻选择都来自下面这组参数参数值/范围说明从机默认地址0x5A7位地址写0xB4读0xB5最大时钟频率100 kHzSMBus标准超过后可能丢ACKSDA/SCL上拉2k~10k3.3V取2k5V取4.7kCRC多项式0x07初始值0x00匹配SMBus规范To温度分辨率0.02℃16位有符号数负温需处理补码EEPROM写周期典型10ms改地址或发射率后必须等待上拉电阻对 C51 特别关键。P3 口在准双向模式下有弱上拉但驱动能力只有 100uA 左右。MLX90614 的 SDA 是开漏输出没有外部电阻时低电平释放后很难回到高电平。用 10k 在 5V 下也能工作但总线电容稍大上升沿会超过 1us。我一般用 4.7k并在程序里把 SCL 高电平时间拉到 5us 以上给边沿留余量。2.3.1 为什么 C51 硬件 I2C 不适合 MLX90614STC12、STC15 系列内部带硬件 I2C但很多人实测读 MLX90614 不稳定。因为 SMBus 要求数据在 SCL 下降沿后保持一段时间而硬件 I2C 外设的采样窗口是由分频器固定的无法灵活插入延时。模拟 IO 可以做到按微秒调时序这才是参考代码几乎全部使用模拟 I2C 的原因。3. 用 Keil5 写 C51 的 MXL90614 模拟 I2C 通讯代码3.1 Keil5 兼容 C51 和 STM32 安装时怎么把 MXL90614 工程建好Keil5 默认安装 MDK 后只能建 ARM 工程找不到 8051 芯片。需要在 Pack Installer 的 Packs 标签页搜索Keil::C51点击 Install 后Device 列表才会出现 Atmel AT89C51、NXP P89V51 等型号。新版 Keil5 还会要求同时安装 Legacy Support否则老 C51 工程会报缺 Device 头文件。C51 和 MDK 可以共存安装路径不要有中文否则链接器生成 HEX 文件时可能报路径错误。工程建好后在 Options for Target 的 Output 选项卡勾选 Create HEX File。模拟 I2C 用到的_nop_()需要包含intrins.h。另外注意 C51 编译器会把没有使用的中断函数优化掉如果 SDA/SCL 引脚定义写在头文件但头文件只在某个.c文件里引用链接时可能找不到符号。稳妥做法是在mlx90614.c文件顶部直接写#include reg51.h #include intrins.h sbit SCL P3^6; sbit SDA P3^7;不要把这几个sbit放到公共头文件里因为不同工程引脚定义不同放.c文件里维护更清晰。3.2 模拟 I2C 的四个基础函数下面这段代码按 11.0592MHz 晶振编写适用于 AT89C51 和大多数兼容 C51 核void i2c_delay(void) { unsigned char i; for (i 0; i 4; i) _nop_(); } void i2c_start(void) { SDA 1; SCL 1; i2c_delay(); SDA 0; i2c_delay(); SCL 0; i2c_delay(); } void i2c_stop(void) { SDA 0; SCL 1; i2c_delay(); SDA 1; i2c_delay(); } bit i2c_send_byte(unsigned char dat) { unsigned char i; bit ack; for (i 0; i 8; i) { SDA (dat 0x80) ? 1 : 0; dat 1; SCL 1; i2c_delay(); SCL 0; i2c_delay(); } SDA 1; SCL 1; i2c_delay(); ack SDA; SCL 0; i2c_delay(); return ack; } unsigned char i2c_recv_byte(bit send_ack) { unsigned char i, dat 0; for (i 0; i 8; i) { SCL 1; i2c_delay(); dat 1; if (SDA) dat | 0x01; SCL 0; i2c_delay(); } SDA send_ack ? 0 : 1; SCL 1; i2c_delay(); SCL 0; i2c_delay(); SDA 1; return dat; }逻辑说明i2c_start在 SCL 为高时把 SDA 从 1 变 0产生起始条件i2c_stop在 SCL 为高时把 SDA 从 0 变 1产生停止条件。i2c_send_byte在第 9 个时钟释放 SDA读取从机应答。i2c_recv_byte在 SCL 高电平期间采样 SDA参数send_ack为 1 时主机拉低 SDA 回应答为 0 时释放 SDA 回非应答。参数说明i2c_delay的循环次数决定 SCL 频率。11.0592MHz 下一次_nop_()约 0.09us循环 4 次加上函数调用开销延时约 1.8us完整周期约 7.2us频率约 139kHz已经超过 MLX90614 的 100kHz 限制。建议把for循环改成i 5把频率压到 100kHz 以下。3.3 读 MLX90614 温度的函数封装有了底层函数可以封装出可复用的mlx90614_read_regunsigned char mlx90614_read_reg(unsigned char reg, unsigned int *value) { unsigned char data_lo, data_hi, crc; i2c_start(); if (i2c_send_byte(0xB4)) { i2c_stop(); return 1; } if (i2c_send_byte(reg)) { i2c_stop(); return 2; } i2c_start(); if (i2c_send_byte(0xB5)) { i2c_stop(); return 3; } data_lo i2c_recv_byte(1); data_hi i2c_recv_byte(1); crc i2c_recv_byte(0); i2c_stop(); *value ((unsigned int)data_hi 8) | data_lo; return 0; }逻辑说明先写从机地址和寄存器指针然后用一次i2c_start()产生重复起始再发读地址。返回 0 表示读取成功1、2、3 分别对应不同阶段的应答失败方便在主循环里直接判断错误来源。value指向的 16 位数据是原始 ADC 码需要乘 2 才能得到 0.01℃ 的精度值。温度转换时注意负数如果data_hi的最高位为 1说明物体温度是负的要把整个*value按 16 位补码转成带符号数再乘 2。忽略这一步冬天测到零下温度时会显示 65535 附近的错误值。3.4 常见编译错误与参数调整Keil C51 在编译这个工程时常碰到的几个问题如下表错误/警告原因处理方法LINKER ERROR: ?C_STARTUP缺少启动文件工程中添加 STARTUP.A51WARNING: BAD PARAMETER位变量定义导致 data 区溢出把 bit 变量移到 bdata 区读回温度恒为 0xFFFF上拉过弱或从机地址不对测 SCL/SDA 电平改用 4.7k读回温度跳动大未校验 CRC 或电源纹波大加滤波电容 100nF提示模拟 I2C 的每个函数里SCL 拉高之后要等 SDA 电平稳定再读这是 SMBus 和普通 I2C 最容易忽略的时序点。延时宁可长不可短100kHz 的上限不会因为多延几微秒而出问题。4. 实战排查MLX90614 通讯失败的常见原因与波形验证4.1 引脚连接与上拉电阻先检查电平域MLX90614 的 SDA 和 SCL 都是开漏输出必须接上拉。先量电压再查逻辑把 C51 程序烧录进去悬空 SDA 和 SCL对 VDD 测量。正常应接近 VDD如果在 2.5V 左右可能是 C51 的准双向口弱上拉和外部强上拉分压的结果并不一定有问题需要用示波器看边沿速度。供电也要注意。老批次 MLX90614 内部没有稳压VDD 长时间接 5V 会明显发热。建议传感器侧用 3.3V 供电上拉电阻接到 3.3VC51 的 5V IO 通过开漏形式访问不会产生电平冲突。如果是 5V 系统且不换电至少要在 VDD 引脚并联 100nF 10uF 电容。4.2 用示波器确认时序起始、重复起始、ACK示波器 CH1 接 SCLCH2 接 SDA共地触发通道选 SDA 下降沿。正常读序列应该是SDA 拉低时 SCL 为高这是起始条件接着 8 个时钟发 0xB4第 9 个时钟 SDA 被从机拉低说明从机应答之后 8 个时钟发寄存器地址然后 SDA 释放并再次拉低形成重复起始再是 0xB5读两个数据字节和一个 CRC 字节最后 SDA 拉低后释放产生停止条件。如果逻辑分析仪只看到起始、地址、寄存器地址后面没有重复起始说明restart没有产生。许多 C51 参考代码用start()来代替restart()但这两个函数在 SCL 电平不同时刻的行为不一样。i2c_start要求 SCL 为低、SDA 为高所以直接调用可以完成重复起始如果你的代码里把 SCL 拉高后才调用start()那就成了“空闲总线上重新启动”会多一个停止位。4.3 数据一直返回 0xFFFF 的常见原因这张表整理了高频故障现象原因排查方法返回 0xFFFF从机地址错误、寄存器写不进去先读 EEPROM 设备 ID返回 0x0000寄存器地址指向 0x00 且未读 CRC读 0x07 环境温度验证通讯时好时坏上拉电阻过大、线缆过长上拉改为 4.7k线长小于 20cm连续读几次后卡死最后一个字节回了 ACK检查i2c_recv_byte(0)是否最后调用返回 0xFFFF 最常见原因是写指针没有收到 ACK。可以在主循环里显示mlx90614_read_reg的返回值返回 1 说明第一个地址没有确认先查接线和延时返回 3 说明重复起始后从机不应答检查 SDA 是否被外部器件拉低。4.4 CRC 校验的位运算法CRC 校验不一定每次都做但做了能避免把噪声数据当真。下面的函数逐位计算 CRC-8unsigned char calc_crc(unsigned char *data, unsigned char len) { unsigned char crc 0x00; unsigned char i; while (len--) { crc ^ *data; for (i 0; i 8; i) { if (crc 0x80) crc (crc 1) ^ 0x07; else crc 1; } } return crc; }逻辑说明把起始后主机发送的所有字节从机地址、寄存器地址以及读回的数据低字节、高字节按顺序放进缓冲区计算得到的值应与第三个读回字节相同。注意数据缓冲区不包含读地址 0xB5 和 CRC 本身。位运算法在 C51 上最多循环 24 次耗时几十微秒对测温采样频率没有影响。5. 把 MXL90614 参考代码变成 6 个可以复用的技巧5.1 用宏把主频和延时解耦在i2c_delay前面加一个主频宏比如#define CPU_FREQ_MHZ 12再用条件编译选择循环次数。从 12MHz 移植到 24MHz 时只改一个宏不用每次排查“低频率能读高频率读不到”的问题。5.2 中位数滤波替代平均值红外测温容易受环境光干扰连续采 5 次排序后取第 3 个。中位数滤波对尖峰脉冲的抑制比均值好C51 跑 5 个数排序只需要几十行代码。每次采样间隔至少 250ms整体测温周期在 1.5 秒内。5.3 同时输出物体温度和环境温度读 0x01 得到 To读 0x07 得到 Ta。环境温度可以直接用于补偿计算两条读取指令可以连续执行不需要额外等待因为同一转换周期内的数据是同步的。把两个结果都放到结构体里方便串口输出 JSON 或 OLED 分行显示。5.4 备份 EEPROM 参数并恢复改发射率或从机地址前先把 EEPROM 内容读出来存到代码数组。下次上电发现 CRC 不对时从备份恢复默认配置。写 EEPROM 需要 10ms 左右不能放在采样循环里否则会阻塞 I2C 总线。5.5 用超时避免 I2C 卡死模拟 I2C 最大的风险是 SDA 被从机拉低后程序一直while(SDA)等待。给每个读函数加一个循环计数比如最多等待 1000 次超时后强制发送停止信号再把 SDA 和 SCL 都置高。这样总线即使挂死下次采样前也能重新初始化。5.6 C51 链接器控制文件里调整栈位置当程序逐渐变复杂C51 默认栈放在 IDATA 低端容易和位变量区冲突。在 Options for Target 的 L51 Misc 里填入STACK(?STACK0X80)把栈顶挪到 0x80 以上的 IDATA 区。这个技巧对 MXL90614 这类函数嵌套不深但中断比较多的工程能消除不少随机复位问题。本文还有配套的精品资源点击获取