ESP32 Arduino開發平台目前最新版是3.x,跟《超圖解ESP32深度實作》採用的1.x版相比,調整了某些語法,但整體的程式運作邏輯不變。在新的版本中,有些是API被刪除或改名,例如,diy1_1的「選定要控制的接腳」:
gpio_pad_select_gpio(BLINK_GPIO);
改成「重置控制接腳」:
gpio_reset_pin(BLINK_GPIO);
有些則是底層的程式庫不再自動引用某些標頭檔,我們編寫的程式要自行引用。例如,第15章的一個BLE(低功耗藍牙)範例程式,透過esp_bt_dev_get_address()取得裝置的位址,在3.x版開發平台,程式開頭要明確引用esp_bt_device.h標頭檔,否則會發生編譯錯誤:
#include <esp_bt_device.h> // 內含esp_bt_dev_get_address()的宣告
此外,之前版本允許用底下的敘述直接定義C++的string(字串)型態變數:
using namespace std; string s = "hello world!";
在3.x版開發平台,必須引用string程式庫:
#include <string> // 必須明確引用string using namespace std; string s = "hello world!";
第15章的藍牙BLE程式中,接收並暫存「特徵」值的敘述:
std::string rxVal = pCharact->getValue();
其變數要改成Arduino的String型態:
String rxVal = pCharact->getValue();
第16章的「鍵盤滑鼠組的程式庫(ESP32-BLE-Keyboard-Mouse)」,取得藍牙裝置名稱的deviceName屬性值:
BLEDevice::init(BleKBMouseObj->deviceName);
也要轉換成Arduino的String型態:
BLEDevice::init(String(BleKBMouseObj->deviceName.c_str()));
第19章使用的支援SSL/TSL加密的網站伺服器程式庫esp32_https_server,尚未更新,因此仍需採用2.x版開發平台編譯。
讀取ESP32內部霍爾感測器的功能則被刪除,所以底下這個API不存在了,要使用這項功能,請安裝2.x版的開發環境。
hallRread() // 讀取霍爾感測值
部份用到霍爾感測器的範例,例如第15章的BLE(低功耗藍牙)單元,改用讀取A0腳,讀者可在A0腳改接可變電阻代替,當然,也可以連接霍爾感測器模組(日後說明)。
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) int hallVal = analogRead(A0); // 讀取A0腳 #else int hallVal = hallRead(); // 讀取霍爾感測器值 #endif
調整程式記憶體分區的大小
有些程式庫有更新支援3.x版開發平台,例如,驅動I2S裝置輸出立體聲的ESP32-A2DP,請下載、安裝新版程式庫。然而,使用新版ESP32-A2DP程式庫,搭配WEMOS D1 MINI ESP32開發板,編譯動手做14-1的原始碼,出現編譯錯誤 “text section exceeds available space in board(文字部分超出開發板可用空間)”。
底下是編譯器的部分輸出訊息:
Sketch uses 1430869 bytes (109%) of program storage space. Maximum is 1310720 bytes.
草稿碼使用了 1430869 位元組 (109%) 的程式儲存空間。上限為 1310720 位元組。
Global variables use 54008 bytes (16%) of dynamic memory, leaving 273672 bytes for local variables. Maximum is 327680 bytes.
全域變數使用了 54008 位元組 (16%) 的動態記憶體,剩餘 273672 位元組用於區域變數。上限為 327680 位元組。
解決辦法是加大程式記憶體分區。WEMOS D1 MINI ESP32開發板的Partition Scheme(分區方案)選項的預設值是“Default(預設)”,改成“No OTA (Large APP)”,代表沒有OTA(Over-The-Air,無線更新韌體)分區。
重新編譯就過關了,編譯訊息如下:
Sketch uses 1430869 bytes (68%) of program storage space. Maximum is 2097152 bytes.
草稿碼使用 1430869 位元組 (68%) 的程式儲存空間。上限為 2097152 位元組。
計時器中斷
書本4-23頁的「計時器單元」程式,採用Timer(計時器)物件和中斷處理常式建立「每隔一段時間,執行某段敘述」的程式。Timer物件在3.x版開發平台也做了些許調整,樂鑫官網的Timer(計時器)文件,列舉了Timer API的全部函式與說明。本文只用到下列四個函式:
- timerBegin():設定並啟用計時器,用手機APP來比喻,此舉相當於「打開計時器APP」。
- timerAttachInterrupt():附加中斷處理常式給計時器物件
- timerDetachInterrupt():解除附加在計時器物件的中斷處理常式
- timerAlarm():設定計時器的觸發時間和次數,相當於在手機上設定計時器APP的鬧鈴時間。
- timerEnd():結束計時器,相當於在手機上關閉計時器APP。
新版Timer API的運作流程,大致跟書本上採用的舊版API相同:宣告計時器物件、定義計時器中斷處理常式,但初始化和啟用計時器的流程從呼叫4個函式簡化成3個,原本偵測計時器時脈上下緣(edge)的參數也取消了。

