Arduino Uno で電子工作

[2015/10/01新規] [2015/11/01更新]

表題のテーマを備忘録として書き留めていきます。

Contents

Arduino に関する主な情報源

以下の Arduino に関する情報は必ず必要になります。

  1. Arduino Uno の一次情報源 … ここから様々なサイトに辿ります。
  2. Arduino Software(IDE) … このアプリケーションをダウンロードして、コンパイルや Arduino への書き込みを行います。
  3. 電子回路エディタ Fritzing … 電子回路設計だけでなく、コンパイルと Arduino への書き込みが上の Arduino Software(IDE) を介して出来ます。「Fritzing」メニューの「Preferences」の「Code View」にて Arduino Software(IDE) のパスを指定しておいて下さい。
  4. Arduino Uno の概要図 … まさに一次情報元のひとつです。
  5. Arduino 言語リファレンス … スケッチを書くのに必要な基本中の基本です。
  6. Arduino ライブラリリファレンス … スケッチを書くのに必要です。
  7. AVR Libc ホームその日本語訳 … Arduino Uno で用いられているマイコンチップ ATMEGA328P-PU は Atmel AVR のひとつです。その標準 C ライブラリ(但し、サブセット)の一次情報元です。スケッチを書くのに有用です。

他のサイトは曖昧で廃れたことが書いてあったりするので混乱するだけということが多いです。なるべく一次情報元に近づいて調べる努力が必要です。

Arduino の様々なスケッチと回路図

何もしないスケッチ

何かとお世話になるのが、Arduino を起動した時に、以前の作業で入れっぱなしにしたスケッチが意図しない動作をしないよう、後始末として入れておく「何もしないスケッチ」'empty.ino'です。

void setup() {}
void loop() {}

通常であれば 'setup' 関数に何らかの初期化の手続きを、'loop' 関数に繰り返し実行される手続きを書きます。

お休みスケッチ

しかし、この状態でつい夢中になって長時間放置してしまうと、いささか消費電力が気になります。そこで、よりお勧めなのが、後始末として入れておく「お休みスケッチ」'sleep.ino'です。

#include <avr/sleep.h>
void setup()
{
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_mode();
}
void loop() {}

'set_sleep_mode' で指定できる定数は Atmel AVR マイコンの種類によって異なるのですが、Arduino Uno で用いられているマイコンチップ ATMEGA328P-PU では以下から選びます。

SLEEP_MODE_IDLE
SLEEP_MODE_ADC
SLEEP_MODE_PWR_DOWN
SLEEP_MODE_PWR_SAVE
SLEEP_MODE_STANDBY
SLEEP_MODE_EXT_STANDBY

ここでは、詳しくは後述するとして、SLEEP_MODE_PWR_DOWN を選びます。

これらのスケッチを Arduino で 'Command+U' コンパイルして Arduino Uno に送り込んでおけば、次にうっかり別の配線で USB で繋いで起動してしまっても安心です。

Arduino のスケッチ言語

LED点滅器

led_blinker-pinno13_breadboard.svg led_blinker-pinno13_circuit.svg

LED には極性があって端子が、長いか細い方が陽極(アノード)、短いか太い方が陰極(カソード)です。5V の電圧が掛かりますので、極性を間違えて接続すると壊れる場合がありますので気をつけましょう。そして、「電流制限抵抗値」として、およそ 1kΩ の抵抗を直列して繋ぎ、Arduino のいずれかの 'GND' 端子に戻しましょう。

ここで、「この 1kΩ の抵抗は不要」というどこぞの向きは、古い Arduino NG での構成(Arduino NG の概要図)によるもの、という向きもあり、しかし、それを見ても抵抗が不要に見えません。そんなネット情報に惑わされず、ちゃんと計れば 5V が出力されています。よって、抵抗がないと LED を痛めますので注意しましょう。

さて、デジタル13番ピンでこのように LED を接続して光らせることは、Arduino Uno 組み込みの LED のうち「L」と刻印された黄色の LED を光らせることと同じです。よって、簡便な入門や動作試験に適した例としてよく取り上げられます。

ここでは簡単な差分方程式により点滅の間隔がゆらぐスケッチ 'led_blinker-pinno13.ino' を示しましょう。

double modified_Bernoulli_map(double x, double b1 = 2, double b2 = 2)
{
  static const double epsilon(/*std::numeric_limits<double>::min()*/1e-13);
  if (x <= .5)
    x += pow(2., b1-1)*(1 - 2*epsilon)*pow(x, b1) + epsilon;
  else
    x -= pow(2., b2-1)*(1 - 2*epsilon)*pow(1-x, b2) - epsilon;
  return x;
}

const int led_pinno(13);
int led_state(false);
double x(.1), interval(.2)/*[sec]*/;

void setup()
{
  pinMode(led_pinno, OUTPUT);
}
void loop()
{
  led_state = !led_state;
  digitalWrite(led_pinno, led_state ? HIGH : LOW);
  x = modified_Bernoulli_map(x);
  delay(1000*interval*x);
}

まず、'setup' ルーチンにて 'pinMode(pinno, OUTPUT)' でデジタル pinno 番ピンを出力モード 'OUTPUT' にする必要があります。その上で、'digitalWrite(pinno, HIGH)' とすると、そのデジタル pinno 番ピンに 5V の電圧が 掛かり、抵抗器を介して LED が光ります。逆に、'digitalWrite(pinno, LOW)' とすると、そのデジタル pinno 番ピンの電圧がオフになり、LED が消えます。そして、その繰り返しの 'loop' ルーチンにて、'delay' 関数に渡しているミリ秒が差分方程式 'modified_Bernoulli_map' の値 (0, 1) の間でゆらぎますので、しばらく光らなかったり光り続けたりするスケッチになっています。

LED に直列する抵抗器

さて、先の LED に直列した抵抗器が、なぜ 1kΩ なのでしょうか。これは、その LED に流すことが出来る電流(それを越えると壊れるか痛めます)と順方向電圧(Vf)があり、一般的に数mA〜20mAと2Vfあたりです。5V 電圧のうち Vf=2[V] で 20mA 流してよいとすると、

\((5\textrm{[V]} - 2\textrm{[V]}) / 20\textrm{[mA]} = (5 - 2) \times 50 = 150\textrm{[Ω]}\)

となります。Vf=2[V] で 10mA 流してよいとすると、

\((5\textrm{[V]} - 2\textrm{[V]}) / 10\textrm{[mA]} = (5 - 2) \times 100 = 300\textrm{[Ω]}\)

となります。Vf=2[V] で 5mA しか流してはいけないとすると、

\((5\textrm{[V]} - 2\textrm{[V]}) / 5\textrm{[mA]} = (5 - 2) \times 200 = 600\textrm{[Ω]}\)

となります。その LED に流すことが出来る電流と順方向電圧は LED の種類毎に依りますので、安全を見て 1000Ω = 1kΩ としたのです。よって、ものによっては暗く光りますので、その場合この「電流制限抵抗値」を下げる必要があります。

抵抗器の帯の色

抵抗器の帯の色は以下の数字で抵抗値やその許容差(精度)を表しています。

数字色名とその配色許容差[±%]
-210
-15
0
11
22
3
4
50.5
60.25
70.1
8
9

このように、色は「金」と「銀」を除いて、HSV色空間の色相(Hue)を想わせる順序なので、さほど覚えづらいということはないと思います。「黒茶赤が012から始まり、橙黄緑青が3456を経て、紫灰白で789、戻って金銀で-1-2」と覚え易いです。一方、許容差の方は覚えにくいですが、用途や価格帯で「金」5±%、「緑」0.5±%といずれかに揃いますので実用上困惑はありません。具体的には、以下の4帯の配色の抵抗器一覧のように、配色は2桁の有効数字、乗ずる10の冪数、抵抗値許容差の4色からなっており、最初の3配色A,B,Cから抵抗値を求めるには(10A+B)*10Cとなります。

registers_breadboard.svg

この例では、4色目の帯が「金」色ですので、すべて5%の許容差の抵抗器です。そして、そもそも生産されていて入手できるのは、飛び飛びの(対数目盛で分割された)値のE系列規格の抵抗器です。よって、そのなかでもメジャーなこのE6列抵抗器の以下の配色一覧({10,15,22,33,47,68}*10C)は覚えてしまいましょう。

配色抵抗値配色抵抗値配色抵抗値配色抵抗値配色抵抗値配色抵抗値
10/10=1Ω15/10=1.5Ω22/10=2.2Ω33/10=3.3Ω47/10=4.7Ω68/10=6.8Ω
10*100=10Ω15*100=15Ω22*100=22Ω33*100=33Ω47*100=47Ω68*100=68Ω
10*101=100Ω15*101=150Ω22*101=220Ω33*101=330Ω47*101=470Ω68*101=680Ω
10*102=1kΩ15*102=1.5kΩ22*102=2.2kΩ33*102=3.3kΩ47*102=4.7kΩ68*102=6.8kΩ
10*103=10kΩ15*103=15kΩ22*103=22kΩ33*103=33kΩ47*103=47kΩ68*103=68kΩ
10*104=100kΩ15*104=150kΩ22*104=220kΩ33*104=330kΩ47*104=470kΩ68*104=680kΩ
10*105=1MΩ

