Arduinoでライントレース用ラップタイマーを作る
ラップタイマーが欲しい!
ライントレースで遊んでいると欲しくなりますよネ。どのパラメータの組みが速いかも知りたいですし。しかもブレッドボードで組むんじゃなくて、子基板(シールドって言うんですね)を載せてポータブルにしたい!ってことで作っちゃいました。
全体像

写真は Arduino のキット についてきたセンサやディスプレイをサンハヤトのArduino用ユニバーサル基板 UB-ARD03-P の上に載せたものです。この基板は Arduino のリセットボタンが隠れず、また GND と VCC がベタで引き回されているので配線が楽でした。逆に Arduino のピンから信号を引っ張り出すにはそれらを跨がなければならず、狭い基板がますます狭くなるので、使い易さと使い難さが同居している感じです。
またセンサやディスプレイはピンソケットから引っこ抜いて再利用できるようにしています(ケチ)。
センサモジュール


出力ピンがL字型で基板に対して垂直に立つタイプなので、取り外して縦型のピンヘッダに交換しようかとも思いましたが、センサの高さを低く抑えたかったのでL字型のピンソケットを介して基板に対して水平になるようにしています。
タクタイルスイッチ
サンハヤトの基盤についてくるタクタイルスイッチはリセットがかかるようパターンが引かれているので、カッターでカットしてラップのリセット用にすることにしました。
電源
006P 電池は東芝のニッケル水素電池 IMPULSE で 充電器 と一緒に Amazon 購入です。容量が 200mAh なので連続で数時間遊んでいるとなくなります。フル充電に6時間かかります。
圧電ブザー
1つはアクティブ(自励振)タイプでラップを刻んだ時に「ピッ」と音を鳴らします。もう1つはパッシブ(他励振)タイプで、ビュートローバーARM に実装した楽譜再生プログラムを Arduino に移植することを考えてます。タイムを計測しながらカッコいい曲を流す予定です!
配線とプログラムはチュートリアルから!
SunFounder の「コンポーネントの基本」ページから「IR 赤外線障害物回避センサーモジュール」と「OLEDディスプレイモジュール」を参考に “ニコイチ” にします。


