☰
Linux下USB转1-Wire读取多路DS18B20:内核w1与OWFS实战
2026/10/27 21:12:58 网站建设 项目流程

1. 为什么要在 Linux 上用 USB 转 1-Wire,而不是 GPIO

先把场景摆出来:一套机柜环境里要挂十几颗 DS18B20 测温度,主控是一台工控机或者一台装了 Linux 的小主机。打开机箱一看,裸露的 GPIO 引脚要么根本没有,要么已经被别的 SPI、I2C 外设占满,要么就是被机箱结构挡得死死的,飞线出去还要考虑走线长度、干扰、绝缘,工程上非常不体面。这种情况下,DS9490R 这类 USB 转 1-Wire 适配器就成了最省事的选择——插上就是一个标准 USB 设备,Linux 内核或者用户态工具直接接管,前端那颗 DS18B20 挂在后面,跟主机内部的引脚资源彻底解耦。这篇内容就是围绕“Linux 环境下用 DS9490R 读取多个 DS18B20”这一条链路,把选型逻辑、硬件物理层的取舍、内核方案与 OWFS 方案的实操差异、以及我踩过的几个典型坑,完整拆一遍。

先说清楚这套东西能干什么:DS9490R 是一个 USB 接口的 1-Wire 总线主机适配器,它把主机侧的 USB 协议翻译成 1-Wire 的时序信号,总线上可以挂一颗也可以挂几十颗 1-Wire 从设备;DS18B20 是其中最常见的数字温度传感器,每颗出厂带一个 64 位全球唯一 ROM ID,所以“多个 DS18B20 挂在同一根总线上”这件事在协议层是完全没问题的,问题是出在物理层和软件层的调度上。适合读这篇的人大致有三类:一是第一次接触 1-Wire、拿到适配器不知道从哪下手的;二是已经能读出单颗、但一挂上多颗就开始丢设备或者读出 85.0 的;三是打算把这套东西做成长期采集、需要评估轮询周期和总线负载的。

1.1 三种 1-Wire 主机方案的现实取舍

Linux 下让主机跟 1-Wire 总线对话,常见有三条路,各自适用的场景完全不一样,先做个横向对照,后面所有实操的取舍都建立在这张表上。

方案典型形态优点明显的短板
GPIO 位操作主机引脚 + 上拉电阻 + 自己写时序成本极低,引脚够用时不花钱时序对内核调度极其敏感,非实时内核下容易偶发失败;每台机器的引脚情况不同,代码不可移植
专用 1-Wire 主机芯片I2C 或 SPI 接口的桥接芯片时序由硬件保证,稳定需要额外占用一路 I2C/SPI,还要处理地址冲突
USB 转 1-Wire 适配器DS9490R 这类成品即插即用、可热插拔、跨机器可移植依赖 USB 子系统,驱动加载顺序有讲究,批量读取存在固有延迟

我个人的结论很直接:只要不是大批量量产的板卡级产品,只是做测试、做样机、做小规模部署,USB 适配器方案的综合成本最低。GPIO 位操作看起来最“原生”,实际上是最容易在半夜出问题的方案——你没法保证内核在拉低总线的那几百微秒里不切走当前进程,一旦切走,时序就废了,从设备返回的就是 CRC 错误。这不是代码写得不够好,而是通用 Linux 内核本身就不是为微秒级硬时序设计的。

1.2 内核 w1 栈与用户态 OWFS 的本质差异

USB 适配器插上之后,软件层有两条完全不同的路线,很多人一开始会在这里纠结,其实搞明白它们的本质区别就不纠结了。

第一条是内核 w1 子系统。Linux 内核里有一整套 1-Wire 框架,包含总线核心、主机控制器驱动和从设备驱动三层。DS9490R 对应的是主机控制器驱动,DS18B20 对应的是从设备驱动(温度传感器驱动)。这套东西跑通之后,你会在sysfs里看到一颗颗设备目录,读一个普通文本文件就能拿到温度值。它的优点是零依赖、零用户态进程、开机即用;缺点也很明显,内核把“发起温度转换”和“读取转换结果”绑成了一个原子操作,你每读一次文件,它就重新转换一次,多个设备只能串行排队。

