家電遙控器通信距離往往要求不高,而紅外的成本比其它無線設(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)制。
結(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)先級,再編譯一下,下載到單片機里,然后按鍵試試,是不是沒有任何閃爍了呢?而中斷嵌套的意義也有所體會了吧。