しばらく前に製作したワンハンドルのパワーパックは、三角波をアナログ回路で発生させたもので、電源電圧までのフルスイング(=ハイベタ)ができませんでした。高速型を目指したわけではなく、どちらかというと低速(超低速)を重視したので、その方向性には問題ありませんが、この回路の肝ともいえる三角波をマイコン回路を使って作れないかと検討を始めました。AIにヒントを求めたところ、D/Aコンバータ(以下、DAC)を使ってできそうだということで早速実験を開始。三角波を作るためには一定時間毎にDACへのデータの書き込みを繰り返す 例えば1-2-3-4-5-4-3-2-1 とすれば良いわけで、周波数を50Hzとすると1周期を256ステップでは78.125μ秒、128ステップでは156.25μ秒に1回ずつデータを書き替えていけばできそうです。
データは予め配列に読み込ませておき、一定時間毎の割り込みが入るたびに次のデータに書き替えます。割り込みが78.125μ秒では、あまり余裕がないので、1周期(=20m秒)を128ステップ 156.25μ秒で実験したところ、三角波として問題無い形が出力できたので、この128ステップに決定。12ビットのDAC(=分解能が4,096)のために以下のデータを作りました。
Dacdata: Data 64% , 128% , 192% , 256% , 320% Data 384% , 448% , 512% , 576% , 640% Data 704% , 768% , 832% , 896% , 960% Data 1024% , 1088% , 1152% , 1216% , 1280% Data 1344% , 1408% , 1472% , 1536% , 1600% Data 1664% , 1728% , 1792% , 1856% , 1920% Data 1984% , 2048% , 2112% , 2176% , 2240% Data 2304% , 2368% , 2432% , 2496% , 2560% Data 2624% , 2688% , 2752% , 2816% , 2880% Data 2944% , 3008% , 3072% , 3136% , 3200% Data 3264% , 3328% , 3392% , 3456% , 3520% Data 3584% , 3648% , 3712% , 3776% , 3840% Data 3904% , 3968% , 4032% , 4096%
上記のデータテーブルを以下のコードで配列に読み込ませます。この64個のデータは三角形の左半分(右上がり)のデータで、65個から128個目はこれと対象のデータですからテーブル上には置かずにコード上で定義しています。最後の3行で全てのデータを0以下の数字としています。これが元データですが、この状態では実質的には出力はゼロです。この数字に対して加算をしていくことで三角形がゼロレベルから上に顔を出すように上がっていき、4095を加算すると完全な三角、そこから更に上げていくと、最終的にはハイベタ=直流になるという仕組みです。
Restore Dacdata '読み込むデータのラベル For I = 0 To 63 Read Tridata(i)-1 '三角波データの読み込み 0から127個目 Next I For I = 64 To 127 Tridata(i) = Tridata(127 - I) '128個目が頂点で移行は下降させる Next I For I = 0 To 127 Tridata(i) = Tridata(i) - 4095 '256個のデータを-4095~0の値にレベルシフトする Next I
初めの構想ではAVRマイコン1個で全て処理できるかもと思っていましたが、156.25マイクロ秒で割り込みを繰り返すことを考えると、どうやら無理な感じ・・・ということで、この時点でマイコンを2個使う構成に方針を変更。パワーパックとしての基本的な機能ースイッチの読み込みやマスコンの位置の読み込みーはCPU1に、三角波と照明用のPWM波を作るのはCPU2で行うということに。この場合、CPU1で取得したマスコン位置のデータをCPU2に渡す必要がありますが、このデータの受け渡しをどのように行うかということが次の課題。初めはSPIインターフェースを使った方法で動かしてみたところ、どうもおかしい。CPU1はCPU2からのSPIのSS信号を使った割り込みで処理しようとしていましたが、これではダメなんですね。CPU2からのリクエスト(割り込み)が来た時点でデータを返さないといけない。ところが割り込みが入るのが1バイト目の8クロックを受信してからということがオシロの波形から分かりました。これでは割り込みが入った時点で既に手遅れ。次に、BASCOMのSHIFOUTとSHIFTINコマンドが使えないかと、CPU2からCPU1のINT1ピンへ割り込みを入れてシリアル転送してみましたが、処理が遅くてだめ。156.25μ秒の間にDACへの書き込みとこの転送を入れないといけません。
さあ、どうするかと悩んだ結果、SPIの割り込みをコンパイラ任せにせずにビット操作にして、割り込みをCPU1のINT1に入れて転送するデータを用意させ、CPU1が転送するデータを用意できた頃を見計らってSPIINを実行すれば良いのではないかと実験。ただし、CPU2からのSS信号をCPU1のINTピンに入れただけではだめで、スレーブ設定したCPU1のSSピンにも並列で接続(通常の使い方ではSS信号同士を接続しますが、今回はマスター設定したCPU2からのSS信号をスレーブ設定したCPU1のSSとINT両方に接続という意味です。)しないとSPIは動きませんでした。(データシートにその記述あり)
次にCPU2側のコードです。156.25μ秒の割り込みで動かしたDACの処理の直後に以下を実行します。本来であればデータ取得を行ってからDACの処理を行いたいところですが、データ取得がCPU1のA/D変換中は行えない場合に156.25μ秒の処理がずれてしまうため、最優先でDACの処理を行い、その後に転送処理を行うという方法になりました。そのため、ここで取得したデータは次のDACの書き込みで使うための値ということです。
Tx_spi: 'マスターからのデータ取得 If Pind.0 = 1 Then Return 'CPU1がA/D変換中は処理を行わない Reset Portb.2 'SSをLOW(通信開始) Waitus 4 ' 上位バイトの送受信 Buffer(1) = Dummy '送信データ(ダミー) Spi1in Buffer(1) , 1 '上位バイト受信 High_byte = Buffer(1) Set Portb.2 'SSをHIGH(通信終了) Waitus 2 Reset Portb.2 'SSをLOW(通信開始) Waitus 4 ' 下位バイトの送受信 Buffer(1) = Dummy 'ダミーデータ送信 Spi1in Buffer(1) , 1 ' 下位バイト受信 Low_byte = Buffer(1) Set Portb.2 'SSをHIGH(通信終了) Shift_data = Makeint(low_byte , High_byte) '16ビットデータに結合 Return
ちなみにですが、最近使うことの多いATmega328PBでは、タイマーが増えただけではなくSPIインターフェースも1つ増えて2系統載っています。そこで1系統目はプログラムの書き込み専用とし、2系統目をCPU間のデータの転送用としました。
CPU1側での処理は次の通りで、ad_data3にA/Dコンバータ(MCP3428 )で取得した生データが入っています。なお、A/D変換はCPU1側において100ミリ秒毎に行いますが、A/D変換中にSPIの割り込みが入るとデータが化けてしまうので、CPU1からCPU2に対してBUSY信号を設定し、A/D変換中にはSPIによる割り込みが入らない処理をしています。(上のコードの2行目のIf文。)また、ツインCPUということで電源投入後の初期化後の動作を合わせるために、このBUSY信号を動作開始の同期にも使っています。具体的には電源オンとともにCPU1からBUSY信号をセットし、このBUSY信号がクリアされるまではCPU2は初期化が終了してもメインループに入らずに待つようにしました。
' SPI割り込み処理 Spi_isr: If Byte_count = 0 Then Spdr1 = High(ad_data3) ' 上位バイト送信 Byte_count = 1 Else Spdr1 = Low(ad_data3) ' 下位バイト送信 Byte_count = 0 End If Return
上記のコードはINT1ピンに割り込みが入ったときの処理ですが、1回目の割り込みでA/D変換された16bitデータの上位8bit、2回目の割り込みで下位8bitが返され、次回の割り込み用にカウンタ(Byte_count)をクリアしています。
これをオシロで見ると、ch3(赤のライン)が2回続けてLowになっているところがありますが、ここがSPIによる転送です。