ちなみに、0Ω抵抗器は、安価なジャンパとして需要があるので敢えてここに記しました。

他に3桁の有効数字である5帯や、さらに温度係数を加えた6帯で仕様を表す抵抗器もあります(参考:鶴 剛 著: 学部授業「エレクトロニクス」講義ノート, 京都大学 理学部 物理学第二教室 宇宙線研究室, 2008)。また、抵抗器には定格電力というのがあり(1/6W, 1/4W, 1/2W, 1W, 5W, 40Wなどと付記されている)、種類と価格帯、用途もそれぞれ異なります。

ブレッドボードの種類

ブレッドボードにはいくつかの種類があります。電子回路エディタ Fritzing で用意されているものをみてみましょう。

breadboard_types_breadboard.svg

基本的に「数字」の列、つまり「縦方向」に結線されています。その他、上部と下部に「横方向」に結線された行が加わったものもあります(上図の full+, full, half+, half, BB-301)。その「横方向」の結線が真中で途切れているものもあります(上図の BB-301)。これを知らなければ悩みの原因になります(例えば、EIC-901BEIC-102B とは結線が異なります!)。「縦方向」の結線と「横方向」の結線の穴の位置が揃っていないものもあります(上図の half+)。

Fritzing によるブレッドボードと回路図の編集

LEDアナログ点灯器

led_quiver_breadboard.svg led_quiver_circuit.svg

さて、デジタル13番ピンではなく、デジタル11番ピンで LED をこのアナログ(PWM)でゆらがせます。それが以下の 'led_quiver.ino' です。

double modified_Bernoulli_map(double x, double b1 = 2, double b2 = 2)
{
  static const double epsilon(/*std::numeric_limits<double>::min()*/1e-13);
  if (x <= .5)
    x += pow(2., b1-1)*(1 - 2*epsilon)*pow(x, b1) + epsilon;
  else
    x -= pow(2., b2-1)*(1 - 2*epsilon)*pow(1-x, b2) - epsilon;
  return x;
}

const int led_pinno(11);
int led_brightness(0);
double x(.1), interval(.01)/*[sec]*/;

void setup()
{
  pinMode(led_pinno, OUTPUT);
}
void loop()
{
  led_brightness = (led_brightness + ((x <= .5) ? 1 : -1)) % 256;
  analogWrite(led_pinno, led_brightness);
  x = modified_Bernoulli_map(x);
  delay(1000*interval);
}

先の 'led_blinker-pinno13.ino' と若干異なり、'digitalWrite(pinno, HIGH)' ではなく 'analogWrite(pinno, value)' となっています。'value' は 0〜255 です。'led_brightness' が差分方程式 'modified_Bernoulli_map' の値 (0, 1) によってゆらぎながら増減しますので、消灯に近付いたり点灯に近付いたりを不規則に繰り返すスケッチになっています。

デジタルピンの用途

このように Arduino Uno のデジタルピンの番号の後ろに「〜」と刻印されているピンは、アナログ、パルス幅変調(PWM: Pulse Width Modulation)信号を出力することができます。以下に、それが可能なピン一覧をまとめておきます。

pinno012345678910111213
PWD
specialRXTXINT0INT1 SSMOSIMISOSCK(LED)
ATMEGA328P-PU23456111213141516171819

ついでに、Arduino Uno で用いられているマイコンチップ ATMEGA328P-PU のピン番号 1〜28 への対応や、以下の他の特別な用途も添えました。

  1. 後述しますが、デジタルピン番号 0 と 1 は Arduino Software(IDE) との USB 経由での TTL シリアル通信(RX: Receive, TX: Transmit)に用いられており、それぞれ「RX」,「TX」と刻印された黄色の LED が光ります。
  2. 後述しますが、デジタルピン番号 2 と 3 は 'attachInterrupt(interrupt, function, mode)' 関数でそれぞれ interrupt'0''1' とするデジタル入力による「割り込み」をサポートしています。
  3. 上述のように、デジタルピン番号 3, 5, 6, 9, 10, 11 は 'analogWrite(pinno, value)' による 8-bit PWD 出力をサポートします。
  4. 後述しますが、デジタルピン番号 10, 11, 12, 13 はシリアル周辺装置インターフェース(SPI: Serial Peripheral Interface)で用いる SS(Slave Select), MOSI(Master Out Slave In), MISO(Master In Slave Out), SCK(Serial Clock) にそれぞれ使われる場合があります。
  5. 上述のように、デジタル 13 番ピンは「L」と刻印された LED がデジタル出力で光ります。

スイッチ

スイッチの常時監視

led_blinker-pinno13_switch_breadboard.svg led_blinker-pinno13_switch_circuit.svg

さて、タクトスイッチを用いて LED のオンオフを手動で切替えることを考えます。デジタル 2 番ピンにタクトスイッチが押されたときに 10kΩ の抵抗器を介して電圧を掛けることにしましょう。この 10kΩ の抵抗器がないと過剰な電流が流れて、マイコンを壊してしまうので注意。

まずは 'digitalRead(pinno)' でそれを読みとることにします。それが以下の 'switch_loop.ino' です。

const int sw_pinno(2);
const int led_pinno(13);
const double interval(.1)/*[sec]*/;
bool led_sw(true), sw_previous(0);

void setup()
{
  pinMode(sw_pinno, INPUT);
  pinMode(led_pinno, OUTPUT);
}
void loop()
{
  bool sw(digitalRead(sw_pinno));
  if (!sw_previous && sw)
    led_sw = !led_sw;
  digitalWrite(led_pinno, led_sw ? HIGH : LOW);
  delay(1000*interval);
  sw_previous = sw;
}

'pinMode(pinno, INPUT)' でデジタルピンを入力モードにしています。そして、'sw = digitalRead(pinno)' オンで、かつ前回の 'sw_previous' がオフのときのみ 'led_sw' を反転させており、それにより LED のオンオフを手動で切替えられるようになっています。しかし、これでは常にデジタルピンの入力を監視していて消費電力が気になります。

スイッチによる割り込み

そこで、上述の「お休みスケッチ」の発展させて、タクトスイッチで割り込みをさせて LED のオンオフを手動で切替えるようにしましょう。それが以下の 'switch.ino' です。

#include <avr/sleep.h>

const int led_pinno(13);
const double interval(.1)/*[sec]*/;
volatile bool led_sw(true);

void callback_falling()
{
  led_sw = !led_sw;
}
void setup()
{
  attachInterrupt(digitalPinToInterrupt(2), callback_falling, FALLING);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  pinMode(led_pinno, OUTPUT);
}
void loop()
{
  digitalWrite(led_pinno, led_sw ? HIGH : LOW);
  if (!led_sw)
    sleep_mode();
  delay(1000*interval);
}

配線は先のと一緒です。'attachInterrupt(digitalPinToInterrupt(2), callback_falling, FALLING)' でデジタル 2 番ピンに入力があった場合に 'callback_falling' 関数が呼ばれるようになっています。この関数内では 'delay' などあまり凝ったことはできません。ここでは 'led_sw' の反転のみをしています。'led_sw' が真であれば LED は光りますが、偽であればスリープ状態になるというスケッチになっています。'FALLING' を含め他に、すべてではありませんが以下のような割り込みのトリガとなる種類があります。

CHANGE値が変わった時
RISING値が LOW から HIGH に変わった時
FALLING値が HIGH から LOW に変わった時

この割り込みは Arduino Uno ではデジタルピン番号 2 と 3 のみ有効です。ここで注意すべきは 'led_sw''volatile bool led_sw' という定義になっている点です。真偽値 'bool' であるのはよいとして 'volatile' とは何でしょうか。これはプログラミング言語 C/C++ の最適化による副作用を抑止する為の予約後で、'led_sw' 変数を高速なレジスタに格納せずにメモリから参照せよという意味です。この場合、割り込みでの 'led_sw' とグローバル変数としての 'led_sw' が意図するように同一のものであることを定義しているわけです。さもないと、最適化の状況によっては、これらは別のものになってしまいますので、それを防いでいるのが 'volatile'(揮発性)という予約後の役割なのです。

半固定抵抗器によるアナログ入力

led_quiver_parameter_breadboard.svg led_quiver_parameter_circuit.svg

次は、Arduino へのアナログ入力を半固定抵抗器によって手動で変化させ、その値によって LED の明るさを変化させてみましょう。以下がその 'led_quiver_parameter.ino' です。(ちなみに、この主題であれば、LED に半固定抵抗器を直列すれば同じようなことは出来ますが、それは別途のちに補足します。)

//#define SERIAL_DEBUG

const int led_pinno(11), parameter_pinno(0);
const double interval(.2)/*[sec]*/;

