はじめに
Gowin_PicoRV32の命令の実行方法は下記の3択になっております。
フラッシュメモリ 上でプログラムを実行
フラッシュメモリ でブートするが、その後ITCMでプログラムを動かす
ITCM上でプログラムを実行する
実行速度は3.が最も早く、1.が最も大きなプログラムが動かせます。
TangNano20KのITCMのデフォルトサイズは32KBで(実際にサイズの大きなプログラムを作る機会があるか?というのは置いておいて)、ちょっと込み入ったものを作るとすぐに枯渇してしまいます。
TangNano20KのBSRAMは32KB使った状態でもまだ余っているので、サイズを大きくできる余地があるとはいえ、そんなに大きくできるわけではありません。
じゃーフラッシュメモリ を使えばよいのではないか?という話になるのですが、読み出し速度がとてつもなく遅く、実行速度がガタ落ちになってしまいます。
というわけで、SDRAM 上に命令コードを置いて、かつキャッシュメモリ を挟めば、そこそこの速度を出しつつ大容量(8MB)の命令空間を確保できるのはないか?というのが発想です。
ただ今にして思えば、フラッシュメモリ にキャッシュメモリ を挟むだけでよかったのでは?とも思います。
課題
フラッシュメモリ からSDRAM へ命令コードを転送 して、かつSDRAM へPC(プログラムカウンタ)を移動 させる必要があります。メモリ配置でリンカをうまく使う必要がありそうで、その知識がないため難しそうです。
という慣れた人には、なんら課題でもない気がしますが、自分には大きな課題でした。
今回、IP版のPicoRV32には"フラッシュメモリ →ITCM"へ命令を移すという事が簡単 に出来てしまうので、ITCM部分をSDRAM へ変更すれば実現できるのはないか?と思ったからです。
ソースコード の解析とか色々やってますが、SDRAM 上で動かすための方法だけを知りたい方は、
ITCM実行部分をSDRAM領域へ差し替えを行う(ソフト面) へ飛んでください。
フラッシュからITCMへ命令実行を遷移する方法の解析
どのように実現しているかを前回使ったリファレンスデザインのソフトを解析してみました。
ブート方法をFLASH →ITCMとした場合には、loader.cのboot() から実行されます。
(config.hで BUILD_MODE == BUILD_BURN としている場合)
なぜboot()
だと特定したかは後ほど記載します。
_lma_ldsec_start などの変数はextern で参照されています。
本変数はリンカファイルsections.lds にて定義されています。
本変数がアドレスであることは、想像がつくかと思いますが、実アドレスの特定は難しいので一旦置いておいて、boot()で何を行っているかを見てみます。
おそらく_lma_ldsec_start から配置されているコードを_vsloader の示すアドレスへコピーしていることは分かります。またそのコピーサイズは(&lma_ldsec_end - & lma_ldsec_start) byteです。
また、
src_ptr += 0x4100000;
src_ptr_end += 0x4100000;
コピー元アドレスの算出のため、lma_ldsec_startに対し0x4100000を加算しています。
この値は4byte単位である(unsigned intでsrc_ptr変数を作っている)ので、0x4100000をbyte単位に変換すると、0x1040_0000になります。このアドレスはFLASH メモリのアドレスになります。
リンカファイルで定義されたアドレス情報を命令コードが置かれているFLASH メモリのアドレスへ変換しているのだと思います。
つまり、FLASH メモリに書き込まれた命令コードを、リンカファイルで定義されたアドレス** vsloader**へコピーしているのだろうと思います。
おそらく、リンカファイルで定義されたアドレスとはITCMのアドレスを示しているのだろうと大方予想が付きます。
読み進めると
__asm__ __volatile__("la t3, _vsloader");
__asm__ __volatile__("jr t3");
という記述が現れるので、_vsloaderのアドレスへbranchしていることが分かります。
つまりここでコピーしたコードの先へbranchしています。
ここでリンカファイルの方へ戻って、_lma_ldsec_end の定義を見てみます。
_lma_ldsec_end = _lma_ldsec_start + SIZEOF(.ldsec);
と書いてあります。コピーサイズは.ldsecのサイズ分だという事が分かります。
.ldsecのサイズはリンカファイルから
.ldsec : AT (_lma_ldsec_start)
{
_vsloader = .;
~
} >SMEM
の部分であることが想像は付きますが、具体的にどこのコード部分かが分かりません。
また、本sectionに_vsloader と記載があるので、先ほどのコピー先がこの位置であることが想像できます。
このセクションの最後のSMEM
というキーワードですが、リンカファイルの先頭で
SMEM (rxai!w) : ORIGIN = 0x02000000, LENGTH = 32K /* LENGTH based on hardware configuration of ITCM size */
と記載があります。 SMEMとはITCMの領域で先頭アドレスが0x02000000 だったのですね。
とはいえ、セクション.ldsecのスタートがITCMの先頭ではなく、SMEM領域の定義はこの部分より前に.text のセクションが存在しています。この.textセクションがITCMの先頭に配置されます。
そしてITCMが0x0200_0000から配置されています。
(本来はハード情報から、このmakefile を作る流れになりますが、私はmakefile が分からないので本来とは逆の順序で見ています)
_vsloader のアドレスの特定
make時に、ディスアセンブル リストが生成されます。生成場所は以下になります。
Gowin_PicoRV32_V1.3\ref_design\MCU_RefDesign\picorv32_demo\Debug\picorv32_demo.lst
このリストで、_vsloader のアドレスを特定することにします。
本ファイル中のvsloaderを検索すると私のディスアセンブル リストでは以下の様になっていました。
02001c34 g .ldsec 00000000 _vsloader
アドレス0x02001c34
が vsloderの先頭なので、disアセンブル リストから02001c34
を検索します。
void loader(void)
{
2001c34: 1101 addi sp,sp,-32
2001c36: ce22 sw s0,28(sp)
2001c38: 1000 addi s0,sp,32
…
上記のところにヒットします。関数loaderの先頭だという事が分かります。
関数loaderもコピー処理になります。きちんとは見ていないですが、おそらくFLASH の内容を同様にITCMへコピーして、ITCMのアドレスへブランチしているものと思われます。
リセット解除後、最初に実行される関数は?
最初に実行される関数は何でしょうか?
リンカのファイルにはそれらしい記述は、なかったです。正しいアドレスへプログラムが配置されればよいだけなので、もしかするとリンカは理解していないかもしれません。
ソフト面から追うのではなく、ハード面から追うことにしました。とにかくpicorv32のリセットベクタさえ分かればよいので、IDE がインストールされているIPが格納されているフォルダをなんとなく見てみました。
C:\Gowin\Gowin_V1.9.8.11_Education\IDE\ipcore\GowinPicoRV32\Gowin_PicoRV32\picosoc.v
がGOWIN_PicoRV32のTOPモジュールのようで、ここを見てみると
localparam [31:0] PROGADDR_RESET = `PROGADDR_RESET_DEF;
という記述があります。PROGADDR_RESET_DEF
が定義されているところを探すと
同フォルダのpico_config.vh
なるファイルにて定義されていました。
`define PROGADDR_RESET_DEF 32'h1040_0000
とあるのでリセットベクタはアドレス0x1040_0000
らしい事が分かりました。
このあたりのハード情報が記載されているドキュメントを見つけることが出来ませんでした。
分かりやすいところに記載があると良いのですが。。。
ここで逆アセンブル リストでアドレス10400000
を探すと、以下を見つけることが出来ました。
void boot(void)
{
10400000: 1101 addi sp,sp,-32
10400002: ce22 sw s0,28(sp)
10400004: 1000 addi s0,sp,32
…
つまりリセット解除後、最初に実行される関数はboot()
関数のようです。
リンカファイルsections.lds
でも
FLASH (rxw) : ORIGIN = 0x10400000, LENGTH = 1M
となっていてアドレスが同じですね。
リンカはどうやってアドレスを決めているか?
大体Cのソースコード の流れは把握しました。
リンカはどのようにしてアドレスを決めているのでしょうか?
改めて、 リンカファイルsections.lds
へ目を向けると
ENTRY(start)
の後に
SECTIONS {
_lma_btsec_start = 0;
.btsec : AT (_lma_btsec_start)
{
_vsbtsec = .;
KEEP(*.o(.btsec))
. = ALIGN(4);
} >FLASH
_lma_btsec_end = _lma_btsec_start + SIZEOF(.btsec);
大きなSECTIONS
の中に.btsec
というセクションが切られています。
そして、アドレスは_lma_btsec_start
であると記載され、_lma_btsec_start=0
と書かれています。
また本セクションはFLASH に割り付けらています。
つまり0x10400000
をベースとするアドレス0番地が.btsec
でありFLASH メモリであると分かります。
そして.btsec
はCソースコード のどこを指すかというとloader.c
に以下の様な記述があります。
void boot(void) __attribute__((section(".btsec")));
void loader(void) __attribute__((section(".ldsec")));
ここでboot(void)
は.btsec
セクションであると定義されています。
また。loader(void)
は、.ldsec
セクションであると定義されています。
リンカファイルsections.lds
へ戻って続きを読むと 、.ldsecセクションは.text
の後ろに定義されています。
また.ldesc, .text
両セクションともSMEM領域であるとも記載されています。
.text
セクションがSMEM領域の先頭と定義されているので、つまりココが、ITCMの先頭アドレスになります。
.text
セクションはどのソースかというと、逆アセンブル リストによるとstart.S
になります。
02000000 <_vstext>:
reset_vec():
<ソース配置フォルダ>\Gowin_PicoRV32_V1.3\ref_design\MCU _RefDesign\picorv32_demo\Debug/../src/start.S :26
.section .text
.global irq
きちんとアドレスと関数の関係及び、実行するアドレス順序をまとめたい気がしますが、目的はプログラムをSDRAM で実行させることで、ここまでの理解で十分だと思うので、細かく追うのはここまでにします。
ITCM実行部分をSDRAM 領域へ差し替えを行う(ソフト面)
boot()が実行されるアドレス(0x200_0000番地でした)をSDRAM のアドレスへ書き換えればよいと分かります。
より簡単に実現するために、単にITCM空間をSDRAM 空間へ差し替えます。
よってリンカファイルsections.lds
を以下の様に書き換えます。
SMEM (rxai!w) : ORIGIN = 0x30000000, LENGTH = 1M /* LENGTH based on hardware configuration of ITCM size */
SDRAM 領域は0x3000_0000から1Mbyte分用意することにしました。(後ほど、ハード情報を記載します)
ITCMは使わない設定にします。
但し、GOWIN_PicoRV32をインスタンス する際に気が付くと思いますが、ITCMのサイズは0に出来なくて、最小8KB作る必要があるようです。(今回はこの8KBは全く使わない、もったいないRAMになってしまいました)
スタックポインタの設定を行う(ソフト面)
サンプルデモではスタックを使わないのかスタックポインタの設定がされていませんでした。
loader.c
のloader(void)関数
でスタックポインタを設定します。
__asm__ __volatile__("la t3, _vstext");
の行の手前に以下を追記します。
extern int _stack_start;
__asm__ __volatile__("la sp, _stack_start-4");
__asm__ __volatile__("la t3, _vstext");
_stack_start
変数はリンカファイルsections.lds
で設定します。
.eh_frame
セクションの下に以下のような.stack
セクションを記述します。
.stack ORIGIN(RAM) + LENGTH(RAM) - _stack_size :
{
. = ALIGN(16);
PROVIDE(_stack_end = .);
. = _stack_size;
. = ALIGN(16);
PROVIDE(_stack_start = .);
} >RAM
ソフト面はリンカファイルの書き換え、追記とスタックポインタの設定を行うのみです。
逆アセンブル リストを追ったりしましたが、正直なところ不要でした。キーワード検索で本ブログを参考にした方々にも無駄な時間を使わせた気がします。
ITCM実行部分をSDRAM 領域へ差し替えを行う(ハード面)
picorv32のリセットベクタは変わらずFLASH の0x1040_0000のままでよいです。
しかし、先ほどのpicorv32のリセットベクタを記述しているファイルpico_config.h
には、もう一行defineがありました。
`elsif BUILD_BURN
`define PROGADDR_RESET_DEF 32'h1040_0000
`define PROGADDR_IRQ _DEF 32'h0200_0010 ←この行がITCMのアドレス
PROGADDR_IRQ_DEF
というdefineです。ITCMのアドレス+0x0010
が定義されています。
割込みベクタの定義でしょうか?このあたりの説明のドキュメントが見つけられなくて、勘になります。
SDRAM 領域は0x3000_0000以降1Mbyteと決めているので、以下の様に書き換えてしまいます。
ただ、本ファイルはIDE インストールフォルダのIPが格納されているフォルダにあるファイルですので、書き換えてしまうと、オリジナルのGOWIN_picoRV32とは異なるものになってしまいます。オリジナルは取っておくなどして、十分注意して作業を行いましょう。
`elsif BUILD_BURN
`define PROGADDR_RESET_DEF 32'h1040_0000
//`define PROGADDR_IRQ _DEF 32'h0200_0010
`define PROGADDR_IRQ _DEF 32'h3000_0010
書き換えた後、GOWIN_IDE でpicorv32を再度IP呼び出しを行ってください。
ITCMの設定はMCU boot from External flash and run in ITCM
へチェックを入れます。またITCMのサイズは0が設定できないので最小の8KBに設定します。
また、この際に生成フォルダ(Create In:のところ)をデフォルトから書き換えてgowin_picorv32_run_sdram
と後から見て分かりやすい場所にしておくというのも良いかと思います。見やすい以外にも理由があって後述します。
OKボタンを押すとPicoRV32の合成が始まります。
PicoRV32の合成が終わってしまえば、pico_config.h
はオリジナルに戻してしまってもよいです。全体の合成時に、PicoRV32単体の合成が再度かかる事はありません。
以上、ハード面は1行を書き換えればよいです。
合成済みPicoRV32の再利用方法
少し話は逸れまずが、オリジナルPicoRV32と今回修正したPicoRV32をデバッグ などの目的でちょくちょく切り替えることが想定されるような場合に時短できる小技を紹介します。
GOWIN_IDE のDesignタブでRTLファイルを指定しますが、IPの場合にはIPを呼び出すとココに自動的に追加されます。ソースファイルを右クリックすると下のスクショの様にEnable,Disableが切り替えられます。
designのenable,disable切替え
この機能を使って、Designペインのところにあらかじめ、合成済みのSDRAM 実行版とITCM実行版の両方のpicorv32を用意しておきます(よってpicorv32の合成時は生成フォルダを別にしておかないといけません)。
このDesignペインで使う側のpicorv32をEnable,使わない側をDisableとして全体合成を行うと、disableと指定した側は合成対象外になるので、都度picoRV32を作成しなくても、あらかじめ合成済みのものを呼び出して使うという事が可能になります。
つまりは、構成の異なるPicoRV32(今回はSDRAM で動く版とITCMで動く版)をあらかじめ合成してDesignペインに追加しておけば、マウス右クリックで簡単に切り替えが出来ます。
SDRAM で命令を実行するためのその他のハード情報
アドレスマップ
アドレスマップ
全体ブロック図
ブロック図
簡単ですが、今回作成した回路のブロックを示します。PicoRV32から発行されたアクセス(アドレス0x3000_0000から1MB分の領域のみ)をキャッシュコントローラ経由でSDRAM メモリへアクセスします。
キャッシュメモリ の構成は、(こんなショボい容量ではありますが)4wayセットアソシアティブです。容量は4KBです。BSRAMの構成上、おそらく容量は4倍まで増やしても利用BSRAM数は変わらないんじゃないかと思われます。
(BSRAMは1個あたり1Kx16bitの容量を持つので、今回32bit幅のRAMとして使っているので1wayあたり2個のBSRAMを消費してしまいます。つまり4wayで8個、容量にしてトータル16KBにもなります)。詳しくは過去記事参照↓
spend-carefree.hatenablog.com
キャッシュメモリ コントローラの性能
PicoRV32のwishboneバスにぶら下げていて、以下のような性能になります。あまり速くありません。
Read Hit時 : 4cycle
Write Hit時: 2cycle
Readの時は、TAGメモリからアドレスを引き出してHit/Miss判定を行うため、時間がかかってしまいます。
Write動作は、キャッシュの判定を待たないで、一旦書き込む動作を行っているので早いです。
また、SDRAM へのアクセスは、命令の読み出し動作がほとんどになるので、write動作がread動作より早いというのは全体の実行時間に対する割合は非常に小さいです。
それでも4cycleは外部FLASH メモリで動作するよりずっと高速です。
回路の公開
SDRAM コントローラは前回の記事でgitで公開していますが、そこからバグ取りなど行い再度アップロードしています。以下になります。
■SDRAM コントローラ
tangnano20k/sdramc at main · kazuokada/tangnano20k · GitHub
■キャッシュコントローラ(新規)
tangnano20k/picorv32_cachec/rtl at main · kazuokada/tangnano20k · GitHub
公開はしていますが、デバッグ 中(後述)という事もあり、動作は不安定です。ひたすらにコードは汚いです。
GOWINのドキュメントによると166MHzまで動作するはずですが、安定動作が確認できたのは95MHzでした。 133MHzへ上げると、動いたり動かなかったりで不安定でした。
キャッシュコントローラに不具合があるのか、SDRAM へのアクセスがうまくいかないのか。どちらに原因があるのか突き止められませんでした。
SDRAM から出力されるリードデータをキャプチャするタイミングはSDRAM コントローラとは独立したクロック(clk_capdq)で行っていて、何通りか振ってみたのですが、調整の結果、アクセスが可能になったはずなのに、次の日に試したら何も変更していないのに 時折データが化けたりと安定しません。リードタイミングを振りましたが、ライトで失敗している可能性もあります(低速では動作しているのでライトはOKだと思いますが)。
とりあえず95MHzでは安定動作しているので、95MHzで動かす事にします。(なのでキャッシュコントローラはうまく動けていると信じたい)
なんとか、133MHzでも安定動作を目指したいですけど、安定化作業だけで既に3week程度使っているので、一旦原因突き止めは休憩です。
リードタイミングに係わる部分でパラメータを変更するようにしているので、そのサンプルという事で、インスタンス 部を置いておきます。
`define SDRAM95M
のdefine定義がインスタンス 前に行われているという前提です。(ifdef SDRAM95M
が存在していないのですが、という指摘は合ってます。現状、何もセットしなくても意図通りのパラメータが引き渡されます)
sdramc #(
`ifdef SDRAM166M
. FREQ ( 166 * 1000000 ),
. PHASE_SHIFT_CLKIN ( 0 )
`else
`ifdef SDRAM150M
. FREQ ( 150 * 1000000 ),
. PHASE_SHIFT_CLKIN ( 1 )
`else
`ifdef SDRAM132M
. FREQ ( 132 * 1000000 ),
. PHASE_SHIFT_CLKIN ( 1 )
`else
. FREQ ( 95 * 1000000 ),
. PHASE_SHIFT_CLKIN ( 1 )
`endif
`endif
`endif
) inst_sdramc (
. SDRAM_DQ ( IO_sdram_dq),
. SDRAM_A ( O_sdram_addr),
. SDRAM_BA ( O_sdram_ba),
. SDRAM_nCS( O_sdram_cs_n),
. SDRAM_nWE( O_sdram_wen_n),
. SDRAM_nRAS( O_sdram_ras_n),
. SDRAM_nCAS( O_sdram_cas_n),
. SDRAM_CLK ( O_sdram_clk),
. SDRAM_CKE ( O_sdram_cke),
. SDRAM_DQM ( O_sdram_dqm),
. clk( sdramclk),
. clk_sdram( sdramclk),
`ifdef SDRAM166M
. clk_capdq( sdramclk),
`else
`ifdef SDRAM150M
. clk_capdq(~ sdramclk),
`else
`ifdef SDRAM132M
. clk_capdq(~ sdramclk),
`else
. clk_capdq( sdramclk_90d),
`endif
`endif
`endif
. resetn( sys_resetn_sdramc),
. addr( cmd_addr_sdram),
. busy(),
. cmd( cmd_sdram),
. cmd_en( cmd_en_sdram),
. cmd_ack( cmd_ack_sdram),
. cmd_len( cmd_len_sdram),
. rd_data( cmd_rdata_sdram),
. rd_data_valid( cmd_rvalid_sdram),
. wr_data( cmd_wdata_sdram),
. wr_mask( cmd_wmask_sdram)
);
動作速度について
以前に、以下の記事でbmp 画像をHDMI 表示させる挑戦しましたが、この時の動作速度との比較をします。
spend-carefree.hatenablog.com
10倍以上高速化が出来ました。
Gowin_PicoRV32合成でリセットパスのセットアップエラーの修正
本記事とは関係ないのですが、、、
PicoRV32を37.125MHzで利用しています。
Place&Route工程でリセットの同期化と思われる部分でセットアップエラーが発生しました。
パスを確認すると、リセットをクロックの逆相で同期化していました。そのため、PicoRV32コアへのリセットパスが半サイクル分のセットアップ時間しかなくエラーになりました。
以下のファイルを修正すると直りますが、これまたIP部を触ることになりますので、重々承知の上、作業を行う必要があります。
IDE のverは1.9.8.11
<GOWIN IDEインストールdir>\IDE\ipcore\GowinPicoRV32\Gowin_PicoRV32\picosoc.v
316行目のalways文のnegedgeをposedgeへ変更
まとめ
ソフト開発環境はすごく便利(MCU designer)。
SDRAM を高速動作させるのは難しい。phy部のタイミングが未考慮で済むようにSDRAM データが必ず取得できるクロックとかあるとありがたいのだが。(私の設計のロジックのバグの可能性がありますが)
SDRAM のACタイミングが公開されていないのがつらい。(ドキュメント見つけられないだけかも)
2、3部分で時間の大半を使ってるので。。
IPになっているGOWIN_PicoRV32 取り扱いが簡単でよい。
ただ、SIMが出来ないのは我慢するとして、ロジックアナライザ ―で内蔵RAMとの接続部分が観測出来たらなぁとデバッグ 時に思う事がありました。
参考資料
次回予定
TangPrimer20Kを触りたいと思います。ただネタはなし。実は一度破壊しました。再注文しました。