Grove Beginner Kit for Arduinoの使い方 その2

組み込みエンジニア

こんにちは、ENGかぴです。

Grove Beginner kit for Arduinoは初心者のためのスターターキットです。照度センサーなど複数のセンサーが実装されておりArduino環境でのライブラリの使い方C言語(C++)の学習ができます。

本記事ではGrove Beginner Kit for Arduino(Seeed Studio製)を使用してLight Sensor(以下は照度センサーとする)と可変抵抗の電圧をAD変換します。可変抵抗のAD変換値が照度センサーの値を下回った時にブザーを鳴らします。

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を接続すると各センサーの動作を確認することができます。プリインストールされているソフトは公開されているのでいつでも初期状態に戻すことができます。

Arduinoでの開発はArduino IDEを使って行います。インストールの手順などは下記記事にまとめています。

Arduinoの開発環境の作り方とスケッチ例の使い方

VsCodeを使用してもArduino環境でのソフト開発ができます。インストールの手順などは下記記事にまとめています。

VSCodeを使ったArduinoのソフト開発環境作りの手順

Arduino IDEでは書き込みとシリアルモニターを使用し、ソースコードをVsCodeで開発するなど使い分けることもできます。

標準ライブラリでブザーを鳴らす

電子ブザーは発振回路がついており極性があるものが多く電圧を加えるとブザーが鳴りますが、電流を流し続けると発熱などの問題があるためパルス波形を与えて消費電流を抑えながら電圧を加えることが推奨されます。スターターキットのブザーは圧電ブザーで電圧を加えるとブザーが鳴ります。

スターターキットではシルク印刷の横に~が付いているピンがPWM対応のピンになりますがピンによってキャリア周波数が変化します。その関係を以下の表にまとめています。

対象ピンキャリア周波数
3・9・10・11ピン490Hz
5・6ピン980Hz
PWMのキャリア周波数の関係

スターターキットは5ピンがブザーに接続されているためキャリア周波数980Hzで動作させることができます。圧電ブザーは電圧を加えると鳴りますが、PWM波形のデューティー比を変更することで若干音程を変えることができます。

#define PIN_BUZ 5

analogWrite(PIN_BUZ,duty); //最大値255に対して値を入れて比率を決める

標準ライブラリのanalogWrite()関数を使用するとPWM波形が生成できます。引数1はPWM波形を出力するピン番号を指定します。引数2は最大値255に対してパルスを切り替える値を指定します。値を大きくするほどデューティー比(PWMパルスのHIGH区間)が高くなります。

例えばデューティ比を50%を指定する場合は引数2に127を指定します。0にすると理論的にはPWM波形が生成されませんが、実際は僅かに誤差の範囲で出力されることがあります。最大値の255の場合についても同様で電圧出力が100%にならないことがあります。

音程を調整しながらブザーを鳴らすにはtone()関数を使用することもできますがtone()関数を使用すると動作が遅延します。今回の例ではブザーを鳴らすことが目的なので動作遅延の少ないanalogWrite()でブザーを鳴らしています。

Grove Light Sensor(照度センサー)の確認

Grove Light Sensor
Grove Light Sensor
LEDライトを当てた時のアナログ値:Lightの値
LEDライトを当てた時のアナログ値:Lightの値

スターターキットに付属している照度センサーの動作確認を行います。照度センサーはスターターキットのA6に配線されているのでanalogRead()関数で値を確認します。OLEDに照度センサーから取得したAD変換値を表示して確認を行いました。

LEDライトを照射するとAD変換値は最大で742になりました。従って暗さの判定の閾値の設定を可変抵抗の値を742以下になるように調整する必要があります。

ライブラリの使用例(analogRead)

aiData[0].adbuf[aiData[0].adwp] = analogRead(PIN_AD0);
aiData[1].adbuf[aiData[1].adwp] = analogRead(PIN_AD1);

for(j=0; j<ADNUM_MAX; j++){
  if( ++aiData[j].adwp >= ADBUF_MAX){
    aiData[j].adwp = 0;
  }

  sum[j] = 0;
  for( i=0; i < ADBUF_MAX; i++){
    sum[j] += aiData[j].adbuf[i];
  }
  aiData[j].ad = sum[j] /ADBUF_MAX;  
}

AD変換値を取得するライブラリは標準ライブラリなのでインクルードの必要はありません。AD変換値の取得はanalogRead()関数を使用します。引数にデータを取得するアナログピンの番号を指定します。戻り値がAD変換値になるので変数に格納して使用します。

例では取得したデータを保管する場所を更新しながら貯めていき平均値を計算した結果をAD変換値として採用しています。

アナログデータはノイズの影響を受けることがあるため複数回取得したデータの平均値をとる(ローパスフィルタの効果)ことでノイズによるデータのふらつきを防止する効果があります。