第二条是OWFS(1-Wire File System)。这是一个用户态的文件系统实现,通过 FUSE 把 1-Wire 总线挂载成一个目录树,每颗设备是一个子目录,里面的temperature、temphigh、resolution都是普通文件。它最大的价值在于把 1-Wire 协议的全部细节暴露出来了,包括“同时转换”这种内核没给你的能力。

这两条路线在物理上是互斥的:同一根总线同一时刻只能被一个主机控制器驱动占用,所以不能既让内核 w1 栈接管,又让 OWFS 去抢。下面两套实操我会分开讲,实际用的时候选一条走到底。

2. 总线物理层:多个 DS18B20 挂一起时最先崩的地方

很多人调试多设备失败,第一反应是驱动装错了、代码写错了,其实十个里面有六七个问题出在物理层。1-Wire 叫“单总线”,名字容易让人误会成“随便拉一根线连起来就行”,实际上它对总线电容、上拉强度、拓扑形状相当敏感。单颗设备的时候你怎么接都能读出来,一挂到五六颗以上,各种玄学问题就全冒出来了。

2.1 星形拓扑为什么会让总线上所有设备一起失联

1-Wire 的电气模型可以理解成一颗电容在充放电。总线上的每一段线缆、每一个器件的引脚,都会贡献几十 pF 的寄生电容,这些电容并联在一起,总量直接决定信号上升沿的陡峭程度。总线主机是靠“拉低”来发逻辑 0,靠“释放让上拉电阻拉高”来发逻辑 1,所以上升沿的快慢完全取决于上拉电阻给这堆电容充电的速度。

星形拓扑(所有设备从一个中心点各自拉一条线出去)之所以是灾难,是因为它把总线的电容集中在一个节点上,同时还引入了多段线缆的阻抗不连续,反射会让本来就不陡的上升沿变得更烂。表现出来就是:挂两颗能读,挂五颗开始随机丢,挂八颗直接一颗都搜不到。

我实测下来比较靠谱的做法是主干加短分支:一根主线从适配器出发,所有 DS18B20 沿着这条主线依次挂接,每个分支从主线到器件的距离控制在几十厘米以内。如果实在做不到纯菊花链,退而求其次用“短主干 + 集中分支”,把分支长度压到最短。另外主线尽量用双绞线,信号线和地线绞在一起,对抗共模干扰的效果比散线好太多。

2.2 上拉电阻、线长和供电方式的组合选择

上拉电阻的经典值是 4.7kΩ,这个值在短距离、少设备的情况下基本通吃。但它是有一个隐含前提的:总线电容在可接受范围内。线拉长到十几米、设备挂到十几颗之后,4.7kΩ 就明显偏大了,上升沿变缓,从设备采样出错。这时候可以考虑降到 2.2kΩ 甚至 1kΩ,但代价是静态电流上升,主机侧的驱动能力要跟得上。

供电方式的选择比上拉电阻更关键。DS18B20 有两种供电接法:

  • 寄生供电:VDD 引脚直接接地,器件从数据线的空闲高电平期间“偷电”存到内部电容里。优点是省一根线,两线制走线极其省事;缺点是温度转换期间电流需求会突然拉高,单靠 4.7kΩ 上拉供不上,必须依赖主机在转换期间切换到强上拉。
  • 外部供电:VDD 单独接 3.3V 或 5V,三线制。多花一根线,换来的是转换期间供电稳定、读数可靠、不依赖主机的强上拉能力。

多设备场景下我强烈建议走外部供电三线制。寄生供电在单颗设备演示时很优雅,但一旦总线上有七八颗设备同时进入转换状态,那点储能电容根本扛不住,结果就是部分设备返回默认值 85.0,或者干脆转换没完成、CRC 校验直接挂掉。这个坑我在早期项目里踩过不止一次,每次都是“明明代码没改,怎么换个接法就好了”,后来干脆把三线制写进了规范。

2.3 DS9490R 的强上拉到底该不该开

DS9490R 内部的主控芯片是支持在温度转换期间开启强上拉的,这个能力在内核 w1 子系统里是可以通过 sysfs 控制的。在总线主机的目录下(通常是/sys/bus/w1/devices/w1_bus_master1/)可以看到一个w1_master_pullup属性,往里写 1 就开启,写 0 就关闭。

# 查看当前强上拉状态 cat /sys/bus/w1/devices/w1_bus_master1/w1_master_pullup # 开启强上拉(寄生供电时必须) echo 1 | sudo tee /sys/bus/w1/devices/w1_bus_master1/w1_master_pullup

