ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【ESP32】自作ゼルダエフェクトーンを改良してみた

 前にゼルダエフェクトーンを自作してみましたが、電池の持ちが悪すぎたので今回スリープモードを導入した改良版を作りました。
 具体的には「ドアを開けた時だけ起動して、ドアが閉まっている時にはスリープモードになる」ようにしています。

 回路も一部変更しているので注意してください。使う材料は全く同じです。


1:回路図改良版
f:id:rikoubou:20170622121049p:plain
 修正箇所は「マグネットスイッチから出ているIO12に繋いでいた線をGNDに繋げている」点のみです。IO14のピンのHIGH、LOWでスリープモードの切り替えを行うようにプログラムも修正します。


2:プログラム改良版
 改良したプログラムは以下のようになります。

・Effectone_v2.ino

#include <esp_deep_sleep.h>

#define BUZZER_PIN 23       // ブザーを鳴らすためのピン
#define JUDGE_PIN 14        // 磁石スイッチの判断をするピン

#define BEAT 150            // 一つの音を鳴らす時間
#define LEDC_CHANNEL 0      // チャンネル
#define LEDC_TIMER_BIT 13
#define LEDC_BASE_FREQ 5000

void setup() {
  // マグネットスイッチに関係するピンの準備
  pinMode(JUDGE_PIN, INPUT);

  // 音を鳴らす準備
  ledcSetup(LEDC_CHANNEL, LEDC_BASE_FREQ, LEDC_TIMER_BIT);
  ledcAttachPin(BUZZER_PIN, LEDC_CHANNEL);

  callZeldaSound();   // ゼルダの謎解き音を鳴らす

  // スリープのための諸々の設定(今回はGPIO14ピンを使う)
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_AUTO);
  gpio_pullup_en(GPIO_NUM_14);
  gpio_pulldown_dis(GPIO_NUM_14);
  esp_deep_sleep_enable_ext0_wakeup(GPIO_NUM_14, 1); // GPIO14がHIGHになったらスリープモード解除

  // 扉が閉まるまで待つ
  do {
    ;
  } while (digitalRead(JUDGE_PIN) == HIGH);

  // 扉が閉まったらスリープモード実行
  esp_deep_sleep_start();
}

void loop() {
  // loopではなにもしない
}

/**
 * ゼルダの謎解き音
 */
void callZeldaSound() {
  ledcWriteTone(LEDC_CHANNEL, 3136); // ソ
  delay(BEAT);
  ledcWriteTone(LEDC_CHANNEL, 2960); // ♯ファ
  delay(BEAT);
  ledcWriteTone(LEDC_CHANNEL, 2489); // ♯レ
  delay(BEAT);
  ledcWriteTone(LEDC_CHANNEL, 1760); // ラ
  delay(BEAT);
  ledcWriteTone(LEDC_CHANNEL, 1661); // ♯ソ
  delay(BEAT);
  ledcWriteTone(LEDC_CHANNEL, 2637); // ミ
  delay(BEAT);
  ledcWriteTone(LEDC_CHANNEL, 3322); // ♯ソ
  delay(BEAT);
  ledcWriteTone(LEDC_CHANNEL, 4186); // ド
  delay(BEAT);
  ledcWriteTone(LEDC_CHANNEL, 0);    // 音を止める
}

 loop関数での処理を一切やめてsetup関数のみで色々やるようにしています。

 今回使用しているマグネットスイッチは磁石が近づいている時がLOW、磁石が遠くにある時がHIGHなので、無限ループで扉が閉まるまで待っています。扉がしまったらスリープモードに移行するようにしています。

 今回の改良で前回よりは電池の持ちがよくなりました。体感的には1日以上持つようになりました。(改良前は数時間で電池切れしていた)

 ESP32は結構電池を食うので、コンセントから電源を取らない場合はスリープモードも考慮する必要があるようです。


■改良前の記事とスリープモードについて
【ESP32】ゼルダエフェクトーンを自作してみた - ソースに絡まるエスカルゴ
【ESP32】スリープモードについて - ソースに絡まるエスカルゴ

【ESP32】マルチタスク中におけるdelayについて

