鍍金池/ 教程/ 數(shù)據(jù)庫/ 16.3 NEC 協(xié)議紅外遙控器
18. RS485 通信與 Modbus 協(xié)議
17.5 A/D 差分輸入信號
15.8 C 語言復(fù)合數(shù)據(jù)類型(結(jié)構(gòu)體,共用體,枚舉類型)
16.3 NEC 協(xié)議紅外遙控器
13.1 單片機通信時序解析
14.4 單片機 EEPROM 單字節(jié)讀寫操作時序
13.3 多個 .c 文件的初步認識
18.2 Modbus 通信協(xié)議介紹
15.1 BCD 碼介紹
18.3 單片機 Modbus 多機通信程序設(shè)計
18.1 單片機 RS485 通信接口、控制線、原理圖及程序?qū)嵗?/span>
15. 實時時鐘 DS1302
14.7 單片機 I2C 和 EEPROM 的綜合編程
17. 模數(shù)轉(zhuǎn)換與數(shù)模轉(zhuǎn)換
16.2 紅外遙控通信原理
13.2 1602 液晶整屏移動程序
17.6 D/A 輸出
17.7 單片機信號發(fā)生器程序
16.4 溫度傳感器 DS18B20
14.6 單片機EEPROM的頁寫入
13.4 單片機計算器程序設(shè)計[詳細]
17.2 A/D(模數(shù)轉(zhuǎn)換)的主要指標
17.4 PCF8591 應(yīng)用程序
17.1 A/D 和 D/A 的基本概念
17.3 PCF8591硬件接口(電路圖引腳圖)
14.3 單片機 EEPROM 簡介
13.5 單片機串口通信原理和控制程序
15.5 DS1302 寄存器介紹
15.2 單片機 SPI 通信接口
15.6 DS1302 通信時序介紹
14.5 單片機 EEPROM 多字節(jié)讀寫操作時序
16. 紅外通信與 DS18B20 溫度傳感器
14.1 單片機 I2C 時序介紹
15.3 實時時鐘芯片 DS1302 介紹
15.9 單片機電子時鐘程序設(shè)計
16.1 紅外光的基本原理
15.4 DS1302 的硬件信息
15.7 DS1302 的 BURST 模式
14.2 單片機 I2C 尋址模式
14. 單片機 I2C 總線與 EEPROM
13. 單片機 1602 液晶與串口的應(yīng)用實例

16.3 NEC 協(xié)議紅外遙控器

家電遙控器通信距離往往要求不高,而紅外的成本比其它無線設(shè)備要低的多,所以家電遙控器應(yīng)用中紅外始終占據(jù)著一席之地。遙控器的基帶通信協(xié)議很多,大概有幾十種,常用的就有 ITT 協(xié)議、NEC 協(xié)議、Sharp 協(xié)議、Philips RC-5 協(xié)議、Sony SIRC 協(xié)議等。用的最多的就是 NEC 協(xié)議了,因此我們 KST-51 開發(fā)板配套的遙控器直接采用 NEC 協(xié)議,我們這節(jié)課也以 NEC 協(xié)議標準來講解一下。

NEC 協(xié)議的數(shù)據(jù)格式包括了引導(dǎo)碼、用戶碼、用戶碼(或者用戶碼反碼)、按鍵鍵碼和鍵碼反碼,最后一個停止位。停止位主要起隔離作用,一般不進行判斷,編程時我們也不予理會。其中數(shù)據(jù)編碼總共是4個字節(jié)32位,如圖16-7所示。第一個字節(jié)是用戶碼,第二個字節(jié)可能也是用戶碼,或者是用戶碼的反碼,具體由生產(chǎn)商決定,第三個字節(jié)就是當前按鍵的鍵數(shù)據(jù)碼,而第四個字節(jié)是鍵數(shù)據(jù)碼的反碼,可用于對數(shù)據(jù)的糾錯。

http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/30.png" alt="" />

圖16-7 NEC 協(xié)議數(shù)據(jù)格式

