ESP32でパルスカウンタを利用したロータリーエンコーダー

概要

ロータリーエンコーダーの利用例を見ているとArduino UNOで入力割り込みを使った例が多いです。ESP32はハードウエアでパルスカウンタを持っており、入力割り込みを利用しなくてもロータリーエンコーダーが使えるとのことで検証してみました。

ロータリーエンコーダー

持っていなかったと思っていたのですが、購入履歴を確認したらありました!

Amazonでは購入していないのですが、これと同じやつだと思います。回転するとかくかくってノッチがありまして、一周で20ノッチでした。

接続方法

本当はいけないのですが、レイアウト的に5Vを使っています。。。

ロータリーエンコーダーESP32
CLK(A相)GPIO0
DT(B相)GPIO36
SWGPIO26
+3V3
GNDGND

上記で接続します。ESP32は本来3.3V動作ですので、電源は3.3Vに接続してください。5Vを接続すると入力にも5Vが来るのでESP32では対応していない電圧になります。

実験1 単純確認

void setup() {
  Serial.begin(115200);
  delay(1000);

  pinMode(0, INPUT);
  pinMode(26, INPUT);
  pinMode(36, INPUT);
}

void loop() {
  Serial.printf("%d, %d, %d\n", digitalRead(0), digitalRead(26), digitalRead(36));
  delay(10);
}

とりあえず入力をしてみて、どんな動きをするのかを確認してみます。

上記の結果になりました。赤がGPIO26で押し込むとHIGHからLOWになります。右回転をしたときには青のGPIO0がLOWになってから、緑のGPIO36がLOWになります。左回転のときには逆で緑がLOWになってから青がLOWになります。

実験2 パルスカウンタを使ってみる

上記などを参考にパルスカウンタを使ってみたいと思います。

#include "driver/pcnt.h"

#define PULSE_PIN_CLK 0
#define PULSE_PIN_DT  36
#define PULSE_PIN_SW  26