rikoubou.hatenablog.com

 前回取り上げたマルチタスクの続きです。ESP32のタスクの中でミリ秒単位やマイクロ秒単位でのdelayを行う方法について色々わかったので記録しておきます。

 結論から言うと、実機で検証した結果マルチタスク中ではdelay、delayMicroseconds、microsといったArduinoでの時間を扱う関数が無効になるので代替関数を使用しなくてはならない」というものです。


1:ESP32で動いているOS
 前回のマルチタスクの記事の参考記事にあるように、ESP32で動いているOSは「freeRTOS」というOSらしいです。マルチタスクは、ArduinoではなくこのOSに対応する記述をすることで実現しています。
 つまり、マルチタスク中ではfreeRTOSに即した記述をする必要があるのです。
(※上記の理由から、Arduinoのdelayなどの関数がマルチタスク内で無効になっていると考えられます)


2:マルチタスク中におけるdelayの書き方
 では実際にdelayを行う方法です。以下のように「vTaskDelay」関数を使います。

void hoge() {
  Serial.println("hoge");
}

void hello() {
  const portTickType yDelay = 1000 / portTICK_RATE_MS; // 1000ms
  while(1) {
    Serial.println("hello");
    vTaskDelay(yDelay); // delay
  }
}

void hogeTask(void *pvParameters) {
  const portTickType xDelay = 3000 / portTICK_RATE_MS; // 3000ms
  while(1) {
    hoge();
    vTaskDelay(xDelay); // delay
  }
}

void helloTask(void *pvParameters) {
  hello();
}

void setup() {
  Serial.begin(115200);
  xTaskCreate(hogeTask,"hogeTask", 1024, NULL, 1, NULL);   // hogeTaskタスク登録して実行
  xTaskCreate(helloTask,"helloTask", 1024, NULL, 2, NULL); // helloTaskタスク登録して実行
}

void loop() {
}

 上記のプログラムを書き込んでシリアルモニタを立ち上げると、1秒ごとにhello、3秒ごとにhogeが出力されます。

const portTickType yDelay = 3000 / portTICK_RATE_MS; // 3000ms
const portTickType yDelay = 1000 / portTICK_RATE_MS; // 1000ms

 vTaskDelay引数の計算で直接の数値ではなくportTICK_RATE_MSを用いているのは、freeRTOSはどうやらTICKという単位で処理をしているらしく、portTICK_RATE_MSという「1ミリ秒辺りのTICK」の基準を使って正確な値を出すためです。
(※ちゃんと調べてないので合ってるかの確証はありません)


 またミリ秒よりもさらに小さいマイクロ秒単位でのdelayも可能です。vTaskDelayの引数に「小数」を設定しても問題ありません。

 たとえば8マイクロ秒delayさせたい場合は以下のようにします。

vTaskDelay((8 / portTICK_RATE_MS) / 1000); // 8μs delay


 以上がマルチタスク中でdelayを使う方法になります。マルチタスク中にdelayMicrosecondsなどがあってもコンパイルが通るため、実機でこの現象に気づくまでにえらく時間がかかりました。またそれをどうにか解決する方法にたどり着くまでも苦労しました。

 ネットで探してもイマイチ情報がないようなので、この記事が役に立てば幸いです。


■参考記事
なんか作ろうよ ESP-WROOM-32でマルチタスク
FreeRTOS のメモ - Qiita

【ESP32】マルチタスクについて

 ESP32でマルチタスクをする方法を探していたら見つけたので記録しておきます。

■ESP32でマルチタスクを行うプログラム
 基本的には参考記事のところにあるソースで良いのですが、なぜか自分のに書き込むと動かなかったので少し修正しました。

int maincount = 0; // メインカウンタ

// 裏で動かすタスク
void testTask(void *pvParameters) {
  int subcount = 1000; // サブカウンタ
  while(1) {
    Serial.println("sub task " + String(subcount));
    subcount++;
    delay(1000);
  }
}

void setup() {
 Serial.begin(115200);
 // タスクの登録(1とある引数が多分タスクのナンバリング)
 xTaskCreate(testTask,"testTask", 1024, NULL, 1, NULL);
}

