本文假裝修理電腦開機(jī)鍵皿伺,其實(shí)是在解決無線電遙控研究的一個(gè)基礎(chǔ)問題:怎樣用廉價(jià)的方案持續(xù)監(jiān)聽ASK/OOK信號(hào)并自動(dòng)解碼员辩。ASK/OOK廣泛應(yīng)用于各種無線電遙控設(shè)備,包括家電鸵鸥、卷簾門奠滑、車門……
背景
我的臺(tái)式機(jī)的開機(jī)鍵確實(shí)是壞了。曾經(jīng)買過一個(gè)嘗試替換妒穴,但因規(guī)格不同裝不上去宋税;還用復(fù)位鍵代替開機(jī)鍵用過一陣子,但復(fù)位鍵太小讼油,按得不爽杰赛。本來有小米的WiFi開關(guān),但還不知如何整合矮台。萬般無奈乏屯,用無線電遙控按鈕加上網(wǎng)絡(luò)喚醒成了最后簡便可行的方案根时。
另一方面,以前文章說過用軟件無線電(比如HackRF + Inspectrum)可以接收ASK/OOK信號(hào)并解碼辰晕,但有幾個(gè)缺點(diǎn):
- 成本高蛤迎;
- 不能自動(dòng)解碼:如果要解碼大量數(shù)據(jù)(比如研究滾碼),用Inspectrum手動(dòng)解碼是非常煩瑣的伞芹;
- 不能用于長期監(jiān)聽:因?yàn)镚NU Radio儲(chǔ)存的是高采樣率的原始信號(hào)忘苛,每秒數(shù)MB。
所以唱较,還是要再找一條路扎唾。
所需材料
- ASK接收/解調(diào)模塊:圖左上, 有兩種,都是5元左右南缓;
- 樹莓派:圖右上胸遇;
- 無線按鈕:圖下, 本來可以用其它無線遙控器代替,但這種比較像按鈕汉形。假貨寶上不多見纸镊,略貴,15元概疆。
ASK接收/解調(diào)模塊的作用是將315/433MHz的調(diào)幅信號(hào)解調(diào)逗威。除了電源,它有一個(gè)數(shù)據(jù)腳岔冀,輸出解調(diào)的高低電平凯旭;有的模塊有一個(gè)使能腳,但不接也行(因?yàn)楦?dòng)為高電平使套,即啟用狀態(tài))罐呼。
圖中有一個(gè)模塊沒接天線,其實(shí)也可以收到信號(hào)侦高;也可以用一根20cm左右的導(dǎo)線做天線嫉柴。感覺差別不大。
無線按鈕的作用則相反:將一組數(shù)據(jù)編碼奉呛,然后用315/433MHz調(diào)幅發(fā)射出去计螺。這組數(shù)據(jù)是芯片預(yù)置的,據(jù)說每個(gè)芯片都不一樣瞧壮。編碼方式實(shí)測是脈寬編碼(PWM)登馒,這是無線電遙控器里最常用的。
認(rèn)識(shí)接收模塊
把這兩個(gè)接收模塊加上5V電源馁痴,然后用示波器觀察數(shù)據(jù)引腳。
然后發(fā)現(xiàn)杯具了肺孤,怎么總是有“信號(hào)”罗晕?——這济欢,就是傳說中的噪聲。便宜無好貨? 也許小渊。但電路噪聲法褥、空中擁擠的無線電、甚至樹莓派產(chǎn)生的射頻……要完全消除噪聲酬屉,并不是那么容易的半等。所以,保持平常心呐萨。
再看有信號(hào)時(shí):按一下無線按鈕杀饵。這次倒比較像樣。
為了驗(yàn)證其正確性切距,用樹莓派發(fā)射一個(gè)信號(hào)。黃色是發(fā)射的惨远,綠色是接收的谜悟。可以看到波形基本一致北秽,有一丟丟的延時(shí)葡幸。
認(rèn)識(shí)無線按鈕
從圖4可以看出,無線按鈕的信號(hào)是脈寬調(diào)制(PWM)的贺氓,即一對(duì)高低電平的組合表示1bit蔚叨,高電平較寬的代表1,低電平較寬的代表0掠归。圖中的波形可以解讀為 1000 1010 1001 10...
縮小比例缅叠,看看整體。
可以看到虏冻,以一段大約10ms的低電平作為分隔肤粱,我們可以將其作為信號(hào)開始的標(biāo)志,即 start0 + data (+ start0)厨相。
在上一篇文章中领曼,我們建立的遙控信號(hào)的模型是:先有一段較長的高電平作為開始,然后一小段低電平蛮穿,然后是數(shù)據(jù)庶骄,最后有一段低電平作為結(jié)束,即 start1 + start0 + data + stop0践磅。
顯然后者是更合理的单刁。因?yàn)闆]有信號(hào)時(shí),接收模塊的輸出就是低電平府适。這個(gè)無線按鈕有偷工之嫌羔飞。但我們能適應(yīng)肺樟,硬件不夠,軟件來補(bǔ)逻淌。
關(guān)鍵代碼
初始化
接收模塊的驅(qū)動(dòng)能力一般很弱么伯,所以,不要使用上拉或下拉電阻卡儒。
# 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田柔,用于在每個(gè)電平跳變時(shí)進(jìn)行響應(yīng)/處理。但實(shí)際試了下骨望,效果并不好硬爆。感覺它們更適合于低速的場合,比如檢測傳統(tǒng)的開關(guān)狀態(tài)锦募。
最后摆屯,回歸到最樸素的輪詢式。另一方面也是因?yàn)殡娐分杏性肼暱纺叮词篃o信號(hào)時(shí)虐骑,數(shù)據(jù)腳也一直存在電平的跳變,用callback并不會(huì)更快赎线。
實(shí)測采樣周期設(shè)置為 0.1ms 或 0.05ms 都處理得過來廷没。由于ASK/OOK數(shù)據(jù)頻率通常在1~2KHz,所以 0.05ms相當(dāng)于20KHz的采樣率垂寥,足夠了颠黎。
下面的代碼保證間隔一個(gè)采樣周期進(jìn)行一次采樣,返回采樣時(shí)的電平及當(dāng)時(shí)的時(shí)間滞项。
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)
接收一段信號(hào)
根據(jù)前面的分析狭归,有的信號(hào)會(huì)以高電平作為前導(dǎo),有的會(huì)以低電平作為前導(dǎo)文判。所以过椎,下面的代碼等待一段足夠?qū)挼碾娖剑ū热?~10ms)作為信號(hào)的開始,而不管它是高電平還是低電平戏仓。之所以忽略太長的電平疚宇,是因?yàn)闆]有信號(hào)(萬一也沒有噪聲)時(shí),將會(huì)一直是低電平赏殃。
我們記錄起始時(shí)的電平敷待,然后將每個(gè)電平翻轉(zhuǎn)時(shí)對(duì)應(yīng)的時(shí)間記入一個(gè)時(shí)間戳數(shù)組。我們忽略太窄的脈寬仁热,因?yàn)樗ǔJ敲蹋ㄒ惨馕吨@多半是一段噪聲)榜揖;此時(shí),我們直接丟棄這段“信號(hào)”。
直到下一段較寬的電平時(shí)举哟,我們認(rèn)為這段信號(hào)結(jié)束钳幅。將時(shí)間戳數(shù)組和起始電平作為一個(gè)波形對(duì)象返回。這相當(dāng)于這個(gè)信號(hào)的物理層表示炎滞。
太短的“信號(hào)”也將被丟棄,因?yàn)樗ǔR彩窃肼曃芷颍挥幸饬x的數(shù)據(jù)不會(huì)那么短册赛。太長的“信號(hào)”也將被丟棄,因?yàn)檫@也可能是噪聲震嫉,而且它會(huì)使數(shù)組過大森瘪,可能導(dǎo)致程序崩潰。
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解碼
解碼是指票堵,從物理層的波形扼睬,解碼出數(shù)據(jù)鏈路層的比特流。
我們將接收到的波形按PWM來解碼悴势。但要注意窗宇,信號(hào)可能并不是PWM的,而且噪聲可能帶來毛刺或波形寬度的變化特纤。所以军俊,一旦碰到任何不符合預(yù)期的地方,我們毫不留情地返回失敗捧存。
首先我們要排除起始的高/低電平粪躬,最后一個(gè)bit也要排除,因?yàn)樗ǔв薪Y(jié)束時(shí)的低電平昔穴,而使得寬度不同于別的bit镰官。用這一段數(shù)據(jù),我們可以計(jì)算出每個(gè)符號(hào)(bit)的平均時(shí)長吗货,作為周期泳唠。
在解碼的過程中,任何一個(gè)電平寬度超過周期的卿操,都被視為不合法警检。
同時(shí),我們還把每對(duì)電平中較寬的脈寬累加起來害淤,用其平均值/周期作為占空比扇雕。占空比太大也被視為不合法。因?yàn)閷?duì)于1KHz的數(shù)據(jù)窥摄,如果占空比為95%镶奉,意味著窄的電平是0.05ms,這已經(jīng)是我們采樣精度的極限了。而且事實(shí)上哨苛,沒人會(huì)用這么高的占空比鸽凶,通常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
效果
因?yàn)榇a是輪詢式玻侥,所以要關(guān)心一下CPU占用率。在樹莓派3代上亿蒸,采樣周期設(shè)置為0.05ms時(shí)凑兰,用top查看,CPU占用率大約25%边锁;而且改變采樣周期姑食,基本沒有變化。換不同的電路板時(shí)倒有有差別(另一塊電路板在10%以下茅坛,可惜已經(jīng)掛了)音半,可能和噪聲水平有關(guān)系。
其實(shí)在同一個(gè)樹莓派上贡蓖,開兩個(gè)進(jìn)程曹鸠,一個(gè)發(fā)送,一個(gè)接收斥铺,完全沒有壓力物延,收到的數(shù)據(jù)能正確解碼。
至于噪聲仅父,雖然示波器上一直有噪聲電平叛薯,但經(jīng)過層層檢驗(yàn),幾乎沒出現(xiàn)把噪聲解碼為信號(hào)的情況笙纤。
但有信號(hào)時(shí)會(huì)出現(xiàn)誤碼耗溜,表現(xiàn)在:
- 如果發(fā)射端比較遠(yuǎn)或信號(hào)比較弱時(shí),有一定的誤碼省容;
- 在按一下鍵時(shí)抖拴,通常會(huì)將信號(hào)重復(fù)發(fā)送3~5次,其中可能會(huì)有解碼錯(cuò)誤的腥椒;尤其是最后一個(gè)信號(hào)阿宅。其實(shí)最后一個(gè)信號(hào)可能根本沒有完整發(fā)送。因?yàn)橛械倪b控器在按下鍵時(shí)會(huì)一直發(fā)送笼蛛,松開時(shí)就立即停止發(fā)送洒放。
- 經(jīng)過博聯(lián)(Broadlink)RM2這樣的萬能遙控器學(xué)習(xí)后、再發(fā)送的“二手碼”滨砍,誤碼會(huì)更高一些往湿。
下圖是實(shí)際運(yùn)行效果妖异,簡單說明下:
- [WAV] 表示原始波形信息。44.4ms/0/51 表示總長44.4ms领追,以低電平開始他膳,總共51段,后面是每段電平的時(shí)長绒窑,單位ms棕孙。
- [PWM] 表示PWM解碼成功,后面是其信息:起始高電平/低電平時(shí)長/結(jié)束低電平時(shí)長些膨、比特周期散罕、占空比、總位數(shù)傀蓉、數(shù)據(jù)。
- 綠色框表示解碼符合預(yù)期职抡;橙色框表示雖然是正確的PWM碼葬燎,但數(shù)據(jù)有丟失,不同于發(fā)射數(shù)據(jù)缚甩;紅色框則是噪聲了(不能成功解碼)谱净。
另外,對(duì)于按一下鍵擅威,連續(xù)發(fā)射的多個(gè)信號(hào)壕探,并不能全部接收到。因?yàn)榻邮盏揭粋€(gè)信號(hào)后郊丛,程序就會(huì)去解碼李请,而解碼耗費(fèi)的時(shí)間可能會(huì)錯(cuò)過一個(gè)信號(hào)(的起始電平)。用多線程也許能解決這個(gè)問題厉熟。但并沒有多大必要导盅,因?yàn)榘l(fā)射端本來就會(huì)重復(fù)發(fā)送多次同樣的信號(hào),只要能接收到其中一個(gè)完整的就行了揍瑟。
成果
有了這個(gè)自動(dòng)接收/解碼機(jī)白翻,首先解碼了無線電按鈕,據(jù)此觸發(fā)WOL绢片,實(shí)現(xiàn)了按鍵就能開電腦的效果B蒜伞(我容易嗎我)
然后,很容易地解碼了家里的所有無線電遙控器底循,做成字典巢株。一方面,配合發(fā)射模塊熙涤,可以程序控制所有設(shè)備纯续;另一方面随珠,可以監(jiān)視家里的所有無線電活動(dòng)(作為日志嘛)。當(dāng)然猬错,也許會(huì)收到別的信號(hào)(你懂的)窗看。
結(jié)語
用樹莓派+5元錢的ASK接收模塊,即可實(shí)現(xiàn)對(duì)ASK/OOK信號(hào)的自動(dòng)接收和解碼倦炒。相比于軟件無線電套件(HackRF, GNU Radio, Inspectrum)显沈,輕巧低廉。而且在這一領(lǐng)域逢唤,可以說更強(qiáng)大拉讯。實(shí)測用腳本語言就可以滿足數(shù)據(jù)采集和解碼的性能要求,開發(fā)調(diào)試非常方便鳖藕。主要的問題是處理噪聲魔慷,增加一些有效性檢查。
在修好電腦開機(jī)鍵的同時(shí)著恩,順便完成了發(fā)射/接收院尔,解碼/編碼的整套工具集,可以用于ASK/OOK的研究喉誊。
代碼已開源:https://github.com/loblab/rfask
目前支持PWM和曼徹斯特編碼邀摆。