2020年11月21日 星期六

Arduino教學:LED閃爍

作者:王一哲
日期:2018/3/12



前言


如果完全沒有寫過 Arduino 程式,請從頭開始看;如果已經寫過一些 Arduino 的小程式,建議從程式4開始閱讀即可。以下共有 5 個程式,目標分別為
  1. 程式1:使用 delay 控制 LED 的閃爍週期
  2. 程式2:使用 millis 控制 1 個 LED 的閃爍週期
  3. 程式3:使用 millis 使 2 個 LED 有不同的時間閃爍週期
  4. 程式4:使用可變電阻控制 1 個 LED 的閃爍週期
  5. 程式5:使用可變電阻控制 2 個 LED 的閃爍週期

所需器材


  1. Arduino Uno 開發板1塊
  2. 3 mm 或 5 mm 紅光 LED 1個
  3. 3 mm 或 5 mm 綠光 LED 1個
  4. 220 Ω 固定電阻2個(串聯 LED 用)
  5. 10 kΩ 可變電阻2個
  6. 麵包板 1 塊
  7. 麵包板用連接線數條

程式1:使用 delay 控制 LED 的閃爍週期


程式1、2裝置照片(程式1只需用到左側)

程式1、2電路圖(程式1只需用到左側)

/* 實驗1: 利用 delay 使 LED 閃爍                   *
 * 日期: 2018/3/6                                 *
 * 作者: 王一哲                                    */
#define LED 7 //定義LED接腳
int delaytime = 500;

void setup() {
    pinMode(LED, OUTPUT); //定義LED接腳為輸出
    Serial.begin(9600);
}

void loop() {
    digitalWrite(LED, HIGH);
    delay(delaytime);
    digitalWrite(LED, LOW);
    delay(delaytime);
}

如果將程式碼用 Arduino IDE 的編輯器開啟,在預設狀態下看到的顏色應該會很類似上面的樣子,編輯器會自動將在 Arduino 中有特殊功能的保留字、引號內的文字、註解等用不同的顏色標示出來,這樣能便於閱讀程式碼。Arduino 有兩種註解方釋
  1. 多行註解:於 /* 及 */ 之間的文字
  2. 單行註解:於 // 之後到該行結束為止的文字
編譯器在執行程式碼時會忽略註解的部分,雖然註解對程式運作沒有幫助,但是對使用者而言非常重要,如果沒有寫註解,很有可能連作者本人在過了幾天之後也忘記自己在寫什麼,更不用說要讓其他使用者看懂程式碼,所以請務必養成寫註解的習慣。我習慣在一開始先用多行註解寫清楚程式的名稱、功能、日期及作者。
整個程式大約可以分成三個部分:
  1. 參數設定
  2. void setup(),只有在 Arduino 板子重置之後執行一次
  3. void loop(),不斷重複執行,主程式

參數設定


在參數設定部分,我習慣將一些在程式常中會不斷用到的數值指定到對應的變數或參數中,並幫這些變數或參數取一些容易看懂的名稱。在 Arduino 中可以用英文字母、數字及底線作為名稱,其中英文字母區分大小寫,變數名稱開頭不能為數字,不能使用系統保留字。理論上可以按照自己的喜好命名,但為了便於理解它們的用途,最好使用有意義的名稱,例如將控制 LED 閃爍頻率的延遲時間取名為 delaytime。 在此我用了兩種不同的寫法,第一種是
#define [名稱] [數字]
專有名詞為前置處理器,當編譯器讀到指定的名稱時會自動用對應的數字代入,習慣上名稱通常全為大寫字母,我通常用來指定接腳的名稱及編號。例如:
#define LED 7
是將連接 LED 的接腳指定為 7。

第二種是設定變數,目前只用到一種變數:帶有正負號的整數 int,格式為
int [名稱];
int [名稱] = [數值];
可以先指定變數名稱但沒有指定數值,或是同時指定變數名稱和數值,同時記得在行末加上分號。例如:
int delaytime = 500;
定義名為 delaytime 的變數,種類為 int,數值為 500。


void setup()


開頭的 void 代表這段程式碼沒有回傳值,setup() 括號內沒有東西,代表不需要指定參數,兩個大括號內的程式碼為 void setup() 會執行的部分,通常用來定義接腳模式以及開啟開發板及電腦之間的 USB 通訊。