void setup() {
  Serial.begin(115200);
  delay(1000);

  pinMode(26, INPUT);

  pcnt_config_t pcnt_config = {};
  pcnt_config.pulse_gpio_num  = PULSE_PIN_CLK;
  pcnt_config.ctrl_gpio_num   = PULSE_PIN_DT;
  pcnt_config.lctrl_mode      = PCNT_MODE_KEEP;
  pcnt_config.hctrl_mode      = PCNT_MODE_REVERSE;
  pcnt_config.pos_mode        = PCNT_COUNT_INC;
  pcnt_config.neg_mode        = PCNT_COUNT_DEC;
  pcnt_config.counter_h_lim   = 32767;
  pcnt_config.counter_l_lim   = -32768;
  pcnt_config.unit            = PCNT_UNIT_0;
  pcnt_config.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 loop() {
  int16_t count = 0;
  pcnt_get_counter_value(PCNT_UNIT_0, &count);
  Serial.printf("%d\n", count);
  if (digitalRead(PULSE_PIN_SW) == LOW) {
    pcnt_counter_clear(PCNT_UNIT_0);
  }
  delay(10);
}

結構シンプルですね。ボタンを押し込むとカウンタを0になるようにしています。

動かしてみると、1ノッチで2カウントします。一周20ノッチなので40カウントですね。右回転をするとプラスになるようにしています。使っているロータリーエンコーダーが右回転をするとマイナスになる場合には PCNT_MODE_KEEPと PCNT_MODE_REVERSEを入れ替えてみてください。

パルスカウンタの中でカウントしているので処理落ちを気にしなくても大丈夫です。また、カウンタの範囲ですがuint16_tなので-32768から32767までの範囲が指定できます。-100から100などに設定してみるとわかりやすいのですが、-100からマイナスにカウントされると0になります。範囲外のなると0クリアなので気をつけて使ってください。

実験3 パルスカウンタを2つ使ってみる

実験2ではA相のパルスカウンタで処理をしていましたが、B相にもパルスカウンタを設定することでカウント精度を上げることができます。

#include "driver/pcnt.h"

#define PULSE_PIN_CLK 0
#define PULSE_PIN_DT  36
#define PULSE_PIN_SW  26

void setup() {
  Serial.begin(115200);
  delay(1000);

  pinMode(26, INPUT);

  pcnt_config_t pcnt_config1 = {};
  pcnt_config1.pulse_gpio_num = PULSE_PIN_CLK;
  pcnt_config1.ctrl_gpio_num  = PULSE_PIN_DT;
  pcnt_config1.lctrl_mode     = PCNT_MODE_KEEP;
  pcnt_config1.hctrl_mode     = PCNT_MODE_REVERSE;
  pcnt_config1.pos_mode       = PCNT_COUNT_INC;
  pcnt_config1.neg_mode       = PCNT_COUNT_DEC;
  pcnt_config1.counter_h_lim  = 32767;
  pcnt_config1.counter_l_lim  = -32768;
  pcnt_config1.unit           = PCNT_UNIT_0;
  pcnt_config1.channel        = PCNT_CHANNEL_0;

  pcnt_config_t pcnt_config2 = {};
  pcnt_config2.pulse_gpio_num = PULSE_PIN_DT;
  pcnt_config2.ctrl_gpio_num  = PULSE_PIN_CLK;
  pcnt_config2.lctrl_mode     = PCNT_MODE_REVERSE;
  pcnt_config2.hctrl_mode     = PCNT_MODE_KEEP;
  pcnt_config2.pos_mode       = PCNT_COUNT_INC;
  pcnt_config2.neg_mode       = PCNT_COUNT_DEC;
  pcnt_config2.counter_h_lim  = 32767;
  pcnt_config2.counter_l_lim  = -32768;
  pcnt_config2.unit           = PCNT_UNIT_0;
  pcnt_config2.channel        = PCNT_CHANNEL_1;

  pcnt_unit_config(&pcnt_config1);
  pcnt_unit_config(&pcnt_config2);

  pcnt_counter_pause(PCNT_UNIT_0);
  pcnt_counter_clear(PCNT_UNIT_0);
  pcnt_counter_resume(PCNT_UNIT_0);
}

void loop() {
  int16_t count = 0;
  pcnt_get_counter_value(PCNT_UNIT_0, &count);
  Serial.printf("%d\n", count);
  if (digitalRead(PULSE_PIN_SW) == LOW) {
    pcnt_counter_clear(PCNT_UNIT_0);
  }
  delay(10);
}

ユニットは一つのままなので、カウントは共通です。A相とB相でピンとカウント方向の設定を入れ替えて設定しています。

この状態で動かすと1ノッチで4カウントします。ゆっくり回すとわかりやすいのですが、さっきよりも角度の精度が上がっています。

実験4 角度取得

#include "driver/pcnt.h"

#define PULSE_PIN_CLK 0
#define PULSE_PIN_DT  36
#define PULSE_PIN_SW  26

void setup() {
  Serial.begin(115200);
  delay(1000);

  pinMode(26, INPUT);

  pcnt_config_t pcnt_config1 = {};
  pcnt_config1.pulse_gpio_num = PULSE_PIN_CLK;
  pcnt_config1.ctrl_gpio_num  = PULSE_PIN_DT;
  pcnt_config1.lctrl_mode     = PCNT_MODE_KEEP;
  pcnt_config1.hctrl_mode     = PCNT_MODE_REVERSE;
  pcnt_config1.pos_mode       = PCNT_COUNT_INC;
  pcnt_config1.neg_mode       = PCNT_COUNT_DEC;
  pcnt_config1.counter_h_lim  = 80;
  pcnt_config1.counter_l_lim  = -80;
  pcnt_config1.unit           = PCNT_UNIT_0;
  pcnt_config1.channel        = PCNT_CHANNEL_0;

  pcnt_config_t pcnt_config2 = {};
  pcnt_config2.pulse_gpio_num = PULSE_PIN_DT;
  pcnt_config2.ctrl_gpio_num  = PULSE_PIN_CLK;
  pcnt_config2.lctrl_mode     = PCNT_MODE_REVERSE;
  pcnt_config2.hctrl_mode     = PCNT_MODE_KEEP;
  pcnt_config2.pos_mode       = PCNT_COUNT_INC;
  pcnt_config2.neg_mode       = PCNT_COUNT_DEC;
  pcnt_config2.counter_h_lim  = 80;
  pcnt_config2.counter_l_lim  = -80;
  pcnt_config2.unit           = PCNT_UNIT_0;
  pcnt_config2.channel        = PCNT_CHANNEL_1;

  pcnt_unit_config(&pcnt_config1);
  pcnt_unit_config(&pcnt_config2);

  pcnt_counter_pause(PCNT_UNIT_0);
  pcnt_counter_clear(PCNT_UNIT_0);
  pcnt_counter_resume(PCNT_UNIT_0);
}

void loop() {
  int16_t count = 0;
  pcnt_get_counter_value(PCNT_UNIT_0, &count);
  Serial.printf("%d %d\n", count, (count + 80) % 80);
  if (digitalRead(PULSE_PIN_SW) == LOW) {
    pcnt_counter_clear(PCNT_UNIT_0);
  }
  delay(10);
}

実験3のコードの範囲を-80から80に変更しています。これで1周80カウントなのでどの角度かを取得できます。結果のところで (count + 80) % 80 としていますが、これで角度が80段階で取得できることになります。角度だけ取得したい場合には一周のカウント数と範囲を同じにすることで実現できます。

実験5 割り込み追加

#include "driver/pcnt.h"
#include "soc/pcnt_struct.h"

#define PULSE_PIN_CLK 0
#define PULSE_PIN_DT  36
#define PULSE_PIN_SW  26

pcnt_isr_handle_t user_isr_handle = NULL;
static void IRAM_ATTR pcnt_intr_handler(void *arg);
volatile int intr_count = 0;

void setup() {
  Serial.begin(115200);
  delay(1000);

  pinMode(26, INPUT);

  pcnt_config_t pcnt_config1 = {};
  pcnt_config1.pulse_gpio_num = PULSE_PIN_CLK;
  pcnt_config1.ctrl_gpio_num  = PULSE_PIN_DT;
  pcnt_config1.lctrl_mode     = PCNT_MODE_KEEP;
  pcnt_config1.hctrl_mode     = PCNT_MODE_REVERSE;
  pcnt_config1.pos_mode       = PCNT_COUNT_INC;
  pcnt_config1.neg_mode       = PCNT_COUNT_DEC;
  pcnt_config1.counter_h_lim  = 80;
  pcnt_config1.counter_l_lim  = -80;
  pcnt_config1.unit           = PCNT_UNIT_0;
  pcnt_config1.channel        = PCNT_CHANNEL_0;

  pcnt_config_t pcnt_config2 = {};
  pcnt_config2.pulse_gpio_num = PULSE_PIN_DT;
  pcnt_config2.ctrl_gpio_num  = PULSE_PIN_CLK;
  pcnt_config2.lctrl_mode     = PCNT_MODE_REVERSE;
  pcnt_config2.hctrl_mode     = PCNT_MODE_KEEP;
  pcnt_config2.pos_mode       = PCNT_COUNT_INC;
  pcnt_config2.neg_mode       = PCNT_COUNT_DEC;
  pcnt_config2.counter_h_lim  = 80;
  pcnt_config2.counter_l_lim  = -80;
  pcnt_config2.unit           = PCNT_UNIT_0;
  pcnt_config2.channel        = PCNT_CHANNEL_1;

  pcnt_unit_config(&pcnt_config1);
  pcnt_unit_config(&pcnt_config2);

  pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_H_LIM);
  pcnt_event_enable(PCNT_UNIT_0, PCNT_EVT_L_LIM);

  pcnt_counter_pause(PCNT_UNIT_0);
  pcnt_counter_clear(PCNT_UNIT_0);
  pcnt_counter_resume(PCNT_UNIT_0);

  pcnt_isr_register(pcnt_intr_handler, NULL, 0, &user_isr_handle);
  pcnt_intr_enable(PCNT_UNIT_0);
}

