PIC AVR 工作室->TopPage->AVRの工作2->DDSファンクションジェネレータ

DDSファンクションジェネレータ

ふとファンクションジェネレータが欲しくなって、市販品を買ってくるか自作するかを考えてみて、 あまりスペックを求めなければマイコンを使ったDDSファンクションジェネレータが簡単に作れそうだったので作ってみました。

AVRのMEGAシリーズ1個+DAC-IC1個で、音声信号周波数レベルの出力が出来るくらいの簡易的なものが実現できたので、まとめておきます。

DDSファンクションジェネレータ

DDSはDirect Digital Synthesizerの略で、デジタル合成で任意の波形(sin波や矩形波など)を出力する、 名前の通り一種のデジタルシンセサイザです。そしてこれを用いたファンクションジェネレータがDDSファンクションジェネレータです。

簡単に作れる範囲で、部品にはお金をかけず、再現性や部品入手性にも問題がなく、制御(操作)も簡単に行えるようにと考えてみました。 Arduino(互換)基板にブートローダを書き込まずに「単なるAVRマイコン基板」として使い、 最低限必要な外付け部品をシールド形状(と同じピン配置)にすることで、必要なときだけDDSとして、 必要ないときはArduinoとして使えるようにしました。

写真の様にArduino互換基板を使い、上に万能基盤に組んだDAC回路(≒シールド)を載せ、 RCA端子もしくはピンヘッダから信号を取り出せるようにしてみました。 Arduino基板を持っている人なら必要な出費はDAC用の万能基板一式だけです。 (ただし、hexファイルの書き込みのためにAVR ISP mkIIなどプログラマが必要)

作戦の前に情報整理

作るとはいっても、何を隠そうDDSに限らずファンクションジェネレータ自体触ったことがありません。 どんなスペックにしたらいいのかを事前に考えてみました。すでにご存知の方、興味ない方は適当に読み飛ばしてください。

信号出力方式

ファンクションジェネレータには、主にアナログの発振回路がベースのタイプと、 デジタル処理で波形を生成するタイプがある→後者がいわゆるDDS方式のファンクションジェネレータ。それぞれメリットデメリットあり。

出力波形形状

アナログ式、デジタル式ともに、「sin波」「矩形波」「三角波」「ノコギリ波」程度は実装されていることが多い様子。 矩形波のデューティー比を可変に出来るものもある。

なおデジタル式の場合、サンプリング波形など複雑な波形を任意の周波数で出力できるものもある。

制御方法

アナログ方式なら発振回路は当然アナログ回路で制御するので、周波数の調整が少し面倒。 一方、デジタル方式の場合周波数はデジタル値で指定(一般に1Hz単位)することが出来、 設定した周波数に対して出力される周波数は一般に相当正確。処理方式次第だけど、水晶の発振周波数精度に近いレベルが実現できる。 (計算処理に起因する周波数誤差を数ppmとか数十ppmとかに抑えようと思えば抑えられる)

ちゃんとした製品ではMhzオーダーとかそれ以上の波形も出力できるが、簡易品では音声帯域レベルのモノが多い。 用途次第。今回は音声帯域で十分。

波形形状の選択はアナログ/デジタル共に大差なし。ただしDDS方式の場合前述のとおり、複雑な波形形状の出力も可能。

出力回路

アナログ量(電圧)を出力するため、どちらの方式にしてもアナログ出力回路が必要。特にデジタル回路を念頭に整理。

DDS方式で波形を生成した後、DAC回路でアナログ信号に変換する必要あり。DACの方式としてとりあえず以下の選択肢を想定。

(1)外付けDAC-IC
(2)内蔵PWMによるアナログ出力
(3)ラダー抵抗によるDAC回路

それぞれ必要に応じてオペアンプによるLPFと出力バッファを組む。 (デジタル出力のため微小時間で見ると波形がカクカク階段状になり、高周波ノイズが載ることに留意)

出力信号の振幅を制御する為に、アッテネータが要る。

…といったあたりを念頭にして、作戦を考えてみることにします。