void loop() {
  // loopの中では別の処理
  Serial.println("arduino loop "+ String(maincount));
  maincount++;
  delay(1000);
}

 上記をESP32に書き込んでからシリアルモニタを立ち上げると、以下のように二つのカウントアップ文字列が表示されます。
f:id:rikoubou:20170612153723p:plain


■参考記事
なんか作ろうよ ESP-WROOM-32でマルチタスク

■追加情報
マルチタスクでさらにわかったことを記事にしました。
rikoubou.hatenablog.com

【ESP32】赤外線通信について

 ESP32で赤外線を使って色々やってみようと思ったですが、ArduinoUnoで使えていたライブラリ等がESP32ではまだないようです。
 色々調べて行くとライブラリを使わずに赤外線通信を行なっている記事を見つけたので、それを元にESP32で動かした時のことを備忘録として記録しておきます。


1:材料
 今回自分が使ったのは以下のものですが、すべての材料は安いものでも大丈夫だと思います。

・赤外線LED
3φ砲弾型赤外線LED 940nm LIR034の通販ならマルツオンライン

・赤外線受光器
リモコン受光モジュール RPM7138-Rの通販ならマルツオンライン

・適当な単色LED
・ESP32-DevKitC×2
・単3電池ボックス(必要に応じて)
・200Ω程度の抵抗×2
・ブレッドボード×2


2:受信側について
・プログラム

#define READ_PIN 26
#define LOW_STATE 0
#define HIGH_STATE 1
 
void setup(){
  Serial.begin(115200);
  pinMode(READ_PIN,INPUT);
 
  Serial.println("Ready to receive");
}
 
void waitLow() {
  while (digitalRead(READ_PIN)==LOW) {
    ;
  }
}
 
int waitHigh() {
  unsigned long start = micros();
  while (digitalRead(READ_PIN)==HIGH) {
    if (micros() - start > 5000000) {
      return 1;
    }
  }
  return 0;
}
 
unsigned long now = micros();
unsigned long lastStateChangedMicros = micros();
int state = HIGH_STATE;
 
void loop() {
    if (state == LOW_STATE) {
      waitLow();
    } else {
      int ret = waitHigh();
      if (ret == 1) {
        Serial.println("");
        return;
      }
    }
 
    now = micros();
    Serial.print((now - lastStateChangedMicros) / 10, DEC);
    Serial.print(",");
    lastStateChangedMicros = now;
    if (state == HIGH_STATE) {
      state = LOW_STATE;
    } else {
      state = HIGH_STATE;
    }
}

 基本的には参考記事にあるものそのままです。

 回路は、材料にあげたのと同じものを使うのであれば、以下の赤外線受光器のデータシートにあるように繋ぎます。
f:id:rikoubou:20170612140529p:plain

 シリアルモニタを立ち上げてESP32のリセットボタンを押すと、「Ready to receive」が表示されます。
 この状態で赤外線受光器にリモコンを向けてボタンを押すと数値の羅列が表示され、受信できていることが確認できます。


3:送信側について
・プログラム

const int IR_SEND_PIN = 33; // 赤外線LEDのピン
const int LED_PIN = 25;     // LEDのピン

// 送信データ
unsigned int data[] = {1000,2000,3000,4000,5000,6000,7000};

void setup() {
  pinMode(IR_SEND_PIN, OUTPUT);
  pinMode(LED_PIN, OUTPUT);
}

void loop() {
  digitalWrite(LED_PIN, HIGH);
  sendSignal(); // 赤外線信号を送信
  digitalWrite(LED_PIN, LOW);
  delay(3000);
}

// dataの赤外線信号を送信
void sendSignal() {
  int dataSize = sizeof(data) / sizeof(data[0]);
  for (int cnt = 0; cnt < dataSize; cnt++) {
    unsigned long len = data[cnt]*10;
    unsigned long us = micros();
    do {
      digitalWrite(IR_SEND_PIN, 1 - (cnt&1));
      delayMicroseconds(8);
      digitalWrite(IR_SEND_PIN, 0);
      delayMicroseconds(7);
    } while (long(us + len - micros()) > 0); // 送信時間に達するまでループ
  }
}

 こちらも参考記事にあるものほぼそのままです。

 回路図も書くほどではないので省きます。LEDの足の長い方と出力ピンの間に抵抗を入れ、足の短い方をGNDに繋ぐだけです。
 単色LEDは赤外線が送信されていることを肉眼で確認するためです。


