TL;DR:在 Raspberry Pi OS 的 6.18 内核上,使用 dtoverlay=pwm-ir-tx 的红外发射器不会发送任何可用的内容。一个 100 ms 脉冲、100 ms 间隔、100 ms 脉冲的测试码被施加到 PWM 上,表现为 7 微秒内的四次状态变化,全部来自一次定时器中断,因此 LED 总共只亮了几微秒。ir-ctl --send 仍然以 0 退出,并且仍然耗时约 350 ms,因为 LIRC 核心在返回前会等待信号的长度。因此,无论是退出码还是耗时都无法暴露问题。原因是 6.18 的一次清理,被描述为切换到“一个等效的更高层辅助函数”,它反转了驱动定时器回调中的循环条件。在同一引脚上切换到 dtoverlay=gpio-ir-tx 后,发送出了正确计时的 100 ms 脉冲串。该工具包的 check-ir-tx.sh 可以找到受影响的发射器并对其进行测量。上游报告:raspberrypi/linux#7678 。
Raspberry Pi 4B,Raspberry Pi OS(Debian 13,arm64),内核 6.18.34+rpt-rpi-v8 。在 /boot/firmware/config.txt 中添加了一行:
dtoverlay = pwm - ir - tx
重启后一切看起来都正常:
rc rc0: PWM IR Transmitter as /devices/platform/pwm-ir-transmitter/rc/rc0 rc rc0: lirc_dev: driver pwm-ir-tx registered at minor = 0, no receiver, raw IR transmitter $ pinctrl get 18 18: a5 pd | lo // GPIO18 = PWM0_0 $ ir-ctl -d /dev/lirc0 -f Send features /dev/lirc0: - Device can send raw IR - IR scancode encoder - Set carrier - Set duty cycle
一个 100 ms 开、100 ms 关、100 ms 开、38 kHz 的测试信号:
$ printf 'pulse 100000\nspace 100000\npulse 100000\n' > ir300.txt $ ir-ctl -d /dev/lirc0 --carrier = 38000 --send = ir300.txt send 1: rc = 0 elapsed_ms = 359 send 2: rc = 0 elapsed_ms = 345 send 3: rc = 0 elapsed_ms = 349
退出码 0,耗时约为信号长度,dmesg 中没有任何内容。内核的 pwm_apply 跟踪事件显示了 PWM 实际被要求做什么:
ir-ctl-1230 [002] d.h1. 42.847753: pwm_apply: pwmchip0.0: period=26316 duty_cycle=13158 polarity=0 enabled=1 err=0 ir-ctl-1230 [002] d.h1. 42.847758: pwm_apply: pwmchip0.0: period=26316 duty_cycle=13158 polarity=0 enabled=0 err=0 ir-ctl-1230 [002] d.h1. 42.847759: pwm_apply: pwmchip0.0: period=26316 duty_cycle=13158 polarity=0 enabled=1 err=0 ir-ctl-1230 [002] d.h1. 42.847760: pwm_apply: pwmchip0.0: period=26316 duty_cycle=13158 polarity=0 enabled=0 err=0
载波设置是正确的(26,316 ns 的周期是 38 kHz,占空比 50%)。时序不对:两个 100 ms 脉冲分别只亮了 5 µs 和 1 µs,整个码在 7 µs 内就结束了。d.h1. 中的 h 表示所有四次都是在硬中断上下文中应用的,来自同一个回调。重启后,同样的测试在 6 µs 内给出了相同的四个边沿。
上游的报告来自一台运行 32 位内核( 6.18.50+rpt-rpi-v7 )的 Pi 3B+,其 LED 一直不亮。这里是运行 64 位内核的 Pi 4B,因此它不限于某一型号或架构。
你通常会做的每一项检查都能通过。overlay 加载了,/dev/lirc0 以正确的特性出现,引脚处于 PWM 模式,并且 ir-ctl 返回 0。
时序也无济于事。人们很容易认为,一次传输若坍缩到微秒级,就会让 ir-ctl 立即返回,而测量它就能捕捉到问题。事实并非如此:无论驱动做了什么,LIRC 核心(lirc_dev.c 中的 lirc_transmit())都会休眠,直到信号的完整持续时间过去后,write 才返回。对于 300 ms 的信号,ir-ctl 耗时 345 到 359 ms,看起来完全正常。
而且红外是不可见的。除非你把手机摄像头对准 LED,否则唯一的症状就是电视或功放没有反应,这看起来像是代码错误、LED 太弱或角度不对。那些用了多年正常工作的红外发射器的人,会在更新内核后开始调试他们的代码。
如果你用长信号测试,还有一个陷阱:一个 600 ms 的测试代码(三个 200 ms 段)被直接拒绝,报错 failed to send: Invalid argument (exit 74),因为 LIRC 限制单次传输总时长为 500 ms(IR_MAX_DURATION)。这是一个诚实的错误,但它会让你去错误的地方查找。
pwm-ir-tx 有两种发送方式。如果 PWM 驱动可以从原子上下文调用,它就从 hrtimer 回调中编程每个边沿;否则它在边沿之间休眠。树莓派的 PWM 驱动是原子的,并且 dmesg 中没有显示 "TX will not be accurate as PWM device might sleep" 这一行,所以树莓派使用的是定时器路径。
定时器回调应用一个边沿,安排下一个到期时间,如果已经落后就循环,以在延迟尖峰后追赶。直到 6.17,循环条件一直是:
now = timer -> base -> get_time (); } while ( hrtimer_get_expires_tv64 ( timer ) < now );
也就是说,只要下一个到期时间已经过去就继续。提交 44d7fb8acdf8("media: pwm-ir-tx: Avoid direct access to hrtimer clockbase",首次出现在 v6.18)将其替换为其提交信息所称的 "一个等价的高层辅助函数":
} while ( hrtimer_expires_remaining ( timer ) > 0 );
hrtimer_expires_remaining() 是到期时间减去当前时间。当下一个到期时间在未来时,它是正值,而在添加下一个边沿的持续时间之后,这总是成立的。因此,过去只在落后时才运行的循环,现在只要准时就会运行:第一个回调依次应用每个边沿,完成传输并停止定时器。这正是上面的跟踪结果。同一行代码也存在于 rpi-6.18.y 和当前主线中。
在条件被恢复之前,在同一引脚上使用 bit-banged 驱动:
dtoverlay = gpio - ir - tx , gpio_pin = 18
而不是 dtoverlay=pwm-ir-tx,然后重启。同样的测试信号,通过 gpio_value 事件跟踪:
rc=0 elapsed_ms=331 burst length_ms=105.98 toggles=7604 burst length_ms=99.99 toggles=7600 gap between bursts ms=100.01
两个约 100 ms 的突发,相隔 100 ms,每个有 7,600 次翻转,即 100 ms 的 38 kHz。gpio-ir-tx 通过在 CPU 上忙等待来生成载波,因此在发送代码时会消耗 CPU 时间,其时序可能会受到其他负载的干扰。对于偶尔发送代码的红外发射器来说,与什么都不发送相比,这是一个公平的权衡。
要检查一台机器,check-ir-tx.sh 会列出红外发射器,在 6.18 或更高版本内核上标记 pwm-ir-tx,并在使用 --probe(以 root 身份)时发送一个 30 ms 的测试码,并测量其 PWM 边沿在 trace 中的分布。在这台带有 pwm-ir-tx 的 Pi 上,它报告在 7 µs 内有 4 个边沿。使用 gpio-ir-tx 时则通过。
当一项变更被描述为等价时,检查那个会使它不等价的关键点。这里的关键点是比较的方向:“过期时间早于现在”和“剩余时间为正”听起来像是同一个问题,实际上却是相反的。一次只交换了值的获取方式的重构,仍然可能翻转条件的含义,而提交信息和代码审查都把它读作无操作。
另一个习惯是关于什么才算作测试。退出码和持续时间都是软件对自身做出的声明。对于任何离开机器的内容——红外码、数据包、对磁盘的写入——都要测量实际发生了什么:一个 trace 事件、一台摄像头、另一端的抓包。
这是 Pi 的 6.18 内核第二次悄悄拿走了在 6.12 上还能工作的东西:旧版 iptables 模块不再被构建,而配置仍然声称它们已启用。
本文的修复方案已在工具包中作为经过测试、可直接运行的脚本提供。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:DEV.to
原文作者:The Homelab Postmortem;如有侵权,请联系管理员删除。