我是怎么修好电脑开机键的:ASK/OOK自动接收与解码

本文假装修理电脑开机键,其实是在解决无线电遥控研究的一个基础问题:怎样用廉价的方案持续监听ASK/OOK信号并自动解码。ASK/OOK广泛应用于各种无线电遥控设备,包括家电、卷帘门、车门……

背景

我的台式机的开机键确实是坏了。曾经买过一个尝试替换,但因规格不同装不上去;还用复位键代替开机键用过一阵子,但复位键太小,按得不爽。本来有小米的WiFi开关,但还不知如何整合。万般无奈,用无线电遥控按钮加上网络唤醒成了最后简便可行的方案。

另一方面,以前文章说过用软件无线电(比如HackRF + Inspectrum)可以接收ASK/OOK信号并解码,但有几个缺点:

  1. 成本高;
  2. 不能自动解码:如果要解码大量数据(比如研究滚码),用Inspectrum手动解码是非常烦琐的;
  3. 不能用于长期监听:因为GNU Radio储存的是高采样率的原始信号,每秒数MB。

所以,还是要再找一条路。

所需材料

  1. ASK接收/解调模块:图左上, 有两种,都是5元左右;
  2. 树莓派:图右上;
  3. 无线按钮:图下, 本来可以用其它无线遥控器代替,但这种比较像按钮。假货宝上不多见,略贵,15元。
图1:修理电脑开机键所需材料

ASK接收/解调模块的作用是将315/433MHz的调幅信号解调。除了电源,它有一个数据脚,输出解调的高低电平;有的模块有一个使能脚,但不接也行(因为浮动为高电平,即启用状态)。

图中有一个模块没接天线,其实也可以收到信号;也可以用一根20cm左右的导线做天线。感觉差别不大。

无线按钮的作用则相反:将一组数据编码,然后用315/433MHz调幅发射出去。这组数据是芯片预置的,据说每个芯片都不一样。编码方式实测是脉宽编码(PWM),这是无线电遥控器里最常用的。

认识接收模块

把这两个接收模块加上5V电源,然后用示波器观察数据引脚。

图2. 用示波器观察解调的信号

然后发现杯具了,怎么总是有“信号”?——这,就是传说中的噪声。便宜无好货? 也许。但电路噪声、空中拥挤的无线电、甚至树莓派产生的射频……要完全消除噪声,并不是那么容易的。所以,保持平常心。

图3. 接收模块的噪声

再看有信号时:按一下无线按钮。这次倒比较像样。

图4. 有信号输入时,接收模块的输出

为了验证其正确性,用树莓派发射一个信号。黄色是发射的,绿色是接收的。可以看到波形基本一致,有一丢丢的延时。

图5:发射和接收的信号

认识无线按钮

从图4可以看出,无线按钮的信号是脉宽调制(PWM)的,即一对高低电平的组合表示1bit,高电平较宽的代表1,低电平较宽的代表0。图中的波形可以解读为 1000 1010 1001 10...

缩小比例,看看整体。

图6. 无线按钮发射的信号整体

可以看到,以一段大约10ms的低电平作为分隔,我们可以将其作为信号开始的标志,即 start0 + data (+ start0)。

在上一篇文章中,我们建立的遥控信号的模型是:先有一段较长的高电平作为开始,然后一小段低电平,然后是数据,最后有一段低电平作为结束,即 start1 + start0 + data + stop0。

显然后者是更合理的。因为没有信号时,接收模块的输出就是低电平。这个无线按钮有偷工之嫌。但我们能适应,硬件不够,软件来补。

关键代码

初始化

接收模块的驱动能力一般很弱,所以,不要使用上拉或下拉电阻。

    #  do NOT use pull_up_down=GPIO.PUD_UP nor GPIO.PUD_DOWN
    GPIO.setup(pin_rx, GPIO.IN)

如何采样

树莓派有提供 callback(即 add_event_detect) 和 wait_for_edge,用于在每个电平跳变时进行响应/处理。但实际试了下,效果并不好。感觉它们更适合于低速的场合,比如检测传统的开关状态。