スポンサーリンク

ライブラリの追加

タイマ管理で使用するMsTimer2ライブラリとOLEDの表示を行うU8g2ライブラリは標準ライブラリとして搭載されていないためライブラリマネージャーで追加します。ライブラリの追加の仕方については下記記事にまとめています。

Grove Beginner Kit for Arduinoの使い方

ライブラリの準備と初期化

#include <U8g2lib.h>

//U8X8_SSD1306_128X64_NONAME_SW_I2C型の変数を宣言
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R2, /* reset=*/U8X8_PIN_NONE);

void setup() {
  u8g2.begin(); //初期化
}

U8g2ライブラリを使用するためU8x8lib.hをインクルードします。

クラス宣言する型は対象のOLEDによって異なりますがスターターキットはSSD1306が使用されているのでU8G2_SSD1306_128X64_NONAME_1_HW_I2Cのオブジェクト宣言としてu8g2をインスタンス化(変数を準備する操作できるようにする)します。u8g2に引数を指定して表示方向とリセットピンなどを指定します。

第1引数はU8G2_R0~U8G2_R3で表示方向を指定します。R0はスターターキットのシルク印刷(基板の白い文字)に合わせた向きを基準にすると180度回転した表示になります。R1はR0を90度回転、R2はR1を90度回転、R3はR2を90度回転した表示になります。U8G2_R2を指定するとスターターキットのシルク印刷と同じ向きでのOLEDの表示になります。

第2引数以降はリセットピンを使用する場合やクロックを指定する場合に使用しますが、特に操作しない場合はピンを使用しない設定が反映されるため指定する必要がありません。例ではリセットのピンを使用しない設定を指定しています。

begin()関数を使用するとOLEDの初期化が行われ文字を表示する準備ができます。

#include <MsTimer2.h>

void setup() {

  MsTimer2::set(10, mainTimer); // 10ms period
  MsTimer2::start();
}
/* タイマ管理(timer2がタイムアップするとコールされる) */
void mainTimer(void){
 //処理する内容を入れる
}

MsTimer2はマイコンのタイマで割り込みを任意タイミング(ms)で設定することができ、割り込み発生時にコールバック関数を指定できるライブラリです。MsTimer2ライブラリを使用するためMsTimer2.hをインクルードします。

MsTimer2のネームスペースで定義されたクラスのset()関数でタイマ割り込みが発生するタイミングとコールバックする関数を指定します。

第1引数にms単位で時間を指定します。第2引数にコールする関数名を指定します。例では10ms毎にmainTimer()がコールされます。start()関数でタイマーがスタートして割り込みが発生します。

ライブラリの使用例(U8g2)

void anaShow(void){
  uint16_t bar;

  u8g2.firstPage(); //バッファをクリア
  do{
    u8g2.setFont(u8g2_font_t0_16_mr);
    u8g2.setCursor(20, 16);
    u8g2.print("Light:");
    u8g2.setCursor(80, 16);
    u8g2.print(aiData[1].ad);
    u8g2.drawHLine(10, LIGHT_BAR_BASE, 100);
    barl = aiData[0].ad /10 + 10; //アナログ値
    barr = aiData[1].ad /10 + 10; //Light値
    u8g2.drawLine(barl,LIGHT_BAR_BASE-3,barl,LIGHT_BAR_BASE+3);
    u8g2.drawDisc(barr,48,3,U8G2_DRAW_ALL);
  }while(u8g2.nextPage());
}

OLEDに文字を表示する方法を説明します。firstPage()関数で内部のバッファをクリアします。setCursor()関数でOLEDの表示位置にカーソルを合わせます。

第1引数にX座標を指定します。X座標は画面左から右に向かって進みます。第2引数にY座標を指定します。Y座標は画面上から下に向かって進みます。

print()関数は引数に指定した文字列を表示します。変数を引数に指定した場合は引数の値を文字列に変換して表示します。

可変抵抗と照度センサーの値をOLEDのサイズである127に収まるようにAD変換値を10で割っています。+10はX座標の10から110までの線を左右に動くように表示するためです。換算した値を使ってOLEDに可変抵抗の値を線、照度センサーの値を円で表示します。

点と線をOLEDに表示する方法は使用する関数によって複数通りありますが、私がよく使用する関数を紹介します。

