Learning Objectives
- 說出 Arduino 指的是開發板、開發環境與函式庫,以及它的由來
- 讀懂 Arduino 草稿碼的 setup() 與 loop(),知道 main() 替你做了什麼
- 把 pinMode、digitalWrite、digitalRead、analogRead、map、delay 對應到 MicroPython
- 說明 delay() 會讓程式停住,改用 millis() 做到同時處理多件事
為什麼要看得懂 Arduino
這個軌道一直用 MicroPython 寫程式,但你在網路上搜尋「感測器怎麼接」「馬達怎麼控制」時, 找到的範例有很大一部分是 Arduino 的程式。看得懂它,就能把別人的範例改寫成 MicroPython,也能和用 Arduino 的同學合作。
這一課只讀不寫 C++:每段 Arduino 程式旁邊都有意思相同的 MicroPython,逐步動畫與模擬器跑的是 MicroPython 版。
Arduino 是什麼
「Arduino」同時指三件事:
| 層次 | 內容 |
|---|---|
| 開發板 | 最經典的是 Arduino UNO:ATmega328P 晶片、16 MHz、14 支數位腳位(其中 6 支可輸出 PWM)、6 支類比輸入,工作電壓 5V |
| 開發環境 | Arduino IDE:寫程式、編譯、上傳一鍵完成 |
| 語言與函式庫 | 用 C++ 寫,加上一組簡單好記的函式:pinMode、digitalWrite、analogRead… |
Arduino 起源於義大利伊夫雷亞(Ivrea)的互動設計學院(Interaction Design Institute Ivrea),2005 年前後由 Massimo Banzi、 David Cuartielles、Tom Igoe、Gianluca Martino、David Mellis 等人推出,目的是讓設計與藝術背景的學生也能做互動作品。 它的基礎來自同一所學院的碩士生 Hernando Barragán 在 2003 年開始的 Wiring 專案。
後來「Arduino 的寫法」被移植到很多晶片上:ESP32 有樂鑫(Espressif)維護的 Arduino 核心,Pico 也有社群維護的 Arduino 核心。 所以同一塊 ESP32 可以用 Arduino(C++)寫,也可以燒 MicroPython,只是一次只能用一種韌體。
第一個草稿碼:Blink
Arduino 的程式叫草稿碼(sketch),副檔名 .ino。下面是讓 LED 閃爍的經典範例,旁邊是意思相同的 MicroPython:
// Arduino(C++)
const int LED = 23; // 常數:LED 接的腳位
void setup() { // 通電或重設後執行一次
pinMode(LED, OUTPUT); // 設成輸出
}
void loop() { // 執行完又從頭執行,永遠重複
digitalWrite(LED, HIGH); // 高電位:亮
delay(1000); // 等 1000 毫秒
digitalWrite(LED, LOW);
delay(1000);
}
# MicroPython
from machine import Pin
import time
LED = 23
led = Pin(LED, Pin.OUT)
while True:
led.value(1)
time.sleep_ms(1000)
led.value(0)
time.sleep_ms(1000)
讀 C++ 時注意幾個和 Python 不同的地方:
| C++ | Python | 說明 |
|---|---|---|
{ } 大括號 |
縮排 | C++ 用大括號圈出區塊,縮排只是為了好讀 |
每行結尾 ; |
不用 | 少了分號會編譯失敗 |
int、bool、void |
不用寫 | C++ 的變數與函式要宣告型別;void 表示函式不回傳值 |
// 註解 |
# 註解 |
|
HIGH/LOW |
1/0 |
ESP32 軌與自造者入門的範例都用 GPIO23 接 LED,所以這裡的 Arduino 程式也寫 23;如果是 UNO,板上 LED 在第 13 腳(LED_BUILTIN)。
逐步看:setup() 跑一次、loop() 一直跑
Arduino 草稿碼沒有 main(),但編譯時 Arduino 的核心程式會提供一個,大意是:先執行 setup() 一次,
再用無窮迴圈一直呼叫 loop()。下面用 MicroPython 把這個結構寫出來,注解寫的是對應的 Arduino 寫法。
看左側行號:第 17 行只執行一次,第 19 行一再回到第 11 行。
從草稿碼到晶片:編譯與上傳
MicroPython 是直譯:板子上已經有 MicroPython 韌體,你送過去的是原始碼,由板子一行一行執行。 Arduino 是編譯:按下「上傳」時,電腦上會做這些事:
- 前處理:把草稿碼加上
#include <Arduino.h>,替你的函式補上宣告(所以函式寫在呼叫它的地方後面也沒關係)。 - 編譯:用 C++ 編譯器(UNO 是 avr-gcc)把程式翻成這顆晶片的機器碼。
- 連結:和 Arduino 核心(
digitalWrite等函式、那個隱藏的main())合併成一個檔案。 - 上傳:透過 USB 傳到板子,寫進快閃記憶體,取代板子上原本的程式。
| Arduino(C++) | MicroPython | |
|---|---|---|
| 改一行要多久 | 重新編譯、上傳(幾秒到幾十秒) | 存檔就能跑,也可以在 REPL 直接試 |
| 錯誤什麼時候發現 | 型別、拼字錯誤在編譯時就被擋下 | 執行到那一行才發現 |
| 執行速度與記憶體 | 機器碼,快、省記憶體 | 有直譯器的負擔,較慢 |
| 板子上有什麼 | 只有你的程式 | MicroPython 韌體+你的 .py 檔 |
兩種都是好工具:需要精準時序、記憶體很小的晶片,常用 C++;想快速試驗、資料處理方便,MicroPython 比較順手。
類比讀取與 map()
Arduino 讀可變電阻、調 LED 亮度是這樣寫的:
void loop() {
int raw = analogRead(A0); // UNO:10 位元,0~1023
int level = map(raw, 0, 1023, 0, 255); // 換成 0~255
analogWrite(9, level); // PWM 輸出,0~255
Serial.println(raw); // 印到序列埠監控視窗
delay(100);
}
analogRead()在 UNO 上是 10 位元:0~1023 對應 0~5V,每一格約 4.9 mV。map(值, 原本下限, 原本上限, 新下限, 新上限)把數字從一個範圍換到另一個範圍。官方文件特別寫明:它用整數運算,小數直接捨去,不會四捨五入。analogWrite()名字有 analog,其實輸出的是 PWM(第 06 課),不是真正的類比電壓。Serial.println()相當於 MicroPython 的print()。
每塊板子的範圍不同:ESP32 的 MicroPython 讀值是 12 位元(0~4095)、Pico 的 read_u16() 是 0~65535。
把別人的程式移植過來時,先確認讀值與 PWM 的範圍,最常出錯的就是這裡。
逐步看:自己寫一個 map()
用 MicroPython 寫一個和 Arduino map() 一樣的函式,把 ESP32 的 12 位元讀值(0~4095)換成 PWM 的 0~1023。
注意第 6 行的 //:Python 的整數除法,結果和 C++ 的整數除法一樣把小數捨去(兩邊都是正數時)。
delay() 的問題:程式停住就什麼都看不到
delay(1000) 的意思是「這一秒什麼都不做」。只控制一顆 LED 沒問題;但如果同時要讀按鈕,
在 delay 的這一秒裡按下又放開,程式根本不會發現。
Arduino 的解法是 millis():回傳開機後經過的毫秒數。每一圈 loop 都看一下時間,「到了才做」,不到就繼續做別的事:
unsigned long lastFast = 0;
void loop() {
unsigned long now = millis();
if (now - lastFast >= 200) { // 距離上次切換超過 200 ms 了嗎?
lastFast = now;
digitalWrite(23, !digitalRead(23));
}
// 這裡可以繼續讀按鈕、讀感測器,不會被卡住
}
MicroPython 的對應是 time.ticks_ms() 與 time.ticks_diff()。unsigned long 是 32 位元的無號整數,
millis() 大約 49.7 天後會從 0 重新數起;用「現在減上次」的寫法,重新數起時也能算對。
MicroPython 的 ticks_diff() 也是為了處理這種「繞回 0」的情況而設計,所以不要直接用減號。
逐步看:兩顆 LED 用不同節奏閃爍
紅燈每 200 ms 切換、綠燈每 500 ms 切換。如果用 delay(sleep)寫,等紅燈的時候就不能管綠燈;
這裡每一圈只「看一下時間」,誰到了就切換誰。拖動步數,看第 12 行與第 15 行各在什麼時候執行。
最後的 sleep_ms(50) 只是讓模擬器的時間往前走,真的板子上可以拿掉。
小測驗:讀懂 Arduino 程式
Arduino 草稿碼裡的 setup() 和 loop(),各會執行幾次?
pinMode(4, INPUT_PULLUP); 對應哪一行 MicroPython?
Arduino 的 map() 用整數運算。下面這段 Python 模擬 map(512, 0, 1023, 0, 255),會印出什麼?
x = 512
print((x - 0) * (255 - 0) // (1023 - 0) + 0)
小測驗:型別與時間
C++ 裡 int a = 7; int b = 2;,a / b 的結果是 3(整數相除捨去小數)。下面哪一行 Python 會印出一樣的結果?
為什麼 Arduino 常建議用 millis() 取代 delay()?
下面這段 Python 算出 32 位元無號整數最多能數多少毫秒、換算成天數。會印出什麼?
ms = 2 ** 32
print(round(ms / 1000 / 86400, 1))
重點整理
- Arduino=開發板(經典的 UNO 是 5V)+開發環境+C++ 函式庫;2005 年前後起源於義大利伊夫雷亞的互動設計學院。
- 草稿碼的
setup()執行一次、loop()一直重複;隱藏的main()替你寫好了這個結構。 - 對照:
pinMode→Pin(…, Pin.OUT/IN)、digitalWrite→value()、delay(ms)→time.sleep_ms()、Serial.println→print。 analogRead在 UNO 是 0~1023;map()用整數運算,小數捨去;移植程式時先確認每塊板子的讀值與 PWM 範圍。- Arduino 要編譯再上傳,錯誤在編譯時就發現;MicroPython 直接執行,改起來快。
- 想同時做好幾件事,用
millis()(MicroPython 的ticks_ms()/ticks_diff())取代delay()。
下一課離開程式,回到硬體:作品要用電池時,電壓、電流與安全要怎麼規劃。