void setup()
{
#if defined(SERIAL_DEBUG)
  Serial.begin(9600);
#endif
  pinMode(led_pinno, OUTPUT);
}
void loop()
{
#if defined(SERIAL_DEBUG)
  int c(analogRead(parameter_pinno));
  analogWrite(led_pinno, map(c, 0, 1023, 0, 255));
  Serial.println(c);
#else
  analogWrite(led_pinno, map(analogRead(parameter_pinno), 0, 1023, 0, 255));
#endif
  delay(1000*interval);
}

半固定抵抗器をマイナスドライバーで回してみると、それに応じて LED の明るさが変わるスケッチになっています。

5V 電圧の分圧をアナログ入力で計るだけなので半固定抵抗器は適当でよいのですが、ここでは '103' と刻印された最大抵抗値 10*103 = 10kΩ の半固定抵抗器を用いています。

そして、'analogRead(pinno)' が 1024 の分解能でその半固定抵抗器の真中の端子の電圧を計っているわけです。それを 256 にスケール変換して LED に 'analogWrite(pinno, value)' しています。

ちなみに、'map' はコアライブラリにある Arduino 特有のユーティリティ関数で、符号付き整数値をスケール変換するだけのものです(C++ STL の 'map' とは無関係)。リファレンスが https://www.arduino.cc/en/Reference/Map にあります。

シリアル通信デバッグ

スケッチの作成、すなわちプログラミングにおける最も単純なデバッグ方法は、プログラムによって変化する状態の変化を印字してみることです。印字すること自体が状態を変えてしまうこともあるので万能ではありませんが、設計やアルゴリズムの確認には十分に有用です。上記のスケッチのうち、'SERIAL_DEBUG' が真のときに有効になるコードがそれにあたります。つまり、'//#define SERIAL_DEBUG' のコメント行を有効にすると以下のようになります。

const int led_pinno(11), parameter_pinno(0);
const double interval(.2)/*[sec]*/;

void setup()
{
  Serial.begin(9600);
  pinMode(led_pinno, OUTPUT);
}
void loop()
{
  int c(analogRead(parameter_pinno));
  analogWrite(led_pinno, map(c, 0, 1023, 0, 255));
  Serial.println(c);
  delay(1000*interval);
}

素の Arduino ではシリアルポートに出力するのが簡便です。それにはまず、'Serial.begin(9600)' によってシリアルポートを初期化します。その後は、'Serial.println(value...)' で印字したい状態を印字すれば、IDE の「シリアルモニタ」でそれを受信することが出来ます。

半固定抵抗器と LED の直列接続

先程「この主題であれば、LED に半固定抵抗器を直列すれば同じようなことは出来ます」と書きました。それが以下のような配線となります。

led_quiver_potentiometer_breadboard.svg led_quiver_potentiometer_circuit.svg

この例では実のところ、Arduino は電圧を供給しているだけなのでスケッチですべきことはあまりないのですが、多少工夫をしてみた結果、有用な例が以下の 'led_quiver_potentiometer.ino' です。

#include <avr/sleep.h>

const int led_pinno(13);

void setup()
{
  pinMode(led_pinno, OUTPUT);
  digitalWrite(led_pinno, HIGH);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_mode();
}
void loop() {}

先程と同様に、半固定抵抗器をマイナスドライバーで回すと、それに応じて LED の明るさが変わる回路なのですが、スケッチは、デジタルピンを出力モードにしたのち速やかにスリープするというものになっています。

つまり、Arduino はスリープしてもデジタルピンへの出力は継続されることがわかりました。即ち、省電力を目指した設計をするのであれば、不要な出力はオフにした上でスリープを行うようにする必要があるということです。

ちなみに、半固定抵抗器は 0Ω になるときがあるので、150Ω の電流制限抵抗値は必要です。つまり、5V 電圧のうち LED の順方向電圧 Vf=2[V] で 20mA 流してよいとすると、

\((5\textrm{[V]} - 2\textrm{[V]}) / 20\textrm{[mA]} = (5 - 2) \times 50 = 150\textrm{[Ω]}\)

というように求めます。また、白のジャンパは半固定抵抗器が接点不良を起しても少なくとも最大抵抗値を保つようにするためのものです。

温度計とアナログ入力

thermometer_breadboard.svg thermometer_circuit.svg

温度センサ LM61CIZ の使用例を示します。回路図で示したように、この温度センサは3端子あり、5V 電圧供給、GND と電圧出力端子です。使用する温度センサの定格表をみて、端子を間違えずに配線します。気温をシリアル通信へ出力するスケッチは以下の 'thermometer.ino' となります。

#define SERIAL_DEBUG

class thermometer {
public:
  thermometer(const int pin = 5) : pinno(pin) {}
  double get_temperature()/*[degrees Celsius]*/ const
  {
    return map(map(analogRead(pinno),
                   0, 1023, 0, 5000),
               300, 1600, -30, 100);
  }
private:
  int pinno;
};

const double interval(1)/*[sec]*/;

void setup()
{
#if defined(SERIAL_DEBUG)
  Serial.begin(9600);
#endif
}
void loop()
{
  double temperature(thermometer().get_temperature());
#if defined(SERIAL_DEBUG)
  Serial.println(temperature);
#endif
  delay(1000*interval);
}

温度センサの定格表によると、5V のうち 300mV〜1600mV の範囲が -30℃〜100℃ ということなので 'map' を上記のように使用して温度に変換しています。それを 1 秒間隔で出力を繰り返すスケッチになっています。

諸情報の確認

ここで一旦立ち止まって、Arduino と言語に関するさまざまな情報を、シリアル通信端末に印字して確認してみましょう。ただ印字するだけでは面白くないので、ここでは C++ の 'iostream' のように出力できる Streaming ライブラリを IDE に追加します。これを用いてさまざまな情報を出力するのが以下の 'cout.ino' です。

#include <avr/sleep.h>
#include <Streaming.h>

void setup()
{
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

#if defined(__STDC_VERSION__)
  Serial << "__STDC_VERSION__:\t" << __STDC_VERSION__ << endl;
#endif
#if defined(__cplusplus)
  Serial << "__cplusplus:\t" << __cplusplus << endl;
#endif
  
  Serial << "sizeof(char):\t" << sizeof(char) << endl;
  Serial << "sizeof(short):\t" << sizeof(short) << endl;
  Serial << "sizeof(int):\t" << sizeof(int) << endl;
  Serial << "sizeof(long):\t" << sizeof(long) << endl;
  Serial << "sizeof(long long):\t" << sizeof(long long) << endl;
  Serial << "sizeof(float):\t" << sizeof(float) << endl;
  Serial << "sizeof(double):\t" << sizeof(double) << endl;
  Serial << "sizeof(long double):\t" << sizeof(long double) << endl;
  Serial << "sizeof(size_t):\t" << sizeof(size_t) << endl;
  Serial << "sizeof(void *):\t" << sizeof(void *) << endl;
  Serial << "sizeof(ptrdiff_t):\t" << sizeof(ptrdiff_t) << endl;
  Serial << "sizeof(bool):\t" << sizeof(bool) << endl;
  Serial << "sizeof(boolean):\t" << sizeof(boolean) << endl;

  Serial << "__AVR_LIBC_VERSION_STRING__:\t" << __AVR_LIBC_VERSION_STRING__ << endl;
  Serial << "__AVR_LIBC_VERSION__:\t" << __AVR_LIBC_VERSION__ << endl;
  Serial << "__AVR_LIBC_DATE_STRING__:\t" << __AVR_LIBC_DATE_STRING__ << endl;
  Serial << "__AVR_LIBC_DATE_:\t" << __AVR_LIBC_DATE_ << endl;
  Serial << "__AVR_LIBC_MAJOR__:\t" << __AVR_LIBC_MAJOR__ << endl;
  Serial << "__AVR_LIBC_MINOR__:\t" << __AVR_LIBC_MINOR__ << endl;
  Serial << "__AVR_LIBC_REVISION__:\t" << __AVR_LIBC_REVISION__ << endl;

  Serial << "LED_BUILTIN:\t" << LED_BUILTIN << endl;

  Serial << "RAMEND:\t0x" << _HEX(RAMEND) << endl;
  Serial << "XRAMEND:\t0x" << _HEX(XRAMEND) << endl;
  Serial << "E2END:\t0x" << _HEX(E2END) << endl;
  Serial << "FLASHEND:\t0x" << _HEX(FLASHEND) << endl;
  Serial << "SPM_PAGESIZE:\t" << SPM_PAGESIZE << endl;
  Serial << "RAMEND:\t0x" << _HEX(RAMEND) << endl;

  Serial.flush();

  digitalWrite(LED_BUILTIN, LOW);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_mode();
}
void loop() {}

手元の Arduino Uno では以下のように出力されました。

