はじめに
ソフトウェアエンジニア一本でやってたのですが、急にFPGAの開発をやるようになっちゃいました。
そこで、勉強の意味も込めて趣味でもFPGAやってやろうと思い。
記念すべき1回目はFPGAでマトリクスLEDを制御してみようです。
やっぱり組み込みといえばまずはLチカですよね!
開発環境
- 評価ボード周り : Tang Nano 9k, MAX7219(4連8x8マトリクスLED)
- IDE : Gowin EDA
→GowinEDAで書き込みまでできるのだが、不安定らしいので書き込みツールは別で用意している(ただしデバッグができないのは困る) - 言語 : SystemVerilog
- シミュレーションツール : Icarus Verilog(iverilog)
- 波形表示ツール : gtkwave v3.3.104
- 書き込みツール : openFPGALoader v0.10.0
- ホストマシン : macOS(Big Sur 11.7.4)
- 仮想マシン : VirtualBox7.0(Ubuntu 22.04.2)
→macOSはGowinEDA非対応なためLinux環境を構築して動かしている
※特に難しくはない&長くなるため、今回の記事ではインストール方法を全て端折ります。
Tang Nano 9kについて
sipeed社(中国)が開発した安価(税込3000円ぐらい)なFPGAボードとなります。
安価な割には一通りの機能が搭載されています。
標準で32MbitsのFlashは搭載しているし、USB-JTAGもついているので便利ですね。
以下にwikiがあるので色々載っています。
https://wiki.sipeed.com/hardware/en/tang/Tang-Nano-9K/Nano-9K.html
MAX7219について
まずはMAX7219の制御方法を確認しないと始まらないので確認する。
以下がデータシート。
https://pdfserv.maximintegrated.com/jp/ds/1339J.pdf
うーん、とりあえず制御ピンは「MOSI」と「CLK」と「LOAD」か。
データを流している間はLOADをLowにする。
で、そのデータフォーマットは1データが16bit幅。
上位4bitが無効、その次の4bitでアドレス、最後の8bitでデータを格納している。
特に難しくはないな。ただ、わからない点が
①上記のデータフォーマットが8x8のLEDにどう紐づいているのかがピンとこない
②初期設定周りをどのような順番で設定すればいいのか
なので他の人の記事を見てみた。
https://lawn-tech.jp/dotmatrix.html
①について
こういうことらしい。
データがrowの8bit幅(4連なので32bit)でアドレスがcolの8ラインになる。
なので制御する時は2次元配列で持った方が便利そうかも。
例)1ライン目のみを点灯させたい場合は以下のコマンドを投げる
0x01FF 0x01FF 0x01FF 0x01FF
※2ライン目以降は全て"0"を投げればいい
※CS(LOAD)の制御は1ラインごとにLowに落としてあげる
②について
LEDが4つ連結しているので、4回分コマンドを送る必要あるらしい。
送るコマンドは「Decode Mode」「Intensity」「Scan Limit」「ShutDown」
の4つで大丈夫そう。流れとしては以下。
Decode Mode x4
↓
Intensity x4
↓
Scan Limit x4
↓
ShutDown x4
設計
MAX7219の使い方が判明したので設計していこう。※GowinEDAの細かい使い方は別の記事で掲載しようと思ってます。
以下の流れで設計する。
仕様決め→基本設計(ブロック図、ピン配置)→詳細設計(ブロック単位、状態遷移、タイミングチャート)→実装
1.仕様
・MAX7219の動作クロックは9MHzとする※1
・FPGAのLED制御部の内部動作クロックは9MHzとする※2
・LEDの点灯データは内部生成とする
・LEDの点灯パターンは左スイープのアニメーションのみとする
※1 最大周波数は10MHzまでだが、pllの精度の問題で10MHzピッタシが作れないため9MHzとしている(設定次第では作れるかも。要調査。)
※2 簡略化のため内部動作クロックもMAX7219と同じにしている
まぁざっくりとこんな感じでいいか。
2.基本設計
使用するピンも見えてきたので次はピン配置を決めよう。
正式なピン配は後で説明するためここまでで一旦終わり。
ピン番号 | 信号名 | in/out | IO_TYPE |
---|---|---|---|
4 | RST_N | in | LVCMOS33 |
25 | CLK_9M | out | LVCMOS33 |
26 | CS | out | LVCMOS33 |
27 | DOUT | out | LVCMOS33 |
52 | CLK_27M | in | LVCMOS33 |
3.詳細設計
次にモジュールごとの詳細を決めていく。
■pll
・27MHzを入力とし、9MHzを出力する。
・pllの出力が安定したら動いて欲しいので、lock信号解除をmatrix_topのリセット信号として使用する。
信号名 | in/out | ビット幅 | 説明 |
---|---|---|---|
clkin | in | - | 27MHzを入力 |
reset | in | - | ActiveLowのリセット入力 |
clkout | out | - | 9MHzを出力 |
lock | out | - | Lowで解除 |
GowinのIP(rpll)を使用する。
39Pを参照。
http://cdn.gowinsemi.com.cn/UG286J.pdf
■matrix_top
・matrix_pgとmatrix_coreを管理する。
こいつは特に説明することがないのでこの辺で。
信号名 | in/out | ビット幅 | 説明 |
---|---|---|---|
clk_9m | in | - | 9MHzを入力 |
pll_rst | in | - | ActiveHighのリセット入力 |
cs | out | - | LOADを出力 |
dout | out | - | シリアルデータ出力 |
■matrix_pg
・マトリクスLEDの送信データを作成する。
・送信データは左スイープのアニメーションとする。
・LEDのドライバーモジュールとはen⇆doneで送信データのタイミングを図る。
→前述でもあったとおり送信データの持ち方は2次元配列にしよう。
まずマトリクスLEDが8x8の4連なので1ラインは32bitの8ラインある。
で、1ラインの内訳はというと16bit(無効:4bit,アドレス:4bit,データ:8bit)でLED1個*4個。
つまり1ラインは64bit幅。それが8ライン必要。
・点灯間隔はとりあえず以下とする。
→人の目だと30Hz(33.33ms)ぐらいなら十分に点滅を感じられるらしい。それを基準に考えよう。そしたら点灯間隔は
111.11ns * 300000 = 33333000ns(約33.33ms)ぐらい空ければ大丈夫でしょ。まぁここは定数でもって調整できるようにするか。
信号名 | in/out | ビット幅 | 説明 |
---|---|---|---|
clk_9m | in | - | 9MHzを入力 |
pll_rst | in | - | ActiveHighのリセット入力 |
send_done | in | - | ドライバーからの送信完了通知 |
en | out | - | データ生成完了 |
dout | out | [63:0] | 送信データ64bit*8ライン |
・WAIT:リセット後の状態かつドライバーからの送信完了待ち状態
・PACKING;送信データを用意している状態
・DELAY:送信間隔調整状態
という感じに状態を持つことにする。
タイミングチャート
※信号名切れててすみません。end_doneはsend_doneで、elay_doneはdelay_doneです。
WAIT状態ではmatrix_coreからのsend_done信号を待っています。send_done信号がアサートされたら、PACKING状態へ遷移します。
PACKING状態では送信データをバッファリングし完了すると、en信号をアサートしmtrix_coreへ通知します。
その後再度WAIT状態へ遷移し、send_done信号を待ちます。
■matrix_core
・MAX7219へデータを送信する(データはmatirx_pgで生成)。
・パターンジェネレータージュールとはen⇆doneで送信データのタイミングを図る。
信号名 | in/out | ビット幅 | 説明 |
---|---|---|---|
clk_9m | in | - | 9MHzを入力 |
pll_rst | in | - | ActiveHighのリセット入力 |
en | in | - | パターンジェネレーターからのデータ生成完了通知 |
din | out | [63:0] | 送信データ64bit*8ライン |
send_done | out | - | 送信完了 |
cs | out | - | LOAD信号 |
dout | out | - | シリアルデータ出力 |
このモジュールも状態遷移させて動かそう。
・INIT:リセット後の状態
・IDLE:パターンジェネレーターからの送信データ待ち状態
・SETTING:送信データ読み込み中
・SENDING:MAX7219へデータ送信中
タイミングチャート
※信号名切れててすみません。l_max_flgはcol_max_flgで、end_doneはsend_doneです。
INIT状態は起動時の状態又はリセット後の初期状態となります。
SETTING状態ではデータをバッファリングし、完了したら次にSENDING状態へ遷移する動きです。
SENDING状態ではdata_buffの中身をrowカウンタとcolカウンタで回し、MSB順にデータをMAX7219へ送信する動作となっています。
完了すればsend_done信号をアサートし、matrix_pgへ通知します。
その後IDLE状態へ遷移し、matrix_pgからのen信号を待ちます。
4.実装
ここにソースコードを載せると長くなるのでgithubを参照。
https://github.com/kiiisy/MatrixLed
次にとても大事なことが。最初に検討したピン配置をしていこう。
上記ソースコードのsrcフォルダの中に「xxx.cst」ファイルがあるのでそちらを用意しておく。
書き方は以下の通り。最低限以下を記載すればとりあえずは大丈夫かと、、、
IO_LOC "[信号名]" [ピン番号];
IO_PORT "[信号名]" IO_TYPE=[LVCMOSとかLVDS];
次はタイミング制約を記載していこう。
上記ソースコードのsrcフォルダの中に「xxx.sdc」ファイルがあるのでそちらを用意しておく。
今回はタイミング的に厳しいものはないので特にないが、1点必ず記載しないといけないことがある。
クロック生成だ。要はどの信号をどのようなクロック(デューティー比など)として扱うかをGowinに教えてあげないといけない。
書き方は以下の通り。今回の場合だと、27MHzと9MHzのクロックがあるのでそちらを記載する。
create_clock -name [信号名] -period [ns単位の時間] -waveform {[ns単位でのデューティー比]} [get_ports {[信号名]}] -add
これで設計は終了。
で、後は実機に焼いて、、、としたいところだが、その前にシミュレーションで動作確認をした方が安全。
大体1発目は動かないから(笑)。なので次は軽くシミュレーションをして波形を見ていく。
シミュレーション
シミュレーションツールはiverilogを使っていく。初めて使うのであまり使い方が分かりませんが、、、
あと、iverilogだけでは波形表示ができないので、波形表示ツールはgtkwaveを使います。
(この記事を書いていて思ったことが、iverilogってmacOS対応しているんですね。Ubuntu上で動かしていた、、、)
まずはシミュレーションで実行するシナリオを作成しよう!
先程のgitのsimフォルダの中のscenario.svがそれに該当する。
今回は入力データなどを入れる必要がないので、ただ単にリセットとクロック信号を入れて動かしているだけです。
ただし1点だけ注意したいことが。
シナリオファイル内に
$dumpfile("[ファイル名].vcd");
$dumpvars([信号表示したい階層(0なら全ての階層の信号)], [シナリオモジュール名]);
の記載は必要です。これがないと波形ファイルが作れないです。
ではiverilogの使い方です。
iverilog -g2012 -o [出力ファイル名].out -s [トップモジュール名] [.svファイル]
で、実行するとコンパイルが開始されて成功すると出力ファイルが作成される。
が、ここでエラーが返ってきてしまって1回つまってしまった。IPの実態がないよと。
まぁGowinのインストールしたフォルダの中にでもいるでしょと思って見に行ったらいないいない、、、
いないというのは語弊があるが、おそらくGowinはIPの実態を暗号化したデータ(.soファイル)で持ってそう。
これじゃ使えないと思って、諦めムードで他のフォルダを漁っていたら、simlibというフォルダを見つけてしまった。
そしたらこの中にシミュレーション用のIP達がverilogとvhdファイルであったのでこのファイルをコンパイルしたら
期待通りに動いた。ということでGowinのIPを使ってシミュレーションしたい場合は
/IDE/simlib/[型番]/prim_sim.v or prim_sim.vhd
を使用しましょう。
※これが正解かわかっていないので他に情報あれば教えていただけると助かります。
その後に以下のコマンドを打つと、[出力ファイル名].vcdファイルが作成される。
vvp [出力ファイル名].out
これで波形ファイルの準備は完了。
次は先程作った波形ファイルを表示させよう。
gtkwave [出力ファイル名].vcd
上記のコマンドで実施できる。これまた簡単。
じゃあ波形を見ていこうか。
・matrix_pg
※タイミングチャートと少し信号名が違います
うん、思ってた通りに動いている。OKでしょう!(点灯間隔は短くしてます)
・matrix_core
※タイミングチャートと少し信号名が違います
よし、これも期待通りに動いている。
と、まぁこんな感じでシミュレーションをしました。(実際は1発でうまいこと動いていません。何回か修正を加えました)
実機動作
波形で問題なさそうなら実機に焼いていきましょう。
設計の最後で作成した「xx.fs」ファイルがコンフィグファイルとなります。
これをopenFPGALoaderツールを使って書き込んでいきます。
※fsファイルは"Fuses file"の略らしい
まずはFPGAが認識していることを確認しましょう。
openFPGALoader --detect
上記のコマンドを送信したら以下の結果が返ってくるはずです。そしたら接続に問題はありません。
問題がないことを確認したら、次に以下のコマンドを送信し、書き込み開始です。
openFPGALoader -f ./xxx.fs
※ちなみに-fオプションを付けないと、Flashに書き込んでくれません。
なので電源断すると起動しなくなります。
write Flashが100%になり、最後にDoneと出れば書き込み完了です。
こんな感じに光りました。※動画を変換している都合、点灯間隔が実際よりは早くなっているように見えます。
さいごに
これでひとまずマトリクスLEDをFPGA制御で点灯させることができました。
マイコン(arduinoやラズパイ)で制御するならライブラリとかが用意されていてとても簡単ですが、
FPGAだとフルスクラッチで書かないと制御できませんね。
たかがLED制御ですがマイコンよりかは難易度があがりますね。
(そもそもFPGAの情報がweb上にあまりないっていうのもありますが、、、)
今回はFPGAのとてもいい勉強になったのでこれかものんびり続けていければなと。