微控制器學習(九)定時器掃描按鈕和數碼管與PWM的使用

CodeReaper發表於2021-09-01

一、使用定時器掃描按鈕和數碼管

1. 使用定時器進行掃描的緣由

之前掃描按鈕和數碼管都是需要通過CPU主迴圈進行的,使用這種方式有著很大的弊端,(1)首先是會佔用CPU的資源,在掃描按鈕和數碼管時會浪費一定的時間(2)其次是我們的按鈕檢測是通過鬆手檢測進行的,當我們按下按鈕還沒有鬆開時,程式即會進入長時間的while迴圈中,無法完成其他的操作,必須要鬆手後才能釋放CPU資源完成其他的功能。因此使用定時器代替CPU進行掃描和檢測是非常必要的。

2. 定時器掃描獨立按鈕

原來的獨立按鈕相關程式碼:

// Button.c
unsigned int ButtonKey() {
    unsigned int res = 0;
    if(P3_1 == 0)       {res = 1;deley(20);while (P3_1 ==0);deley(20);}
    else if (P3_0 == 0) {res = 2;deley(20);while (P3_0 ==0);deley(20);}
    else if (P3_2 == 0) {res = 3;deley(20);while (P3_2 ==0);deley(20);}
    else if (P3_3 == 0) {res = 4;deley(20);while (P3_3 ==0);deley(20);}
    return res;
}

可以發現我們之前需要使用deley(20)進行扭動消抖,然後需要使用while迴圈進行等待鬆手,最後再使用deley(20)進行鬆手消抖,而在這等待鬆手的過程中,其他的器件如數碼管等都無法正常執行了,造成很大的不便。

然後我們現在的想法是:使用計時器每隔20ms獲取一次當前按鈕所在暫存器bit的狀態,如下圖所示

image-20210827211242213

當發現當前電平為1,而上一次電平為0時,則判斷按鈕按動鬆手,再進行其他相應的操作。

現在我們先去掉這些deleywhile

unsigned int Button_GetState() {
    unsigned int res = 0;
    if(P3_1 == 0)       { res = 1; }
    else if (P3_0 == 0) { res = 2; }
    else if (P3_2 == 0) { res = 3; }
    else if (P3_3 == 0) { res = 4; }
    return res;
}

然後定義一個Loop函式供外部定時器呼叫,在main.c中它是這樣的:

void Timer0_Routine() interrupt 1 {
    static unsigned int T0Count;
    TL0 = 0x18;
    TH0 = 0xFC;
    T0Count++;
    if (T0Count >= 20) {
        T0Count = 0;
        Button_Loop();	// <--在這裡不斷呼叫Button_Loop()函式
    }
}

即我們使用定時器每隔20ms就呼叫一次獨立按鈕模組的Button_Loop()函式,而在這個函式中我們通過當前電平和上一次電平的關係來判斷是哪個按鈕按下並鬆手(即key.released):

void Button_Loop() {
    static unsigned char nowKey, lastKey;
    unsigned char i = 0;
    lastKey = nowKey;
    nowKey = Button_GetState();
    for(i=1;i<=4;i++) {
        // 判斷按下鬆手,如果是則更新currentKey
        if(nowKey == 0 && lastKey == i) {
            currentKey = i;
            return;
        }
    }
}

然後我們還需要編寫獲取當前鬆手的key的函式:

unsigned char currentKey;
unsigned char Button_GetKey() {
    unsigned char tmp;
    tmp = currentKey;
    currentKey = 0;
    return tmp;
}

這裡使用tmp中間變數是為了方便給currentKey重新置零。

我們可以使用LED進行相應的測試:

void main() {
    unsigned char key;
    Timer0_Init();
    while (1) {
        key = Button_GetKey();
        if(key) {
            if(key == 1) { P2_0 = ~P2_0; }
            if(key == 2) { P2_1 = ~P2_1; }
        }
    }
}

測試結果是我們點選第一個和第二個按鈕可以分別控制第一個和第二個LED燈的亮和滅。

最後為了方便客戶端(main.c)呼叫,為獨立按鍵模組新增定時器呼叫函式Button_Routine()

void Button_Routine() {
    static unsigned int btn_Count;
    btn_Count++;
    if (btn_Count >= 20) {
        btn_Count = 0;
        Button_Loop();
    }
}

然後在main.c中的定時器程式只需要簡單呼叫即可:

void Timer0_Routine() interrupt 1 {
    TL0 = 0x18;
    TH0 = 0xFC;
    Button_Routine(); // <--
}