作戦

信号出力方式

マイコン野郎なのでDDSを採用。(※:本当は、周波数設定の容易さや、サンプリング波形の出力にも対応できるから)

出力波形状

そもそもファンクションジェネレータが欲しくなった理由でもあるsin波は必須。 そのほか矩形波(デューティー比1:1固定)、三角波、ノコギリ波、逆ノコギリ波は実装したい。 デューティー比を可変にする場合は、sin波のテーブル(定数テーブル)を弄りなおせば出力が可能といえば可能なので、ひとまず見送る。

制御方法

色々情報検索してみると、自作品でもLCDパネルや制御用ボタンなどを実装してスタンドアロン機として使えるものもあるが、 作るのが面倒なのでとりあえず見送り。まずはPC上のシリアルコンソール(TeraTermなど) から文字情報として制御コマンドを送ってコントロールする形態としてみる。

もし気が乗ったら、PCの代わりになる制御専用回路とプログラムを組んで、シリアル接続からコマンド送信すればスタンドアロン化可能。 (そうなるような制御体系としておく)

メモリに内蔵する信号波形テーブルは、後述のDACに合わせてビット幅を12ビット(2バイト以内に収まる)とする。 MEGA168(容量16KB)に収めることを念頭に、2バイト×4096点=8192バイトのテーブルサイズで収めることで様子を見る。

出力回路

安い部品でサクッと仕上げてしまいたいところではあるものの、出来るだけ面倒が起こらないことを期待。 あと出力波形の品質もちょっと考慮。PWM、ラダー抵抗、DAC-ICについて比較。

少ない部品という意味でまず思い浮かぶのはPWM。PWMの場合はLPFを掛けて波形を整えるのは必須になるので、 オペアンプによるアクティブフィルタ(LPF)と出力バッファをこれに兼ねさせる案が思い浮かぶ。 が、そもそもそれなりの出力波形形状を実現するためには80000sps程度(可聴上限20000Hzを4サンプルで表現)は欲しいところ。 しかしAVRの内蔵PWMクロック周波数程度ではちょっとキビシイ。→PWMを除外。

ラダー抵抗によるDACについて。今回は少なくとも入出力ピンを殆ど使わなくて済むので好きなだけ使ってかまわないし、 何しろR-2RによるDACは出力時間がきわめて高速なので、選択肢には充分入ると考えられる。 一方、マイコンの出力端子を使ってR-2RラダーDACを組む場合、同時に出力できるデジタルピンの幅は8ビットまで (具体的には同一ポートの0番から7番に1バイト単位で出力できる範囲内)となるので、最大でも8ビット幅しか出力できない。

まぁ、外部にラッチICでも組んで2バイト程度に広げることは可能といえば可能だけど、そもそもR-2RラダーDACの出力精度自体が低く、 8ビット幅程度でも満足に実現できるかは微妙(特にゼロクロスひずみが大きい、金皮抵抗でギリギリ8ビット程度?)。

また、出力部分のインピーダンスは大きいのでそれなりにLPFとバッファを組みたくなるところだけど、いざオペアンプでLPFを組もうとすると、 電源をどうするとか、動作中点をどうするとか、DCカットするかしないかとか、DCカットしないならオペアンプの種類や電源をどうするか (正負電源?入出力R2Rオペアンプ?)などなど色々面倒なことが出てくるので、考えるのが面倒になってきて却下。 まぁそんなに面倒でも無いんだけど、R-2Rの精度の低さが最大のネック。→ラダー抵抗を除外。

DAC専用のICを使う場合、出力精度はカタログに明示されているし、実際どれもそこそこの精度が実現可能。 またマイコンとは比較的簡単なI/F(SPIやI2C)で繋ぐことができて便利。出力も大負荷電流を求めなければ簡単なバッファぐらい付いてるっぽい。

で、入手性、精度、単価を考えて超定番MicrochipのMCP4922をチョイス。 今回は1CH出力しか要らないのに2CH出力付いたICを使うのはちょっともったいないんだけど、1CH出力のMCP4822より安いので。