要不要开?判断标准很简单:走寄生供电就必须开,走外部供电就关掉。外部供电时开强上拉没有任何收益,反而可能因为主机强行把总线拉到更高的驱动能力,在长线上引入额外的反射。

注意:不是所有 1-Wire 主机控制器驱动都实现了w1_master_pullup这个属性。如果看到目录里没这个文件,说明该驱动不支持软件控制强上拉,此时寄生供电方案就得慎重。

3. 内核路线走通:从 lsusb 到 w1_slave 的完整链路

内核方案是我在正式部署里首选的一条,原因很实际:不依赖任何用户态守护进程,重启之后自动就绪,没有 FUSE 那一层的额外开销,读一个文件就能拿到结果,脚本写起来也简单。下面按实际执行顺序走一遍。

3.1 USB 识别与模块加载的先后顺序

第一步永远是确认 USB 层有没有认到这个设备。DS9490R 的 USB 厂商号和产品号组合是固定的,用lsusb就能过滤出来:

lsusb | grep -i 04fa # 期望输出类似: # Bus 001 Device 005: ID 04fa:2490 ...

这里有一个特别常见的翻车点:市面上有一部分标着同样型号的适配器,内部根本不是原厂主控,而是换了一颗 USB 转串口的芯片,厂商号产品号完全是另一套。这种情况下,内核的专用驱动根本不会去绑定它,lsusb能看到设备但w1目录永远空着。所以第一步的lsusb输出必须核对清楚,认错了后面全都白搭。

确认设备在了,接着加载内核模块。这里要注意模块之间的依赖顺序:

sudo modprobe wire sudo modprobe w1_ds2490 sudo modprobe w1_therm

三个模块的作用分别是:wire是 1-Wire 总线核心(老内核里模块名可能就是w1),w1_ds2490是 USB 适配器的主机控制器驱动,w1_therm是温度传感器系列的从设备驱动。顺序上先核心后主机再从设备,一般不会有问题,但如果你只modprobe w1_ds2490,依赖会被自动拉起来,反而是把顺序搞反了手动加载才容易失败。

在新版本内核上,这三个模块名是这套;在比较老的发行版上,主机驱动可能叫ds2490,从设备驱动可能叫w1_therm,找不到报错的话先modprobe -l | grep w1(或者find /lib/modules -name "*w1*")看一眼实际名字。另外,如果发行版用的是精简内核,这几个模块可能压根没编译进去,需要确认内核配置里CONFIG_W1、CONFIG_W1_MASTER_DS2490、CONFIG_W1_SLAVE_THERM这三项是打开状态,有可能是m也有可能是y。

模块加载成功之后,设备节点就出现了:

ls /sys/bus/w1/devices/ # 28-00000a1b2c3d 28-00000a1b2c4e w1_bus_master1

看到28-开头的目录,说明总线搜索已经完成,并且成功识别到了温度传感器。这个28是 DS18B20 的家族码,后面的十二位十六进制是它 48 位序列号部分,最后八位 CRC 被内核校验过、没有拼进目录名。如果是 DS18S20 或者更老的型号,前缀可能是10-。

3.2 w1_slave 两行文本的逐字段拆解

进到某个设备目录里,最核心的文件叫w1_slave,直接cat出来是两行文本:

cat /sys/bus/w1/devices/28-00000a1b2c3d/w1_slave

典型的正常输出长这样:

50 01 4b 46 7f ff 0c 10 57 : crc=57 YES 50 01 4b 46 7f ff 0c 10 57 t=21062

第一行前半段是九个字节的十六进制值,这是 DS18B20 内部暂存器(scratchpad)的原始内容,前两个字节就是温度寄存器,后面依次是高温阈值、低温阈值、配置寄存器和保留字节,最后一个字节是 CRC。冒号后面crc=57 YES表示内核算出来的 CRC 和器件返回的一致,数据可信;如果显示NO,那这九个数一个都不能用。

第二行前半段是同样的九个字节,末尾的t=21062才是我们要的东西,单位是毫摄氏度,也就是 21.062°C。除以 1000 就是摄氏度。

