PIC AVR 工作室->TopPage->AVRの工作2->DDS-FGコントローラ
DDS-FGコントローラ
AVRで作ったDDSファンクションジェネレータは、USART経由でコマンドを入力出来るようにして、 PCのシリアル端末上から「文字情報」で制御コマンドを送るように設計したんですが、USARTならPCじゃなくても動かせるようにも考えておいたので、 そのコントローラ部分を作ってみました。
Arduino用の「LCDキーパッドシールド」をaitendoで入手できたので、これを使ってスタンドアローンのコントローラを作成。
「Arduino」と「LCDキーパッドシールド」で組み上げたのと、スケッチ類はC-LCDの使い方や、 ADCによる複数ボタンの入力処理方法を使いまわすための サンプルスケッチ程度の価値しかないかもしれません。 (Arduinoサイトの方に載せておいた方がヨサゲなのかもしれません)
LCDキーパッドシールドについて
表示と入力に使う機器
元々、DDSファンクションジェネレータを作った当時、PCを使わずにスタンドアローンでも動かせるようにと構想してました。
なので、インターフェースは汎用で使えるUSARTにしていたんですが、「表示」と「操作」を簡単に扱う適当な機器が見当たらなかったので、 キャラクターLCDとジョイスティックを足し合わせたようなシールドでもつくろうかな…と思ってました。
そんな中、aitendoでさっきの写真左上のLCDキーパッドコントローラを発見(右下はDDSファンクションジェネレータ基板)。 丁度使いやすい、「LCDとキーパッドが一体化したシールド」っぽかったので、それを使いました。
DF robot社、Arduino LCD キーパッドシールド
aitendoで買ってきたこのシールド。
LCDはArduinoの「C-LCD用ライブラリ」を使って、4線I/Fで操作可能な扱いやすいものなんだけど、 キーパッド部分は、抵抗の分圧を使ってADCで入力するタイプだったので、スレショルド値を確認しておく必要あり。
確かめる為に、ブログの記事でテストプログラムをアップしたところ、 情報を頂き、aitendoオリジナルのシールドじゃなくて、DF robot社のArduino LCD KeyPad Shield (SKU: DFR0009) らしいことがわかりました。(シルクが微妙に違う気もするので、互換品かも)
この製品、いくつかバージョンがあるようで、キーパッド関係の「抵抗値」はバージョンごとに異なり、スレショルド値も多少異なるようです。 (ちなみにこの写真、ピンヘッダをハンダづけして、ブレッドボードで使いやすいようにアレンジしてあります)
ちなみに、私のゲットした基板では、各ボタンを押したときのアナログ入力端子の入力値は、 「オープン:1023」「select:720」「左:479」「下:307」「上:132」「右:0」となったので、 これらの値を元に、スレショルド値を設定します。
(詳細はスケッチ参照。基板のバージョンに合わせ、必要があれば変えてください)
私のゲットした基板で、ADCから入力される値を元に、E24系列の抵抗で一番近そうな組み合わせを推測してみたところ、
こんなかんじかなぁ?と思ったのですが、テスターを当てて試してみたところ、どうやらこれより一桁ほど小さい値の抵抗を使っているようです。
盛り込む機能(スペック)
大枠では、「ユーザI/F」「入力機能」「シリアル出力機能」「電源供給」の、4つの機能が必要になります。
シリアル出力機能まわり
シリアル出力は、19200bpsで、「周波数設定」「波形設定」「リセット」の3通りのコマンドを出力します。
周波数設定は、周波数の値(5桁以内の数値)の後ろにASCII文字で「F」を、 波形設定は、波形の番号(数値)の後ろに「W」を出力することで実現できます。「R」でリセットが掛かります。
これらの値を出力する処理を、「シリアル出力まわり」として盛り込みます。
(ちなみに今回の場合、「振幅」は送信データでの制御ではなく、DDS-FG基板側の「ボリウム」で制御するので、振幅関係のコマンド出力は無し)
入力機能まわり
入力は、LCD KeyPad Shiledに搭載されている「上下左右とselect」の計5個のボタンで操作します。
この5個のボタンは、アナログ0番端子に繋がっていて、抵抗分圧による電圧をADCで読み出すことでボタンを特定します。
先ほどのアナログ入力値を元にして、スレショルド値を設定し、どのボタンが押されたのかを判別します。
なお、ADCを使ったこの手のボタン入力の場合、複数ボタン同時押しのセンシングは出来ません。 また、チャタリングなどを加味すると、一定時間(と言ってもミリ秒程度)の間、同一キーが押されたままであることを確認し、 「ボタンを押す途中(または離す途中)ではないことを、処理ロジックとして組み込んでおく必要があります。
ユーザI/Fまわり
扱う制御情報は、「周波数」と「波形」と「リセット」なので、LCD上にこれらを表示しておいて、 「カーソル移動」と「selectボタン」で数値を弄る(もしくはコマンドを選択する)という方式で処理することにします。
試してみたところ、このC-LCDは、各文字の下(ドットフォントの8行目)に、普通にカーソルが表示できるタイプだったので、 カーソル表示機能をありがたく利用することにします。
具体的には、カーソルの左右キーで「選択」を、上下キーで「数値の各桁の変更」を行い、数値が変化したときには周波数と波形を送信します。 なお、カーソルがリセットのところにあるときに、「selectキー」押下で「リセットコマンド」送信ということにします。
電源供給まわり
折角なので、「Arduino + LCD基板」と「DDS FG基板」の電源は共有にしてみました。
電源に加え、シリアル接続用配線も合わせて、「Vcc、GND、シリアル」の3本の線だけで接続できます。 これで、PCを使わずに、LCD操作パネルつきのスタンドアローンDDSファンクションジェネレータ、一丁あがり。(冒頭の写真参照)
ちなみに、私の「秋月製Arduino互換基板・アレンジ版」では、電源は5V供給(3端子レギュレータ搭載無し)なので、 両方とも5V供給で済んでいます。(LCD用基板、DDS用基板とも)
Arduino-UNOなどを使う場合は、「9V」程度の電源を繋いで「Vin」端子を通じてこの外部電源を共有出来るようにすると良いと思います。
スケッチ
スケッチ
/* AVR DDS function generator controller */ /* with aitendo LCD keypad shield */ /* http://www.dfrobot.com/wiki/index.php/Arduino_LCD_KeyPad_Shield_%28SKU:_DFR0009%29 */ /* */ /* *input : keypad on LCD shield */ /* *output : LCD and USART */ /* */ /* created by nekosan */ /* created : 2013.10.21 V0.1 */ /* */ #include <LiquidCrystal.h> /* I/O pins for aitendo lcd shield */ LiquidCrystal lcd(8, 9, 4, 5, 6, 7); /**********************************? /*** declare global variables */ /**********************************? /* key names table */ char key_name[][10] = { "none ", "select ", "left ", "down ", "up ", "right " }; /* wave form name table */ char wave_name[][4] = { "sin", "sq ", "tr ", "sw1", "sw2"}; /* freq and wave memory */ char num_data[6] = {0,0,0,0,0 ,0}; // initial = 0Hz , sine wave /* limit table */ char limit[6] = {2,9,9,9,9 ,4}; // limit freq = 29999hz // limit wave = 4 (sw2) /* cursor position X */ int cur_x[7] = {0,1,2,3,4,7 ,15}; int pos_x = 0; /*****************************/ /*** declare functions ***/ /*****************************/ /* cognition witch button is pushed */ int key_sense() { /* last key */ static char last_key = 0; // initial = none /* key record ( for sensing key-change ) */ static char key_record[3] = { 0,0,0 }; // initial = none, none, none /* define threshold for keypads */ const int keypad_th[] = { (1023 + 720) / 2, /* none - select */ (720 + 479) / 2, /* select - left */ (479 + 307) / 2, /* left - down */ (307 + 132) / 2, /* down - up */ (132 + 0) / 2 }; /* up - right */ int key_in; int i; key_in = analogRead(0); key_record[2] = key_record[1]; key_record[1] = key_record[0]; int now_key = 0; for (i=0; i<5; i++) { if (key_in < keypad_th[i]) { now_key = i+1; } } key_record[0] = now_key; if ((key_record[0] == key_record[1]) && (key_record[1] == key_record[2])) { last_key = now_key; } delay(1); return last_key; } /* pick up key changing */ int key_check(int new_key) { static int old_key = 0; //initial = none int ret = 0; if (new_key != old_key) { old_key = new_key; ret = new_key; } return ret; } /* change a value */ void change_value(int key_change) { int i; switch (key_change) { case 1: // select if (pos_x == 6) { reset_dds(); // send reset command for (i=0;i<6;i++) { num_data[i] = 0; // all clear } } break; case 2: // left pos_x--; if (pos_x < 0) { pos_x = 0; } break; case 3: // down if (pos_x < 6) { num_data[pos_x]--; // declement if (num_data[pos_x] < 0) { num_data[pos_x] = 0; } } break; case 4: // up if (pos_x < 6) { num_data[pos_x]++; // inclement if (num_data[pos_x] > limit[pos_x]) { num_data[pos_x] = limit[pos_x]; } if (num_data[0] >= 2) { // if over 20000hz num_data[0] = 2; for (i=1;i<5;i++) { num_data[i] = 0; } } } break; case 5: // right pos_x++; if (pos_x > 6) { pos_x = 6; } break; default: break; } print_lcd(); // print freq and wave data on lcd } /* print out on lcd */ void print_lcd() { int i; /* print freq data */ lcd.setCursor(0, 0); for (i=0;i<5;i++) { lcd.write(num_data[i] + '0'); } lcd.print("F "); /* print wave data */ lcd.write(num_data[5] + '0'); lcd.print("W("); lcd.print(wave_name[num_data[5]]); lcd.print(") R"); // 'R' means 'reset' /* set cursor */ lcd.setCursor(cur_x[pos_x],0); } /* output to dds via uart */ void uart_out() { int i; /* output freq data */ for (i=0;i<5;i++) { Serial.write(num_data[i] + '0'); } Serial.print("F"); /* output wave data */ Serial.write(num_data[5] + '0'); Serial.print("W"); } /* reset dds */ void reset_dds(){ Serial.print("R"); } /************************/ /*** main process ***/ /************************/ void setup() { /* setup LCD */ analogWrite(10,127); // back light brightness lcd.begin(16, 2); lcd.clear(); lcd.cursor(); // display cursor on // lcd.blink(); // blink cursor on print_lcd(); // print freq and wave on lcd Serial.begin(19200); // to DDS synthesizer delay(100); reset_dds(); // Serial.println( (int)pow(10.0,4.0-(float)0.0 ) ); } void loop() { int i=0; int key_in; int key_change; key_in = key_sense(); key_change = key_check(key_in); if (key_change != 0) { change_value(key_change); uart_out(); //Serial.println(); } }
簡単な説明
まずLCDシールドとの接続。このシールドのI/Fは、「データ4本」+「Enable」「RS」の計6本で接続することになっています。
データ線は「4,5,6,7」の4本。RSが「8」、Enableが「9」です。ちなみに、D10はバックライト用LEDに繋がっていて、 D10のPWMを利用すると明るさを調整することも出来ます。(オン/オフのデジタル制御も可)
配列「wave_name」は、波形の番号(0~4)を元に、波形形状の名前にデコードするための文字列配列です。(LCD表示用)
配列「num_data」は、最初の5個が周波数の各桁の値を、残りの1個が波形の番号を保持しておくための数値配列です。 配列「limit」は、それぞれの数値の最大値で、キー操作によってこの範囲からはみ出ないように制限するのに使います。
配列「cur_x」は、キー操作で左右に移動したときに、どの位置にカーソルを表示するかのX座標で、 最初の5桁が周波数用、その次の1桁が波形番号用、残りの1個がリセット用です。
関数「key_sense」は、戻り値として押されたキーを番号で返します。
ADC入力による複数ボタンの入力を行う場合は、単に1回読み込んでスレショルド値と比較するだけだと、 状態の遷移中では「誤ったボタンをセンシングしてしまう」恐れがあるので、このスケッチでは、3回読み込んでみて、 連続3回とも同じキーと判断できた場合には、「キーが間違えなく押されている」と判断することにしています。
関数の最後で、delay(1)で1ミリ秒待っているので、3個のデータを拾う間(2ミリ秒)は同じキーを押していることを待っていることになります。 ちなみに、面倒だったのでこの関数の末尾でdelay(1)を使ってますが、これだとキーセンスをするだけで1ミリ秒も食ってしまいます。
タイマ割り込みなどを使って、ミリ秒単位で自動的にセンシングするほうが、他の処理に影響を及ぼさないので、 通常はそういう処理に組んでおいた方がよいでしょう。
関数「key_check」は、押した瞬間(正確には変化した瞬間)をピックアップする関数です。 押しっぱなしでは「何も押してない」のと同じ状態を作り出しています。押しっぱなしでキーリピートを掛けるには、 この関数を弄りなおす必要があるでしょう。
関数「change_value」は、キー入力を元に周波数などを変数に格納し、 LCDに表示します。関数「print_lcd」が、実際にLCDにフォーマット整形して出力する部分です。 カーソルが「R」の位置で「select」を押すと、DDS-FGをシステムリセットします。(関数「reset_dds」を呼び出し)
関数「uart_out」は、周波数値、波形番号をUSART経由でDDS-FGに送信します。
「setup」関数でシリアルI/Fの初期化などを行っています。シリアルは19200bpsで通信します。
動かしてみる
実行画面
実行直後は、LCDにこのように表示されます。(クリックで別画面に拡大表示)
ストロボのせいで見づらいですが、「00000F 0W(sin) R」と書かれていて、カーソルが一番左の「0」の下にあります。
初期値では周波数がゼロ(直流)、波形番号はゼロ(sin:正弦波)です。
キーパッドの左右キーで数値(と「R」)の部分を行ったり来たりして、上下キーで各桁の数値を変更します。 「R」の位置で「select」を押すとシステムリセットです。
結果
キーを操作して、周波数を1Hz、波形をノコギリ波に設定して、オシロで拾ったのがこれ。(クリックで拡大表示)
こんな感じで、PC要らずで操作出来るようになりました。
まとめ
ファンクションジェネレータ自体は、秋月とかでも安いキットが売られているし、 色々なサイトでもDDSのファンクションジェネレータが公開されていたりします。特に音声信号程度の周波数なら、ネット上に数多あります。
DDS方式がのファンクションジェネレータが欲しくて、また音声信号レベルで色々な波形を(waveテーブルさえ用意すれば)出力でき、 さらには2チャンネル出力も出来るものが欲しいと思って、あえて自作してみました。
ここで触れているのは1チャンネル出力のモノでしたが、以前、 AT-MEGA164シリーズを使った2チャンネル出力のタイプ も作成しました。これもほぼ同じI/F仕様になっているので、若干のスケッチ修正で2チャンネル出力基板にも接続できます。 (LCDの2行目は、そのために空けてあります)
この手の治具類は、作ることより使うことに意味があります。2チャンネル出力用のDDSファンクションジェネレータに改造してから、 あれやこれや、音声信号関係の実験が楽に行えるようになるはず。
とりあえず、欲しい機能を持った機器が作れたので、それなりに使いまわしていこうかと思います。 なお、スケッチは適当に改造したりして、お好きなように使ってください。
追記
U/I見直し版のスケッチ
スケッチの、ユーザインターフェース周りを見直しました。
周波数設定をする際に、桁あふれが起こるときに、ちゃんと上の桁にあふれさせたり、上の桁からもらってきたり、 という処理を組み込んで、少しだけ使いやすくしました。
/* AVR DDS function generator controller */ /* with aitendo LCD keypad shield */ /* http://www.dfrobot.com/wiki/index.php/Arduino_LCD_KeyPad_Shield_%28SKU:_DFR0009%29 */ /* */ /* *input : keypad on LCD shield */ /* *output : LCD and USART */ /* */ /* created by nekosan */ /* created : 2013.10.21 V0.1 */ /* */ #include <LiquidCrystal.h> /* I/O pins for aitendo lcd shield */ LiquidCrystal lcd(8, 9, 4, 5, 6, 7); /**********************************? /*** declare global variables */ /**********************************? /* key names table */ char key_name[][10] = { "none ", "select ", "left ", "down ", "up ", "right " }; /* wave form name table */ char wave_name[][4] = { "sin", "sq ", "tr ", "sw1", "sw2"}; /* freq and wave memory */ char num_data[6] = {0,0,0,0,0 ,0}; // initial = 0Hz , sine wave /* backup */ char num_backup[6] = {0,0,0,0,0 ,0}; /* cursor position X */ int cur_x[7] = {0,1,2,3,4,7 ,15}; int pos_x = 0; /*****************************/ /*** declare functions ***/ /*****************************/ /* cognition witch button is pushed */ int key_sense() { /* last key */ static char last_key = 0; // initial = none /* key record ( for sensing key-change ) */ static char key_record[3] = { 0,0,0 }; // initial = none, none, none /* define threshold for keypads */ const int keypad_th[] = { (1023 + 720) / 2, /* none - select */ (720 + 479) / 2, /* select - left */ (479 + 307) / 2, /* left - down */ (307 + 132) / 2, /* down - up */ (132 + 0) / 2 }; /* up - right */ int key_in; int i; key_in = analogRead(0); key_record[2] = key_record[1]; key_record[1] = key_record[0]; int now_key = 0; for (i=0; i<5; i++) { if (key_in < keypad_th[i]) { now_key = i+1; } } key_record[0] = now_key; if ((key_record[0] == key_record[1]) && (key_record[1] == key_record[2])) { last_key = now_key; } delay(1); return last_key; } /* pick up key changing */ int key_check(int new_key) { static int old_key = 0; //initial = none int ret = 0; if (new_key != old_key) { old_key = new_key; ret = new_key; } return ret; } /* inclement num */ void inc_num(int digit) { int i; num_data[digit]++; if (num_data[digit] == 10) { if (digit > 0) { num_data[digit] = 0; inc_num(digit -1); } } } /* declement num */ void dec_num(int digit) { int i; num_data[digit]--; if (num_data[digit] == -1) { if (digit > 0) { num_data[digit] = 9; dec_num(digit -1); } } } /* change a value */ void change_value(int key_change) { int i; int sum; /* back up */ for (i=0; i<6; i++) { num_backup[i] = num_data[i]; } switch (key_change) { case 1: // select if (pos_x == 6) { reset_dds(); // send reset command for (i=0;i<6;i++) { num_data[i] = 0; // all clear } } break; case 2: // left pos_x--; if (pos_x < 0) { pos_x = 0; } break; case 3: // down if (pos_x == 5) { // wave form num_data[pos_x]--; // declement if (num_data[pos_x] < 0) { num_data[pos_x] = 0; } } else if (pos_x < 5) { // digits dec_num(pos_x); } /* if under 0hz ,fix it */ sum = 0; for (i=0; i<5; i++) { sum *= 10; sum += num_data[i]; } if (sum < 0) { // if under 0hz for (i=0; i<5; i++) { num_data[i] = 0; } } break; case 4: // up if (pos_x == 5) { // wave form num_data[pos_x]++; // inclement if (num_data[pos_x] > 4) { num_data[pos_x] = 4; } } else if (pos_x < 5) { // digits inc_num(pos_x); } /* if over 20000hz ,fix it */ sum = 0; for (i=0; i<5; i++) { sum *= 10; sum += num_data[i]; } if (sum > 20000) { // if over 20000hz num_data[0] = 2; for (i=1;i<5;i++) { num_data[i] = 0; } } break; case 5: // right pos_x++; if (pos_x > 6) { pos_x = 6; } break; default: break; } print_lcd(); // print freq and wave data on lcd } /* print out on lcd */ void print_lcd() { int i; /* print freq data */ lcd.setCursor(0, 0); for (i=0;i<5;i++) { lcd.write(num_data[i] + '0'); } lcd.print("F "); /* print wave data */ lcd.write(num_data[5] + '0'); lcd.print("W("); lcd.print(wave_name[num_data[5]]); lcd.print(") R"); // 'R' means 'reset' /* set cursor */ lcd.setCursor(cur_x[pos_x],0); } /* output to dds via uart */ void uart_out() { int i; /* output freq data */ for (i=0;i<5;i++) { Serial.write(num_data[i] + '0'); } Serial.print("F"); /* output wave data */ Serial.write(num_data[5] + '0'); Serial.print("W"); } /* reset dds */ void reset_dds(){ Serial.print("R"); } /************************/ /*** main process ***/ /************************/ void setup() { /* setup LCD */ // analogWrite(10,127); // back light brightness lcd.begin(16, 2); lcd.clear(); lcd.cursor(); // display cursor on // lcd.blink(); // blink cursor on print_lcd(); // print freq and wave on lcd Serial.begin(19200); // to DDS synthesizer delay(100); reset_dds(); // Serial.println( (int)pow(10.0,4.0-(float)0.0 ) ); } void loop() { int i=0; int key_in; int key_change; key_in = key_sense(); key_change = key_check(key_in); if (key_change != 0) { change_value(key_change); uart_out(); //Serial.println(); } }
まぁ、あいかわらずこれ自体単体で使うものじゃないので、U/I周りだけ使いまわしできれば使ってください。