で、出力振幅の制御用(いわゆるアッテネータ)には10KΩBカーブの可変抵抗分圧でお茶を濁すことに。LPFすら省略。 本当はインピーダンスを下げる為にオペアンプで一旦フォロって置くのがアタリマエなんだけど、 色々面倒だったのと、手っ取り早く使いたかったのと、必要になったらあとでLPFとバッファを外付けで組めばいいやと。 まぁ、かなり手抜きで済ますことに。後日談だけど、よく考えたらVref端子を使えば内蔵バッファを100%有効に使えて、 インピーダンスは十分下げられたのに…というオチ。(この辺は回路図のところで改めて)

全体像

専用基板を作ってもいいんだけど、それほどしょっちゅう使わないだろうし、どうせなら汎用マイコン基板の流用でいいんじゃないかと。 で、Arduino(互換)基板+シールドのカタチにしておいて、普段は普通にArduinoとして使えるようにしておくことに。

ただし、80000spsをArduinoの処理パワーで実現するのは厳しいので、ブートローダを殺して単なるMEGA168/328基板として使用。 開発言語はアセンブラを念頭。とにかく処理時間との戦い。 16MhzのArduino(互換基板)で80000spsとなると、タイマ割込み1回の処理時間は最大でも200クロック未満。 もちろんメイン側処理も通信処理などちゃんと遅延無く行わないといけないので、200クロックギリギリではお話にならない。 80000spsとか言ってるけど本当に実現できるかは知恵の絞り方次第。(最大のネックはDAC-ICへのSPI出力の通信速度)

シールド側の電源はマイコン基板側から供給。出力は音声信号の帯域なので、 当然音声信号関係のアプリが多いだろうということでRCA端子を設ける。一方、ブレッドボードとの相性もよくするためにピンヘッダも用意。 まぁ、バッファ回路は組まないのでRCAを直接オーディオ機器と繋いで使えるかは疑問あり。 (でも直近ではそこまでのバッファドライブ能力は必要ないので無視)

UIとしては、シリアル通信による簡単な文字情報(ASCIIコード)による制御を想定。 PCのターミナルソフトと繋いでキーボードから文字を打ち込んで使えるように。機能要件としては周波数変更、波形形状切り替え程度。 アセンブラなのでそもそもあまり複雑な仕様にはしたくない。

Arduino基板を既存のモノを流用してゼロコストと考え、その他ザックリと部品代を考えてみると、 万能基板約100円、ピンヘッダ約100円、DAC-IC約250~400円、その他可変抵抗、配線、RCAコネクタ、パスコン、半田と考えても500~700円程度。 市販品を買うよりかなり安く済むとニマリ。

秋月のArduino互換基板をこれの専用品としてしまっても (USBシリアル変換はいつもの通りSparkFunのFTDI Basic breakout用のピン配置に改造するので)1000円も掛からない。 合わせても1500円にもならないレベル。

まぁ、あとは後付でLPFやバッファをちゃんと組むかどうかだけど、それでもオペアンプと抵抗、コンデンサ数個と考えれば100円~200円程度。 (実はMCP4922を使う限り、LPFはともかくバッファは内蔵してるので不要なんですが…あとから気づいた…)

と、こんな感じで作ることにしました。

DDSについて整理

一旦DDSの処理方法について一応整理。(ご存知の方は適当に読み飛ばしてください)

DDSは名前のとおりデジタル制御で波形を生成する処理方法。今回出力波形をデジタル式(DDS)にした以上、 そのメリット、デメリットを踏襲することになるでしょう。なので、この処理方法について一旦整理。

あらためてDDS

今回は1Hzから20000Hzまでのかなり広い周波数ダイナミックレンジを「一つの計算ロジック」だけで実現する必要があります。 つまり、周波数を指定すればそれに合わせた任意の波形をそれなりに正確な周波数、それなりに整った波形で出力してくれないと困るわけです。

