《超圖解ESP32應用實作》分別使用「中斷常式」以及「查表法」,檢測附帶霍爾感測器的馬達的轉向和轉速,以及旋轉編碼器的轉向和脈衝數。本文將沿用書中「動手做15-1:使用自訂程式庫製作旋鈕介面」的電路,介紹採用ESP32內建的「脈衝計數器(Pulse Counter)」,編寫檢測旋轉編碼器的轉向和脈衝數的程式。
ESP32晶片內部有偵測與計算脈衝訊號變化的專屬電路,叫做「脈衝計數器」。比起用「中斷」或「查表法」,使用晶片內建的脈衝計數器不占用處理器的運作資源,檢測頻率也更高,但缺點是,程式僅適用於ESP32晶片,無法用於其他微控器開發板。
實際上,ESP32脈衝計數器的相關程式庫,並未包含在ESP32 Arduino平台,而是位於樂鑫官方的ESP-IDF開發環境。由於ESP32 Arduino的底層就是ESP-IDF(詳閱《超圖解ESP32深度實作》第一章),所以Arduino程式可引用ESP-IDF的程式庫。
脈衝計數器的詳細說明以及完整的操作函式介紹,請參閱樂鑫官方的「脈衝計數器」(簡體中文)說明文件(這是英文版)。本文僅說明範例實作所需的背景知識與相關函式。
ESP32的脈衝計數器單元
ESP32內部有8組獨立運作的脈衝計數器單元,分別命名為PULSE_CNT_U0 ~ PULSE_CNT_U7,用於計數輸入脈衝的上升邊緣或下降邊緣,偵測的脈衝頻率上限為40MHz。底下是ESP32晶片內部的脈衝計時器單元簡圖:
每個脈衝計數器單元均有一個帶正負號的16位元(即:int16_t)計數暫存器,以及兩個通道(channel),可用程式配置在偵測到脈衝訊號時,要增加或減少計數值。每個通道都有一個脈衝輸入訊號以及一個控制輸入訊號(如:控制增加或減少計數值)。
「脈衝訊號」是必要的,「控制訊號」則可有可無。以典型的旋轉編碼器波形為例,其中一個訊號(此例為CLK腳)可當作「脈衝訊號」,另一個訊號(此例為DT腳)當作「控制訊號」。
設置「脈衝計時器」工作模式的pcnt_config_t結構體
在ESP32 Arduino開發環境1.x及2.x版,操控脈衝計時器的是pcnt.h程式庫,3.x版本之後,改用另一個程式庫(下一篇文章再說明),但採用pcnt.h的程式仍可在3.x版本開發環境中正常編譯執行。
pcnt.h程式庫定義了用於配置脈衝計數器的pcnt_config_t結構體,它具有以下排列順序的成員:
- int pulse_gpio_num:設定輸入「脈衝訊號」的接腳編號,例如,連接旋轉編碼器CLK的接腳。
- int ctrl_gpio_num:設定輸入「控制訊號」的接腳編號,例如,連接旋轉編碼器DT的接腳。
- pcnt_ctrl_mode_t lctrl_mode:設定「控制訊號」於「低電位」時的脈衝訊號計數模式。pcnt_ctrl_mode_t是此程式庫定義的enum(列舉)型態常數,其可能值為下列之一:
- PCNT_MODE_KEEP:根據下文介紹的pos_mode和neg_mode的設定進行計數;KEEP意指「維持」。
- PCNT_MODE_REVERSE:計數方式與下述的pos_mode及neg_mode中的設定相反;REVERSE代表「反向」。
- PCNT_MODE_DISABLE:不計數;DISABLE代表「取消」。
- PCNT_COUNT_DIS:不計數,“DIS”意指“DISABLE”(取消)。
- PCNT_COUNT_INC:計數加1,“INC”意指“INCREASE”(增加)。
- PCNT_COUNT_DEC:計數減1,“DEC”意指”DECREASE”(減少)。
假若要將脈衝計數器設定成:採用PCNT_UNIT_0計數器單元的通道0、僅在偵測到脈衝訊號的上升邊緣時,增加計數值,計數值的上、下限分別設為10, -10。
設置pcnt_config結構體的程式片段如下:
#include <driver/pcnt.h> // 引用pcnt.h程式庫 #define CLK_PIN 4 // 旋轉編碼器的CLK,接脈衝訊號輸入腳。 #define DT_PIN 5 // 旋轉編碼器的DT,接控制訊號輸入腳。 #define SW_PIN 6 // 開關腳 : 略 pcnt_config_t pcnt_config = { .pulse_gpio_num = CLK_PIN, // 脈衝訊號腳 .ctrl_gpio_num = DT_PIN, // 控制訊號腳 .lctrl_mode = PCNT_MODE_DISABLE, // 控制訊號低電位時:取消。 .hctrl_mode = PCNT_MODE_KEEP, // 控制訊號高電位時:維持以下設定。 .pos_mode = PCNT_COUNT_INC, // 上升邊緣:計數+1 .neg_mode = PCNT_COUNT_DIS, // 下降邊緣:不計數 .counter_h_lim = 10, // 數值上限 .counter_l_lim = -10, // 數值下限 .unit = PCNT_UNIT_0, // 計數器單元名稱 .channel = PCNT_CHANNEL_0 // 通道名稱 };
脈衝計數器相關函式
設置pcnt_config結構體之後,必須執行pcnt_unit_config函式初始化脈衝計時器,此函式的原型如下,它接收一個參數,也就是指向pcnt_config_t結構型態的變數的指標。
esp_err_t pcnt_unit_config(const pcnt_config_t *pcnt_config);
此函式有三種可能的傳回值:
- ESP_OK:初始化成功
- ESP_ERR_INVALID_STATE:初始化錯誤,可能是之前已初始化,或者執行底下的函式時,尚未初始化。
- ESP_ERR_INVALID_ARG:參數錯誤
本文的範例程式將不接收與處理函式的傳回值。下列三個函式用於控制計數器,它們都接收一個代表「脈衝計時器單元」的參數,可能值為PCNT_UNIT_0 ~ PCNT_UNIT_7;傳回值跟上面一樣。
esp_err_t pcnt_counter_pause(pcnt_unit_t pcnt_unit):暫停計數器
esp_err_t pcnt_counter_resume(pcnt_unit_t pcnt_unit):重啟計數器
esp_err_t pcnt_counter_clear(pcnt_unit_t pcnt_unit):清除計數值
底下是取得脈衝計數值的函式,第一個參數是「脈衝計時器單元」,第二個參數則是指向儲存計數值的int16_t型態的變數;傳回值跟上面一樣。
esp_err_t pcnt_get_counter_value(pcnt_unit_t pcnt_unit, int16_t *count);
第一個脈衝計數器的完整程式碼
建立一個「順時針轉動」旋轉編碼器時,增加計數值;「逆時針轉動」時,數值維持不變;計數值上限為10。完整的程式碼如下:
#include <driver/pcnt.h> // 引用pcnt.h程式庫 #define CLK_PIN 4 // 旋轉編碼器的接腳,請自行修改。 #define DT_PIN 5 #define SW_PIN 6 // 初始化脈衝計時器 void initPCNT() { pcnt_config_t pcnt_config = { .pulse_gpio_num = CLK_PIN, // 脈衝訊號腳 .ctrl_gpio_num = DT_PIN, // 控制訊號腳 .lctrl_mode = PCNT_MODE_DISABLE, // 控制訊號低電位時:取消。 .hctrl_mode = PCNT_MODE_KEEP, // 控制訊號高電位時:維持以下設定。 .pos_mode = PCNT_COUNT_INC, // 上升邊緣:計數+1 .neg_mode = PCNT_COUNT_DIS, // 下降邊緣:不計數 .counter_h_lim = 10, // 數值上限 .counter_l_lim = -10, // 數值下限 .unit = PCNT_UNIT_0, // 計數器單元名稱 .channel = PCNT_CHANNEL_0 // 通道名稱 }; pcnt_unit_config(&pcnt_config); // 套用上述設定 pcnt_counter_pause(PCNT_UNIT_0); // 暫停脈衝計數器 pcnt_counter_clear(PCNT_UNIT_0); // 清除脈衝計數值 pcnt_counter_resume(PCNT_UNIT_0); // 重啟脈衝計數器 } void setup() { Serial.begin(115200); /* 全部接腳都設為「輸入」模式。如果你的旋轉編碼器沒有 內建上拉電阻,請將INPUT改成INPUT_PULLUP。 */ pinMode(CLK_PIN, INPUT); pinMode(DT_PIN, INPUT); pinMode(SW_PIN, INPUT); initPCNT(); // 初始化脈衝計時器 } void loop() { static int16_t lastCount = 0; // 紀錄「上一次」計數值 int16_t count = 0; // 暫存「本次」計數值 pcnt_get_counter_value(PCNT_UNIT_0, &count); // 取得脈衝計數值 if (lastCount != count) { lastCount = count; Serial.printf("%d\n", count); // 顯示計數值 } if (digitalRead(SW_PIN) == LOW) { // 若開關被按下… pcnt_counter_clear(PCNT_UNIT_0); // 清除計數值 } delay(10); }
編譯上傳到ESP32開發板之後,依順時針方向持續轉動旋轉編碼器,序列埠監控窗將顯示1, 2, 3, …. 9, 0, 1, 2, …。往逆時針方向轉動,數值不會改變。
旋轉方向增、減計數值的程式
把上一節的程式規則改成:順時針轉動時,增加計數值;逆時針轉動,減少計數值,也就是加入偵測脈衝訊號的「下降邊緣」,以及反向計數的「控制訊號」:
程式本體不變,僅需修改pcnt_config結構體:
pcnt_config_t pcnt_config = { .pulse_gpio_num = CLK_PIN, .ctrl_gpio_num = DT_PIN, .lctrl_mode = PCNT_MODE_REVERSE, // 低電位:反向 .hctrl_mode = PCNT_MODE_KEEP, // 高電位:保持不變 .pos_mode = PCNT_COUNT_INC, // 增加計數 .neg_mode = PCNT_COUNT_DEC, // 減少計數 .counter_h_lim = 10, .counter_l_lim = -10, .unit = PCNT_UNIT_0, .channel = PCNT_CHANNEL_0 };
編譯上傳到ESP32開發板之後,依順時針方向持續轉動旋轉編碼器,會增加計數值。往逆時針方向轉動,數值會減少。