__cplusplus:	199711
sizeof(char):	1
sizeof(short):	2
sizeof(int):	2
sizeof(long):	4
sizeof(long long):	8
sizeof(float):	4
sizeof(double):	4
sizeof(long double):	4
sizeof(size_t):	2
sizeof(void *):	2
sizeof(ptrdiff_t):	2
sizeof(bool):	1
sizeof(boolean):	1
__AVR_LIBC_VERSION_STRING__:	1.8.0svn
__AVR_LIBC_VERSION__:	10800
__AVR_LIBC_DATE_STRING__:	20111229
__AVR_LIBC_DATE_:	20111229
__AVR_LIBC_MAJOR__:	1
__AVR_LIBC_MINOR__:	8
__AVR_LIBC_REVISION__:	0
LED_BUILTIN:	13
RAMEND:	0x8FF
XRAMEND:	0x8FF
E2END:	0x3FF
FLASHEND:	0x7FFF
SPM_PAGESIZE:	128
RAMEND:	0x8FF

ちなみに 'Serial.flush()' がないと、最後まで出力されないときがあるので注意しましょう。

圧電ブザーによる演奏

piezomelody_breadboard.svg piezomelody_circuit.svg

Arduino コアライブラリには圧電ブザーやスピーカーから周波数を指定して音を出す 'tone' 関数があります。デジタルピンを出力モードにした上でこの 'tone' を使えば音がなります。ちなみに、PWD 対応のデジタルピンに繋がなくても構いません。では、上図の配線でメロディを奏でてみましょう。それが以下のスケッチ 'piezomelody.ino' 、そして曲名は「デイジー・ベル」です。

static inline
double range_map_logarithm(const double src_value,
                           const double src_lb, const double src_ub,
                           const double dst_lb, const double dst_ub)
{
  const double src_sz = src_ub - src_lb, dst_sz = log(dst_ub) - log(dst_lb);
  return dst_lb*exp(dst_sz*(src_value - src_lb)/src_sz);
}
static inline
double twelve_equal_temperament(const double c_freq_end, const int n_octaves, const int octave, const int i)
{
  static const int n_equal_temperament(12);
  return range_map_logarithm(octave*n_equal_temperament + i,
                             0, n_octaves*n_equal_temperament,
                             c_freq_end/(1ULL<<n_octaves), c_freq_end);
}

#define R	-1
#define C	0
#define CS	1
#define D	2
#define DS	3
#define E	4
#define F	5
#define FS	6
#define G	7
#define GS	8
#define A	9
#define AS	10
#define B	11

#define L32	1./32
#define L16	1./16
#define L16d	L16+L32
#define L8	1./8
#define L8d	L8+L16
#define L4	1./4
#define L4d	L4+L8
#define L2	1./2
#define L2d	L2+L4
#define L1	1./1

struct music_note {
  int octave, i;
  double beet;
} one_octave[] = {
#define O 0
  { O, C, L4 }, { O, D, L4 }, { O, E, L4 }, { O, F, L4 }, { O, G, L4 }, { O, A, L4 }, { O, B, L4 }, { O+1, C, L4 }, { O+1, R, L4 },
}, *octaves, daisy_bell[] = {
  { O+1, A, L2d }, { O+1, E, L2d }, { O+1, C, L2d }, { O, G, L2d }, { O, A, L4 }, { O, B, L4 }, { O+1, C, L4 }, { O, A, L2 }, { O+1, C, L4 }, { O, G, L2d+L2d },
  { O+1, D, L2d }, { O+1, F, L2d }, { O+1, E, L2d }, { O+1, C, L2d }, { O, A, L4 }, { O, B, L4 }, { O+1, C, L4 }, { O+1, D, L2 }, { O+1, E, L4 }, { O+1, D, L2d+L2 }, { O+1, E, L4 },
  { O+1, F, L4 }, { O+1, E, L4 }, { O+1, D, L4 }, { O+1, G, L2 }, { O+1, E, L4 }, { O+1, D, L4 }, { O+1, C, L2+L2 }, { O+1, D, L4 }, { O+1, E, L2 }, { O+1, C, L4 }, { O, A, L2 }, { O+1, C, L4 }, { O, A, L4 }, { O, G, L2+L2 }, { O, G, L4 },
  { O+1, C, L2 }, { O+1, E, L4 }, { O+1, D, L2 }, { O, G, L4 }, { O+1, C, L2 }, { O+1, E, L4 }, { O+1, D, L4 }, { O+1, E, L4 }, { O+1, F, L4 }, { O+1, G, L4 }, { O+1, E, L4 }, { O+1, C, L4 }, { O+1, D, L2 }, { O, G, L4 }, { O+1, C, L2d+L2d },
#undef O
};

#undef L32
#undef L16
#undef L16d
#undef L8
#undef L8d
#undef L4
#undef L4d
#undef L2
#undef L2d

#undef C
#undef CS
#undef D
#undef DS
#undef E
#undef F
#undef FS
#undef G
#undef GS
#undef A
#undef AS
#undef B

size_t n_one_octave(sizeof(one_octave)/sizeof(one_octave[0])), n_octaves, n_daisy_bell(sizeof(daisy_bell)/sizeof(daisy_bell[0]));

class piezo_buzzer {
public:
  int pinno, c_freq_end, n_octaves, modulation;
  piezo_buzzer(const int pn = 9, const int safe_c_freq = 4186/4, const int safe_n_octaves = 3, const int key = 0) : pinno(pn), c_freq_end(safe_c_freq), n_octaves(safe_n_octaves), modulation(key)
  {
    pinMode(pinno, OUTPUT);
  }
  void play(const int n_note, const struct music_note note[], const double tempo)
  {
    for (int i=0; i<n_note; ++i) {
      if (note[i].i < 0)
        delay(note[i].beet*60000/tempo);
      else {
        int duration(note[i].beet*60000/tempo);
        int freq_end(round(twelve_equal_temperament(c_freq_end, n_octaves, n_octaves, modulation)));
        int freq(round(twelve_equal_temperament(freq_end, n_octaves, note[i].octave, note[i].i)));
        tone(pinno, freq, duration);
        delay(duration);
      }
      delay(1);
    }
  }
} piezo_buzzer;

const double tempo(200./4);

void setup()
{
  n_octaves = n_one_octave * piezo_buzzer.n_octaves;
  octaves = (music_note *)malloc(sizeof(music_note)*n_octaves);
  for (int i=0; i<piezo_buzzer.n_octaves; ++i)
    for (int j=0; j<n_one_octave; ++j) {
      octaves[i*n_one_octave + j] = one_octave[j];
      octaves[i*n_one_octave + j].octave = i + (one_octave[j].octave - one_octave[0].octave);
    }
}
void loop()
{
  piezo_buzzer.play(n_daisy_bell, daisy_bell, tempo);
  delay(1000*3);
  piezo_buzzer.play(n_octaves, octaves, tempo);
  delay(1000*3);
}

このスケッチの解説をします。

  1. 音階と周波数の対応は 'https://code.google.com/p/rogue-code/wiki/ToneLibraryDocumentation#Musical_Notes' にて一覧表になっていますが、これはアルゴリズムで得られますので必要ありません。代わりに、上記 'twelve_equal_temperament' 関数を使います。この関数は引数にて、'n_octaves' オクターブあるなかで最大の「シ」の次の「ド」の周波数を 'c_freq_end' に指定しつつ、音階 'octave' 番目の平均律の音度 'i' (0〜11)を指定すれば、返り値としてその周波数が得られるようになっています。
  2. 'c_freq_end' の代わりに 'freq_end = twelve_equal_temperament(c_freq_end, n_octaves, n_octaves, modulation)' のように求めた 'freq_end' を使うと、さらに音度 'modulation' (-11〜11) で変調した周波数が得られるようになっています。
  3. '#define O 0' は人が試行し易いように音階のバイアスをマクロ定義しているものです。これは使い終わったら '#undef O' のように未定義にしておきます。
  4. '#define C 〜' などは音度を、人が入力し易いように対応付けたマクロ定義です。そして、'#define R 〜' は「休符」を表します。これらも使い終わったら '#undef C' のように未定義にしておきます。
  5. '#define L32 〜' などは音価(音の長さ)への対応です。'#define L4 〜' が「四分音符」または「四分休符」になります。やはりこれらも使い終わったら '#undef L32' のように未定義にしておきます。
  6. 'struct music_note' はひとつの「音階、音度、音価」を格納する構造体で、それを要素とする配列がここでの「楽譜」になります。この場合は、'one_octave[]', '*octaves', 'daisy_bell[]' がそれに相当します。
  7. 'struct music_note' を要素とする配列のサイズが「楽譜の長さ」に相当し、それがこの場合、それぞれ 'n_one_octave', 'n_octaves', 'n_daisy_bell' にそのサイズが代入されています。
  8. 'class piezo_buzzer' はコンストラクタ 'piezo_buzzer' にてデジタル 'pinno' 番ピンを出力モードにして、'play' メンバ関数にて「楽譜」を演奏するクラスになっています。このインスタンスが 'piezo_buzzer' です。
  9. 'play' メンバ関数の引数にて、'n_note' に 「楽譜の長さ」、'note' 配列に「楽譜」、そして 'tempo' にその曲のテンポ、「四分音符=200」なら '200./4' 、「二分音符=100」なら '100./2'、つまり1分間の全音符における拍数を指定します。
  10. 'setup' では、全オクターブの楽譜 'octaves' を 1 オクターブ 'one_octave' から生成しています。'n_octaves' には全オクターブの楽譜の長さが代入されます。
  11. 'loop' では、「デイジー・ベル」を奏でた後、全オクターブの楽譜 'octaves' も奏でます。それの繰り返しです。
  12. 'class piezo_buzzer' のメンバ変数 'modulation' には「変調」を指定できます。これはコンストラクタの引数 'key' で既定値は '0' です。