3. 定時器掃描數碼管

原來的數碼管核心程式碼:

// 段碼
u8 code smgduan[16]={
    0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07,
    0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71            
};
// 位選(位使能)
void enableIndexLED(u8 index) {
    P2 = P2 & ~(0x07 << 2);
    P2 = P2 | (0x7 - index)<<2;
}

void displayOneNum(u8 index, u8 num) {
    // 位選
    enableIndexLED(index);
    // 段選
    P0 = smgduan[num];
    // 延時
    deley(2);
    // 段清零
    P0 = 0x00;
}

然後我們調換一下各個操作的順序,並移除掉deley,就得到了展示單個數碼管的函式:

void Nixie_DisplayOnePos(u8 index, u8 duanValue) {
    // 段清零
    P0 = 0x00;
    // 位選
    Nixie_enableIndexLED(index);
    // 段選
    P0 = duanValue;
}

然後我們需要做的和按鈕的檢測一樣,每隔一段時間呼叫一次相關的函式,我們這裡設計每呼叫一次就展示其中的一位:例如第一次展示第一位數字,第二次展示第二位數字...以此類推。這樣我們就可以實現定時器掃描數碼管了。

而至於每位應該展示什麼樣的內容,我們使用一個8個元素的陣列進行儲存

#define NOT_DISPLAY 0
// 用於快取顯示的內容
static u16 Nixie_Buf[8] = {NOT_DISPLAY, NOT_DISPLAY, NOT_DISPLAY, NOT_DISPLAY,
                           NOT_DISPLAY, NOT_DISPLAY, NOT_DISPLAY, NOT_DISPLAY};

然後在迴圈呼叫的函式中將快取中的內容展示出來:

void Nixie_Loop() {
    static u8 curIndex = 0;
    Nixie_DisplayOnePos(curIndex, Nixie_Buf[curIndex]);
    
    curIndex++;
    if (curIndex >= 8) {
        curIndex = 0;
    }
}

與獨立按鍵模組同理,我們設定生命週期函式方便中斷程式的呼叫:

void Nixie_Routine() {
    static unsigned int nixie_Count;
    nixie_Count++;
    if (nixie_Count >= 2) {
        nixie_Count = 0;
        Nixie_Loop(); // <--每隔2個單位時間呼叫一次,當前單位時間為1ms
    }
}

而當我們希望修改顯示的內容,我們修改Nixie_Buf快取陣列中的內容即可,故我們對外暴露幾個函式方便外部使用:

// 直接使用段碼設定Buffer中的元素
void Nixie_SetBufWithDuan(u8 index, u8 duan) {
    Nixie_Buf[index] = duan;
}
// 置空某元素
void Nixie_SetBlank(u8 index) {
    Nixie_SetBufWithDuan(index, NOT_DISPLAY);
}
// 設定顯示為某數字(即設定某元素的值為數字對應的段碼)
void Nixie_SetBufWithNum(u8 index, u8 num) {
    Nixie_SetBufWithDuan(index, smgduan[num]);
}
// 展示多位的數字
void Nixie_ShowNum(u16 num) {
    u8 i = 7;
    if (num == 0) {
        Nixie_SetBufWithNum(i, num);
        i--;
    }
    while (num) {
        Nixie_SetBufWithNum(i, num % 10);
        num /= 10;
        i--;
    }
    // 清空前面的內容
    i++;
    while (1) {
        i--;
        Nixie_SetBlank(i);
        if (i == 0)
            break;
    }
}

main.c中進行測試:

void main() {
    unsigned int cnt = 0;
    Timer0_Init();
    while(1) {
        Nixie_ShowNum(cnt);
        defaultDeley();
        cnt++;
    }
}

void Timer0_Routine() interrupt 1 {
    TL0 = 0x18;
    TH0 = 0xFC;
    Key_Routine();
    Nixie_Routine();
}

執行結果:數碼管展示的數字從0開始不斷遞增。

二、PWM的使用

1. PWM簡介

  • PWM (Pulse Width Modulation)脈衝寬度調製,在具有慣性的系統中,可以通過對一系列脈衝的寬度進行調製,來等效地獲得所需要的模擬參量,常應用於電機控速開關電源等領域
  • PWM重要引數:
    • 頻率=1/Ts
    • 佔空比= Ton/Ts
    • 精度=佔空比變化步距
image-20210831231814085

例如我們希望電機使用相對最大功率50%的功率進行旋轉,則我們可以設定 TON=TOFF達到該效果,其他的功率也是一樣的原理。

