認識ESP32內建的脈衝計數器(一)

超圖解ESP32應用實作》分別使用「中斷常式」以及「查表法」,檢測附帶霍爾感測器的馬達的轉向和轉速,以及旋轉編碼器的轉向和脈衝數。本文將沿用書中「動手做15-1:使用自訂程式庫製作旋鈕介面」的電路,介紹採用ESP32內建的「脈衝計數器(Pulse Counter)」,編寫檢測旋轉編碼器的轉向和脈衝數的程式。

ESP32旋轉編碼器電路

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_ctrl_mode_t hctrl_mode:控制「控制訊號」為「高電位」時的脈衝訊號計數模式,其可能值跟上面的lctrl_mode一樣。
  • pcnt_count_mode_t pos_mode:設定「脈衝訊號」於「上升邊緣」的計數模式;pos意指“positive”(正向,由低到高)。pcnt_count_mode_t是此程式庫定義的enum常數,其可能值為下列之一:
    • PCNT_COUNT_DIS:不計數,“DIS”意指“DISABLE”(取消)。
    • PCNT_COUNT_INC:計數加1,“INC”意指“INCREASE”(增加)。
    • PCNT_COUNT_DEC:計數減1,“DEC”意指”DECREASE”(減少)。
  • pcnt_count_mode_t neg_mode:設定「脈衝訊號」於「下降邊緣」的計數模式;neg意指“negative”(負向,由高到低),其可能值跟上文的pos_mode一樣。
  • int16_t counter_h_lim:設定計數上限(最大值:32767);若超過上限,計數將重設為0。
  • int16_t counter_l_lim:設定計數的下限(最小值:- 32768);若超過下限,計數將重設為0。
  • pcnt_unit_t unit:指定要使用的PCNT單元,其可能值為PCNT_UNIT_0 ~ PCNT_UNIT_7,共8個單元可用。
  • pcnt_channel_t channel:指定PCNT單元所使用的通道,其可能值為PCNT_CHANNEL_0或PCNT_CHANNEL_1。
  • 假若要將脈衝計數器設定成:採用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開發板之後,依順時針方向持續轉動旋轉編碼器,會增加計數值。往逆時針方向轉動,數值會減少。

    序列監控窗

    Posts created 494

    發佈留言

    發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

    Related Posts

    Begin typing your search term above and press enter to search. Press ESC to cancel.

    Back To Top