ESP32 + SH1106 OLED + EC11エンコーダ 開発ガイド

データーシート

ESP32 + SH1106 OLED + EC11エンコーダ 開発ガイド

## 概要

このドキュメントは、ESP32でSH1106 OLEDディスプレイとEC11ロータリーエンコーダを使用するための完全なガイドです。

### 対象ハードウェア

| 項目 | 型番/仕様 |
|------|----------|
| マイコン | ESP-WROOM-32 |
| ディスプレイ | SH1106 1.3インチ OLED (I2C, 128x64) |
| エンコーダ | EC11 ロータリーエンコーダ |
| モジュール | Estardyn OLEDモジュール(SH1106ドライバ/EC11エンコーダ統合) |

---

## ハードウェア

### ピン接続

| モジュール側 | ESP32 | 機能 |
|-------------|-------|------|
| VCC (3V3-5V) | 3.3V | 電源 |
| GND | GND | グランド |
| OLED_SDA | GPIO21 | I2Cデータ |
| OLED_SCL | GPIO22 | I2Cクロック |
| ENCODER_TRA | GPIO32 | エンコーダA相 |
| ENCODER_TRB | GPIO33 | エンコーダB相 |
| ENCODER_PUSH | GPIO25 | エンコーダ押しボタン |
| CONFIRM | GPIO26 | 確認ボタン |
| BAK (BACK) | GPIO27 | 戻るボタン |

### 回路図

ESP32 モジュール ┌─────────┐ ┌─────────────┐ │ 3.3V├──────────────┤VCC │ │ GND├──────────────┤GND │ │ GPIO21├──────────────┤OLED_SDA │ │ GPIO22├──────────────┤OLED_SCL │ │ GPIO32├──────────────┤ENCODER_TRA │ │ GPIO33├──────────────┤ENCODER_TRB │ │ GPIO25├──────────────┤ENCODER_PUSH │ │ GPIO26├──────────────┤CONFIRM │ │ GPIO27├──────────────┤BAK │ └─────────┘ └─────────────┘


### ボタンの動作

すべてのボタンはプルアップ接続です。

| 状態 | 電圧 | digitalRead() |
|------|------|---------------|
| 押していない | HIGH (3.3V) | 1 |
| 押している | LOW (0V) | 0 |

### エンコーダの動作

EC11は2相(A相、B相)のインクリメンタルエンコーダです。

時計回り(CW)の波形: A相: ▁▁▁▔▔▔▁▁▁▔▔▔ B相: ▔▔▁▁▁▔▔▔▁▁▁▔ ↑ A相立ち上がり時、B相はLOW

反時計回り(CCW)の波形: A相: ▁▁▁▔▔▔▁▁▁▔▔▔ B相: ▁▁▔▔▔▁▁▁▔▔▔▁ ↑ A相立ち上がり時、B相はHIGH


1クリック(デテント)で4つの状態遷移が発生します(4逓倍)。

---

## ソフトウェア

### 必要なライブラリ

Arduino IDEのライブラリマネージャからインストールしてください。

| ライブラリ | 用途 | 備考 |
|-----------|------|------|
| U8g2 | OLED表示 | SSD1306用ライブラリは使用不可 |

### 対応Arduino Coreバージョン

| バージョン | PCNT API | 対応状況 |
|-----------|----------|----------|
| 2.x系 | driver/pcnt.h | ✅ 対応 |
| 3.x系 | driver/pulse_cnt.h | ✅ 対応 |

コード内で`ESP_ARDUINO_VERSION_MAJOR`マクロにより自動判定します。

### OLEDディスプレイ

#### 初期化