這個 NEC 協(xié)議,表示數(shù)據(jù)的方式不像我們之前學(xué)過的比如 UART 那樣直觀,而是每一位數(shù)據(jù)本身也需要進行編碼,編碼后再進行載波調(diào)制。

  • 引導(dǎo)碼:9 ms 的載波 +4.5 ms 的空閑。
  • 比特值“0”:560 us 的載波 +560 us 的空閑。
  • 比特值“1”:560 us 的載波 +1.68 ms 的空閑。

結(jié)合圖16-7我們就能看明白了,最前面黑乎乎的一段,是引導(dǎo)碼的 9 ms 載波,緊接著是引導(dǎo)碼的 4.5 ms 的空閑,而后邊的數(shù)據(jù)碼,是眾多載波和空閑交叉,它們的長短就由其要傳遞的具體數(shù)據(jù)來決定。HS0038B 這個紅外一體化接收頭,當收到有載波的信號的時候,會輸出一個低電平,空閑的時候會輸出高電平,我們用邏輯分析儀抓出來一個紅外按鍵通過HS0038B 解碼后的圖形來了解一下,如圖16-8所示。

http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/31.png" alt="" />

圖16-8 紅外遙控器按鍵編碼

從圖上可以看出,先是 9 ms 載波加 4.5 ms 空閑的起始碼,數(shù)據(jù)碼是低位在前,高位在后,數(shù)據(jù)碼第一個字節(jié)是8組 560 us 的載波加 560 us 的空閑,也就是 0x00,第二個字節(jié)是8組 560 us的載波加 1.68 ms 的空閑,可以看出來是 0xFF,這兩個字節(jié)就是用戶碼和用戶碼的反碼。按鍵的鍵碼二進制是 0x0C,反碼就是 0xF3,最后跟了一個 560 us 載波停止位。對于我們的遙控器來說,不同的按鍵,就是鍵碼和鍵碼反碼的區(qū)分,用戶碼是一樣的。這樣我們就可以通過單片機的程序,把當前的按鍵的鍵碼給解析出來。

我們前邊學(xué)習中斷的時候,學(xué)到51單片機有外部中斷0和外部中斷1這兩個外部中斷。我們的紅外接收引腳接到了 P3.3 引腳上,這個引腳的第二功能就是外部中斷1。在寄存器TCON 中的 bit3 和 bit2 這兩位,是和外部中斷1相關(guān)的兩位。其中 IE1 是外部中斷標志位,當外部中斷發(fā)生后,這一位被自動置1,和定時器中斷標志位 TF 相似,進入中斷后會自動清零,也可以軟件清零。bit2 是設(shè)置外部中斷類型的,如果 bit2 為0,那么只要 P3.3 為低電平就可以觸發(fā)中斷,如果 bit2 為1,那么 P3.3 從高電平到低電平的下降沿發(fā)生才可以觸發(fā)中斷。此外,外部中斷1使能位是 EX1。那下面我們就把程序?qū)懗鰜恚褂脭?shù)碼管把遙控器的用戶碼和鍵碼顯示出來。

Infrared.c 文件主要是用來檢測紅外通信的,當發(fā)生外部中斷后,進入外部中斷,通過定時器1定時,首先對引導(dǎo)碼判斷,而后對數(shù)據(jù)碼的每個位逐位獲取高低電平的時間,從而得知每一位是0還是1,最終把數(shù)據(jù)碼解出來。雖然最終實現(xiàn)的功能很簡單,但因為編碼本身的復(fù)雜性,使得紅外接收的中斷程序在邏輯上顯得就比較復(fù)雜,那么我們首先提供出中斷函數(shù)的程序流程圖,大家可以對照流程圖來理解程序代碼,如圖16-9所示。

http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/32.png" alt="" />

圖16-9 紅外接收程序流程圖

/***************************Infrared.c 文件程序源代碼*****************************/
#include <reg52.h>
sbit IR_INPUT = P3^3; //紅外接收引腳
bit irflag = 0; //紅外接收標志,收到一幀正確數(shù)據(jù)后置 1
unsigned char ircode[4]; //紅外代碼接收緩沖區(qū)

