目次
動機・前提・価値
FPGA開発をしていると、だいたい誰しも一度は思うやつがあります。
「AXI4-Liteのレジスタ定義、毎回コーディングするのだるくない?」
私は毎回思っています、goldear@JiN_C125です。
ベンダが提供しているIPを使えば一発で終わるケースもありますが、業務用途では、そう単純に割り切れないことも多いです。レジスタ構成が独特だったり、途中で仕様変更が入ったり、内部仕様をきちんと把握しておきたかったりと、最終的に自作する判断になる場面は少なくありません。
また、ベンダIPをそのまま使う場合、ライセンスや依存関係の問題が出てくることもあります。特に「後から別のデバイスに持っていけない」「そのまま使い回せない」という点は、業務では意外と効いてきます。その結果、「中身が分かっていて、自分でコントロールできるもの」を用意する、という判断になることも多いです。
さらに、手書き実装を減らしたい理由は、単に面倒だからというだけではありません。定型的な処理をスクリプト化することで、品質を安定させたり、実績のある構成を再利用しやすくしたり、検証コストを下げたりといった効果が期待できます。
こうした背景から、「一度作って終わりではなく、何度も使い回せる形でレジスタ実装を用意したい」という動機が生まれました。
そこで、Pythonを使ってAXI4-Lite対応のレジスタモジュールを自動生成するツールを用意しました。
この記事では、そのツールと、生成されたコードを最低限検証するための環境について紹介します。
本記事で紹介しているツールはこちらです。
- AXI4-Lite対応レジスタモジュール生成ツール:GN-gen_reg_module
- 自動生成したVerilogを確認するための簡易テスト環境:GN-hdl_axi4l_test
目的と設計思想
このツールを作るにあたって、目指したのは「とにかく何でもできるAXI4-Lite IP」を用意することではありません。むしろ、用途をかなり絞った、割り切った設計にしています。
狙っているのは、「制御レジスタ用途として、よくある構成を、安定して量産できること」です。
AXI4-Liteのレジスタ実装自体は、仕様を理解してしまえば難しいものではありません。実際にやっていることは、主に次のような処理の組み合わせです。
- アドレスデコード
- Write時のレジスタ更新
- Read時のデータ返却
- リセット時の初期化
問題になるのは、これらを「毎回」「手で」書く点です。レジスタ数が増えたり、ビットフィールドが細かくなったり、途中で仕様変更が入ったりすると、同じようなコードを何度も書くことになります。
そこで、このツールでは役割をはっきり分けています。開発者は「何を作りたいか」を定義し、定型的な処理の展開はスクリプトに任せる、という考え方です。
- 開発者はレジスタ仕様を定義する
- スクリプトがその定義に従ってVerilogを生成する
- 定型処理を自動化し、実装ミスを減らす
この形にすることで、単に「楽になる」だけでなく、次のような効果も狙っています。
- 実装のばらつきを減らす
- 実績のある構成を横展開しやすくする
- 検証コストを抑える
つまり、このツールの目的は「AXI4-Lite IPを作ること」そのものではなく、「制御レジスタ実装を、安定した形で再利用できる状態にすること」です。
そのために、Pythonでレジスタ仕様を記述し、それをもとにAXI4-Lite対応のVerilog-HDLを自動生成する、という構成を採用しました。
何が作られるのか
このツールで生成されるのは、「AXI4-Liteに対応した制御レジスタ用のモジュール」です。
ここで重要なのは、IP全体を丸ごと生成するわけではない、という点です。実際の開発では、多くの場合、IPは次のような構成になります。
- 実際の処理を行うロジックモジュール
- 制御・ステータス用のレジスタモジュール
今回のツールが対象としているのは、このうちの「レジスタモジュール」の部分です。CPUやSoC側からAXI4-Lite経由でアクセスされる制御・ステータスレジスタを担当する、いわば“窓口”のような役割を持つブロックになります。
以下の図は、今回想定しているIP構成のイメージです。
この図では、IP Wrapperの中に「Logic module」と「Register module」が含まれています。上側のLogic moduleは、実際の処理を担当する部分で、たとえばAXI4-Streamなどのデータパスと接続されます。
一方、下側のRegister moduleが、今回のツールで生成される対象です。AXI4-Liteに接続されるBus interfaceと、レジスタ本体であるRegister Coreで構成されており、ここが制御・ステータスの受け渡しを担当します。
つまり、このツールは、
- AXI4-Lite slaveとしてのバスインタフェース部分
- 制御・ステータス用のレジスタ群
をまとめて生成するためのものです。
Logic module側では、Register Coreと信号線で接続し、設定値を参照したり、ステータスを更新したりする形になります。レジスタアクセス周りの定型的な処理を切り出しておくことで、開発者は本来やりたい処理に集中できるようになります。
この構成を前提にしておくと、レジスタ構成を変更したい場合も、Register moduleだけを差し替えればよくなり、IP全体への影響を小さく抑えられます。
以降の章では、このRegister moduleがどのように生成されるのか、そして実際にどんなVerilogが出力されるのかを見ていきます。
ツール概要
GN-gen_reg_module は、AXI4-Lite対応の制御レジスタモジュールを自動生成するためのPythonツールです。
基本的な考え方はシンプルで、「レジスタ仕様を定義すること」と「その定義からVerilogを生成すること」を明確に分離しています。
本ツールは Python で実装しているため、Windows / Linux のどちらの環境でも動作します。特定のOSや開発環境に依存しないため、プロジェクトやチーム構成が変わっても使い回しやすい点が特徴です。
開発者が行うのは、レジスタ構成を仕様として定義することだけです。
具体的には、次のような情報を記述します。
- レジスタの個数
- 各レジスタのアドレス配置
- フィールドのビット幅
- Read / Write の可否
これらの情報をもとに、スクリプト側でAXI4-Lite slaveとして必要になる定型処理を組み立て、Verilog-HDLのソースコードを出力します。
自動生成されるのは、以下のような部分です。
- AXI4-LiteのRead / Writeハンドシェイク処理
- アドレスデコード
- レジスタ更新ロジック
- リセット時の初期化処理
これらは構造がほぼ固定で、毎回同じようなコードになりがちな部分です。このツールでは、そうした定型処理をスクリプト側に寄せることで、開発者が書くコード量をできるだけ減らしています。
また、生成されるVerilogは、ブラックボックス化されたものではありません。通常のHDLとして読める形になっており、生成後に内容を確認したり、必要に応じて修正したりできることを前提にしています。
- Verilogとして読める
- デバッグできる
- 必要なら手で調整できる
「生成されたから触れない」という前提ではなく、雛形として使い、必要に応じて調整するという距離感を想定しています。
このツールが対象としているのは、制御用・ステータス用の単純なレジスタアクセスです。AXI4(フル)や、複雑なトランザクションを前提とした用途は想定していません。
AXI4-Liteで制御レジスタをさっと用意したい、という用途にちょうどいいレベル感を狙っています。
ここまでが「このツールがどういう思想で、何を生成するのか」の全体像です。
次の章では、実際にこのツールを使って、どのようにVerilogを生成するのかを見ていきます。
使い方(クイックスタート)
ここでは、GN-gen_reg_module を実際に使って「制御レジスタモジュール」を生成するまでの流れを、サンプル付きで紹介します。 本ツールは Python で実装されており、Windows / Linux のどちらの環境でも動作します。特別なライブラリは不要で、標準的な Python 環境があればそのまま実行できます。
実際のソースコードやサンプルは、以下のGitHubリポジトリに置いています。
まずはリポジトリを取得し、用意されている example を動かしてみます。
サンプルの取得
以下のコマンドでリポジトリを取得します。
$ cd GN-gen_reg_module
サンプルの雛形は example/ ディレクトリにあります。この中にある CSV ファイルがレジスタ仕様の雛形で、同じディレクトリ内に、その CSV から生成された Verilog ソースコードも置かれています。
レジスタ仕様(CSV)の書き方
CSV ファイルは、レジスタ構成を定義するための雛形です。
例えば、次のような形式になっています。
name,addr,bit,reset,rw CTRL,0x00,0,0x0,RW MODE,0x04,2:1,0x0,RW STATUS,0x08,0,0x0,RO
各項目の意味は以下の通りです。
- name:レジスタ名
- addr:レジスタのアドレス(AXI4-Liteのオフセット)
- bit:ビット位置、またはビット範囲
- reset:リセット時の初期値
- rw:Read / Write 属性(RW / RO / WO など)
この CSV を編集することで、生成されるレジスタ構成を変更できます。
スクリプトを実行して Verilog を生成
CSV を用意したら、スクリプトを実行して Verilog-HDL を生成します。
$ python3 gn_gen_reg.py ../example/gn_regmap_sample.csv –base gn_sample –outdir ./src
for Windows
> py -3 gn_gen_reg.py ..\example\gn_regmap_sample.csv –base gn_sample –outdir .\src
実行すると、指定した仕様に沿った AXI4-Lite 対応のレジスタモジュールが生成されます。
example ディレクトリには、あらかじめ生成済みの Verilog も置かれています。
生成されるファイル
生成される Verilog には、以下のようなファイルが含まれます。
- AXI4-Lite レジスタモジュール本体
- レジスタ定義用のヘッダファイル
- 補助的なユーティリティファイル(必要に応じて)
これらは、そのまま Vivado などの FPGA 開発環境に組み込んで使用できます。
生成物の使い方
生成されたレジスタモジュールは、IP Wrapper 内に組み込み、Logic module と接続して使用します。
- AXI4-Lite バスに接続
- 制御・ステータス用のレジスタとして利用
- Logic module 側から設定値・状態を参照
この関係は、IP構成のイメージ通りです。
青色で示した Register module 部分を、このツールで生成した Verilog に置き換える形になります。
ここまでが、「どうやって使うのか」の全体像です。
次の章では、実際のサンプルCSVと、そこから生成されたVerilogを見ながら、
「どの指定が、どのコードに反映されるのか」を具体的に確認していきます。
CSVと生成Verilogの対応(サンプルで確認)
ここまでで、「どういう思想で」「どうやって使うか」を説明してきました。
この章では、実際に CSVで書いたレジスタ仕様が、どのようにVerilogに反映されるのか を、サンプルを使って具体的に確認します。
GN-gen_reg_module は、CSVでレジスタ仕様を定義し、その内容に従って reg_core(制御レジスタ本体)を生成します。ここでは、サンプルCSVと、生成された gn_sample_reg_core を使って「どの列が、どのコードに反映されるのか」を確認します。
CSVの列(今回のサンプル)
今回のCSVは、レジスタをフィールド単位で定義します。同じレジスタ(同じ name と offset)に対して、複数行でフィールドを積み上げる形式です。
- name:レジスタ名(例:REG_CTRL)
- offset:レジスタオフセット(例:0x0000)
- access:アクセス属性(RW / RO / WO / W1C)
- reset:リセット値(32bit)
- field:フィールド名(例:MODE)
- lsb / msb:フィールドのビット範囲
- desc:説明(生成コードのコメントに反映)
「仕様として読めるコメント」の生成
生成された reg_core の冒頭には、CSV内容がレジスタ単位に整理されたコメントとして出力されます。仕様と実装の対応を追いやすいので、地味に便利です。
// REG_CTRL @0x0000 [RW] reset=32'h00000000 // - ENABLE[0]: Enable block // - MODE[3:1]: Mode select // - SOFT_RST[4]: Software reset (self-clearing by SW) // // REG_CFG @0x0004 [RW] reset=32'h00000010 // - DIV[7:0]: Clock divider // - THRESH[15:8]: Threshold // // REG_STATUS @0x0008 [RO] reset=32'h00000001 // - READY[0]: Ready status // - BUSY[1]: Busy status // - ERR[8]: Error flag // // REG_IRQ_CLR @0x0010 [W1C] reset=32'h00000000 // - IRQ_CLR[7:0]: Write-1-to-clear interrupt bits // // REG_TX_DATA @0x0014 [WO] reset=32'h00000000 // - DATA[31:0]: Write-only TX data
offset → ADDR定数 → アドレスデコード
CSVの offset は、生成コードではアドレス定数(localparam)として反映されます。ここが Read/Write の分岐にそのまま使われます。
localparam [AXI_ADDR_W-1:0] ADDR_REG_CTRL = 0; localparam [AXI_ADDR_W-1:0] ADDR_REG_CFG = 4; localparam [AXI_ADDR_W-1:0] ADDR_REG_STATUS = 8; localparam [AXI_ADDR_W-1:0] ADDR_REG_IRQ_STATUS = 12; localparam [AXI_ADDR_W-1:0] ADDR_REG_IRQ_CLR = 16; localparam [AXI_ADDR_W-1:0] ADDR_REG_TX_DATA = 20;
name → レジスタ実体とLogic側への出力
CSVの name は、レジスタ変数名(r_REG_CTRL など)と、Logic module 側へ渡す参照出力(w_REG_CTRL_o など)に反映されます。
reg [AXI_DATA_W-1:0] r_REG_CTRL; reg [AXI_DATA_W-1:0] r_REG_CFG; reg [AXI_DATA_W-1:0] r_REG_STATUS; reg [AXI_DATA_W-1:0] r_REG_IRQ_STATUS; reg [AXI_DATA_W-1:0] r_REG_IRQ_CLR; reg [AXI_DATA_W-1:0] r_REG_TX_DATA; assign w_REG_CTRL_o = r_REG_CTRL; assign w_REG_CFG_o = r_REG_CFG; assign w_REG_STATUS_o = r_REG_STATUS; assign w_REG_IRQ_STATUS_o = r_REG_IRQ_STATUS; assign w_REG_IRQ_CLR_o = r_REG_IRQ_CLR; assign w_REG_TX_DATA_o = r_REG_TX_DATA;
access → Write更新ルールと Read返却ルール
CSVの access は、生成コードの挙動を決める最重要項目です。今回のサンプルでは RW / RO / WO / W1C を扱っています。
- RW:Writeで更新し、Readで返却します。
- RO:Writeでは更新せず、Readで返却します(値の更新は別ロジック側で行う想定)。
- WO:Writeで更新しますが、Readでは返却しません(0を返すように生成されています)。
- W1C:Write時に「1を書いたビットだけをクリア」します(IRQクリア系でよくある挙動)。
補足: reg_core は、アクセス属性に基づく「更新ルール」だけを実装します。
self-clearing などの意味的な振る舞いは含まず、必要な場合はソフトウェア側や Logic module 側で制御する想定です。
RWの例:reset と Write更新(wr_mask 対応)
RWレジスタでは、CSVの reset が初期化に反映され、Write時には wr_mask を使って部分更新できる形になっています。
// REG_CFG (RW)
always @(posedge clk) begin
if (!reset_n) begin
r_REG_CFG = 32'h00000010;
end else begin
if (wr_en && (wr_addr == ADDR_REG_CFG)) begin
r_REG_CFG = (r_REG_CFG & ~wr_mask) | (wr_data & wr_mask);
end
end
end
ROの例:Write更新を生成しない
ROレジスタは「Writeで更新しない」ことがコードで明確になります。Read mux 側では返却対象として残るため、外部のステータス反映ロジックと組み合わせて使う想定です。
// REG_STATUS (RO)
always @(posedge clk) begin
if (!reset_n) begin
r_REG_STATUS = 32'h00000001;
end else begin
// RO: no write update
end
end
W1Cの例:「1を書いたビットだけクリア」
IRQ系でよく使う W1C は、Write時の更新式が変わります。マスク後の書き込みデータで、該当ビットをクリアする形になっています。
// REG_IRQ_CLR (W1C)
always @(posedge clk) begin
if (!reset_n) begin
r_REG_IRQ_CLR = 32'h00000000;
end else begin
if (wr_en && (wr_addr == ADDR_REG_IRQ_CLR)) begin
r_REG_IRQ_CLR = r_REG_IRQ_CLR & ~(wr_data & wr_mask);
end
end
end
WOの例:Readは0返却
WOレジスタは書き込みはできますが、Readでは0を返すように生成されています(「読めない」ことがコード上でも分かります)。
// Read mux (combinational)
always @(*) begin
rd_data = {AXI_DATA_W{1'b0}};
case (rd_addr)
ADDR_REG_CTRL: rd_data = r_REG_CTRL;
ADDR_REG_CFG: rd_data = r_REG_CFG;
ADDR_REG_STATUS: rd_data = r_REG_STATUS;
ADDR_REG_IRQ_CLR: rd_data = r_REG_IRQ_CLR;
ADDR_REG_TX_DATA: rd_data = {AXI_DATA_W{1'b0}};
default: rd_data = {AXI_DATA_W{1'b0}};
endcase
end
Hit判定:存在しないアドレスを弾く
生成された reg_core は、アクセスが有効なアドレスかどうかを wr_hit / rd_hit として出力します。上位のBus interface側でレスポンスを決めたり、デバッグに使えます。
// Hit decode (combinational)
always @(*) begin
wr_hit = 1'b0;
case (wr_addr)
ADDR_REG_CTRL: wr_hit = 1'b1;
ADDR_REG_CFG: wr_hit = 1'b1;
ADDR_REG_STATUS: wr_hit = 1'b1;
ADDR_REG_IRQ_STATUS: wr_hit = 1'b1;
ADDR_REG_IRQ_CLR: wr_hit = 1'b1;
ADDR_REG_TX_DATA: wr_hit = 1'b1;
default: wr_hit = 1'b0;
endcase
end
field / lsb / msb はどう反映される?
今回のサンプル生成物では、Write更新は「レジスタ全体を wr_mask で部分更新」する形になっています。つまり field/lsb/msb は、レジスタ単位のコメントとして明確に出力され、実装側は wr_mask によってビット単位の更新を実現しています。
例えば REG_CTRL の MODE(lsb=1, msb=3)は、Write時に wr_mask[3:1] を立てたデータを書けば、その範囲だけが更新されます。フィールド更新を個別に展開するのではなく、マスク更新に寄せてシンプルにしているのが、この生成コードの特徴です。
検証
コードを自動生成する仕組みを用意すると、次に気になるのが「本当に正しく動くのか」という点です。見た目がそれっぽいVerilogが出てきても、実際にRead / Writeが成立しなければ意味がありません。
そこで、生成されたAXI4-Lite slaveが、制御レジスタとして最低限使えるかどうかを確認するために、簡易的なテスト環境として GN-hdl_axi4l_test を用意しました。
ここでやっているのは、いわゆる「ガチの検証」ではありません。UVM環境を組んだり、網羅的なランダムテストを回したり、といったレベルのものではなく、あくまで「制御レジスタとして成立しているか」を確認することにフォーカスしています。
具体的には、次のような点だけをチェックしています。
- Writeアクセスが正しく反映されるか
- Readアクセスで期待した値が返るか
- アドレスデコードが仕様通りになっているか
このレベルの検証を用意しておくだけでも、生成ロジックを変更した際に「基本動作が壊れていないか」をすぐに確認できるようになります。
また、自動生成系のツールは、一度不具合が入ると影響範囲が広がりがちです。そういう意味でも、軽いテスト環境をセットで持っておくことには実用上の価値があります。
- 明らかなバグを早い段階で検出できる
- 仕様変更時の確認コストを下げられる
- 生成スクリプトを触る心理的ハードルが下がる
ここでは、「完璧な検証」を目指すのではなく、「安心して使える状態かどうか」を確認できるラインを狙っています。
今回の簡易テストでは、以前まとめたシミュレーション環境をそのまま流用しています。環境構築の詳細については、以下の記事を参照してください。

まとめ
今回は、AXI4-Lite対応の制御レジスタモジュールをPythonで自動生成するツールと、その基本動作を確認するための簡易テスト環境について紹介しました。
この仕組みは、レジスタ仕様を定義し、定型的なAXI4-Lite slaveのコードを自動生成し、最低限のRead / Write動作を確認する、という流れに集約されます。
- レジスタ仕様を定義する
- AXI4-Lite slaveのレジスタモジュールを生成する
- 基本動作を簡易テストで確認する
VivadoのIPカタログで完結するケースも多く、そうした運用を否定する意図はありません。一方で、制御レジスタを何度も実装する開発では、定型処理の作り込みや再利用性が効いてきます。
- 定型処理の実装ミスを減らせる
- 構成を揃えやすく、横展開しやすい
- 仕様変更への追従がしやすい
- 立ち上げ時の作業コストを下げられる
今回のツールは、「AXI4-Liteで制御レジスタを作る」という用途に絞っているので、万能ではありません。その代わり、よくある構成を手早く用意したい場面では、役に立つはずです。
もはや仕様書みたいになっていますが、私の備忘録ということで参考までにどうぞ。



コメント