ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

MLX90614 Linux驱动源码解析:SMBus时序与GPIO模拟实战

MLX90614 Linux驱动源码解析:SMBus时序与GPIO模拟实战

简介:这份资源是MLX90614红外温度传感器的Linux驱动源码,面向嵌入式Linux驱动开发者、Android底层工程师及传感器驱动学习者,解决在特定内核环境下快速接入非接触式红外测温模块的问题。源码已在Android 6.0、内核3.4.39的实际产品中验证使用,具备工程参考价值。压缩包内共1个文件,为C语言驱动源码,整体约5KB,代码中完整实现了SMBus起始位、停止位、ACK/NACK时序以及RAM与EEPROM访问命令,并包含SCL/SDA引脚配置、GPIO功能描述结构体与设备名定义,可直接作为驱动移植与调试的起点。目前已有464人学习下载,适合需要理解I2C类传感器驱动框架、掌握内核模块注册与GPIO控制流程的读者参考,也可用于课程设计或产品原型开发中的温度采集方案验证。

1. 从一颗 SMBus 温度芯片说起:这份 MLX90614 Linux 驱动源码能省掉你多少试错

如果你手头有一块 MLX90614 红外温度传感器,跑在 Android 6.0、内核 3.4.39 的全志平台上,想用最少的代码把它接进系统,那这份mlx90614_linux驱动.c值得先看一遍。它不是那种只跑通 I2C 读一个寄存器的 demo,而是把 SMBus 时序、GPIO 模拟、misc 设备节点、ioctl 接口都揉进了一个文件里,属于产品上验证过的驱动骨架。MLX90614 本身是单点红外测温,出厂校准过,通过 SMBus 兼容接口读 RAM 里的 TOBJ1、TOBJ2、TA 三个量,难点从来不在协议本身,而在于时序容错和平台适配。这份源码把这两块都摊开了,适合做嵌入式 Linux 驱动练手,也适合直接拿去做产品移植的起点。

2. 拆开驱动看结构:SMBus 时序、GPIO 模拟与 misc 设备怎么串起来

2.1 为什么不用内核 i2c-gpio,而是自己写 SMBus 时序

MLX90614 的物理层是 SMBus,和 I2C 电气兼容,但协议层有区别:它要求 PEC 可选、时钟拉伸容忍度低、重复起始位的时序窗口窄。内核自带的i2c-gpio驱动走的是标准 I2C 状态机,遇到 MLX90614 这种对 Start/Stop 之间延时敏感的从机,容易出现读回全 0 或者 NACK 的情况。这份源码选择自己用 GPIO 翻转来模拟 SMBus,把 SCL、SDA 的拉高拉低、采样点全部握在手里,代价是要自己处理延时和 ACK 判断,收益是时序可控。

源码里定义了两个 GPIO 描述结构:

#define SCL_NAME "sensor_sck" #define SDA_NAME "sensor_sda" struct gpio_func_desc { unsigned short pin; char *name; }; struct gpio_func_desc SCLK = {0, SCL_NAME}; struct gpio_func_desc SDIN = {0, SDA_NAME};

pin字段初始为 0,实际引脚号在 probe 阶段通过script_item_u从 sys_config 里取。这是全志平台的老套路,Android 6.0 时代 sys_config.fex 还没被设备树完全取代,GPIO 编号、上下拉、驱动能力都写在 fex 里,驱动运行时用script_get_item读出来再gpio_request。如果你换到设备树平台,这一段要改成of_get_named_gpio,但后面的 SMBus 时序逻辑可以原样保留。

2.2 SMBus 读写函数的实现要点

源码里声明了SMBus_StartBit、SMBus_StopBit,后面还有SMBus_SendByte、SMBus_ReceiveByte、SMBus_ReadMemory这一串。核心逻辑是:Start 时 SCL 高电平期间 SDA 由高变低,Stop 时 SCL 高电平期间 SDA 由低变高,发送字节时 MSB 先出,每个字节后跟一个 ACK 位。MLX90614 的 RAM 读取命令格式是 SlaveAddr + Command + 重复起始 + SlaveAddr+1 + 读数据 + PEC。

关键参数在头文件宏里:

#define ACK 0 #define NACK 1 #define SA 0x5a // 单个 MLX90614 时地址为 0x00,多个时默认 0x5a #define RAM_ACCESS 0x00 #define EEPROM_ACCESS 0x20 #define RAM_TOBJ1 0x07 #define RAM_TOBJ2 0x08 #define RAM_TA 0x06

SA这里写 0x5a,注释说单个时地址为 0x00,多个时默认 0x5a。实际 MLX90614 的 7 位从机地址出厂默认就是 0x5A,左移一位后写地址 0xB4、读地址 0xB5。注释里那句“单个时地址为 0x00”容易让人误解,我一般会直接按 0x5A 处理,除非你改过 EEPROM 里的 SMBus 地址。