第一行用到函式 pinMode,格式為
pinMode([接腳編號], [INPUT or OUTPUT]);
請特號注意字母的大小寫。在 Arduino 中的習慣,如果是函式則開頭字母為小寫,如果函式為多個英文單字組合而成,從第二個單字開始,單字開頭的字母為大寫。代表輸入、輸出的狀態的 INPUT 及 OUTPUT 則全為大寫。因此
pinMode(LED, OUTPUT);
會將 LED 也就是 7 號接腳設定為輸出。

第二行用到函式 Serial.begin,格式為
Serial.begin([鮑率]);
鮑率(baud)是指每秒幾位元。Serial.begin 用來開啟開發板及電腦間的 USB 連線,並設定每秒傳輸的資料為幾個位元,如果沒有特殊的需求基本上設定為 9600 即可。


void loop()


Arduino 會不斷地執行 loop() 大括弧中的程式碼,所以這裡為主程式,通常也是花最多時間寫的部分。

這裡用到的第一個函式為digitalWrite,格式為
digitalWrite([接腳編號], [電位值]);
將電壓值寫入指定的接腳編號,由於是數位值,所以只有高(HIGH)或低(LOW)兩種。

用到的第二個函式為delay,格式為
delay([時間]);
延遲(暫停執行程式)一段時間,時間的單位為毫秒(ms)。

因此這整段程式碼會先將 LED 接腳設定為高電位,延遲 500 ms 後,再將 LED 接腳設定為低電位,再延遲 500 ms 後,不斷重複以上動作,故 LED 閃爍的週期為 1 s。


程式2:使用 millis 控制 1 個 LED 的閃爍週期


裝置照片及電路圖與程式1相同。

/* 實驗2: 利用 millis 使1個 LED 閃爍              *
 * 日期: 2018/3/6                                *
 * 作者: 王一哲                                   */

#define LED 7 //定義LED接腳
const int dt = 1000/2;
unsigned long tp = 0;
unsigned long tc = 0;
bool ledState = LOW;

void setup() {
    pinMode(LED, OUTPUT); //定義LED接腳為輸出
    Serial.begin(9600);
}

void loop() {
    tc = millis();
    if(tc - tp >= dt) {
        tp = tc;
        Serial.println(tc);
        if(ledState == LOW) {
            ledState = HIGH;
        } else {
            ledState = LOW;
        }
        //ledState = !ledState;
        Serial.println(ledState);
        digitalWrite(LED, ledState);
    }
}

使用 delay 雖然可以達到使 LED 閃爍的效果,但是 delay 會將整個程式停住,就沒有辦法同時執行別的部分,因此在程式2中我們改用 millis 讀取系統時間,並用 if 讓程式在符合我們設定的時間間隔時改變 LED 的明暗狀態。接下來只針對和程式1不同的部分講解。


參數設定


首先,延遲時間 dt 的格式改用 const int,這是因為我們在這個程式中不會改變 dt,所以將它設為常數。
const int dt = 1000/2;

為了計算經過的時間,在此設定了兩個變數 tp 和 tc,預設值皆為0,格式為不帶正負號的長整數 unsigned long,因為程式運行經過的時間只有正值,所以不需要加上正負號,可以儲存到較長的時間值。
unsigned long tp = 0;
unsigned long tc = 0;

設定 LED 的狀態,取名為 ledState,預設值為 LOW,格式為布林 bool。在 Arduino 中,true = HIGH = 1,false = LOW = 0。
bool ledState = LOW;



void setup()


這部分和程式1一模一樣。


void loop()


用函式 millis 讀取程式開始執行後經過的時間,單位為 ms,將讀取到的數值指定給 tc。
tc = millis();  

當 tc - tp 大於、等於 dt 時,執行大括弧中的程式碼,先將現在的 tc 數值指定給 tp,再用 Serial.println(tc) 將 tc 的數值藉申序列埠監控視窗印出來,這是為了除錯用的,可以註解掉沒關係。第一次執行時,由於 tp == 0,當條件成立時,代表程式運行時間大於、等於設定的延遲時間 dt,要改變 LED 的狀態,並且將此時的 tc 數值(現在的時刻)存到 tp 當中。等到程式經過的時間 - 第一次執行 if 的時間再次大於、等於 dt 時,再執行一次 if 大括弧中的程式碼。
if(tc - tp >= dt){
    ...
}

