かずの不定期便ブログ

備忘録代わりに書きます

FPGA Sipeed Tang Nano 9Kを使ってPSRAMへアクセス

リーズナブルFPGAシリーズのTang Nano 9Kを使ってPSRAMへアクセスしました

TERM表示

1. PSRAMについて

TangNano9Kに実装されている64Mbitのメモリなのですが、sipeedのwikiにはSDR SDRAMと記載されています。(下記リンク参照)

https://wiki.sipeed.com/hardware/en/tang/Tang-Nano-9K/Nano-9K.html

実はPSRAMが実装されています。

2. コントローラの実装について

PSRAMメモリーインターフェースIPの選択

GOWIN FPGA Designerから、PSRAMの仕様を知らなくとも簡単に制御できるPSRAMメモリーインターフェースIPが利用できます。(逆にPSRAM自体を制御しようにも仕様が見つけられなかったです) PSRAMメモリーインターフェースIPは以下の2種類が選択できるようです。

  • PSRAM Memory Interface HS
    チャネル数:1ch
    データ幅:64bit
     
  • PSRAM Memory Interface HS 2CH
    チャネル数:2ch
    データ幅:32bit
     

チャネル数とデータ幅が異なりますが、チャネル数についてはインターフェースIPとの間にスイッチを挟んで増やせばよいです。

とりあえず、動かしてみることが目的ですので、どちらでもよいのですが今回はデータ幅が小さい2ch,32bitの方を選択する事にします。

CMD間隔(Tcmd)がBurstlength(後述しますがこれは連続の転送バイト数)で決まっていて、BurstLength=64byteの場合でuser clock(アクセスするインターフェースのクロック)で26サイクル必要です。つまり64byteの転送で26サイクルかかるので、2.4byte/cyc程度(166MHzのメモリクロックで199.2Mbyte/sec)の性能しか出せません(これはメモリ側の帯域666Mbyte/secと比較すると、とても小さい値です)。チャネルインターフェースの2chは独立しているので同時に使う事で倍の4.8byte/cyc出せます。
ここで、おや?と思う事が出てきます。この計算にはチャネルのデータ幅が出てきません。両者のチャネルインターフェースのch数とデータ幅を見ただけだと性能は同一だと思いますが、Tcmdが連続転送バイト数で決まっているという縛りから同一にはなりません。2ch版の方がよくなります。チャネル側から見たピーク性能は1ch(64bit)版は、8byte/cycです。ところがTcmd間隔を守ると2.4byte/cycになります。一方で2ch(32bit)は、1ch版と同じくチャネルインターフェース側は8byte/cycですが、Tcmd間隔はチャネル独立で守ればよいので、トータルとしては倍になり2.4x2=4.8byte/cycを使えます。
つまり1チャネルのみでは、メモリ側の帯域を使いきることは出来ません。
因みにuser guideによると128burst lengthで71%, 64burst lengthで61%の性能が出ると記載されています。2ch版だと4.8byte/8byte = 60%なので、仕様通りの性能になります。

チャネルインターフェースの仕様の確認

FPGA Designerで上のアイコンから「IP core Generator」アイコンを押して、画像の様にSoft IP Core→Memory Control → PSRAM Memory Interfaceを選択するとPSRAMのIFが2種類出てくるので利用する方をクリックするとドキュメントへの案内が提示されるので、クリックします。Gowinのサポートサイトに繋がるのでログイン後、User Guideとリファレンスデザインをダウンロードします。

IPコア選択(PSRAMインターフェース)

www.gowinsemi.com