2. LED呼吸燈

按照我們上面關於PWM的介紹,若我們希望LED以一半的亮度工作,我們可以編寫如下程式碼:

sbit LED = P2^0;

void main() {
    while (1) {
        LED = 0;
        LED = 1;
    }
}

若我們增長LED滅的時間:

void main() {
    while (1) {
        LED = 0;
        LED = 1;
        LED = 1;
        LED = 1;
        LED = 1;
        LED = 1;
        LED = 1;
        LED = 1;
    }
}

此時我們會發現LED的亮度變暗了許多

這驗證了我們可以通過改變PWM佔空比控制LED的亮度

實現一

按照這個原理,我們可以這樣實現呼吸燈:

sbit LED = P2 ^ 0;

void LED_light(int brightness) {
    unsigned char times;
    for (times = 0; times < 10; times++) {
        LED = 0;
        deley(brightness);
        LED = 1;
        deley(100 - brightness);
    }
}

void main() {
    int brightness = 0;
    while (1) {
        for (brightness = 0; brightness <= 100; brightness++) {
            LED_light(brightness);
        }
        for (brightness = 100; brightness >= 0; brightness--) {
            LED_light(brightness);
        }
    }
}

執行效果:

hd7QXt.gif

實現二

我們還可以使用定時器來實現PWM

原理圖如下:

image-20210901001401973

計數值隨著時間的推移進行變化,即按照上圖中的先勻速增加到最大值,然後再返回到最小值繼續開始遞增。然後通過判斷計數值和比較值的關係來輸出0或1,最後我們只需要設定比較值的大小即可輕鬆設定佔空比了。

先進行定時器的設定,這裡選取100μs作為定時長度:

image-20210901002241750

然後我們在main.c中不斷切換佔空比的值即可實現呼吸燈效果了:

unsigned char counter=0, compare;

void main() {
    Timer0_Init();
    while(1) {
        // 不斷修改比較值切換佔空比
        for(compare = 0; compare <= 100; compare++) {
            deley(500);
        }
        for(compare = 100; compare != 255; compare--) {
            deley(500);
        }
    }
}

void Timer0_Routine() interrupt 1 {
    TL0 = 0x9C;	//設定定時初值
    TH0 = 0xFF;	//設定定時初值

    counter++;	  //計數器自增
    counter%=100; //達到最大時清零
    if(counter < compare) {	//計數器小於比較值輸出0
        LED = 0;
    }
    else {			//計數器大於等於比較值輸出1
        LED = 1;
    }
}

3. 按鈕控制LED亮度和電機轉速

按照上面的LED呼吸燈實現二的原理,加入獨立按鍵模組和LED數碼管模組,我們很容易就可以實現對LED亮度調整的程式:

void main() {
    unsigned char key;
    Timer0_Init();
    Nixie_SetNum(0, 0);
    while (1) {
        key = Key();
        if (key) {
            Nixie_SetNum(0, key);
            if (key == 1) {
                compare = 20;
            }
            if (key == 2) {
                compare = 50;
            }
            if (key == 3) {
                compare = 100;
            }
        }
    }
}

void Timer0_Routine() interrupt 1 {
    TL0 = 0x9C;//設定定時初值
    TH0 = 0xFF;//設定定時初值

    Key_Routine();
    Nixie_Routine();
    counter++;
    counter %= 100;
    if (counter < compare) {
        LED = 0;
    } else {
        LED = 1;
    }
}

同理,我們可以對電機進行調速:

sbit Motor = P1 ^ 0;

unsigned char counter = 0, compare;

void main() {
    unsigned char key, speed = 0;
    Timer0_Init();
    Nixie_SetNum(0, speed);
    while (1) {
        key = Key();
        if (key) {
            if (key == 1) {
                speed++;
                speed%=4;
                Nixie_SetNum(0, speed);
                if(speed == 0) {compare = 0; }
                if(speed == 1) { compare = 50; }
                if(speed == 2) { compare = 70; }
                if(speed == 3) { compare = 100; }
            }
        }
    }
}

void Timer0_Routine() interrupt 1 {
    TL0 = 0x9C;//設定定時初值
    TH0 = 0xFF;//設定定時初值

    Key_Routine();
    Nixie_Routine();
    counter++;
    counter %= 100;
    if (counter < compare) {
        Motor = 1;
    } else {
        Motor = 0;
    }
}

tips:電機連線在P10口和GND

相關文章