大括弧中的程式碼如下,效果是當 ledState 為 LOW 時,將 ledState 改為 HIGH;反之則改為 LOW。
if(ledState == LOW) {
    ledState = HIGH;
} else {
    ledState = LOW;
}
這部分可以用以下的程式碼代替
ledState = !ledState;
其中驚嘆號 ! 代表邏輯運算中的 not。這行程式碼會將 ledState 的值取 not 後再指定給 ledState,也就是將 ledState 由 LOW 變成 HIGH 或是由 HIGH 變成 LOW。


程式3:利用millis使2個LED有不同的閃爍週期


裝置照片及電路圖與程式1相同。

/* 實驗3: 利用millis使2個LED有不同的時間閃爍週期   *
 * 日期: 2018/3/6                                *
 * 作者: 王一哲                                   */

#define LED1 7 //定義LED1接腳
#define LED2 8 //定義LED2接腳
const int dt1 = 1000/2; // LED1閃爍週期為 1000 ms
const int dt2 = 1400/2; // LED2閃爍週期為 1400 ms
unsigned long tp1 = 0; // 以下4個為計算閃爍時間用的變數
unsigned long tc1 = 0;
unsigned long tp2 = 0;
unsigned long tc2 = 0;
bool ledState1 = LOW; // LED狀態
bool ledState2 = LOW;

void setup() {
    pinMode(LED1, OUTPUT); //定義LED接腳為輸出
    pinMode(LED2, OUTPUT); //定義LED接腳為輸出
    Serial.begin(9600);
}

void loop() {
    tc1 = millis();
    tc2 = millis();
// 當 tc1 - tp1 >= dt1 時改變 LED1 的狀態
    if(tc1 - tp1 >= dt1) {
        Serial.println(tc1);        
        tp1 = tc1;
        ledState1 = !ledState1;
        digitalWrite(LED1, ledState1);
    }
// 當 tc2 - tp2 >= dt2 時改變 LED2 的狀態
    if(tc2 - tp2 >= dt2) {
        Serial.println(tc2);        
        tp2 = tc2;
        ledState2 = !ledState2;
        digitalWrite(LED2, ledState2);
    }
}

其實就是將程式2複製、貼上,多寫一組控制 LED2 的程式碼,沒有用到任何新的東西。


程式4:使用可變電阻控制 1 個 LED 的閃爍週期


程式4裝置照片

程式4電路圖

/* 實驗4: 使用可變電阻控制 1 個 LED 的閃爍週期     *
 * 日期: 2018/3/12                               *
 * 作者: 王一哲                                   */

#define LED 7               // 定義LED接腳
#define POT A0              // 定義可變電阻讀取接腳
unsigned int dt;            // 延遲時間
unsigned long tmin = 25;    // 週期最小值
unsigned long tmax = 1000;  // 週期最大值
int val;                    // 可變電阻讀取值
unsigned long tp = 0;      // 計算經過時間用的變數
unsigned long tc = 0;
bool ledState = LOW;       // LED 狀態

void setup() {
    pinMode(LED, OUTPUT); //定義LED接腳為輸出
    pinMode(POT, INPUT);  //定義可變電阻接腳為輸出, 可省略
    Serial.begin(9600);
}

void loop() {
    val = analogRead(POT); // val = 0 ~ 1023
    dt = map(val, 0, 1023, tmin, tmax) / 2;
    Serial.println(dt);
    tc = millis();
    if(tc - tp >= dt) {
        tp = tc;
        Serial.println(tc);
        ledState = !ledState;
        Serial.println(ledState);
        digitalWrite(LED, ledState);
    }
}

這是將程式2稍加修改之後得到的結果,我們加上了一個可變電阻,可變電阻的端電壓為 5V,利用類比輸入讀取中間接電腳的電壓值,將電壓值轉換為 LED 閃爍的週期。


參數設定


多出來的部分為
#define POT A0
定義可變電阻讀取接腳 POT 為 A0。

unsigned long tmin = 25;
週期最小值為 25 ms。

unsigned long tmax = 1000;
週期最大值為 1000 ms。

int val;
存放可變電阻讀取值的變數。


void setup()


多出來的部分為
pinMode(POT, INPUT);
定義可變電阻讀取接腳 POT 為輸入,其實這一行可以省略,因為 alalog in 接腳預設狀態即為輸入。


void loop()