timerAlarm()最後的「計數值(counter)」參數,用於設定每次計時器觸發後的計數器初始值,通常設定成0。這個計數值存位計時器物件內部,可透過timerRead(讀取計數值)、timerReadMicros(讀取轉成微秒後的計數值)以及timerReadMillis(讀取轉成毫秒後的計數值)等函式取得。
假設計時器物件叫做timer,中斷處理常式叫做onTimer,底下三個程式片段將分別令計時器每隔1秒、0.5秒和10秒,反覆觸發:
底下是一個簡易的定時器程式範例,令開發板內建的LED每0.5秒交替閃爍。
volatile bool toggle = 0; // 紀錄內建LED的狀態
portMUX_TYPE tmux = portMUX_INITIALIZER_UNLOCKED; // 資源鎖
hw_timer_t * timer; // 計時器物件
void IRAM_ATTR onTimer() { // 計時器中斷處理常式
portENTER_CRITICAL(&tmux);
toggle = !toggle;
digitalWrite(LED_BUILTIN, toggle);
portEXIT_CRITICAL(&tmux);
}
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
timer = timerBegin(1e6); // 初始化計時器,頻率:1Mhz
timerAttachInterrupt(timer, &onTimer); // 附加onTimer給計時器
timerAlarm(timer, 5e5, true, 0);
}
void loop() { }
底下是修改書本「動手做4-4」,支援3.x版開發平台的範例:
#define LED0 2
#define LED1 19 // 外接LED的接腳
volatile bool state0 = 0; // 紀錄內建LED的狀態
volatile bool state1 = 0; // 紀錄外接LED的狀態
volatile byte counter = 0; // 計數器
portMUX_TYPE mux0 = portMUX_INITIALIZER_UNLOCKED; // 資源鎖0
portMUX_TYPE mux1 = portMUX_INITIALIZER_UNLOCKED; // 資源鎖1
hw_timer_t * timer0; // 計時器物件0
hw_timer_t * timer1;
void IRAM_ATTR onTimer0() {
portENTER_CRITICAL(&mux0);
state0 = !state0;
digitalWrite(LED0, state0);
portEXIT_CRITICAL(&mux0);
}
void IRAM_ATTR onTimer1() {
portENTER_CRITICAL(&mux1);
state1 = !state1;
digitalWrite(LED1, state1);
if (++counter == 10) {
if (timer1 != NULL) {
#if ESP_ARDUINO_VERSION < ESP_ARDUINO_VERSION_VAL(3, 0, 0)
timerAlarmDisable(timer1); // 用於2.x版
#endif
timerDetachInterrupt(timer1);
timerEnd(timer1);
timer1 = NULL;
}
}
portEXIT_CRITICAL(&mux1);
}
void setup() {
pinMode(LED0, OUTPUT);
pinMode(LED1, OUTPUT);
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
timer0 = timerBegin(1e6); // 1e6 = 1000000,1MHz
timer1 = timerBegin(1e6);
timerAttachInterrupt(timer0, &onTimer0);
timerAttachInterrupt(timer1, &onTimer1);
timerAlarm(timer0, 1e6, true, 0); // 1000ms
timerAlarm(timer1, 5e5, true, 0); // 500ms
#else
timer0 = timerBegin(0, 80, true); // 設置計時器0
timer1 = timerBegin(1, 80, true); // 設置計時器1
timerAttachInterrupt(timer0, &onTimer0, true);
timerAttachInterrupt(timer1, &onTimer1, true);
timerAlarmWrite(timer0, 1000000, true); // 1000ms(1秒)
timerAlarmWrite(timer1, 500000, true); // 500ms(0.5秒)
timerAlarmEnable(timer0); // 啟動計時器
timerAlarmEnable(timer1);
#endif
}
void loop() { }
《超圖解ESP32深度實作》範例檔已全數更新,請讀者重新下載。有關使用ESP_ARDUINO_VERSION_VAL()巨集區分ESP32 Arduino開發平台、Arduino IDE 2.x版的操作簡介、在IDE 2.x上傳資料檔到ESP32 SPIFFS分區,以及PWM輸出的語法更新說明,請參閱「《超圖解ESP32應用實作》範例檔更新支援開發環境3.x版及Arduino IDE 2.x資料上傳工具操作說明」貼文。
