陸上競技のタイム計測機を模擬して作ります。「いちについて、よーい、どん」の音とともに、タイム計測を開始して、ゴールしたかどうかは赤外線センサーで検知します。Arduino機器はスタート地点とゴール地点にそれぞれ設置します。これまで紹介した、ZigBee通信、音再生、タイム計測などを組み合わせた作品です。
使用する部品
- ELEGOO Arduino用UNO R3コントロールボード ATmega328P ATMEGA16U2 2個
- XBee S2C / ワイヤアンテナ型 2個
- XBeeシールド(Seeed Studio社製) 2個
- Pirate Radio用アドオンスピーカーキット
- シリアル接続7セグメント4桁LED(赤)
- LED赤・緑・青 各2個
- 220 ohm 抵抗 6個
- タクトスイッチ 2個
- 電池スナップDCプラグアダプタ 2個
- 小型ボリューム 10KΩB
- PAM8012使用2ワットD級アンプモジュール
- ジャンパーワイヤ
かけっこタイム計測器の概要
完成品のイメージは、陸上トラック競技のゴールに置かれている計測器です。スタートの音でタイム計測を開始し、経過時間がディジタル表示で進んでいきます。ゴールした瞬間にタイマーのカウントアップを止めます。
器械は2台作成します。1台はスタート地点において、スタート音を鳴らします。もう1台はゴール地点でタイマーをカウントアップして、ゴールとともにタイマーを止めます。
ピンアサイン
スタートボタン用に2番ピンを割り当てます。このピンはプルアップに設定し、スイッチを接続します。スイッチが押されていないときはHighが入力され、スイッチが押されたときはLowが入力されます。詳細は、「ArduinoでSTEM教育 基礎編:プッシュスイッチ」を参照して下さい。
圧電スピーカに出力する端子は3番ピンを割り当てます。これはPWMを出力する必要があるため、Arduino Uno 以外でコードを書く場合は、端子の仕様を確認して下さい。こちらについても、以前の記事で詳細を紹介していますので、「ArduinoでSTEM教育 基礎編:パッシブブザー」を参照して下さい。
リセットボタン用に8番ピンを割り当てます。リセットボタンは、スタートでフライングしたときに使用します。このボタンを押すことで初期状態(スタートボタンを押すのを待つ状態)に戻ります。
スタート機の動作状態を視覚的にわかるように、LEDを接続します。LEDは赤・緑・青を5・6・7番ピンに接続します。詳細は「ArduinoでSTEM教育 基礎編:RGB-LED」を参照して下さい。
const int startPin = 2;
const int speakerPin = 3; //PWM
const int resetPin = 8;
const int redLedPin = 5;
const int greenLedPin = 6;
const int blueLedPin = 7;
void setup() {
pinMode(startPin, INPUT_PULLUP);
pinMode(speakerPin, OUTPUT);
pinMode(resetPin, INPUT_PULLUP);
pinMode(redLedPin, OUTPUT);
pinMode(greenLedPin, OUTPUT);
pinMode(blueLedPin, OUTPUT);
Serial.begin(9600);
TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
TCCR2B = _BV(CS20);
}
ステートマシン
スタート機の状態を管理するため、ステートマシンを使用します。ライブラリを使用するため、冒頭でFiniteStateMachine.hを読み込みます。ステートは5個定義します。”READY”は、スタートボタンが押されるのを待っている状態。”Cal”は、ゴール機の準備が完了するまで待っている状態。”Audio”は、スタート音源を再生している状態。”Measure”は、走っている時間を測定している状態。”Goal”は、ゴールしてリセットボタンかスタートボタンが押されるまで待っている状態です。 ステートとLEDの関係は以下のとおりです。LEDの制御はステートが変化するときのみ行います。
<スタート機/ゴール機ともに共通>
READY :緑
CAL :青
AUDIO :青
MEASURE :赤
GOAL :緑
#include <FiniteStateMachine.h>
void enterReady();
void enterCal();
void enterAudio();
void enterMeasure();
void enterGoal();
State READY = State(enterReady, NULL, NULL);
State CAL = State(enterCal, NULL, NULL);
State AUDIO = State(enterAudio, NULL, NULL);
State MEASURE = State(enterMeasure, NULL, NULL);
State GOAL = State(enterGoal, NULL, NULL);
FSM stateMachine = FSM(READY);
void enterReady() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, HIGH);
digitalWrite(blueLedPin, LOW);
}
void enterCal() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, LOW);
digitalWrite(blueLedPin, HIGH);
}
void enterAudio() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, LOW);
digitalWrite(blueLedPin, HIGH);
}
void enterMeasure() {
digitalWrite(redLedPin, HIGH);
digitalWrite(greenLedPin, LOW);
digitalWrite(blueLedPin, LOW);
}
void enterGoal() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, HIGH);
digitalWrite(blueLedPin, LOW);
}
スタート機とゴール機の間で通信する(READY, CAL)
スタート機のスタートボタンが押されると、スタート機は’S’を無線通信(ZigBee)で出力します。ゴール機はこの’S’を受信して、’CAL’ステートに移行します。’CAL’ステートでは、「よーい」と「どん」の間合いを何秒にするか、ランダム関数で生成し、また、ゴールを検知する赤外線センサーのキャリブレーションも行います。「randTime = random(0,999);」で0~1000msのランダム時間を生成します。「baseline = calSense(1000);」で赤外線センサーから得た電圧の 1000ms間平均値を取得します。そして、ゴール機は「Serial.print(randTime);」で生成した値を 無線通信(ZigBee)で出力します。 スタート機は、その値を「waitTime = Serial.read();」で受け取って、次のステート’AUDIO’に移行します。
<スタート機のプログラム>
if(stateMachine.isInState(READY)) {
if(digitalRead(startPin) == LOW) {
Serial.print('S');
stateMachine.transitionTo(CAL);
}
}
if(stateMachine.isInState(CAL)) {
while(Serial.available() == 0) {
}
waitTime = Serial.read();
stateMachine.transitionTo(AUDIO);
}
(以下省略)
<ゴール機のプログラム>
if(stateMachine.isInState(READY)) {
timeCount = 0;
while(Serial.available() == 0) {
int sensorVal = analogRead(sensePin);
float a = 0.6;
filterSensorVal = filterSensorVal = a * filterSensorVal + (1-a) * sensorVal;
Serial.print(sensorVal);
Serial.print(",");
Serial.println(filterSensorVal);
delay(1);
}
if(Serial.read() == 'S') {
stateMachine.transitionTo(CAL);
}
}
if(stateMachine.isInState(CAL)) {
dispS7S(timeCount);
randTime = random(0,999);
baseline = calSense(1000);
threshold = baseline + 150;
Serial.print(randTime);
//Serial.println(baseline);
//Serial.println(threshold);
stateMachine.transitionTo(AUDIO);
}
(途中省略)
int calSense(int time) {
int count = 0;
int sensorVal = 0;
long sumVal = 0;
float average = 0;
// <time>ms間のセンサ値を合計する
for(int i=0; i<time; i++) {
sensorVal = analogRead(sensePin);
sumVal += sensorVal;
count++;
delay(1);
}
//平均値を計算
average = sumVal / count;
return average;
}
スタート音を再生する(AUDIO)
「位置について、よ~い、どん!」や「On your mark, get set, go!」というように、音を鳴らします。Arduinoで音を鳴らす方法については、以前の記事「ArduinoでSTEM教育 応用編:WAV音源をデータ変換して圧電スピーカから出力する」で説明しています。簡単に説明すると、今回使用する音の再生時間は短く、音質は悪くても問題ありません。走り出すタイミングが分かりさえすれば良いのです。したがって、SDカードを使うこと無く、音源をArduinoのメモリに置いて、圧電スピーカから再生することが出来ます。
音源は3種類用意します。「On your mark(いちについて)」、「Get set(よ~い)」、「Go(どん)」の3種類です。それぞれファイルを用意してプログラム冒頭で読み込みます。私は英語の音声が欲しかったので、 AudioJungle で音源を購入しましたが、まずは自分の声で試してみるのが良いと思います。音源は以下のように、データ変換してヘッダーファイルとして保存しておきます。
const uint8_t soundGetSet[] PROGMEM = {
0x80,0x80,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x80,0x80,0x80,0x80,0x80,0x80,
0x80,0x80,0x80,0x80,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x80,0x80,0x80,0x80,0x80,0x80,
0x80,0x80,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x80,0x80,0x80,0x80,0x80,
(以下省略)
タイマーで時間を計測する(MEASURE)
時間の計測については、「ArduinoでSTEM教育 応用編:割り込みタイマーで時間を計測する」で説明した方法でやります。スタート機は、「よーい、ドン」の音声再生後、「Serial.print(‘G’);」で’G’を無線通信(ZigBee)で出力します。ゴール機は、 タイマーを ‘Audio’ステートで「MsTimer2::start();」で開始します。そして、無線通信で ‘G’ を受けると、timeCountを0にして、’Audio’ステートから’Measure’ステートに移動します。ゴールすると、タイマーのカウントアップを止めます。
ゴールしたことを認識する(GOAL)
ゴール機は’Measure’ステートに移動すると、「if(analogRead(sensePin) > threshold)」で赤外線センサがゴールする人を検知するまで、経過時間をLEDセグメントモジュールに表示させながら待ちます。検知すると、タイマーのカウントアップを止めます。検知するしきい値は、’CAL’ステートで決めています。先述したとおり、「baseline = calSense(1000);」で赤外線センサーから得た電圧の 1000ms間平均値を取得します。そして、人を検知するしきい値は、この値に150を加算します。150という値は、実際にセンサーの前を通り過ぎるときに、どれくらいになるかを試して、調整した値です。コードでは、「 threshold = baseline + 150;」と記述しています。
プログラム
スタート機のプログラム
//www.stemship.com
//2020.01.03
#include <FiniteStateMachine.h>
#include "On-Your-Marks.h"
#include "Get-Set.h"
#include "Go.h"
const int startPin = 2;
const int speakerPin = 3; //PWM
const int resetPin = 8;
const int redLedPin = 5;
const int greenLedPin = 6;
const int blueLedPin = 7;
void enterReady();
void enterCal();
void enterAudio();
void enterMeasure();
void enterGoal();
State READY = State(enterReady, NULL, NULL);
State CAL = State(enterCal, NULL, NULL);
State AUDIO = State(enterAudio, NULL, NULL);
State MEASURE = State(enterMeasure, NULL, NULL);
State GOAL = State(enterGoal, NULL, NULL);
FSM stateMachine = FSM(READY);
void setup() {
pinMode(startPin, INPUT_PULLUP);
pinMode(speakerPin, OUTPUT);
pinMode(resetPin, INPUT_PULLUP);
pinMode(redLedPin, OUTPUT);
pinMode(greenLedPin, OUTPUT);
pinMode(blueLedPin, OUTPUT);
Serial.begin(9600);
TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
TCCR2B = _BV(CS20);
}
void loop() {
int waitTime;
int resultTime;
if(stateMachine.isInState(READY)) {
if(digitalRead(startPin) == LOW) {
Serial.print('S');
stateMachine.transitionTo(CAL);
}
}
if(stateMachine.isInState(CAL)) {
while(Serial.available() == 0) {
}
waitTime = Serial.read();
stateMachine.transitionTo(AUDIO);
}
if(stateMachine.isInState(AUDIO)) {
playAudio(soundOnYourMarks, sizeof soundOnYourMarks / sizeof soundOnYourMarks[0]);
delay(5000);
playAudio(soundGetSet, sizeof soundGetSet / sizeof soundGetSet[0]);
delay(2000+waitTime);
playAudio(soundGo, sizeof soundGo / sizeof soundGo[0]);
Serial.print('G');
stateMachine.transitionTo(MEASURE);
}
if(stateMachine.isInState(MEASURE)) {
while(Serial.available() == 0) {
if(digitalRead(resetPin) == LOW) {
Serial.print('R');
stateMachine.transitionTo(READY);
break;
}
}
if(Serial.read() == 'F') {
resultTime = Serial.read();
stateMachine.transitionTo(GOAL);
}
}
if(stateMachine.isInState(GOAL)) {
if( (digitalRead(resetPin) == LOW) or (digitalRead(startPin) == LOW) ) {
Serial.print('R');
stateMachine.transitionTo(READY);
}
}
//それぞれの状態に応じてアップデータ
stateMachine.update();
}
void enterReady() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, HIGH);
digitalWrite(blueLedPin, LOW);
}
void enterCal() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, LOW);
digitalWrite(blueLedPin, HIGH);
}
void enterAudio() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, LOW);
digitalWrite(blueLedPin, HIGH);
}
void enterMeasure() {
digitalWrite(redLedPin, HIGH);
digitalWrite(greenLedPin, LOW);
digitalWrite(blueLedPin, LOW);
}
void enterGoal() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, HIGH);
digitalWrite(blueLedPin, LOW);
}
void playAudio(const uint8_t* soundArray, unsigned int soundArrayLength) {
for (int i = 0; i < soundArrayLength; i++) {
OCR2B = pgm_read_byte_near(&soundArray[i]);
delayMicroseconds(125);
}
}
ゴール機のプログラム
//www.stemship.com
//2019.12.28
#include <FiniteStateMachine.h>
#include <MsTimer2.h>
#include "sound.h"
#include <Wire.h> // Include the Arduino SPI library
// Here we'll define the I2C address of our S7S. By default it
// should be 0x71. This can be changed, though.
const byte s7sAddress = 0x71;
char tempString[10]; // Will be used with sprintf to create strings
//const int startPin = 2;
//const int resetPin = 4;
const int redLedPin = 5;
const int greenLedPin = 6;
const int blueLedPin = 7;
const int sensePin = 0;
unsigned int timeCount = 0;
int filterSensorVal = 0;
int calSense();
void enterReady();
void enterCal();
void enterAudio();
void enterMeasure();
void enterGoal();
void timerFire();
State READY = State(enterReady, NULL, NULL);
State CAL = State(enterCal, NULL, NULL);
State AUDIO = State(enterAudio, NULL, NULL);
State MEASURE = State(enterMeasure, NULL, NULL);
State GOAL = State(enterGoal, NULL, NULL);
FSM stateMachine = FSM(READY);
void setup() {
// pinMode(startPin, INPUT_PULLUP);
// pinMode(resetPin, INPUT_PULLUP);
pinMode(redLedPin, OUTPUT);
pinMode(greenLedPin, OUTPUT);
pinMode(blueLedPin, OUTPUT);
pinMode(sensePin, INPUT);
Serial.begin(9600);
MsTimer2::set(1, timerFire);
Wire.begin(); // Initialize hardware I2C pins
// Clear the display, and then turn on all segments and decimals
clearDisplayI2C(); // Clears display, resets cursor
// Custom function to send four bytes via I2C
// The I2C.write function only allows sending of a single
// byte at a time.
s7sSendStringI2C("-HI-");
setDecimalsI2C(0b111111); // Turn on all decimals, colon, apos
// Flash brightness values at the beginning
setBrightnessI2C(0); // Lowest brightness
delay(1500);
setBrightnessI2C(255); // High brightness
delay(1500);
// Clear the display before jumping into loop
clearDisplayI2C();
stateMachine.update();
}
void loop() {
long randTime;
int baseline;
int threshold;
unsigned long resultTime;
unsigned int sample_raw_len = sizeof sample_raw / sizeof sample_raw[0];
unsigned int timeC = sample_raw_len * 125 * 0.001; //[ms]
unsigned int time1, time2;
if(stateMachine.isInState(READY)) {
timeCount = 0;
while(Serial.available() == 0) {
int sensorVal = analogRead(sensePin);
float a = 0.6;
filterSensorVal = filterSensorVal = a * filterSensorVal + (1-a) * sensorVal;
Serial.print(sensorVal);
Serial.print(",");
Serial.println(filterSensorVal);
delay(1);
}
if(Serial.read() == 'S') {
stateMachine.transitionTo(CAL);
}
}
if(stateMachine.isInState(CAL)) {
dispS7S(timeCount);
randTime = random(0,999);
baseline = calSense(1000);
threshold = baseline + 150;
Serial.print(randTime);
//Serial.println(baseline);
//Serial.println(threshold);
stateMachine.transitionTo(AUDIO);
}
if(stateMachine.isInState(AUDIO)) {
timeCount = 0;
MsTimer2::start();
while(Serial.available() == 0) {
}
if(Serial.read() == 'G') {
time1 = timeCount;
timeCount = 0;
//MsTimer2::start();
stateMachine.transitionTo(MEASURE);
}
}
if(stateMachine.isInState(MEASURE)) {
while(1) {
if(analogRead(sensePin) > threshold){
time2 = timeCount;
MsTimer2::stop();
Serial.print('F');
resultTime = time2; //time2 - timeC - time1/2;
Serial.print(resultTime);
stateMachine.transitionTo(GOAL);
break;
}
else{
//Serial.println(analogRead(sensePin));
if(timeCount % 10 == 0){
dispS7S(timeCount);
//Serial.println(timeCount - timeC - time1/2);
}
if(Serial.read() == 'R') {
MsTimer2::stop();
timeCount = 0;
dispS7S(timeCount);
stateMachine.transitionTo(READY);
break;
}
}
}
}
if(stateMachine.isInState(GOAL)) {
dispS7S(resultTime);
while(Serial.available() == 0) {
}
if(Serial.read() == 'R') {
stateMachine.transitionTo(READY);
}
}
//それぞれの状態に応じてアップデータ
stateMachine.update();
}
void enterReady() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, HIGH);
digitalWrite(blueLedPin, LOW);
}
void enterCal() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, LOW);
digitalWrite(blueLedPin, HIGH);
}
void enterAudio() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, LOW);
digitalWrite(blueLedPin, HIGH);
}
void enterMeasure() {
digitalWrite(redLedPin, HIGH);
digitalWrite(greenLedPin, LOW);
digitalWrite(blueLedPin, LOW);
}
void enterGoal() {
digitalWrite(redLedPin, LOW);
digitalWrite(greenLedPin, HIGH);
digitalWrite(blueLedPin, LOW);
}
int calSense(int time) {
int count = 0;
int sensorVal = 0;
long sumVal = 0;
float average = 0;
// <time>ms間のセンサ値を合計する
for(int i=0; i<time; i++) {
sensorVal = analogRead(sensePin);
sumVal += sensorVal;
count++;
delay(1);
}
//平均値を計算
average = sumVal / count;
return average;
}
void timerFire() {
if(timeCount == 65535){
timeCount = timeCount;
}
else {
timeCount++;
}
}
// This custom function works somewhat like a serial.print.
// You can send it an array of chars (string) and it'll print
// the first 4 characters in the array.
void s7sSendStringI2C(String toSend)
{
Wire.beginTransmission(s7sAddress);
for (int i=0; i<4; i++)
{
Wire.write(toSend[i]);
}
Wire.endTransmission();
}
// Send the clear display command (0x76)
// This will clear the display and reset the cursor
void clearDisplayI2C()
{
Wire.beginTransmission(s7sAddress);
Wire.write(0x76); // Clear display command
Wire.endTransmission();
}
// Set the displays brightness. Should receive byte with the value
// to set the brightness to
// dimmest------------->brightest
// 0--------127--------255
void setBrightnessI2C(byte value)
{
Wire.beginTransmission(s7sAddress);
Wire.write(0x7A); // Set brightness command byte
Wire.write(value); // brightness data byte
Wire.endTransmission();
}
// Turn on any, none, or all of the decimals.
// The six lowest bits in the decimals parameter sets a decimal
// (or colon, or apostrophe) on or off. A 1 indicates on, 0 off.
// [MSB] (X)(X)(Apos)(Colon)(Digit 4)(Digit 3)(Digit2)(Digit1)
void setDecimalsI2C(byte decimals)
{
Wire.beginTransmission(s7sAddress);
Wire.write(0x77);
Wire.write(decimals);
Wire.endTransmission();
}
void dispS7S(unsigned int counter) {
// Magical sprintf creates a string for us to send to the s7s.
// The %4d option creates a 4-digit integer.
if(counter/10 > 5999) {
sprintf(tempString, "%4d", 5999);
}
else {
sprintf(tempString, "%4d", counter/10);
}
// This will output the tempString to the S7S
s7sSendStringI2C(tempString);
// Print the decimal at the proper spot
setDecimalsI2C(0b00000010); // Sets digit 3 decimal on
}
コメントを残す