2014年12月13日土曜日

ストップウォッチを造ろう!

おはようございます、今日はArduinoでストップウォッチを作ろうと思います。


なぜ、ストップウォッチなんだ、そんなもの量販店で2000円も出したら買えるじゃないか、という方のために、今回作成するストップウォッチには他のどの時計にもない、ユニークな機能を追加しました。




その機能というのは、ラップタイム計測機能です。

肩透かしを食らった方、ちょっと待ってください、ブラウザの戻るボタンを押すのはまだ早いです。

なんと、このストップウォッチ、ラップタイムをSDカードに書きだすことが出来るのです。
自分で作らない限り、日本中探しても「ラップタイムをSDに書きだせる」ストップウォッチの作成と紹介はここぐらいだと思います。

これが完成品の写真です。
何やら配線がごちゃごちゃしていますが、Arduinoの電源を入れると、初期化終了後にストップウォッチが走り出します。

オレンジ色のボタンでラップタイム計測+SD書き出し、黒色のボタンでストップウォッチが終了します。

また、時刻を表示する液晶の1行目には、マイコンのオーバーヘッドが表示されます。Arduinoのクロック原は基本的にセラミック振動子なので、時刻によって多少のジッターが観測されます。それもまたアナログ的で面白いところです。

では、紹介が終わったところで、早速作り方の説明です。

まず、ストップウォッチというのは基本的には屋外へ持っていってArduino単独で動かす物なので、モバイルバッテリーを用意してください。
あと、時刻の表示に使う液晶モジュールが必須です。
筆者はSunlikeのSD1602HULBを使用しました。

それと、ラップタイムを書きだすためのSDカードが必要です。カードスロットDIP化モジュールの作り方は前回説明したので、この際作ってしまうのもテかもしれません。

あとは、基板に起こす前の動作確認として、適当なサイズのブレッドボードがあれば準備は完了です。

早速Arduino IDEを起動して、スケッチを描きこんでいきましょう。
筆者の書いたスケッチは以下の通りです。


 /*
 Chronograph with SD recording.
 Time Keeper v2.3
 based on SD Example.
 by 2014 A.Fujimoto.
 I appriciate to Arduino team, and its library!


  The circuit:
 * SD card attached to SPI bus as follows:
 CM  Pin role         Pin No.
 ++ CS                 8
 ++ MOSI              11
 ++ MISO              12
 ++ SCLK              13
 ++ Hardware Reserved 10
 ++ LCD_RS            2
 ++ LCD_RW            GND
 ++ LCD_Enable        3
 ++ DataBit4..7       4..7
 ++ SW_record_time    9
 ++ SW_stop_timekeep  0
 */