回路図は描いた事がなかったので部品のレイアウトに苦労しましたが、一応 Fritzing で配線図と回路図を引いてみました。エキスパートの方々は KiCad を使っているらしいので、いづれ習熟してオリジナル基板を発注するのが夢です。
また Minima のパーツは Fritzing のフォーラム からもらいました。ホント、ありがたいですネ。
ソースコードのご紹介
ソースコードを Github に上げてありますが、ここでも簡単に解説したいと思います。
要求
- センサでロボット車の通過を検知する
- コースを1周するごとにラップタイムをディスプレイに表示する
- ディスプレイの上段に経過時間、下段にラップタイムを表示する
- タイムは 1/100 秒まで計測する
- 計測の状態変化を音と表示で知らせる
要件
- 小型、ポータブル、乾電池で動作すること
- Arduino UNO R3/R4(秋月電子で検索:Arduino UNO)
- FC-51(Amazonで検索:赤外線障害物回避モジュール)
- OLEDモジュール(Amazonで検索:128X64、I2C、SSD1306)
- 圧電ブザー
- 006P 乾電池
- 制約条件
- コースを1周するのに4秒以上であること
- 黒色の車体は計測の対象外とする
- …
仕様
- ロボット車の通過に伴うセンサの状態変化を割り込みで処理する
- 車体の凹凸などに伴うセンサ出力のチャタリングを除去する
- タイマーの状態は「計測前」、「計測中」の2状態とする
- ラップタイム更新時には音を鳴らし、表示を点滅させる
- ラップタイムの点滅は、点滅周期と点滅時間で制御する
実装
まずは OLED の設定です。チュートリアル「OLEDディスプレイモジュール」に従い、Adafruit の SSD1306 用ライブラリとグラフィック用ライブラリが IDE にンストールされていることが前提です。
/*
This code initializes an OLED display (SSD1306) using the Adafruit SSD1306 library,
and displays various text, numbers, and scroll animations on the screen.
Board: Arduino Uno R4 (or R3)
Component: OLED (SSD1306)
Library: https://github.com/adafruit/Adafruit_SSD1306 (Adafruit SSD1306 by Adafruit)
https://github.com/adafruit/Adafruit-GFX-Library (Adafruit GFX Library by Adafruit)
*/
// Libraries in use: "Adafruit SSD1306", "Adafruit BusIO", "Adafruit GFX"
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for SSD1306 display connected using I2C
#define OLED_RESET (-1) // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
続いてタイマーとラップ表示に関する設定です。割り込みを使わない設定もできますが、振る舞いが若干変わります。
/*--------------------------------------------------
* タイマーとラップ表示の設定
*--------------------------------------------------*/
#define DEBUG_PRINT 0 // デバッグコンソールに出力する場合は 1
#define USE_INTERRUPT 1 // センサモジュール FC-51 の状態変化を割り込みで処理する場合は 1
#define IR_SENSOR_PIN 2 // センサモジュール FC-51 出力の入力ピン番号
#define BLINK_COUNT 3 // ラップタイムの点滅回数
#define BLINK_INTERVAL 250 // ラップタイムの点滅周期 [msec]
#define LAP_HYSTERESIS 4000 // チャタリング除去用のヒステリシス [msec]
#define TIMER_CURSOR_X 16 // ディスプレイ表示のX座標
#define TIMER_CURSOR_Y 10 // ディスプレイ表示のY座標
/*--------------------------------------------------
* 状態変化時の音の設定
*--------------------------------------------------*/
#define BUZZER_PIN 8 // ブザー出力のピン番号
#define BUZZER_DURATION 16 // 音の長さ [msec]
#define BUZZER_FREQUENCY 2794 // 音階の周波数(ファ)
割り込みハンドラには引数も戻り値も無いので、メインループとはグローバル変数でやりとりします。メインループ内の処理にとっては知らない所(=割り込みハンドラ)で変数の値が変更されるため、volatile
をつけてコンパイラに適切に扱うよう指示(=最適化を適用しない)しています。
/*--------------------------------------------------
* 割り込みハンドラ内で設定される変数
*--------------------------------------------------*/
// ラップタイマー関連
volatile static bool start; // 計測開始前[false]、計測開始後[true]
volatile static char strLap[] = "00:00:00"; // ディスプレイ用ラップタイム
volatile static u_int32_t timerT0, timerT1; // ラップ計測用タイム
// ラップの点滅表示関連
volatile static int blinkCount; // 点滅した回数
volatile static uint32_t blinkT0; // 点滅開始時間
次は Arduino の setup()
で呼び出す初期化の関数群です。
/*--------------------------------------------------
* OLED の設定
*--------------------------------------------------*/
void setupDisplay() {
// OLED 用インスタンスの初期化
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.begin(9600);
while (true) { // エラーが起きた場合、シリアル通信にメッセージを出し続ける
Serial.println(F("SSD1306 allocation failed"));
delay(1000);
}
}
// ディスプレイバッファをクリア
display.clearDisplay();
// Display Text
display.setTextSize(2); // テキストサイズの設定
display.setTextColor(WHITE); // テキスト表示色の設定(黒 or 白)
}
/*--------------------------------------------------
* 障害物センサの設定
*--------------------------------------------------*/
void setupSensor() {
pinMode(IR_SENSOR_PIN, INPUT);
#if USE_INTERRUPT
// 障害物センサ状態が変化した時に割り込みハンドラ checkStateIR() を起動する
void checkStateIR(void);
attachInterrupt(digitalPinToInterrupt(IR_SENSOR_PIN), checkStateIR, CHANGE);
#endif
}
/*--------------------------------------------------
* タイマーとラップ表示の初期化
*--------------------------------------------------*/
void resetTimer(void) {
// タイマーの初期化
start = false;
timerT0 = timerT1 = 0;
strcpy((char*)strLap, "00:00:00");
// ラップ表示の初期化
blinkT0 = 0;
blinkCount = 0;
}
続いて、ミリ秒を 1/100 秒単位の文字列に変換をするためのサブルーチンです。
/*--------------------------------------------------
* ミリ秒を文字列に変換する (00:00:00 - 59:59:99)
*--------------------------------------------------*/
char* time2str(uint32_t msec, char *str) {
uint32_t m, s, x;
// 1/1000[sec] --> 1/100[sec]
x = (msec /= 10UL) % 100UL;
s = (msec /= 100UL) % 60UL;
m = (msec /= 60UL) % 60UL;
sprintf(str, "%02d:%02d:%02d", m, s, x);
return str;
}
次は割り込みハンドラの定義です。障害物センサ FC-51 は、未検出の場合は High
、検出した場合は Low
を出力します。即ちエッジの立ち下がりを検知すれば通過を判定できるのですが、setupSensor()
で attachInterrupt()
に FALLING
を設定しても CHANGE
と同じ振る舞いが観測されたため、わざわざ digitalRead()
で状態を確認しています。
Arduino R4 コアのソースコード には HIGH
の場合に 問題がありげ なコメントが書かれているので、何かあるのかもしれません。
またアクションとして状態変化に伴う音を tone()
で鳴らしています。音の長さを BUZZER_DURATION
(16[msec])で指定していますが、鳴り終わるまでここで待っているわけではなく、タイマーに「指定した周波数で指定時間分だけアナログ出力せよ」と指示してすぐに戻ってきます。
/*--------------------------------------------------
* 計測と表示の状態変化を判定し、アクションを実行する
*--------------------------------------------------*/
void checkStateIR(void) {
bool status = digitalRead(IR_SENSOR_PIN);
#if DEBUG_PRINT
Serial.println(status);
#endif
if (!status) {
if (start) {
uint32_t delta = timerT1 - timerT0;
if (delta >= LAP_HYSTERESIS) { // 計測開始後4秒以上経過していれば状態を変えても良い
time2str(delta, (char*)strLap);
timerT0 = timerT1;
blinkT0 = timerT0;
blinkCount = 1;
tone(BUZZER_PIN, BUZZER_FREQUENCY, BUZZER_DURATION);
}
}
// 計測開始の1回だけ実行する
else {
start = true;
timerT0 = timerT1;
tone(BUZZER_PIN, BUZZER_FREQUENCY, BUZZER_DURATION);
}
}
}
続いて Arduino お約束の setup()
で先に定義した初期化の関数群を呼び出します。
/*--------------------------------------------------
* OLED, 障害物センサ、タイマーと表示の初期化
*--------------------------------------------------*/
void setup() {
// OLED の設定
setupDisplay();
// FC-51 赤外線センサモジュールの設定
setupSensor();
// 計測と表示用のグローバル変数の初期化
resetTimer();
#if DEBUG_PRINT
Serial.begin(9600);
#endif
}
最後はメインループです。millis()
で最新の経過時間を読み込み、その表示とラップタイムの表示を担当しています。
/*--------------------------------------------------
* メインループ
*--------------------------------------------------*/
void loop() {
// for timer
char strNow[10];
uint32_t delta;
timerT1 = millis();
// 計測開始の1回だけ実行
if (!start) {
timerT0 = timerT1;
}
delta = timerT1 - timerT0;
#if ! USE_INTERRUPT
checkStateIR(); // 割り込みを使わない場合は、ここで状態変化を判定する
#endif
display.clearDisplay();
display.setCursor(TIMER_CURSOR_X, TIMER_CURSOR_Y);
display.println(time2str(delta, strNow));
// ラップ表示の点滅が開始されたら点滅周期ごとに点滅回数を更新する
if (blinkCount) {
if ((timerT1 - blinkT0) >= BLINK_INTERVAL) {
blinkT0 = timerT1;
if (blinkCount++ > (BLINK_COUNT * 2 - 1)) {
blinkCount = 0;
}
}
}
if (blinkCount % 2 == 0) { // 2回に1回、点灯と消灯の状態を変えて表示する
display.setCursor(TIMER_CURSOR_X, TIMER_CURSOR_Y + 32);
display.println((char*)strLap);
}
display.display(); // ディスプレイバッファを反映する
#if DEBUG_PRINT
Serial.print(strNow);
Serial.print("\t");
Serial.print((char*)strLap);
Serial.println();
#endif
}
以上!
ライントレースで楽しんでもらえたら嬉しいです ^.^)y