ブロックRAMからの正弦波でブリージングLED効果を実現する
ここ数年で購入した多くのガジェットが、LED点滅から「LED呼吸」に移行していることに気づきました。ほとんどの電子機器にはステータスLEDがあり、その振る舞いでデバイス内部の状態がわかります。
私の電動歯ブラシは充電が必要になるとLEDを点滅させ、携帯電話は様々な理由でLEDを使って注意を促します。しかし、昔のようにLEDが単純に点滅することはありません。連続的に強度が変化するアナログ的なパルス効果です。
以下のGIFアニメーションは、私のLogitechマウスがバッテリー充電中であることを示すためにこの効果を使用している様子です。光強度パターンが人間の呼吸サイクルと速度・加速度の面で似ているため、これをブリージングLEDと呼びます。照明サイクルが正弦波パターンに従うため、非常に自然に見えます。

この記事は、先週のパルス幅変調(PWM)に関するブログ記事の続きです。前回のチュートリアルで作成したPWMモジュール、ノコギリ波カウンター、リセット・モジュールを使用します。PWMに関する記事は以下のリンクからご覧ください!
VHDLでPWMコントローラを作成する方法
ブロックRAMに正弦波値を格納する
FPGAのDSPプリミティブを使って実行時に正弦波を生成することも可能ですが、より簡単な方法はサンプル・ポイントをブロックRAMに格納することです。実行時に値を計算する代わりに、合成時に一連の正弦値を計算し、それらを格納する読み取り専用メモリ(ROM)を作成します。
以下の最小例は、フル正弦波を3×3ビットROMに格納する方法を示しています。アドレス入力とデータ出力の両方が3ビット幅で、0〜7の整数値を表現できます。ROMには8つの正弦値を格納でき、データ分解能も0〜7です。