/* 初始化紅外接收功能 */
void InitInfrared(){
    IR_INPUT = 1; //確保紅外接收引腳被釋放
    TMOD &= 0x0F; //清零 T1 的控制位
    TMOD |= 0x10; //配置 T1 為模式 1
    TR1 = 0; //停止 T1 計數(shù)
    ET1 = 0; //禁止 T1 中斷
    IT1 = 1; //設(shè)置 INT1 為負邊沿觸發(fā)
    EX1 = 1; //使能 INT1 中斷
}
/* 獲取當前高電平的持續(xù)時間 */
unsigned int GetHighTime(){
    TH1 = 0; //清零 T1 計數(shù)初值
    TL1 = 0;
    TR1 = 1; //啟動 T1 計數(shù)
    while (IR_INPUT){ //紅外輸入引腳為 1 時循環(huán)檢測等待,變?yōu)?0 時則結(jié)束本循環(huán)
        //當 T1 計數(shù)值大于 0x4000,即高電平持續(xù)時間超過約 18ms 時,
        //強制退出循環(huán),是為了避免信號異常時,程序假死在這里。
        if (TH1 >= 0x40){
            break;
        }
    }
    TR1 = 0; //停止 T1 計數(shù)
    return (TH1*256 + TL1); //T1 計數(shù)值合成為 16bit 整型數(shù),并返回該數(shù)
}
/* 獲取當前低電平的持續(xù)時間 */
unsigned int GetLowTime(){
    TH1 = 0; //清零 T1 計數(shù)初值
    TL1 = 0;
    TR1 = 1; //啟動 T1 計數(shù)
    while (!IR_INPUT){ //紅外輸入引腳為 0 時循環(huán)檢測等待,變?yōu)?1 時則結(jié)束本循環(huán)
        //當 T1 計數(shù)值大于 0x4000,即低電平持續(xù)時間超過約 18ms 時,
        //強制退出循環(huán),是為了避免信號異常時,程序假死在這里。
        if (TH1 >= 0x40){
            break;
        }
    }
    TR1 = 0; //停止 T1 計數(shù)
    return (TH1*256 + TL1); //T1 計數(shù)值合成為 16bit 整型數(shù),并返回該數(shù)
}
/* INT1 中斷服務(wù)函數(shù),執(zhí)行紅外接收及解碼 */
void EXINT1_ISR() interrupt 2{
    unsigned char i, j;
    unsigned char byt;
    unsigned int time;

    //接收并判定引導(dǎo)碼的 9ms 低電平
    time = GetLowTime();
    //時間判定范圍為 8.5~9.5ms,
    //超過此范圍則說明為誤碼,直接退出
    if ((time<7833) || (time>8755)){
        IE1 = 0; //退出前清零 INT1 中斷標志
        return;
    }
    //接收并判定引導(dǎo)碼的 4.5ms 高電平
    time = GetHighTime();
    //時間判定范圍為 4.0~5.0ms,
    //超過此范圍則說明為誤碼,直接退出
    if ((time<3686) || (time>4608)){
        IE1 = 0;
        return;
    }
    //接收并判定后續(xù)的 4 字節(jié)數(shù)據(jù)
    for (i=0; i<4; i++){ //循環(huán)接收 4 個字節(jié)
        for (j=0; j<8; j++){ //循環(huán)接收判定每字節(jié)的 8 個 bit
            //接收判定每 bit 的 560us 低電平
            time = GetLowTime();
            //時間判定范圍為 340~780us,
            //超過此范圍則說明為誤碼,直接退出
            if ((time<313) || (time>718)){
                IE1 = 0;
                return;
            }
            //接收每 bit 高電平時間,判定該 bit 的值
            time = GetHighTime();
            //時間判定范圍為 340~780us,
            //在此范圍內(nèi)說明該 bit 值為 0
            if ((time>313) && (time<718)){
                byt >>= 1; //因低位在先,所以數(shù)據(jù)右移,高位為 0
                //時間判定范圍為 1460~1900us,
                //在此范圍內(nèi)說明該 bit 值為 1
            }else if ((time>1345) && (time<1751)){
                byt >>= 1; //因低位在先,所以數(shù)據(jù)右移,
                byt |= 0x80; //高位置 1
            }else{ //不在上述范圍內(nèi)則說明為誤碼,直接退出
                IE1 = 0;
                return;
            }
        }
        ircode[i] = byt; //接收完一個字節(jié)后保存到緩沖區(qū)
    }
    irflag = 1; //接收完畢后設(shè)置標志
    IE1 = 0; //退出前清零 INT1 中斷標志
}

