如果你手头正好有一片 TMP75AIDR 温度传感器,想在 Linux 环境下把它顺利跑起来,第一反应千万别急着去搜所谓"官方驱动包"。在绝大多数嵌入式项目里,这个活儿用内核自带的 lm75 驱动家族就能办完,只要做好三件事:确认内核配置、补设备树节点、加载模块并验证读数。所谓 Linux 设备驱动移植,很多时候并不是从寄存器读写开始手搓一个驱动,而是把软硬件链路打通、把现成代码用到正确的位置上。这篇就按我第一次做 TMP75AIDR 驱动移植时的实际路径来拆,适合刚接触 I2C 传感器驱动的朋友,也适合已经调过几次设备树、想快速搞定温度采集的工程师。
1. 项目概述与整体设计
1.1 TMP75AIDR 到底是什么芯片
TMP75AIDR 是一颗数字温度传感器,走 I2C 接口(兼容 SMBus),内部带 12 位 ADC,温度分辨率能做到 0.0625°C/LSB。供电电压范围比较宽,2.7V 到 5.5V 都能工作,因此常被用在嵌入式控制板上直接测量板级温度,或者配合 MCU/Linux 主机做环境温度监控。
芯片本身封装很小,引脚也不多,最需要关注的其实是地址配置。它有两个地址引脚 A0、A1、A2,或者三个(不同批次引脚定义略有差异,AIDR 后缀属于工业级版本),这些引脚的电平组合决定了芯片在 I2C 总线上的 7 位地址。默认情况下,如果这三个引脚都接地,地址就是 0x48,这是我最常用的一个地址。也就是说,总线上的 I2C 地址由高四位 1001 加低三位 A2/A1/A0 组成,稍微算一下就能得到 0x48 到 0x4F 的完整地址范围。
测量范围方面,这种工业级版本一般标称可以覆盖 -40°C 到 +125°C,在常温区间比如 -10°C 到 +85°C 内精度表现会更好,误差通常在 ±1°C 左右。手册里不同温度窗口的精度指标不同,具体以你拿到的手册批次为准。移植驱动前先看一眼寄存器结构也有必要:寄存器 00h 是温度数据寄存器,01h 是配置寄存器,02h/03h 分别存的是高温阈值和低温迟滞阈值。温度换算那块我后面细讲。
1.2 为什么优先选 lm75 驱动这条路线
Linux 内核的 hwmon 子系统里有个非常经典的驱动文件drivers/hwmon/lm75.c,它不只是为原版 LM75 芯片服务的。打开这个文件里的 of_match_table 就能看到,它兼容了一大批"LM75 协议兼容"的温度传感器,其中明确包含了"ti,tmp75"这个 compatible 字符串。TMP75 和 TMP75AIDR 在寄存器布局和读取语义上基本一致,所以内核驱动可以直接拿来用。
选择内核自带驱动而不是厂商提供的外挂驱动,有几个很实际的原因。第一,lm75 驱动已经在内核社区经过了很多年的验证,寄存器读写、符号扩展、温度换算这些基础逻辑都被反复打磨过,出问题的概率比我临时写的驱动低得多。第二,它天然接入 sysfs 和 hwmon 框架,驱动加载成功后,用户态直接用cat /sys/class/hwmon/hwmon0/temp1_input就能拿到温度值,完全不用自己写应用层协议解析。第三,它和设备树配合得很好,我在设备树里声明一个 compatible 为"ti,tmp75"的节点,内核的 I2C 子系统就会自动把驱动和设备绑定起来。
所以这次的移植工作,本质上是"适配 + 验证",不是"从零开发"。理解这一点之后,整个项目的设计思路就清晰了:我先梳理内核配置,再编写设备树节点,接着编译加载驱动,最后用各种工具确认温度读数。如果硬件地址有变化,我只要调整设备树中的 reg 属性即可。
1.3 移植规划:从硬件接线到用户态数据
在动手敲设备树之前,我建议先把三条信息确认清楚:芯片挂在哪条 I2C 总线上、总线编号是多少、芯片引脚上的地址电平是什么。这三个信息直接决定设备树节点写在哪个控制器下、reg 填什么值。
我这次的实际场景是某嵌入式主板上的 i2c-2 总线挂了一颗 TMP75AIDR,芯片 A0/A1/A2 全部接低,地址为 0x48。于是规划的任务清单是:先在内核配置里打开 lm75 驱动的编译开关,再在设备树里给 i2c-2 增加一个tmp75@48节点,然后编译并替换设备树,加载 lm75 模块,最后通过 hwmon 接口读取温度并和参考温度计做对比。整个过程不涉及修改驱动源码,只有一处地方可能需要微调,就是当芯片地址不是 0x48 时改设备树的 reg。
这样的规划方式也适用于其他 I2C 温度传感器,比如 TMP275、TMP102 这些型号,原理相同:先确认内核有没有现成兼容驱动,如果没有,再考虑自己写一个轻量的 hwmon 驱动。
2. 内核与设备树侧的准备工作
2.1 内核配置:先把 lm75 驱动编译开关打开
首先是内核配置。lm75 驱动对应的 Kconfig 选项是CONFIG_SENSORS_LM75,在 menuconfig 里大致路径是 Device Drivers → Hardware Monitoring support 下面的 LM75 compatible 相关选项。不同内核版本菜单显示可能略有差异,但关键词都是 LM75。
这个选项可以编成y直接编入内核,也可以编成m生成独立模块。我的习惯是先编成模块,调试阶段方便反复加载卸载,确认稳定后再改成y固化进去,这样也能减少启动后手动加载模块的步骤。如果之前内核已经编译过,修改配置后一定要重新编译内核或模块,别只编译设备树就完事,否则驱动根本不存在。
配置完成后,建议检查一下生成的 .config 里确实有CONFIG_SENSORS_LM75=y或=m这一行。编译内核或模块后,如果选的是m,会得到lm75.ko文件。把它放到目标板的模块目录,或者使用内核编译流程里的modules_install安装,然后执行depmod -a更新模块依赖。
这里有个我踩过的坑:有时候为了省事直接在目标板上用 modprobe 加载模块,结果提示找不到符号或版本不匹配。大多数情况是因为模块编译用的内核源码版本和当前运行内核版本不一致,或者交叉编译环境没配对。最稳妥的方式是按你开发板厂商提供的 SDK 流程,把内核和模块一起编译、一起部署。
2.2 设备树节点写法与地址计算
设备树是整个移植过程里最容易出问题、也最值得认真检查的部分。我最终的节点写法是这样的:
&i2c2 { status = "okay"; clock-frequency = <100000>; tmp75@48 { compatible = "ti,tmp75"; reg = <0x48>; }; };先说reg = <0x48>这个值怎么来的。TMP75AIDR 的 I2C 地址是 7 位,格式是1001 A2 A1 A0。如果 A0/A1/A2 都接地,二进制就是1001000,换算成十六进制就是 0x48。如果 A2 接高而其他接地,地址变成1001100,也就是 0x4C。硬件上怎么接,就填对应的值。很多开发板为了兼容不同模块,会预留地址跳线或者焊盘,调试时先拿万用表量一下引脚电平最保险。
compatible = "ti,tmp75"对应的是事件驱动的匹配字符串,也对应内核源码里of_device_id表。建议整个字符串原样保留,不要自己改成tmp75aidr之类的东西,因为驱动匹配靠的就是这个名字。补充一点,老版本内核的设备树绑定文档里写的是ti,tmp75,但某些内核版本也接受tmp75之类的旧写法,如果发现驱动没有 probe,可以查一下当前内核源码里到底支持哪些字符串。
&i2c2表示挂在第二条 I2C 控制器上。具体编号要根据你的平台手册确定,有的平台叫i2c1、i2c3,不要照抄我这里的写法。如果总线上已经有时钟或者又有挂载其他设备,节点加在同一个控制器下面即可。
2.3 编译、部署与初步确认
设备树改好后,按平台标准流程编译 dtb。比如使用 ARM 交叉编译环境时,通常是执行make dtbs或者直接走 SDK 的打包脚本。编译完成后,建议先反编译一遍生成的 dtb,确认新节点确实编译进去了。用dtc工具反编译的命令大致是:
dtc -I dtb -O dts -o output.dts your-board.dtb然后在 output.dts 里搜tmp75,能看到节点内容就说明设备树层没问题。把 dtb 刷进目标板启动,如果之前选了模块方式,启动后执行:
modprobe lm75然后查看 I2C 总线上设备是否被创建:
ls /sys/bus/i2c/devices/正常会多出一个类似2-0048的目录。2是 I2C 总线号,0048是芯片地址。再进一步确认:
ls -l /sys/bus/i2c/devices/2-0048/driver如果驱动成功绑定,这个软链接会指向 lm75 驱动目录,比如显示.../drivers/lm75。到这一步,驱动和设备树的基本链路就算通了。
3. 驱动适配与核心实现细节
3.1 I2C 驱动与设备树节点的匹配过程
不要把它想得太玄学。Linux 内核对 I2C 设备匹配有两种常见方式:一种是驱动里注册了i2c_device_id表,按名字匹配;另一种是注册了of_match_table,按设备树里的 compatible 匹配。lm75 驱动两种都有,所以我的设备树节点一创建,内核的 I2C core 就开始遍历已注册的 I2C 驱动,拿设备树里的 compatible 字符串去和驱动的 of_match_table 比对,一旦对上就调用驱动的 probe 函数。
lm75 驱动源码里大致是这样:
static const struct of_device_id lm75_of_match[] = { { .compatible = "ti,tmp75" }, {} }; MODULE_DEVICE_TABLE(of, lm75_of_match);匹配成功后,probe 里做的事情有:分配设备私有数据结构、设置 I2C 客户端、检查芯片是否存在或做一次初始化读取、注册 hwmon 设备,并且创建对应的 sysfs 属性文件。用户态看到的/sys/class/hwmon/hwmon0/temp1_input就是这一步创建的。
这里我想强调一个思路:理解匹配流程,不是为了背代码,而是为了排查问题。如果driver软链接没出现,先确认 compatible 字符串是否完全一致,再确认这个平台上 I2C 控制器的时钟有没有开启。很多时候驱动没绑定,不是代码问题,而是设备树节点没挂在正确的控制器下面,或者该控制器没被使能。
3.2 温度数据怎么从寄存器变成用户态读数
TMP75AIDR 内部温度寄存器是 12 位有效数据,存放在两个字节里。读取回来的原始值本质是有符号数,最高位是符号位。12 位二进制每个 LSB 对应的温度变化是 0.0625°C,也就是 1/16 摄氏度。所以温度计算公式很简单:
实际温度 = 寄存器值 × 0.0625°C
但这个乘法不能直接用浮点跑,尤其是内核态里,能省则省。lm75 驱动里用整数运算完成了换算,把寄存器的高 16 位有符号值做一次右移,再乘以 500 再除以 8 之类的技巧,反正最后输出的单位是毫摄氏度。sysfs 里返回给用户态的temp1_input就是毫摄氏度,比如读出 28375,代表 28.375°C。
用户态读取一条命令就够了:
cat /sys/class/hwmon/hwmon0/temp1_input如果你想更直观地看到三位小数,可以用 shell 简单换算:
awk '{printf "%.3f\n", $1/1000}' /sys/class/hwmon/hwmon0/temp1_input这里有一个细节容易忽略:芯片上电后默认就是连续转换模式,驱动不需要显式配置就会持续更新温度寄存器。所以读到的数值会是实时变化的,而不是某个固定初值。如果发现读数一动不动,就要怀疑芯片是不是被配置成了单次转换模式,或者硬件上根本没有正常工作。
3.3 配置寄存器与精度取舍
配置寄存器最简单也最实用。它控制转换分辨率、单次/连续转换模式、ALERT 引脚极性等。默认值通常是连续转换模式,分辨率 12 位。12 位模式下的转换时间会长一些,但也只是几百毫秒量级,对温度监控场景完全够用。如果你追求更快的转换周期,可以把分辨率降到 9 位,对应 LSB 变成 0.5°C,转换时间能缩短到几十毫秒,但精度也牺牲得多,一般不推荐。
我的建议是,开发调试阶段保持默认不碰配置寄存器,先把链路跑通。等温度读数正常了,再根据实际需要决定要不要改。比如某些产品对温度变化响应速度要求高,可以尝试降低分辨率;如果需要温度告警,可以配置高温阈值寄存器和迟滞寄存器,再配合 ALERT 输出引脚。这些东西在 lm75 驱动里不一定暴露了配置接口,改起来可能得在设备树或者驱动初始化里加参数,所以不要一上来就把配置寄存器玩坏,否则后面排查时干扰因素会变多。
4. 实操验证与调试记录
4.1 先确认驱动真的绑上了
移植过程中我习惯按四步确认,避免上来就 miss 一个环节。第一步,看总线上有没有这个地址的设备。用i2cdetect扫一下:
i2cdetect -y 2如果总线上确实有芯片,地址 0x48 那格会显示48。如果显示--,要么地址不对,要么芯片没供电,要么上拉电阻有问题,也可能是总线编号不对。第二步,看 sysfs 里有没有生成2-0048目录。第三步,看驱动链接是否指向 lm75。第四步,确认 hwmon 类目录存在:
cat /sys/class/hwmon/hwmon0/name输出应该是lm75。这一步能帮你确认你说的 hwmon0 到底是不是这颗芯片,因为系统里可能还有 CPU 温度传感器、板载其他传感器,它们也会注册 hwmon 节点。如果有多个,就逐个 cat name,找出标签是 lm75 的那个。
4.2 从用户态读温度并做连续监测
驱动绑定正常后,温度读取非常简单。先读一次:
cat /sys/class/hwmon/hwmon0/temp1_input比如输出 28687,说明当前温度 28.687°C。为了观察变化趋势,我经常用watch命令连续刷新:
watch -n 1 'cat /sys/class/hwmon/hwmon*/temp1_input'注意这里用了通配符,方便在不确定哪个 hwmon 是目标时一次看到所有温度传感器输出。实际调的时候,最好还是指定具体路径,避免看混了。如果有多个传感器节点,可以用cat /sys/class/hwmon/hwmon*/name和temp*组合判断。
顺手做个简单验证:把手指按到芯片封装上,或者用电烙铁靠近但不接触,温度读数应该逐渐上升;移开后慢慢回落到环境温度。如果读数变化太快或者几乎不变化,说明要么热耦合不好,要么芯片根本没在测量状态。
4.3 精度评估:与参考温度计做对比
系统集成阶段,光看到数据在变还不够,我还建议做一次简单精度验证。把 TMP75AIDR 和一个经过校准的参考温度计放到同一个恒温块里,或者至少放在热容量接近的同一块金属表面,等温度稳定后,同时记录两边的读数。记录个十几分钟,看差值是否在手册给出的精度范围内。
我一般会把采样脚本写成这样,便于落到 csv 里:
while true; do echo "$(date +%s) $(cat /sys/class/hwmon/hwmon0/temp1_input)" sleep 2 done再结合参考温度计的数据对比,做进一步修正。如果发现固定偏差,比如整体偏高 0.3°C,先别急着改代码,检查一下 PCB 布局是不是让传感器太靠近发热元器件,或者散热焊盘有没有连到大面积铜皮。很多时候误差的来源不是芯片本身,而是传感器所在位置的环境温度并非你想要测的对象。这个环节能省就省的话,后续产品测试时会花更多时间在解释"为什么温度不对"上。
5. 常见问题与排查技巧实录
5.1 驱动加载存在但设备节点找不到
这是最经常遇到的第一类问题。设备树里写了节点,modprobe lm75也提示加载成功,但/sys/bus/i2c/devices/下就是看不到2-0048。优先检查三件事。
第一,设备树是否真的编译进当前启动的 dtb 里了。有时候改的是源码,编译的却是另一个设备树文件,启动加载的文件还是旧版本。用dtc反编译后搜索节点就能确认。第二,I2C 控制器是否使能。很多 SoC 的控制器默认关闭,要在设备树里设置status = "okay"。第三,总线号是否对。不同平台 I2C 控制器编号可能从 0 开始也可能从 1 开始,你以为挂在 i2c2,实际硬件上芯片在 i2c3,设备树节点写在 i2c2 下面自然找不到。拿i2cdetect -l列出系统实际的总线列表,再对着原理图确认芯片的 SCL/SDA 接到了哪组引脚上。
5.2 地址设置错误与总线冲突
如果 i2cdetect 能看到芯片但设备树节点 reg 和实际地址不一致,驱动不会 probe。反过来如果 reg 等于某个地址,而那个地址上挂的其实是另一个设备,也会出乱子。常见的 I2C 地址冲突发生在板载 EEPROM、RTC 等设备身上,它们很多也占据 0x48 到 0x4F 这一段。
地址问题排查就是要会算。前面说过,1001 A2 A1 A0是地址格式,A0/A1/A2 的具体接法决定最终地址。举例来说,A0 接高电平、A1/A2 接地时,地址是 0x49;A2 接高电平时则是 0x4C。有些模块上芯片的地址引脚已经被固定,你可能只能改软件里的 reg 值。如果是自己画板,预留跳线最好,以后扩展多片传感器时还能灵活错开地址。总线上同时挂多颗 TMP75,可以分别把 A0/A1/A2 接不同电平,这样每颗地址不同,设备树里写多个不同 reg 的节点就行。
5.3 温度读数不动或乱跳的处理方向
读数一动不动,先确认是不是芯片处于单次转换模式。如果之前用i2cset把配置寄存器里的 SHOT 位置了 1,芯片就只转换一次,之后温度寄存器不再更新,必须重新触发转换。临时调试可以用 i2cset 改回去,但别乱改其他位。如果是全新上电就有问题,更可能是硬件虚焊、SDA/SCL 颠倒、供电异常,要拿示波器去看 I2C 通信波形。
读数乱跳的原因又不一样。最常见的是供电纹波大、I2C 总线上拉电阻阻值不合适、线缆太长。上拉电阻阻值太大会导致信号上升沿变缓,高速读取时数据错位;太小又会加重总线负载。一般 I2C 总线用 4.7kΩ 或 2.2kΩ 常见,具体要看总线上挂了多少设备。另外,如果传感器旁边有大电流器件切换,或者地平面不连续,也会在温度数据里看到毛刺。这种问题不是驱动能解决的,得回到硬件设计上处理。
5.4 用 i2c-tools 做绕过驱动的底层诊断
当 sysfs 读不到或读数明显异常时,i2c-tools 是很好的底层诊断工具。先拿i2cdetect -y -r 2确认设备地址,再用i2cdump看寄存器原始值:
i2cdump -y 2 0x48命令输出会按照字节显示寄存器内容。温度寄存器是 00h,配置寄存器是 01h。结合数据手册,对照配置寄存器当前值就能判断芯片是否处于连续转换模式。如果想单独读温度寄存器,i2cget也能做:
i2cget -y 2 0x48 0x00 w注意w会按 word 读取,不同芯片和不同工具版本对字节序的处理可能不同,所以看到的数据要和手册仔细对照,不要急着下结论。底层诊断的价值在于,它能帮你区分问题在硬件层面还是软件层面。只要地址能扫到、寄存器能读,那驱动加载不上大概率是设备树或内核配置问题;如果地址都扫不到,那就是硬件链路的问题,回头查供电、上拉、接线才是正道。
在调试过程中如果临时用i2cset改了配置寄存器,一定记得记录原始值,调完恢复默认。特别是 ALERT 相关位和比较器模式位,改坏了会影响后续行为,而这些问题往往隐藏得比较深,会浪费不少时间。
我自己做这类移植时,最深的体会是:其实内核已经替你把 90% 的读写逻辑做完了,真正花时间的通常不是驱动代码,而是设备树、地址、总线拓扑、供电这些外围因素。先把基础环境验证到"地址能扫到、寄存器能读"这一层,再考虑驱动绑定的事情。后续如果想扩展,比如同时挂多片 TMP75AIDR,或者把芯片换成同系列的其他型号,也只需要调整设备树里的节点和地址,基本不用改动内核驱动,这个路线还是可以长期复用的。