3章~5章(特に4章)あたりで概要とインターフェース仕様が記載してあります。
cmd_en="1"入力時にリード、ライトコマンドとアドレスを与えてライト時は、コマンド入力したサイクルからバースト長分のライトデータを連続で与え、リード時はrd_data_valid="H"の時にバースト長分のデータが到着するというプロトコルになっています。
2点ほど注意点、誤解しやすいポイントがあります。以下へ記載します。

  • burst_lengthの定義
    データ転送時のバーストクロック数ではなく、転送バイト長です。よってデータ幅に依存しません。(例:burst_length=64は64byte転送で32bitのデータ幅の時は16サイクル、64bitのデータ幅では8サイクルになります)

  • クロックの種類
    本IPにはクロックが3種類あります。UserDesignとインターフェースするクロックが今一つ分かりずらかったのでここへ記載します。
    4章に記載されているタイミングチャートのclkはIPの端子ではclk_outになります。

端子 I/O 接続すべきクロック
clk Input 水晶27MHzクロック。ボードから入力されるクロック
clk_out Output cmd_en,cmd,addrなどのインターフェースの駆動クロックになります。
これはMemoryClkの1/2だと記載されているのでメモリを166MHzで動かした場合は83MHzが出力されます。
memory_clk Input PLLで作成した166MHzを入力します(もちろん遅くても良い)

ポイントはIPから出力されるclk_outを使ってインターフェース信号を作成する事です。

残念ポイント

バースト長が固定です。1byteアクセスするにも決まったバースト転送を行う必要があります。PSRAMの実体はDDRかな?バースト転送を前提にインターフェースは作った方が良いのだと思います。

PSRAM本体はどこに?

IPの端子にはPSRAM InterfaceとしてO_psram_xxx , IO_psram_xxx なる端子がありますが、これらはどこに結線すればよいのだろうか?という疑問が生じます。
PSRAMはFPGAに内蔵されているようなので、FPGAの外部端子としても出てきません。
ドキュメントを見てみましたが、該当記述を見つけることが出来ず、とりあえずIPの端子を出すだけとしました。(TOP階層に同名の端子を作成して、繋ぐだけ)
なんとこれだけ使えてしまいました。こういうものなのでしょうかね?

3. FPGA DesignerでPSRAMメモリーインターフェースIPの呼び出し

最早、蛇足な気がしますが、FPGA DesignerでPSRAMメモリーインターフェースIPの呼び出し方法を記載します。
メニュー下のIP Core Generatorアイコンをクリック→Soft IP Core → PSRAM Memory Interface →PSRAM Memory Interface HS 2CHをダブルクリックすると以下の様なwindowが出ます。

PSRAMコンフィグ

色々コンフィグがありそうな感じの画面が出ますが、TypeタブではMemory Clock周波数以外は変更できないです。OptionsタブではBurstMode(バーストサイズ(byte))が設定できます。この設定に連動して右側のバースト数が変化します。Generation ConfigのところのDisable I/O Insertionはデフォルトでチェックが入っていますが、外すとIBUF, OBUFなどのバッファセルが挿入されるようです。挿入による効果は分かりません。(user guideにも直接ポートと繋がるかバッファを介すかとしか記載がされていない)チェックを入れたままでよいと思います。ShiftDelayの値を変更できますが、デフォルトのまま(192)でよいです。
OKボタンを押すと、コピペする用のスケルトン論理が生成されますので、コピーペーストして貼り付けたら、インスタンス名を適宜変更します。

4. サンプル

4.1 Wishboneバス → PSRAM インターフェース ブリッジモジュール”brd_wb2ps.v"

memoryclkが約165MHzでPICORV32からアクセスする事が出来ました。
前回のTFカードスロットにアクセスするpicotinyシステムにPSRAMをアクセスする機能を付け足す事で実現しています。空き領域だった0xC000_0000以降の64Mbit分を割り付ています。 PICORV32のWishboneバスに接続出来る様にバスブリッジモジュール”brd_wb2ps"を作成しています。 1回のアクセスを64byteのバースト転送にしてしまうという横着な作りの為、転送効率が非常に悪いです。

wishbone→PSRAM interface bridge

4.2 picotiny.vの改造

sipeedのwikiからpicotinyプロジェクトを引っ張ってきて
https://github.com/sipeed/TangNano-9K-example/tree/main/picotiny
hw/picotiny.v の289行目