大家在閱讀這個程序時,會發(fā)現(xiàn)我們在獲取高低電平時間的時候做了超時判斷 if(TH1 >= 0x40),這個超時判斷主要是為了應(yīng)對輸入信號異常(比如意外的干擾等)情況的,如果不做超時判斷,當輸入信號異常時,程序就有可能會一直等待一個無法到來的跳變沿,而造成程序假死。

另外補充一點,遙控器的單按按鍵和持續(xù)按住按鍵發(fā)出來的信號是不同的。我們先來對比一下兩種按鍵方式的實測信號波形,如圖16-10和16-11所示。

http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/33.png" alt="" />

圖16-10 紅外單次按鍵時序圖

http://wiki.jikexueyuan.com/project/mcu-tutorial-three/images/34.png" alt="" />

圖16-11 紅外持續(xù)按鍵時序圖

單次按鍵的結(jié)果16-9和我們之前的圖16-8是一樣的,這個不需要再解釋。而持續(xù)按鍵,首先會發(fā)出一個和單次按鍵一樣的波形出來,經(jīng)過大概 40 ms 后,會產(chǎn)生一個 9 ms 載波加 2.25 ms 空閑,再跟一個停止位的波形,這個叫做重復(fù)碼,而后只要你還在按住按鍵,那么每隔約 108 ms 就會產(chǎn)生一個重復(fù)碼。對于這個重復(fù)碼我們的程序并沒有對它單獨解析,而是直接忽略掉了,這并不影響對正常按鍵數(shù)據(jù)的接收。如果你日后做程序時需要用到這個重復(fù)碼,那么只需要再把對重復(fù)碼的解析添加進來就可以了。

/*****************************main.c 文件程序源代碼******************************/
#include <reg52.h>
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

