Tang NanoでuartのIPコアを動かした件


初めに

Tang nanoという小さくて安価なFPGAボードをいじくっています。こちらのページこちらのページを参考に3色LEDの制御とかやってみたのですが、やはり他者と接続して通信出来ると色々と便利なのではと考えました。特にUSBで繋がっているので、それを利用出来ないかと。調べてみると、こちらのページにUSBをuartとして使うための設定方法が書かれており、ループバックが用例で載っていました。早速やってみて、ループバックはできました。しかし、このループバック、rxの信号を直接txのピンに充てただけのもので、一切論理を介さないものでした。どうしたものかと、IDEのIP ジェネレータを弄ってみるとuartのIPがありました。これなら接続して、データのやり取りができ、デバッグとかにも使えそうです。やってみたところ、いくつかはまったので、メモにしておきます。サンプルとして、IPを介したループバックの論理を動かします。

2021/08/22 アップデート

  • @uchan_nosさんのご指摘により、ループバック時のキャラクタが1つ遅れる問題を解決しました。ありがとうございました。
  • Gowin_V1.9.8で動作を確認したところ、PLLのIPがrPLLを使用するように変更されておりましたので修正しました。

参考サイト

SiPeed Tang Nanoの環境構築(Windows編)

Sipeed Tang Nanoで遊んでみる (Linux版)

Tang NanoのFPGAとPC間でUART通信をする

GOWIN Semiconductor Corp.(IP仕様書等:要登録)

準備

  • Windows10 PC
  • Tang Nano
  • USB typeCケーブル
  • Gowin IDE (windows 10版)
  • Teratermか、シリアル通信ソフト

手順

まずは、このサイトのようなTang Nanoの立ち上げサイトでIDEのセットアップとテストをしてください。次に、このサイトにある方法で、USBのuartを有効化してテストください。ここまで終了していることを前提にします。
まずは、Gowinを立ち上げ、新規プロジェクトを作成します。FPGAはGW1N-LV1QN48C6/I5を設定してください。

次にIPの設定をします。今回使用するIPはOSC、PLL、UART masterの3個です。
Tools→IP Core Generatorで、IPコアジェネレータを起動します。

IPコアの一覧が出るので、CLOCKをクリックすると、CLOCK系のIPが出ます。
まずOSCを入れるので、クリックします。分周値を入れることができるので、2を入力してOKを押します。GW1N-LV1QN48C6/I5は内部オシレータは240MHzとのことなので、2分周で120MHzになります。

次にrPLLをクリックします。PLLは設定できる項目がたくさんありますが、基本的には入力クロック周波数と出力クロック周波数の設定のみで使用できるようです。入力クロック周波数に、先ほどのOSCの120MHzを入れて、出力にUART masterで使用する50MHzを入力します。

最後にUART masterを設定します。リストの一番下にUARTがあります。パラメータ表示のうち、周波数は50MHzから動きません。どうやら、これがIPが使用する内部クロックで変更不可のようです。もうひとつのボーレートを使いたいものに合わせます。いろいろと試したところ、115200は動かず、57600と9600は動いているので、その間は動くと思われます。

これでIPの設定は終わりです。
次にverilog論理ファイルおよび、I/Oピン設定です。
File→Newでverilogを選択します。空白の.vファイルができるので、以下のテキストをコピーしてください。

2021/8/22 追加

uart_loopback.vの内容を変更いたしました。
IPの特徴(?)としてRxデータ有効フラグが立ったのちのリードが1回リードでは一つ前のデータが読めて、2回リードすることで、正しいデータが読めるようです。なので、フラッグ確認後、2回データをリードして、2回目のデータが正しいとみなして、Txにループバックしております。
あと、IPのPLLの名称がrPLLとなったために、モジュール名を変更しております。

uart_loopback.v
// Tang Nano UART master Loopback sample by yoshiki9636

module uart_loopback (
    //input clk ,
    input rst_n ,
    output [2:0] led , // r,b,g
    input wire fpga_rx ,
    output wire fpga_tx
); 

wire clk;
wire oscclk;
// clock & PLL
Gowin_OSC osc1(
        .oscout(oscclk) //output oscout
);
Gowin_rPLL pll1(
    .clkout(clk), //output clkout
    .clkin(oscclk) //input clkin
);
// uart connection
reg tx_enr ;
wire tx_en ;
wire [2:0] wadr;
reg [7:0] wdata;
wire rx_en; 
wire [2:0] radr;
wire [7:0] rdata;
wire rx_rdy_n = 1'b1;
wire tx_rdy_n = 1'b1;
wire ddis;
wire intr;
wire dcd_n = 1'b1;
wire cts_n = 1'b1;
wire dsr_n = 1'b1;
wire ri_n = 1'b1;
wire dtr_n;
wire rts_n;
// control signals
reg rx_dv ;
wire rdd ;
reg rx_rdy ;
reg rdy_dv ;