傾斜スイッチによるキッチンタイマー

タイマー

Arduino のライブラリに MsTimer2 があります。これを IDE に追加して、1 分のタイマーを作成してみましょう。まずは簡単に Arduino Uno 単体で特に配線なしで試してみます。それが以下の 'timer.ino' です。

#include <avr/sleep.h>
#include <MsTimer2.h>

bool led_state(false);
int callback_timer_count(0), callback_timer_limit(60)/*[sec]*/;
double callback_timer_interval(.5)/*[sec]*/;
void callback_timer()
{
  if (++callback_timer_count == callback_timer_limit/callback_timer_interval) {
    digitalWrite(LED_BUILTIN, (led_state = false));
    callback_timer_count = 0;
    MsTimer2::stop();
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  }
  digitalWrite(LED_BUILTIN, led_state ? HIGH : LOW);
  led_state = !led_state;
}
void setup()
{
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  MsTimer2::set(1000*callback_timer_interval, callback_timer);
  MsTimer2::start();
}
void loop() {}

電源入りかリセットで、LED の点滅を 0.5+0.5 秒毎に繰り返しますが、それが 1 分続いたら点滅を終了して、スリープするというスケッチになっています。

傾斜スイッチ(傾きセンサ)

tiltsw_breadboard.svg tiltsw_circuit.svg

傾斜スイッチは傾きセンサとも呼ばれますが、振動が伝わりスイッチが開閉するというものです。筆者のは ROLL BALL SWITCH (RBS040200-G) というものです。傾斜スイッチの開閉の状態を LED のオンオフに対応させてみます。これが以下の 'tiltsw.ino' です。

#include <avr/sleep.h>

const int tiltsw_pinno(3);

void callback_change()
{
  bool tiltsw_state(digitalRead(tiltsw_pinno));
  digitalWrite(LED_BUILTIN, tiltsw_state ? HIGH : LOW);
}
void setup()
{
  attachInterrupt(digitalPinToInterrupt(tiltsw_pinno), callback_change, CHANGE);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
}
void loop()
{
  sleep_mode();
}

傾斜スイッチの開閉の状態に変化があったら、割り込みによって 'callback_change' 関数が呼ばれます。そこで傾斜スイッチの状態を 'digitalRead(pinno)' で取得して LED のオンオフを設定しています。

キッチンタイマー

kitchen_timer_breadboard.svg kitchen_timer_circuit.svg

さて、これまでの 'tone'、タイマー、傾斜スイッチを応用して、砂時計のように装置を手に取って置いたら、LED の点滅を繰り返しつつ、3分たったらメロディーが鳴る、そしてスリープする、といったものを作成してみましょう。それが以下の 'kitchen_timer.ino' です。

/*
  kitchen_timer

  Written by Taiji Yamada <taiji@aihara.co.jp>
*/
#include <avr/sleep.h>
#include <MsTimer2.h>

static inline
double range_map_logarithm(const double src_value,
                           const double src_lb, const double src_ub,
                           const double dst_lb, const double dst_ub)
{
  const double src_sz = src_ub - src_lb, dst_sz = log(dst_ub) - log(dst_lb);
  return dst_lb*exp(dst_sz*(src_value - src_lb)/src_sz);
}
static inline
double twelve_equal_temperament(const double c_freq_end, const int n_octaves, const int octave, const int i)
{
  static const int n_equal_temperament(12);
  return range_map_logarithm(octave*n_equal_temperament + i,
                             0, n_octaves*n_equal_temperament,
                             c_freq_end/(1ULL<<n_octaves), c_freq_end);
}

#define R	-1
#define C	0
#define CS	1
#define D	2
#define DS	3
#define E	4
#define F	5
#define FS	6
#define G	7
#define GS	8
#define A	9
#define AS	10
#define B	11

#define L32	1./32
#define L16	1./16
#define L16d	L16+L32
#define L8	1./8
#define L8d	L8+L16
#define L4	1./4
#define L4d	L4+L8
#define L2	1./2
#define L2d	L2+L4
#define L1	1./1

struct music_note {
  int octave, i;
  double beet;
} one_octave[] = {
#define O 0
  { O, C, L4 }, { O, D, L4 }, { O, E, L4 }, { O, F, L4 }, { O, G, L4 }, { O, A, L4 }, { O, B, L4 }, { O+1, C, L4 }, { O+1, R, L4 },
}, *octaves, daisy_bell[] = {
  { O+1, A, L2d }, { O+1, E, L2d }, { O+1, C, L2d }, { O, G, L2d }, { O, A, L4 }, { O, B, L4 }, { O+1, C, L4 }, { O, A, L2 }, { O+1, C, L4 }, { O, G, L2d+L2d },
  { O+1, D, L2d }, { O+1, F, L2d }, { O+1, E, L2d }, { O+1, C, L2d }, { O, A, L4 }, { O, B, L4 }, { O+1, C, L4 }, { O+1, D, L2 }, { O+1, E, L4 }, { O+1, D, L2d+L2 }, { O+1, E, L4 },
  { O+1, F, L4 }, { O+1, E, L4 }, { O+1, D, L4 }, { O+1, G, L2 }, { O+1, E, L4 }, { O+1, D, L4 }, { O+1, C, L2+L2 }, { O+1, D, L4 }, { O+1, E, L2 }, { O+1, C, L4 }, { O, A, L2 }, { O+1, C, L4 }, { O, A, L4 }, { O, G, L2+L2 }, { O, G, L4 },
  { O+1, C, L2 }, { O+1, E, L4 }, { O+1, D, L2 }, { O, G, L4 }, { O+1, C, L2 }, { O+1, E, L4 }, { O+1, D, L4 }, { O+1, E, L4 }, { O+1, F, L4 }, { O+1, G, L4 }, { O+1, E, L4 }, { O+1, C, L4 }, { O+1, D, L2 }, { O, G, L4 }, { O+1, C, L2d+L2d },
#undef O
};

#undef L32
#undef L16
#undef L16d
#undef L8
#undef L8d
#undef L4
#undef L4d
#undef L2
#undef L2d

#undef C
#undef CS
#undef D
#undef DS
#undef E
#undef F
#undef FS
#undef G
#undef GS
#undef A
#undef AS
#undef B

size_t n_one_octave(sizeof(one_octave)/sizeof(one_octave[0])), n_octaves, n_daisy_bell(sizeof(daisy_bell)/sizeof(daisy_bell[0]));

class piezo_buzzer {
public:
  int pinno, c_freq_end, n_octaves, modulation;
  piezo_buzzer(const int pn = 9, const int safe_c_freq = 4186/4, const int safe_n_octaves = 3, const int key = 0) : pinno(pn), c_freq_end(safe_c_freq), n_octaves(safe_n_octaves), modulation(key)
  {
    pinMode(pinno, OUTPUT);
  }
  void play(const int n_note, const struct music_note note[], const double tempo)
  {
    for (int i=0; i<n_note; ++i) {
      if (note[i].i < 0)
        delay(note[i].beet*60000/tempo);
      else {
        int duration(note[i].beet*60000/tempo);
        int freq_end(round(twelve_equal_temperament(c_freq_end, n_octaves, n_octaves, modulation)));
        int freq(round(twelve_equal_temperament(freq_end, n_octaves, note[i].octave, note[i].i)));
        tone(pinno, freq, duration);
        delay(duration);
      }
      delay(1);
    }
  }
} piezo_buzzer;

const double tempo(200./4);

volatile bool led_state(false), start_play(false);
volatile int callback_timer_count(0);
const int callback_timer_limit(60*3)/*[sec]*/;
const double callback_timer_interval(.5)/*[sec]*/;
void callback_timer()
{
  if (++callback_timer_count == callback_timer_limit/callback_timer_interval) {
    digitalWrite(LED_BUILTIN, (led_state = false));
    callback_timer_count = 0;
    MsTimer2::stop();
    start_play = true;
  }
  digitalWrite(LED_BUILTIN, led_state ? HIGH : LOW);
  led_state = !led_state;
}

const int tiltsw_pinno(3);
void callback_change()
{
  bool tiltsw_state(digitalRead(tiltsw_pinno));
  if (tiltsw_state) {
    MsTimer2::stop();
    callback_timer_count = 0;
    MsTimer2::set(1000*callback_timer_interval, callback_timer);
    MsTimer2::start();
  }
}