4:送受信の確認
 受信側、送信側のどちらも準備ができたら、両方とも電源を入れて赤外線LEDが赤外線受光器に当たるようにします。
 すると以下のような値が受信側のシリアルモニタに表示されるはずです。(以下の例では2回送信しています)
f:id:rikoubou:20170612141632p:plain

 一番最初の大きな値は無視します。それ以降はdataに設定した値とほぼ同じ値が送信されていることがわかります。
 何度か試した感じでは、誤差は最大で±20程度のようです。またdataとして送る一つの信号の長さは長い方が(1000以上ぐらい)の方が良いようです。


 以上、ESP32を使っての赤外線通信についてでした。


■参考記事
Arduinoで赤外線リモコンの信号を解析する
Arduinoで赤外線リモコン信号を発信する

【ESP32】スリープモードについて

 前に作成した自作ゼルダエフェクトーンが電池の持ちがなさすぎたので、ESP32でのスリープモードについて調べました。


■スリープモードについて
 結論からとしては以下のプログラムで実装できます。

#include "esp_deep_sleep.h"

void setup() {
  Serial.begin(115200);
  Serial.println("start ESP32");
  Serial.println("config IO");
  
  // スリープのための諸々の設定(今回はGPIO0ピンを使う)
  esp_deep_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_AUTO);
  gpio_pullup_en(GPIO_NUM_0);    // use pullup on GPIO
  gpio_pulldown_dis(GPIO_NUM_0); // not use pulldown on GPIO

  esp_deep_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0); // 指定したGPIOがLowの時に起動

  delay(5000);
  Serial.println("deep sleep start!");
  esp_deep_sleep_start(); // スリープモード実行
}

void loop() {
  
}

 このプログラムをESP32に書き込んでからシリアルモニタを立ち上げ、「deep sleep start!」が表示されたあとにスリープモードに入ります。

 今回はGPIO0ピンをLowにすると起動するように設定しているので、GPIO0をGNDに落とすと再び「start ESP32」や「deep sleep start!」が表示されます。またesp_deep_sleep_enable_ext0_wakeup関数の第二引数を1にすると、GPIO0がHighの時に起動するというようにできます。

 色々な関数で使われている「GPIO_NUM_0」という引数の意味は、ESP32のライブラリで使用されている定数で、GPIO_NUM_0〜GPIO_NUM_40まであります。gpio_num_tの型エラーが出た場合はこの定数を使用します。

 今回はGPIOピンからの起動を行いましたが、時間による起動もできるようです。(参考記事より)

 実際に測定はしてないのでわかりませんが、スリープモードだとかなりの節約ができるみたいです。常時駆動させるような場合はスリープモードが必須になると思うので、ご参考になれば。

(一日に二回も更新するなんて偉いぞ、自分)


・参考記事
ESP32 Deep Sleep のテスト (Hibernation mode) - ブログ/こばさんの wakwak 山歩き
pcbreflux: ESP32 Deep Sleep Example

【ESP32】5Vピンについて

akizukidenshi.com

秋月で売られているこのESP32-DevKitC、回路図の例などを調べると3V3が電源というのは様々なサイトで書かれていますが、5Vと書かれたピンが何を意味しているのかを今まで知らなかったので今回メモしておきます。


■5Vピンの謎
 結論から言うと、5Vピンの先には3.3Vに降圧するレギュレーターが入っているので5V以上でも動きます。
(「3V3が電源」という内容で書かれた記事や回路図が多いので混乱しがちですが、USBからの給電が5Vなのでそれを降圧する必要がありレギュレーターは必須なのです……)

 上記商品ページにある回路図を見ると「NCP1117」というレギュレーターが使われています。そのデータシートは以下になります。

 NCP1117 pdf, NCP1117 ディスクリプション, NCP1117 データシート, NCP1117 view ::: ALLDATASHEET :::

 これを読むとどうやら20Vぐらいまではいけるようです(未検証)

 実験として8V程度を5Vピンに入れてやっても焼けることはなかったので、大丈夫です。ただしどれぐらい発熱するかはわからないので、6Vぐらいまでにしておいた方が良いかもしれません。

