docs

Trainer でプログラミングを学ぼう

このページでは,弊サークルメンバーの MOS が作成したはんだ付け・プログラミング練習用キット「Trainer Type-A」を使って,Arduino によるマイコンプログラミング未経験者向けの解説をしていきます

なお,本稿執筆時点では v1 を使用しています

ボードについて

あらかじめ作成しておいてください.はんだ付けは先輩などに教えてもらいながらやりましょう

Arduino IDE について

あらかじめインストールしておいてください.また,D ドライブを作り,D:/Arduino フォルダを作成の上,設定からそのフォルダをスケッチ保存用のフォルダに設定しておいてください

本書の使い方

必要に応じて解説を読んだり,プログラムを書き写したりしてください.また,理解しておいてほしいワードはかぎ括弧「」で囲っているので,わからない場合は検索するなどしてください.誰かに正しく・簡潔に説明できる状態が理想です
一応最後の方に簡単な説明は乗せておこうと思います

第零章|前提知識

Trainer には,「プログラム」が動作する「マイコン」と必要な周辺回路をまとめた「マイコンボード」,数字などを表示できる 7 セグメント LED,それを駆動する回路,普通の砲弾型 LED,入力用の押ボタンスイッチが実装されています

我々がプログラミングと呼ばれる作業によって作成したプログラムはマイコンに搭載されている「CPU」で実行されます.これには様々な入出力の処理や計算などが含まれます
マイコンは電子機器の制御に特化した部品であるため,幾つかの,物によっては多数の「GPIO」が付いています.これらは「IO」とある通り,入力と出力ができます

入力・出力はともに「デジタル」と「アナログ」に二分され,デジタルは処理が高速な代わりに 2 値に限定され,アナログは連続した値を扱える代わりに低速という特徴があります
Arduino の場合,これら デジタル / アナログ の 入力 / 出力 にそれぞれ専用に「関数」が用意されているため,簡単に扱うことができます
ただし,Trainer ではデジタルしか扱っていません.なので,ここからはデジタルの入出力についてのみ説明します.アナログについてはまたの機会に

マイコンが入力を受け付ける時,入力端子の内部にはごく小さなコンデンサが形成されます.そして,そこにたまった電荷が一定値を上回っているか,下回っているかで HIGH / LOW を判断します
マイコンが出力をするとき,出力端子は正電源(5V や 3.3V など)または「GND」に接続されます.その結果,HIGH / LOW のどちらかの電圧を出力します
出力と電源(正側・GND 側問わない)に負荷が接続され,負荷の両端に電位差が生じている場合,負荷に応じた電流が流れることになります.ですが,マイコンの GPIO に流すことができる電流は限られており,既定値を超えて流すと破損の原因となります.ショートや過電流にはくれぐれも気をつけましょう.ただし,LED を光らせる程度の電流であれば大丈夫です

マイコンに対してプログラミングをする行為は一般に “組み込みプログラミング” と呼ばれ,ソフトウェアのプログラミングとは区別されることがあります.これは余談

第一章|回路構成

そのうち書きます

第二章|プログラミング練習

Trainer 用のサンプルコードを使ってやりたい場合は https://i-sys-uf.github.io/docs/edu/getting_started_programming/ を見ながら進めて下さい.もし,分からない等の場合はこっちの方がとっつきやすいかもしれません

書き込みができない等の場合は上記ページを参考にしてください.https://i-sys-uf.github.io/docs/edu/getting_started_programming/#%E4%BD%BF%E3%81%84%E6%96%B9 あたりを見ればだいたい解決するはず

それでは,順にやっていきましょう

新しいスケッチを作成します.Arduino では,プログラムのことをスケッチと呼んでいます
Arduino IDE を起動した際に,過去にいじったスケッチが表示された場合,[Ctrl] + [N] などで新しいスケッチを作成してください
この時点で何かしら名前を付けて保存しておくと良いでしょう.[Ctrl] + [S] を押せば名前を付けて保存ができます

第一節|デジタル出力

ボード上に LED があるので,それを光らせてみましょう
以下のプログラムを写経して,実行してみてください.各構成要素については後で解説します

(写経:プログラムを書き写すこと)

#define LED1 11
#define LED2 10

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
}

void loop() {
  digitalWrite(LED1, HIGH);
  digitalWrite(LED2,  LOW);
  delay(1000);
  digitalWrite(LED1,  LOW);
  digitalWrite(LED2, HIGH);
  delay(1000);
}

写経ができたら,書き込んでみてください.書き込みは IDE の画面左上の右向き矢印のボタンをクリックです

書き込むと自動でプログラムが動き出します.2 つの LED が 1 秒ごとに交互に点滅するはずです.ならない場合や,そもそもコンパイルでコケる場合はコードを見直してみて下さい.どこかが間違ってます

では解説です

このコードは 3 つのブロックに分かれています.上から順に見ていきましょう