三角関数の正弦は、任意の角度入力xに対して[-1, 1]の範囲の数を生成します。また、x ≥ 2πでサイクルが繰り返されます。したがって、ゼロから2π(含まない)までの正弦値のみを格納すれば十分です。2πの正弦値はゼロの正弦値と同じです。上図はこの概念を示しています。ゼロから7π/4まで、2πの直前の最後の均等ステップまで正弦値を格納しています。
デジタル・ロジックでは、角度や正弦値のような実数を無限精度で表現できません。これはあらゆるコンピューター・システムで共通です。倍精度浮動小数点数を使っても、あくまで近似値です。これが2進数の仕組みであり、私たちの正弦ROMも例外ではありません。
利用可能なデータ・ビットを最大限活用するため、ROM内容計算時に正弦値にオフセットとスケールを加えます。最小正弦値-1をデータ値0に、最大正弦値1を2data_bits-1にマッピングします。以下の式で示す通りです。
\mathit{data} = \mathit{Round}\left(\frac{(1 + \sin \mathit{addr}) * (2^\mathit{data\_bits} - 1)}{2}\right)
ROMアドレスを角度xに変換するには、以下の式を使用します:
x = \frac{\mathit{addr} * 2\pi}{2^\mathit{addr\_bits}}
もちろん、この方法では汎用的な角度→正弦値コンバーターにはなりません。それが必要なら、アドレス入力とデータ出力をスケーリングする追加ロジックやDSPプリミティブが必要です。しかし多くのアプリケーションでは、符号なし整数として表現される正弦波で十分です。次節で見る通り、LEDパルシング・プロジェクトにはまさにこれが必要です。
正弦波ROMモジュール
この記事で紹介する正弦波ROMモジュールは、ほとんどのFPGAアーキテクチャでブロックRAMを推論します。ターゲットFPGAのメモリ・プリミティブに合わせて幅と深さのジェネリックを調整すると、最適なリソース利用が得られます。Latticeプロジェクトの例を参照して、実FPGAプロジェクトでの正弦波ROMの使い方を確認してください。
以下のフォームにメールアドレスを入力して、VHDLファイルとModelSim/iCEcube2プロジェクトをダウンロード!
エンティティ
以下のコードは正弦波ROMモジュールのエンティティを示しています。推論されるブロックRAMの幅と深さを指定する2つのジェネリック入力があります。定数に範囲指定を使って意図しない整数オーバーフローを防止しています。詳細は後で説明します。
entity sine_rom is generic ( addr_bits : integer range 1 to 30; data_bits : integer range 1 to 31 ); port ( clk : in std_logic; addr : in unsigned(addr_bits - 1 downto 0); data : out unsigned(data_bits - 1 downto 0) ); end sine_rom;
ポート宣言にはクロック入力がありますが、リセットはありません。RAMプリミティブはリセットを持てないためです。addr入力にスケーリングされた角度(x)値を割り当て、data出力にスケーリングされた正弦値が現れます。
型宣言
VHDLファイルの宣言領域の冒頭で、ROM格納オブジェクト用の型とサブタイプを宣言します。addr_rangeサブタイプはRAMスロット数と等しい整数範囲、rom_typeはすべての正弦値を格納する2次元配列を記述します。
subtype addr_range is integer range 0 to 2**addr_bits - 1; type rom_type is array (addr_range) of unsigned(data_bits - 1 downto 0);
しかし、まだ格納信号を宣言しません。まず、正弦値を生成する関数を定義する必要があります。この関数を使ってROM格納信号に初期値を割り当てるため、信号宣言の上に関数を宣言しなければなりません。
addr_bitsジェネリックをaddr_rangeの基底として使用しているため、addr_bitsの最大値を30に制限しました。より大きな値では2**addr_bits - 1の計算がオーバーフローするためです。VHDL整数は32ビット長ですが、VHDL-2019では64ビット整数が使用されます。しかし現時点では、ツールがVHDL-2019をサポートするまで、VHDLで整数を使う際はこの制約と付き合う必要があります。
正弦値生成関数
以下のコードは正弦値を生成するinit_rom関数を示しています。rom_typeオブジェクトを返すため、型を先に宣言し、次に関数、最後にROM定数という順序で宣言する必要があります。これらはその正確な順序で相互依存しています。
function init_rom return rom_type is variable rom_v : rom_type; variable angle : real; variable sin_scaled : real; begin for i in addr_range loop angle := real(i) * ((2.0 * MATH_PI) / 2.0**addr_bits); sin_scaled := (1.0 + sin(angle)) * (2.0**data_bits - 1.0) / 2.0; rom_v(i) := to_unsigned(integer(round(sin_scaled)), data_bits); end loop; return rom_v; end init_rom;
この関数はrom_vなどの便利な変数を使用します。これは配列のローカル・コピーで、正弦値で埋めていきます。サブプログラム内でforループを使ってアドレス範囲をイテレーションし、各ROMスロットで前述の式を使って正弦値を計算。最後に、すべての正弦サンプルを含むrom_v変数を返します。
forループの最後の行の整数変換が、data_bitsジェネリックを31ビットに制限した理由です。より大きなビット長ではオーバーフローします。
constant rom : rom_type := init_rom;
init_rom関数定義の下で、romオブジェクトを定数として宣言します。ROMは書き込まないRAMなので、これは完全に正当です。ここで関数を使用し、init_romを呼んで初期値を生成します。
ROMプロセス
正弦波ROMファイルの唯一のロジックは、以下にリストされた非常にシンプルなプロセスです。シングル・リード・ポートを持つブロックRAMを記述します。
ROM_PROC : process(clk) begin if rising_edge(clk) then data <= rom(to_integer(addr)); end if; end process;
トップ・モジュール
この設計は、前回のブログ記事で紹介したPWMプロジェクトの続きです。リセット・モジュール、PWMジェネレーター・モジュール、フリー・ランニング・クロック・サイクル・カウンター(ノコギリ波カウンター)モジュールがあります。これらのモジュールの動作は先週の記事を参照してください。
以下の図は、トップ・モジュール内のサブモジュール間の接続を示しています。

以下のコードはトップVHDLファイルの宣言領域を示しています。先週のPWM設計では、duty_cycleオブジェクトはcntカウンターのMSBへのエイリアスでした。しかし今回は正弦波ROMの出力がデューティ・サイクルを制御するため、実際の信号に置き換えました。また、カウンターのMSBをaddrという新しいエイリアスで作成しました。これをROMのアドレス入力として使用します。
signal rst : std_logic; signal cnt : unsigned(cnt_bits - 1 downto 0); signal pwm_out : std_logic; signal duty_cycle : unsigned(pwm_bits - 1 downto 0); -- カウンターのMSBを正弦波ROMアドレス入力に使用 alias addr is cnt(cnt'high downto cnt'length - pwm_bits);
以下のリストで、新しい正弦波ROMをトップ・モジュールでインスタンス化する方法を示します。RAMの幅と深さをPWMモジュールの内部カウンター長に合わせて設定。ROMのdata出力がPWMモジュールのduty_cycle入力を制御します。duty_cycle信号の値は、RAMスロットを順に読み出すことで正弦波パターンを描きます。
SINE_ROM : entity work.sine_rom(rtl) generic map ( data_bits => pwm_bits, addr_bits => pwm_bits ) port map ( clk => clk, addr => addr, data => duty_cycle );
正弦波ROMのシミュレーション
以下の画像は、ModelSimでのトップ・モジュール・シミュレーション波形を示しています。符号なしduty_cycle信号の表示をアナログ形式に変更して、正弦波を観察できるようにしています。

