こんにちは、ENGかぴです。
Grove Beginner kit for Arduinoは初心者のためのスターターキットです。サウンドセンサーなど複数のセンサーが実装されておりArduinoライブラリの使い方やC言語(C++)の学習ができます。
本記事ではGrove Beginner Kit for Arduino(Seeed Studio製)を使用してサウンドセンサーから取得したアナログデータをOLEDに表示します。
Grove Beginner kit for Arduino(以下スターターキットとします。)はArduino UNOと同じシリーズのマイコンを使用しているためArduino UNOと同じように開発することができます。Arduinoのライブラリを使用して動作確認したことをまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
開発環境の作り方
スターターキットの詳細はSeeed Studioのページにまとめられています。Arduinoライブラリの説明やセンサーの使用例やPWMの考え方などが解説されています。
Grove Beginner Kit for Arduino – Seeed Wiki (seeedstudio.com)
日本語対応のページが作成されていませんが、Google Chromeのブラウザーによる変換機能を使用することで日本語でページを表示することができます。
スターターキットにはソフトがプリインストールされているためUSBを接続すると各センサーの動作を確認することができます。プリインストールされているソフトは公開されているのでいつでも初期状態に戻すことができます。
タイマ管理で使用するMsTimer2ライブラリとOLEDの表示を行うU8g2ライブラリは標準ライブラリとして搭載されていないためライブラリマネージャーで追加します。ライブラリの追加の仕方や使用例については下記記事にまとめています。
Grove Beginner Kit for Arduinoの使い方
Grove Beginner Kit for Arduinoの照度センサーの使い方
MsTimer2はサウンドセンサーの値を取得するタイミングを管理し、U8g2は取得したデータをOLEDに表示する目的で使用します。
PR: わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジムPython入門講座の申込
サウンドセンサーの測定値を取得する
スターターキットでは電源を入れるとサウンドセンサーがONします。サウンドセンサーは音声などの音源に応答して電圧値を出力します。ArduinoのAD変換でサウンドセンサーの電圧値を取得して音声の様子をOLEDに表示します。
AD変換で測定値を取得する
sound.dat[sound.wp] = analogRead(PIN_A0);
if(++sound.wp >= 10){
sound.wp = 0;
sum =0;
for(i=0; i<10; i++){
sum += sound.dat[i];
}
sound.filt[sound.fwp] = sum / 10;
}
Arduinoの標準関数であるanalogRead()関数を使用してサウンドセンサーの測定値をAD変換します。標準ライブラリであるため追加のインクルードをせずに使用することができます。analogRead()関数の引数にデータを取得するアナログピンの番号を指定します。戻り値がAD変換値になるので変数に格納して使用します。
例では取得したデータを保管する場所を更新しながら貯めていき10回分の平均値を計算した結果をAD変換値として採用しています。
アナログデータはノイズの影響を受けることがあるため複数回取得したデータの平均値をとる(ローパスフィルタの効果)ことでノイズによるデータのふらつきを防止する効果があります。
サウンドセンサーの測定値の履歴を保存する
サウンドセンサーの測定値が一定の値を超えた時をトリガとして履歴を保存します。トリガの前後25回分の測定データを保存します。サウンドセンサーの測定は10ms毎に行っているためトリガの前後250msのデータになります。
sound.filt[]は測定値の平均値を格納する配列のバッファです。保存先を管理するwpを更新しながら測定値を格納します。hist.data[]はsound.filt[]のデータを最古のデータから並び替えしたデータを格納する配列のバッファです。
if(( sound.filt[sound.fwp] >= HIST_FLG) && (hist.flg == false) ){
//トリガ(イベント)発生
hist.flg = true;
hist.cnt =0;
}
if(++sound.fwp >= DATA_MAX ){ //wpを更新
sound.fwp = 0;
}
if(hist.flg){
if(++hist.cnt >= DATA_HALF){ //トリガから25回経過したか
hist.ts = timsp; //タイムスタンプ
rp = sound.fwp; //最古のデータの位置
for(i=0;i<DATA_MAX;i++){ //hist.data[]に最古のデータから並び替え
hist.data[i] = sound.filt[rp];
if(++rp >= DATA_MAX){
rp = 0;
}
}
hist.flg = false;
}
}
sound.filt[]はwpを更新しながらデータを格納しているためバッファの先頭が最古のデータではありません。そのため履歴データの生成のため最古のデータから順に並び替えて履歴の表示に使用します。
トリガ発生時を起点に前後25回分(合計50回)を履歴データとするのでトリガ発生後25回分の測定値を取得してから履歴データを生成します。
トリガ発生後sound.filt[]を更新しながらhist.cntが25回以上になることを確認します。25回以上になると前後25回分のデータがsound.filt[]に保存されているのでタイプスタンプをセットしてsound.filt[]の最古のデータからhist.data[]に並び替えながら測定値を格納します。
sound.filt[]においてwp(sound.fwp)で現在の測定値を格納した後でwpを更新(sound.fwp+1)しているので更新後のwp(wp+1)が時系列でみた時最古のデータになります。
最古のデータの位置をrpの初期値としてhist.data[]配列のサイズ分順に取得して格納することで時系列で整理したデータになります。
トリガ(イベント発生)直後からデータを取得するのではなくイベント発生以前のデータを含めて取得することでトリガ発生の前後の測定値の様子を確認することができます。
OLEDに測定値を表示する
U8g2ライブラリでイベント発生時の測定値の様子を表示します。ライブラリの追加の仕方や使用例については下記記事を参考にしてください。
Grove Beginner Kit for Arduinoの使い方
Grove Beginner Kit for Arduinoの使い方 その2
以下ではOLEDに測定値の履歴を表示する方法を中心に説明します。
void SoundShow(void){
uint8_t i;
uint16_t y1,y2;
uint8_t x1,x2;
u8g2.firstPage(); //バッファをクリア
do{
u8g2.setFont(u8g2_font_5x7_mr);
u8g2.setCursor(10, 14);
u8g2.print("Hist:");
u8g2.print("ts:");
u8g2.drawFrame(70,5,50,50);
for(i=0;i<DATA_MAX-1;i++){
x1 = i + 70;
x2 = i + 1 +70;
y1 = 55 - histmng.hist[histNo].data[i]/20;
y2 = 55- histmng.hist[histNo].data[i+1]/20;
u8g2.drawLine(x1, y1,x2,y2);
}
}while(u8g2.nextPage());
}
最初にfirstPage()関数でOLEDに使用している内部のバッファをクリアします。
setFont()関数でOLEDに表示する文字のフォントを指定します。フォントの定義はライブラリのフォルダ(XXX\Arduino\libraries\U8g2\src\clib)にu8g2.hにあります。XXX\はArduinoのライブラリの保存先に指定しているアドレスです。
setCursor()関数でOLEDの表示位置にカーソルを合わせます。第1引数にX座標を指定します。X座標は画面左から右に向かって進みます。第2引数にY座標を指定します。Y座標は画面上から下に向かって進みます。
print()関数は引数に指定した文字列を表示します。変数を引数に指定した場合は引数の値を文字列に変換して表示します。print()関数で文字列と取得したデータを表示しています。
drawFrame()関数は四角形のフレームを描きます。第1引数にX座標、第2引数にY座標、第3引数に幅、第4引数に高さを指定します。
drawLine()関数は基準のポイントから次点のポイントまで線を描きます。第1引数に基準のX座標、第2引数に基準のY座標、第3引数に次点のX座標、第4引数に次点の座標を指定します。例ではfor()文でX座標とY座標を変更しながら線を描いて履歴データをグラフ表示しています。
Y座標はサウンドセンサーの測定値を指定しますが、測定レンジをグラフの表示範囲に入れるため換算しています。AD変換値の最大値は1023なので20で割って最大値が50になるように調整しています。
nextPage()関数で行データごとのデータを更新してOLEDの表示を作りますが、ウェイトが必要となるためdo while()で表示が終えるまでループさせています。本記事では約120ms必要でした。OLEDの表示は遅延が発生するため頻繁な更新はしないように調整が必要です。
動作確認
電源を入れるとサウンドセンサーの履歴を表示します。履歴がない場合はグラフの表示はありません。スターターキットの可変抵抗で表示する履歴を最大で5つ選択できるようにしています。
サウンドセンサーに「あ~っ」と発声したとき測定値が閾値を超えると履歴保存してOLEDに表示します。Hist: に表示している履歴番号0~4を表示し、ts: に電源を入れてからの秒数のタイムスタンプを表示します。
Arduino IDEのシリアルプロッタに履歴のデータを表示して確認を行いました。OLEDのグラフは表示の分解能が低くなるため細かな部分は表現できていませんがとシリアルプロッタの表示と類似の波形になっていることが確認できました。
複数回発声して残したサウンドセンサーの履歴を可変抵抗で表示する履歴を変更しながら確認することができました。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include <MsTimer2.h>
#include <U8g2lib.h>
#define TIME_UP 0
#define TIME_OFF -1
#define TIM_OLED 20
#define TIM_HIST 100
#define TIM_1S 100
#define TIM_AD 2
#define PIN_A0 A2 //サウンド
#define PIN_A1 A0 //可変抵抗
#define FILT_MAX 10
#define DATA_MAX 50
#define ADBUF_MAX 4
#define DATA_HALF (DATA_MAX/2)
#define HIST_SZ 5
#define HIST_FLG 500
#define LINE_BOTTOM 55
#define LINE_Y 50
#define LINE_X 70
struct SOUND_TYP{
uint8_t wp;
uint8_t fwp;
uint16_t dat[FILT_MAX];
uint16_t filt[DATA_MAX];
};
struct HIST_TYP{
uint32_t ts;
uint16_t data[DATA_MAX];
bool flg;
uint8_t cnt;
};
struct HIST_MNG{
uint8_t hwp;
HIST_TYP hist[HIST_SZ];
};
struct ADFILT{
uint8_t wp;
uint16_t adbuf[ADBUF_MAX];
uint16_t ad0;
};
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R2, /* reset=*/U8X8_PIN_NONE);
int16_t timmeas =0;// TIM_MEAS;
int16_t timcnt;
int16_t cnt1s;
int16_t timad;
SOUND_TYP sound;
HIST_TYP hist;
HIST_MNG histmng;
uint32_t timsp;
ADFILT ad;
uint8_t histNo;
/* プロトタイプ宣言 */
void mainTimer(void);
void mainApp(void);
void SoundShow(void);
void setup() {
Serial.begin(115200);
u8g2.begin();
MsTimer2::set(10, mainTimer); // 10ms period
MsTimer2::start();
}
void loop() {
mainApp();
}
/* メイン処理 */
void mainApp(void){
uint8_t i;
uint16_t sum;
if( timmeas == TIME_UP ){
timmeas = TIM_HIST;
SoundShow();
for(i=0;i< DATA_MAX;i++){
Serial.println(histmng.hist[histNo].data[i]);
}
}
if( timad == TIME_UP ){
timad = TIM_AD;
ad.adbuf[ad.wp] = analogRead(PIN_A1);
if( ++ad.wp >= ADBUF_MAX){
ad.wp = 0;
}
sum=0;
for( i=0; i < ADBUF_MAX; i++){
sum += ad.adbuf[i];
}
ad.ad0 = sum /ADBUF_MAX;
histNo = ad.ad0/200;
if(histNo >= HIST_SZ){
histNo = HIST_SZ -1;
}
}
}
/* タイマ管理 */
void mainTimer(void){
uint16_t sum;
uint8_t i;
uint8_t rp;
if( timmeas > TIME_UP ){
--timmeas;
}
if( timad > TIME_UP){
--timad;
}
if( ++cnt1s >= TIM_1S){
cnt1s -= TIM_1S;
++timsp;
}
sound.dat[sound.wp] = analogRead(PIN_A0);
if(++sound.wp >= FILT_MAX ){
sound.wp = 0;
sum =0;
for(i=0; i<FILT_MAX; i++){
sum += sound.dat[i];
}
sound.filt[sound.fwp] = sum / FILT_MAX;
if(( sound.filt[sound.fwp] >= HIST_FLG) && (hist.flg == false) ){
hist.flg = true;
hist.cnt =0;
}
if(++sound.fwp >= DATA_MAX ){
sound.fwp = 0;
}
if(hist.flg){
if(++hist.cnt >= DATA_HALF){
hist.ts = timsp;
rp = sound.fwp;
for(i=0;i<DATA_MAX;i++){
hist.data[i] = sound.filt[rp];
if(++rp >= DATA_MAX){
rp = 0;
}
}
hist.flg = false;
histmng.hist[histmng.hwp] = hist;
if(++histmng.hwp >= HIST_SZ){
histmng.hwp = 0;
}
}
}
}
}
/* OLED表示*/
void SoundShow(void){
uint8_t i;
uint16_t y1,y2;
uint8_t x1,x2;
uint8_t x3,x4;
uint8_t y3,y4,y5,y6;
u8g2.firstPage(); //バッファをクリア
do{
u8g2.setFont(u8g2_font_5x7_mr);
u8g2.setCursor(10, 14);
u8g2.print("Hist:");
u8g2.setCursor(12, 24);
u8g2.print(histNo);
u8g2.setCursor(10, 34);
u8g2.print("ts:");
u8g2.setCursor(12, 44);
u8g2.print(histmng.hist[histNo].ts);
u8g2.drawFrame(LINE_X,5,DATA_MAX,LINE_Y);
for(i=0;i<DATA_MAX-1;i++){
x1 = i + LINE_X;
x2 = i + 1 +LINE_X;
y1 = LINE_BOTTOM - histmng.hist[histNo].data[i]/20;
y2 = LINE_BOTTOM - histmng.hist[histNo].data[i+1]/20;
u8g2.drawLine(x1, y1,x2,y2);
}
x3 = LINE_X + DATA_HALF;
x4 = LINE_X + DATA_MAX;
u8g2.drawLine(x3,LINE_BOTTOM-2,x3,LINE_BOTTOM+2);
u8g2.drawLine(x4,LINE_BOTTOM-2,x4,LINE_BOTTOM+2);
u8g2.setCursor(x3-5,LINE_BOTTOM+9);
u8g2.print(DATA_HALF);
u8g2.setCursor(x4-2,LINE_BOTTOM+9);
u8g2.print(DATA_MAX);
y3 = LINE_BOTTOM - LINE_Y*1/4;
y4 = LINE_BOTTOM - LINE_Y*2/4;
y5 = LINE_BOTTOM - LINE_Y*3/4;
y6 = LINE_BOTTOM - LINE_Y;
u8g2.drawLine(LINE_X-2,y3,LINE_X+2,y3);
u8g2.setCursor(LINE_X-18,y3+3);
u8g2.print("250");
u8g2.drawLine(LINE_X-2,y4,LINE_X+2,y4);
u8g2.setCursor(LINE_X-18,y4+3);
u8g2.print("500");
u8g2.drawLine(LINE_X-2,y5,LINE_X+2,y5);
u8g2.setCursor(LINE_X-18,y5+3);
u8g2.print("750");
u8g2.drawLine(LINE_X-2,y6,LINE_X+2,y6);
u8g2.setCursor(LINE_X-21,y6+3);
u8g2.print("1000");
}while(u8g2.nextPage());
}
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
【クリエイターズファクトリー】卒業がない!挫折する心配なし!Webスクール説明会申し込み
最後まで、読んでいただきありがとうございました。