CPUは飾りです。エライ人にはそれがわからんのです。これはPSoC Advent Calendar 10日目の記事です。(内容が最初の予定と変わってしまいすみません)
先日、7セグメントVFD (蛍光表示管) とPSoC 5LPを使って時計を作りました。いかにしてCPUを使わないかという工夫に着目し、PSoCに実装したものの中身を紹介します。
はじめに
数年前、大須のボントンでLD8035という型番の蛍光表示管が4本800円 (だったか?) で売られていました。7セグ+αのセグメントを持っていて、適当な工作に楽しそうだったので買っておいたのが引き出しに入れてありました。(そういうのはたくさんありますね...) セグメント / グリッドのドライバに使うTD62783も引き出しから出てきた、よかった...
蛍光表示管とは
真空管で、蛍光色素に電子が当たって光るみたいです。青とか緑とかにかっこよく光っているのをよく見ます。15Vくらいのアノード電圧で動くので比較的お手軽に駆動できるのが良いです。
PSoCに全部入れたい
4本あったので時計にします。時計は簡単に実用的なものが作れてしまうので、気づくとどんどん増えますね。VFDを駆動するので、5V → 15Vの昇圧回路とフィラメントの定電流回路も必要です。これらを合わせてPSoCに入れてみます。
5V → 15V昇圧回路
7セグメント4chのVFDには50mAもとれれば十分です。要求される電圧精度と応答特性も厳しくないので、適当に昇圧DC/DCコンバータを組みます。部品箱にあった1mHのインダクタとNchFETとSBDを使いました。
一般的なDC/DCコンバータでは、FETの駆動パルスを三角波生成回路とコンパレータの組み合わせで生成したりしますが、ここでは同じ機能をPSoCに入れるため、8bit SAR ADCと8bit PWMの組み合わせに落とします。
PWMのクロックには24MHzのMASTER_CLKを1/2した12MHzを入れたため、その1/256の47kHzがFETの駆動周波数になります。ADCは、PWMの1周期 (47kHz) ごとにトリガされ、現在の出力電圧 (の1k/(1k+4.7k)の電圧) を8bitに変換します。ADCの出力値からPWMのデューティを計算する部分は事前に計算しておき、8bit値256要素のテーブルにして埋め込んでおきます。
テーブルを使ってPWMのデューティを取り出す
普通に実装すると、ADCのend-of-conversion信号を割り込みで拾い、割り込みルーチン内でテーブルを参照するといった感じになるでしょう。が、CPUはできる限り使いたくないので、今回はこれを2つのDMAで実現しました。
まず、ADC出力値 → PWMデューティの変換テーブル (8bit 256要素) はコンパイル時に256バイトの境界に乗るようにしておきます。PSoC CreatorでPSoC 5LP向けのコードを書く場合、コンパイラにgccを使うことになるため、変換テーブルを uint8_t table[256] __attribute__(( aligned(256) ));
で宣言することでこれに対処します。
2つのDMAは前段、後段の順に必ず一組になってinvokeされます。後段のDMAが実際にテーブルの要素にアクセスし、拾った8bitの値をPWMのperiodレジスタに転送します。前段のDMAはADCから8bitの値を受け取り、後段のDMAのsrcアドレスを格納した32bitの領域の下位8bitに転送します。この、2つのDMAを組み合わせてテーブルを参照する方法は非常に便利で、PSoC Creator添付のDMAコンポーネントのデータシート等では、indexed DMAという名前で紹介されています。ちょっと応用するとDMAを使ってlinked listをたどるような演算もできそうで、わくわくしますね (やりたくはないですが)。
- indexed DMAの解説 (AN84810: Advanced DMA TopicsからFigure 34,35を抜粋)
DMAのアーキテクチャとindexed DMAの構成は、以下の2つのアプリケーションノートが参考になります。
- Getting Started with DMA: DMAのアーキテクチャの解説
- Advanced DMA topics: Indexed DMAなどいくつかの応用の解説
最後に2つのDMAの初期化のコードを示します。for文delayがかっこ悪い。
static uint8_t boost_coef_table[256] __attribute__(( aligned(256) ));
static uint8_t DMA1_Chan, DMA2_Chan;
static uint8_t DMA1_TD[1], DMA2_TD[1];
static inline
void boost_start(void)
{
int i, offset = -10;
/* bootstrap */
PWM_Start();
PWM_WriteCompare(100);
ADC_Start();
for(i = 0; i < 100000; i++) {}
/* initialize coefficient table */
#define clip(x) ( (x) < 0 ? 0 : ((x) > 255 ? 255 : (x)) )
for(i = 0; i < 256; i++) {
boost_coef_table[i] = clip(i + offset);
}
/*
* indexed DMA: DMA1 and DMA2 cooperatively calculates
* `pwm_duty <- boost_coef_table[adc_read_value]' on every ADC conversion.
* DMA1: write current ADC value to the base address register of DMA2
* DMA2: transfer the content of boost_coef_table to PWM compare register
*/
DMA2_Chan = DMA2_DmaInitialize(1, 0, HI16(CYDEV_SRAM_BASE), HI16(CYDEV_PERIPH_BASE));
DMA2_TD[0] = CyDmaTdAllocate();
CyDmaTdSetConfiguration(DMA2_TD[0], 1, DMA_INVALID_TD, 0);
CyDmaTdSetAddress(DMA2_TD[0], LO16((uint32)boost_coef_table), LO16((uint32)PWM_COMPARE1_LSB_PTR));
CyDmaChSetInitialTd(DMA2_Chan, DMA2_TD[0]);
CyDmaChEnable(DMA2_Chan, 1);
CyDmaClearPendingDrq(DMA2_Chan);
DMA1_Chan = DMA1_DmaInitialize(1, 0, HI16(CYDEV_PERIPH_BASE), HI16(CYDEV_PERIPH_BASE));
DMA1_TD[0] = CyDmaTdAllocate();
CyDmaTdSetConfiguration(DMA1_TD[0], 1, DMA_INVALID_TD, DMA1__TD_TERMOUT_EN);
CyDmaTdSetAddress(DMA1_TD[0], LO16((uint32)ADC_SAR_WRK0_PTR), LO16((uint32)&((dmac_tdmem2 CYXDATA *)DMAC_TDMEM)[DMA2_TD[0]].src_adr));
CyDmaChSetInitialTd(DMA1_Chan, DMA1_TD[0]);
CyDmaChEnable(DMA1_Chan, 1);
CyDmaClearPendingDrq(DMA1_Chan);
return;
}
ちなみにDC/DCコンバータの応答・周波数特性は全然考えていないですが、負荷が暴れる要素はなさそうなので特に問題はないでしょう。
フィラメントドライバ
特になんの工夫もありません。8bit DACの出力を、外部に置いた電流ドライバにつないでいます。これ作るときにオペアンプの+と-を逆につないでいてはまりました。(この回路図あってるかな?)
「もしかして...」 https://t.co/6rmmOEJx39
— パセリなずな (@ocxtal) September 30, 2016
RXとTXも目覚めたら入れ替わってたりしますね。
計時回路とセグメントのパターン生成
74HC162と74HC247が4組並んでいる (4510と4511が4組並んでいる) ような回路が必要です。ここでもCPUをできる限り使わないことを目指し、Verilog HDLでごにょります。
時刻のカウント
単にカウンタを並べるだけなのですが、シンプルにalways @(posedge hoge)
するとPSoCのPLDアーキテクチャではリソースの使用効率が悲しい感じになってしまいます。なのでカウンタはdatapathモジュールに実装します。
(PSoCのPLDにはdatapathという名前の機能ブロックがあってな...: PSoC 5LP Architecture TRMより Figure 21-6を抜粋)
datapathの8ステートのうち、最下位ビットが0に対応する4ステートはidleにします。これは全体を同期回路として動かすために必要です。最下位ビットが1になる残りの4ステートは、それぞれ +1 / -1 / ゼロクリア / 最大値のロード に対応します。4つのステートを適切に選択すれば、通常の計時モードと時刻合わせモードで必要な操作が実装できます。
- 計時カウンタのdatapathの8つのステート (伝われ...)
モードの選択
モード選択スイッチが押されるたびに、計時モード → 分調整モード → 時調整モード の3ステートを順に遷移します。これは単なるカウンタなので、count7モジュールを利用しました。
cy_psoc3_count7 #(
.cy_period(7'd2),
.cy_route_ld(`FALSE),
.cy_route_en(`TRUE))
mode_state_counter (
.clock(clk_intl),
.reset(1'b0),
.load(1'b0),
.enable(mode_posedge),
.count(mode_state_counter_out)
);
- (PSoCのPLD部にはcount7という名前の7bitダウンカウンタがあってな...)
バルククロック生成器
時分調整モードで up / down ボタンを長押しした際、連続して高速にカウントアップ / カウントダウンされるような挙動を実現します。以下のように値が変化するカウンタを実装すればよさそうです。
ここでもdatapathを利用します。カウンタのリロードを行うステートを2つに分け、D1レジスタからlong periodを、D0レジスタからshort periodをそれぞれロードするようにします。
- バルククロック発生器のdatapathの4つのステート (伝われ......)
VFDのダイナミック点灯
32kHzの入力を15段のカウンタで1Hzに分周する際の、7段目と8段目の出力を使って4桁を選択しています。(512Hzで駆動することになるのかな?)
2進 → 7セグメントパターンへの変換は、DC/DCコンバータの部分で示したものと同様にindexed DMAを利用し、各桁のグリッド選択信号を使って2段のDMAを叩いています。変換テーブルは、10位を取り出すテーブルと1位を取り出すテーブルを両方用意し、桁によって使い分けるようにしました。(時/分それぞれで60進/24進カウンタになっているためです)
最後にモジュール化
全体をclock24
モジュールにまとめました。32kHzのクロック入力と、up / down / modeの3つのスイッチ入力を受け取ります。
できた😁
こんな感じになりました。
ソースコードをgithubにおいておきます。興味のある方は中を見てもらえれば (そしてマサカリをふるっていただければ) と思います。
まとめ
各要素の解説が短く、かなり不親切な記事ではありますが、PSoCのCPUを使わず機能を作り込んだ製作例を紹介しました。特に、
- indexed DMAを使うとCPUを使わずテーブル参照ができる
- 簡単なカウンタはcount7を利用しよう
- 複雑なカウンタ等はステートを整理し、datapathに作り込もう
の3点がポイントです。count7やdatapathの機能の詳細は、テクニカルリファレンスマニュアル (TRM) に書かれているので読むと楽しいです。また、PWMやカウンタなどのモジュールのデータシート、ソースコードと合わせて読むとそれらの機能ブロックの使い方の理解が深まります。
そして、せっかくCPUを使わないように作り込んで、
while(1) {
/* nothing to do */
}
を実現したのですが、CPUをdeep sleepにするとバスもUDBも止まってしまうのですね。(おわり)