这里有个必须知道的事实:第二行有时候会缺失t=部分。当温度转换还没完成、或者总线通信在转换后读暂存器时失败,第二行就只有那串十六进制,没有温度值。很多脚本只读第二行不判断,结果就是解析异常或者读到一个空值。稳妥的解析逻辑必须同时检查两件事:第一行结尾是YES,第二行包含t=。

t=后面的值也不是随便什么数都可信。DS18B20 上电默认暂存器里的温度寄存器是 0x0550,换算下来正好是85.0°C,所以只要读到t=85000,基本可以判定是转换没完成或者供电不足导致的复位,不是真实温度。这个值在冷链、机房这类真实温度不可能到 85 度的场景里就是明确的错误信号。

3.3 用 ROM ID 建立设备身份映射表

多设备场景里最容易出问题的是“设备身份漂移”——你昨天记录的是 A 点 21 度,今天换了个接线顺序,同一个 ROM ID 对应的物理位置全变了。所以工程上必须做一件事:给每个 ROM ID 建立到物理位置的固定映射。

做法不复杂,第一次部署时逐个确认,把 ROM ID 和标签对应关系记录下来,可以是 CSV,也可以是配置文件:

rom_id,location,note 28-00000a1b2c3d,机柜A-上层-左侧,靠近进风口 28-00000a1b2c4e,机柜A-上层-右侧,靠近电源模块 28-00000a1b2c5f,机柜A-下层-中部,变压器上方

建立映射的方法:只接一颗设备,读出 ROM ID,贴标签;断开这一颗接下一颗,重复。听起来笨,但这是唯一可靠的方式。如果嫌麻烦,也可以全部接好之后用热风或者手指依次加热每颗传感器,观察哪个读数在动,反推物理位置,这个方法在设备数量多的时候反而更快,只是准确度靠眼力。

这张表建立之后,后续所有脚本和图表都基于它来渲染,ROM ID 变成主键,位置信息只是附加描述。设备坏了换新的时候,只需要在表里改一行 ROM ID,历史数据的映射关系不受影响。

4. OWFS 路线:批量读取与并发转换的性能解法

内核方案好用,但它有一个绕不过去的硬伤:每读一颗设备,都要触发一次完整的温度转换。DS18B20 在 12 位分辨率下的单次转换时间是 750ms,读十颗设备就是七秒半,读二十颗就是十五秒。对于采样频率要求不高、只是偶尔看一眼的场景还凑合,但如果要做秒级刷新,这条路直接走不通。

OWFS 就是为了解决这个问题而存在的。

4.1 owfs 挂载与 owserver 分离的部署方式

OWFS 的本质是一个把 1-Wire 设备树映射成目录的用户态文件系统。最简的用法是一步挂载:

sudo apt install owfs owfs-fuse sudo mkdir -p /mnt/1wire sudo owfs -u --allow_other /mnt/1wire

-u表示使用 USB 适配器。挂载成功之后ls /mnt/1wire就能看到设备目录,目录名是把 ROM ID 里的横杠换成点,比如28.00000A1B2C3D。进到目录里,temperature文件就是温度值:

cat /mnt/1wire/28.00000A1B2C3D/temperature

除了temperature,还有一批在调试时非常有用的文件:

文件名内容用途
temperature当前温度,摄氏度浮点日常读取
temphigh/templow高低温报警阈值配置报警
resolution当前分辨率位数改精度、改转换时间
power供电方式标识排查寄生/外部供电
crc8上一次通信的 CRC 状态判断通信质量
family家族码区分不同型号

生产环境里我更推荐把 owserver 和 owfs 分开部署。owserver 负责跟硬件打交道,owfs 只负责挂载文件系统跟 owserver 通信,两者可以跑在不同进程甚至不同机器上:

# 后台起 owserver,监听 4304 端口 owserver -u -p 4304 --foreground & # 挂载文件系统,指向 owserver owfs -s localhost:4304 /mnt/1wire

这么拆的好处是设备访问集中在一个进程里,多个客户端(比如一个采集脚本、一个监控看板、一个手工调试的终端)可以同时挂着,不会出现谁把总线抢了谁就用不了的情况。这个结构在后面做长期采集的时候尤其重要。

4.2 simultaneous 分支解决串行 750ms 的等待问题

OWFS 最有价值的一个特性藏在挂载点下的simultaneous目录里。它的作用是把“触发转换”和“读取结果”这两个动作拆开,并且让总线上所有设备同时转换。

