微控制器學習(十)紅外遙控與外部中斷

CodeReaper發表於2021-09-02

參考資料:https://www.bilibili.com/video/BV1Mb411e7re?p=37

一、紅外遙控原理

1. 紅外遙控簡介

  • 紅外遙控是利用紅外光進行通訊的裝置,由紅外LED將調製後的訊號發出,由專用的紅外接收頭進行解調輸出

  • 通訊方式:單工,非同步

  • 紅外LED波長:940nm

  • 通訊協議標準:NEC標準

紅外接收頭:

image-20210901185933364

2. 紅外LED和接收頭的硬體電路

LED硬體電路

LED硬體電路有以下兩種:

image-20210902164549625 image-20210902164648477

其中第一種的訊號形式是這樣的:

image-20210902165655105

即LED或者是完全不發光,或者是以38KHz的頻率進行閃爍發出紅外光

而第二種則需要軟體控制IN的輸入。

接收頭硬體電路

image-20210902170126151 image-20210902170156382

接收頭會對接收到的紅外光進行一定的過濾工作,然後輸出到P32引腳上。

3. 基本傳送與接收

紅外LED的三種傳送狀態下接收頭的輸出

  • 空閒狀態:紅外LED不亮,接收頭輸出高電平
  • 傳送低電平:紅外LED以38KHz頻率閃爍發光,接收頭輸出低電平
  • 傳送高電平:紅外LED不亮,接收頭輸出高電平

例如:

image-20210902172813302

4. NEC編碼

紅外接收頭接收到的訊號調製輸出的結果應符合紅外NEC協議

image-20210902170920380

輸出訊號的格式

\[Address+\overline{Address}+Command+\overline{Command} \]

每個部分1個位元組(8位),而第一和第二,第三和第四個部分互為反碼是為了進行資料的校驗

訊號的三種情況

接收到的訊號分為以下三種情況

  1. 資訊頭,圖中的紅色訊號,提示將要傳送訊號
  2. 資訊體,圖中的藍色訊號,真正需要傳輸的內容
  3. 重複訊號,圖中的綠色訊號,代表前面傳送的內容重複傳送

編碼任務

我們的任務就是編寫接收訊號的驅動程式,在程式碼層讀取接收到的訊號,將其轉化為AddressCommand儲存起來(將訊號轉為數字)

我們可以觀察將得到的訊號轉化為對應的編碼的一組例子:

image-20210902171756406

可以發現點選不同的按鍵,接收頭得到的鍵碼是不一樣的。

5. 遙控器的鍵碼

image-20210902172040682

我們可以參考這個圖上的編碼來對遙控器的輸入進行對應的響應。

二、外部中斷

1. 51微控制器的外部中斷資源

  • STC89C52有4個外部中斷

  • STC89C52的外部中斷有兩種觸發方式:

    • 下降沿觸發
    • 低電平觸發
  • 中斷號:
    image-20210902172310992

  • 外部中斷引腳
    image-20210902172532926
    這裡只有兩個引腳:P32P33,所以實際上是只有兩個外部中斷

我們使用下降沿觸發中斷,然後獲取到前後兩個下降沿相距的時間,然後通過這個時間推斷出是訊號頭,還是訊號體(0,1)以及repeat

2. 中斷通路

image-20210902180822614

三、編碼實現

1. 外部中斷的中斷通路配置

按照上面的配置圖,我們可以得到以下的配置:

void Int0_Init() {
    // 修改IT0可以控制是下降沿觸發(1)還是低電平觸發(0)
    IT0 = 1;
    IE0 = 0;
    EX0 = 1;
    EA = 1;
    PX0 = 1;
}

然後我們可以編寫一小段程式測試通路是否配置完成:

unsigned char Num;

void main() {
    LCD_Init();
    // 初始化,配置外部中斷通路
    Int0_Init();
    while (1) {
        LCD_ShowNum(1, 1, Num, 3);
    }
}

// 外部中斷
void Int0_Routine() interrupt 0 {
    Num++;
}

因為我們的獨立按鈕3也是連線在P32引腳上的,因此當我們點選按鈕3時也會觸發外部中斷,故我們可以看到當點選按鈕3時,會發生Num++

image-20210902181902845 hsPpyn.gif

這說明外部中斷配置成功,我們把外部中斷初始化也抽象出一個模組Int0