それをうまい具合に実現できるのがDDSという方式で、先述のようにDirect Digital Synthesizerの略…今回の核心部分です。 DDSでは具体的にどんな風に信号処理をするのか整理しておきます。

DDSの計算処理について

DDSの計算方法について簡単なイメージで整理しておきます。 DDSで信号生成をする際に使用する「回転角」と「角速度」などの概念について触れます。

DDSで扱う(出力する)信号は、「周期性を持った波」の形をしています。正弦波、矩形波、三角波…すべて周期性を持った波です。

これらの波から「1周期分(1波形分)」を切り出して、ROMテーブル上に波形データテーブルとして格納しておき、 そのテーブルから順々に値を読み出しアナログ出力すると、テーブル化しておいた波形がアナログ信号の「波」として表現することができます。

その際、テーブルから読み出すときのポインタ(配列でいうと添え字)が指す位置を時間経過にしたがって変えてやる (変化の大きさ=速さ(角速度))ことで、波形の「周波数」を自由に制御することができます。

上の図は、角速度ωで、ある円周上をグルグルと回るときの事をあらわしています。微小時間が経過するごとにωずつ回転していきます。 一定の時間が経過すると、角速度ωに時間を掛けた値(角度=θ)だけ回転します(図で言うと星印がなす角度)。 下の図はこの時のy軸の値だけを取り出してみたものです。

下の図の「y座標」と角度「θ」そして角速度「ω」に着目し、コンピュータで扱えるようなテーブルにあらわしてみるとこんな感じになるでしょう。

一番左の欄は角度「θ」、その次は正弦波とした場合の「y座標の値」、右側にある緑の矢印は角速度です。 ω1はちょっと遅めの角速度(周波数低め)で、ω2はもっと速めの角速度(周波数高め)でテーブルを読み出す状態を表しています。

この様に、テーブルを読み出す際に単純に1個1個順々に読み出すのではなく、1つおきとか2つおきとかで読み出すと、 1個1個順々に読み出すときと比べて「出力される波の周波数が2倍、3倍…」と可変にすることができることがわかると思います。 さらには、2倍や3倍のようにちょうど整数倍である必要も無く、0.1倍とか0.01倍とか1.2倍とか2.3倍のように角速度を小数値で扱えば、 微妙な周波数も表現できることがわかります。先ほどのω1では低い周波数、ω2では高い周波数の波が再生できることがわかるかと思います。

もともと角速度ωは角度を時間で微分したものなんですが、 コンピュータで扱える数値は微小な離散値(特に微小時間といっても数ミリ秒~数マイクロ秒とか)だし、 ファンクションジェネレータの場合は一度周波数を設定したら周波数(や角速度)は一定なので、 あまりその辺の細かい概念にこだわらず、マイコンであればタイマ割込みが発生するごとに微小角度(つまり角速度ω)を加算していき、 求まった角度に該当する位置からテーブルのデータを読み出せば、事実上任意周波数の正弦波などの「波形」が得られるわけです。

ω1のように角速度が小さい(遅い)場合は1波形の長さが長くなり、ω2のように角速度が大きい(速い)場合は1波形の長さが短くなります。 それぞれ周波数の低い/高いが表現されるわけです。

読み出される波形をグラフにするとこんな感じです。

図の薄い橙色の矢印はタイマ割込み1回分の時間経過と思ってください。 低い周波数(上)は角速度が小さいので長い時間(たくさんの矢印)を掛けて1つの波を、 高い周波数(下)は角速度が大きいので短い時間(少ない矢印)で1つの波を出力することになります。

なお、テーブルには1波形分しか格納されていないので、一番下まで読み終わったらまたテーブルのアタマに戻って読み直すことになります。 (これによって出力信号は「連続した波形」になります)

計算精度について

このように「角速度」を希望の周波数に合わせて設定しておけば、テーブルからの読み出し速度を任意に変えることができ、 つまり周波数を任意に制御できるというわけです。この例ではsin波でしたが、テーブルの中身を書き換えれば、 矩形波や三角波さらにはサンプリング波形のような複雑な形状でも同様に、任意の周波数で出力することができます。

