ADT75温度传感器Linux驱动实战:I2C读取与寄存器配置
发布时间:2026/10/1 4:10:58来源:尧图网络
简介一份基于C语言的ADT75数字温度传感器驱动程序以RAR压缩包形式发布包内仅包含一个源代码文件adt75.c整体大小仅3KB适用于嵌入式开发者、Linux驱动学习者以及需要在项目中集成温度监控功能的硬件工程师。ADT75是Analog Devices公司推出的一款高性能数字温度传感器支持精确温度测量常见于工业自动化、环境监测、电子设备冷却系统等温度敏感场合。驱动源码围绕传感器通信展开主要涵盖初始化配置设置工作模式与参数、通过I²C或SPI等接口读取温度原始数据、将二进制结果转换为可读的摄氏温度值以及通信异常时的错误检测与报告部分实现还会涉及根据传感器特性进行校准和调整以确保长期测量的准确性。文件虽然精简但从底层寄存器操作到上层数据解析均有体现适合用来学习传感器驱动开发的基本框架也可作为自行编写类似设备驱动时的参考模板。目前已有87人学习是一份小巧实用的入门级嵌入式驱动资源。1. adt75 资源包到底装了什么先知道这颗芯片是干什么的拿到的adt75.rar_adt75这类工程压缩包在 BSP、评估板驱动、国产替代选型资料里太常见了。名字拆开就两样东西adt75是芯片型号.rar是驱动、手册或参考工程的打包方式。一般解压后你会看到ADT75.pdf、寄存器说明、示例驱动代码以及一份用 I2C 总线读温度的参考程序。这篇实战笔记就把这套东西讲透。ADT75 是 onsemi 出品的一颗数字温度传感器I2C 接口12 位分辨率测温范围 -40°C 到 125°C精度按温度等级不同大约在 ±1°C 到 ±2°C 之间封装常见的是 SOT-23-5。它在 LM75 生态里属于直接兼容的替换型号。对工程师来说这颗芯片最大的价值就是资源好找、寄存器少、驱动简单三五十分钟就能在 Linux 或单片机上把温度读出来。适合的场景包括服务器主板测温、电源模块过温保护、工业采集板温度监控以及任何需要低成本、无校准、直接出数字温度的地方。你在论坛或供应商资源中心下载到的这个压缩包绝大部分情况下就是围绕adt75这颗芯片的配套资料。接下来的内容我不假设包内具体文件长什么样而是按照「这颗芯片应该怎么用」这条主线把驱动编写、寄存器配置、排错和校准方法完整过一遍。这也是我拿到任何xxx.rar_xxx型号包之后的标准拆解流程。2. 先看手册再动手ADT75 的管脚、I2C 地址与寄存器布局2.1 管脚与地址确定为什么同一个型号能有好几个 I2C 地址ADT75 是 I2C 从设备SDA、SCL 两根线必须接上拉电阻典型值 4.7kΩ总线上其他设备保持在 1MHz 以内的工作速度即可。它支持标准模式100kHz和高速模式400kHz但不支持 SMBus 的 PEC所以你在用i2c-tools读它的时候直接按普通 I2C 设备处理就行不用额外挂 CRC 校验。地址是新手第一个容易踩坑的地方。ADT75 的 7 位 I2C 地址低三位由 A0、A1、A2 引脚决定默认悬空或接地时基地址是0x48变化范围是0x48到0x4F。如果你的板子上有多颗 ADT75 或者其他 LM75 兼容芯片就必须把基地址错开。常见做法是用地址翻译芯片、或者借助 PCB 走线把 A0-A2 焊到不同电平而不是让多颗芯片共用同一组地址去硬扛。注意SOT-23-5 封装的 ADT75 通常会把 A0、A1、A2 固定到默认电平这时地址不可配置只能靠板级设计去规划。# 在 Linux 下确认 ADT75 挂在哪个 /dev/i2c-N 上 # 先扫描当前总线上有哪些设备 sudo i2cdetect -l # 假设总线号为 3扫描 0x48~0x4F 地址段 sudo i2cdetect -y 3 # 正常输出下你会看到 0x48 或 0x49 等位置出现 48 / 49 字样扫描输出里出现UU表示该地址已被内核驱动占用出现48这类十六进制值表示该地址有设备响应。如果什么反应都没有先查上拉电阻和总线复用开关不要急着怀疑芯片坏了。这里的-y参数是跳过确认提示脚本化使用时很关键。2.2 寄存器表温度、配置、上限、下限一共就四个ADT75 的寄存器空间非常小上手前把这张表记住后面所有代码都围绕它转。指针寄存器Pointer Register通过 I2C 的第一个字节选择后续要访问的寄存器地址偏移如下偏移寄存器名读写作用0x00温度寄存器只读12 位温度数据低 4 位为 LSB上电默认全 00x01配置寄存器读写控制比较/中断模式、极性、故障队列、关断0x02THIGH 上限寄存器读写过温阈值11 位数据0x03TLOW 下限寄存器读写低温阈值11 位数据温度寄存器读出来是 16 位但有效数据只有 12 位。其中温度数据的符号位在 bit15数据位从 bit15 到 bit4低 4 位固定为 0。所以你要把读回来的两个字节按照(MSB 8) | LSB拼起来再右移 4 位得到一个 12 位有符号数。每 LSB 对应的温度是 0.0625°C即一个最低位代表 1/16 摄氏度。这就是整个芯片最核心的计算方式后面用户态程序和内核驱动都跑不出这个公式。# 用 shell 直接读 ADT75 温度寄存器用户态不带驱动 # 先读指针寄存器指向 0x00再读两个字节 i2cset -y 3 0x48 0x00 temp_raw$(i2cget -y 3 0x48 0x00 w) # i2cget 读 w 模式返回的是小端顺序的两个字节需要交换 # 示例输出 0x1c40表示温度值右移 4 位 0x1c4 452乘以 0.0625 28.25°Ci2cget的w参数表示按 word16 位读取不同内核版本下字节序可能不同所以不能无脑按0x1c40去算。更稳的办法是分两次b读取 MSB 和 LSB自己拼。这条命令模式适合在验证驱动前后快速确认芯片是否活着实际工程不会用 shell 去做周期采样太慢且没法处理错误重试。2.3 配置寄存器极性、比较器模式、故障队列一次配对配置寄存器是 8 位常用的几个 bit 要背下来Bit名称值含义0SHUTDOWN1关断芯片不采样温度寄存器保留最后值1OS_POL1OS/ALERT 引脚输出极性为高电平有效2-3OS_F_QUE00/01/10/11连续 1/2/4/6 次超限才触发报警4OS_COMP_INT0比较器模式温度回到滞回区间后自动释放4OS_COMP_INT1中断模式触发后需要读寄存器清除比较器模式下温度超过 THIGH 且满足故障队列次数后OS/ALERT被拉低或拉高取决于 OS_POL当温度降到 THIGH 减去 10°C 滞回值以下才释放。这个行为特别适合直接去控制风扇或者触发硬件断电不需要软件介入。中断模式则更合适接到 MCU 的 GPIO 中断引脚由软件决定怎么响应。你拿到adt75.rar里如果有驱动源码先看它把配置寄存器写成了什么值再对照这张表决定要不要改。# 设置配置寄存器比较器模式、低电平有效、故障队列 2 次、正常工作状态 # 配置字 0x08bit3 置位表示故障队列 2 i2cset -y 3 0x48 0x01 0x08 # 验证写入是否生效 i2cget -y 3 0x48 0x01故障队列换成工程语言就是去抖次数。单次超限立刻触发报警在真实板子上往往造成误报我建议至少在风扇转速控制和过温保护场景里配 2 次以上。OS_POL的配置必须和你的硬件原理图一致——如果 OS 引脚接到 MCU 的下降沿中断而你把极性配成了高有效芯片永远不触发也正常。3. 用户态把温度读出来C 程序直连 /dev/i2c-N亲手算一次 0.06253.1 用 Linux i2c-dev 接口写一个最小读取程序在内核驱动还没写好的时候最快验证 ADT75 是否正常、精度是否能接受的方式就是写一个用户态 C 程序通过 Linux 的 i2c-dev 接口直接访问芯片。这个方案不依赖特定内核版本不需要编译设备树跑起来就能看到温度值非常适合在 bring-up 阶段做验证。// adt75_user_read.c // 编译方法: gcc adt75_user_read.c -o adt75_user_read #include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/i2c-dev.h #include stdint.h int main(int argc, char *argv[]) { int fd; int bus 3; // /dev/i2c-3按你的实际环境修改 uint8_t addr 0x48; // ADT75 默认地址 uint8_t reg 0x00; // 温度寄存器 uint8_t buf[2] {0}; int16_t raw; float temp; char path[32]; snprintf(path, sizeof(path), /dev/i2c-%d, bus); fd open(path, O_RDWR); if (fd 0) { perror(open /dev/i2c 失败, 先检查 i2c-dev 模块); return 1; } if (ioctl(fd, I2C_SLAVE, addr) 0) { perror(设置从机地址失败); close(fd); return 1; } // 写指针寄存器选择温度寄存器 buf[0] reg; if (write(fd, buf, 1) ! 1) { perror(写指针寄存器失败); close(fd); return 1; } // 读 2 字节MSB 和 LSB if (read(fd, buf, 2) ! 2) { perror(读温度寄存器失败); close(fd); return 1; } // 拼合 16 位右移 4 位得到 12 位有符号数 raw (int16_t)((buf[0] 8) | buf[1]) 4; temp raw * 0.0625f; printf(ADT75 raw0x%02x%02x, temp%.2f°C\n, buf[0], buf[1], temp); close(fd); return 0; }这段程序做了三件事打开 I2C 总线设备、设置从机地址、写指针寄存器后读取两个字节温度数据。最后一步 4不能省因为 ADT75 的温度数据有效位是 12 位且左对齐存放。有些参考代码不做右移直接用 16 位去乘 0.0625那个结果是错的而且温度越高偏差越大。I2C_SLAVE这个 ioctl 设置的是不带读写的纯从机地址。如果你的总线上有地址复用、或者需要 10 位地址访问那就得用I2C_SLAVE_FORCE或I2C_TENBIT但 ADT75 是 7 位地址标准设备用不到。程序里write和read是两次独立的事务中间芯片会完成内部指针自增这是 I2C 协议的标准行为。每次读温度前都要重新跑一遍write指定指针寄存器不要假设上电后指针一定在 0x00因为配置寄存器的写入操作会把指针留在其它位置。3.2 为什么用两个字节去承载 12 位数据符号扩展才是负温度的关键温度低于 0°C 时ADT75 温度寄存器以二进制补码存储读数。比如 -1°C寄存器值是0xFFF0右移 4 位后是0xFFF即十进制 -1。如果你用uint16_t而不是int16_t去接收移位结果-1 会被算成 4095乘 0.0625 后得到 255.94°C——完全离谱。这个符号扩展的坑在几乎所有 LM75 兼容芯片里都存在。我在实际项目里见过有人用raw (buf[0] 8) | buf[1]; raw 4;但buf是uint8_t数组C 语言整型提升规则会把buf[0] 8提升成int然后再赋值给int16_t这种情况反而可能对。但只要你声明成了uint16_t移位就是逻辑移位高位补 0负温度必翻车。稳的写法只有一个int16_t raw (int16_t)((buf[0] 8) | buf[1]) 4;先拼 16 位再整体右移。编译器层面(buf[0] 8)在这里已经转成int强制转int16_t后右移是算术移位。这是整个 ADT75 读取实现里最值得写注释的一行代码。后续如果要在内核驱动里做同样转换内核的sign_extend32宏能派上用场但用户态这样写就足够了。3.3 连续采样怎么读才不出错实际操作中不会只读一次温度就结束。常见做法是循环读取但有个性能细节要注意I2C 每次传输都有地址字节和 ACK/NACK 开销在 400kHz 总线上单次读取温度大约需要几毫秒而 ADT75 内部转换时间约几十毫秒级别。所以循环里必须做延时否则你在芯片还没完成一次新转换时反复读读到的永远是上一次的结果十次全一样。# 一个简单的循环采样脚本每 200ms 读一次 while true; do temp_raw$(i2cget -y 3 0x48 0x00 b) temp_raw2$(i2cget -y 3 0x48 0x00 b) # 第二次 b 读的是低字节 # 实际生产代码请用上面 C 程序一次性读两个字节 echo $(date %T) raw_msb$temp_raw raw_lsb$temp_raw2 sleep 0.2 done这个 shell 循环只能验证采样连续性不能直接拿数值去算温度因为两次i2cget之间可能插入一次新的转换完成导致 MSB 和 LSB 来自不同时刻的采样结果。要得到自洽的 12 位读数必须在一个 I2C 事务里连续读两字节也就是 C 程序里read(fd, buf, 2)那一步。硬件工程师看到这里容易困惑ADT75 内部采样时间是固定的为什么两个字节还会撕裂因为它读取期间新的转换可能完成寄存器更新和你的字节读取不是原子的。为了读到一个完整快照ADT75 会在数据锁存上做处理但标准和兼容芯片的行为不完全一致。作为软件你要么在读取前等待超过一个转换周期要么对同一对 MSB/LSB 连续读两次做一致性校验。后者是更稳健的工程解法。4. 把 ADT75 挂进 Linux hwmon写一个最小内核驱动4.1 hwmon 子系统是什么为什么不让应用层直接去读上一章的用户态程序在验证阶段够用但产品代码里不能这么干。原因很直接总线号在不同内核启动顺序下会漂移多个进程同时open(/dev/i2c-3)会产生并发访问问题而且温度数据没有被统一抽象监控工具直接对接很别扭。Linux 内核为此提供 hwmon 子系统把温度传感器、风扇控制器、电源监测器统一到/sys/class/hwmon下。用户态程序读的是hwmonN/temp1_input不用关心底下是 I2C 还是 SPI也不用关心芯片地址。ADT75 这类 I2C 温度传感器的内核驱动本质上要处理四件事探测设备是否存在、在probe函数里注册 hwmon 设备、读取温度寄存器并换算成毫摄氏度、实现 sysfs 属性文件的回调。读懂这四件事比直接拿一个现成驱动去编译更有价值因为你要改芯片型号时只换寄存器布局就行。4.2 驱动代码从 i2c_driver 到 hwmon_device_register下面这个驱动是完整的可编译模块我缩写掉了无关的MODULE_AUTHOR等元信息聚焦在主逻辑上// adt75_hwmon.c #include linux/module.h #include linux/i2c.h #include linux/hwmon.h #include linux/hwmon-sysfs.h #include linux/err.h #include linux/mutex.h #define ADT75_TEMP_REG 0x00 #define ADT75_CONF_REG 0x01 static struct mutex adt75_lock; static int adt75_read_temp(struct i2c_client *client, long *temp_mc) { int msb, lsb; s16 raw; int ret; mutex_lock(adt75_lock); ret i2c_smbus_read_byte_data(client, ADT75_TEMP_REG); if (ret 0) goto out; msb ret; ret i2c_smbus_read_byte_data(client, ADT75_TEMP_REG 1); if (ret 0) goto out; lsb ret; raw (s16)((msb 8) | lsb) 4; *temp_mc raw * 62500 / 1000; // 0.0625°C 62.5 m°C避免浮点 ret 0; out: mutex_unlock(adt75_lock); return ret; } static int adt75_probe(struct i2c_client *client) { struct device *hwmon_dev; int temp_mc; int ret; mutex_init(adt75_lock); // 探测读一次温度失败即认为设备不存在 ret adt75_read_temp(client, temp_mc); if (ret 0) return -ENODEV; hwmon_dev devm_hwmon_device_register_with_info( client-dev, adt75, NULL, NULL, NULL); if (IS_ERR(hwmon_dev)) return PTR_ERR(hwmon_dev); return 0; } static void adt75_remove(struct i2c_client *client) { // 使用 devm 接口后移除不需要额外清理 } static const struct i2c_device_id adt75_id[] { { adt75, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, adt75_id); static struct i2c_driver adt75_driver { .driver { .name adt75, }, .probe adt75_probe, .remove adt75_remove, .id_table adt75_id, }; module_i2c_driver(adt75_driver); MODULE_LICENSE(GPL);这个驱动真正干活的函数是adt75_read_temp。它用i2c_smbus_read_byte_data连续读取两个寄存器的值合成s16右移 4 位再把温度值转成毫摄氏度。/sys/class/hwmon下的温度属性默认单位就是毫摄氏度内核约定避免浮点计算所以raw * 62500 / 1000这一步把 0.0625°C 转成了 62.5 毫摄氏度再乘 1000最后得到毫摄氏度精度。我要强调一点i2c_smbus_read_byte_data并不等效于裸 I2C 读两个字节它内部会先发送寄存器偏移然后重复起始位再读一个字节。第二次调用时手动传ADT75_TEMP_REG 1效果等同于指针寄存器自增但如果你的芯片对地址自增模式支持不好就改成i2c_smbus_read_i2c_block_data(client, ADT75_TEMP_REG, 2, buf)一次读两字节。ADT75 手册标注的温度寄存器是 16 位宽但一次块读取行为不是所有兼容芯片都一致这里更推荐 block 读法返回来再拼。4.3 怎么确认驱动真的挂上了编译这个模块需要内核头文件最稳的方式是在目标板的内核源码目录外单独编用make -C /lib/modules/$(uname -r)/build M$PWD modules。编好之后insmod adt75_hwmon.ko如果设备树里没有声明这个 I2C 设备probe不会自动被调用。两个办法解决一是用i2c-dev注册一个新设备结点二是检查设备树中 I2C 总线节点下是否有adt7548这样的子节点。# 手动注册设备临时验证重启失效 echo adt75 0x48 /sys/bus/i2c/devices/i2c-3/new_device # 验证驱动是否加载 dmesg | tail -20 ls /sys/class/hwmon/ # 读取温度 cat /sys/class/hwmon/hwmonN/temp1_input你看到/sys/class/hwmon下新增一个目录且temp1_input返回值接近 28000表示 28.000°C就说明整套链路通了。这套流程是从最简核到可运行最快路径但生产环境不要用手动注册应该在设备树里把adt7548写清楚这样内核启动时自动完成probe。5. ADT75 避坑与排查5 个让工程师翻车的细节5.1 读到的温度始终是 255.94°C现象用户态程序读回温度恒定 255.94°C 或不合理大值室温下也是。原因uint16_t承接了 12 位补码负温度或高位符号没扩展0xFFF0 4被当成 4095。解决先确认raw的类型是int16_t并验证(buf[0] 8) | buf[1]拼好后先转int16_t再右移 4 位。你不信这个邪的话可以在代码里打一行原始值日志用常温下的正温度日志对比。正温度情况下符号扩展问题不会暴露一降温就炸所以测试务必覆盖负温度。把板子丢进冰箱前先写好断言测试别靠肉眼。5.2 地址冲突0x48 同时被两颗芯片响应现象i2cdetect在 0x48 显示UU但你的驱动报ENODEV或者读到数值跳变。原因总线上还有其它 I2C 设备恰好使用 0x48。LM75 家族默认地址就是 0x48很多 EEPROM 和监控芯片默认也在 0x48-0x4F 区间这几乎是最常见的地址冲突区域。解决硬件上把 ADT75 的 A0/A1/A2 拉到不同电平改成 0x49、0x4A 等。软件上先用i2cdetect -y 总线号全扫一遍确认地址没有歧义。如果设计已经定型改不了硬件就用地址开关芯片这不是软件能绕过去的。5.3 读取频率过快温度值要么不更新要么毛刺很大现象每秒读 50 次数据看起来几乎不变隔很久突跳一次。原因ADT75 内部转换时间在几十毫秒量级你读得再快它也只在转换周期边界更新温度寄存器。读得频繁没有收益徒增 I2C 总线的负载。解决采样周期设为转换时间的两倍以上。直接在应用层做时间戳过滤上一次读成功后至少等 100ms 再发起下一次。你要是做风扇 PID 控制100ms 的采样周期完全够用温度反馈通道本来就属于慢变量。5.4 OS/ALERT 引脚不动作接线查了半天现象温度超过 THIGHOS 引脚电平状态就是不变或者相反过来了。原因配置寄存器的 OS_POL 位和 OS_COMP_INT 位没有按硬件逻辑设置。比较器模式下芯片内部逻辑输出但外部 MCU 中断引脚可能配置的是上升沿触发极性不匹配就永远不触发。另外一个很容易漏的细节THIGH 和 TLOW 也是 11 位有符号数写入时同样要左移 4 位后再存直接在寄存器里写0x1F4那种原始十进制会被当成非常离谱的阈值。解决先用i2cset手动往 0x02 寄存器写一个明确阈值然后把芯片加热到阈值之上观察 OS 引脚电平变化。把配置寄存器的 OS_POL 从 0 改成 1 再试一次往往能快速定位是极性还是模式的问题。阈值寄存器写入时记得阈值原始值 4比如 75°C 对应 75 / 0.0625 1200实际写入 0x4B00。5.5 内核驱动 probe 失败但 i2cdetect 能扫到芯片现象i2cdetect能看到设备但insmod后dmesg报probe returned -ENODEV或-ENXIO。原因probe里的读取函数用了错误的寄存器偏移或者总线上挂了 I2C MUX你需要先切换 MUX 通道。还有一种情况是设备树里compatible写了adi,adt75但驱动id_table只配了{ adt75, 0 }不匹配自然不进probe。解决probe函数里读一次温度寄存器做设备检测非常可靠但前提是寄存器偏移正确、总线 MUX 已选中。用i2cget手动读一次温度寄存器确认返回的 MSB 数值在合理范围比如 0x01、0x02 左右再回来看驱动。设备树里compatible建议写成adt75如果硬件平台要求兼容厂商前缀就把id_table和of_match_table成对配置别只改一处。这条我在多个平台上踩过属于典型的「手册没看完就开写」翻车现场。6. 进阶玩法从单次读取到连续温度曲线和软校准先把「能读到温度」升级成「能验证温度读得准」。手边没有恒温槽的时候我一般直接用沸水和冰水混合液做两个参考点。0°C 校准点在冰水混合物里稳定存在25°C 或 100°C 用精密水银温度计和芯片背靠背绑在一起等读数稳定后记录差值。ADT75 是出厂校准芯片误差主要是系统性的偏置你可以把误差值写在配置文件里应用层直接对读数做一次线性修正不必改驱动。# 温度数据连续记录到文件做离线分析 while true; do temp$(cat /sys/class/hwmon/hwmonN/temp1_input) echo $(date %s.%N) $temp /tmp/adt75_trace.log sleep 1 done把temp1_input变成 1000 倍毫摄氏度配合你想做的报警阈值、风扇曲线调试非常直观。要画曲线就用gnuplot第一列时间戳第二列温度值直接plot一张图看趋势。PID 控温实验里我习惯把原始温度曲线和 PWM 输出叠在一张图上可以发现温度滞后和振荡周期。最后说一个经验I2C 类的温度传感器软件做得再花哨都不如先把硬件上拉电阻和 PCB 布局搞干净。ADT75 的 SDA/SCL 走线过长、上拉电阻不匹配会导致读回数据偶发跳变这种问题用逻辑分析仪排查最直接别盲目调驱动。我处理过多起「温度读数偶尔翻车」的工单最后定位都是总线上串扰不是芯片问题。先用 i2cdetect 反复扫描确认总线稳定再折腾软件顺序别反。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网