简介:一份面向嵌入式Linux开发者的MLX90614红外温度传感器驱动源码,来自实际产品项目,已在Android 6.0环境下配合内核3.4.39验证使用。驱动基于Linux杂项设备框架,采用GPIO模拟SMBus时序完成单总线读写,代码中完整涵盖总线起始/停止位、ACK/NACK应答、RAM与EEPROM访问命令、0x5A从机地址配置等关键环节,同时结合平台sys_config管脚配置方式,便于在不同单板间做引脚适配;对设备节点注册、open/read/ioctl接口实现、copy_to_user数据拷贝等字符设备驱动核心要点也有清晰展示,适合作为协议型传感器驱动的入门与进阶参考。资源包共1个文件,为单个C语言源文件,压缩后仅5KB,体量小、结构紧凑,包含GPIO配置结构、寄存器地址宏定义以及SMBus起始/停止和数据读写函数,便于按模块拆解学习,也可在同类项目中直接移植参考。已有464人浏览学习,对正在调试MLX90614、研究内核驱动或GPIO模拟I2C/SMBus的开发者具有直接参考价值。
1. MLX90614红外温度传感器,真正难写的不是获取温度那一行
MLX90614 是 Melexis 出品的一款数字红外测温芯片,I2C 接口,出厂校准过,测温范围 -40℃ 到 125℃(物体温度 -70℃ 到 382℃),在很多非接触测温项目里是首选。很多人拿到手第一反应是「不就是读个 I2C 寄存器吗」,但实际写 Linux 驱动时会发现坑不少:它走的是 SMBus 协议,RAM 地址只有 5 位,数据里带 PEC 校验,而且默认 7 位设备地址 0x5A 很容易和你板子上其他 I2C 器件撞车。这让「抄一个 i2c_smbus_read_word_data 就完事」的思路在真实项目里撑不住。这篇文章围绕这包驱动源码要解决的四个问题展开:SMBus 和普通 I2C 读写的差别、CRC 校验怎么处理、设备树地址配置、以及发射率校准对测温精度的影响。目标是让看过的人能自己把源码编进内核,而不是停留在「代码能跑」的层面。
2. 驱动源码主线:SMBus 协议、PEC 校验、regmap 封装
2.1 为什么 MLX90614 走 SMBus 而不是直接 I2C 读
MLX90614 的物理层是 I2C,但它的读写时序严格遵循 SMBus 规范。区别不只是「能不能读」,而是寄存器地址长度和字节序。标准 I2C 读某个寄存器通常是「先发设备地址 + 寄存器地址,再重发设备地址读数据」,寄存器地址可以是 8 位甚至 16 位。MLX90614 的 RAM 地址只有 5 位,全部映射在 0x00-0x3F 空间里,而且它要求在同一个 STOP 条件内完成写地址和读数据,这正好符合 SMBus 的SMBUS_READ_WORD_DATA时序。
#define MLX90614_RAM_TA 0x06 #define MLX90614_RAM_TOBJ 0x07内核里访问它最简洁的方式就是用i2c_smbus_read_word_data(),它替你把 start、地址、寄存器、重复 start、读两个字节、stop 全部封装好。你要是自己用i2c_master_send/i2c_master_recv分开写,大概率会遇到地址对不上或者波形不对的问题。很多人在这卡住,换回 smbus 函数立刻就好。
2.2 驱动源码里的三要素:i2c_client、cdev、温度换算
源码包里的mlx90614.c核心结构是典型的字符设备驱动框架:
probe里拿到i2c_client,分配私有结构体保存数据。- 用
alloc_chrdev_region和cdev_add创建设备节点。 read函数里调用i2c_smbus_read_word_data读 RAM,把 16 位数据换算成温度。
温度换算公式是两个:物体温度Tobj = 0.02 * data - 273.15每 LSB 对应 0.02℃,环境温度Ta = 0.02 * data - 273.15。有个细节值得注意——MLX90614 输出的是 16 位有符号数,但靠左对齐,高两位是符号扩展,所以直接data & 0x7FFF会把负数温度变成巨大正数。正确的是用s16类型直接接收i2c_smbus_read_word_data的返回值,或者手动做符号扩展。
static s32 mlx90614_read_temp(struct i2c_client *client, u8 reg) { s32 data; data = i2c_smbus_read_word_data(client, reg); if (data < 0) return data; return (s16)le16_to_cpu(data); } static ssize_t mlx90614_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { struct mlx90614_data *data = filp->private_data; s32 raw; int temp; raw = mlx90614_read_temp(data->client, MLX90614_RAM_TOBJ); if (raw < 0) return raw; temp = (raw * 20) / 100; /* 0.02℃ 每 LSB,放大 100 倍存整数 */ if (copy_to_user(buf, &temp, sizeof(temp))) return -EFAULT; return sizeof(temp); }这里最关键的是(s16)转换,它把读到的 16 位原始值按有符号处理,再乘 20 除 100,避免浮点运算。copy_to_user传的是放大 100 倍的整数温度,用户态自己除 100 就行,省去内核里禁止浮点的麻烦。
2.3 CRC-8 PEC 字节怎么处理
MLX90614 带 SMBus PEC(Packet Error Checking),在读取命令里,主设备可以要求从设备在数据后额外返回一个 CRC 字节。这个校验算法是 CRC-8,多项式x^8 + x^2 + x^1 + 1,即 0x07,初始值 0x00。内核里i2c_smbus_read_word_data默认不读 PEC,你拿到的就是纯数据,但源码包里如果用了I2C_SMBUS_PEC标志,就得知道 CRC 算在哪几个字节上。
PEC 覆盖的字节顺序是:从设备地址(左移一位的写地址)+ 寄存器地址 + 数据低字节 + 数据高字节。注意,PEC 计算时用的地址是 7 位地址左移一位加上读写位之前的那个字节,不是 7 位原值。
static u8 mlx90614_crc8(u8 *buf, int len) { u8 crc = 0; int i, j; for (i = 0; i < len; i++) { crc ^= buf[i]; for (j = 0; j < 8; j++) { if (crc & 0x80) crc = (crc << 1) ^ 0x07; else crc <<= 1; } } return crc; }如果你把 PEC 看成「可选」而忽略,多数情况能读到数据,但偶尔会因为总线时序毛刺收到错值,表现是温度忽跳几十度。生产环境的做法是开启 PEC,验证失败就重试一次。驱动源码里这块逻辑通常在read函数中需要用户态传入I2C_SMBUS_PEC标志,且读 PEC 后和计算值比对,不一致返回-EIO。
3. 把驱动源码接入实际板子:设备树配置和加载流程
3.1 设备树节点怎么写
MLX90614 挂在哪条 I2C 总线上,设备树里就要把探测地址写清楚。默认地址 0x5A,但你可以通过 SMBus 的 EEPROM 访问方式改地址,比较少见。设备树模板大概是:
&i2c1 { mlx90614@5a { compatible = "melexis,mlx90614"; reg = <0x5a>; interrupt-parent = <&gpio0>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; }; };reg = <0x5a>是 I2C 从地址,注意这里要写 7 位地址,不是 8 位。很多刚上手的人把 datasheet 里的写地址 0xB4 填进去,内核直接报-ENXIO。interrupts这行是可选的——MLX90614 没有数据就绪中断脚,如果你不需要中断就删掉。驱动probe里匹配id_table用的是"melexis,mlx90614"这个 compatible,源码里要对应写全,大小写敏感。
3.2 编译进内核还是以模块加载
源码包的 Makefile 如果只写了一行obj-m += mlx90614.o,那它是按模块编译的。常见做法是交叉编译后用insmod动态加载,调试方便。更好的做法是把它编进内核,在drivers/misc/下建目录,Kconfig 加上依赖:
config MLX90614 tristate "Melexis MLX90614 infrared temperature sensor driver" depends on I2C help Say Y here to support the MLX90614 series.模块加载后的表现是i2c: I2C adapter i2c-1 registered后多出一行设备注册信息。如果你在用户态用i2cdetect -y 1能看到5a地址,说明设备树探测成功,接下来就是看/dev下有没有你注册的字符设备。一般我习惯在probe里用device_create建一个mlx90614类下的设备节点,或者干脆用iio框架——但原版源码包通常用 miscdevice,节点名是mlx90614或带序号。
3.3 用户态如何验证驱动工作
模块加载成功但读不到温度,最直接的排查命令是:
i2cdetect -y 1 i2cget -y 1 0x5a 0x07 wi2cget的w参数表示以「字」方式读取,正好对应 SMBus 的 read word 时序。如果你能在命令行直接拿到一个数值,比如0x7ef9,说明硬件通路没问题,问题在驱动。拿到原始值后自己算一下:0x7ef9是 32505,乘 0.02 减 273.15 等于 37.0℃,这就有底气继续查驱动代码。
4. 校准、发射率和排错:从读数对到测温准
4.1 发射率(Emissivity)怎么在代码里体现
MLX90614 出厂默认发射率是 1.0,也就是说它假设被测物是理想黑体。现实里塑料、氧化金属、皮肤都不满足。发射率可以通过写 EEPROM 单元 0x04 来调整,范围 0.1 到 1.0。驱动源码如果只做了读,没做写,那你测金属表面温度会系统性偏低,测反光铝板甚至能差十几度。
实际项目里,我一般不在驱动里开放 EEPROM 写操作,而是留一个 sysfs 属性,把发射率系数交给应用层。算法很简单:把目标物体真实温度对应的辐射功率折算回黑体等效温度。Melexis 提供过一个简化公式,结论是当你把发射率调为 0.5 时,测同样的目标温度,MLA90614 输出的温度会变低,你需要乘一个比例系数修正。这块源码包里如果有emissivity参数,通常实现是sensor->emissivity = value后重新算内部补偿变量。
4.2 三个高频坑:地址冲突、EMI、总线速度
I2C 地址冲突是板级最常发生的问题。MLX90614 固定地址 0x5A,如果板子上还有个 AT24C02 EEPROM 也在 0x5A,两个器件都会 ACK,总线数据完全错乱。解决办法是通过自带的SMBus Alert机制给传感器改地址,或者换别的器件地址。驱动里如果设备树reg写 0x5A 但 i2cdetect 显示两个设备都在 0x5A,那就是硬件冲突。
电磁干扰(EMI)也会让它丢数据。MLX90614 的 SDA/SCL 引脚上拉电阻最好在 4.7kΩ 到 10kΩ 之间,走线不要超过 10cm,否则在电机启动瞬间温度读数会卡住。这类问题驱动里只能加 PEC 重试来缓解。总线速度也有讲究,MLX90614 支持 100kHz 标准模式,但不少开发板 I2C 控制器默认跑 400kHz,高速模式下它也能工作,可时序裕量变小,建议在设备树里限制clock-frequency = <100000>。
4.3 排错时看什么:dmesg、i2cdetect、逻辑分析仪
遇到读不出来别急着改代码,先按顺序排除:
dmesg | grep -i mlx90614 dmesg | grep -i "i2c.*error" i2cdetect -l i2cdetect -y 1dmesg里如果出现failed to read RAM之类的错误,先确认 I2C 总线编号对不对。有些 SoC 有多个 I2C 控制器,设备树里挂的设备不一定在 i2c-1 上。i2cdetect -l列出来的总线编号就是你的设备树里的顺序,通常从 0 开始。如果驱动注册成功但read返回-EIO,大概率是 PEC 计算有误——检查 CRC 覆盖的起始字节是不是从设备地址开始。
5. 把驱动的 raw 值打包成「人类可读」的温度值
驱动最终交付给应用层的接口形态很重要。源码包里最常见的做法是在read函数里返回 raw 值,应用层自己换算;差一点的驱动甚至会返回 16 位原始寄存器值不做任何处理。我习惯在内核里把温度放大 1000 倍后以整数形式返回,这样既能保留 0.001℃ 的分辨率,又不让内核碰浮点运算,应用层也省去协议解析歧义。
除了字符设备的 read,还有一个值得做的操作是在unlocked_ioctl里加一条MLX90614_IOC_READ_RAW,让应用可以直接读环境温度和物体温度两个通道:
#define MLX90614_IOC_MAGIC 0x91 #define MLX90614_IOC_READ_TA _IOR(MLX90614_IOC_MAGIC, 0x01, int) #define MLX90614_IOC_READ_TOBJ _IOR(MLX90614_IOC_MAGIC, 0x02, int)用户态读环境的代码形如:
int fd = open("/dev/mlx90614", O_RDWR); int temp_x1000; ioctl(fd, MLX90614_IOC_READ_TOBJ, &temp_x1000); printf("Tobj: %.3f\n", temp_x1000 / 1000.0);这样做的价值在于把「硬件协议、CRC、换算」全部收敛在驱动里,应用层只面对一个带符号的定点数。最后再给一个验证技巧:用tail -f /var/log/kern.log同时观察dmesg,拿一个稳定的热源贴近传感器,看温度输出是否有正常上升趋势;如果读数在 0 和满量程之间乱跳,多半是 PEC 重试逻辑没生效,把环境温度和物体温度都打出来对比,就能快速定位问题是出在 sensor 本身还是驱动换算。
本文还有配套的精品资源,点击获取