Arduinoを使ったメモリキーヤ プログラムの開発記録
アマチュア無線で使う、CWメモリキーヤを作ってみた。
Arduino Uno R3または Pro miniを使ったCWメモリ・キーヤ・プログラム開発の経緯を記録する。
このメモリ・キーヤでは、送出用メッセージ文を、予めプログラム内のデータとして書き込み、Arduinoに
焼きこんでおく方式。
スイッチを押せばそれを送信できるようにするもの。こうすればプログラムの開発が容易になる。メッセージを
随時パドルから入力できるようにはしない。実用上、それ程しょっちゅう送出メッセージは書き換えないし、
Arduino IDE を使えば簡単にメッセージを書き換えできるから、これでよしとする。
勿論パドルをつないでエレキー操作もできるようにする。おまけでCW受信練習機能(ランダムに
英数字出力する)もくっつけた。遊び心で、LED電飾も作ってみようかなぁ。
(追加説明)エレキー、メモリ・キーヤ、メッセージ・キーヤは昔流で言うと、
それぞれ少しずつ
違う。エレキーはdashパドルとdotパドルを押すと、単純にそれぞれdash, dotが自動で出力されるだけのもの。
メモリ・キーヤはエレキーとほぼ同じだが、ほんの少しだけdash, dotそれぞれのパドル押下を記憶(メモリ)
する機能を持ったエレキー。(この違いによる操作性は全然異なる。詳細後述)
メッセージ・キーヤはここで扱うように、予め記憶させておいたメッセージ
(電文)をボタン1つで自動送出できるもの。
しかし、マイクロプロセッサを手軽に使える今日では、メモリ・キーヤと言えば、当然ここで言う、
メッセージ・キーヤの事になる。マイクロプロセッサを使わないで、メッセージ・キーヤを作るとなると、
恐ろしく複雑な回路になるだろう。昔なら、個人が趣味片手間で作るなんて、とても無理なお話だった。
|
開発手順
- 時間稼ぎ関数を自作する。整数値を入れてコールすれば、その値に比例した時間をつぶす
関数。dot, dash, spaceなどの長さを作るために必要。Arduinoにはdelay( )関数があるが、
delay( )関数は中身が分からないし、割込みでおかしくなるらしいので、今回利用しない。
時間稼ぎ関数が核プログラムになるので、これから作り始める。
- モールス出力関数を作る。予め作ったメッセージ文のデータから、時間稼ぎ関数を使って、
dot, dash, space を出力する関数。
外付けスイッチで、文字単位で停止できるよう、最初から配慮して関数を作る。
- 外付けスイッチで外部割込みを発生させ、リアルタイムでメッセージ出力を停止・再生させる。
- 別の外付けスイッチで、出力するメッセージ文を選択できるようにする。メッセージ文は3本の中から
選択できるようにする。これは増やすのは容易に修正できるはず。
- 外付けパドルでエレキー操作もできるようにする。
- 外付けボリュームで、メッセージ送信、エレキー送信の速度を連続可変できるようにする。
- おまけでCW練習機能を付加する。遊びで、LED電飾もやってみるかなぁ・・。
ハード構成
プロッセサはAruduino UNO R3、または ProMini。
デジタル2番ピン |
再生/停止スイッチ |
デジタル3番ピン |
メッセージ選択スイッチ |
デジタル7、8、9番ピン |
メッセージ番号表示ランプ |
デジタル10番ピン |
パドルスイッチ(dash) |
デジタル11番ピン |
パドルスイッチ(dot) |
デジタル12番ピン |
信号出力(トランジスタ経由でブザーや無線機へ) |
アナログ3番ピン |
スピード調整用ボリューム(10kΩ) |
実験用ハードセット
本プログラムの核になる「時間稼ぎ関数」から作る。for文を入れ子にしてぐるぐる回して
時間稼ぎするだけのものを作ってみて、それでdot, dashをうまく出力できるかの実験をする。
それができれば、それを関数にして、実際に複数のメッセージ文をモールスで出力させる。
時間稼ぎの実験
時間稼ぎ関数はdot, dashの幅を作るためのもので、
Arduinoのdelay( )関数は使わない。
for文でぐるぐる回って時間をかせぐ文を書いたが、コンパイラがfor文の最適化をするため
当初for文が期待どおり動かなかった。
問題はfor文内のローカル変数がどうもだめだったようで、volatileを指定したグローバルを使って初めて、
期待どおりの動作になった。
|
このプログラムは、デジタル12番ピンのLED(またはブザー)をon/offさせるだけのもの。
digitalWrite(12,HIGHT)とdigitalWrite(12,LOW)の後ろにfor文を入れている。
dm1=1; 等の代入文はダミーで、for文内で何も処理しないと、
コンパイラが不要なfor文と判断してはしょってしまうようだ。
第一行目で、for文で使う変数i, j を宣言している。当初、i, jはfor文の
中で宣言していたが、うまく時間が稼げず、LEDはついたままのように見えていた。
i, j をいくら大きな値にしても、入れ子を数段重ねても、時間をかせぐ事ができないでいた。
原因を突き止めるのに時間がかかってしまったが、コンパイラが最適化していて、for文をそのままには
実行していないようだった。
そこで、for文で使うカウント用の変数 i, jを volatile で宣言してみた。
すると、ちゃんとfor文はそのまま実行されて、時間かせぎが出来るようになった。
volatileは変数 i, j に、CPU内レジスタではなく、
RAMを割り当てるように、コンパイラへ指示するものだ。(volatileとは揮発性という意味)
左図プログラム中、volatileを外すだけで、LEDは点滅しなくなる。実験をしてみると
わかる。12番ピンのLEDには電流制限抵抗を忘れずに。
|
|
for文を使った時間かせぎ関数 time1( )
以上の実験を経て、time1( )を作った。
time1( )の引数に 65535までの数値を入れて呼ぶと、引数値に比例した時間が稼げる。
(m秒単位ではない、数値を変えて実際に自分でスピードをみて値を決める方法)
12番ピンにLEDをつないで、実際に time1( )に数値を入れて、点滅をみる。
time1( )関数内の入れ子のfor文にある 0xff という値と引数 tx1の積が、稼ぐ時間に比例する。
時間単位を細かくして操作したい時は 0xffの値を小さくして、kn値を大きくすればいいし、
長い時間スケール範囲で操作する場合 0xffを 0xffffまでの範囲で大きくすればいい。
|
メッセージを出力するための関数を作っていくが、出力するメッセージ文は予めプログラム
に入れておく方式にする(外部のパドルで入力、記憶させる方式ではない)。
そのメッセージ・データの形式を3次元配列で作って、それを
読みこみ、モールスにして出力する関数を作る。
|
メッセージのデータ構造
メッセージはコンテスト用や、普段のCQ用に、複数のメッセージを記録しておき、つど選択して
送れるようにする。
そのため、データは3次元の配列とする。
1つめの[ ]はメッセージ番号、2つめの[ ]は文字番号、
3つめの[ ]は1文字の構成要素。
メッセージ番号は、メッセージの種類を指定するのに使う。1つ目はCQ、2つ目はTEST
など。文字番号は1つ目の文字、2つ目の文字・・というように、送出関数でカウントアップ
させる時に使う数字。
文字の構成要素とは 1=dot、4=dash、0=なし になる。a=1,4,0,0 b=4,1,1,1
となる。また0,1とくればspace、0,0とくればメッセージの行末とする。
左図の配列の1行目は、メッセージ番号が1、1つめの文字が、a という事。
配列の宣言(定義)では、配列の個数が[ ]内に入る。しかし[ ]内に入る番号は
0から始まるので、注意が必要。(念の為に記す)
|
|
モールスを出力する関数 mout( )
引数c1でもらうのは、メッセージ番号。そのメッセージ番号で上述の配列データを読み込み、
順次、出力していく。
digitalWrite(12,HIGH) で出力をonしてから digitalWrite(12,LOW)でoffにするまでの時間を time1( )で
かせぐ。そのかせぐ時間の長さは、配列データ値をkn倍する。
time1( md [c1] [j] [1] *kn );
配列データ値が1ならdot分の時間、4ならその4倍の時間つまりdash分の時間だけ
出力をonにする。
変数knは基本の長さで、グローバル変数でその数値を定義する。
つまり kn値を変える事で出力スピードを可変できる。
最初のif文:配列データ値が {0,0,...}と来たら、メッセージの終了と判断し、関数終了。
次のif文:配列データ値が0でなければ、文字なので通常出力とする。
配列データ値が0なら出力操作はパスされる。例えばNなら {4,1,0,0,0}になり、4,1 以降の
0は全部パスされる。そして最後は文字間の時間として time1(3*kn)分を取っている。
下の方のelseでは文字データが{0,1,...}(0の次が1でなくてもいいが)になるので、
これはスペースとして判断しLOWを dot*5の長さだけ出力する。
ここまでのプログラム。
上記のメッセージ・データを繰り返し、出力するだけもの。knの値を変えると
出力スピードが変わる。
|
ここで、メッセージをモールスで出力する部分は完成。次にスイッチを押すと出力の停止/再生ができる
機能を、割込み制御を利用して作る。
なるべくボタンを押したらすぐに停止/再生する感じにしたい。
デジタル2番ピンを20kΩ程度でプルアップして、外付け SWで GNDに落とす。
それを外部割り込みで拾って制御する。2、3番ピンは外部割込みに使用できるピン。
2番ピンは「割込み番号=0」に相当。3番ピンなら「割込み番号=1」
Arduinoの外部割込みを受付けるようにする関数 attachInterruput( )を使う。
attachInterrupt ( 0, sw1, FALLING )
最初の引数0が「割込み番号=0」に相当。sw1は割込みがかかって
飛んでいく先の関数名(void関数のみ)、FALLINGは、プルアップされた電圧が落ちるとき(fall)に、
割り込みがかかる設定。
割込みがかかった時に、呼び出す関数を、attachInterrupt( )の引数に設定しておいてコールする
「コールバック」という機能だが、そんなの気にせず使う。
|
割込み処理関数 sw1( )
attachInterrupt(0, sw1, FALLING); を実行すると以降、2番ピンが LOWになった瞬間に、プログラム
中のどこにいても、sw1( )関数が呼ばれ、その中の割り込み処理が実行される。最初はsetup( )内で実行しておく。
sw1( )関数の1行目で detachInterrupt(0); を実行して、割込み処理中に更に割込みがかからないよう
にしておいて、必要処理をし終わったらまた attachInterrupt( )で割込み可能に戻しておく。
割込みがかかり、sw1( )関数が呼ばれたら、その中で
グローバル変数 volatile unsigned char sf1 を on/offする。
sf1 = !sf1; は実行されるたびに、sf1の内容が反転する。
|
|
モールス出力関数 mout( )の修正
mout( )関数で、出力を実行中に、スイッチが押され割込みがかかったら、出力を即中止するように仕掛けを
する(左図中の赤い矢印)。
割込みで読み込んだスイッチ sf1の値を、1文字分の出力が終了するごとでチェックして、
sf1==0 だと、mout( )関数を終了し、呼び出し元( loop( ) )に帰る為の行を挿入(左図中の赤い矢印)。
こうしておけば、2番ピンのスイッチが押されるとすぐにモールス出力を停止できる。
また loop( )の方でも sf1!=0 になったら、mout( )関数を再開するようにしておけば、2番ピンのスイッチ
でメッセージ出力をスタートさせる事も可能になる。
|
|
以上の関数を組み込み、loop( )の中のfor文で、メッセージ番号をカウントアップしてmout( )関数を呼べば、
メッセージを順番に再生する。デジタル12番ピンにブザーやLEDをつけると出力が確認できる。
ここまでのプログラム全文
|
ここからはエレキー機能の製作に入る。プログラムは上述のとは別に新しく一から書く。
time1( ) だけ利用する。取りあえずエレキー機能だけを作って実験し、動作を確認
できてから、あとでメッセージ送出機能と合体する。
エレキー機能は dot用、dash用のスイッチがオンになったのを検出し、それぞれを自動で出力する。
スイッチを放しても、一旦始まった dot、dashは途中で切れず、1つ分は完全に出力してから
終わる仕組とする。
また、いつ押されるか分からないスイッチはずっとArduino側で監視して、押されたらすぐに反応し、
1つ分の dotかdashを出力するようにしなくてはいけない。それには内蔵タイマ割込みを使って、
数m秒間隔で2つのスイッチを常時見張るようにする。
dot、dashの長さは time1( )関数を使って制御する。スピードの制御もtime1( )関数に入れる引数の
乗数を変化させて行う。
|
タイマー割込みを使うので、そのためのライブラリを導入する。予め MsTimer2.zipをダウンロードして
おき、Arduino IDEの メニュ・バー「スケッチ」−「ライブラリを使用」−「Add Library...」で、
ダウンロード・保存しておいた MsTimer2.zipを指定する。zipファイルのまま指定するだけでライブラリが
導入できてしまう。便利だね!
|
|
タイマー割込み機能の設定
左図のように、void setup( ) の所に書いて初期設定する。
MsTimer2::set(10, yomi)
の10は10msecごとに割り込みを発生させる設定。
yomiはその10msecごとに強制的に呼ばれる関数名(void関数を指定)。
そしてMsTimer2::start( )を実行すれば、それ以降10msecごとに yomi( )関数が呼ばれる。
|
|
yomi( ) と el( ) 関数
yomi( )関数は10msecごとにデジタル10番、11番ピンを読取り、変数 a0, a1に入れるだけ。
10番、11番ピンはプルアップしたものをスイッチでGNDに落とすようにする。
el( )関数は、a0==0(10番ピン押下)なら dashを、a1==0(11番ピン押下)なら dotを出力する
関数で、loop( ) から呼び続けている。
dot, dash の長さは time1( ) の引数 knで設定している。
これでエレキー機能は完成。dot, dashの長さを外付けボリュームで可変できるようにしたのが次。
|
|
外付けボリュームでスピード可変に
10kΩBカーブのボリュームの真ん中の線をアナログ3番ピンに接続し、残りの線は5VとGNDに接続した。
そして、el( )関数内で、その読み取り値を kn=analogRead(3) で knに入れている。これでボリューム
をまわすと連続的に、エレキー出力のスピードを変えることができる。
これで"単純"エレキー機能は完成。実際にエレキー用パドルとブザーをつけ、音を聞きながら操作する。
使えなくはないが、dashとdotの間ではパドルをはっきり放さなければいけなくて、使いづらい。
速い打鍵はできない。そこでdash, dotを少しだけ
記憶する部分を付け足して、いわゆる"メモリ・キーヤ"にする。
ここまでのエレキープログラム全文
|
エレキーを操作するとき、例えばQ:−−・−を打つ時、通常dashのパドルを押さえたまま、−−(ツーツー)と
聞き終わる前に、dashのパドルは押したままで、dotのパドルを重ねてちょんと押す。
すると−−の後に・が入って、さらにその後にdashも入って−−・−となる。 −−と聞いて、
dashのパドルを放してから、dotのパドルを押し、またdashを押す、という操作は非常にやりづらい。
上記までのプログラムでは、dashを出力中なら、dotのパドルが押されても、dash出力が終わる前に、
dotパドルが放されると、放された瞬間にdotが押されている状態がなくなり、dotを出力しない。dotがとんでしまう
状態になる。
そこで、dash, dotのパドルが一旦押されると、それを出力するまで、押された"事実"を保持する機能を
追加した。以下のプログラムがそれだ。
|
パドルが押された"事実"を記憶する為の変数b0, b1を追加した。
yomi( )関数に、パドル(スイッチ)を読んだ内容を保持する機能を追加。パドルが一度でも
押されると、放しても押された"事実"が変数b0, b1に残るようにした。
el( )関数では、b0, b1の内容に従って、dash, dotを出力し、出力し終わってからb0, b1を元に
戻しておく。こうすればdash, dot出力中にパドルが操作されても、とばす事はなくなる。メモリ・
キーヤと呼ばれるゆえん。
実際にパドルを操作して、音を聞いても、なめらかなエレキー操作が出力される。エレキー
(メモリ・キーヤ)完成!
実際にこのプログラムと上記のものと、両方使ってみると、メモリ・キーヤのすばらしさが分かる。
完成したエレキープログラム全文
|
ここからは、メッセージ自動送出機能とメモリ・キーヤ機能を合体させる。合体は簡単だが、
その前に、メッセージをボタンで選んで、選んだ状態をLEDで表示させる機能を追加する。
ボタンを押すごとに、メッセージが1番−>2番−>3番−>1番・・・と変わり、それを
3つのLEDで表示させる。
LEDをデジタル7、8、9番ピンにつなぎ、メッセージ選択ボタンはプルアップした
デジタル3番をGNDに落とすように接続する。
|
メッセージ選択ボタンも、外部割込みを使う。デジタル3番ピンのスイッチなので、割込み番号=1。
|
|
メッセージ番号を保持する変数mbを新規に導入し、選択ボタンが押されるごとに、mbをカウントアップし、
2までアップしたら次は0に戻る仕掛けが、最初のif文。次のswitch文は、変数mbの値によって、表示ランプ
(LED)を光らせる。
|
これでメッセージ選択機能は完成した。あとはメモリ・キーヤとメッセージ送出機能を合体させるだけ。
その前に2つ、メッセージ送出機能を修正する箇所がある。
それは外付けボリュームでメッセージ送出もスピード可変できるようするのと、
メッセージが最後の文字になったら停止させるようにする事。
|
mout( )関数にメモリ・キーヤの所と同じように、ボリュームの値を読み込み、それで
time1( )の引数を変化させる。左図赤矢印
メッセージ送出スピードと、エレキー操作のスピードと、同じボリュームを使っているので
スピードが同じになる。独立させたい場合、もう1つ別のボリュームをつけて、そちらの値を
mout( )関数で読むようにすればいい。
1組のメッセージが終わったら、自動停止させるために、メッセージ配列データが{0,0,x,x}になったら、
sf1=0にしてから mout( )関数を終了するようにした。左図青矢印
これでプログラムは全部完成。完成プログラムはここ。
|
おまけ機能を付加した。CW受信の練習用に、英数字をランダムに出力させる機能。
出力は商用の欧文暗号文形式で、5文字ずつ羅列で出力される。
ただし、利用できるメッセージは2本になる。第3番メッセージの配列データ値(上記完成プログラム
中の3番メッセージ=全アルファベットと数字)を
利用する。メッセージ選択ボタンを3回押して、この機能を選びスタートボタンを押す。
|
ランダムに英数字を出力する ran1( )関数
おまけ機能で付加した関数は ran1( )で、内容はほぼ mout( )関数と同じ。ここで乱数を発生させて、
その値で第3番メッセージの配列データから出力する文字を選んでいる(左図赤矢印)。
変数rrはlongで宣言しておく必要がある。random(0,37)関数はArduino付属の関数で、乱数を発生
させる。引数の0は下限値、37は上限値+1(つまり上限値は36まで)。アルファベット26文字+
数字10文字の36。
このプログラムでは、5文字ずつ出力するので、for( )文で5回まわし、最後にスペースを出力している。
これ全体を更にfor( )文で100回まわしてる。
loop( )関数で、メッセージ選択ボタンの値を保持する変数 mbが2になったら、ran( )関数を呼び出す
ように改造する。
おまけを含めた完成プログラム。
|
製作費は、Arduino Pro miniの互換性基板(中国直輸入)を使えば、プロセッサ基板\250、
ピンソケット\10、基板\50、ブザー\50、ボリューム\50、LED\15、タッパケース\50、接続用トランジスタ\10、
抵抗\10、タクトスイッチ\20、3端子レギュレータ\20、コンデンサ\20、9V電池ソケット\10、
電源用スイッチ\30、以上中国輸入品で作ると合計約\600
(正規Promini=\2000、それ以外も日本のパーツ・ショップで買うと倍ぐらいになるので、合計約\3000ほどか)
中国直輸入のPro mini互換基板(左 \230) と USBアダプタ書き込み基板(右 \250)
個人輸入しても、保証はない。2個に1つだめで、倍の価格としても500円ぐらい(でも未だに
不良品には遭った事はない)。
水晶の精度は信用できないようだ。Arduino IDEを使ってプログラミングするのはライセンスの
問題があるのかどうか分からないが、
道義的にはよくないだろう。壊れるかも知れないような実験をするにはインチキ物でいいが、
最終的にセットに入れるには、本家本物を利用する。
とは言うものの、デジタル機器の性でしょうが、どんどん安くて高機能な製品が出来ている。
2000円も出すなら、Arduino Prominiよりは、mbedのボードがいいね。下記のNucleo STM32F401RE
なら2000円でARM32bitCPU+FPU, 512kBのプログラムメモリ、動作速度48MHz、12bitADCが10ch、
Arduino Uno R3互換ピン配置で、USBでパソコン直結して、mbedサイトで無料コンパイルできる。
おまけに強力なハードウェア・デバッグ機能搭載だ。かなり行き着いている感じ。(大阪日本橋
デジットで\1944税込み。2014/5)でもただの
ARM基盤は開発環境がよくないのご注意を。mbedでないとね。
2500円(スイッチサイエンス 2014/1)ならフリースケール社のおもしろい基板もある。詳細はここ。
もどる
|