關閉

第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 常用函式

前面的 Arduino 實作已經陸續使用 pinMode()、digitalWrite()、digitalRead()、analogRead()、analogWrite()、delay() 與 Serial.println()。這些函式看似獨立,實際專案中卻經常需要一起使用。

以旋鈕控制 LED 亮度為例,程式必須先讀取可變電阻,再將 ADC 數值轉換成 PWM 範圍,必要時限制上下限,最後輸出到 LED。

掌握這種資料處理流程後,就能把感測器、按鍵、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);

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);

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 並定時輸出狀態

實驗目標

接線方式

完整程式

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 閃爍

練習三:數值安全限制

建立 motorSpeed 變數,不論輸入值是多少,都必須使用 constrain() 限制在 80~200。

練習四:電子骰子

按下按鍵後,使用 random(1, 7) 產生 1~6,並顯示在 Serial Monitor。

挑戰題:Arduino 多工控制器

readPotentiometer();
updatePwmLed();
readButton();
updateDigitalLed();
printSystemStatus();

本章重點整理

下一章預告:Arduino Interrupt 中斷與即時事件處理

下一章將從一般輪詢控制進入即時事件處理,說明 Interrupt、attachInterrupt()、ISR、volatile 與 Arduino Uno 外部中斷腳位。