unsigned char code LedChar[] = { //數(shù)碼管顯示字符轉(zhuǎn)換表
    0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
    0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[6] = { //數(shù)碼管顯示緩沖區(qū)
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
unsigned char T0RH = 0; //T0 重載值的高字節(jié)
unsigned char T0RL = 0; //T0 重載值的低字節(jié)
extern bit irflag;
extern unsigned char ircode[4];
extern void InitInfrared();
void ConfigTimer0(unsigned int ms);

void main(){
    EA = 1; //開總中斷
    ENLED = 0; //使能選擇數(shù)碼管
    ADDR3 = 1;
    InitInfrared(); //初始化紅外功能
    ConfigTimer0(1); //配置 T0 定時 1ms
    //PT0 = 1; //配置 T0 中斷為高優(yōu)先級,啟用本行可消除接收時的閃爍

    while (1){
        if (irflag){ //接收到紅外數(shù)據(jù)時刷新顯示
            irflag = 0;
            LedBuff[5] = LedChar[ircode[0] >> 4]; //用戶碼顯示
            LedBuff[4] = LedChar[ircode[0]&0x0F];
            LedBuff[1] = LedChar[ircode[2] >> 4]; //鍵碼顯示
            LedBuff[0] = LedChar[ircode[2]&0x0F];
        }
    }
}
/* 配置并啟動 T0,ms-T0 定時時間 */
void ConfigTimer0(unsigned int ms){
    unsigned long tmp; //臨時變量
    tmp = 11059200 / 12; //定時器計數(shù)頻率
    tmp = (tmp * ms) / 1000; //計算所需的計數(shù)值
    tmp = 65536 - tmp; //計算定時器重載值
    tmp = tmp + 13; //補償中斷響應(yīng)延時造成的誤差
    T0RH = (unsigned char)(tmp>>8); //定時器重載值拆分為高低字節(jié)
    T0RL = (unsigned char)tmp;

    TMOD &= 0xF0; //清零 T0 的控制位
    TMOD |= 0x01; //配置 T0 為模式 1
    TH0 = T0RH; //加載 T0 重載值
    TL0 = T0RL;
    ET0 = 1; //使能 T0 中斷
    TR0 = 1; //啟動 T0
}
/* LED 動態(tài)掃描刷新函數(shù),需在定時中斷中調(diào)用 */
void LedScan(){
    static unsigned char i = 0; //動態(tài)掃描索引
    P0 = 0xFF; //關(guān)閉所有段選位,顯示消隱
    P1 = (P1 & 0xF8) | i; //位選索引值賦值到 P1 口低 3 位
    P0 = LedBuff[i]; //緩沖區(qū)中索引位置的數(shù)據(jù)送到 P0 口
    if (i < sizeof(LedBuff)-1){ //索引遞增循環(huán),遍歷整個緩沖區(qū)
        i++;
    }else{
        i = 0;
    }
}
/* T0 中斷服務(wù)函數(shù),執(zhí)行數(shù)碼管掃描顯示 */
void InterruptTimer0() interrupt 1{
    TH0 = T0RH; //重新加載重載值
    TL0 = T0RL;
    LedScan(); //數(shù)碼管掃描顯示
}

main.c 文件的主要功能就是把獲取到的紅外遙控器的用戶碼和鍵碼信息,傳送到數(shù)碼管上顯示出來,并且通過定時器 T0 的 1 ms 中斷進行數(shù)碼管的動態(tài)刷新。不知道大家經(jīng)過試驗發(fā)現(xiàn)沒有,當我們按下遙控器按鍵的時候,數(shù)碼管顯示的數(shù)字會閃爍,這是什么原因呢?單片機的程序都是順序執(zhí)行的,一旦我們按下遙控器按鍵,單片機就會進入遙控器解碼的中斷程序內(nèi),而這個程序執(zhí)行的時間又比較長,要幾十個毫秒,而如果數(shù)碼管動態(tài)刷新間隔超過 10 ms 后就會感覺到閃爍,因此這個閃爍是由于程序執(zhí)行紅外解碼時,延誤了數(shù)碼管動態(tài)刷新造成的。

如何解決?前邊我們講過中斷優(yōu)先級問題,如果設(shè)置了中斷的搶占優(yōu)先級,就會產(chǎn)生中斷嵌套。中斷嵌套的原理,我們在前邊講中斷的時候已經(jīng)講過一次了,大家可以回頭再復(fù)習一下。那么這個程序中,有2個中斷程序,一個是外部中斷程序負責接收紅外數(shù)據(jù),一個是定時器中斷程序負責數(shù)碼管掃描,要使紅外接收不耽誤數(shù)碼管掃描的執(zhí)行,那么就必須讓定時器中斷對外部中斷實現(xiàn)嵌套,即把定時器中斷設(shè)置為高搶占優(yōu)先級。定時器中斷程序,執(zhí)行時間只有幾十個 us,即使打斷了紅外接收中斷的執(zhí)行,也最多是給每個位的時間測量附加了幾十 us 的誤差,而這個誤差在最短 560 us 的時間判斷中完全是容許的,所以中斷嵌套并不會影響紅外數(shù)據(jù)的正常接收。在 main 函數(shù)中,大家把這行程序“//PT0 = 1;”的注釋取消,也就是使這行代碼生效,這樣就設(shè)置了 T0 中斷的高搶占優(yōu)先級,再編譯一下,下載到單片機里,然后按鍵試試,是不是沒有任何閃爍了呢?而中斷嵌套的意義也有所體會了吧。