Arduinoを使ったメモリキーヤ プログラムの開発記録
 
アマチュア無線で使う、CWメモリキーヤを作ってみた。
Arduino Uno R3または Pro miniを使ったCWメモリ・キーヤ・プログラム開発の経緯を記録する。
このメモリ・キーヤでは、送出用メッセージ文を、予めプログラム内のデータとして書き込み、Arduinoに 焼きこんでおく方式。 スイッチを押せばそれを送信できるようにするもの。こうすればプログラムの開発が容易になる。メッセージを 随時パドルから入力できるようにはしない。実用上、それ程しょっちゅう送出メッセージは書き換えないし、 Arduino IDE を使えば簡単にメッセージを書き換えできるから、これでよしとする。
勿論パドルをつないでエレキー操作もできるようにする。おまけでCW受信練習機能(ランダムに 英数字出力する)もくっつけた。遊び心で、LED電飾も作ってみようかなぁ。
(追加説明)エレキー、メモリ・キーヤ、メッセージ・キーヤは昔流で言うと、 それぞれ少しずつ 違う。エレキーはdashパドルとdotパドルを押すと、単純にそれぞれdash, dotが自動で出力されるだけのもの。 メモリ・キーヤはエレキーとほぼ同じだが、ほんの少しだけdash, dotそれぞれのパドル押下を記憶(メモリ) する機能を持ったエレキー。(この違いによる操作性は全然異なる。詳細後述) メッセージ・キーヤはここで扱うように、予め記憶させておいたメッセージ (電文)をボタン1つで自動送出できるもの。
しかし、マイクロプロセッサを手軽に使える今日では、メモリ・キーヤと言えば、当然ここで言う、 メッセージ・キーヤの事になる。マイクロプロセッサを使わないで、メッセージ・キーヤを作るとなると、 恐ろしく複雑な回路になるだろう。昔なら、個人が趣味片手間で作るなんて、とても無理なお話だった。

 
開発手順
  1. 時間稼ぎ関数を自作する。整数値を入れてコールすれば、その値に比例した時間をつぶす 関数。dot, dash, spaceなどの長さを作るために必要。Arduinoにはdelay( )関数があるが、 delay( )関数は中身が分からないし、割込みでおかしくなるらしいので、今回利用しない。 時間稼ぎ関数が核プログラムになるので、これから作り始める。
  2. モールス出力関数を作る。予め作ったメッセージ文のデータから、時間稼ぎ関数を使って、 dot, dash, space を出力する関数。
  3. 外付けスイッチで、文字単位で停止できるよう、最初から配慮して関数を作る。
  4. 外付けスイッチで外部割込みを発生させ、リアルタイムでメッセージ出力を停止・再生させる。
  5. 別の外付けスイッチで、出力するメッセージ文を選択できるようにする。メッセージ文は3本の中から 選択できるようにする。これは増やすのは容易に修正できるはず。
  6. 外付けパドルでエレキー操作もできるようにする。
  7. 外付けボリュームで、メッセージ送信、エレキー送信の速度を連続可変できるようにする。
  8. おまけで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)ならフリースケール社のおもしろい基板もある。詳細はここ。  
 
もどる