void loop() {
  int16_t count = 0;
  pcnt_get_counter_value(PCNT_UNIT_0, &count);
  Serial.printf("%d %d %d \n", count, (count + 80) % 80, intr_count);
  if (digitalRead(PULSE_PIN_SW) == LOW) {
    pcnt_counter_clear(PCNT_UNIT_0);
    intr_count = 0;
  }
  delay(10);
}

static void IRAM_ATTR pcnt_intr_handler(void *arg) {
  if (PCNT.status_unit[PCNT_UNIT_0].h_lim_lat) {
    // 0: positive value to zero
    intr_count++;
  } else if (PCNT.status_unit[PCNT_UNIT_0].l_lim_lat) {
    // 1: negative value to zero
    intr_count--;
  }
  PCNT.int_clr.val = BIT(PCNT_UNIT_0);
}

割り込み機能があるので追加してみました。 pcnt_event_enable()でイベントを有効化します。イベントは0になったところ、カウンタがマックスになったところ、任意の値を超えたところが設定できます。今回はマックスを超えた場合に設定してみました。

割り込み関数の中ではPCNT変数から内部情報を取得して割り込みの処理を行います。PCNT.int_st.valにどのユニットが割り込みが入ったのかのフラグがあるのですが、今回はユニット0のみ利用しているので省略しています。

イベントの種類としてカウンタの範囲外になって0に戻ったときに割り込みを発生しています。種類をみてintr_countを変更して、右と左に何回転したのかを保存しています。

イベントの間隔を測定することで回転数なども計算できる気がします。

まとめ

実際のところノッチがあるロータリーエンコーダーの場合にはノッチ以上の精度はいらないのですが、モーターの回転などを測定する場合には高精度である方が好ましいです。また、手で回すロータリーエンコーダーだとカウント漏れがあまり発生しないはずですので入力割り込みでもそこそこ動くはずです。

情報は少なめなんですが、利用自体は比較的かんたんそうでした。

コメント

タイトルとURLをコピーしました