はじめに
突然ですが「任意の波形を手軽に出力したい」と思ったことはないでしょうか。
正弦波やパルスといった普通の波形であればファンクションジェネレータ(FG)で簡単に出力できます。定型波ではない任意の波形も最近のFGや任意波形発生器で出力させることが可能です。
さていま丁度、IoT関連の開発でありがちな『読み取った電圧の波形異常を検出するプログラム』を作成しているとします。そしてプログラムで異常が検出できるかを軽く確認したい。そこでFGや任意波形発生器を用意して異常値を含む波形を出力させようとすると…急に腰が重くなってしまう。
この手の機器、なんだか仰々しいんですよね。操作体系も独特だったりしますし、機能を熟知する必要もあります。手元でちょっと確認したいだけなのに…まさに中国地方の方言で言うところの「たいぎい」という感覚に近いです(笑)。
簡単な動作確認だから精度とか求めない。もっと気軽に任意の波形を出力できないものか。
そんな希望を叶えるため、簡単な任意波形生成器を作成します。当部署の開発室にも似たようなハンドメイド任意波形生成器があるのですが、気軽に出力できて場所も取らないので重宝しています。
今回は自宅にもひとつ欲しくなったので自宅で作成しました。そのため登場する機材(私物)がチープな点はご容赦ください。
DACとR2Rラダー回路
今回はRaspberry PiのGPIOを用いて任意の波形を出力させようと思いますが、その際に必要となるのがDACです。
DACとは
DAC(D/Aコンバータ)とは『Digital to Analog Converter』の略で、デジタル値をアナログに変換します。逆にアナログをデジタル値に変換するのはADC(A/Dコンバータ)となります。
一昔前は音楽を聴くのにCDを使っていたと思いますが、CDにはアナログ波である音声をデジタルデータに変換したものが無圧縮で記録されています。波形を時間軸方向に一定間隔(44.1kHz)で区切り、振幅方向も同じように区切り(0~65535)、一定時間おきに振幅の値をゼロイチのデジタル値に変換。このAnalog to Digitalの変換がADCの役割で、ADCが出力したデジタル値を円盤に記録したものがCDです。
このデジタル値をアナログに戻して音を鳴らすのがCD再生機器の役割ですが、先ほどとは逆にDigital to Analogの変換が必要です。この変換を担うのがDACということになります。
R2Rラダー回路とは
DACには様々な方式があり各種ICもあるのですが、今回はR2Rラダー回路を採用します。R2Rラダー回路が面白いのは抵抗とスイッチだけで作ることができる(==手作りできる)という点です。原理は真面目に解説すると長くなるので気になる方は各自調べて頂くとして、回路は以下のように抵抗値RΩとその倍の2RΩの抵抗を接続したものになります。こちらは8bitのデジタル値をアナログに変換するR2Rラダー回路です。
ベースの電圧を $Vb$ 、左上の出力電圧を $Vout$ とします。そして各スイッチをOnにするとそれぞれ $1/2Vb, 1/4Vb ... 1/128Vb, 1/256Vb$ が出力され、合計の電圧が $Vout$ に出力されます。ここで 0〜255(8bit) の値を2進数に変換して各桁をそれぞれのスイッチにアサインし(MSB:$1/2Vb$、LSB:$1/256Vb$)、1でスイッチON、0でOFFにすると、$Vout$ から2進数の値に応じた電圧が出力されます。
例えば255でしたらオール1ですので全スイッチOnで全部足して約 $1Vb$ 、128(10000000)では $1/2Vb$ 、そして 154(10110010) では $(1/2Vb + 1/8Vb + 1/16Vb + 1/128Vb) \simeq 0.695Vb$ が $Vout$ から出力されることになります。このように2進数のデジタル値に応じてスイッチをOn/Offに設定し、合計電圧をVoutから出力することでデジタル値→アナログへの変換を行います。
例えるならば、それぞれのスイッチが水門になっていて、水門の開閉によって下流の水量が決まるのと同じようなイメージといえるでしょう。その下流の水量がR2Rラダー回路では電圧になります。
そしてこのスイッチを高速に動かしてアナログ値を変化させると、自在に波形の出力ができるはずです。
しかし簡単にDACを作ることができる反面、R2Rラダー回路には欠点があります。それは抵抗には誤差があるということです。カーボン皮膜抵抗のカラーコードには4つ目に金銀が入りますが、これは金だと最大±5%、銀だと最大±10%の誤差があるという意味です。これだけ誤差があると、出力電圧値にそれなりの影響が出る可能性があります。
R2Rラダー回路を作ろう
今回はこのような回路を製作します。R2Rラダー回路のスイッチの代わりにRaspberry PiのGPIO 8本を接続してOn/Offを制御し、デジタル値をアナログ電圧として出力します。接続するGPIOが8本ですので、8bitのDACとなります。
この回路ではR2Rラダー回路の後ろにオペアンプとコンデンサを入れています。オペアンプは出力インピーダンスを下げるためのボルテージフォロワとして使います。出力電圧範囲を電源電圧と同等とするためフルスイング(レールtoレール)タイプ、また立ち上がり/立ち下がりのキレが良い高スルーレートタイプ、ということでMCP6022-I/P(秋月電子通商)を調達しました。
またコンデンサは波形に含まれる直流成分を除去するために入れています。R2Rラダー回路の出力電圧の範囲はRaspberry PiのGPIO電圧に準じ 0〜3.3V となります。しかし壁コンセントから出ている商用交流電源のように、波形の電圧値は負の値にもなり得ます。なのでこの回路が出力する波形も負の電圧値が出せるようにしたい。要するに -1.65〜+1.65V の範囲で出力させたいのです。そこで 0〜3.3V の波形は「-1.65〜+1.65V の波形に、+1.65V の直流成分のバイアスが足されたもの」と考え、この +1.65V の直流成分を除去して -1.65〜+1.65V の波形にするためにコンデンサを入れています。
最後は抵抗です。先ほど「抵抗には誤差がある」という話をしました。なのでR2Rラダー回路を作るには、抵抗間の誤差を小さくしたい。そこで今回はカーボン皮膜抵抗ではなく、誤差が±1%の金属皮膜抵抗(10kΩ、20kΩ)(秋月電子通商)を各200本ほど調達しました。
そして誤差による影響をゼロに近づけるため、力業を行使します。
このように1本ずつテスターで抵抗値を測定し、小数点2桁まで同じ抵抗値のものをより分けます。ちなみに計400本の抵抗を仕分けするのに半日かかりました。気になる誤差はプラス方向はなし、マイナス方向も1%以内に収まっていたので、けっこう優秀だと思います。
これで役者は揃いましたので、ユニバーサル基板にハンダ付けして作成します。Raspberry Piと接続しやすいよう、ピンヘッダを立ててあげましょう。またオペアンプのVCC(5V)とGNDのピンヘッダも忘れずに。
Raspberry Piに接続して任意波形を出力しよう
R2Rラダー回路とRaspberry Pi (今回はPi 4)を接続します。接続するRaspberry PiのGPIOピンは回路図に記載していますが、下図のピンになります。GPIO 10側がLSBですが、LSBとMSBは逆にすると奇天烈な波形もどきが出力されますので注意してください。また5VはオペアンプのVDDに接続します。
そしてRaspberry Piにaptでpigpioインストールをし、Pythonプログラムを動かします。引数には波形データのCSVと周波数(Hz)を指定します。
# 実行書式
python r2r.py [CSVファイル] [波形の周波数]
import csv
import sys
import pigpio
import time
# R2Rと接続するGPIOピン
DATA_GPIOS = [10, 9, 11, 5, 6, 13, 19, 26] # LSB -> MSB
# 引数処理
if len(sys.argv) != 3:
print("Usage: " + sys.argv[0] + " [filename]" + " [frequency(int)]")
sys.exit(1)
try:
FREQENCY = int(sys.argv[2]) # 波形の周波数
except ValueError:
print("Usage: " + sys.argv[0] + " [filename]" + " [frequency(int)]")
print("[frequency] must be an integer.")
sys.exit(1)
values = [] # 波形の各ポイントの値(0-255)を格納するリスト
# CSVを読み込み2列目の値を取得し、valuesに保存
# CSVの書式: [インデックス], [-128〜127の整数値]
with open(sys.argv[1], newline='') as f:
reader = csv.reader(f)
for linenum, row in enumerate(reader, start=1):
if not row:
continue # 空行は無視
elif len(row) != 2:
print("CSV Error: " + str(linenum) + ": The number of columns is not 2.")
sys.exit(1) # 列数が2でない行がある場合はエラー終了
try:
v = int(row[1])
except ValueError:
print("CSV Error: " + str(linenum) + ": " + row[1] + " is not an integer.")
sys.exit(1) # 列2の値が整数でない場合はエラー終了
v += 128 # R2Rは0−255の値をとるため、-128〜127の値を0〜255に変換
# 足した分(直流バイアス)はコンデンサで除去
if v > 255:
values.append(255)
elif v < 0:
values.append(0)
else:
values.append(v)
RESOLUTION = len(values)
PERIOD_US = int(1000000 / (FREQENCY * RESOLUTION)) # valuesの値をR2Rに出力する間隔(us)
print(values)
# GPIOの初期化
pi = pigpio.pi()
if not pi.connected:
print("pigpio init failed")
sys.exit(1)
for gpio in DATA_GPIOS:
pi.set_mode(gpio, pigpio.OUTPUT)
pi.write(gpio, 0)
# 波形テーブルの作成
pi.wave_clear()
waveforms = []
for i, val in enumerate(values):
gpio_on_mask = 0 # ONにするGPIOのビットマスク
gpio_off_mask = 0 # OFFにするGPIOのビットマスク
for n in range(8):
g = DATA_GPIOS[n]
if (val >> n) & 1: # valのnビット目が1ならGPIO(g)をON
gpio_on_mask |= (1 << g) # gpio_on_maskのgビット目を立てる
else: # valのnビット目が0ならGPIO(g)をOFF
gpio_off_mask |= (1 << g) # gpio_off_maskのgビット目を立てる
waveforms.append(pigpio.pulse(gpio_on_mask, gpio_off_mask, PERIOD_US))
# 波形をR2Rに出力
pi.wave_add_generic(waveforms)
wave = pi.wave_create()
if wave < 0:
print(f"wave_create failed!! wave id: {wave}")
pi.stop()
sys.exit(1)
pi.wave_send_repeat(wave)
print("Generate wave and send it to R2R DAC ...")
print(f"Resolution: {RESOLUTION}, Frequency: {FREQENCY} Hz")
try:
while True:
time.sleep(1) # 波形を出力し続ける
except KeyboardInterrupt:
print("Stopping...")
# 終了処理
pi.wave_tx_stop()
pi.wave_clear()
pi.stop()
print("Finished...")
pigpioを使用したのはGPIOを1本ずつ逐次On/Off操作するのではなく、一度に8本のGPIOのOn/Offを操作するためです。これにより1つの値を出力するのに波がガタガタにならず、また速度面でも有利となります。しかし難点もあり、GPIOの更新間隔(PERIOD_US)が整数値指定で、PERIOD_USの計算 $PERIOD\_US=10^6 / (FREQENCY*RESOLUTION)$ で綺麗に割り切れなかった場合、丸め誤差のため出力周波数にズレが生じます。しかし今回は「精度はそこまで求めない」という要件があるのでヨシとします。
CSVの書式は[インデックス],[-128〜+127の整数値]で、1周期の波の形を-128〜+127の振幅の範囲で記述します。インデックスはプログラム中では使用しませんので任意の文字列でも大丈夫です。試しに正弦波を出力する場合、1周期を時間軸方向に100分割してその時間の振幅をCSVの2列目に書きます。分割数(解像度)は100でなくてもよいですが、少なすぎるとガタガタな波形になりますし、多すぎると周波数の上限が低くなります。また上記のPERIOD_USが綺麗と整数になるように分割数を調整するのもひとつの手です。
それでは早速、正弦波のCSVと100Hzを指定してプログラムを実行し波形が出力できているか、チープなオシロスコープで見てみましょう。
見事に100Hzの正弦波が出力されています。下(水色)はコンデンサ通過前、上(黄色)はコンデンサ通過後の波形です。コンデンサ通過前はおおよそ0〜3.6Vですが、通過後は直流成分が抜かれて-1.8〜1.7Vとなりました。少々ズレてるのは残念ですが、上出来でしょう。
次にのこぎり波を出力させて最大限まで拡大させた画像です。抵抗の誤差が大きいとまっすぐにならないはずですが、少々のうねりが見えるものの良好です。頑張って半日つぶして抵抗を仕分けた甲斐がありました。
では最後に、以下のように適当に作った波形(?)を出力させてみたいと思います。同じく100Hz指定です。
1周期を画面の全体に収めて横に引き伸ばしているためガタガタ、むしろピコピコと表現したくなる見た目ですが、波形は再現できています。分割数を増やして細かく作り込めば綺麗な波形になると思います。オシロスコープが周波数を正しく拾えていないのとキャプチャ画像も目盛りが見づらいのですが、オシロスコープの画面上で数えたところ1周期あたりちょうど10div(=10ms)でしたので、指定通り100Hzで出力できていることが分かりました。
さいごに
今回はR2Rラダー回路を用いた任意波形生成器を作成しました。抵抗とOn/Offスイッチだけの単純な回路で波形が作れるというのは面白く、この仕組みを考えた人を尊敬してしまいます。
気軽に使える任意波形生成器を安く作れることもメリットです。今回はCSVのファイル転送が楽なRaspberry Piを使いましたが、何かしらのマイコンボードを用いてさらに安く仕上げることも可能かと思います。よろしければ皆様もぜひ作成してみてください。
We Are Hiring!