void setup()
{
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  attachInterrupt(digitalPinToInterrupt(tiltsw_pinno), callback_change, CHANGE);
}
void loop()
{
  if (start_play) {
    start_play = false;
    detachInterrupt(digitalPinToInterrupt(tiltsw_pinno));
    piezo_buzzer.play(n_daisy_bell, daisy_bell, tempo);
    attachInterrupt(digitalPinToInterrupt(tiltsw_pinno), callback_change, CHANGE);
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  }
  delay(1000*.1);
}

ここで悩ましいのは、'callback_timer()' 関数の中で 'piezo_buzzer.play()' をしたいのですが、これは中で 'delay' を使っているのでそれが出来ません。仕方がないのでメインループ 'loop()' 内で 'piezo_buzzer.play()' をしています。

照度センサ

照度センサとして「CdS(硫化カドミウム)セル」という素子を使います。

手元の CdS セルは正確な素性がわからず、実は壊れた100均の LED ナイトライトから素子を取ってきたものです。見ためからして CdS セル(MI527)だと推測されます。

CdS セルは照度が強くなると抵抗値が下がるという特性がありますので、CdS セルの抵抗と 1kΩ の抵抗器で 5V を分圧して 1kΩ の電圧を 'analogRead(pinno)' で計ることにしましょう。

light_sensor_breadboard.svg light_sensor_circuit.svg

さて、データシートの特性からすると、だいたい、 照度(\(E\textrm{[lx]}\))と CdS セルの抵抗値(\(R_\textrm{photo}\textrm{[kΩ]}\))の関係は以下のように表せそうです。

\(E\textrm{[lx]} = (R_\textrm{photo}\textrm{[kΩ]}/1000)^{-0.7}\)

CdS セルの抵抗値(\(R_\textrm{photo}\textrm{[kΩ]}\))、1kΩ の抵抗器(\(R\textrm{[kΩ]}\)) の電圧(\(E_R\textrm{[mV]}\))、電源電圧 5000\(\textrm{[mV]}\) の関係から、

\(\dfrac{E_R\textrm{[mV]}}{5000\textrm{[mV]}} = \dfrac{R\textrm{[kΩ]}}{R\textrm{[kΩ]} + R_\textrm{photo}\textrm{[kΩ]}}\)

\(R_\textrm{photo}\textrm{[kΩ]} = R\textrm{[kΩ]} \times (5000\textrm{[mV]} / E_R\textrm{[mV]} - 1)\)

ですので、以下のように照度(\(E\textrm{[lx]}\))が電圧(\(E_R\textrm{[mV]}\))から求まります。

\(E\textrm{[lx]} = ((R\textrm{[kΩ]} \times (5000\textrm{[mV]} / E_R\textrm{[mV]} - 1))/1000)^{-0.7}\)

では、以上を用いて簡単な照度センサを実現するのが以下のスケッチ 'light_sensor.ino' です。

/*
  light_sensor

  Written by Taiji Yamada <taiji@aihara.co.jp>
*/
class photocell {
public:
  unsigned long voltage/*[mV]*/;
  double photo_resistance/*[kOhm]*/;
  photocell(int pn = 3) : resistance(1), pinno(pn) {}
  unsigned long measure()
  {
    return (voltage=map(analogRead(pinno), 0, 1023, 0, 5000));
  }
  double get_illuminance()
  {
    return pow((photo_resistance=resistance*(5000./voltage - 1))/1000, -.7);
  }
private:
  const int pinno;
  const double resistance/*[kOhm]*/;
};

photocell light_sensor;
void setup()
{
  Serial.begin(9600);
}
void loop()
{
  light_sensor.measure();
  Serial.print(light_sensor.voltage); Serial.print("\t");
  Serial.print(light_sensor.photo_resistance); Serial.print("\t");
  Serial.print(light_sensor.get_illuminance()); Serial.println("\t");
  delay(1000*.1);
}

クラス 'photocell' のインスタンス 'light_sensor''measure()' メッセージを送ると測定結果を返したり保持したりします。その後、'get_illuminance()' とすると照度が得られます。 それを 0.1 秒毎に照度を印字するスケッチになっています。

距離センサによる「デジタルテルミン」

超音波距離センサ

超音波距離センサは色々ありますが、ここでは Grove システムの非接触距離センサモジュールを用います。白のケーブルは「NC」と刻印された端子に繋がっており「未接続」ですので使いません。そこで配布されている謹製のライブラリをそのまま用いてもよいのですが、ここでは理解を深める為にライブラリ内で実現されているほぼ同等の操作を再現することにします。

ultrasonic_range_sensor_breadboard.svg ultrasonic_range_sensor_circuit.svg

まずは簡単に、距離センサが計る、超音波スピーカーからの音の反射が超音波マイクに到達する時間、そして、音速とその時間から距離を求めるクラス 'ultrasonic_range_sensor' を作成し、そのクラスのインスタンス 'range_sensor' の測定値を繰り返し出力することにします。それが以下の 'ultrasonic_range_sensor.ino' です。

class ultrasonic_range_sensor {
public:
  ultrasonic_range_sensor(const int pin = 4/*2...13*/) : pinno(pin) {}
  long duration/*[usec]*/;
  double temperature;
  unsigned long measure(void)
  {
    pinMode(pinno, OUTPUT);
    digitalWrite(pinno, LOW);
    delayMicroseconds(2); 
    digitalWrite(pinno, HIGH);
    delayMicroseconds(5);
    digitalWrite(pinno, LOW);
    pinMode(pinno, INPUT);
    duration = pulseIn(pinno, HIGH);
    return duration;
  }
  double get_range(double temperature = 17/*[degrees Celsius]*/)/*[cm]*/
  {
    return duration*(331.5+0.61*(this->temperature=temperature))/10000/2;
  }
private:
  int pinno;
};

ultrasonic_range_sensor range_sensor;
const double temperature(20)/*[degrees Celsius]*/;
const double interval(.1)/*[sec]*/;

void setup()
{
  Serial.begin(9600);
}
void loop()
{
  if (!range_sensor.measure())
    Serial.println("timeout");
  else {
    Serial.print(range_sensor.duration);
    Serial.print("\t");
    Serial.println(range_sensor.get_range(temperature));
  }
  delay(1000*interval);
}

ここでの要は 'pulseIn(pinno, HIGH)' です。これは入力 'HIGH' から 'LOW' までの時間[usec]を返します。それに先立ち、'digitalWrite(pinno, value)' を極めて短い間隔で 'LOW', 'HIGH', 'LOW' しています。つまり、センサが超音波を発した時刻から、反射した超音波が消音するまでの時間を計っているようです。

音速\(c\textrm{[m/sec]}\)と温度\(T\textrm{[℃]}\)の関係の近似式は以下の通りです。

\(c=331.5 + 0.61T\)

距離\(l\textrm{[m]}\)と音速\(c\textrm{[m/sec]}\)、音が反射して戻ってくるまでの到達時間\(t\textrm{[sec]}\)の関係は \(2l = ct\) ですので、よって、

\(l\textrm{[m]} = c\textrm{[m/sec]} t\textrm{[sec]}/2\), \(l\textrm{[cm]}\times 10^{-2}\textrm{[m/cm]} = c\textrm{[m/sec]} t\textrm{[usec]} \times 10^{-6}\textrm{[sec/usec]}/2\), \(l\textrm{[cm]} = c\textrm{[m/sec]} t\textrm{[usec]} \times 10^{-4}\textrm{[c/u]}/2\),

\(l\textrm{[cm]} = (331.5 + 0.61T)\textrm{[m/sec]} t\textrm{[usec]} \times 10^{-4}\textrm{[c/u]}/2\).

となります。

さて、温度が必要なら、温度センサと併用すればベターじゃないか!というのはいいアイデアです。しかし、実際にやってみると温度センサの値はかなりふらつきますので、結果、求まる距離がふらついてしまうことになります。温度の測定値の平均をとる、ノイズや回路構成など工夫が必要なアイデアですので、ここでは省略します。

距離センサと圧電ブザーによるデジタルテルミン

さて、テルミンとは手を中に浮かせてその位置で二つの周波数の「うなり」による音高を変化させて演奏する電子楽器です。音高が「無段階」なのもテルミンの特徴です。安価なものでは、学研の「大人の科学マガジン Vol.17」 が有名で、基本的な回路図も配布されています。

ここでは、テルミン風の楽器を距離センサと圧電ブザーで実現します。

switch_buzzer_range_sensor_breadboard.svg switch_buzzer_range_sensor_circuit.svg

それが以下の 'switch_buzzer_range_sensor.ino' です。

/*
  switch_buzzer_range_sensor

  Written by Taiji Yamada <taiji@aihara.co.jp>
*/
#include <avr/sleep.h> // set_sleep_mode(SLEEP_MODE_{IDEL,ADC,PWR_SAVE,PWR_DOWN,STANDBY,EXT_STANDBY})
//#define SERIAL_DEBUG

