第14章 Arduino 常用函式與程式設計技巧:map、millis、random 與程式結構完整教學
本章完整整理 Arduino 常用函式與程式設計技巧,包含 pinMode()、digitalRead()、digitalWrite()、analogRead()、analogWrite()、map()、constrain()、millis()、micros()、random() 與 Serial.print()。透過旋鈕控制 LED、非阻塞計時、隨機效果與自訂函式實作,學會建立結構清楚、容易維護且可擴充的 Arduino 程式。
Arduino 程式通常不是只靠單一函式完成,而是把腳位設定、輸入讀取、數值轉換、輸出控制與時間管理組合成完整流程。本章將常用函式集中整理,協助你建立可重複使用的程式工具箱。
內容以 Arduino Uno 為主要範例,涵蓋 pinMode()、digitalRead()、digitalWrite()、analogRead()、analogWrite()、map()、constrain()、millis()、random() 與 Serial 輸出等常見功能。
學習重點不只是記住語法,而是理解每個函式適合處理什麼工作,以及如何避免 delay() 阻塞、數值超出範圍與程式結構過度集中等常見問題。
本章學習目標
- 理解 Arduino 常用函式的用途與適用情境。
- 正確使用數位輸入、數位輸出、類比輸入與 PWM。
- 使用 map() 轉換數值範圍。
- 使用 constrain() 限制數值上下限。
- 使用 millis() 建立不阻塞程式。
- 了解 delay() 對程式執行造成的影響。
- 使用 random() 產生隨機效果。
- 將多個函式整合成結構清楚的 Arduino 程式。
為什麼要整理 Arduino 常用函式
前面的 Arduino 實作已經陸續使用 pinMode()、digitalWrite()、digitalRead()、analogRead()、analogWrite()、delay() 與 Serial.println()。這些函式看似獨立,實際專案中卻經常需要一起使用。
以旋鈕控制 LED 亮度為例,程式必須先讀取可變電阻,再將 ADC 數值轉換成 PWM 範圍,必要時限制上下限,最後輸出到 LED。
- 使用 analogRead() 讀取可變電阻。
- 使用 map() 將 0~1023 轉換成 0~255。
- 使用 constrain() 限制安全範圍。
- 使用 analogWrite() 輸出 PWM。
掌握這種資料處理流程後,就能把感測器、按鍵、LED、馬達與序列通訊組合成更完整的 Arduino 專案。
pinMode():設定 Arduino 腳位模式
pinMode() 用於指定腳位是輸入、輸出或啟用內部上拉電阻。通常應在 setup() 中完成設定。
基本語法
pinMode(pin, mode);
pin 是 Arduino 腳位編號,mode 可使用 INPUT、OUTPUT 或 INPUT_PULLUP。
OUTPUT 輸出模式
當腳位需要控制 LED、繼電器或其他數位裝置時,應設定為 OUTPUT。
const int LED_PIN = 9;
void setup()
{
pinMode(LED_PIN, OUTPUT);
}
完成設定後,可使用 digitalWrite() 或在支援 PWM 的腳位上使用 analogWrite()。
INPUT 輸入模式
INPUT 用於讀取外部數位訊號。若外部電路沒有上拉或下拉電阻,輸入腳位可能處於 Floating(浮接)狀態,造成讀值不穩定。
const int BUTTON_PIN = 2;
void setup()
{
pinMode(BUTTON_PIN, INPUT);
}
INPUT_PULLUP 內部上拉模式
INPUT_PULLUP 會啟用 Arduino 內部上拉電阻,按鍵可直接接在輸入腳位與 GND 之間。此接法的邏輯通常與直覺相反:放開時為 HIGH,按下時為 LOW。
pinMode(BUTTON_PIN, INPUT_PULLUP);
if (digitalRead(BUTTON_PIN) == LOW)
{
Serial.println("Button pressed");
}
digitalWrite():輸出 HIGH 或 LOW
digitalWrite() 用於控制數位腳位的高低電位。
基本語法
digitalWrite(pin, value);
value 可以是 HIGH 或 LOW。以 5V 邏輯的 Arduino Uno 為例,LOW 約為 0V,HIGH 約為 5V。
LED 閃爍範例
const int LED_PIN = 9;
void setup()
{
pinMode(LED_PIN, OUTPUT);
}
void loop()
{
digitalWrite(LED_PIN, HIGH);
delay(1000);
digitalWrite(LED_PIN, LOW);
delay(1000);
}
這段程式會讓 LED 亮一秒、熄滅一秒,持續循環。
digitalRead():讀取數位輸入
digitalRead() 用於讀取數位腳位狀態,回傳值為 HIGH 或 LOW。
基本語法
digitalRead(pin);
按鍵控制 LED
使用 INPUT_PULLUP 時,可將按鍵接在 D2 與 GND 之間。按下按鍵時讀值為 LOW。
const int BUTTON_PIN = 2;
const int LED_PIN = 9;
void setup()
{
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop()
{
int buttonState = digitalRead(BUTTON_PIN);
if (buttonState == LOW)
{
digitalWrite(LED_PIN, HIGH);
}
else
{
digitalWrite(LED_PIN, LOW);
}
}
按下按鍵時 LED 亮起,放開時 LED 熄滅。
analogRead():讀取類比訊號
analogRead() 用於讀取類比輸入腳位。Arduino Uno 的 ADC 為 10 位元,因此回傳值範圍為 0~1023。
基本語法
analogRead(pin);
讀取可變電阻
const int POT_PIN = A0;
void setup()
{
Serial.begin(9600);
}
void loop()
{
int sensorValue = analogRead(POT_PIN);
Serial.println(sensorValue);
delay(100);
}
轉動可變電阻後,Serial Monitor 會顯示持續變化的 ADC 數值。analogRead() 取得的是 ADC 數值,不是直接取得電壓。
analogWrite():輸出 PWM
analogWrite() 在 Arduino Uno 上用於輸出 PWM,value 範圍為 0~255。它不是輸出真正的連續類比電壓,而是透過快速切換 HIGH 與 LOW 改變平均輸出效果。
基本語法
analogWrite(pin, value);
固定亮度範例
const int LED_PIN = 9;
void setup()
{
pinMode(LED_PIN, OUTPUT);
}
void loop()
{
analogWrite(LED_PIN, 128);
}
128 約等於 50% 占空比,因此 LED 會呈現中等亮度。Arduino Uno 支援 PWM 的腳位通常為 D3、D5、D6、D9、D10 與 D11。
map():轉換數值範圍
map() 適合將一組數值範圍轉換為另一組範圍,例如把 analogRead() 的 0~1023 轉換成 analogWrite() 所需的 0~255。
基本語法
map(value, fromLow, fromHigh, toLow, toHigh);
int pwmValue = map(sensorValue, 0, 1023, 0, 255);
這代表把輸入 0~1023 對應成輸出 0~255。map() 使用整數運算,回傳型別通常是 long。
旋鈕控制 LED 亮度
const int POT_PIN = A0;
const int LED_PIN = 9;
void setup()
{
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}
void loop()
{
int sensorValue = analogRead(POT_PIN);
int pwmValue = map(sensorValue, 0, 1023, 0, 255);
analogWrite(LED_PIN, pwmValue);
Serial.print("ADC: ");
Serial.print(sensorValue);
Serial.print(" PWM: ");
Serial.println(pwmValue);
delay(50);
}
map() 不會自動限制結果
若輸入值超出 fromLow 與 fromHigh,map() 的結果也可能超出目標範圍。因此,輸入來源不穩定或可能超出預期時,應搭配 constrain()。
int result = map(1200, 0, 1023, 0, 255);
constrain():限制數值上下限
constrain() 可將數值限制在指定的最小值與最大值之間。
基本語法
constrain(value, minimum, maximum);
int safeValue = constrain(pwmValue, 0, 255);
搭配 map() 使用
int pwmValue = map(sensorValue, 0, 1023, 0, 255);
pwmValue = constrain(pwmValue, 0, 255);
這樣可避免輸出值超出 PWM 的有效範圍。
delay():簡單但會阻塞程式
delay() 會讓主要程式流程暫停指定的毫秒數。
基本語法
delay(milliseconds);
delay(1000);
delay(1000) 代表暫停 1000 毫秒,也就是 1 秒。
delay() 的優點
- 容易理解。
- 程式簡單。
- 適合初學者測試。
- 適合簡單閃燈與固定等待流程。
delay() 的缺點
當程式執行 delay() 時,loop() 會停在原地等待。等待期間無法及時處理其他工作,這種情況稱為 Blocking(阻塞)。
digitalWrite(LED_PIN, HIGH);
delay(5000);
digitalWrite(LED_PIN, LOW);
- 按鍵可能反應遲鈍。
- 顯示器無法即時更新。
- 感測器讀取頻率降低。
- 序列資料可能來不及處理。
- 其他 LED 或輸出工作無法同時執行。
millis():建立不阻塞計時
millis() 會回傳 Arduino 從啟動到目前為止經過的毫秒數。程式可持續執行 loop(),只在時間間隔到達時執行指定工作。
基本語法
unsigned long currentTime = millis();
使用 millis() 製作閃爍 LED
const int LED_PIN = 9;
unsigned long previousMillis = 0;
const unsigned long INTERVAL = 1000;
bool ledState = false;
void setup()
{
pinMode(LED_PIN, OUTPUT);
}
void loop()
{
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= INTERVAL)
{
previousMillis = currentMillis;
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
}
程式會持續執行 loop()。每次檢查是否已經經過 1 秒,時間到達後才切換 LED 狀態,因此等待期間仍可讀取按鍵、更新 LCD 或處理 Serial 資料。
時間變數應使用 unsigned long
millis() 回傳 unsigned long。Arduino Uno 的 int 通常只有 16 位元,不適合儲存長時間累積的毫秒值。
unsigned long previousMillis = 0;
unsigned long currentMillis = millis();
正確處理 millis() 溢位
Arduino Uno 的 millis() 約每 49.7 天會回到 0。使用減法比較可在 unsigned long 溢位時維持正確判斷。
if (currentMillis - previousMillis >= INTERVAL)
不建議使用 currentMillis >= previousMillis + INTERVAL,因為加法在溢位附近較容易造成錯誤判斷。
delayMicroseconds() 與 micros()
需要處理短脈衝或微秒級時序時,可使用 delayMicroseconds() 與 micros()。
delayMicroseconds()
delayMicroseconds(10);
- 產生短脈衝。
- 控制超音波模組 Trigger。
- 測試時序訊號。
micros()
unsigned long currentMicros = micros();
- 測量短時間。
- 計算脈衝寬度。
- 處理高速感測器。
- 分析通訊時序。
min()、max() 與 abs()
min() 與 max()
min() 回傳兩個數值中較小者,max() 回傳較大者。
int lowValue = min(20, 50);
int highValue = max(20, 50);
motorSpeed = max(motorSpeed, 80);
brightness = min(brightness, 200);
若需要同時限制最小值與最大值,通常使用 constrain() 會更清楚。
abs() 取得絕對值
int difference = abs(valueA - valueB);
- 比較兩次感測值的差異。
- 判斷數值變化是否過大。
- 計算目標值與實際值之間的誤差。
random() 與 randomSeed()
random() 產生隨機數
long value = random(10);
random(10) 會產生 0~9,不包含 10。
long dice = random(1, 7);
random(1, 7) 會產生 1~6,適合模擬骰子。最大值參數不包含在輸出範圍內。
randomSeed() 設定隨機種子
若每次開機都直接使用 random(),可能出現相同數列。以下範例假設 A5 未連接任何元件,利用浮接類比輸入的雜訊初始化隨機種子。
const int RANDOM_SEED_PIN = A5;
void setup()
{
Serial.begin(9600);
randomSeed(analogRead(RANDOM_SEED_PIN));
}
void loop()
{
int dice = random(1, 7);
Serial.println(dice);
delay(1000);
}
隨機 LED 亮度
const int LED_PIN = 9;
const int RANDOM_SEED_PIN = A5;
void setup()
{
pinMode(LED_PIN, OUTPUT);
randomSeed(analogRead(RANDOM_SEED_PIN));
}
void loop()
{
int brightness = random(0, 256);
analogWrite(LED_PIN, brightness);
delay(200);
}
Serial.print() 與格式化輸出
清楚的序列輸出能協助除錯。與其只輸出單一數字,更建議加入欄位名稱與單位。
Serial.print("ADC: ");
Serial.print(sensorValue);
Serial.print(" PWM: ");
Serial.print(pwmValue);
Serial.print(" LED: ");
Serial.println(ledState ? "ON" : "OFF");
預期輸出格式為:ADC: 523 PWM: 130 LED: ON。
指定數字進位
int value = 15;
Serial.println(value, DEC);
Serial.println(value, HEX);
Serial.println(value, OCT);
Serial.println(value, BIN);
指定進位格式適合檢查暫存器、位元資料與通訊封包。
指定浮點數小數位數
float voltage = 2.45678;
Serial.print("Voltage: ");
Serial.print(voltage, 2);
Serial.println(" V");
預期輸出為 Voltage: 2.46 V。
綜合實驗:旋鈕控制 LED 並定時輸出狀態
實驗目標
- 使用 analogRead() 讀取可變電阻。
- 使用 map() 與 constrain() 轉換數值。
- 使用 analogWrite() 控制 LED 亮度。
- 使用 millis() 定時輸出狀態。
- 使用 Serial.print() 顯示 ADC、電壓與 PWM。
接線方式
完整程式
const int POT_PIN = A0;
const int LED_PIN = 9;
unsigned long previousMillis = 0;
const unsigned long REPORT_INTERVAL = 500;
void setup()
{
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}
void loop()
{
int adcValue = analogRead(POT_PIN);
int pwmValue = map(adcValue, 0, 1023, 0, 255);
pwmValue = constrain(pwmValue, 0, 255);
analogWrite(LED_PIN, pwmValue);
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= REPORT_INTERVAL)
{
previousMillis = currentMillis;
float voltage = adcValue * 5.0 / 1023.0;
Serial.print("ADC: ");
Serial.print(adcValue);
Serial.print(" Voltage: ");
Serial.print(voltage, 2);
Serial.print(" V");
Serial.print(" PWM: ");
Serial.println(pwmValue);
}
}
預期輸出
ADC: 210 Voltage: 1.03 V PWM: 52
ADC: 512 Voltage: 2.50 V PWM: 127
ADC: 803 Voltage: 3.92 V PWM: 200
為什麼不使用 delay()
若使用 delay(500),程式每次都會停頓半秒。當專案加入按鍵、LCD、UART、蜂鳴器或多個感測器後,容易出現反應遲鈍。使用 millis() 控制報告時間,可讓 LED 亮度持續即時更新。
將重複程式整理成自訂函式
當程式逐漸變長,可將讀取、轉換、輸出與顯示拆成不同函式。每個函式只負責一項主要工作,能提高可讀性與維護性。
const int POT_PIN = A0;
const int LED_PIN = 9;
unsigned long previousReportMillis = 0;
const unsigned long REPORT_INTERVAL = 100;
void setup()
{
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
}
void loop()
{
int adcValue = readPotentiometer();
int pwmValue = convertToPwm(adcValue);
updateLed(pwmValue);
printStatusPeriodically(adcValue, pwmValue);
}
int readPotentiometer()
{
return analogRead(POT_PIN);
}
int convertToPwm(int adcValue)
{
long mappedValue = map(adcValue, 0, 1023, 0, 255);
return constrain(mappedValue, 0, 255);
}
void updateLed(int pwmValue)
{
analogWrite(LED_PIN, pwmValue);
}
void printStatusPeriodically(int adcValue, int pwmValue)
{
unsigned long currentMillis = millis();
if (currentMillis - previousReportMillis < REPORT_INTERVAL)
{
return;
}
previousReportMillis = currentMillis;
Serial.print("ADC: ");
Serial.print(adcValue);
Serial.print(" PWM: ");
Serial.println(pwmValue);
}
- 程式結構更清楚。
- 發生錯誤時較容易定位。
- 未來修改功能較方便。
- 函式可在其他專案重複使用。
良好的 Arduino 程式設計習慣
使用有意義的變數名稱
int sensorValue;
int pwmValue;
避免使用 a、b 等無法表示用途的名稱。
使用 const 定義固定腳位與常數
const int LED_PIN = 9;
const unsigned long REPORT_INTERVAL = 500;
修改腳位或時間設定時,只需要調整一個位置。固定常數使用大寫命名,也能讓程式更容易閱讀。
一個函式只處理一項主要工作
readSensor();
updateDisplay();
controlMotor();
sendReport();
不要把所有工作都塞進非常長的 loop()。
加入有價值的註解
// 使用 millis() 避免 delay() 阻塞按鍵讀取
好的註解應說明為什麼這樣設計,而不是重複程式本身已經清楚表達的動作。
避免魔術數字
const int SENSOR_THRESHOLD = 700;
const int ALERT_BRIGHTNESS = 180;
if (sensorValue > SENSOR_THRESHOLD)
{
analogWrite(LED_PIN, ALERT_BRIGHTNESS);
}
將固定數值改成具名常數,可讓程式用途更清楚,也方便後續調整。
Arduino 初學者常見錯誤
忘記設定 pinMode()
使用 digitalWrite() 前應先把腳位設定為 OUTPUT;讀取按鍵時也應選擇適當的 INPUT 或 INPUT_PULLUP。
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH);
analogWrite() 使用不支援 PWM 的腳位
Arduino Uno 通常只有 D3、D5、D6、D9、D10 與 D11 支援 PWM。使用其他腳位時,無法得到預期的亮度或速度控制效果。
把 analogRead() 結果當成電壓
int value = analogRead(A0);
float voltage = value * 5.0 / 1023.0;
analogRead() 回傳 ADC 數值。換算電壓時,5.0 應依實際參考電壓調整。
map() 後未檢查範圍
map() 不會自動限制結果。當輸入可能超出預期值時,應搭配 constrain()。
大量使用 delay()
- 按鍵反應遲鈍。
- 顯示更新卡頓。
- 通訊資料可能遺失。
- 多項工作無法同時執行。
使用 int 儲存 millis()
unsigned long previousMillis = 0;
unsigned long currentMillis = millis();
誤解 random() 的上限
random(1, 7) 只會產生 1~6,不會產生 7。
本章實作練習
練習一:五段式 LED 亮度
讀取可變電阻,依 ADC 範圍輸出五段不同 PWM 亮度。
練習二:不使用 delay() 的雙 LED 閃爍
- LED 1 每 500 毫秒切換一次。
- LED 2 每 1000 毫秒切換一次。
- 兩顆 LED 必須以不同速度同時閃爍。
練習三:數值安全限制
建立 motorSpeed 變數,不論輸入值是多少,都必須使用 constrain() 限制在 80~200。
練習四:電子骰子
按下按鍵後,使用 random(1, 7) 產生 1~6,並顯示在 Serial Monitor。
挑戰題:Arduino 多工控制器
- 使用可變電阻控制第一顆 LED 亮度。
- 每 500 毫秒在 Serial Monitor 顯示 ADC、電壓與 PWM。
- 使用按鍵控制第二顆 LED。
- 主程式不得使用 delay()。
- 使用自訂函式整理程式結構。
readPotentiometer();
updatePwmLed();
readButton();
updateDigitalLed();
printSystemStatus();
本章重點整理
- pinMode() 用於設定輸入、輸出與內部上拉模式。
- digitalWrite() 與 digitalRead() 負責數位輸出與輸入。
- analogRead() 在 Arduino Uno 上讀取 0~1023 的 ADC 數值。
- analogWrite() 在支援 PWM 的腳位輸出 0~255。
- map() 可以轉換數值範圍,但不會自動限制結果。
- constrain() 可以將數值限制在指定上下限。
- delay() 適合簡單流程,但會造成阻塞。
- millis() 可以建立不阻塞的時間控制。
- random() 與 randomSeed() 可建立隨機效果。
- 將程式拆分成自訂函式,可提高可讀性與維護性。
下一章預告:Arduino Interrupt 中斷與即時事件處理
下一章將從一般輪詢控制進入即時事件處理,說明 Interrupt、attachInterrupt()、ISR、volatile 與 Arduino Uno 外部中斷腳位。
- 理解 Interrupt(中斷)的用途。
- 比較 digitalRead() 輪詢與中斷處理。
- 認識 Arduino Uno 的外部中斷腳位。
- 學習 attachInterrupt() 的使用方式。
- 理解 ISR 的設計原則與 volatile 關鍵字。
- 實作按鍵、編碼器與脈衝訊號的中斷應用。