最后,回归到最朴素的轮询式。另一方面也是因为电路中有噪声,即使无信号时,数据脚也一直存在电平的跳变,用callback并不会更快。

实测采样周期设置为 0.1ms 或 0.05ms 都处理得过来。由于ASK/OOK数据频率通常在1~2KHz,所以 0.05ms相当于20KHz的采样率,足够了。

下面的代码保证间隔一个采样周期进行一次采样,返回采样时的电平及当时的时间。

    def get_sample(self):
        self.sample_time += self.sample_period
        now = time.time()
        wait = self.sample_time - now
        if wait > 0:
          time.sleep(wait)
        b = GPIO.input(self.pin_rx)
        now = time.time()
        return (b, now)

接收一段信号

根据前面的分析,有的信号会以高电平作为前导,有的会以低电平作为前导。所以,下面的代码等待一段足够宽的电平(比如3~10ms)作为信号的开始,而不管它是高电平还是低电平。之所以忽略太长的电平,是因为没有信号(万一也没有噪声)时,将会一直是低电平。

我们记录起始时的电平,然后将每个电平翻转时对应的时间记入一个时间戳数组。我们忽略太窄的脉宽,因为它通常是毛刺(也意味着这多半是一段噪声);此时,我们直接丢弃这段“信号”。

直到下一段较宽的电平时,我们认为这段信号结束。将时间戳数组和起始电平作为一个波形对象返回。这相当于这个信号的物理层表示。

太短的“信号”也将被丢弃,因为它通常也是噪声;有意义的数据不会那么短。太长的“信号”也将被丢弃,因为这也可能是噪声,而且它会使数组过大,可能导致程序崩溃。

    def receive(self):
        wave = BitWave()
        ts = wave.timestamp
        b = GPIO.input(self.pin_rx)
        now = time.time()
        ch = GPIO.wait_for_edge(self.pin_rx, GPIO.BOTH, timeout=self.min_gap)
        if ch is not None:
            return None
        b0 = b
        t0 = now
        ts.append(t0)
        ch = GPIO.wait_for_edge(self.pin_rx, GPIO.BOTH, timeout=self.max_gap-self.min_gap)
        b = GPIO.input(self.pin_rx)
        now = time.time()
        if ch is None:
            return None
        if b == b0:
            return None
        wave.startbit = b0
        self.bit = b
        self.sample_time = now
        self.edge_time = now
        ts.append(now)
        min_gap = 1e-3 * self.min_gap
        while True:
            (b, now) = self.get_sample()
            if b == self.bit:
                if now - self.edge_time > min_gap:
                    if b == 0:
                        ts.append(now)
                    return wave if len(ts) > 5 else None
            else:
                if now - self.edge_time < self.sample_period:
                    return None
                self.edge_time = now
                self.bit = b
                ts.append(now)
                if len(ts) > self.max_len:
                    return None
        return wave

PWM解码

解码是指,从物理层的波形,解码出数据链路层的比特流。

我们将接收到的波形按PWM来解码。但要注意,信号可能并不是PWM的,而且噪声可能带来毛刺或波形宽度的变化。所以,一旦碰到任何不符合预期的地方,我们毫不留情地返回失败。

首先我们要排除起始的高/低电平,最后一个bit也要排除,因为它通常带有结束时的低电平,而使得宽度不同于别的bit。用这一段数据,我们可以计算出每个符号(bit)的平均时长,作为周期。

在解码的过程中,任何一个电平宽度超过周期的,都被视为不合法。