读温度的函数大致长这样:

u16 SMBus_ReadMemory(u8 slaveAddress, u8 command) { u16 data; u8 pec; u8 arr[3]; SMBus_StartBit(); SMBus_SendByte(slaveAddress); // 写地址 SMBus_ReceiveBit(); // 读 ACK SMBus_SendByte(command); // 命令字 SMBus_ReceiveBit(); SMBus_StartBit(); // 重复起始 SMBus_SendByte(slaveAddress + 1); // 读地址 SMBus_ReceiveBit(); arr[0] = SMBus_ReceiveByte(ACK); // 低字节 arr[1] = SMBus_ReceiveByte(ACK); // 高字节 pec = SMBus_ReceiveByte(NACK); SMBus_StopBit(); data = (arr[1] << 8) | arr[0]; return data; }

逻辑说明:先发写地址,再发 RAM 命令字,然后重复起始位切换成读地址,连续读两个字节,最后发 NACK 再 Stop。data是 16 位原始值,低字节在前。参数说明:slaveAddress传 0xB4,command传RAM_TOBJ1即 0x07。读回来的原始值要转成摄氏度,公式是temp = data * 0.02 - 273.15,这个 0.02 是 MLX90614 的固定分辨率,别改成别的。

2.3 misc 设备节点与 ioctl 接口

驱动用miscdevice注册,设备名mlx90614,这样系统起来后/dev/mlx90614就能直接 open。ioctl 命令一般定义成读 TOBJ1、读 TOBJ2、读 TA 三个:

#define MLX90614_GET_TOBJ1 _IOR('m', 0x01, int) #define MLX90614_GET_TOBJ2 _IOR('m', 0x02, int) #define MLX90614_GET_TA _IOR('m', 0x03, int) static long mlx90614_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int temp; switch (cmd) { case MLX90614_GET_TOBJ1: temp = mlx90614_read_temp(RAM_TOBJ1); break; case MLX90614_GET_TOBJ2: temp = mlx90614_read_temp(RAM_TOBJ2); break; case MLX90614_GET_TA: temp = mlx90614_read_temp(RAM_TA); break; default: return -EINVAL; } if (copy_to_user((int __user *)arg, &temp, sizeof(temp))) return -EFAULT; return 0; }

mlx90614_read_temp内部调用SMBus_ReadMemory,把原始值转成 0.01 摄氏度单位的整数返回,避免浮点进内核。用户态拿到后除以 100 就是摄氏度。这种设计比直接暴露 sysfs 更灵活,适合需要高频轮询的场景,但要注意 ioctl 里不能睡太久,SMBus 一次读大概几百微秒,可以接受。

3. 从编译到出数:把驱动挂进 3.4.39 内核的完整步骤

3.1 编译环境与 Makefile 写法

Android 6.0 时代的内核编译通常走make -C $(KERNEL_DIR) M=$(PWD) modules。这份源码是单文件,Makefile 可以写得很短:

obj-m += mlx90614_linux驱动.o mlx90614_linux驱动-objs := mlx90614_linux驱动.o KERNEL_DIR ?= /path/to/android/kernel PWD := $(shell pwd) all: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

注意文件名里有中文和空格风险,实际编译前建议把mlx90614_linux驱动.c改成mlx90614_linux.c,Makefile 里对应改掉。obj-m那行如果源文件名带中文,make 解析时可能出问题,这是血泪经验,别问为什么编译报No rule to make target。

编译前确认内核已经编译过一次,Module.symvers存在,否则模块加载时会出现disagrees about version of symbol的玄学错误。全志 3.4.39 内核的交叉编译工具链一般是arm-linux-gnueabi-或arm-none-linux-gnueabi-,在 Makefile 里通过CROSS_COMPILE传入。

3.2 设备树与 sys_config 的适配

Android 6.0 全志平台可能还在用 sys_config.fex,也可能已经切到设备树。如果是 fex,在[sensor]段加:

[sensor_sck] gpio = port:PH05<1><default><default><default> [sensor_sda] gpio = port:PH06<1><default><default><default>

驱动里用script_get_item("sensor_sck", "gpio", &item)读出来,再gpio_request、gpio_direction_output。如果是设备树,节点写成:

mlx90614 { compatible = "mlx90614"; scl-gpios = <&pio 5 0>; sda-gpios = <&pio 6 0>; status = "okay"; };

驱动 probe 里用of_get_named_gpio取。两种方式选一种,别混用,混用会出现 GPIO 申请失败但 probe 不报错的情况,最后读出来全是 0xFF。

3.3 加载模块与验证读数

编译出mlx90614_linux驱动.ko后,push 到设备:

adb push mlx90614_linux驱动.ko /data/local/tmp/ adb shell insmod /data/local/tmp/mlx90614_linux驱动.ko adb shell dmesg | tail -20

dmesg 里应该看到mlx90614: probe success之类的打印。然后写个用户态测试程序:

#include <fcntl.h> #include <sys/ioctl.h> #include <stdio.h> #define MLX90614_GET_TOBJ1 _IOR('m', 0x01, int) int main(void) { int fd = open("/dev/mlx90614", O_RDWR); if (fd < 0) { perror("open"); return -1; } int temp; ioctl(fd, MLX90614_GET_TOBJ1, &temp); printf("TOBJ1: %.2f C\n", temp / 100.0); close(fd); return 0; }

交叉编译后 push 进去跑,手放在传感器前面,读数应该从室温往 30 多度爬。如果一直是 0 或者 65535,先查 GPIO 有没有被别的驱动占用,再查 SCL/SDA 有没有接反。MLX90614 的 SDA 需要上拉,模块上一般自带 4.7k,如果没有,读出来会全是 NACK。

4. 避坑与排查:MLX90614 驱动最容易翻车的五个地方

4.1 读回全 0 或 0xFFFF

现象:ioctl 返回 0 或者 65535,温度换算后是 -273.15 或 1037.55。 原因:SMBus 时序不对,ACK 没收到就继续读,或者 GPIO 方向没切对。MLX90614 在 Start 之后如果 100us 内没收到时钟,会释放总线。 解决:用示波器抓 SCL/SDA,确认 Start 后第 9 个时钟 SDA 被从机拉低。没有示波器就加大udelay,把 SCL 高电平时间从 1us 调到 5us 试。

4.2 GPIO 申请失败但 probe 不报错

现象:insmod 成功,dmesg 没有 error,但读数是 0。 原因:sys_config 里 GPIO 被其他驱动先占了,gpio_request返回 -EBUSY,但代码里没检查返回值。 解决:在gpio_request后加if (ret) { printk("gpio request fail %d\n", ret); return ret; },把错误暴露出来。常见占用者是 backlight 或者 tp 驱动。

4.3 温度跳变超过 1 度

现象:同一目标,连续读值在 25.0 和 27.0 之间跳。 原因:MLX90614 的 TOBJ1 是 0.02 分辨率,但内部滤波没开,或者读的时候刚好跨了刷新周期。 解决:连续读三次取中间值,或者在 EEPROM 里把 IIR 滤波系数设成 50%。源码里没做滤波,用户态可以自己加滑动平均。

4.4 模块加载后系统卡死

现象:insmod 后 adb 掉线,屏幕黑。 原因:SMBus 时序里用了mdelay而不是udelay,在原子上下文里睡太久触发调度异常。 解决:所有延时改成udelay,单次不超过 100us。如果必须长延时,放到工作队列里做。

4.5 换平台后编译报mach/sys_config.h找不到

现象:从全志 3.4.39 换到 4.9 或 5.4 内核,编译直接失败。 原因:mach/sys_config.h和mach/platform.h是全志老平台私有头文件,新内核没有。 解决:把 GPIO 获取改成设备树方式,删掉这两个 include,用linux/gpio.h和linux/of_gpio.h替代。SMBus 时序部分不用动。

5. 进阶:把这份驱动改成设备树 + 标准 i2c 客户端的思路

如果你想把这份源码往新内核上迁,最省事的路径不是继续用 GPIO 模拟,而是注册成标准 i2c 客户端,让内核 i2c 子系统去处理时序。MLX90614 在标准 i2c 下也能读,只是要处理 PEC 和重复起始位。具体做法是:把SMBus_ReadMemory换成i2c_smbus_read_word_data,设备树里加compatible = "melexis,mlx90614",然后在 probe 里i2c_set_clientdata。这样代码量能砍掉三分之二,但前提是你的平台 i2c 控制器支持重复起始位,全志的 i2c 控制器在 3.4.39 上对重复起始支持一般,所以原作者才自己写 GPIO 模拟。

我一般会保留 GPIO 模拟版本作为兜底,同时写一个 i2c 客户端版本,用 Kconfig 切换。验证方法很简单:两个版本都读室温,差值在 0.5 度以内就算通过。如果 i2c 版本读出来是 0,先查设备树里 i2c 节点的clock-frequency是不是 100k,MLX90614 不支持 400k。

从那以后我每次移植这类 SMBus 传感器,都强制先用示波器抓一遍 Start 和第一个 ACK,确认时序没问题再写上层逻辑。希望帮到你。

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

RELATED READING

延伸阅读

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