1-Wire 协议本身是支持这个的:主机发一个 Skip ROM 命令(跳过 ROM 寻址,对所有设备广播),紧接着发 Convert T 命令,总线上所有 DS18B20 就会在同一时刻开始转换。等 750ms 之后,主机再逐颗寻址读取结果。这样总耗时就是 750ms 加上一小段读结果的时间,跟设备数量基本无关。

具体用法:

# 触发所有设备同步转换(读这个文件本身就会触发) cat /mnt/1wire/simultaneous/temperature # 转换完成后逐个读结果 for d in /mnt/1wire/28.*/; do echo "$d $(cat $d/temperature)" done

第一次读simultaneous/temperature的时候,OWFS 会阻塞到所有设备转换完成,返回的就是一批温度值。这个过程对十颗设备和对三十颗设备的耗时差别很小,这是内核方案完全给不了的能力。

我做过一个粗略对比,同样是十二颗 DS18B20、12 位分辨率:内核方案逐个cat w1_slave一轮下来,实测在 9.5 到 10 秒之间,基本就是 12 乘以 750ms 再叠加各种开销;OWFS 走 simultaneous,一轮在 1 秒出头。差了将近十倍。

4.3 owfs 与内核对同一根总线的争用

这里必须强调一个非常容易忽略的约束:内核 w1 栈和 OWFS 不能同时使用同一根总线。

原因很直白,USB 适配器在操作系统里只有一个设备节点,谁打开它谁就独占。内核的w1_ds2490驱动加载之后会主动去绑定这个 USB 设备,这时候 OWFS 再去打开就会失败,报的错误通常是找不到适配器或者设备忙。反过来也一样,OWFS 占着的时候,/sys/bus/w1/devices/下可能会残留目录但读不出数据。

如果你确实要从内核方案切到 OWFS,操作顺序是:

# 先卸载内核驱动释放 USB 设备 sudo modprobe -r w1_ds2490 sudo modprobe -r w1_therm # 再启动 owserver owserver -u -p 4304 --foreground &

想永久避免这个冲突,可以在/etc/modprobe.d/里加一条黑名单,让内核永远不自动加载这个驱动:

blacklist w1_ds2490

建议在装机阶段就把这个决定做掉,选一条路线走到底,别两套混着用。混用的结果是每次重启之后行为都不确定,排查起来非常痛苦。

5. 排错链路实录:一个都读不到,到读到但全是 85.0

这一节按我实际排错的顺序走,从最外层到最里层,一层一层往下剥。你遇到问题时也可以照这个顺序来,不要跳步。

5.1 第一层:USB 枚举与驱动绑定

症状:/sys/bus/w1/devices/下只有w1_bus_master1,一个28-目录都没有。

先看 USB 层:

lsusb -d 04fa:2490 -v

如果这条命令没有输出,说明设备根本没被识别。拆开来看是几个可能:

  1. 线材或者接口问题。换个 USB 口、换根线,尤其是那些只能充电不能传数据的老线,这个坑极其普遍。
  2. 厂商号产品号不对。前面说过,有些仿制品用的是别的芯片,lsusb能看到但 VID/PID 完全不一样。这时候需要去查它实际用的是什么芯片,有可能需要装完全不同的驱动。
  3. USB 供电不足。部分主机的 USB 口在挂了多个外设之后供电能力下降,适配器起来了但工作不稳定。可以插到一个带独立供电的 USB HUB 上试试。

设备识别到了,但没有出现28-目录,下一步看驱动有没有绑定上:

lsmod | grep -E "w1|wire" dmesg | tail -30

dmesg里通常会有驱动的绑定信息,如果看到类似“new USB device found”之后没有跟着 w1 相关的日志,说明驱动没接管。这时候检查模块是否加载、内核配置是否包含对应驱动。

在驱动正确加载、但从设备搜不到的情况下,还要检查一件事:w1_master_slave_count的值。

cat /sys/bus/w1/devices/w1_bus_master1/w1_master_slave_count

如果这个值是 0,说明主机发起了搜索但总线上没有任何设备应答,问题在物理层;如果这个值大于 0 但ls看不到对应目录,那是命名或者权限的问题。

5.2 第二层:总线搜索超时与设备数不匹配

症状:接了十颗设备,w1_master_slave_count显示 6 或者 7,剩下几颗时有时无,每次重启数量还不一样。

