紅外遙控解碼:NEC協(xié)議與定時(shí)器中斷實(shí)現(xiàn)空調(diào)遙控碼接收)
簡(jiǎn)介面向51單片機(jī)學(xué)習(xí)者和紅外遙控解碼開發(fā)者這套格力空調(diào)遙控碼接收程序采用中斷方式完成信號(hào)采集并在1602液晶上實(shí)時(shí)顯示解碼結(jié)果適合用于智能家電控制、紅外協(xié)議分析或畢業(yè)設(shè)計(jì)參考。壓縮包共56個(gè)文件總大小5.17MB文件類型以C源碼、Keil工程文件uv2、plg、opt、頭文件以及編譯生成的lst、obj、hex燒錄文件為主另有PDF說明、JPEG效果圖和格力遙控碼分析文檔便于對(duì)照查看原理與實(shí)際運(yùn)行狀態(tài)。已有1176人學(xué)習(xí)下載。程序模塊劃分清晰從遙控碼接收、中斷處理到液晶顯示均有對(duì)應(yīng)實(shí)現(xiàn)同時(shí)附帶多個(gè)調(diào)試階段的版本包可對(duì)比不同修改對(duì)解碼效果的影響幫助讀者快速掌握空調(diào)遙控碼的解析思路與移植方法。 前陣子收拾東西翻出一個(gè)舊格力空調(diào)遙控器想著把它的按鍵碼抓下來做成一個(gè)用51單片機(jī)識(shí)別的遙控碼接收程序。說干就干我直接用STC89C52加一個(gè)一體化紅外接收頭把遙控器發(fā)出來的紅外信號(hào)解調(diào)成電平變化再通過外部中斷和定時(shí)器把每一段脈沖寬度記錄下來最后從時(shí)序里還原出按鍵碼。這個(gè)項(xiàng)目很適合正在學(xué)51單片機(jī)的人上手它不涉及復(fù)雜算法但能把中斷、定時(shí)器、電平時(shí)序分析這幾個(gè)關(guān)鍵知識(shí)點(diǎn)全部串在一起。做完之后想擴(kuò)展成紅外發(fā)射、自學(xué)習(xí)遙控器也順理成章。這篇文章把整個(gè)思路、硬件連接、NEC協(xié)議拆解、參考代碼和調(diào)試踩坑都寫出來。不管你用的是普中、江科大還是其他開發(fā)板原理都一樣照著接好線就能跑起來。1. 項(xiàng)目思路拆解51單片機(jī)做遙控碼接收到底在接什么1.1 核心需求解析這個(gè)項(xiàng)目的本質(zhì)是把空調(diào)遙控器發(fā)出的紅外光信號(hào)轉(zhuǎn)成單片機(jī)能夠處理的電平變化序列。一體化紅外接收頭已經(jīng)完成了光電轉(zhuǎn)換、放大、解調(diào)這幾步輸出引腳上直接就是高低電平。單片機(jī)要做的只有一件事測(cè)量每一段電平持續(xù)的時(shí)間長(zhǎng)度再按照協(xié)議規(guī)則還原成0和1??赡苡腥藭?huì)問為什么不買現(xiàn)成的紅外解碼模塊或者直接用帶紅外庫的開發(fā)板對(duì)于學(xué)習(xí)目的來說自己用51寫接收程序才能真正搞懂紅外遙控的工作機(jī)制。而且STC89C52這種幾塊錢的單片機(jī)處理紅外碼完全夠用??照{(diào)遙控器雖然按鍵多但協(xié)議層面并不復(fù)雜主要是一個(gè)幀結(jié)構(gòu)解析的問題。應(yīng)用場(chǎng)景也很明確自學(xué)習(xí)遙控器、智能家居控制面板、空調(diào)碼采集。最實(shí)用的方向是把接收到的碼保存下來配合紅外發(fā)射管做回放變成萬能空調(diào)遙控器。這一步只要接收端做穩(wěn)定了后面就是水到渠成的事。1.2 方案選型為什么用51單片機(jī)可能有人覺得STC32、STM32或者Arduino性能更強(qiáng)為什么非要用經(jīng)典的51我的答案很簡(jiǎn)單夠用而且學(xué)習(xí)價(jià)值最高。紅外接收頭解調(diào)后的信號(hào)頻率非常低單次脈沖寬度在微秒到毫秒級(jí)51單片機(jī)完全能處理過來。51的定時(shí)器和外部中斷是學(xué)習(xí)嵌入式繞不開的基礎(chǔ)模塊。用這個(gè)項(xiàng)目練手比單純點(diǎn)燈有意思多了。我用的是STC89C52晶振12MHz這樣定時(shí)器計(jì)數(shù)值可以直接和微秒對(duì)應(yīng)省去換算的麻煩。如果你用普中開發(fā)板或者江科大視頻里那套板子原理完全相同只要把紅外接收頭接到P3.2引腳就能復(fù)用這套代碼。2. 硬件準(zhǔn)備與連接別在接線這一步翻車2.1 器件清單與紅外接收頭選型硬件方面需要的器件非常少基本上一個(gè)51最小系統(tǒng)加一個(gè)接收頭就能開工。我用的是一體化紅外接收頭VS1838B這種接收頭內(nèi)置了光敏二極管、AGC放大器和帶通濾波器中心頻率38kHz正好匹配格力遙控器發(fā)射端用的載波頻率。市面上常見的HS0038B也可以參數(shù)基本一致。接收頭通常有三個(gè)引腳不同廠家的引腳順序不完全一樣。以VS1838B為例從接收面的左側(cè)看引腳順序是OUT、GND、VCC。接之前最好對(duì)著器件手冊(cè)確認(rèn)或者用萬用表量一下輸出引腳的對(duì)地電壓。空閑時(shí)輸出應(yīng)該接近高電平有紅外信號(hào)時(shí)會(huì)被拉低找到這個(gè)規(guī)律就不會(huì)接錯(cuò)。除了接收頭還需要51單片機(jī)、12MHz晶振、兩個(gè)30pF負(fù)載電容、10k電阻若干、面包板和杜邦線。如果想用串口把解析結(jié)果打印到電腦上還需要一個(gè)USB轉(zhuǎn)TTL模塊。建議再準(zhǔn)備一個(gè)邏輯分析儀哪怕是十幾塊錢的簡(jiǎn)易版本對(duì)調(diào)試幫助非常大。2.2 接線方式與供電細(xì)節(jié)接線其實(shí)非常簡(jiǎn)單核心就幾條線。接收頭的OUT接單片機(jī)的P3.2引腳也就是INT0外部中斷輸入口。VCC接5VGND接GND。重要的是接收頭的輸出引腳是開漏結(jié)構(gòu)還是推挽結(jié)構(gòu)不同模塊表現(xiàn)不一樣。如果是裸接收頭最好在OUT和VCC之間加一個(gè)10k上拉電阻保證輸出高電平的驅(qū)動(dòng)能力。很多開發(fā)板上的紅外接收模塊已經(jīng)集成了上拉電阻和濾波電容直接插上去就能用但面包板搭電路時(shí)別省這一步。供電濾波是我實(shí)際踩過的一個(gè)坑。如果用USB轉(zhuǎn)TTL模塊供電電流余量不是很大紅外接收頭對(duì)電源紋波又比較敏感工作頻率一高就容易誤觸發(fā)。我的做法是在接收頭VCC和GND之間并一個(gè)10uF電解電容再并一個(gè)0.1uF瓷片電容實(shí)測(cè)能明顯減少隨機(jī)誤碼。另外晶振一定要用12MHz這樣定時(shí)器每個(gè)計(jì)數(shù)單位正好是1微秒。如果順手抓了11.0592MHz的晶振也能做只是計(jì)數(shù)值換算成微秒要乘以1.085調(diào)試起來多一道轉(zhuǎn)換工序沒必要。3. 紅外通信基礎(chǔ)NEC協(xié)議和格力變體的關(guān)鍵差異3.1 NEC協(xié)議幀結(jié)構(gòu)絕大多數(shù)格力老款空調(diào)遙控器使用的是NEC協(xié)議準(zhǔn)確說是NEC協(xié)議的擴(kuò)展變體。標(biāo)準(zhǔn)NEC協(xié)議一幀數(shù)據(jù)由引導(dǎo)碼、32位數(shù)據(jù)位和結(jié)束位三部分組成。引導(dǎo)碼是9ms低電平加4.5ms高電平數(shù)據(jù)位緊跟在引導(dǎo)碼后面共32位順序是8位地址碼、8位地址反碼、8位命令碼、8位命令反碼。每個(gè)數(shù)據(jù)位本身也很有特點(diǎn)。不管發(fā)送的是0還是1每一位都會(huì)先輸出一段560us的低電平然后用高電平的長(zhǎng)度區(qū)分?jǐn)?shù)值。高電平持續(xù)1.69ms左右代表1高電平持續(xù)560us左右代表0。結(jié)束位是最后560us的低電平之后信號(hào)線回到高電平空閑狀態(tài)。發(fā)送完一幀后如果按鍵沒有松開遙控器會(huì)每隔108ms左右發(fā)送一個(gè)重復(fù)幀。重復(fù)幀沒有數(shù)據(jù)位只有9ms低電平加2.25ms高電平加560us低電平作用是告訴接收端“按鍵還在按住”。3.2 為什么用下降沿周期測(cè)量法接收程序最核心的問題是如何準(zhǔn)確測(cè)量脈沖寬度。我選擇的是下降沿周期測(cè)量法而不是分別測(cè)量高低電平。原理很簡(jiǎn)單外部中斷設(shè)置為下降沿觸發(fā)每次信號(hào)從高到低跳變時(shí)進(jìn)入中斷在中斷里讀取定時(shí)器的計(jì)數(shù)值就能得到從上一次下降沿到本次下降沿之間經(jīng)過的總時(shí)間。這個(gè)總時(shí)間等于一段低電平加一段高電平的周期長(zhǎng)度。引導(dǎo)碼的周期是9ms加4.5ms約13.5ms。數(shù)據(jù)位1的周期是560us加1.69ms約2.25ms。數(shù)據(jù)位0的周期是560us加560us約1.12ms。這三個(gè)數(shù)值差距明顯非常容易區(qū)分。如果把觸發(fā)方式改成上升沿再去單獨(dú)測(cè)高低電平程序復(fù)雜度會(huì)高很多而且51單片機(jī)在短時(shí)間中斷響應(yīng)上容易出問題新手不推薦。3.3 格力空調(diào)碼的實(shí)際情況格力空調(diào)的編碼在NEC框架下做了一些變化。很多格力老機(jī)型的地址碼固定為0x00也有部分型號(hào)使用0xFD之類的值命令碼則對(duì)應(yīng)著電源、模式、溫度、風(fēng)量等按鍵。不同批次、不同型號(hào)的遙控器碼值可能完全不同網(wǎng)上雖然能查到一些格力遙控碼表但直接抄過來大概率對(duì)不上。最可靠的方法就是自己用接收頭抓取自己手里遙控器的碼。只要幀結(jié)構(gòu)符合NEC協(xié)議這套接收程序就能正常工作。萬一遇到新機(jī)型使用其他協(xié)議也不用慌脈寬捕獲這一步是通用的只需要調(diào)整后續(xù)的解析邏輯就行。4. 核心代碼實(shí)現(xiàn)中斷定時(shí)器的脈寬測(cè)量方案4.1 代碼設(shè)計(jì)思路程序整體結(jié)構(gòu)圍繞外部中斷0和定時(shí)器0展開。定時(shí)器0配置為模式1也就是16位定時(shí)器。每次外部中斷進(jìn)入時(shí)先讀取TH0和TL0得到從上一次清零到本次下降沿之間的計(jì)數(shù)值然后立刻清零重新開始計(jì)時(shí)。因?yàn)榫д袷?2MHz51單片機(jī)又是12T模式機(jī)器周期正好是1us所以定時(shí)器讀到的原始計(jì)數(shù)值就是微秒數(shù)不需要任何轉(zhuǎn)換。讀取時(shí)要注意先后順序必須先讀TH0再讀TL0因?yàn)?6位定時(shí)器在運(yùn)行中如果先讀TL0TH0可能剛好進(jìn)位讀出來的值就是錯(cuò)的。數(shù)據(jù)接收采用簡(jiǎn)單狀態(tài)機(jī)空閑狀態(tài)下等待引導(dǎo)碼檢測(cè)到10ms到16ms之間的脈寬就認(rèn)為收到引導(dǎo)碼進(jìn)入數(shù)據(jù)接收狀態(tài)。隨后每來一個(gè)下降沿就根據(jù)脈寬判斷當(dāng)前位是1還是0移位存入緩沖區(qū)。收集滿32位后置幀完成標(biāo)志回到空閑狀態(tài)等待下一幀。4.2 參考源碼下面是我驗(yàn)證過的核心代碼可以直接在STC89C52上編譯運(yùn)行。主函數(shù)里只留了最簡(jiǎn)單的反碼校驗(yàn)和LED翻轉(zhuǎn)方便你確認(rèn)程序是否收到有效數(shù)據(jù)幀。#include reg52.h #define uchar unsigned char #define uint unsigned int sbit IR_IN P3^2; // 紅外接收頭OUT接INT0(P3.2) sbit LED P1^0; // 調(diào)試LED收到完整幀后翻轉(zhuǎn) #define STATE_IDLE 0 #define STATE_DATA 1 volatile unsigned long g_code; // 完整32位碼 volatile unsigned char g_frame_ready; // 置1表示收到一幀 // 定時(shí)器0初始化模式116位定時(shí)器 void Timer0_Init(void) { TMOD 0xF0; // 只改低4位 TMOD | 0x01; TH0 0; TL0 0; TR0 1; // 啟動(dòng)定時(shí) } // 外部中斷0初始化下降沿觸發(fā) void Ext0_Init(void) { IT0 1; // INT0下降沿觸發(fā) EX0 1; // 開INT0中斷 EA 1; // 開總中斷 } // INT0中斷服務(wù)函數(shù)測(cè)量脈寬并還原數(shù)據(jù) void Ext0_ISR(void) interrupt 0 { static uchar state STATE_IDLE; static uchar bit_cnt 0; static unsigned long code_buf 0; uint cnt; // 讀取定時(shí)器計(jì)數(shù)值單位us cnt TH0; cnt (cnt 8) | TL0; TH0 0; TL0 0; // 10ms到16ms之間引導(dǎo)碼或重復(fù)幀引導(dǎo) if (cnt 10000 cnt 16000) { state STATE_DATA; bit_cnt 0; code_buf 0; return; } // 超過16ms干擾或異常復(fù)位狀態(tài)機(jī) if (cnt 16000) { state STATE_IDLE; return; } if (state STATE_DATA) { if (cnt 1800) // 高電平較長(zhǎng) - 1 { code_buf (code_buf 1) | 0x01; } else if (cnt 900) // 高電平較短 - 0 { code_buf (code_buf 1) | 0x00; } else { state STATE_IDLE; // 毛刺忽略 return; } bit_cnt; if (bit_cnt 32) // 收滿32位幀完成 { g_code code_buf; g_frame_ready 1; state STATE_IDLE; } } } void main(void) { uchar addr, addr_inv, cmd, cmd_inv; Timer0_Init(); Ext0_Init(); while (1) { if (g_frame_ready) { g_frame_ready 0; // 收到的順序地址、地址反碼、命令、命令反碼 addr (g_code 24) 0xFF; addr_inv (g_code 16) 0xFF; cmd (g_code 8) 0xFF; cmd_inv g_code 0xFF; // 反碼校驗(yàn) if (addr (uchar)~addr_inv cmd (uchar)~cmd_inv) { LED ~LED; // 實(shí)際項(xiàng)目中可在這里用串口打印addr/cmd } } } }4.3 代碼講解與注意事項(xiàng)這套代碼的核心在中斷服務(wù)函數(shù)但我建議你重點(diǎn)關(guān)注幾個(gè)設(shè)計(jì)細(xì)節(jié)。首先是閾值的選取我把引導(dǎo)碼范圍定為10000到16000us而不是精確的13500us附近。原因是紅外接收頭在不同距離、不同供電環(huán)境下脈寬會(huì)有一定漂移留出20%到30%的余量是必要的。數(shù)據(jù)位的判定閾值放在1800us和900us同樣是為了適應(yīng)漂移。其次是中斷服務(wù)函數(shù)一定要短。接收頭的下降沿是連續(xù)不斷的一次中斷處理太久就會(huì)錯(cuò)過下一次下降沿直接導(dǎo)致數(shù)據(jù)錯(cuò)亂。我的代碼里中斷函數(shù)只做計(jì)數(shù)和移位不涉及串口發(fā)送這類耗時(shí)操作。驗(yàn)證數(shù)據(jù)是否有效、打印結(jié)果這些事情全部放到主循環(huán)里處理這樣中斷才能保證實(shí)時(shí)性。代碼里還隱藏了一個(gè)細(xì)節(jié)收到重復(fù)幀時(shí)11.25ms的脈寬同樣落在引導(dǎo)碼判斷范圍內(nèi)程序會(huì)重新進(jìn)入DATA狀態(tài)但因?yàn)橹貜?fù)幀后面沒有完整數(shù)據(jù)位bit_cnt不會(huì)達(dá)到32也就不會(huì)產(chǎn)生錯(cuò)誤的幀完成標(biāo)志。這個(gè)設(shè)計(jì)保證了長(zhǎng)按遙控器時(shí)不會(huì)反復(fù)觸發(fā)。5. 常見問題與排查技巧實(shí)錄5.1 問題速查表做這個(gè)項(xiàng)目最容易遇到的問題我整理成了一張速查表。很多問題看起來像是程序bug實(shí)際查到最后都是硬件層面的小毛病?,F(xiàn)象可能原因排查手段完全沒反應(yīng)接線錯(cuò)誤、接收頭損壞、晶振沒起振萬用表量VCC示波器看OUT波形換接收頭接收距離短供電不足、缺少上拉、接收頭朝向不對(duì)VCC加10uF電容OUT加上拉避開陽光直射數(shù)據(jù)校驗(yàn)失敗閾值不合適、協(xié)議不是NEC、數(shù)據(jù)位序理解錯(cuò)先打印原始脈寬對(duì)照協(xié)議確認(rèn)幀結(jié)構(gòu)按住按鍵收到很多幀正?,F(xiàn)象遙控器會(huì)發(fā)送重復(fù)幀程序只取首幀或加時(shí)間間隔過濾串口打印亂碼波特率不匹配、晶振頻率不同導(dǎo)致波特率偏差統(tǒng)一兩端波特率和晶振頻率設(shè)置5.2 調(diào)試?yán)鞔诖蛴≡济}寬如果你遇到數(shù)據(jù)解析不出來我強(qiáng)烈建議先把“解析”這一步放一放直接把中斷里測(cè)到的脈寬值用串口發(fā)到電腦上看。正常的NEC幀打印出來應(yīng)該能觀察到一組這樣的數(shù)據(jù)先是13500us左右的引導(dǎo)碼脈寬后面跟著32個(gè)數(shù)值每個(gè)都在1120us或2250us附近徘徊。如果能看到這組數(shù)說明硬件鏈路完全正常問題只出在軟件判斷邏輯上。打印脈寬的方法很簡(jiǎn)單在中斷服務(wù)函數(shù)里只做一件事把脈寬值按順序存進(jìn)一個(gè)全局?jǐn)?shù)組然后置一個(gè)標(biāo)志位。主循環(huán)檢測(cè)到標(biāo)志位后用串口把所有數(shù)組元素發(fā)送出去一次發(fā)完再把標(biāo)志位清零。這個(gè)方法特別適合初次接觸紅外解碼的同學(xué)能讓你直觀看到時(shí)序波形而不是對(duì)著解析結(jié)果瞎猜。5.3 收尾小技巧有一次我長(zhǎng)時(shí)間測(cè)試后發(fā)現(xiàn)接收到的數(shù)據(jù)偶爾會(huì)錯(cuò)位排查了半天最后發(fā)現(xiàn)是USB轉(zhuǎn)TTL模塊共地沒接牢電源地線松動(dòng)導(dǎo)致接收頭電平漂移。從那以后我養(yǎng)成了一個(gè)習(xí)慣所有外設(shè)先共地再想別的。另外補(bǔ)充一個(gè)擴(kuò)展方向。接收程序跑通之后只需要在另一個(gè)引腳接一個(gè)紅外發(fā)射管配合38kHz載波調(diào)制就能把接收到的碼值回放出去。接收和發(fā)射共地程序里注意不要同時(shí)收發(fā)一個(gè)簡(jiǎn)單的自學(xué)習(xí)遙控器就完成了。做空調(diào)遙控碼接收程序的樂趣就在這里前期把接收做扎實(shí)后面能玩出的花樣遠(yuǎn)比想象的要多。本文還有配套的精品資源點(diǎn)擊獲取