関数説明
drawLine(X1, Y1, X2, Y2)X1, Y1の座標からX2, Y2の座標まで線を引く
drawHLine(X, Y, W)X, Y座標の位置からX方向にWで指定した幅の線を引く
drawVLine(X, Y, H)X, Y座標の位置からY方向にHで指定した高さの線を引く
drawHVLine(X, Y, len, dir)X, Y座標の位置からlenの長さの線をdirで指定した方向に引く
dirは0でX側, 1でY側, 2で-X側, 3で-Y側
drawCircle(X, Y, rad, opt)
drawDisc(X, Y, rad, opt)
X, Y座標の位置からradで指定したサイズ円をoptに従って書くoptは以下の通り
U8G2_DRAW_UPPER_RIGHT(0x01) は右上の1/4円
U8G2_DRAW_UPPER_LEFT(0x02) は左上の1/4円
U8G2_DRAW_LOWER_LEFT(0x04) は左下の1/4円
U8G2_DRAW_LOWER_RIGHT(0x08) は右下の1/4円
U8G2_DRAW_ALL(0x0F) は円全体
Discは円を塗りつぶす場合に使用する
u8g2_DrawEllipse(X, Y, rx, ry ,opt)
u8g2_DrawFilledEllipse(X, Y, rx, ry ,opt)
X, Y座標の位置からrxで指定した幅とryで指定した高さの楕円を書くoptは上記のoptと同じ
DrawFilledEllipseは楕円を塗りつぶす場合に使用する
点と線をOLEDに表示する関数

drawHLine()関数でX座標10,Y座標48の位置から幅100の線を引きます。可変抵抗と照度センサーの値はこの線上を移動するように表示します。

照度センサーの値はdrawDisc()関数を使用して円で表示します。X座標にAD変換値、Y座標を48指定することで円が左右に動くように表示することができます。円は全体を表示するためU8G2_DRAW_ALLで指定しています。

可変抵抗の値はdrawLine()関数を使用して線を表示します。X1とX2座標に照度センサーの値を指定します。Y座標を48を中心に+3した値から-3した値を指定することで線が左右に動くように表示することができます。

nextPage()関数で行データごとのデータを更新してOLEDの表示を作りますが、ウェイトが必要となるためdo while()で表示が終えるまでループさせています。本記事では約120ms必要でした。OLEDの表示は遅延が発生するため頻繁な更新はしないように調整が必要です。

スポンサーリンク

動作確認

照度センサーとブザーの動作確認
照度センサーとブザーの動作確認

電源を入れるとOLEDにLight:に照度センサーの値を表示しAnalog: に可変抵抗の値を表示します。可変抵抗のAD変換値は照度センサーの動作の閾値に使用します。照度センサーの値が閾値以下(可変抵抗の値以下)になるとブザーを鳴らしてLEDをBlink動作します。

ブザーとLEDを動作はボタンを押すと停止することができます。ボタンを押さない場合は約10秒が経過すると停止するようにしています。ボタンを押すと次の動作を許可しますが、照度センサーの値が閾値以下の場合にボタンを押した場合は動作の条件となるためブザーが鳴ります。

OLEDの線上の表示について可変抵抗を操作するとAnalogの値が上下するのと連動して | 表示が左右に動くことが確認できました。またLightの値は照度センサーに手をかざして光を遮断すると●表示が左側に進み手を遠ざけると右側に進むことが確認できました。

明るい部屋で照度センサーの値が300を超えている時に手を上からかざして光を遮断していくとLightの値が低くなっていき閾値以下になった時ブザーが鳴りLEDがBlink動作することが確認できました。

すき間時間で資格をゲット【STUDYing(スタディング)】

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

#include <MsTimer2.h>
#include <U8g2lib.h>

#define DIFILT_MAX 4
#define ADNUM_MAX 2
#define ADBUF_MAX 8
#define TIME_UP 0
#define TIME_OFF -1
#define TIM_DIFILT 1
#define TIM_OLED 20
#define TIM_BUZ_OFF 1000
#define PIN_DI1 DD6
#define PIN_DO1 DD4
#define PIN_BUZ DD5
#define PIN_AD0 PIN_A0
#define PIN_AD1 PIN_A6
//#define BUZ_HZ 300
#define BUZ_DUR 50
#define LIGHT_BAR_BASE 48
#define BUZ_LOW 30

struct DIFILT{
  uint8_t wp;
  uint8_t buf[DIFILT_MAX];
  uint8_t di4;
  bool  di4flg;
};

struct ANAFILT{
  uint8_t adwp;
  uint16_t adbuf[ADBUF_MAX];
  uint16_t ad;
};

U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R2, /* reset=*/U8X8_PIN_NONE);
int16_t timDifilter = TIME_OFF;
int16_t timbuz = TIME_OFF;
int16_t timbuzoff = TIME_OFF;
int16_t timoled;
DIFILT diData;
ANAFILT aiData[ADNUM_MAX];
uint8_t btncnt;
uint16_t barl;
uint16_t barr;
uint8_t buz_start;
bool buzflg;
uint8_t bzcnt;

/* Local function prototypes */
void mainTimer(void);
void DiFilter(void);
void mainApp(void);
void anaShow(void);