class ultrasonic_range_sensor {
public:
  ultrasonic_range_sensor(const int pin = 4/*2...13*/) : pinno(pin) {}
  long duration/*[usec]*/;
  double temperature;
  unsigned long measure(void)
  {
    pinMode(pinno, OUTPUT);
    digitalWrite(pinno, LOW);
    delayMicroseconds(2); 
    digitalWrite(pinno, HIGH);
    delayMicroseconds(5);
    digitalWrite(pinno, LOW);
    pinMode(pinno, INPUT);
    duration = pulseIn(pinno, HIGH);
    return duration;
  }
  double get_range(double temperature = 17/*[degrees Celsius]*/)/*[cm]*/
  {
    return duration*(331.5+0.61*(this->temperature=temperature))/10000/2;
  }
private:
  int pinno;
};

static inline
double range_map(const double src_value,
                 const double src_lb, const double src_ub,
                 const double dst_lb, const double dst_ub)
{
  const double src_sz = src_ub - src_lb, dst_sz = dst_ub - dst_lb;
  return dst_lb + dst_sz*((src_value - src_lb)/src_sz);
}

static inline
double range_map_logarithm(const double src_value,
                           const double src_lb, const double src_ub,
                           const double dst_lb, const double dst_ub)
{
  const double src_sz = src_ub - src_lb, dst_sz = log(dst_ub) - log(dst_lb);
  return dst_lb*exp(dst_sz*(src_value - src_lb)/src_sz);
}

static inline
double logarithm_range_map(const double src_value,
                           const double src_lb, const double src_ub,
                           const double dst_lb, const double dst_ub)
{
  return range_map(log(src_value), log(src_lb), log(src_ub), dst_lb, dst_ub);
}

class thermometer {
public:
  thermometer(const int pin = 5) : pinno(pin) {}
  double get_temperature()/*[degrees Celsius]*/ const
  {
#if 0
    return map(map(analogRead(pinno),
                   0, 1023, 0, 5000),
               300, 1600, -30, 100);
#else
    return range_map(range_map(analogRead(pinno),
			       0, 1023, 0, 5000),
		     300, 1600, -30, 100);
#endif
  }
  double get_temperature_average(const int n_trial = 379)/*[degrees Celsius]*/ const
  {
    double rv(0);
    for (int i=0; i<n_trial; ++i)
      rv = (i*rv + get_temperature())/(i+1);
    return rv;
  }
private:
  int pinno;
};

class ultrasonic_range_sensor_with_thermometer : public ultrasonic_range_sensor {
public:
  double temperature;
  ultrasonic_range_sensor_with_thermometer(const int pin = 4/*2...13*/, const int thermometer_pin = 5) : ultrasonic_range_sensor(pin), temperature_sensor(thermometer_pin) {}
  double get_range() { return ultrasonic_range_sensor::get_range((temperature=temperature_sensor.get_temperature_average(379/4))); }
private:
  thermometer temperature_sensor;
};

ultrasonic_range_sensor range_sensor;
//ultrasonic_range_sensor_with_thermometer range_sensor;
const double interval(.01);
const int piezo_pinno(9), led_pinno(11);
volatile int state(1);

void callback_falling()
{
  state = (state + 1) % 3;
}
void setup()
{
#if defined(SERIAL_DEBUG)
  Serial.begin(9600);
#endif
  attachInterrupt(digitalPinToInterrupt(2), callback_falling, FALLING);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  pinMode(led_pinno, OUTPUT);
  if (!state) sleep_mode();
}
void loop()
{
  const double c_freq_end(4186/2);
  const int n_equal_temperament(12), n_octaves(12-1);
  if (state) {
    double range, i, freq;
    if (range_sensor.measure() && (range=range_sensor.get_range()) < 200) {
      i = range_map((range=range_sensor.get_range()), 3/*[cm]*/, 100/*[cm]*/, n_octaves*n_equal_temperament, 0);
      if (state > 1) i = round(i);
      freq =
	range_map_logarithm(i, 0, n_octaves*n_equal_temperament,
			    c_freq_end/(1ULL<<n_octaves), c_freq_end);
#if defined(SERIAL_DEBUG)
      Serial.print(range); Serial.print("\t"); Serial.print(i); Serial.print("\t"); Serial.println(freq);
#endif
      digitalWrite(led_pinno, HIGH);
      tone(piezo_pinno, freq);
    }
    else {
      digitalWrite(led_pinno, LOW);
      noTone(piezo_pinno);
    }
    delay(1000*interval);
  }
  else {
    digitalWrite(led_pinno, LOW);
    noTone(piezo_pinno);
    sleep_mode();
  }
}

やっていることは今までの技術の組み合わせです。

タクトスイッチを一度押すと、テルミンの特徴である無段階の音高がオフになり、12平均葎の音程での演奏になります。もう一度タクトスイッチを押すとスリープです。

簡易ライブラリの作成と導入

Arduino へのライブラリの導入は IDE のツールメニューから行うことになっていますが、そのガイドに書いてある通り、手動で行うこともできます。ここでは、'~/Documents/Arduino/libraries/misc/' に以下の 'range_map.h' をコピーして使います。

/*
  range_map.h

  Copyright (C) 2015 Taiji Yamada <taiji@aihara.co.jp>
*/
#ifndef _RANGE_MAP_H_
#define _RANGE_MAP_H_

#if __STDC_VERSION__ >= 199901L || defined(__cplusplus)

/*
  C99 inline version of range_map, range_map_logarithm and logarithm_range_map
 */
static inline
double range_map(const double src_value,
                 const double src_lb, const double src_ub,
                 const double dst_lb, const double dst_ub)
{
  const double src_sz = src_ub - src_lb, dst_sz = dst_ub - dst_lb;
  return dst_lb + dst_sz*((src_value - src_lb)/src_sz);
}

static inline
double range_map_logarithm(const double src_value,
                           const double src_lb, const double src_ub,
                           const double dst_lb, const double dst_ub)
{
  const double src_sz = src_ub - src_lb, dst_sz = log(dst_ub) - log(dst_lb);
  return dst_lb*exp(dst_sz*(src_value - src_lb)/src_sz);
}

static inline
double logarithm_range_map(const double src_value,
                           const double src_lb, const double src_ub,
                           const double dst_lb, const double dst_ub)
{
  return range_map(log(src_value), log(src_lb), log(src_ub), dst_lb, dst_ub);
}

#else

/*
  C89 macro version of range_map, range_map_logarithm and logarithm_range_map
 */

#define range_map(src_value, src_lb, src_ub, dst_lb, dst_ub) \
  (dst_lb + (dst_ub - dst_lb)*((src_value - src_lb)/(src_ub - src_lb)))

#define range_map_logarithm(src_value, src_lb, src_ub, dst_lb, dst_ub) \
  (dst_lb*exp((log(dst_ub) - log(dst_lb))*(src_value - src_lb)/(src_ub - src_lb)))

#define logarithm_range_map(src_value, src_lb, src_ub, dst_lb, dst_ub) \
  range_map(log(src_value), log(src_lb), log(src_ub), dst_lb, dst_ub)

#endif /* __STDC_VERSION__ < 199901L */

#endif

これは Arduino 独自の 'map' を倍精度浮動小数点で使えるようにしたもの('range_map')です。さらにこれまでも幾度か登場しているように、対数スケール変換('range_map_logarithm')と逆変換('logarithm_range_map')もあります。さらに、さまざまなバージョンの C/C++ で使えるように工夫してあります。

すると次の例のように、この簡易ライブラリが使えるようになります。

分割型AC電流センサ

分割型AC電流センサを使ってみます。これはイヤフォンジャックに接続するタイプになっています。

split_core_current_transformer_breadboard.svg split_core_current_transformer_circuit.svg

これをデータシートに基づいて、測定電圧から電流値へ変換して、それを0.1秒毎にシリアル通信端末に印字するスケッチが以下の 'split_core_current_transformer.ino' です。

#include <math.h>
#include <range_map.h>

class split_core_current_transformer {
public:
  split_core_current_transformer(const int pn = 4) : pinno(pn)
  {
    analogReference(INTERNAL);
  }
  static
  double voltage_to_current(const double voltage) { return range_map(voltage, 0, 300/*[mV]*/, 0, 60/*[A]*/); }
  static
  double current_to_voltage(const double current) { return range_map(current, 0, 60/*[A]*/, 0, 300/*[mV]*/); }
  double get_voltage() const
  {
    return range_map(analogRead(pinno), 0, 1023, 0, 1100/*[mV]*/);
  }
  double get_voltage_average(const unsigned long duration = 100000/*[usec]*/) const
  {
    unsigned long t(micros());
    double rv(0), v;
    for (int i=0; micros() - t < duration; ++i)
      rv = (i*rv + (v=get_voltage())*v)/(i+1);
    return sqrt(rv*2);// i\approx 500...600
  }
  double get_current() const
  {
    return voltage_to_current(get_voltage());
  }
  double get_current_average(const unsigned long duration = 100000/*[usec]*/) const
  {
    return voltage_to_current(get_voltage_average(duration));
  }
private:
  int pinno;
};