#define LED1 11
#define LED2 10

ここではマクロ定義と呼ばれる処理になります.プログラムが「コンパイル」される前に動く「プリプロセス」という段階でコードに加工を加える時に使用されるもので,#define A B と書いたときに A という文字や文字列をすべて,B という文字や文字列,数字に置き換えてくれます

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
}

これはセットアップ関数と呼ばれるもので,起動直後に一度だけ実行される関数になります
ここで実行されているものは pinMode() 関数のみです.この関数について簡単に説明しておきます

pinMode(PIN, STATUS) を実行すると,PIN に指定したピン番号の GPIO が,STATUS で指定した状態に設定されます.STATUS には INPUTINTPUT_PULLUPOUTPUT のどれかを指定します.今回は LED を光らせたい,つまり出力にしたいので OUTPUT とします

void loop() {
  digitalWrite(LED1, HIGH);
  digitalWrite(LED2,  LOW);
  delay(1000);
  digitalWrite(LED1,  LOW);
  digitalWrite(LED2, HIGH);
  delay(1000);
}

これはループ関数と呼ばれ,セットアップ関数が実行されたあとに延々と呼ばれ続ける関数です
ここでは二種類の関数が使用されています.それぞれについて簡単に説明します

digitalWrite(PIN, STATUS) を実行すると,PIN に指定したピン番号の GPIO から,STATUS で指定した状態を出力します.STATUS には HIGHLOW を指定します
Trainer の回路は GPIO -> 抵抗 -> LED -> GND となっているので,HIGH を出力すると点灯し,LOW を出力すると消灯します.ちなみにこれを正論理と表現します

delay(time_ms) を実行すると,time_ms で指定した時間だけ待ちます.単位はミリ秒です.このコードでは 1000 を指定しているので,都合 1 秒待つという処理になります

これらの組み合わせにより,1 秒ごとに交互に点滅するという動作をします

第二節|デジタル入力

出力をやったので入力を使ってみましょう.Traner には,入力として 2 つのスイッチが用意されています

先ほど作成したスケッチを変更しましょう.以下のようにしてください

コピペではなく,手打ちの方が望ましいです.そう長くはないのでがんばって写経しましょう

#define LED1 11
#define LED2 10

#define SW1 5
#define SW2 6

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);

  pinMode(SW1, INPUT_PULLUP);
  pinMode(SW2, INPUT_PULLUP);
}

void loop() {
  if(digitalRead(SW1) == LOW) {
    digitalWrite(LED1, HIGH);
  }else {
    digitalWrite(LED1,  LOW);
  }

  if(digitalRead(SW2) == LOW) {
    digitalWrite(LED2, HIGH);
  }else {
    digitalWrite(LED2,  LOW);
  }
}

マクロ定義,セットアップ関数については第一節の通りなので説明は省きます

ループ関数の様子が先程とは違っています.一つづつみていきましょう

新たな構文 if が出てきました.これは条件分岐をすることができる構文で,以下のように書いて使います

if(条件式) {
  // 条件式が真のときに実行される
}else {
  // 条件式が偽のときに実行される
}

条件式 のところには文字通り条件式が入ります.条件式には以下のものがあります

関係演算子は 2 つの変数の大小関係を,等値演算子は 2 の変数が同じ値かそうでないかを見ることができ,論理演算子はそれらを合成することができます

ここでは等値演算子を使っているので等値演算子についてのみ説明しますが,その他についても知りたい場合は自分で調べてみましょう.苦しんで覚える C 言語あたりが参考になります

新たな関数 digitalRead(PIN) が出てきました.この関数は PIN で指定した GPIO の状態を読み取り,HIGHLOW を返します.その結果が LOW と等しいかを関係演算子で判断し,条件分岐を動かしています
スイッチを押すと,接続されている GPIO の電圧は 0V になるので,スイッチが押されているときに digitalRead(PIN) を呼び出すと LOW が返ってきます
その結果を,定数 LOW と比較して等しければ(つまりスイッチが押されていれば),条件式 digitalRead(PIN) == LOWtrue を返します

if 文の条件式が true になると,その直後のコードを実行します.ここでは対応する LED を光らせています.false なら,else の後が実行され,結果 LED を消します
これにより,スイッチが押されている間対応する LED が点灯し,離すと消えるという動きをします.書き込んで実行してみましょう

第三節|データの処理

ここまででデジタルの入出力ができたので,入力を読み取ってデータを加工し,出力に反映させるということをやってみよう
具体的にはスイッチが “押された瞬間” を検出してみる

では,まずどうしたら押された瞬間を検出できるかを考えてみよう

まず,普通に digitalRead(PIN) でスイッチの状態を読んでわかることが何なのかを知る必要があるわけですが,これは結論を言うと,“今” スイッチが押されているかどうかが分かります
つまり,ただ状態を読み取るだけでは押された瞬間を知ることはできない訳なので,工夫が必要になります