这是典型的物理层问题,而且是最难缠的一类。

首先确认拓扑。把所有设备从总线上拔下来,只留一颗,看能不能稳定读到。一颗稳定说明主机和驱动都没问题,问题出在“多设备共存”。

然后按这个顺序处理:

  • 上拉电阻改小。从 4.7kΩ 换成 2.2kΩ 试一轮,如果设备数变多了,说明就是上升沿不够陡,直接确定病因。
  • 缩短分支长度。星形改主干加短分支,这一步往往比换电阻更有效。
  • 总线两端加终端。超长线(十几米以上)的时候,在总线末端加一个合适的终端电阻可以压住反射。
  • 换线材。双绞线替换普通排线,注意信号线和地线要成对。

还有一个隐蔽的问题:设备本身的引脚虚焊。DS18B20 常见的是 TO-92 封装,三根细引脚很容易在焊接时虚接,表现就是接触不良、时通时断。拿万用表逐个量一下 VDD-GND 之间的阻值,以及在总线空闲时测数据线对地电压,正常应该接近上拉电压。

w1_master_timeout这个属性也值得关注,它控制总线搜索的周期(单位是秒)。如果设备数量多、总线较长,搜索一次本身就要花时间,把周期设得太大反而会让新接入的设备迟迟不被发现。

5.3 第三层:CRC 失败与 85.0 恒温值的成因

症状:设备都识别到了,但读出来的温度里有几个是 85.0,或者w1_slave第一行显示crc=xx NO。

这两个现象背后往往是同一个根因,但排查角度不同。

crc=NO说明通信过程中有比特位翻转,属于电气层问题。按前面 5.2 的那套流程再走一遍,重点是上拉强度和线缆质量。另外,如果总线旁边有大功率开关电源、变频器这类干扰源,走线一定要远离,必要时给数据线套磁环。

85.0则有两种可能。一种是上面说的上电默认值,意味着转换根本没有成功执行,或者执行到一半器件被复位了。另一种是真实温度确实接近 85 度,但在常规场景里几乎不可能,所以默认按错误处理。判断方法是看power文件(OWFS 下)或者观察是否每次都是同样的 85.0——如果每次都是,那就是供电不足导致的转换失败,改三线制外部供电基本能解。

还有一种情况是分辨率设置导致的读取时序问题。DS18B20 支持 9 到 12 位四种分辨率,对应转换时间从 93.75ms 到 750ms 不等。如果代码里按 750ms 去等,但器件被配置成了 12 位以外的分辨率,或者反过来,等的时间不够就直接读,就会拿到上一次转换的结果或者默认值。OWFS 下可以直接改:

echo 11 > /mnt/1wire/28.00000A1B2C3D/resolution

把分辨率定下来之后再调等待时间,这个问题就消失了。内核方案里没有直接改分辨率的接口,需要自己发命令写暂存器配置字节,比较麻烦,这也是我后来批量场景全转 OWFS 的原因之一。

6. 长期采集的工程化收尾:脚本、服务与总线负载控制

调通之后,真正的工作才开始。一次性读出来和连续跑三个月不出错,中间隔着一整套工程细节。

6.1 一份能跑几个月的 Python 采集脚本

基于内核 sysfs 的采集脚本,核心逻辑要处理好三件事:CRC 校验、t=缺失、异常隔离。读一颗设备失败了不能让整轮采集都崩掉。

#!/usr/bin/env python3 import glob import os import time import json import logging BASE = "/sys/bus/w1/devices" log = logging.getLogger("w1") logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") def read_one(dev_dir): """读一颗 DS18B20,失败返回 None""" path = os.path.join(dev_dir, "w1_slave") try: with open(path, "r") as f: text = f.read() except OSError as e: log.warning("read %s failed: %s", path, e) return None lines = text.strip().split("\n") if len(lines) < 2: return None if not lines[0].rstrip().endswith("YES"): log.warning("%s crc failed", dev_dir) return None if "t=" not in lines[1]: log.warning("%s conversion not ready", dev_dir) return None milli = int(lines[1].split("t=")[1]) if milli == 85000: log.warning("%s returned power-on default 85.0", dev_dir) return None return milli / 1000.0 def collect(): result = {} for dev_dir in sorted(glob.glob(os.path.join(BASE, "28-*"))): rom = os.path.basename(dev_dir) t = read_one(dev_dir) if t is not None: result[rom] = t return result if __name__ == "__main__": while True: data = collect() print(json.dumps({"ts": int(time.time()), "data": data})) time.sleep(10)