なお、今回周波数の指定は1Hz~20000Hzとすることにします。その際の角速度(タイマ割込み1回分で進む角度)の値は、 1Hz~20000Hzとけっこう広いダイナミックレンジになりますが、計算処理方法は結構単純で済みます。 例えば、1Hzの場合の角速度を定数値として用意しておき、それに「ユーザから入力された周波数値」を掛ければ、 必要となる角速度の数値が求まります。

この際の、基準となる「1Hz時の角速度」の定数値が内含する誤差が「周波数誤差」として表出することになります。 この誤差については後ほど詰めていきます。

一般的なDDSの計算方法、および今回の処理方針についてふれました。これを元に今回どのように処理すればいいのかを具体化していきます。

詳細設計

詳細設計というほどのモノでもないけど、各個別機能ごとの仕様をブレークダウンしていきます。

角速度の計算

まず肝となる角速度と精度について詰めておきます。

出力周波数の精度は、「クリスタルの発振精度」と「波形生成(角速度や角度)の計算精度」に因ります。

角速度や角度の精度は格納する変数の桁数次第なので、必要な精度が確保できる桁数をあらかじめ検討しておく必要があります。 アセンブラで高速に演算するために、当然「固定小数点」演算とします。 今回は、角速度および角度を格納する変数の桁数をともに4バイト(32ビット)とし、 ともに整数部、小数部それぞれ16ビットずつとして考えてみました。

整数部というのは、テーブルを読み出すときの添え字そのものと思ってください。 今回はテーブルのデータ件数を4096個と置いているので、1バイト(8ビット)では足りず、 またバイト単位を跨いで小数点を打つと処理が面倒なので、2バイト(16ビット)としました。

小数部は、テーブルからの読み出し時には無視されるところですが、微妙な(微小な)角度の変化をあらわすための部分です。 2バイト(16ビット)幅で表現できるのは、テーブル1件(1行分)を1/65536に分解した程度までということになります(分解能)。

この小数部が65536段階まで分解できるということを加味して、周波数値から角速度を計算する式はこうなります。

 ω = (65536 × 4096 / (20000*4)) × freq

小数部の分解能まで含めると、テーブルのアタマからオシリまで全件読み終わるまでがは65536×4096段階に分解できます。 1Hzならこの1波形分65536×4096段階を1秒間で舐めることになります。1秒間当たりの割込み頻度は20000×4=80000回なので、 割込み1回当たりに進む回転量(つまり角速度ω)は割り算をすれば求まります。これが1Hzの時の角速度なので、 これにユーザ入力の「周波数値」を掛ければ、任意周波数での角速度ωが求まります。

ただし、当然この角速度は小数部を含めて65536倍された値なので、 下位16ビット分を切捨て(もしくは四捨五入)してから波形のテーブル参照することになります。 もっと言うと、テーブルから(整数部が指し示す)近傍2件を読み出して、それらを小数部を元に按分計算を行えば、 波形(振幅)の値はテーブル自体が持つ分解能よりもスムーズにすることが可能ですが、処理時間が長くなるので今回は単純に切捨てとします。

補足:正弦波はテーブルからの読み出しで数値を得ますが、矩形波、三角波、ノコギリ波はテーブルを持つとメモリがもったいないので、 計算で波形を算出することにします。

出力周波数の精度

今回は、そこそこの計算精度とそれなりの処理速度を実現する必要があり、特に処理速度のほうを優先したかったので、 小数点以下を16ビット幅としてみました。さて、この場合の周波数精度を考えてみます。1Hz時の角速度ωの値(の小数部を65536倍したもの)は、

65536×4096÷80000 = 3,355.4432

となりますが、テーブルサーチの際に端数(小数部)は16ビット幅からあふれて誤差となるので…

(3,355.4432 - 3355)÷3,355.4432 = 0.000132084…

誤差は132ppmほど。1000Hzを指定すると999.8679…Hzが出力される程度。 Arduino基板上のクリスタルだと発振精度が大体50~100ppm程度でしょうから、それより少し悪くなる計算です。