トップレベル出力のled_5がPWM信号を伝え、外部LEDを制御します。デューティ・サイクルが上昇または下降しているとき、出力は急速に変化します。しかし波が正弦波の頂点にあるとき、led_5は安定した'1'になります。波が谷底にあるとき、出力は一時的に'0'のままです。
自宅で試してみませんか? 以下のフォームにメールアドレスを入力して、VHDLファイルとModelSim/iCEcube2プロジェクトを入手してください!
FPGAでのLEDブリージング実装
Lattice iCEcube2ソフトウェアを使用して、iCEstick FPGAボードに設計を実装しました。上記のフォームからプロジェクトをダウンロードして、お手持ちのiCEstickで試してみてください!
以下のリストは、iCEcube2に付属のSynplify Proソフトウェアが報告したリソース使用量です。設計が1つのブロックRAMプリミティブを使用していることがわかります。これが正弦波ROMです。
Resource Usage Report for led_breathing Mapping to part: ice40hx1ktq144 Cell usage: GND 4 uses SB_CARRY 31 uses SB_DFF 5 uses SB_DFFSR 39 uses SB_GB 1 use SB_RAM256x16 1 use VCC 4 uses SB_LUT4 65 uses I/O ports: 7 I/O primitives: 7 SB_GB_IO 1 use SB_IO 6 uses I/O Register bits: 0 Register bits not including I/Os: 44 (3%) RAM/ROM usage summary Block Rams : 1 of 16 (6%) Total load per clock: led_breathing|clk: 1 @S |Mapping Summary: Total LUTs: 65 (5%)
iCEcube2で設計をルーティング後、.binファイルはled_breathing_Implmnt\sbt\outputs\bitmapフォルダ内、Lattice_iCEcube2_projプロジェクト・ディレクトリの中にあります。
iCEstickユーザー・マニュアルに従って、Lattice Diamond Programmer StandaloneソフトウェアでFPGAをプログラミングします。実際に試したところ、以下のGIFアニメーションのように、LEDの光強度が正弦波パターンで振動します。想像力を働かせると、LEDが「呼吸」しているように見えます。

まとめ
事前計算された正弦値をブロックRAMに格納するのは非常に簡単です。しかし、この手法が限られたX軸とY軸分解能の正弦波にしか適さないいくつかの制限があります。
まず思い浮かぶのは、前述の32ビット整数値の制限です。正弦値をより賢く計算すればこの問題を克服できると思いますが、それだけではありません。
ROMアドレスを1ビット拡張するごとに、RAM使用量が倍増します。X軸で高精度が必要な場合、より大きなFPGAでもRAMが足りない可能性があります。
Y軸正弦値に使用するビット数がネイティブRAM深度を超えると、合成ツールは追加のRAMやLUTを使ってROMを実装します。Y精度を上げるにつれて、リソース予算をより多く消費します。
理論的には、正弦波の1象限だけを格納すれば十分です。したがって、有限状態機械(FSM)でROM読み出しを制御すれば、RAM使用量を4分の1に削減できます。X軸とY軸の4つの順列すべてで正弦象限を反転させ、ブロックRAMに格納された単一象限からフル正弦波を構築します。
残念ながら、4つのセグメントを滑らかに接続するのは困難です。正弦波の頂点と谷底の接合部で2つの等しいサンプルがあると、正弦波にフラット・スポットが生じデータが歪みます。正弦波の精度向上のために象限のみ格納する目的を、ノイズ導入が台無しにします。
VHDL
- フリクションレスなユーザー体験の構築:UXデザインのブループリント
- OPC UAクライアント経由でPLCファンクションブロックを呼び出し:実践ガイド
- デジタル製造が現代医療の解剖学的モデリングを革新する
- 初めてのAWS CloudFormationテンプレート構築:実践ガイド
- VHDLで文字列リストを作成する: ベスト・プラクティスと実装例
- Tcl駆動テストベンチでVHDLコード・ロック・モジュールを検証する
- VHDLでPWMコントローラを実装: 設計、シミュレーション、FPGAデモ
- VHDLのTEXTIOライブラリでテキスト・ファイルからFPGAブロックRAMを初期化する
- VHDLで自己検証テストベンチを構築する実践ガイド
- VHDLで保護型とアクセスポインタを用いた動的リンクリスト実装
- Javaオブジェクト配列の作成:ステップバイステップガイド