```cpp
#include <U8g2lib.h>
#include <Wire.h>

// SH1106 128x64 I2C (アドレス: 0x3C)
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

void setup() {
  u8g2.begin();
}

注意点

  • SSD1306ライブラリは使用不可: SH1106とSSD1306は互換性がありません
  • 画面がおかしい場合はドライバICを確認してください
  • I2Cアドレスは通常0x3C(0x3Dの場合もあり)

基本的な描画

Copyvoid loop() {
  u8g2.clearBuffer();                 // バッファクリア
  u8g2.setFont(u8g2_font_ncenB14_tr); // フォント設定
  u8g2.setCursor(0, 20);              // カーソル位置
  u8g2.print("Hello!");               // 文字描画
  u8g2.sendBuffer();                  // 画面に転送
}

よく使うフォント

フォント名サイズ用途
u8g2_font_5x7_tr極小詳細情報
u8g2_font_6x10_trステータス表示
u8g2_font_ncenB08_tr通常テキスト
u8g2_font_ncenB14_trタイトル、数値
u8g2_font_ncenB18_tr特大メイン表示

エンコーダ読み取り方式

2つの方式があります。用途に応じて選択してください。

方式1: 割り込み方式

特徴

  • シンプルで理解しやすい
  • どのESP32でも動作
  • CPUを少し使用(エンコーダ程度なら問題なし)
  • 移植性が高い

実装

#define ENC_A 32
#define ENC_B 33

volatile int rawCount = 0;

void IRAM_ATTR encISR() {
  static uint8_t lastAB = 0;
  
  uint8_t a = digitalRead(ENC_A);
  uint8_t b = digitalRead(ENC_B);
  uint8_t ab = (a << 1) | b;
  
  // 状態遷移テーブル
  static const int8_t trans[] = {
     0, -1,  1,  0,
     1,  0,  0, -1,
    -1,  0,  0,  1,
     0,  1, -1,  0
  };
  
  rawCount += trans[(lastAB << 2) | ab];
  lastAB = ab;
}

void setup() {
  pinMode(ENC_A, INPUT_PULLUP);
  pinMode(ENC_B, INPUT_PULLUP);
  attachInterrupt(ENC_A, encISR, CHANGE);
  attachInterrupt(ENC_B, encISR, CHANGE);
}

int getEncoderValue() {
  noInterrupts();
  int raw = rawCount;
  interrupts();
  
  // 4逓倍なので4で割る
  if (raw >= 0) {
    return raw / 4;
  } else {
    return (raw - 3) / 4;
  }
}

方式2: PCNT方式(推奨)

特徴

  • CPU負荷ゼロ(ハードウェアカウント)
  • ハードウェアグリッチフィルタ内蔵
  • 高速回転でも取りこぼさない
  • オーバーフロー対応可能
  • Arduino Coreバージョンで API が異なる

2.x/3.x両対応の完全な実装は「完全なサンプルコード」セクションを参照


PCNT設定パラメータ

2.x系 pcnt_config_t

パラメータ説明設定例
pulse_gpio_numパルス入力ピンENC_A または ENC_B
ctrl_gpio_num制御ピンENC_B または ENC_A
pos_mode立ち上がりエッジの動作PCNT_COUNT_INC
neg_mode立ち下がりエッジの動作PCNT_COUNT_DEC
lctrl_mode制御ピンLOW時の動作PCNT_MODE_KEEP/REVERSE
hctrl_mode制御ピンHIGH時の動作PCNT_MODE_REVERSE/KEEP
counter_h_limカウンタ上限10000
counter_l_limカウンタ下限-10000

4逓倍の正しい設定

Copy// チャンネル0: A相のエッジを検出
config0.pulse_gpio_num = ENC_A;
config0.ctrl_gpio_num  = ENC_B;
config0.lctrl_mode     = PCNT_MODE_KEEP;
config0.hctrl_mode     = PCNT_MODE_REVERSE;
config0.pos_mode       = PCNT_COUNT_INC;
config0.neg_mode       = PCNT_COUNT_DEC;

// チャンネル1: B相のエッジを検出
config1.pulse_gpio_num = ENC_B;
config1.ctrl_gpio_num  = ENC_A;
config1.lctrl_mode     = PCNT_MODE_REVERSE;  // ←ここが逆
config1.hctrl_mode     = PCNT_MODE_KEEP;     // ←ここが逆
config1.pos_mode       = PCNT_COUNT_INC;
config1.neg_mode       = PCNT_COUNT_DEC;

重要: チャンネル0とチャンネル1でlctrl_modehctrl_modeが入れ替わります。これを間違えるとカウントが打ち消し合います。


値の変換

raw値からエンコーダ値への変換

4逓倍なので4で割りますが、負の値の割り算に注意が必要です。

Copyint32_t rawToEnc(int32_t raw) {
  if (raw >= 0) {
    return raw / 4;
  } else {
    // 負の値は調整しないと0を2回通過する
    return (raw - 3) / 4;
  }
}
raw値通常の/4修正後
411
300
000
-10-1
-4-1-1
-5-1-2

完全なサンプルコード

デバッグツール(PCNT方式、2.x/3.x両対応、オーバーフロー対応)

Copy/*
 * ============================================================
 * PCNT ロータリーエンコーダ デバッグツール
 * Version: 7 (Final)
 * ============================================================
 */

#include <U8g2lib.h>
#include <Wire.h>

#if ESP_ARDUINO_VERSION_MAJOR >= 3
  #include "driver/pulse_cnt.h"
  #define USE_NEW_PCNT_API
#else
  #include "driver/pcnt.h"
#endif

#define VERSION "v7"

U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

#define ENC_A    32
#define ENC_B    33
#define ENC_BTN  25
#define CONFIRM  26
#define BACK     27

#define PCNT_H_LIM  10000
#define PCNT_L_LIM -10000
#define GLITCH_FILTER_NS 1000
#define GLITCH_FILTER_CLK 100

volatile int32_t overflowCount = 0;
int32_t encValue = 0;
int32_t lastFullCount = 0;

#ifdef USE_NEW_PCNT_API
  pcnt_unit_handle_t pcntUnit = NULL;
  
  bool pcntCallback(pcnt_unit_handle_t unit, const pcnt_watch_event_data_t *edata, void *ctx) {
    if (edata->watch_point_value == PCNT_H_LIM) {
      overflowCount++;
    } else if (edata->watch_point_value == PCNT_L_LIM) {
      overflowCount--;
    }
    return false;
  }
#else
  #define PCNT_UNIT PCNT_UNIT_0
  
  void IRAM_ATTR pcntISR(void *arg) {
    uint32_t status = 0;
    pcnt_get_event_status(PCNT_UNIT_0, &status);
    if (status & PCNT_EVT_H_LIM) overflowCount++;
    if (status & PCNT_EVT_L_LIM) overflowCount--;
  }
#endif

bool setupPCNT() {
#ifdef USE_NEW_PCNT_API
  pcnt_unit_config_t unitConfig = {
    .low_limit = PCNT_L_LIM,
    .high_limit = PCNT_H_LIM,
  };
  
  if (pcnt_new_unit(&unitConfig, &pcntUnit) != ESP_OK) return false;
  
  pcnt_glitch_filter_config_t filterConfig = { .max_glitch_ns = GLITCH_FILTER_NS };
  pcnt_unit_set_glitch_filter(pcntUnit, &filterConfig);
  
  pcnt_chan_config_t chanAConfig = { .edge_gpio_num = ENC_A, .level_gpio_num = ENC_B };
  pcnt_channel_handle_t chanA = NULL;
  pcnt_new_channel(pcntUnit, &chanAConfig, &chanA);
  pcnt_channel_set_edge_action(chanA, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE);
  pcnt_channel_set_level_action(chanA, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE);
  
  pcnt_chan_config_t chanBConfig = { .edge_gpio_num = ENC_B, .level_gpio_num = ENC_A };
  pcnt_channel_handle_t chanB = NULL;
  pcnt_new_channel(pcntUnit, &chanBConfig, &chanB);
  pcnt_channel_set_edge_action(chanB, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE);
  pcnt_channel_set_level_action(chanB, PCNT_CHANNEL_LEVEL_ACTION_INVERSE, PCNT_CHANNEL_LEVEL_ACTION_KEEP);
  
  pcnt_unit_add_watch_point(pcntUnit, PCNT_H_LIM);
  pcnt_unit_add_watch_point(pcntUnit, PCNT_L_LIM);
  
  pcnt_event_callbacks_t callbacks = { .on_reach = pcntCallback };
  pcnt_unit_register_event_callbacks(pcntUnit, &callbacks, NULL);
  
  pcnt_unit_enable(pcntUnit);
  pcnt_unit_clear_count(pcntUnit);
  pcnt_unit_start(pcntUnit);
  return true;

#else
  pcnt_config_t config0 = {};
  config0.pulse_gpio_num = ENC_A;
  config0.ctrl_gpio_num  = ENC_B;
  config0.lctrl_mode     = PCNT_MODE_KEEP;
  config0.hctrl_mode     = PCNT_MODE_REVERSE;
  config0.pos_mode       = PCNT_COUNT_INC;
  config0.neg_mode       = PCNT_COUNT_DEC;
  config0.counter_h_lim  = PCNT_H_LIM;
  config0.counter_l_lim  = PCNT_L_LIM;
  config0.unit           = PCNT_UNIT_0;
  config0.channel        = PCNT_CHANNEL_0;
  if (pcnt_unit_config(&config0) != ESP_OK) return false;
  
  pcnt_config_t config1 = {};
  config1.pulse_gpio_num = ENC_B;
  config1.ctrl_gpio_num  = ENC_A;
  config1.lctrl_mode     = PCNT_MODE_REVERSE;
  config1.hctrl_mode     = PCNT_MODE_KEEP;
  config1.pos_mode       = PCNT_COUNT_INC;
  config1.neg_mode       = PCNT_COUNT_DEC;
  config1.counter_h_lim  = PCNT_H_LIM;
  config1.counter_l_lim  = PCNT_L_LIM;
  config1.unit           = PCNT_UNIT_0;
  config1.channel        = PCNT_CHANNEL_1;
  if (pcnt_unit_config(&config1) != ESP_OK) return false;
  
  pcnt_set_filter_value(PCNT_UNIT, GLITCH_FILTER_CLK);
  pcnt_filter_enable(PCNT_UNIT);
  
  pcnt_event_enable(PCNT_UNIT, PCNT_EVT_H_LIM);
  pcnt_event_enable(PCNT_UNIT, PCNT_EVT_L_LIM);
  pcnt_isr_service_install(0);
  pcnt_isr_handler_add(PCNT_UNIT, pcntISR, NULL);
  
  pcnt_counter_pause(PCNT_UNIT);
  pcnt_counter_clear(PCNT_UNIT);
  pcnt_counter_resume(PCNT_UNIT);
  return true;
#endif
}

int32_t getFullCount() {
#ifdef USE_NEW_PCNT_API
  int rawCount = 0;
  if (pcntUnit != NULL) pcnt_unit_get_count(pcntUnit, &rawCount);
  return (overflowCount * PCNT_H_LIM) + rawCount;
#else
  int16_t rawCount = 0;
  pcnt_get_counter_value(PCNT_UNIT, &rawCount);
  return (overflowCount * PCNT_H_LIM) + rawCount;
#endif
}

void clearCount() {
#ifdef USE_NEW_PCNT_API
  if (pcntUnit != NULL) pcnt_unit_clear_count(pcntUnit);
#else
  pcnt_counter_clear(PCNT_UNIT);
#endif
  overflowCount = 0;
}

int32_t rawToEnc(int32_t raw) {
  if (raw >= 0) return raw / 4;
  else return (raw - 3) / 4;
}

void setup() {
  Serial.begin(115200);
  delay(500);
  
  Serial.println("============================================");
  Serial.print("PCNT エンコーダ デバッグツール ");
  Serial.println(VERSION);
  Serial.println("============================================");
  
  pinMode(ENC_BTN, INPUT_PULLUP);
  pinMode(CONFIRM, INPUT_PULLUP);
  pinMode(BACK, INPUT_PULLUP);
  
  if (!setupPCNT()) {
    Serial.println("PCNT初期化失敗!");
    while (1) delay(1000);
  }
  
  u8g2.begin();
  Serial.println("準備完了!");
}

void loop() {
  int32_t fullCount = getFullCount();
  encValue = rawToEnc(fullCount);
  
  int pinA = digitalRead(ENC_A);
  int pinB = digitalRead(ENC_B);
  int btnEnc = digitalRead(ENC_BTN);
  int btnConf = digitalRead(CONFIRM);
  int btnBack = digitalRead(BACK);
  
  if (fullCount != lastFullCount) {
    Serial.print("raw:");
    Serial.print(fullCount);
    Serial.print(" enc:");
    Serial.print(encValue);
    Serial.print(" ovf:");
    Serial.println(overflowCount);
    lastFullCount = fullCount;
  }
  
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_6x10_tr);
  u8g2.drawStr(0, 10, "[PCNT]");
#ifdef USE_NEW_PCNT_API
  u8g2.drawStr(40, 10, "3.x");
#else
  u8g2.drawStr(40, 10, "2.x");
#endif
  u8g2.drawStr(60, 10, "4x");
  u8g2.drawStr(80, 10, VERSION);
  
  u8g2.setFont(u8g2_font_ncenB14_tr);
  u8g2.setCursor(35, 30);
  u8g2.print(encValue);
  
  u8g2.setFont(u8g2_font_5x7_tr);
  u8g2.setCursor(0, 42);
  u8g2.print("raw:");
  u8g2.print(fullCount);
  u8g2.print(" ovf:");
  u8g2.print(overflowCount);
  
  u8g2.setCursor(0, 52);
  u8g2.print("A:");
  u8g2.print(pinA);
  u8g2.print(" B:");
  u8g2.print(pinB);
  
  u8g2.setCursor(0, 62);
  u8g2.print("E:");
  u8g2.print(btnEnc == LOW ? "*" : "-");
  u8g2.print(" C:");
  u8g2.print(btnConf == LOW ? "*" : "-");
  u8g2.print(" B:");
  u8g2.print(btnBack == LOW ? "*" : "-");
  
  u8g2.sendBuffer();
  
  if (btnEnc == LOW) {
    clearCount();
    Serial.println("リセット!");
    delay(200);
  }
  
  delay(10);
}

トラブルシューティング

OLEDが表示されない

症状原因対処
何も表示されないI2C接続不良SDA/SCL配線確認、I2Cスキャンでアドレス確認
ランダムなドットドライバIC違いSSD1306ではなくSH1106用ライブラリを使用
画面がずれる解像度設定違い128×64を確認

I2Cアドレススキャン

Copyvoid scanI2C() {
  Serial.println("I2Cスキャン中...");
  for (byte addr = 1; addr < 127; addr++) {
    Wire.beginTransmission(addr);
    if (Wire.endTransmission() == 0) {
      Serial.print("発見: 0x");
      Serial.println(addr, HEX);
    }
  }
}

エンコーダが動かない

症状原因対処
全く反応しないピン接続間違いA/Bピンの配線確認
値が増減しないPCNT設定ミス2チャンネルのlctrl/hctrl設定確認
値が打ち消し合うチャンネル設定が同じch0とch1でlctrl/hctrlを入れ替える
チャタリングフィルタ不足フィルタ値を増やす

PCNTが動かない(3.x系)

エラー: driver/pcnt.h: No such file or directory

→ 3.x系ではdriver/pulse_cnt.hを使用してください。

Stack smashing protect failure!

バッファオーバーフローです。sprintfのバッファサイズを確認するか、u8g2.print()を使用してください。

Copy// NG: バッファ溢れの可能性
char buf[16];
sprintf(buf, "A:%d B:%d tot:%d", a, b, total);

// OK: 安全
u8g2.print("A:");
u8g2.print(a);
u8g2.print(" B:");
u8g2.print(b);

参考リンク


更新履歴

バージョン日付内容
v1-v4デバッグ・調整
v54逓倍動作確認
v6オーバーフロー対応追加
v72.x/3.x両対応、最終版
タイトルとURLをコピーしました