// Int0.c
void Int0_Init() {
    // 修改IT0可以控制是下降沿觸發(1)還是低電平觸發(0)
    IT0 = 1;
    IE0 = 0;
    EX0 = 1;
    EA = 1;
    PX0 = 1;
}

/*
// 外部中斷
void Int0_Routine() interrupt 0 {
    
}
*/

2. 定時器模組

此時我們需要將Timer0作為我們的計時器,與之前不同的是我們此時不是用它來發起中斷了,而是專門用它來記錄時間

// Timer0.c
void Timer0_Init(void)
{
    TMOD &= 0xF0;  //設定定時器模式
    TMOD |= 0x01;  //設定定時器模式
    TL0 = 0;       //設定定時初值
    TH0 = 0;       //設定定時初值
    TF0 = 0;       //清除TF0標誌
    TR0 = 0;       //定時器0不計時
}
// 設定計數器的值
void Timer0_SetCouter(unsigned int value) {
    TH0 = value / 256;
    TL0 = value % 256;
}
// 獲取當前計數器的值
unsigned int Timer0_GetCounter() {
    return (TH0 << 8) | TL0;
}
// 設定是否開始計時,0為停止,1為開始
void Timer0_Run(unsigned char flag) {
	TR0 = flag;
}

因為我的開發板上的晶振是11.0592MHz的,因此每隔12/(11.0592*10^6)s,即1.085μs計數器即會加一,我們到時需要使用這個時間來計算各個訊號時間需要經過的計數器記錄數

3. 編寫接收器模組

接收器模組需要依賴Timer0模組和Int0模組,首先是Init函式:

void IR_Init() {
    Timer0_Init();
    Int0_Init();
}

我們首先定義一些全域性變數用於接收資訊或標誌資訊:

// 接收時間的變數
unsigned int IR_Time;
// 狀態變數
unsigned char IR_State;

// 儲存4個位元組的陣列
unsigned char IR_Data[4];
// 陣列指標(下標)
unsigned char IR_pData;

// 標誌資料是否讀取完成
unsigned char IR_DataFlag;
// 標誌資料是否讀取到Repeat
unsigned char IR_RepeatFlag;
// 儲存Address資訊
unsigned char IR_Address;
// 儲存Command資訊
unsigned char IR_Command;

然後接收資訊的過程使用一個有限狀態機來完成,這個狀態機一共有三個狀態:

  1. 開始狀態,接收到第一個下降沿開始計時,轉為狀態2
  2. 接收資訊頭狀態,當再接收到一個訊號下降沿時計算經過的時間:
    • 經過時間為9ms+4.5ms=13500μs,判斷為開始訊號,轉為狀態3【接收資訊體狀態】
    • 經過時間為9ms+2.25ms=11250μs,判斷為repeat訊號,轉為狀態1【開始狀態】
    • 否則一直維持狀態2【接收資訊頭狀態】
  3. 接收資訊體狀態,每接收到一個下降沿根據經過的時間判斷是0還是1,並將這些資料儲存起來,當接收完4*8=32個bit之後轉變為狀態1【開始狀態】,若接收到的資料非01(資料異常),則返回狀態2【接收資訊頭狀態】

狀態轉換圖如下圖所示:

image-20210902221424991

因為我們使用的開發板上是11.0592MHz的晶振,故每隔1.085μs計數器會加一,我們需要事先計算出開始訊號,repeat訊號,資訊體中0和1的訊號時間:

  • 訊號頭:9ms+4.5ms=13500μs,對應計時數:13500/1.085≈12442
  • repeat訊號:9ms+2.25ms=11250μs,對應計時數:11250/1.085≈10368
  • 資訊體:
    • 邏輯0:560μs+560μs=1120μs,對應計時數:1120/1.085≈1032
    • 邏輯1:560μs+1690μs=2250μs,對應計時數:2250/1.085≈2074

由此我們可以編寫程式碼:

// 用於判斷是否在輸入是否在compare附近,offset是偏移量
u8 IR_AroundNum(u16 num, u16 compare, u16 offset) {
    return num >= (compare - offset) && num <= (compare + offset);
}
// 外部中斷進入的程式
void Int0_Routine() interrupt 0 {
    // 1.開始狀態
    if (IR_State == 0) {
        Timer0_SetCounter(0);
        Timer0_Run(1);
        IR_State = 1;
    } 
    // 2.接收資訊頭狀態
    else if (IR_State == 1) {
        IR_Time = Timer0_GetCounter();
        Timer0_SetCounter(0);
        // 接收到開始訊號9ms+4.5ms=13500μs
        if (IR_AroundNum(IR_Time, 12442, 500)) {
            P2 = 0;
            IR_State = 2;
        }
        // repeat:9ms+2.25ms=11250μs
        else if (IR_AroundNum(IR_Time, 10368, 500)) {
            IR_RepeatFlag = 1;
            // 以一幀為單位接收,接收完則返回0狀態
            IR_State = 0;
        } 
        else {
            IR_State = 1;
        }
    } 
    // 3.接收資訊體狀態
    else if (IR_State == 2) {
        IR_Time = Timer0_GetCounter();
        Timer0_SetCounter(0);
        // 邏輯0:560μs+560μs=1120μs
        if (IR_AroundNum(IR_Time, 1032, 500)) {
            // 置零
            IR_Data[IR_pData / 8] &= ~(0x01 << (IR_pData % 8));
            IR_pData++;
        }
        // 邏輯1:560μs+1690μs=2250μs
        else if (IR_AroundNum(IR_Time, 2074, 500)) {
            // 置一
            IR_Data[IR_pData / 8] |= (0x01 << (IR_pData % 8));
            IR_pData++;
        }
        // 接收到異常訊號轉為接收資訊頭狀態
        else {
            IR_pData = 0;
            IR_State = 1;
        }
        // 判斷是否接收完32bit
        if(IR_pData>=32) {
            IR_pData = 0;
            // 資料校驗
            if((IR_Data[0]==~IR_Data[1]) && (IR_Data[2]==~IR_Data[3])) {
                // 轉存到IR_Address和IR_Command變數中
                IR_Address = IR_Data[0];
                IR_Command = IR_Data[2];
                IR_DataFlag = 1;
            }
            Timer0_Run(0);
            IR_State = 0;
        }
    }
}

程式碼中使用if-else條件判斷來實現有限狀態機,其實是一種耦合度比較高的實現方式,但是由於c語言難以實現物件導向,就只好暫時使用這樣的實現了。

然後這個模組再暴露出去一些獲取標誌變數接收到的資料的函式:

u8 IR_GetDataFlag() {
    if(IR_DataFlag) {
        IR_DataFlag = 0;
        return 1;
    }
    return 0;
}

u8 IR_GetRepeat() {
    if(IR_RepeatFlag) {
        IR_RepeatFlag = 0;
        return 1;
    }
    return 0;
}

u8 IR_GetAddress() {
    return IR_Address;
}

u8 IR_GetCommand() {
    return IR_Command;
}

然後在main.c中編寫程式測試一下功能:

unsigned char Num;
unsigned char Address;
unsigned char Command;

void main() {
    LCD_Init();
    IR_Init();
    
    LCD_ShowString(1,1,"ADDR");
    LCD_ShowString(1,7,"CMD");
    LCD_ShowString(1,12,"NUM");

    while (1) {
        if(IR_GetDataFlag() || IR_GetRepeat()) {
            Address = IR_GetAddress();
            Command = IR_GetCommand();
            LCD_ShowHexNum(2,1,Address,2);
            LCD_ShowHexNum(2,7,Command,2);
            // 0x15是VOL-
            if(Command == 0x15) {
                Num--;
            }
            // 0x09是VOL+
            else if(Command == 0x09) {
                Num++;
            }
            LCD_ShowHexNum(2,12,Num,3);
        }
    }
}

執行的結果是我們點選一個按鍵,在LCD1602中就會顯示出對應的鍵碼Address值,此外當我們點選VOL+VOL-時展示的Num也會發生變化:

hssYX8.gif

為了使用方便,我們再在IR.h中使用巨集定義定義一些按鍵:

#define IR_POWER        0x45
#define IR_MODE         0x46
#define IR_START_STOP   0x44
#define IR_PREVIOUS     0x40
#define IR_NEXT         0x43
#define IR_EQ           0x07
#define IR_VOL_MINUS    0x15
#define IR_VOL_ADD      0x09
#define IR_VOL_ADD      0x09
#define IR_RPT          0x19
#define IR_USD          0x0D
#define IR_NUM_0        0x16
#define IR_NUM_1        0x0C
#define IR_NUM_2        0x18
#define IR_NUM_3        0x5E
#define IR_NUM_4        0x08
#define IR_NUM_5        0x1C
#define IR_NUM_6        0x5A
#define IR_NUM_7        0x42
#define IR_NUM_8        0x52
#define IR_NUM_9        0x4A