void setup() {
  uint8_t i;

  Serial.begin(115200);
  pinMode(PIN_DI1, INPUT);
  pinMode(PIN_DO1, OUTPUT);

  i=0;
  while( i < 10){
    DiFilter();
    delay(10);
    i++;
    timDifilter = TIME_UP;
  } 
  buz_start = aiData[1].ad; //ブザーの条件を初期セット

  u8g2.begin();
  MsTimer2::set(10, mainTimer); // 10ms period
  MsTimer2::start();
}

void loop() {

  DiFilter();
  mainApp();
}

/*メイン処理*/
void mainApp(void){

  if( diData.di4 == 1 ){
    if( diData.di4flg == false ){
      diData.di4flg = true;
      ++btncnt;
      timbuz = TIME_OFF;
      timbuzoff = TIME_UP;
      buzflg = false;
      //noTone(PIN_BUZ);
      analogWrite(PIN_BUZ,0);
      digitalWrite(PIN_DO1,LOW);
      Serial.print("Button cnt:");
      Serial.println(btncnt);
    }
  }
  else{
    diData.di4flg = false;
  }

  if( aiData[1].ad < aiData[0].ad){
    if( buzflg == false){
      buzflg = true;
      timbuz = TIME_UP;
      timbuzoff = TIM_BUZ_OFF;
    }
  }

  if( timbuz == TIME_UP ){
    timbuz = BUZ_DUR;
    digitalWrite(PIN_DO1, !digitalRead(PIN_DO1));
    //tone(PIN_BUZ,BUZ_HZ,BUZ_DUR); //遅延が発生する
    if( ++bzcnt % 2 ){
      analogWrite(PIN_BUZ,128);
    }
    else{
      analogWrite(PIN_BUZ,0);
    }
    Serial.print("bzcnt:");
    Serial.println(bzcnt);
  }

  if( timbuzoff == TIME_UP){
    timbuzoff = TIME_OFF;
    timbuz = TIME_OFF;
    analogWrite(PIN_BUZ,0);
    digitalWrite(PIN_DO1,LOW);
    bzcnt = 0;
  }

  if( timoled == TIME_UP){
    timoled = TIM_OLED;
    anaShow();
  }

}
/* タイマ管理 */
void mainTimer(void){

  if( timDifilter > TIME_UP ){
    --timDifilter;
  }

  if( timbuz > TIME_UP ){
    --timbuz;
  }

  if( timbuzoff > TIME_UP){
    --timbuzoff;
  }

  if( timoled > TIME_UP ){
    --timoled;
  }
}
/* OLED表示*/
void anaShow(void){

  u8g2.firstPage(); //バッファをクリア
  do{
    u8g2.setFont(u8g2_font_t0_16_mr);
    u8g2.setCursor(20, 16);
    u8g2.print("Light:");
    u8g2.setCursor(80, 16);
    u8g2.print(aiData[1].ad);
    u8g2.setCursor(20, 32);
    u8g2.print("Analog:");
    u8g2.setCursor(80, 32);
    u8g2.print(aiData[0].ad);
    u8g2.drawHLine(10, LIGHT_BAR_BASE, 100);
    barl = aiData[0].ad /10 + 10;
    barr = aiData[1].ad /10 + 10;
    u8g2.drawLine(barl,LIGHT_BAR_BASE-3,barl,LIGHT_BAR_BASE+3);
    u8g2.drawDisc(barr,48,3,U8G2_DRAW_ALL);
  }while(u8g2.nextPage());
}
/* DIフィルタ */
void DiFilter(void){
  bool boo = true;
  uint8_t i;
  uint8_t j;
  uint16_t sum[ADNUM_MAX];

  if( timDifilter == TIME_UP ){
    timDifilter = TIM_DIFILT;

    diData.buf[diData.wp] = digitalRead(PIN_DI1);

    for( i=1; i < sizeof(diData.buf);i++){
      if( diData.buf[i - 1] != diData.buf[i]){
        boo = false;
      }
    }

    if(boo){ //データがすべて一致なので採用する
      diData.di4 = diData.buf[0];
    }
    if( ++diData.wp >= sizeof(diData.buf)){
      diData.wp = 0;
    }

    aiData[0].adbuf[aiData[0].adwp] = analogRead(PIN_AD0);
    aiData[1].adbuf[aiData[1].adwp] = analogRead(PIN_AD1);

    for(j=0; j<ADNUM_MAX; j++){
      if( ++aiData[j].adwp >= ADBUF_MAX){
        aiData[j].adwp = 0;
      }

      sum[j] = 0;
      for( i=0; i < ADBUF_MAX; i++){
        sum[j] += aiData[j].adbuf[i];
      }
      aiData[j].ad = sum[j] /ADBUF_MAX;  
    }
  }
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

GEEKJOB-未経験からITエンジニアに【オンライン無料体験】

最後まで、読んでいただきありがとうございました。

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