【ESP32】PWMでモーターを制御する方法

ESP32でPWM制御をやってみました。ESP32ではPWM用のピンがあるわけではなく、ソフト側でPWMの設定を行います。


1:ESP32でのPWMの使い方
 ledcSetup、ledcAttachPin、ledcWriteの関数を使用します。
 ledcSetupでPWMの設定、ledcAttachPinでチャンネルとピンを対応、ledcWriteで実行という流れです。

// 1:ledcSetup(チャンネル, 周波数, PWMの範囲);
ledcSetup(0, 490, 8);

// 2:ledcAttachPin(ピン番号, チャンネル);
ledcAttachPin(7, 0);

// 3:ledcWrite(チャンネル, PWM値);
ledcWrite(0, 255);

※ledcSetupでのPWMの範囲は、8(8bit)を設定した場合は0〜255、10(10bit)の場合は0〜1023になります。

 これでPWMの使い方がわかったので、ESP32でモーターの制御をやっていきます。


1:材料
・電池ボックス
電池ボックス 単3×2本 リード線・スイッチ付: パーツ一般 秋月電子通商 電子部品 ネット通販

・モータードライバ(半田付けが必要)
DRV8835使用ステッピング&DCモータドライバモジュール: 組立キット 秋月電子通商 電子部品 ネット通販

・モーター
DCモーター FA−130RA−2270: パーツ一般 秋月電子通商 電子部品 ネット通販

・単3電池2本


2:プログラム

/* 使うピンの定義 */
const int IN1 = 25;
const int IN2 = 26;

/* チャンネルの定義 */
const int CHANNEL_0 = 0;
const int CHANNEL_1 = 1;

const int LEDC_TIMER_BIT = 8;   // PWMの範囲(8bitなら0〜255、10bitなら0〜1023)
const int LEDC_BASE_FREQ = 490; // 周波数(Hz)
const int VALUE_MAX = 255;      // PWMの最大値

void setup() {
  pinMode(IN1, OUTPUT); // IN1
  pinMode(IN2, OUTPUT); // IN2

  // ピンのセットアップ
  ledcSetup(CHANNEL_0, LEDC_BASE_FREQ, LEDC_TIMER_BIT);
  ledcSetup(CHANNEL_1, LEDC_BASE_FREQ, LEDC_TIMER_BIT);

  // ピンのチャンネルをセット
  ledcAttachPin(IN1, CHANNEL_0);
  ledcAttachPin(IN2, CHANNEL_1);
}

void loop() {
  forward(255); // 正転
  delay(3000);
  coast();      // 空転
  delay(3000);
  reverse(100); // 逆転
  delay(3000);
  brake();      // ブレーキ
  delay(3000);
}

// 正転
void forward(uint32_t pwm) {
  if (pwm > VALUE_MAX) {
    pwm = VALUE_MAX;
  }
  ledcWrite(CHANNEL_0, pwm);
  ledcWrite(CHANNEL_1, 0);
}

// 逆転
void reverse(uint32_t pwm) {
  if (pwm > VALUE_MAX) {
    pwm = VALUE_MAX;
  }
  ledcWrite(CHANNEL_0, 0);
  ledcWrite(CHANNEL_1, pwm);
}

// ブレーキ
void brake() {
  ledcWrite(CHANNEL_0, VALUE_MAX);
  ledcWrite(CHANNEL_1, VALUE_MAX);
}

// 空転
void coast() {
  ledcWrite(CHANNEL_0, 0);
  ledcWrite(CHANNEL_1, 0);
}


3:回路図
f:id:rikoubou:20170605183739p:plain


 上記のように繋げばモーターが正転、逆転を始めます。PWMの値を変更すると、モーターの回転数も変化します。

 ESP32が3Vで駆動できるので、それに合わせて低い電圧でも動くモータードライバを選びました。基盤が熱くなると注意書きがありますが、3V程度の使用であれば問題はないようです。低電圧でも動くモータードライバもいいですね。