红外遥控是最常见的无线控制方式之一,电视、空调、机顶盒的遥控器都依赖它。如果我们在树莓派或者安装了红外接收头的服务器上跑PHP项目,想把遥控器的按键读进来触发一些动作,比如控制网页翻页、切换灯光状态,就绕不开红外信号的捕获和解码问题。红外信号本质上是一串有规律的电平脉冲,硬件只负责把38kHz载波解调成高低电平,软件层面需要测量这些电平持续的时间,才能还原出遥控器发出来的原始数据。下面详细介绍具体的实现思路和代码。

一、红外信号的基本原理与NEC协议格式
红外接收头(如VS1838B、HS0038B)内部集成了光电探测、放大、滤波和解调电路,它输出的信号与遥控器发出的信号是反相关系:遥控器发射时,接收头输出低电平;没有信号时输出高电平。因此软件读到的数据是一串高低电平交替的脉冲序列。
目前家用遥控器最常见的是NEC协议,它的帧结构比较简单:先是一段9ms的低电平加4.5ms的高电平作为引导码,随后跟32位数据。数据位用不同的脉冲宽度区分,逻辑0是560微秒低电平加560微秒高电平,逻辑1是560微秒低电平加1.68毫秒高电平。32位数据由16位用户码、8位键码和8位键码反码组成,反码的作用是校验数据完整性。
当按键持续按下时,NEC协议每110毫秒发送一次重复码,波形是9ms低电平加2.25ms高电平加560微秒低电平。程序在解码时必须把重复码与正常数据帧区分开,否则长按操作会被误判成连续的独立按键。
二、PHP直接读取GPIO电平并解码
如果使用树莓派,PHP可以通过sysfs接口直接读取GPIO电平。虽然PHP不是实时性最好的语言,测量微秒级脉冲有一定误差,但通过多次采样取平均值,解码NEC协议这种毫秒级信号是可行的。PHP提供了system()、exec()和file_get_contents()等函数,可以结合底层工具完成测量。
下面是一段PHP示例代码,通过调用gpio命令或者直接读写sysfs来轮询电平,并用microtime(true)记录跳变时刻,把脉冲宽度序列转成二进制数据:
<?php
// 红外接收头数据脚接树莓派GPIO17,即sysfs编号17
$gpioPin = 17;
// 初始化GPIO为输入模式
@file_put_contents("/sys/class/gpio/export", $gpioPin);
usleep(100000);
@file_put_contents("/sys/class/gpio/gpio{$gpioPin}/direction", "in");
/**
* 读取当前电平
*/
function readLevel($pin) {
return trim(file_get_contents("/sys/class/gpio/gpio{$pin}/value"));
}
/**
* 等待引导码:9ms低电平 + 4.5ms高电平
* 返回true表示捕获到一帧数据,false表示超时
*/
function waitLeader($pin, $timeoutSec = 2) {
$start = microtime(true);
while ((microtime(true) - $start) < $timeoutSec) {
if (readLevel($pin) == '0') {
$t = microtime(true);
// 测量低电平持续时间
while (readLevel($pin) == '0') {
if (microtime(true) - $t > 0.02) return false;
}
$low = microtime(true) - $t;
// 低电平约9ms则可能是引导码
if ($low > 0.007 && $low < 0.012) {
return true;
}
}
usleep(50);
}
return false;
}
/**
* 读取32位数据,根据高电平宽度判断0和1
*/
function readFrame($pin) {
$bits = '';
for ($i = 0; $i < 32; $i++) {
// 等待低电平结束
while (readLevel($pin) == '0') {}
$t = microtime(true);
// 测量高电平宽度
while (readLevel($pin) == '1') {}
$high = microtime(true) - $t;
// 高电平大于1ms判定为逻辑1,否则为逻辑0
$bits .= ($high > 0.0011) ? '1' : '0';
}
return $bits;
}
// 主循环:持续监听红外信号
while (true) {
if (waitLeader($gpioPin)) {
$bits = readFrame($gpioPin);
if (strlen($bits) == 32) {
$userCode = bindec(substr($bits, 0, 16));
$keyCode = bindec(substr($bits, 16, 8));
$keyInv = bindec(substr($bits, 24, 8));
// 校验键码与反码
if ((($keyCode ^ $keyInv) & 0xFF) == 0xFF) {
printf("用户码: %04X 键码: %02X\n", $userCode, $keyCode);
}
}
}
usleep(1000);
}
这段代码的核心思路是利用microtime(true)获取浮点时间戳,两次读取之间的差值就是脉冲宽度。需要注意的是,PHP的解释执行特性会带来几十微秒的抖动,所以判断阈值要留出足够的容差区间,比如逻辑1的高电平实际是1.68ms,阈值设为1.1ms,逻辑0是0.56ms,两者之间有明显间隔,一般不会误判。
直接读取方式的好处是不依赖额外软件,代码可控;缺点是对系统负载敏感,如果PHP进程被调度打断,可能丢失一帧数据。因此建议在空闲的系统上运行,并在解码失败时静默丢弃该帧,等待下一次按键重试。
三、使用LIRC解码,PHP负责业务逻辑
更稳妥的方案是使用LIRC(Linux Infrared Remote Control)。LIRC是Linux下成熟的红外处理框架,支持内核级精确计时,解码工作全部由C代码完成,PHP只需读取解析结果。树莓派上可以通过apt-get install lirc安装,并在配置文件中把GPIO17指定为输入引脚。
安装配置好LIRC后,先用irrecord工具录制遥控器的编码,生成配置文件。之后LIRC会以守护进程方式运行,通过/var/run/lirc/lircd这个unix socket对外输出按键事件。PHP可以使用stream_socket_client连接这个socket,实时读取按键名称:
<?php
// 连接LIRC的unix socket
$sock = stream_socket_client("unix:///var/run/lirc/lircd", $errno, $errstr);
if (!$sock) {
die("连接LIRC失败: $errstr\n");
}
// 把键码映射成业务动作
$actionMap = [
'KEY_UP' => 'pagePrev',
'KEY_DOWN' => 'pageNext',
'KEY_OK' => 'togglePlay',
'KEY_POWER' => 'shutdown',
];
echo "等待红外按键...\n";
while (true) {
$line = fgets($sock);
if ($line === false) {
// socket断开,稍后重连
sleep(1);
$sock = stream_socket_client("unix:///var/run/lirc/lircd", $e2, $s2);
continue;
}
// LIRC输出格式: 按键码 重复次数 遥控器名 按键名
$parts = preg_split('/\s+/', trim($line));
if (count($parts) > 2) {
$repeat = intval($parts[1]);
if ($repeat > 0) {
// 重复码,长按中,可忽略或做连续调节
continue;
}
$keyName = $parts[2];
if (isset($actionMap[$keyName])) {
echo "触发动作: " . $actionMap[$keyName] . "\n";
// 这里可以调用项目逻辑,比如发送HTTP请求控制前端页面
}
}
}
这种架构把实时性要求高的解码工作交给C语言实现的LIRC,PHP专注于业务处理,稳定性比直接轮询GPIO高得多。代码中通过判断重复次数字段来过滤长按产生的重复码,只有第一次按键才触发动作,符合大多数遥控交互的习惯。
四、常见问题与调试技巧
调试红外解码时,第一步要确认硬件连接正确。接收头有三个引脚:OUT接GPIO、GND接地、VCC接3.3V,三个脚不能接反,接反可能烧毁器件。如果读到的电平一直是0或一直是1,先用手机摄像头对准遥控器发射管按键观察是否有亮点,排除遥控器本身的问题。
解码不稳定时可以在每帧之间增加确认逻辑,比如连续两次读到相同键码才认定有效,这样能过滤掉偶发的干扰脉冲。另外注意系统负载问题,跑PHP解码进程时尽量避免同时执行重计算任务,必要时可以给进程提高调度优先级,用nice或者renice命令调整。
最后建议把解码逻辑封装成独立的守护进程,通过消息队列或者HTTP接口把按键事件推送给Web应用,这样PHP-FPM管理的网页和硬件解码层完全解耦,部署和排查问题都会轻松很多。整体来看,个人实验项目用GPIO轮询方式简单直接,生产环境则优先选择LIRC方案。