// rx read enable maker
reg [1:0] rxen_cntr;
always @ (posedge clk or negedge rst_n) begin
    if (~rst_n)
        rxen_cntr <= 2'b00;
    else if (rxen_cntr >= 2'b10)
        rxen_cntr <= 2'b00;
    else
        rxen_cntr <= rxen_cntr + 2'b01;
        //rx_en <= ~rx_en ;
end
assign rx_en = | rxen_cntr;

// data read enable :  1:read rx-data  0:read rxrdy
assign rdd = rx_rdy & rx_en ;
// register address : 3'd0:rx-data 3'd5:rxrdy
assign radr = rdd ? 3'd0 :3'd5 ;
// read data valid
always @ (posedge clk or negedge rst_n) begin
    if (~rst_n)
        rx_dv <= 1'b0 ;
     else
        rx_dv <= rdd ;
end
// rxRDY valid
always @ (posedge clk or negedge rst_n) begin
    if (~rst_n)
        rdy_dv <= 1'b0 ;
    else
        rdy_dv <= rx_en ;
end
// rx-data ready
always @ (posedge clk or negedge rst_n) begin
    if (~rst_n)
        rx_rdy <= 1'b0;
    else if (rdy_dv)
        rx_rdy <= rdata[0] & ~rx_dv ;
end
// read data latch -> wdata
always @ (posedge clk or negedge rst_n) begin
    if (~rst_n)
        wdata <= 8'd0 ;
    else if (rx_dv)
        wdata <= rdata ;
end
// tx data enable
always @ (posedge clk or negedge rst_n) begin
    if (~rst_n)
        tx_enr <= 1'b0 ;
    else
        tx_enr <= rx_dv ;
end

assign tx_en = tx_enr & ~rx_dv; 

// tx write address : 3'd0 fixed
assign wadr = 3'd0 ;
// uart master IP
UART_MASTER_Top uart1 (
  .I_CLK(clk),
  .I_RESETN(rst_n),
  .I_TX_EN(tx_en),
  .I_WADDR(wadr),
  .I_WDATA(wdata),
  .I_RX_EN(rx_en),
  .I_RADDR(radr),
  .O_RDATA(rdata),
  .SIN(fpga_rx),
  .RxRDYn(rx_rdy_n),
  .SOUT(fpga_tx),
  .TxRDYn(tx_rdy_n),
  .DDIS(ddis),
  .INTR(intr),
  .DCDn(dcd_n),
  .CTSn(cts_n),
  .DSRn(dsr_n),
  .RIn(ri_n),
  .DTRn(dtr_n),
  .RTSn(rts_n)
);
// rgb counter for debugging
wire rr, gg, bb;
reg [2:0] rgb;
always @ (posedge clk or negedge rst_n) begin
    if (~rst_n)
        rgb <= 3'd0 ;
    else if (rdd)
        rgb <= rgb + 3'd1 ;
end
// LED connection
assign rr = rgb[2] ;
assign gg = rgb[1] ;
assign bb = rgb[0] ;
assign led = { ~rr, ~bb, ~gg} ;

endmodule

IPの情報はGOWINのページの中にリファレンスがあります。登録が必要ですが、無料なので登録してみてください。@cinimlさん、情報ありがとうございました。

次に制約ファイルを作成します。
File→NewからPhysical Constraints Fileを選びます。
空の.cstファイルができるため、以下の設定をコピーします。

uart_loopback.cst
IO_LOC "rst_n" 15;
IO_LOC "led[0]" 16;
IO_LOC "led[1]" 17;
IO_LOC "led[2]" 18;
IO_LOC "fpga_rx" 9;
IO_LOC "fpga_tx" 8;

最後に、fpga_rxとfpga_txは元々書き込みに使われるDual Purpose Pinのため、設定を変更します。(これを忘れてハマることがあります。)Project→Configurationで、Dual-Purpose Pinのタブを選択します。すると、Dual Purpose Pinの一覧が出るので、DONEとRECONFIG_Nにチェックを入れます。
これで設定は終了です。

あとは、Processのタブで、Synthesize、Place & Route、Program Deviceと行うことで、Tang Nanoに書き込みができます。

テストは、何らかのシリアル通信ターミナルで行うのが簡単です。今回はteratermを使いました。Tang NanoをUSBで接続した状態で、teratermを開きます。最初の通信相手の選択で、メニューの下の方にCOMポートの欄があります。ここが選択可能になっており、かつ、Tang NanoのCOMポートが選択できればOKで、それを選択して立ち上げます。

次に設定→シリアルポートで、ボーレートを、IPに設定した値にします。

あとは、適当にキーボードを打つと一つ遅れてエコーバックしてくることが確認できます。このとき、Tang NanoのLEDの色が黒→青→緑→シアン→赤→紫→黄→白→黒となれば、1キャラクタずつ受け取ってエコーバックしていることになります。

こんな感じでPC側にエコーできるようになるので、実機デバッグ時にデバッグメッセージを出力することとかに使えると思います。
ただ、50MHzでなければならないという制約がいまいち納得できません。ほかの周波数でうまくいったなどありましたら、コメントいただけると幸いです。