第14章 Arduino 常用函式與程式設計技巧:map、millis、random 與程式結構完整教學

發布日期: |更新日期:
文章主圖:第14章 Arduino 常用函式與程式設計技巧:map、millis、random 與程式結構完整教學

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。

  1. 使用 analogRead() 讀取可變電阻。
  2. 使用 map() 將 0~1023 轉換成 0~255。
  3. 使用 constrain() 限制安全範圍。
  4. 使用 analogWrite() 輸出 PWM。

掌握這種資料處理流程後,就能把感測器、按鍵、LED、馬達與序列通訊組合成更完整的 Arduino 專案。

知識卡:Arduino 常用函式分類
類別 常用函式
腳位設定 pinMode()
數位輸出 digitalWrite()
數位輸入 digitalRead()
類比輸入 analogRead()
PWM 輸出 analogWrite()
延遲控制 delay()、delayMicroseconds()
時間計算 millis()、micros()
數值轉換 map()
數值限制 constrain()
數值比較 min()、max()
絕對值 abs()
隨機數 random()、randomSeed()
序列通訊 Serial.print()、Serial.println()

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);
INPUT_PULLUP 按鍵狀態
按鍵狀態 digitalRead() 結果
放開 HIGH
按下 LOW
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。

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);
constrain() 限制結果
原始值 限制範圍 結果
-20 0~255 0
100 0~255 100
300 0~255 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。

接線方式

可變電阻與 LED 接線
元件 接線
可變電阻一側腳 5V
可變電阻中間腳 A0
可變電阻另一側腳 GND
LED D9 → 220Ω 電阻 → LED → GND

完整程式

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 亮度。

五段式 LED 亮度對照表
ADC 範圍 LED PWM
0~204 0
205~409 64
410~614 128
615~819 192
820~1023 255

練習二:不使用 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 關鍵字。
  • 實作按鍵、編碼器與脈衝訊號的中斷應用。
相關資源與文章匯出
下載 Arduino 入門基礎教材 下載 DOCX PDF/列印

← 回文章列表