split_core_current_transformer current_transformer;
const double interval(.1)/*[sec]*/;

void setup()
{
  Serial.begin(9600);
}
void loop()
{
  double voltage(current_transformer.get_voltage_average());
  Serial.print(voltage); Serial.print("\t");
  Serial.print(split_core_current_transformer::voltage_to_current(voltage)); Serial.print("\n");
  delay(1000*interval);
}

極めて低い電圧なので、'analogReference(INTERNAL)' で基準電圧を Arduino Uno の内部電圧 1.1[V] にします。これをしなければ基準電圧は 5[V] です。繰り返し測定してその自乗平均の平方根(実効値)を取りますが、そのとき交流の正負の片側しか得られませんので、自乗平均した値を倍にしています。

商品説明に書いてある通り、1[A](×100[V]=100[W]) 未満は測定が困難なので、例えば 60[W] 電球などは正確には計れません。また、電灯線の片側をクランプするのだと少なめに測定されている気もします。

動作確認のための「10Lチカ」「LED バーサライト(POV: Persistent Of Vision)」

電子回路をいろいろ試していると、うっかり配線を間違えて「何やら異臭が」とか、素子が「バチン」といって焦げるなんてことは、一度もないにこしたことはないのですが、あります。すると、やはり一番高価な Arduino Uno が壊れた心配におそわれるものです。

そこでてっとり早く多くのデジタルピンを LED で動作確認する回路を用意しておきましょう。

ten_leds_blinker_breadboard.svg ten_leds_blinker_circuit.svg

ただ単に「Lチカ」(LED を点灯させること)させるだけではつまらないので、「LED バーサライト(POV: Persistent Of Vision)」として動作するようにしましょう。以下がその 'ten_leds_blinker' です。基本的には武蔵野電波のブレッドボーダーズの記事と同じです。

/*
  ten_leds_blinker

  Written by Taiji Yamada <taiji@aihara.co.jp>
*/
#include <avr/sleep.h> // set_sleep_mode(SLEEP_MODE_{IDEL,ADC,PWR_SAVE,PWR_DOWN,STANDBY,EXT_STANDBY})

#define new_year_100x10_width 100
#define new_year_100x10_height 10
static unsigned char new_year_100x10_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x48, 0x70, 0x57, 0x00, 0x00, 0x50, 0x00, 0x00, 0x80, 0x03,
   0xc0, 0x01, 0x00, 0x48, 0x56, 0x55, 0x00, 0x07, 0x50, 0x67, 0x85, 0x29,
   0xc2, 0x14, 0x01, 0x00, 0x78, 0x77, 0x67, 0x08, 0x15, 0x61, 0x75, 0x43,
   0x1a, 0x21, 0x8d, 0x50, 0x01, 0x48, 0x15, 0x21, 0x78, 0x57, 0x21, 0x57,
   0x41, 0x8a, 0x20, 0x45, 0x00, 0x00, 0x08, 0x17, 0x21, 0x48, 0xf1, 0x21,
   0x71, 0x81, 0x89, 0xc3, 0xc4, 0x01, 0x00, 0x08, 0x10, 0x21, 0x48, 0x03,
   0x20, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x21, 0x00,
   0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

#define I_love_Japan_100x10_width 100
#define I_love_Japan_100x10_height 10
static unsigned char I_love_Japan_100x10_bits[] = {
   0x70, 0x10, 0xf0, 0x11, 0xe4, 0x07, 0x3c, 0x0e, 0x1f, 0x00, 0x00, 0xa0,
   0x00, 0x20, 0x10, 0x18, 0x13, 0x24, 0x00, 0x10, 0x11, 0x21, 0x00, 0x00,
   0xa0, 0x00, 0x20, 0x10, 0x08, 0x12, 0x24, 0x00, 0x90, 0x20, 0x21, 0x00,
   0x00, 0xa0, 0x00, 0x20, 0x10, 0x08, 0x12, 0x24, 0x00, 0x90, 0x20, 0x21,
   0x00, 0x00, 0xa0, 0x00, 0x20, 0x10, 0x08, 0x12, 0xe4, 0x03, 0x90, 0x20,
   0x21, 0x4f, 0x07, 0xa0, 0x00, 0x20, 0x10, 0x08, 0x12, 0x24, 0x00, 0x90,
   0x20, 0x9f, 0xd0, 0xe8, 0xaf, 0x00, 0x20, 0x10, 0x08, 0x12, 0x24, 0x00,
   0x90, 0x3f, 0x81, 0x50, 0x08, 0xa0, 0x00, 0x20, 0x10, 0x08, 0x32, 0x26,
   0x00, 0x90, 0x20, 0x81, 0x50, 0x08, 0xa0, 0x00, 0x20, 0x10, 0x18, 0x63,
   0x23, 0x80, 0x90, 0x20, 0x81, 0x50, 0x08, 0x00, 0x00, 0x70, 0xf0, 0xf3,
   0xc1, 0xe1, 0x07, 0x8f, 0x20, 0x01, 0x5f, 0x08, 0xa0, 0x00};

#define Merry_Christmas_100x10_width 100
#define Merry_Christmas_100x10_height 10
static unsigned char Merry_Christmas_100x10_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x01, 0x00, 0x80, 0x00, 0x00,
   0x00, 0x00, 0xb0, 0xf1, 0x34, 0x4d, 0x04, 0x41, 0x81, 0x26, 0xdf, 0x63,
   0xf3, 0xf1, 0x01, 0x48, 0x0a, 0x4d, 0x53, 0x84, 0x00, 0x9f, 0x89, 0x80,
   0x90, 0x04, 0x0a, 0x00, 0x48, 0x0a, 0x05, 0x41, 0x84, 0x00, 0xa1, 0xa0,
   0x80, 0x90, 0x04, 0x0a, 0x00, 0x48, 0xfa, 0x05, 0x81, 0x82, 0x00, 0xa1,
   0xa0, 0x9f, 0x90, 0xf4, 0xfb, 0x01, 0x48, 0x0a, 0x04, 0x81, 0x81, 0x00,
   0xa1, 0x20, 0x90, 0x90, 0x14, 0x02, 0x01, 0x48, 0x0a, 0x04, 0x01, 0x01,
   0x41, 0xa1, 0x20, 0x90, 0x90, 0x14, 0x02, 0x01, 0x48, 0xf2, 0x04, 0x81,
   0x00, 0x23, 0xa1, 0xa0, 0x0f, 0x97, 0xe4, 0xfb, 0x00, 0x00, 0x00, 0x00,
   0x40, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

class xbitmap {
public:
  int w, h;
  unsigned char *bits;
  bool get_coordinate(int x, int y)
  {
    return bits[((w + w%8)>>3)*y + (x>>3)] & (1U << (x%8));
  }
} xbitmap_set[] = {
  { new_year_100x10_width,		new_year_100x10_height,			new_year_100x10_bits },
  { I_love_Japan_100x10_width,		I_love_Japan_100x10_height,		I_love_Japan_100x10_bits },
  { Merry_Christmas_100x10_width,	Merry_Christmas_100x10_height,		Merry_Christmas_100x10_bits },
};

const int leds_pinno[] = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, };
const double interval(.05)/*[sec]*/;
volatile int led_status(0);
int x = 0;

void callback_falling()
{
  led_status = (led_status + 1) % 5;
}
void setup()
{
  attachInterrupt(digitalPinToInterrupt(2), callback_falling, FALLING);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  for (int j=0; j<sizeof(leds_pinno)/sizeof(leds_pinno[0]); ++j)
    pinMode(leds_pinno[j], OUTPUT);
}
void loop()
{
  const int i(x), k(led_status - 1);
  int j;
  if (led_status) {
    for (j=0; j<min(xbitmap_set[k].h, sizeof(leds_pinno)/sizeof(leds_pinno[0])); ++j)
      digitalWrite(leds_pinno[j], xbitmap_set[k].get_coordinate(i, j) ? HIGH : LOW);
    x = (x + 1) % xbitmap_set[k].w;
  }
  else {
    for (j=0; j<min(xbitmap_set[k].h, sizeof(leds_pinno)/sizeof(leds_pinno[0])); ++j)
      digitalWrite(leds_pinno[j], LOW);
    sleep_mode();
  }
  delay(1000*interval);
}

ビットマップデータは X Bitmap 形式の保存が出来るペイントソフトがあれば簡単に用意できます。この X Bitmap 形式で得られる '*.xbm' というファイルの中身は、実はテキスト形式で、プログラミング言語 C/C++ に組み込み可能なものなのです。

タクトスイッチを押すとスリープから図柄が変わり、一周してスリープになります。

その他のテーマ

Written by Taiji Yamada <taiji@aihara.co.jp>