#define hardwareSS 10  // don't touch it!
#define CS 8 // you can change this except 10.
#include <SD.h>
#include <LiquidCrystal.h>
#include <avr/io.h>
#include <avr/interrupt.h>
File myFile;
const int sw = 9;
const int owari = 0;
const int dTime = 999;
int val = 0;
int hour = 0;
int minute = 0;
int second = 0;
int sSec = 0;
int curTime = 0;
int processTime = 0;
char hPad = ' ';
char mPad = ' ';
char sPad = ' ';
int iter = 0;  // variable for iteration.
int time = 100; // key scan iterations. Entire loop must be 1 second.
//int dms = 268;  // delay in usec. used in keyscan iteration.
//int dms = 259;  // okureteru
// revised algorhythm. "(big wait)delay -> (bit wait)delayMicroseconds"
int dms = 787;  // inc. to slow. dec. to fast.
int dTime2 = 17;
LiquidCrystal lcd(2, 3, 4, 5, 6, 7);
// 4bitMode  (rs, rw, d4, d5, d6, d7)
void setup()
{
  lcd.begin(16,2);
  lcd.noBlink();
  pinMode(sw, INPUT_PULLUP);  // time record sw.
  pinMode(owari, INPUT_PULLUP);  // time record stop sw.
  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }
  Serial.print("Initializing SD card...");
  // On the Ethernet Shield, CS is pin 4. It's set as an output by default.
  // Note that even if it's not used as the CS pin, the hardware SS pin
  // (10 on most Arduino boards, 53 on the Mega) must be left as an output
  // or the SD library functions will not work.
  pinMode(hardwareSS, OUTPUT);
  if (!SD.begin(CS)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
  // open the file. note that only one file can be open at a time,
  // so you have to close this one before opening another.
  myFile = SD.open("test.txt", FILE_WRITE);
  // if the file opened okay, write to it:
  if (myFile) {
    Serial.print("Writing to test.txt...");
    if(digitalRead(sw) == LOW){
      myFile.println(time);
    }
    myFile.println("testing 1, 2, 3.");
    // close the file:
    Serial.println("done.");
  }
  else {
    // if the file didn't open, print an error:
    Serial.println("error opening test.txt");
  }
  // re-open the file for reading:
  myFile = SD.open("test.txt", FILE_WRITE);
  if (myFile) {
    Serial.println("test.txt:");
    // read from the file until there's nothing else in it:
    while (myFile.available()) {
      Serial.write(myFile.read());
    }
    // close the file:
    //    myFile.close();
  }
  else {
    // if the file didn't open, print an error:
    Serial.println("error opening test.txt");
  }
  myFile = SD.open("test.txt", FILE_WRITE);
  while (myFile.available()) {
    myFile.read();
  }
}
void loop()
{
  // nothing happens after setup
  delay(dTime2);
  delayMicroseconds(dms);
  __asm__("nop");
  curTime = 0;
  curTime = millis();
  sSec += 3;
  //if(time % 3600 > 0){
  //  hour++;
  //}else if(time%60 > 0){
  //  minute++;
  //  if(minute>=60){minute=0;time++;}
  //}
  //if(minute>=60){hour++;}
  if(minute>=60){
    minute=0;
    hour++;
  }
  if(second>=60){
    second = 0;
    minute++;
  }
  if(sSec>=100){
    sSec=0;
    second++;
  }
  /*
   if(hour/10 == 0){
   hPad = ' ';
   }
   else{
   hPad = ' ';
   }
   if(minute/10 == 0){
   mPad = ' ';
   }
   else{
   mPad = ' ';
   }
   if(second/10 == 0){
   sPad = ' ';
   }
   else{
   sPad = ' ';
   }
 
   */
  if(digitalRead(sw) == LOW){  // negative logic.
    Serial.println("button");
    lcd.setCursor(15,0);
    lcd.write(0xFF);
    delay(10);
    myFile.println(hour);
    myFile.println(minute);
    myFile.println(second);
    myFile.println(sSec);
    myFile.println();
    lcd.setCursor(15,0);
    lcd.print(" ");
    //    myFile.close();
  }
  if(digitalRead(owari) == LOW){  // negative logic.
    Serial.println("End of File.");
    lcd.setCursor(15,0);
    lcd.write(0xFF);
    delay(10);
    myFile.println(hour);
    myFile.println(minute);
    myFile.println(second);
    myFile.println(sSec);
    myFile.println("EOF");
    lcd.setCursor(15,0);
    lcd.print(" ");
    closeFile();
    lcd.clear();
    while(1){
      lcd.print("End of file.");
      lcd.setCursor(0,1);
      lcd.print("Remove SD Card...");
    }
  }
  Serial.println(millis()-curTime);
  Serial.print("Time :");
  Serial.print(hour);
  Serial.print(":");
  Serial.print(mPad);
  Serial.print(minute);
  Serial.print(".");
  Serial.print(sPad);
  Serial.print(second);
  Serial.print(sPad);
  Serial.print(sSec);
  Serial.println();
  lcd.setCursor(0,0);
  lcd.clear();
  //  lcd.print("Delay :");
  //  lcd.setCursor(0,1);
  //  lcd.print("Time :");
  lcd.setCursor(5,1);
  lcd.print(hour);
  if(second%2 == 0){
    lcd.print(":");
  }
  else{
    lcd.print(" ");
  }
  lcd.print(mPad);
  lcd.print(minute);
  lcd.print(".");
  lcd.print(sPad);
  lcd.print(second);
  lcd.print(sPad);
  lcd.print(sSec);
  lcd.setCursor(7,0);
  processTime = millis() - curTime;
  lcd.print(processTime);
  lcd.print("ms");
  while(iter<time){
    keycheck();
    iter++;
  }
  iter=0;
}
void closeFile(){
  myFile.close();
}
/*
 char removePad(char s){
 s.erase();
 return s;
 }

 */
void keycheck(){
  if(digitalRead(sw) == LOW){  // negative logic.
    Serial.println("button");
    lcd.setCursor(15,0);
    lcd.write(0xFF);
    delay(10);
    myFile.println(hour);
    myFile.println(minute);
    myFile.println(second);
    myFile.println(sSec);
    myFile.println();
    lcd.setCursor(15,0);
    lcd.print(" ");
    //    myFile.close();
  }
  if(digitalRead(owari) == LOW){  // negative logic.
    Serial.println("End of File.");
    lcd.setCursor(15,0);
    lcd.write(0xFF);
    delay(10);
    myFile.println(hour);
    myFile.println(minute);
    myFile.println(second);
    myFile.println(sSec);
    myFile.println("EOF");
    lcd.setCursor(15,0);
    lcd.print(" ");
    closeFile();
    lcd.clear();
    while(1){
      lcd.print("End of file.");
      lcd.setCursor(0,1);
      lcd.print("Remove SD Card...");
    }
  }
}