assign wbp_ready = 1'b1;

コメントアウトして endmoule手前に以下のコードを貼り付けます。

// S3 0xC000_0000 -> PSRAM
wire psramclk;
wire cmd0;
wire cmd1;
wire cmd_en0;
wire cmd_en1;
wire [20:0] addr0;
wire [20:0] addr1;
wire [31:0] wr_data0;
wire [31:0] wr_data1;
wire [3:0] data_mask0;
wire [3:0] data_mask1;
//wire [1:0] O_psram_ck;
//wire [1:0] O_psram_ck_n;
//wire [1:0] O_psram_reset_n;
//wire [1:0] O_psram_cs_n;
wire init_calib0;
wire init_calib1;
wire clk_out;
wire [31:0] rd_data0;
wire [31:0] rd_data1;
wire rd_data_valid0;
wire rd_data_valid1;
//wire [1:0] IO_psram_rwds;
//wire [15:0] IO_psram_dq;
wire half_psramclk;

wire psramclk_lock_o;
     Gowin_rPLL_psram upll_psram_memclk(
        .clkout(psramclk), //output clkout: 165.375MHz
        .clkin(clk), //input clkin   // 27MHz
        .lock_o(psramclk_lock_o)
    );

brd_wb2ps u_brd_wb2ps (
    .RST(~sys_resetn),
    .cpuclk(clk_p),       // wishbone clk

    // PSRAM 32Mbit x 2
    // cpuclk domain
    .ps_mem_addr(wbp_addr[20:0]),
    .ps_mem_wdata(wbp_wdata),
    .ps_mem_rdata(wbp_rdata),
    .ps_mem_wstrb(wbp_wstrb),
    .ps_mem_valid(wbp_valid),
    .ps_mem_ready(wbp_ready),

    // PSRAM i/f
    // psramclk domain
    .half_psramclk(half_psramclk),       // 83MHz clk
    .cmd0(cmd0),           //input cmd0
    .cmd_en0(cmd_en0),        //input cmd_en0
    .addr0(addr0),
    .wr_data0(wr_data0),
    .rd_data0(rd_data0),
    .rd_data_valid0(rd_data_valid0),
    .data_mask0(data_mask0),
    
    // PSRAM parameter
    .tcmd(5'd26)    // 64byte
    );


    PSRAM_Memory_Interface_HS_2CH_Top u_PSRAM(
        .clk(clk), //input clk 27MHz
        .rst_n(sys_resetn), //input rst_n
        .memory_clk(psramclk), //input memory_clk
        .pll_lock(psramclk_lock_o), //input pll_lock
        .O_psram_ck(O_psram_ck), //output [1:0] O_psram_ck
        .O_psram_ck_n(O_psram_ck_n), //output [1:0] O_psram_ck_n
        .IO_psram_rwds(IO_psram_rwds), //inout [1:0] IO_psram_rwds
        .O_psram_reset_n(O_psram_reset_n), //output [1:0] O_psram_reset_n
        .IO_psram_dq(IO_psram_dq), //inout [15:0] IO_psram_dq
        .O_psram_cs_n(O_psram_cs_n), //output [1:0] O_psram_cs_n
        .init_calib0(init_calib0_o), //output init_calib0
        .init_calib1(init_calib1_o), //output init_calib1
        .clk_out(half_psramclk), //output clk_out
        .cmd0(cmd0), //input cmd0
        .cmd1(1'b0), //input cmd1
        .cmd_en0(cmd_en0), //input cmd_en0
        .cmd_en1(1'b0), //input cmd_en1
        .addr0(addr0), //input [20:0] addr0
        .addr1(21'h00_0000), //input [20:0] addr1
        .wr_data0(wr_data0), //input [31:0] wr_data0
        .wr_data1(32'h0), //input [31:0] wr_data1
        .rd_data0(rd_data0), //output [31:0] rd_data0
        .rd_data1(rd_data1), //output [31:0] rd_data1
        .rd_data_valid0(rd_data_valid0), //output rd_data_valid0
        .rd_data_valid1(rd_data_valid1), //output rd_data_valid1
        .data_mask0(data_mask0), //input [3:0] data_mask0
        .data_mask1(4'hf) //input [3:0] data_mask1
    );
    

4.3 PSRAMのメモリクロックをPLLで作成

PLLモジュールはIP core generatorで作成してください。
pll lock出力はありです。
165MHz出力ですが、以下の様なパラメータで生成しています。

module Gowin_rPLL_psram (clkout, clkin, lock_o);
<略>
defparam rpll_inst.FCLKIN = "27";
defparam rpll_inst.DYN_IDIV_SEL = "false";
defparam rpll_inst.IDIV_SEL = 7;
defparam rpll_inst.DYN_FBDIV_SEL = "false";
defparam rpll_inst.FBDIV_SEL = 48;  // 165.375MHz

以上でハード側の改造が終わったので、FPGA designerでsynthesize→Place & Route を行い、FPGAへビットストリームを書き込みます。

4.4 PICORV32で実行させるファームウェアの作成

picotinyのファームウエアのコードを改造します。
fw/fw-flash/firmware.c のmain関数の手前に以下のコードを追加

// PSRAM 8MB
// 0xc000_0000 - 0xc07F_FFFF
#define PSRAM_ADDR (0xc0000000)

main関数のwhile(1)の手前に以下を追加

    volatile uint32_t *PSRAM;
    PSRAM = (uint32_t*) PSRAM_ADDR;
    //volatile uint32_t *PSRAM = PSRAM_ADDR;
    
    print("write adr=0xc000_0000, data=deadbeef\n");
    print("write adr=0xc001_0000, data=5555aaaa\n");
    print("write adr=0xc001_0004, data=ffff0000\n");
    *PSRAM = 0xdeadbeef;
    *(PSRAM+0x10000) = 0x5555aaaa;
    *(PSRAM+0x10004) = 0xffff0000;
    sprintf(sbuff,"0xc000_0000 : %x\n",*PSRAM);
    print( sbuff );
    sprintf(sbuff,"0xc001_0000 : %x\n",*(PSRAM+0x10000));
    print( sbuff );
    sprintf(sbuff,"0xc001_0004 : %x\n",*(PSRAM+0x10004));
    print( sbuff );
    *(PSRAM+0x10000) = *PSRAM;
    sprintf(sbuff,"0xc001_0000 : %x\n",*(PSRAM+0x10000));
    print( sbuff );

makeして、本firmwareFPGAへ書き込みます。 書き込み方法は前回のblogの「フラッシュメモリファームウェアの書き込み方法」を参照。

spend-carefree.hatenablog.com

4.5 実行結果(TERM出力のコピーペースト。HDMI画像としても表示されております)

write adr=0xc000_0000, data=deadbeef
write adr=0xc001_0000, data=5555aaaa
write adr=0xc001_0004, data=ffff0000
0xc000_0000 : deadbeef
0xc001_0000 : 5555aaaa
0xc001_0004 : ffff0000
0xc001_0000 : deadbeef

5. まとめ

PSRAMの使い方は、理解しました。
これで8MBもの大容量メモリを手に入れたので、picorv32を使った大きなアプリが作れそうですね。

6. 次回

PSRAMをVRAMとして使い、SDカードからbmp画像を読みだしてHDMI表示させるビットマップ画像viewerでも作成しようかなと思います。(いつになる事やら)

参考にさせていただいたサイトと資料

PicoRV on nano 9K - Sipeed Wiki
TangNano-9K-example/picotiny at main · sipeed/TangNano-9K-example · GitHub
UG286-1.9.5J_Gowin Clockユーザーガイド.pdf
DS117-2.9.3E_GW1NR series of FPGA Products DataSheet.pdf
IPUG525-1.3.1E_Gowin PSRAM Memory Interface IP User Guide.pdf