這樣我們即可在外部輕鬆呼叫接收器模組獲取資訊了。

4. 使用紅外遙控控制電機運轉

鑑於定時器0被紅外接收模組用於計時了,因此若我們希望使用定時器實現電機的PWM調速,就只能使用定時器1了,模仿定時器0,我們很容易寫出對應的模組程式碼:

// Timer1.c
void Timer1Init(void)//100微秒@11.0592MHz
{
    TMOD &= 0x0F;//設定定時器模式
    TMOD |= 0x10;//設定定時器模式
    TL1 = 0xA4;  //設定定時初值
    TH1 = 0xFF;  //設定定時初值
    TF1 = 0;     //清除TF1標誌
    TR1 = 1;     //定時器1開始計時
    ET1 = 1;
    EA = 1;
    PT1 = 0;
}

/*定時器中斷函式模板
void Timer1_Routine() interrupt 3
{
	static unsigned int T1Count;
	TL1 = 0xA4;  //設定定時初值
    TH1 = 0xFF;  //設定定時初值
	T1Count++;
	if(T1Count>=1000)
	{
		T1Count=0;
		
	}
}
*/

然後我們需要編寫電機模組:

sbit Motor = P1 ^ 0;
unsigned char Counter = 0, Compare = 0;

void Motor_Init() {
    Timer1Init();
}
// 修改Compare進行調速
void Motor_SetPower(unsigned char power) {
    if (power < 0) {
        power = 0;
    } 
    else if (power > 100) {
        power = 100;
    }
    Compare = power;
}
// 使用風力等級進行調速,一共0~3四級
void Motor_SetPowerByLevelNum(unsigned char level) {
    if(level == 0) {
        Motor_SetPower(0);
    }
    else if(level == 1) {
        Motor_SetPower(60);
    }
    else if(level == 2) {
        Motor_SetPower(80);
    }
    else if(level == 3) {
        Motor_SetPower(100);
    }
}
// PWM調速
void Timer1_Routine() interrupt 3 {
    TL1 = 0xA4;//設定定時初值
    TH1 = 0xFF;//設定定時初值
    Counter++;
    Counter %= 100;
    if (Counter < Compare) {
        Motor = 1;
    } else {
        Motor = 0;
    }
}

main.c:

unsigned char Command;
unsigned char FanState;
unsigned char FanPower;

// 電機兩個狀態,關閉狀態和開啟狀態
#define POWER_OFF 0
#define POWER_ON 1

void main() {
    LCD_Init();
    IR_Init();
    Motor_Init();
    
	// 初始是關閉狀態
    LCD_ShowString(1, 1, "Fan State:");
    LCD_ShowString(1, 11, "OFF");
    FanState = POWER_OFF;
    FanPower = 0;// 初始風力等級為0

    while (1) {
        if (IR_GetDataFlag()) {
            Command = IR_GetCommand();
            // 關閉狀態
            if (FanState == POWER_OFF) {
                // 點選POWER鍵後電機轉為開啟狀態
                if (Command == IR_POWER) {
                    FanState = POWER_ON;
                    // 預設開啟時風力為1級
                    FanPower = 1;
                    LCD_ShowString(1, 11, "ON ");
                    LCD_ShowString(2, 1, "Power:");
                    LCD_ShowNum(2, 7, FanPower, 1);
                }
            } 
            // 開啟狀態
            else if (FanState == POWER_ON) {
                // 點選POWER鍵後電機轉為關閉狀態
                if (Command == IR_POWER) {
                    FanState = POWER_OFF;
                    FanPower = 0;
                    LCD_ShowString(1, 11, "OFF");
                    LCD_ShowString(2, 1, "       ");
                } 
                // 點選按鈕1轉為1擋
                else if (Command == IR_NUM_1) {
                    FanPower = 1;
                    LCD_ShowNum(2, 7, FanPower, 1);
                }
                // 點選按鈕2轉為2擋
                else if (Command == IR_NUM_2) {
                    FanPower = 2;
                    LCD_ShowNum(2, 7, FanPower, 1);
                }
                // 點選按鈕3轉為3擋
                else if (Command == IR_NUM_3) {
                    FanPower = 3;
                    LCD_ShowNum(2, 7, FanPower, 1);
                }
            }
            // 設定電機檔位
            Motor_SetPowerByLevelNum(FanPower);
        }
    }
}

執行效果:

hsy6PA.gif

相關文章