この誤差は、変数のビット幅だけではなくその他の定数によっても影響を受けます。例えばテーブルのデータ点数を4096から8192に変えてみると…

65536×8192÷80000 = 6,710.8864

同様に誤差を計算すると、

(6,710.8864 - 6711)÷6,710.8864 = -0.000016928…

誤差はおよそ16.9ppm。1000Hzを指定すると1,000.0169…Hzが出力されます。これだとテーブルのデータ量が倍になっちゃうんですが、 周波数精度は一桁くらい向上してクリスタルの発振精度誤差より小さくできます。 これ以上計算精度を上げても50ppmの発振精度では意味は無く、オーバースペックとなるでしょう。

なお、割込み間隔を変える手もありますが、これはタイマモジュールの制約があったり出力可能周波数にも影響してしまいます。 また小数部の桁をさらに増やす手もありますが、処理時間に影響します。今回はとりあえず4096点で作ってみました。 (んで、その後さらに8192点でも作り直してみました)

出力周り

MicrochipのMCP4922をDACとして使います。このチップはSPI接続なので、ハードウェアSPIモジュールを使います。 ピン配置もSPI関係のものをそのまま利用します。

ただし、このチップは2バイト分のデータ(有効なのは12ビットですが)を受け取ってDA変換するので、 1バイト受け取って反映、もう1バイト受け取って反映してしまうと、上位/下位更新のタイムラグでノイズが載ってしまいます。 /LDAC端子を使うと、2バイト受信するまでラッチしておくことができるので、これを有効に使うのが得策です。

MCP4922の出力端子を10kΩの可変抵抗で受けて、出力電圧を分圧できるようにしました。 出力インピーダンスが大きいんですが、私が直近で使いたい用途では特に支障ないのでこれでよしとしてしまいました。 ただし実はMCP4922の出力回路はVref端子を持っているので、 可変抵抗で分圧した参照電圧を入力しておけば出力バッファが効いた状態で使うことができます。 ちゃんとデータシート読めばよかったんだけど、とりあえずサクッと作って使いたかったので見落としました。 Vrefを有効にした回路については後ほど触れたいと思います。

入力周り

Arduinoのシリアル接続部分をそのまま使います。8n1の19200bpsで通信します。

今回は割込み処理内で行う処理がヘビーで、通常処理側にはあまり処理時間を割けません。あまり通信速度を上げてしまうと通信内容を取りこぼす恐れがあります。 ある程度ロジックが組みあがってから、通常処理にどの程度処理能力を割けるのか計算してみて、 その範囲内で可能な限り高速な通信速度ということで、結果19200bpsとしました。数クロック単位の攻防です。

周波数、波形制御方法

PC側のターミナルソフトからASCII文字で制御信号を送ります。マイコン側では内容を解釈して、周波数を変更したり波形を変えたりします。

「複数桁の数値+1文字のコマンド文字」のセットで一つのコマンドとして受け付けることにし、 この「1文字のコマンド文字」はデリミタを共用します。波形選択も数値+コマンド文字で処理します。

具体的には…

「周波数変更」:「2500f」…2500が周波数指定部分、fが周波数変更を表すコマンド。大文字のFでも同様。

「波形変更」:「0w」…0が波形指定部分(この場合正弦波)、wが波形変更を表すコマンドで、大文字のWでも同様。 0:正弦波、1:矩形波、2:三角波、3:ノコギリ波、4:逆ノコギリ波。数値は、最後の1桁だけが有効。

「システムリセット」:「r」…数値に関係なく、内部状態を初期化します。大文字のRも可。

初期値は、周波数はゼロ(直流状態)、波形は正弦波を設定します。

20000Hzまで扱うので、数値は10進数5桁となります。SRAM上に5桁までの入力値をバッファしておくエリアをつくり、 6桁以上の数値が入ってきたらFIFOで上の桁から捨てていきます。

