ESP32マウス(shota)マウス自作研修

ESP32マウスPart.32 GPIOとADCで物体検出センサを動かす

ESP32マウス(shota)
Especial - 物体検出センサの実験

こんにちは、shotaです。

前回の記事ではADCを使ってバッテリ電圧を計測しました。

今回はADCとGPIOを使って、物体検出センサ(いわゆる壁センサ)を動かします。

ブロクで書いたサンプルコードはGitHubに公開しています。
https://github.com/ShotaAk/especial/tree/master/examples

初めてのオリジナルマウス Especial

プログラムを書く前の下準備

プログラムを書く前に、Especialの回路図やESP32モジュールのデータシートを読み、情報を集めます。

物体検出センサ回路の回路図を確認

まずはじめに、Especialの回路図を確認します。

↓Especialの回路図はこちらです。
especial回路図.pdf

↓物体検出センサ回路の設計ブログ記事はこちらです
shotaのマイクロマウス研修15[回路設計③ 物体検出センサ回路]

今回必要なところは、物体検出センサ回路です。

Especial – 物体検出センサ回路

Especial – 物体検出センサ回路の接続先

物体検出回路は発光部分と受光部分に分かれます。
発光部分では、IRLED_R_FLIRLED_L_FRで4つの赤外線LEDを点灯させます。
(なぜ2つの端子で4つのLEDを点灯させる回路になったのかは、上に貼ったブログ記事を参照してください。
ESP32回路ブロックを見ると、IRLED_R_FLはIO4に、IRLED_L_FRはIO2に接続されています。

受光部分では、AN_IRLED_FLAN_IRLED_LAN_IRLED_RAN_IRLED_FRで電圧値を取得します。
ESP32回路ブロックを見ると、AN_IRLED_FLはSENSOR_VNに、AN_IRLED_LはIO34に、AN_IRLED_RはIO35に、AN_IRLED_FRはIO32に接続されています。
ESP32のピンにかかる電圧値は、フォトトランジスタの受光量(抵抗値)によって0 ~ 3.3Vに変化するので、その値をADCで読み取ります。
(実際には、フォトトランジスタのCE間にも電圧がかかるので、3.3Vには達しません)

これで必要な情報がそろいました。
GPIO機能を使って、IO4とIO2経由でLEDを点灯せさます。
ADC機能を使って、SENSOR_VN、IO34、IO35、IO32経由でセンサ値を取得します。

ADCのチャンネルを確認

つぎに、ESP32モジュールのデータシートを読み、
SENSOR_VN、IO34、IO35、IO32で使えるADCのチャンネルを確認します。

物体検出センサで使うADCピン

Pin Definitionsより、各ピンで使えるADCチャンネルはつぎのとおりです。

  • AN_IRLED_FLのSENSOR_VNピンは、ADC1_CH3(ADC 1ユニットのチャンネル3)
  • AN_IRLED_LのIO34ピンは、ADC1_CH6
  • AN_IRLED_RのIO35ピンは、ADC1_CH7
  • AN_IRLED_FRのIO32ピンは、ADC1_CH4
  • プログラムの作成

    それではプログラムを作成します。

    これまでに作ったLED点灯プログラムと、ADCでバッテリ電圧を読み取るプログラムを組み合わせます。

    Especial LED点灯サンプルコード

    Especial バッテリ電圧取得サンプルコード

    作成したプログラムがこちらです。

    #include <stdio.h>
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "driver/adc.h"
    #include "esp_adc_cal.h"
    #include "driver/gpio.h"
    
    
    // 公式マニュアル:
    // https://docs.espressif.com/projects/esp-idf/en/stable/api-reference/peripherals/gpio.html
    // https://docs.espressif.com/projects/esp-idf/en/stable/api-reference/peripherals/adc.html
    
    void app_main()
    {
        // ----- GPIOの設定 -----
        static const gpio_num_t GPIO_L_FR = GPIO_NUM_2;
        static const gpio_num_t GPIO_R_FL = GPIO_NUM_4;
    
        gpio_config_t io_conf;
        // 割り込みをしない
        io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
        // 出力モード
        io_conf.mode = GPIO_MODE_OUTPUT;
        // 設定したいピンのビットマスク
        io_conf.pin_bit_mask = ((1ULL<<GPIO_L_FR) | (1ULL<<GPIO_R_FL));
        // 内部プルダウンしない
        io_conf.pull_down_en = 0;
        // 内部プルアップしない
        io_conf.pull_up_en = 0;
        // 設定をセットする
        gpio_config(&io_conf);
    
        // ----- ADCの設定 -----
        enum AN_IR{
            AN_IR_FL = 0,
            AN_IR_L,
            AN_IR_R,
            AN_IR_FR,
            AN_IR_SIZE
        };
    
        static const adc_unit_t         UNIT = ADC_UNIT_1;
        static const adc_channel_t      CHANNELS[AN_IR_SIZE] = {
            ADC_CHANNEL_3, ADC_CHANNEL_6, ADC_CHANNEL_7, ADC_CHANNEL_4};
        // 11dB減衰を設定。フルスケールレンジは3.9V
        static const adc_atten_t        ATTEN = ADC_ATTEN_DB_11;
        // ADCの分解能を12bit (0~4095)に設定
        static const adc_bits_width_t   WIDTH = ADC_WIDTH_BIT_12;
        // eFuseメモリのVrefを使うため、このデフォルト値は使用されない
        static const uint32_t DEFAULT_VREF = 1100;
    
        adc1_config_width(WIDTH);
        // 各チャンネルの減衰量を設定
        for(int ir_i=0; ir_i<AN_IR_SIZE; ir_i++){
            adc1_config_channel_atten(CHANNELS[ir_i], ATTEN);
        }
        // ADCの特性を設定
        esp_adc_cal_characteristics_t *adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t));
        esp_adc_cal_characterize(UNIT, ATTEN, WIDTH, DEFAULT_VREF, adc_chars);
    
        uint32_t adc_readings[AN_IR_SIZE] = {0};
        uint32_t adc_offsets[AN_IR_SIZE] = {0};
        uint32_t sense_millivolts[AN_IR_SIZE] = {0};
        while (1) {
            // 反射する前のセンサ値を取得(オフセット)
            adc_offsets[AN_IR_L] = adc1_get_raw((adc1_channel_t)CHANNELS[AN_IR_L]);
            adc_offsets[AN_IR_FR] = adc1_get_raw((adc1_channel_t)CHANNELS[AN_IR_FR]);
            // LED ON
            gpio_set_level(GPIO_L_FR, 1);
            // 壁に反射するまで待つ
            vTaskDelay(1 / portTICK_RATE_MS);
            // ADC
            adc_readings[AN_IR_L] = adc1_get_raw((adc1_channel_t)CHANNELS[AN_IR_L]);
            adc_readings[AN_IR_FR] = adc1_get_raw((adc1_channel_t)CHANNELS[AN_IR_FR]);
            // LED OFF
            gpio_set_level(GPIO_L_FR, 0);
    
    
            // 反射する前のセンサ値を取得(オフセット)
            adc_offsets[AN_IR_R] = adc1_get_raw((adc1_channel_t)CHANNELS[AN_IR_R]);
            adc_offsets[AN_IR_FL] = adc1_get_raw((adc1_channel_t)CHANNELS[AN_IR_FL]);
            // LED ON
            gpio_set_level(GPIO_R_FL, 1);
            // 壁に反射するまで待つ
            vTaskDelay(1 / portTICK_RATE_MS);
            // ADC
            adc_readings[AN_IR_R] = adc1_get_raw((adc1_channel_t)CHANNELS[AN_IR_R]);
            adc_readings[AN_IR_FL] = adc1_get_raw((adc1_channel_t)CHANNELS[AN_IR_FL]);
            // LED OFF
            gpio_set_level(GPIO_R_FL, 0);
    
            
            // オフセットを処理して、ADCの変換結果を電圧値に変換する
            for(int ir_i=0; ir_i<AN_IR_SIZE; ir_i++){
               // センサ値のオフセットを引く
               if(adc_readings[ir_i] < adc_offsets[ir_i]){
                   // アンダーフローを防ぐ
                   adc_readings[ir_i] = 0;
               }else{
                   adc_readings[ir_i] -= adc_offsets[ir_i];
               }
    
               sense_millivolts[ir_i] =
                   esp_adc_cal_raw_to_voltage(adc_readings[ir_i], adc_chars);
            }
            
            printf("FL, L, R, FR(RAW): \t %d\t, %d\t, %d\t, %d\n",
                    adc_readings[AN_IR_FL], adc_readings[AN_IR_L],
                    adc_readings[AN_IR_R], adc_readings[AN_IR_FR]);
            printf("FL, L, R, FR( mV): \t %d\t, %d\t, %d\t, %d\n",
                    sense_millivolts[AN_IR_FL], sense_millivolts[AN_IR_L],
                    sense_millivolts[AN_IR_R], sense_millivolts[AN_IR_FR]);
    
            vTaskDelay(100 / portTICK_RATE_MS);
        }
    }
    

    GitHubにも公開してます。

    especial/examples/3_object_detector/main/main.c at master · ShotaAk/especial
    ESP32を搭載したマイクロマウスのプログラム. Contribute to ShotaAk/especial development by creating an account on GitHub.

    それでは、必要なところを解説します。

    プログラムの解説

    app_main関数の上部では、GPIOとADCの設定をしています。

    GPIOの設定はつぎのとおりです。
    IO2とIO4を出力ピンに設定しているだけです。

    void app_main()
    {
        // ----- GPIOの設定 -----
        static const gpio_num_t GPIO_L_FR = GPIO_NUM_2;
        static const gpio_num_t GPIO_R_FL = GPIO_NUM_4;
    
        gpio_config_t io_conf;
        // 割り込みをしない
        io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
        // 出力モード
        io_conf.mode = GPIO_MODE_OUTPUT;
        // 設定したいピンのビットマスク
        io_conf.pin_bit_mask = ((1ULL<<GPIO_L_FR) | (1ULL<<GPIO_R_FL));
        // 内部プルダウンしない
        io_conf.pull_down_en = 0;
        // 内部プルアップしない
        io_conf.pull_up_en = 0;
        // 設定をセットする
        gpio_config(&io_conf);
    

    ADCの設定はつぎのとおりです。

    ADCは4チャンネル使うので、enumと配列CHANNELSを使っているところが重要です。
    その他の設定は前回のプログラムと同じです。

        // ----- ADCの設定 -----
        enum AN_IR{
            AN_IR_FL = 0,
            AN_IR_L,
            AN_IR_R,
            AN_IR_FR,
            AN_IR_SIZE
        };
    
        static const adc_unit_t         UNIT = ADC_UNIT_1;
        static const adc_channel_t      CHANNELS[AN_IR_SIZE] = {
            ADC_CHANNEL_3, ADC_CHANNEL_6, ADC_CHANNEL_7, ADC_CHANNEL_4};
        // 11dB減衰を設定。フルスケールレンジは3.9V
        static const adc_atten_t        ATTEN = ADC_ATTEN_DB_11;
        // ADCの分解能を12bit (0~4095)に設定
        static const adc_bits_width_t   WIDTH = ADC_WIDTH_BIT_12;
        // eFuseメモリのVrefを使うため、このデフォルト値は使用されない
        static const uint32_t DEFAULT_VREF = 1100;
    
        adc1_config_width(WIDTH);
        // 各チャンネルの減衰量を設定
        for(int ir_i=0; ir_i<AN_IR_SIZE; ir_i++){
            adc1_config_channel_atten(CHANNELS[ir_i], ATTEN);
        }
        // ADCの特性を設定
        esp_adc_cal_characteristics_t *adc_chars = calloc(1, sizeof(esp_adc_cal_characteristics_t));
        esp_adc_cal_characterize(UNIT, ATTEN, WIDTH, DEFAULT_VREF, adc_chars);
    

    実際にセンサ値を取得しているのはwhileの中です。
    ここでは、次の動作を実行しています。

  • 1. センサLとFRの値を取得(オフセット値)
  • 2. LとFRの赤外線LEDを点灯
  • 3. 1 msec間、赤外線を受光するまで待機
  • 4. センサLとFRの値を取得(受光値)
  • 5. LとFRの赤外線LEDを消灯
  • 6~10. RとFLでも同じ動作を実施
  • 11. 受光値からオフセット値を引いて、電圧値に変換
  • 12. ADCの変換値と電圧値を表示
  • 13. 100 msec待機
  • この動作の中で、オフセットを用いているところが重要です。
    赤外線を発光していないときも、フォトリフレクタには何らかの光が入るため、センサ信号を出力します。
    この信号は不要です。いわゆるノイズです。

    このノイズを除去し、発光して反射された光だけを抽出するために、オフセット値を取得しています。

            // オフセットを処理して、ADCの変換結果を電圧値に変換する
            for(int ir_i=0; ir_i<AN_IR_SIZE; ir_i++){
               // センサ値のオフセットを引く
               if(adc_readings[ir_i] < adc_offsets[ir_i]){
                   // アンダーフローを防ぐ
                   adc_readings[ir_i] = 0;
               }else{
                   adc_readings[ir_i] -= adc_offsets[ir_i];
               }
    
               sense_millivolts[ir_i] =
                   esp_adc_cal_raw_to_voltage(adc_readings[ir_i], adc_chars);
            }
    

    プログラムのビルドと実行

    それではプログラムをビルドして実行します。

    Especial – 物体検出センサの実験

    cd ~/esp
     
    # GitHubからEspecialのプログラムをクローン
    git clone https://github.com/ShotaAk/especial.git
     
    # すでにクローンしている人は、プログラムを更新します
    cd especial
    git pull origin master
    
    # サンプルのプロジェクトまで移動
    cd especial/3_object_detector
     
    # ビルド・書き込み・モニタ起動
    make flash monitor
     
    --- 省略 ---
    FL, L, R, FR(RAW): 	 192	, 0	, 0	, 27
    FL, L, R, FR( mV): 	 301	, 142	, 142	, 164
    FL, L, R, FR(RAW): 	 195	, 0	, 0	, 31
    FL, L, R, FR( mV): 	 303	, 142	, 142	, 168
    FL, L, R, FR(RAW): 	 195	, 0	, 0	, 32
    FL, L, R, FR( mV): 	 303	, 142	, 142	, 168
    FL, L, R, FR(RAW): 	 194	, 0	, 0	, 30
    FL, L, R, FR( mV): 	 302	, 142	, 142	, 167
    FL, L, R, FR(RAW): 	 192	, 0	, 0	, 33
    FL, L, R, FR( mV): 	 301	, 142	, 142	, 169
    

    値が取得できました。

    が、しかし、このプログラムとEspecial本体には問題があります。

    プログラムとハードの問題点

    まず、プログラムについて。

  • 1 msec間、赤外線を受光するまで待機
  • を実現するために、

    // LED ON
    gpio_set_level(GPIO_L_FR, 1);
    // 壁に反射するまで待つ
    vTaskDelay(1 / portTICK_RATE_MS);
    

    のように実装しましたが、これだけでは1 msecの待機(delay)にはなりません。

    FreeRTOSの動作周波数を変更しなければいけません。
    詳細は次回の記事で書きます。

    次にハードの問題について。

    センサの取得結果は次のようになりました。

    FL, L, R, FR(RAW): 	 195	, 0	, 0	, 32
    FL, L, R, FR( mV): 	 303	, 142	, 142	, 168
    FL, L, R, FR(RAW): 	 194	, 0	, 0	, 30
    FL, L, R, FR( mV): 	 302	, 142	, 142	, 167
    FL, L, R, FR(RAW): 	 192	, 0	, 0	, 33
    FL, L, R, FR( mV): 	 301	, 142	, 142	, 169
    

    よく見ると、FLとFRの値が大きく違います。
    白壁をロボットの正面に置いたので、FLとFRは近い値になるはずです。

    気になったので、各センサが反応し始める距離を定規で計測します。
    センサからの距離ではないので、数値は比較するための目安です。

    Especial – 検出距離の測定(FL)

    Especial – 検出距離の測定(FR)

    Especial – 検出距離の測定(R)

    Especial – 検出距離の測定(L)

    計測結果はこちらです。(計測中は1 msec delayが正しく動くように修正しています)

  • FL : 9.9 mm
  • FR : 6.4 mm
  • R : 7.1 mm
  • L : 7.9 mm
  • やはり、FRが反応し始める距離が短いですね。。。

    解決できればブログにまとめます。

    次回の記事

    次回は、FreeRTOSの動作周波数の変更方法について書きます。
    (FLとFRのセンサ値が違う問題も、解決できれば書きます)

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