简介:这是围绕 ADT75 数字温度传感器编写的驱动程序源码包,主要面向嵌入式软硬件开发者和需要做温度监测的工程师,解决系统与传感器之间通过 I2C/SPI 接口通信并准确获取温度数据的问题。ADT75 是 ADI 公司的精密温度传感器,被广泛用于工业自动化、环境监测与电子设备散热控制;驱动代码具体涵盖初始化配置、数据采集、原始数据转换、通信异常处理以及测量校准等核心环节,可为读者理解实际驱动框架和协议实现提供清晰范式。压缩包内仅收录 1 个 C 语言源文件,整体体积约 3KB,代码量精简、结构清晰,适合在 Linux 环境下参考移植与二次开发。目前已有 87 人浏览学习,对于正在调试 ADT75 或准备为同类型传感器编写驱动的开发者,是一份可直接借鉴的实用样例。
1. 一个 rar 包里的 adt75.c,拆开前先看芯片三件事
做温度采集的人多半遇到过这种情况:拿到一个 adt75.rar,解压出来只有一个 adt75.c,既没有 README 也没有 datasheet,第一反应是找规格书还是直接 make?我的建议是先看芯片本身。ADT75 是 ADI 出品、LM75 寄存器布局兼容的 12 位数字温度传感器,测量范围 -55°C 到 +125°C,LSB 是 0.0625°C,I²C 接口,地址由 A0/A1 引脚决定。这颗片子真正难的不是「读温度」这个动作,而是三个容易被忽略的细节:7 位从机地址怎么拼、12 位温度数据在 SMBus 读回来时字节序是反的、负温度下符号位怎么扩展。这篇就顺着 adt75.c 的源码往下拆,把寄存器映射、驱动骨架、编译加载和排错串成一条线,适合正在写 Linux I²C 驱动或想把传感器快速接到嵌入式板子上的开发者。
2. ADT75 寄存器与 I²C 时序:地址、数据格式、配置位
2.1 从 A0/A1 引脚推算 7 位从机地址
ADT75 的 I²C 从机地址是 7 位,高 4 位固定为1001,低 2 位由芯片的 A0、A1 引脚电平决定,中间的 1 位留空。也就是说地址范围是1001 A1 A0,换算成 7 位地址就是 0x48 到 0x4B 四个值,对应 A1A0 为 00、01、10、11。很多第一次用的人直接按 0x48 写死,十块板子里有三块读到错误地址,就是因为 A1/A0 被硬件工程师拉到了不同电平。正确做法是 probe 里用i2c_check_functionality配合i2c_detect去扫,或者至少把地址做成设备树/平台数据可配项。
datasheet 里写的是 7 位地址,但 Linux 内核的struct i2c_client里存的是 7 位左移一位后的 8 位地址(即 0x90~0x96 range,注意 bit0 是读写位)。写驱动的人最容易在这里晕:设备树里reg = <0x48>,内核会自动转成 client->addr = 0x48,I²C 协议层发送时再左移拼上 R/W 位。所以别自己在 probe 里手动左移,否则地址会变成 0x90,总线上一片 NoAck。
| 引脚组合 (A1, A0) | 7 位地址 | SMBus 读写地址(8 位) |
|---|---|---|
| 0, 0 | 0x48 | 0x90 / 0x91 |
| 0, 1 | 0x49 | 0x92 / 0x93 |
| 1, 0 | 0x4A | 0x94 / 0x95 |
| 1, 1 | 0x4B | 0x96 / 0x97 |
2.2 0x00 温度寄存器与 12 位补码格式
ADT75 的核心寄存器是 0x00,只读,存放当前温度。数据是 12 位二进制补码,左对齐存放在两个字节里:高字节是完整 8 位,低字节只用了高 4 位,低 4 位读出时恒为 0。换算关系是:温度 = 原始 12 位值 × 0.0625°C。比如原始值 0x1A4(十进制 420),温度就是 420 × 0.0625 = 26.25°C;负温度如 -2°C,原始值则是 0xFE0。
这里有个常见误读:直接把读回来的两个字节拼成一个 u16,然后右移 4 位得到「温度码」,忽略符号位扩展。12 位补码的符号位是 bit11,当 bit11 为 1 时,必须先把数据当成有符号数做符号扩展,再右移 4 位。否则 -2°C 会算成 4062 × 0.0625 ≈ 253.9°C,直接爆表。后面第 3 章会给出具体换算代码。
除 0x00 外,还有 0x01 配置寄存器、0x02 低阈值 THYST、0x03 高阈值 TOS、0x04 one-shot 寄存器。0x02 和 0x03 都是可读写的比较器阈值,中断模式下用到,普通轮询采集可以不理。
2.3 配置寄存器 0x01 与 one-shot 的工作机制
0x01 配置寄存器是 8 位,重点管两个事:bit7 是 shutdown 位,置 1 时 ADC 停止连续转换,芯片进入低功耗模式,此时读 0x00 返回的是最后一次转换结果;bit6 和 bit5 决定比较器/中断模式及输出极性。如果需要按一下采一次温,就把 bit7 置 1,然后写 0x04 寄存器触发一次单次转换。转换完成后再读 0x00,就能拿到本次采样值。
驱动初始化最常见的写法如下:
/* 读取当前配置寄存器 */ s32 cfg = i2c_smbus_read_byte_data(client, 0x01); if (cfg < 0) return cfg; /* 开启连续转换模式:清除 bit7 (shutdown) */ cfg &= ~0x80; i2c_smbus_write_byte_data(client, 0x01, cfg);这里i2c_smbus_read_byte_data的第一个参数是struct i2c_client *,第二个参数是寄存器地址,返回 0~255 的寄存器值,负值表示通信错误。连续转换模式下,ADT75 默认大约每 100ms 更新一次温度寄存器,不需要外部触发。如果板子对功耗敏感,可以改成 shutdown + one-shot 组合:shutdown 置 1 后待机电流在微安级,需要读数时再写 0x04,读完后继续睡。
3. 从 adt75.c 源码看温度采集:寄存器读写与负温度换算
3.1 驱动入口与 probe:设备树匹配和设备注册
一个完整的 adt75.c 驱动骨架,入口通常长这样:
static const struct i2c_device_id adt75_id[] = { { "adt75", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, adt75_id); static const struct of_device_id adt75_of_match[] = { { .compatible = "adi,adt75" }, { } }; MODULE_DEVICE_TABLE(of, adt75_of_match); static struct i2c_driver adt75_driver = { .driver = { .name = "adt75", .of_match_table = adt75_of_match, }, .probe = adt75_probe, .id_table = adt75_id, }; module_i2c_driver(adt75_driver);module_i2c_driver展开后就是module_init+module_exit,省去手动登记i2c_add_driver的样板代码。of_match_table负责和设备树节点匹配,i2c_device_id是给板级代码i2c_board_info方式用的,两张表都维护,兼容新老两种注册路径。注意 ADT75 与 LM75 寄存器布局高度一致,主线内核的 lm75 驱动并没有收录adi,adt75这个 compatible,所以要么在自己的驱动里维护这张匹配表,要么直接借用nxp,lm75a这类已存在的 compatible 让 lm75 驱动接管。自己写驱动时,我一般会同时保留两张表,并用MODULE_DEVICE_TABLE导出,方便 udev 自动加载模块。
probe 里做的核心事情是初始化配置并注册 hwmon 设备:
static int adt75_probe(struct i2c_client *client) { struct device *hwmon_dev; s32 cfg; cfg = i2c_smbus_read_byte_data(client, ADT75_REG_CFG); if (cfg < 0) return cfg; cfg &= ~ADT75_CFG_SHUTDOWN; /* 清 bit7,进入连续转换 */ cfg &= ~ADT75_CFG_COMPARATOR; /* 默认中断模式,不用比较器 */ i2c_smbus_write_byte_data(client, ADT75_REG_CFG, cfg); hwmon_dev = devm_hwmon_device_register_with_groups( &client->dev, "adt75", client, adt75_groups); return PTR_ERR_OR_ZERO(hwmon_dev); }devm_hwmon_device_register_with_groups是内核推荐的注册方式,设备移除时自动释放,不需要手动hwmon_device_unregister。这里的adt75_groups指向sysfs属性组,用户态通过/sys/class/hwmon/hwmonX/temp1_input读到的温度值,就是由这组属性文件背后的回调函数提供的。
3.2 SMBus 读 word 的字节序陷阱
读取温度寄存器最直接的方法是i2c_smbus_read_word_data(client, 0x00),但这里埋着一个大坑:SMBus 的 word 传输是 little-endian,先读低字节再读高字节,组合出来的 u16 是「低字节在前」。而 ADT75 的 0x00 寄存器是标准的 big-endian 数据:第一个字节是高 8 位,第二个字节才是低 4 位。直接用返回值会被颠倒。
所以我一般不用 SMBus word 接口,而是手动发一个 2 字节读请求,把字节序在自己手里控制住:
static int adt75_read_temp(struct i2c_client *client, long *temp_milli) { u8 reg = ADT75_REG_TEMP; /* 0x00 */ u8 buf[2]; struct i2c_msg msg[2] = { { .addr = client->addr, .flags = 0, .len = 1, .buf = ® }, { .addr = client->addr, .flags = I2C_M_RD, .len = 2, .buf = buf }, }; int ret, raw12; ret = i2c_transfer(client->adapter, msg, 2); if (ret != 2) return ret < 0 ? ret : -EIO; /* buf[0] 是高字节,buf[1] 高位才是有效数据 */ raw12 = ((s16)((buf[0] << 8) | buf[1])) >> 4; /* 0.0625°C 用整数表示为 625/10000 °C */ *temp_milli = raw12 * 625; return 0; }这里的关键是((s16)((buf[0] << 8) | buf[1])) >> 4:先拼成一个有符号 16 位数,此时符号位已经在 bit15 上,再算术右移 4 位,得到 12 位有符号温度码,同时完成了符号扩展。如果写成(u16)… >> 4,负温度就会变成大正数。这也是 adt75.c 里最容易改错的一行。
3.3 温度定标:为什么乘以 625 而不是除以 16
内核 hwmon 的约定是温度以毫摄氏度为单位输出,即 25°C 应该上报为 25000。ADT75 每个 LSB 是 0.0625°C,即 62.5 m°C。直接用浮点会拖慢驱动且引入内核浮点依赖,所以用定点运算:raw12 × 625 = raw12 × 0.0625 × 10000,得到的就是毫摄氏度。举例验证:
| 12 位原始值 | 温度码(有符号) | 换算结果 |
|---|---|---|
| 0x1A4 | 420 | 420 × 625 = 262500 m°C,即 26.25°C |
| 0xFE0 | -32 | -32 × 625 = -20000 m°C,即 -2.0°C |
| 0x000 | 0 | 0 m°C,即 0°C |
raw12 * 625的乘法在 32 位整数范围内完全安全,温度码上限 4095,乘积最大约 255 万,不会溢出 int。temp_milli上报给 hwmon 后,用户态sensors命令或直接cat temp1_input读到的就是毫摄氏度值。
4. 编译加载与 i2c-tools 验证:把驱动跑起来并核对数据
4.1 解压 rar 与搭建外部模块 Makefile
拿到 adt75.rar 先解压,Linux 下用unrar x adt75.rar或者7z x adt75.rar都行。解开后如果只有 adt75.c,需要自己补一个外部模块的 Makefile:
obj-m += adt75.o KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) cleanKERNELDIR指向当前内核的 build 目录,外部模块编译就是进入内核源码树、以M=$(PWD)的方式只编当前目录下的目标文件。编译前确认系统装了linux-headers-$(uname -r)包,否则/lib/modules/…/build是个空链接,一 make 就报找不到 Makefile。编完生成 adt75.ko,insmod adt75.ko加载,dmesg | tail看有没有 probe 成功的日志。
4.2 设备树节点与模块加载顺序
驱动加载只是把i2c_driver注册进内核,真正触发 probe 还要总线上有对应地址的设备。设备树里节点写成这样:
&i2c2 { status = "okay"; adt75@48 { compatible = "adi,adt75"; reg = <0x48>; }; };reg = <0x48>告诉内核这个从设备的 7 位地址是 0x48,compatible用于和adt75_of_match里的字符串匹配。如果模块没有自动加载,需要确认CONFIG_I2C、CONFIG_HWMON都开着,并且 i2c 控制器节点本身状态正常。板级代码老平台没有设备树,就用i2c_board_info:
static struct i2c_board_info __initdata adt75_i2c_info = { I2C_BOARD_INFO("adt75", 0x48), }; i2c_new_client_device(adapter, &adt75_i2c_info);I2C_BOARD_INFO第一个参数必须和adt75_id表里的字符串一致,第二个参数是 7 位地址。相较设备树,这种方式更直观,但每块板子都要改 C 代码,不方便维护,新平台首选设备树。
4.3 用 i2c-tools 和 16 进制视角核对数据
加载驱动前,先用 i2c-tools 确认芯片真的在总线上:
i2cdetect -y 2 i2cdump -y 2 0x48i2cdetect -y 2扫描 2 号 i2c 总线,-y跳过交互确认;如果 0x48 处显示编号或UU,说明设备应答正常。i2cdump -y 2 0x48会以 16 进制形式把寄存器全部导出来,第一列是寄存器地址,后面是值。看 0x00 和 0x01 两个字节,能直接验证驱动里的配置是否写进去了。
用i2cget单独读温度寄存器更能说明字节序问题:
i2cget -y 2 0x48 0x00 w这里w表示 SMBus read word。假设返回0x401a,注意这是小端组合:实际寄存器内容是0x1a40,即 26.25°C。如果直接拿0x401a去套 12 位换算,结果完全对不上。这也是为什么我推荐用i2c_transfer手动读两个字节而不是用 word 接口。想再看细一点,可以把i2cdump的输出重定向到文件,用xxd或任意 16 进制编辑器打开,对着数据手册逐字节核对。
4.4 sysfs 接口验证
驱动 probe 成功后,hwmon 子系统会自动创建属性文件:
ls /sys/class/hwmon/ cat /sys/class/hwmon/hwmon0/temp1_inputtemp1_input输出的是毫摄氏度,比如26250表示 26.25°C。如果读到的是 0 或一个巨大的正数,优先怀疑符号扩展和字节序,而不是芯片坏了。用dmesg看内核日志,如果出现Failed to read temperature之类的 error,先检查i2cdetect是否还能看到设备。
5. 排错清单与一个调试技巧:先扫地址再看字节序
把 adt75.c 跑通的过程中,最常踩的就四类问题。第一,i2cdetect扫不到地址,十有八九是 A0/A1 引脚电平和你预期不一致,或者总线上拉电阻没焊,用示波器量 SCL/SDA 波形最直接;第二,probe 失败但设备存在,检查compatible字符串和设备树 reg 是否匹配,寄存器地址字节是否传成了client->addr;第三,温度读数正负异常、数值翻倍或减半,先怀疑字节序再怀疑换算系数,ADT75 是 12 位数据不是 16 位,右移 4 位这步不能省;第四,读出来一直是同一个值,多数是配置寄存器 bit7 被置 1 进入了 shutdown 模式,此时 0x00 只返回最后一次转换结果。
最后一个实用技巧:分两步验证驱动逻辑。第一步,不做任何换算,直接用i2cget -y 2 0x48 0x00 b连续读 5 次,记录高字节变化;第二步,把手放到芯片封装上,等温度明显上升后再读一次,如果高字节单调递增,说明 I²C 通路和芯片都正常,问题只剩换算。验证脚本随手就能写:
for i in $(seq 1 5); do i2cget -y 2 0x48 0x00 b sleep 1 doneb表示读单字节,返回的是 0x00 寄存器高字节。初始读数大约在 0x1A 附近(对应 26°C 上下),手指按压后变成 0x1C、0x1D 这类更大值,说明采集链路没问题。此时再回头看 adt75.c 里的换算代码,逐个字节比对,通常一眼就能找出符号扩展写错的地方。
本文还有配套的精品资源,点击获取