例外処理について。入力された数値が5桁でも20000Hzを超えている場合は強制的に20000を仮定します。 f、w、r以外の文字が入力された場合は、入力済みの数値をゼロクリアしますが、rと違って波形はそのまま維持します。 数値が入力されずにwだけ指定した場合や、範囲外の番号+wを指定した場合は正弦波が仮定されます。

このくらいやっておけば、変なコマンド列が入ってきても処理内用は破綻しないで済むでしょう。

ちなみに、f、w、r以外のアルファベットは読み込み済みの数値をすべてフラッシュすることになるので、 例えば「c」を「数値クリア」というコマンドとみなしてしまうと便利かもしれません。

その他

タイマ割込みは、timer2モジュールをモード2(CTC)で使って200クロックおきに発生させます。 電源は、PC接続のシリアルコンバータから取り込みます。(この辺の手抜きはArduino基板を流用することのメリット)

回路図

CPU周りはArduino(互換)の基板を利用する為に、特段の考慮は不要となるでしょう。 考えておく必要があるのは、DAC回路周りです。DAC周りを中心に整理します。

回路図その1

ブレッドボードで、とりあえず動く回路を組んでみたのがこれ。sin波テーブル4096点用に、 とりあえず動かしてみるために組んでみた回路がこれです。

PB5、PB3、PB2(ArduinoのD13、D11、D10)の3線はSPI通信用として使用します。PB4(同D12)はDACからの入力信号が無いので接続しません。 とりあえずVrefは5V固定、Voutを可変抵抗で分圧して出力振幅を調整する回路になっています。

/LDAC用に、4096点版はPD4(同D4)を、8192点版はPB1(同D9)を使用していますが、 実は4096点版プログラムは配線とピンの設定(DDRレジスタ)だけ行って、/LDAC信号の上げ下げ処理を組み込んでなかった(常時ゼロ)ので、 4096点テーブル版の方は2バイト分データの反映の狭間で出力信号にノイズが載る状態になっています。 8192点版を参考に適宜修正して使ってください。

出力周りをVrefを使うように変えたのがこの回路図。MCP4922の出力バッファをそのまま活かすことができます。それ以外は一緒。

プログラム

プログラムを以下に挙げます。4096点、8192点ともに3つのファイルで構成されます。 プログラム本体、sin波定数テーブル、掛け算マクロの3ファイルです。掛け算マクロは両方に共用です。 3ファイルを同一ディレクトリに入れてアセンブルしてください。

(右クリックでダウンロードしてください)

4096点版はMEGA168、MEGA328どちらでも動きますが、8192点版はコードサイズの関係でMEGA328でしか動かせません。

使ってみる

配線関係など

まずはブレッドボードでも万能基板を使ったシールドでも何でもいいんですが、回路図どおりに組んでください。 可変抵抗は用途に応じてAカーブでもBカーブでもいいんですが、 折角Aカーブを使ったところで出力バッファが完全にGND電位まで出力できるというわけではないので、 Bカーブでお茶を濁しておいた方がいいと思います。

ブレッドボードに組むとこんな感じです。

ちなみにページ冒頭の写真は万能基板に組んでシールド形式にしてみたものです。 普通の万能基板をシールド形式にする場合、一部のピンヘッダは穴半分ほどずらす必要があるので、 ピンヘッダをこんな風に変形してから取り付ける必要があるでしょう。

全体的に特に大変なところは無いと思いますが、大変なところが無いところが大変で、電源周りはいい加減、 ノイズ対策もろくに考えていない回路なので、出力信号の品質は推して知るべしです。

電源供給及び制御信号送信用としてシリアル変換ケーブル機能経由でPCから電源供給しています。 Arduino基板上の電源用コンデンサと、DACのICに取り付けた0.1uFのパスコンくらいしか盛り込まれてません。 1点接地とかも考えてません。結構ノイズが出ます。

本当は、電源のノイズ対策などちゃんとすればもう少し品質のいい波形が得られるんだろうと思いますが、 Arduino基板流用+シールド形式では限界があるし、私自身の直近のニーズも踏まえてスペックは追求せず、 サクッと仕上げることに特化しました。