シンプルな方法として,一つ前の状況を保持しておき,その状態と組み合わせて判断させるという方法が挙げられます.今回はこの方法(アルゴリズムと言ったりします)を採用するとして,さっそく実装していきましょう

新しいファイルを作るか,前のコードを変更するかは任せますが,以下のコードを写経してください

#define LED1 11
#define LED2 10

#define SW1 5
#define SW2 6

void setup() {
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);

  pinMode(SW1, INPUT_PULLUP);
  pinMode(SW2, INPUT_PULLUP);
}

void loop() {
  static uint8_t sw1_now, sw2_now;
  uint8_t sw1_prev, sw2_prev;

  sw1_prev = sw1_now;
  sw2_prev = sw2_now;
  sw1_now = digitalRead(SW1);
  sw2_now = digitalRead(SW2);

  if((sw1_prev == HIGH) && (sw1_now == LOW)) {
    digitalWrite(LED1, HIGH);
  }else {
    digitalWrite(LED1,  LOW);
  }
  if((sw2_prev == HIGH) && (sw2_now == LOW)) {
    digitalWrite(LED2, HIGH);
  }else {
    digitalWrite(LED2,  LOW);
  }

  delay(50);
}

マクロ定義とセットアップ関数に変更はありません.ループ関数がゴッソリ変わっているので解説します

  static uint8_t sw1_now, sw2_now;
  uint8_t sw1_prev, sw2_prev;

このコードでは,if 文の条件式のところで状態を読むのではなく,事前に読んでおく方式を取っています.そのときにデータを保存する変数をここでまとめて定義しています
変数には「型」と「修飾子」というものがありますが,それに関する解説はここでは省きます.またの機会にでも

  sw1_prev = sw1_now;
  sw2_prev = sw2_now;
  sw1_now = digitalRead(SW1);
  sw2_now = digitalRead(SW2);

ここで条件分岐に使うデータの操作をしています
上の二行は過去のデータを swx_prev という形で(prevprevius の意味)保存しています.x には 12 が入ります
下の二行は現在のデータを swx_now という形で,関数が返す値を保存しています

上二行目の段階で swx_now に保存されているデータは,1 ステップ前に実行されたループ関数内で保存されたデータ,つまり一段階前のデータなので過去のデータになります.それを swx_prev という名前で保存し直している感じです

このようにすることによって,一つ前の過去データと,今のデータを同時に比較させ,“押された瞬間” を検知することができるわけです.便利ですねぇ

ちなみにですが,これができるようになると,BIT(初心者向けのライントレーサ)のようにモード選択を実装することができます.押された瞬間が分かるので,それに応じてカウントアップやカウントダウンができるからですね

そして実は,この処理,やや高度な工夫をすれば変数一つで実現させることが可能です.わざわざ 1bit 分のデータの保存のためだけに 8bit も使うのはもったいないですからね ビット演算をガッツリ使っているので可読性は低いですが,同じ挙動をします.参考までにループ関数のみ以下に乗せておきます
興味があれば動かしてみて下さい.これはコピペしてもいいです

void loop() {
  static uint8_t data;

  data = (data << 2) + (digitalRead(SW2) << 1) + digitalRead(SW1);

  if((data & 0b00000101) == 0b0100) {
    digitalWrite(SW1, HIGH);
  }else {
    digitalWrite(SW1,  LOW);
  }
  if((data & 0b00001010) == 0b1000) {
    digitalWrite(SW2, HIGH);
  }else {
    digitalWrite(SW2,  LOW);
  }
}

第四節|シリアル通信

この節では今までとは趣向を変え,シリアル通信というものをやっていきます
シリアル通信の中にも色々種類がありますが,ここでは PC とつなぐだけで使える UART 通信を使っていきます

新しいスケッチを作成して以下のコードを写経して下さい

#define BAUDRATE 9600

void setup() {
  Serial.begin(BAUDRATE);
  Serial.println("This is setup");

  delay(1000);
}

void loop() {
  Serial.println("This is loop.");

  delay(1000);
}

とても簡単なコードです.新たに 2 つの関数が登場しているので,それぞれについて簡単に説明します

Serial.begin(speed) はシリアル通信を開始するための関数で,速度を指定して呼び出します
Serial.println(text or valiable) は PC に対して UART to USB 変換 IC 経由で文字列を送信します.末尾に改行文字が自動で挿入されます
Serial.print(text or valiable) とすると末尾に改行を入れずに送信することができます

[Ctrl] + [Shift] + [M] でシリアルモニタを開き,実行してみましょう.1 秒ごとに文字列が表示されます

これを使うと,簡単に変数の中身を表示させたり,関数が実行されているかを確認したりすることができます.つまり,デバッグに使うことができます
必要に応じて使ってみて下さい

簡易用語解説