1/100秒単位の解像度は、実装の都合上3刻みになっています。

loopが1秒間に何回か回ります、その中でボタンの検知、液晶への表示を行っています。

スケッチ内
  delay(dTime2);
  delayMicroseconds(dms);
  __asm__("nop");

の部分、delay関数で大まかにArduinoのMCUを待たせ、delayMicroseconds関数で細やかな待ち時間をMCUに与えています。これで転送した後、どうしても誤差が顕在化してきたので、次の行ではインラインアセンブラ(Arduino IDEで使えるんです!!ただしavr/io.hとavr/interrupt.hをインクルードする必要があります。)

この部分のdTime, dms変数をスケッチ内で変更し、loop()関数内での加算処理を+3から+1に減らしてあげると、1/100秒単位のものも実装できます。

コンパイルが済んだら、Arduinoに転送できます。
ちなみにこの規模になると、SDライブラリとLCDライブラリを1つのスケッチに収めているので、バイナリサイズが約17KBと、15.5KBを超えてしまい、ATMega328搭載Arduino(又はその互換機)専用になってしまいます。

自力でポートを叩いたりしてSDやLCDをタイムクリティカルに制御できる方なら是非ATMega168搭載Arduinoなんかに移植するのも一興かと思います。

(というか、それが出来ない人でも簡単にフィジカルコンピューティングを実現できるようにするためにライブラリが存在するので、移植は車輪の再発明という無駄骨になっちゃうのですが…(^^;))

さて、ソフト側が終わったら今度はハード側の開発です。
自分で宣言したピンとSD, LCDのピンが合うようにプロトシールド基板から配線してみてください。
今回のストップウォッチ・シールドは大半が配線を占めています。

信号線にラッピング線をハンダで接着します。
予め、下の様にワイヤの被覆を剥いたラッピング線を半田メッキしてリードフォーミングしておくと、後々の作業が簡単です。ブリッジしにくくなります。

ちなみに筆者の配線がなぜ表面に集中していて、しかもいつもより汚いのかというと、プロトシールド基板の手持ちが少なくなってきたので、失敗したプロトシールド基板からハンダと部品をはがしたからです。

リワークした基板じゃダメですね、ちゃんとまっさらな処女基板を使ってあげましょう。



筆者は液晶を繋ぐ部分とSDカードを挿入するところに、上の写真のようなピンソケットを使用しました。
これでストップウォッチが用済みになっても、高価な液晶モジュールやSDモジュールを別の用途に使えます。

重ね重ねになりますが、今回の配線では上の写真のように、「ピンの1段上がリード線」という配線を要求されるので・・・。
このように、半田ブリッジを上手く使って配線してみてください。
何事も練習です。
さて、ここでお気づきの方もいるでしょうが、配線の末端が表面に来ている理由はこのピンヘッダの取り付けに理由があります。向きを間違えてしまったので、表面から導線を差し込めなくなってしまったのです。
その結果こういう配線形式になってしまった、ってこと(^^;)


うまく行けば、3列を半田ブリッジで配線、なんてこともできます。

閑話休題、さて、ここまで駆け足で説明していきましたが、筆者の「ストップウォッチ・シールド」は以下の写真のようになりました。

冒頭でも説明したように、オレンジのスイッチでラップタイム計測&書き出し、黒のスイッチで計測終了です。

白のタクトスイッチはハードリセット用に付けたのですが、Arduino本体をリセットすればいいだけで芸がないので、ソフトリセット(計測部分のみ0から再開)用に残してあります。

あと、今回のスイッチはスケッチに
pinMode(sw, INPUT_PULLUP);

と記載しているため、全て負論理です。どういうことかというと、ボタンが押されていないとき、ATMegaマイコンの内蔵プルアップ抵抗のおかげで、論理レベルがHになり、ボタンが押下されると論理レベルがLになります。pinMode関数の第2引数にINPUTではなく、INPUT_PULLUPと書くだけで外付け抵抗を削減できます。

ラップタイムの結果はSDのルートにテキストファイルとして保存されます。
ただ、タクトスイッチの構造上の問題上、チャタリングが発生するので2重に書き込まれる場合が多々あります。まあ書き込まれないよりはマシなので、この問題については放置しています。(ぉぃ)
スケッチ上でdelay関数を挿入して対処できないこともないのですが、時刻が狂うと元も子もありません。

実際に動かしてみると、多少の誤差はあるものの、ちゃんとストップウォッチ然とした動作をしてくれるので、手作りストップウォッチ、是非作ってみてくださいね!



0 件のコメント:

コメントを投稿