なお、必要に応じて出力回路部分にはカットオフ20000Hz以上の高次LPFをつけて頂くとよいでしょう。 (DACから出力される1/80000秒毎の階段状ノイズが緩和されて高周波ノイズが減ります)

http://sim.okawa-denshi.jp/Multiple3tool.php

たとえばここでチェビシェフフィルタなんか使って計算するとよろしいかと。私はとりあえず直近では必要なかったので端折りました。

回路が出来たら、あとはアセンブルして出来たhexファイルを書き込めば使えます。

操作の仕方

シリアルコンソールソフト(TeraTermなど)の通信速度は19200bpsの8n1に設定してください。 なお、基板からは何もコールバックが無いので、ローカルエコーはオンに設定しておいてください。 (じゃないとシリアル画面上に何もみえません)

そのシリアルコンソールソフトからこんな風に文字を打って制御します。

この内容を頭から眺めます。最初の「100f」は周波数を100Hzに設定する命令。 続く「50f」「20f」はそれぞれ50Hz、20Hzに設定。その後ろの「0w」「1w」「2w」「3w」「4w」はそれぞれ正弦波、矩形波、 三角波、ノコギリ波、逆ノコギリ波に設定します。この様にシリアル経由で数値+コマンド文字で指定します。

操作系周りの回路を組んだりするのが面倒だったので、PC上のシリアルコンソールから操作するようにしてみました。 この様にコマンドは単純な文字列なので、簡単な制御回路とプログラムで制御基板を構成すれば、 簡単にスタンドアロンのファンクションジェネレータにすることも出来ます。 (DAC回路と制御基板を一つのシールド形状に組めば便利かもしれません)

出力波形を眺めてみる

実際に動かしてみて、オシロで波形を眺めたり、FFTを掛けたりしてみます。(クリックで大きな画像が出ます)

三角波の波形

10Hzの正弦波を出力してみたところ。まぁまぁな感じ。

100Hzの正弦波。まぁまぁ。

1Khzの正弦波。ちょっとギザギザが見えてくる。

10khzの正弦波。正弦波と呼ぶのはキビシイ感じ。

やっぱり10khz程度ともなるとLPF無しではだめですな。

FFTをかける

FFTをかけてみるとこんな感じ。まず10Hz。

100hzではこんな感じ。

1khz。ちょっと高周波ノイズが目立ってくる感じ。

正弦波以外の波形

矩形波を眺めてみる。1hz。

三角波。1hz。

ノコギリ波。100hz。

逆ノコギリ波。1khz。

途中で周波数を変更

周波数変更。正弦波を出力中に周波数を変更した場合の波形。1hz→5hzに変更したところ。

周波数の変更によって変わるのは角速度だけで、位相は引き継がれるので、この様に波は途切れず連続した形になります。

まとめ

Arduino基板を流用することを考えれば、その他の部品代数百円で自分が使いたかったファンクションジェネレータの機能が実現できてなにより。 安上がり。

当初ブレッドボードで組んで使ってたんですが、面倒だったのでシールド形状にしました。 基板面積が限られるのと作るのが面倒だったのでLPFは端折りました。でもやっぱフィルタは付けたいところ。今後の課題。

シリアルコンソールからの制御だと、いちいち周波数値を入力するる必要があってスムーズに周波数を変えられません。 ロータリーエンコーダとか使って専用コントローラでも組めば便利かも。今後の課題。

SPI接続のDACを使ったので80000spsでギリギリといった感じでしたが、アナログ出力部分の処理と回路をR2R-DACとかに変えてしまえば、 もう少し高い周波数まで出せるようになると思います。あと、Arduino基板流用のため16MHzクロックになってますが、 専用基板で考えれば20MHzにしてもうちょっと高周波まで出せるようには出来るかもしれません。 マイコンではせいぜい音声周波数帯が限界でしょうけど。

まぁ、当初必要としていたスペックが数百円で実現できたので、これはこれでよしとしたいと思います。