ch1がDACへのI2Cインターフェースのクロック、ch2がデータ、ch3がCPU1とのSPI-MOSI、ch4がSPI-MISO(CPU1から2へ渡すデータ)
次に三角波とPWM波の出力です。

上が20KHzのPWMで、下がDACが出力している三角波(5Vp-pのフルスイング波形)。PWMを出力しているのはCPU2のタイマー3で、コードは次の通り。なお、compare3aとcompare3bの値は仮の値。
Set Portd.2 '20KHz PWM 照明用 Config Timer3 = Pwm , Prescale = 1 , Clear Timer = 1 , Compare B Pwm = Clear Up '16MHz/800 = 20KHz Set Tccr3a.wgm11 'fast PWM Set Tccr3b.wgm13 'top=ocr1A Compare3a = 800 Compare3b = 100
実はここがとても悩んだところで、ATmega328PBで追加されたタイマー3と4に少々癖が。設定は問題無いはずなのに、どうしても出力しない。AIにコードを確認させても問題なし。なぜか?・・ 答えは1行目のSet PORTD.2 。ATmega328Pの時代からのタイマー0から2と異なり、タイマー3と4ではPORTD.2を出力かつ、Highに設定しておかないと出力が出てこない仕様になっていた・・・。これで無事に照明用のPWM波も出力OK。このPWM波、連続しての出力は必要無いというか、ずっと出ているとこれだけでモータが回ってしまうことがあります。そのためある程度切り刻んでおきたい。以前のパワーパックではコンパレータを使ってTL494を制御するというアナログ的は処理をしていましたが、今回はデジタル処理で。都合の良いことに先ほどのPORTD.2を制御してやることで、出力のON/OFFが容易にできる。(他にもタイマー出力を制御する方法はありますが。)そこで、三角波の1周期の任意のステップでPORTD.2を制御。三角波の頂点は128ステップの半分である64ですから、その64を中心に38から90の間のみPORTD.2をセットし、ここ以外はリセットしておく。すると、三角波の頂点を中心とした部分のみ出力。この数値を変えれば、三角波1周期のどの位置に出すことも容易です。もちろん連続での出力も可能です。
If Cnt > 38 And Cnt < 90 Then Set Portd.2 Else Reset Portd.2 End If
次の画像は上から(上記の制御をした)タイマーの出力のPWM、三角波をオフセットさせて三分の二程度出力したもの、そしてPWMと三角波の合成波形。このPWM波でライトを点灯させ、三角波でモータを回します。マスコンを入れることで、この三角波を全く見えない状態からだんだんと上にオフセットさせていく <車両が停止時に見えているのは常点灯用のPWM波だけで、そこから時間とともに三角の頂上の部分が見えてきます> これが超低速からPWMパワーパックの様なノイズをほぼ出さずにモータを回すことができるという仕組みです。

今回実験した回路では三角波を電源電圧までフルスイングさせていませんが、三角波の底辺側を上にオフセットさせていくことで、最終的には電源電圧にほぼ近い直流波形になります。PWM出力のパワーパックでは、デューティを変化させ最終的にはデューティ100%(=直流)にしますが、この三角波パワーパックも最高出力時には同じように直流出力となります。(自分としては、あくまでも微速から低速域の性能を求めているので、そんな超高速で走らせる必要はないのですけどね。)