多出來的部分為
val = analogRead(POT);
用函式 analogRead 由 POT 接腳讀取類比輸入值,範圍為 0 ~ 1023,可變電阻中間的接腳和 GND 之間的電壓若為 0V 則對應到的值為 0,電壓若為 5V 則對應到的值為 1023。

dt = map(val, 0, 1023, tmin, tmax) / 2;
用函式 map 將 val 轉換為延遲時間 dt。map 的格式為
map([輸入值], [輸入值最小值], [輸入值最大值], [轉換後最小值], [轉換後最大值])



程式5:使用可變電阻控制 2 個 LED 的閃爍週期


程式5裝置照片

程式5電路圖

/* 實驗5: 使用可變電阻控制 2 個 LED 的閃爍週期     *
 * 日期: 2018/3/12                               *
 * 作者: 王一哲                                   */

#define LED1 7               // 定義LED1接腳
#define LED2 8               // 定義LED2接腳
#define POT1 A0              // 定義可變電阻1讀取接腳
#define POT2 A1              // 定義可變電阻2讀取接腳
unsigned int dt1;            // 延遲時間
unsigned int dt2;
unsigned long tmin = 25;    // 週期最小值
unsigned long tmax = 1000;  // 週期最大值
int v1, 2;                  // 可變電阻讀取值
unsigned long tp1 = 0;      // 計算經過時間用的變數
unsigned long tc1 = 0;
unsigned long tp2 = 0;
unsigned long tc2 = 0;
bool ledState1 = LOW;       // LED 狀態
bool ledState2 = LOW;

void setup() {
    pinMode(LED1, OUTPUT); //定義LED接腳為輸出
    pinMode(LED2, OUTPUT);
    pinMode(POT1, INPUT);  //定義可變電阻接腳為輸入, 可省略
    pinMode(POT2, INPUT);
    Serial.begin(9600);
}

void loop() {
    v1 = analogRead(POT1);
    v2 = analogRead(POT2);
    dt1 = map(v1, 0, 1023, tmin, tmax) / 2;
    dt2 = map(v2, 0, 1023, tmin, tmax) / 2;
    Serial.println(dt1);
    Serial.println(dt2);
    tc1 = millis();
    tc2 = millis();
// 當 tc1 - tp1 >= dt1 時改變 LED1 的狀態
    if(tc1 - tp1 >= dt1) {
        tp1 = tc1;
        ledState1 = !ledState1;
        digitalWrite(LED1, ledState1);
    }
// 當 tc2 - tp >= dt2 時改變 LED2 的狀態
    if(tc2 - tp2 >= dt2) {
        tp2 = tc2;
        ledState2 = !ledState2;
        digitalWrite(LED2, ledState2);
    }
}

基本上就是把程式4複製、貼上,增加控制 LED2 的部分。


結語


雖然這個程式看起來只是用可變電阻控制 LED 的閃爍週期,但是利用類比輸入讀取可變電阻中間接腳的電阻值,這部分之後可以用來控制其它的裝置,使 LED 閃爍的部分如果改成接到繼電器,就可以控制工作電流更大的裝置。在學習更多的裝置用法及程式碼寫法之後,就能把這些東西應用在自己想要研究的主題上。


Arduino官方說明書


  1. define: https://www.arduino.cc/reference/en/language/structure/further-syntax/define/
  2. int: https://www.arduino.cc/reference/en/language/variables/data-types/int/
  3. pinMode(): https://www.arduino.cc/reference/en/language/functions/digital-io/pinmode/
  4. Serial.begin(): https://www.arduino.cc/en/serial/begin
  5. digitalWrite(): https://www.arduino.cc/reference/en/language/functions/digital-io/digitalwrite/
  6. delay(): https://www.arduino.cc/reference/en/language/functions/time/delay/
  7. millis(): https://www.arduino.cc/reference/en/language/functions/time/millis/
  8. unsigned long: https://www.arduino.cc/reference/en/language/variables/data-types/unsignedlong/
  9. bool: https://www.arduino.cc/reference/en/language/variables/data-types/bool/
  10. analogRead(): https://www.arduino.cc/reference/en/language/functions/analog-io/analogread/
  11. map(): https://www.arduino.cc/reference/en/language/functions/math/map/


HackMD 版本連結:https://hackmd.io/@yizhewang/SyQDIqV5v

沒有留言:

張貼留言