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資料上傳工具操作說明」貼文。