同时,我们还把每对电平中较宽的脉宽累加起来,用其平均值/周期作为占空比。占空比太大也被视为不合法。因为对于1KHz的数据,如果占空比为95%,意味着窄的电平是0.05ms,这已经是我们采样精度的极限了。而且事实上,没人会用这么高的占空比,通常70%左右比较合理,这样误码率更低。

    def decode(self, wave):
        ts = wave.timestamp
        size = len(ts) - 1
        if size < 17:  # 8 bits at least
            return False
        if wave.startbit == 1:
            self.start1 = ts[1] - ts[0]
            i1 = 2
        else:
            self.start1 = 0
            i1 = 1
        self.start0 = ts[i1] - ts[i1 - 1]
        cbits = size - i1
        if (cbits % 2) > 0:
            return False
        self.bits = BitArray()
        csym = cbits / 2 - 1
        dur = ts[-3] - ts[i1]
        self.period = dur / csym
        self.stop0 = ts[-1] - ts[-3] - self.period
        if self.stop0 < 0:
            return False
        sum1 = 0
        c1 = 0
        for i in range(i1, size - 2):
            w = ts[i + 1] - ts[i]
            if w >= self.period:
                return False
            if w + w > self.period:
                sum1 += w
                c1 += 1
        if (c1 == 0):
            return False
        self.duty = sum1 / c1 / self.period
        if self.duty > 0.9:
            return False

        for i in range(i1, size - 1, 2):
            w = ts[i + 1] - ts[i]
            b = '0b1' if  w + w > self.period else '0b0'
            self.bits.append(b)
        return True

效果

因为代码是轮询式,所以要关心一下CPU占用率。在树莓派3代上,采样周期设置为0.05ms时,用top查看,CPU占用率大约25%;而且改变采样周期,基本没有变化。换不同的电路板时倒有有差别(另一块电路板在10%以下,可惜已经挂了),可能和噪声水平有关系。

其实在同一个树莓派上,开两个进程,一个发送,一个接收,完全没有压力,收到的数据能正确解码。

至于噪声,虽然示波器上一直有噪声电平,但经过层层检验,几乎没出现把噪声解码为信号的情况。

但有信号时会出现误码,表现在:

  1. 如果发射端比较远或信号比较弱时,有一定的误码;
  2. 在按一下键时,通常会将信号重复发送3~5次,其中可能会有解码错误的;尤其是最后一个信号。其实最后一个信号可能根本没有完整发送。因为有的遥控器在按下键时会一直发送,松开时就立即停止发送。
  3. 经过博联(Broadlink)RM2这样的万能遥控器学习后、再发送的“二手码”,误码会更高一些。

下图是实际运行效果,简单说明下:

  • [WAV] 表示原始波形信息。44.4ms/0/51 表示总长44.4ms,以低电平开始,总共51段,后面是每段电平的时长,单位ms。
  • [PWM] 表示PWM解码成功,后面是其信息:起始高电平/低电平时长/结束低电平时长、比特周期、占空比、总位数、数据。
  • 绿色框表示解码符合预期;橙色框表示虽然是正确的PWM码,但数据有丢失,不同于发射数据;红色框则是噪声了(不能成功解码)。
图7:解码及误码情况

另外,对于按一下键,连续发射的多个信号,并不能全部接收到。因为接收到一个信号后,程序就会去解码,而解码耗费的时间可能会错过一个信号(的起始电平)。用多线程也许能解决这个问题。但并没有多大必要,因为发射端本来就会重复发送多次同样的信号,只要能接收到其中一个完整的就行了。

成果

有了这个自动接收/解码机,首先解码了无线电按钮,据此触发WOL,实现了按键就能开电脑的效果!(我容易吗我)

然后,很容易地解码了家里的所有无线电遥控器,做成字典。一方面,配合发射模块,可以程序控制所有设备;另一方面,可以监视家里的所有无线电活动(作为日志嘛)。当然,也许会收到别的信号(你懂的)。

图8:家中的无线电活动

结语

用树莓派+5元钱的ASK接收模块,即可实现对ASK/OOK信号的自动接收和解码。相比于软件无线电套件(HackRF, GNU Radio, Inspectrum),轻巧低廉。而且在这一领域,可以说更强大。实测用脚本语言就可以满足数据采集和解码的性能要求,开发调试非常方便。主要的问题是处理噪声,增加一些有效性检查。

在修好电脑开机键的同时,顺便完成了发射/接收,解码/编码的整套工具集,可以用于ASK/OOK的研究。

代码已开源:https://github.com/loblab/rfask

目前支持PWM和曼彻斯特编码。

图9:ASK/OOK工具集
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容