こんにちは、shotaです。
前回の記事ではADCを使ってバッテリ電圧を計測しました。
今回はADCとGPIOを使って、物体検出センサ(いわゆる壁センサ)を動かします。
ブロクで書いたサンプルコードはGitHubに公開しています。
https://github.com/ShotaAk/especial/tree/master/examples
プログラムを書く前の下準備
プログラムを書く前に、Especialの回路図やESP32モジュールのデータシートを読み、情報を集めます。
物体検出センサ回路の回路図を確認
まずはじめに、Especialの回路図を確認します。
↓Especialの回路図はこちらです。
especial回路図.pdf
↓物体検出センサ回路の設計ブログ記事はこちらです
shotaのマイクロマウス研修15[回路設計③ 物体検出センサ回路]
今回必要なところは、物体検出センサ回路です。
物体検出回路は発光部分と受光部分に分かれます。
発光部分では、IRLED_R_FLとIRLED_L_FRで4つの赤外線LEDを点灯させます。
(なぜ2つの端子で4つのLEDを点灯させる回路になったのかは、上に貼ったブログ記事を参照してください。
ESP32回路ブロックを見ると、IRLED_R_FLはIO4に、IRLED_L_FRはIO2に接続されています。
受光部分では、AN_IRLED_FL、AN_IRLED_L、AN_IRLED_R、AN_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のチャンネルを確認します。
Pin Definitionsより、各ピンで使えるADCチャンネルはつぎのとおりです。
プログラムの作成
それではプログラムを作成します。
これまでに作ったLED点灯プログラムと、ADCでバッテリ電圧を読み取るプログラムを組み合わせます。
作成したプログラムがこちらです。
#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にも公開してます。
それでは、必要なところを解説します。
プログラムの解説
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の中です。
ここでは、次の動作を実行しています。
この動作の中で、オフセットを用いているところが重要です。
赤外線を発光していないときも、フォトリフレクタには何らかの光が入るため、センサ信号を出力します。
この信号は不要です。いわゆるノイズです。
このノイズを除去し、発光して反射された光だけを抽出するために、オフセット値を取得しています。
// オフセットを処理して、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); }
プログラムのビルドと実行
それではプログラムをビルドして実行します。
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本体には問題があります。
プログラムとハードの問題点
まず、プログラムについて。
を実現するために、
// 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は近い値になるはずです。
気になったので、各センサが反応し始める距離を定規で計測します。
センサからの距離ではないので、数値は比較するための目安です。
計測結果はこちらです。(計測中は1 msec delayが正しく動くように修正しています)
やはり、FRが反応し始める距離が短いですね。。。
解決できればブログにまとめます。
次回の記事
次回は、FreeRTOSの動作周波数の変更方法について書きます。
(FLとFRのセンサ値が違う問題も、解決できれば書きます)