几个细节值得单独说。sorted()保证输出顺序稳定,方便后续比对。每颗设备单独 try,一颗坏了不影响其他。判 85.0 那一条看起来多余,实际上拦住了一大批“数据看着有但其实是假的”的情况。真实使用时把print换成写数据库或者打日志,加个重试计数会更稳。

如果是走 OWFS 路线,脚本要简单很多:

import glob def collect_owfs(mount="/mnt/1wire"): # 先触发同步转换 try: open(f"{mount}/simultaneous/temperature", "r").read() except OSError: pass out = {} for d in glob.glob(f"{mount}/28.*"): rom = d.rsplit("/", 1)[-1].replace(".", "-") try: with open(f"{d}/temperature") as f: v = float(f.read().strip()) except (OSError, ValueError): continue if v == 85.0: continue out[rom] = v return out

6.2 drop-in 服务单元与日志轮转

脚本调通之后要做成服务,否则终端一关就停。用 systemd 的 drop-in 方式,把服务单元写到/etc/systemd/system/w1-collector.service:

[Unit] Description=1-Wire temperature collector After=network.target [Service] Type=simple ExecStart=/usr/local/bin/w1_collect.py Restart=always RestartSec=5 StandardOutput=journal StandardError=journal User=root [Install] WantedBy=multi-user.target

Restart=always配上RestartSec=5,保证脚本因为偶发的 USB 异常挂掉之后能自动拉起来。日志走 journald,用journalctl -u w1-collector -f看,避免日志文件无限增长的问题。

对于内核方案,还要额外处理一个开机顺序问题。服务单元里加了After=network.target不代表 USB 设备就一定就绪了,保险起见可以加一个轻量的启动前检查,或者在脚本开头加一段等待循环,轮询/sys/bus/w1/devices/w1_bus_master1是否存在,超时再退出让 systemd 重启。

6.3 轮询周期怎么定才不把总线压垮

这个问题我被问过很多次,没有一个通用答案,但有明确的判断依据。

先算清楚单轮采集的固有耗时。内核方案下,每颗设备一次完整读就是一次转换加一次暂存器读取,12 位分辨率下 750ms 是硬的,改不了。十颗设备就是 7.5 秒起步,加上进程调度和文件 IO,实际 8 到 10 秒。所以内核方案下轮询周期不可能低于 10 秒,写 1 秒只是让脚本空转。

OWFS 的 simultaneous 方案下,一轮在 1 秒左右,理论上可以把周期压到 2 秒。但我不建议这么激进。1-Wire 总线本身没有冲突仲裁机制,所有通信是主机完全主导的串行过程,频率压得太高会让单次通信的容错窗口变小,偶发的 CRC 失败率上升。对于温度这种本身变化极慢的物理量,10 到 30 秒的采样周期完全够用,没必要为了“看起来实时”牺牲稳定性。

还有一个容易被忽略的点:不要让多个客户端同时轮询同一根总线。前面提到过 owserver 分离部署的好处,但如果两个采集脚本各自独立去读 sysfs 或者各自起一个 owfs 实例,总线上的通信会被打乱,表现是两边都偶尔读到错误值。所有读取请求应该收敛到同一个进程,其他消费者通过它对外暴露的接口拿数据。

经验:DS18B20 的热时间常数本身就有一分钟左右,采样快到秒级并不会让你更早发现温度变化,只会让日志变多、总线更累。把周期定在 15 秒,既留足了余量,也足够覆盖绝大多数监控场景。

最后补充一个长期运行的观察。我见过几次“跑了两个月之后突然开始频繁丢设备”的案例,最后查下来都是物理层的老化:杜邦线插针氧化、焊点开裂、USB 接口松动。这类问题软件层完全看不出来,只能靠定期巡检。如果你的部署环境振动大或者温差大,建议把接线方式从插接改成压接或者焊接,能省掉很多半夜被叫起来的麻烦。

写到这里,从选型、物理层、内核方案、OWFS 方案到排错和工程化,整条链路基本走完了。这套东西的门槛其实不在代码,而在对物理层的敬畏——总线上那点电平和时序,比任何一行脚本都更能决定你半夜睡不睡得着。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询