四畳半テクノポリス

コロナのストレスで気が狂い、D進した院生

VIVADOのAXI4Streamを読む:Master編

アドベントカレンダーにはリングオシレータについて書くとの宣言していたのですが、最近後輩指導のために書いていたVivadoのテンプレートからAXI4Streamの記事がいい感じだったのでこちらの記事を掲載させていただくことにしました。

AXIStream

VivadoのIP PackagerでAXI-Streamのインタフェースを持ったIPコアを作成します。Tools->Create and Package IPを選択します。

IPパッケージ

このツールではAXIインタフェースを持ったIPのテンプレートを自動的に生成してくれます。create a new AXI4 peripheralを選択してください。

AXI4ペリフェラルを選択

IPに任意のインタフェースを生成することができるのでAXI4-StreamのMasterのインタフェースを生成します。

インタフェースにAXI4-StreamのMasterを追加

ここでEditを選択するとIPの編集ように新たにウィンドウが立ち上がります。私は毎回パッケージ化することが面倒くさいので、再利用予定が無ければ生成されたテンプレートである[パッケージ名]_M00_AXIS.v[パッケージ名].vをメインのプロジェクトのディレクトリにコピーしてしまいますが、IPの編集ウィンドウでシミュレーションを行うこともできます。

パッケージを編集

 VIVADOが生成するAXIStreamテンプレート

とりあえず生成されたAXI4-Streamのテンプレートを示します。ついでにテンプレート中のコメントもガバガバ翻訳ですが訳しました。見たい人は次の行を開いてください。

開く

`timescale 1 ns / 1 ps

    module myip_v1_0_M00_AXIS #
    (
        // ユーザーはここにパラメータを追加します

        // ユーザーパラメータここまで
        // この行以降のパラメータを変更しないでください

        //S_AXISアドレスバスの幅です。スレーブはC_M_AXIS_TDATA_WIDTHで指定されたビット幅のアドレスで読み書きされます。
        parameter integer C_M_AXIS_TDATA_WIDTH = 32,
        // C_M_START_COUNTは、トランザクションを開始/発行する前にマスター側が待機するクロックサイクル数です。
        parameter integer C_M_START_COUNT  = 32
    )
    (
        // ユーザーはここにポートを追加してください

        // ユーザーのポートはここまでです
        // この行以降のポートは変更しないでください

        // グローバルポート
        input wire  M_AXIS_ACLK,
        // リセット
        input wire  M_AXIS_ARESETN,
        //  マスターストリームポート。TVALID は、マスターが転送の準備ができていることを表します。TVALIDとTREADYの両方がアサート(1になる)されると転送が行われます。
        output wire  M_AXIS_TVALID,
        // TDATA はマスターからスレーブにデータを転送するための主要な経路です。
        output wire [C_M_AXIS_TDATA_WIDTH-1 : 0] M_AXIS_TDATA,
        // バイト・イネーブル。該当するバイトが有効かどうかを示す。
        output wire [(C_M_AXIS_TDATA_WIDTH/8)-1 : 0] M_AXIS_TSTRB,
        // パケットの境界を表します。(パケット終了時には1になります)
        output wire  M_AXIS_TLAST,
        // TREADY は現在のサイクルにおいてスレーブ側がデータの受け入れが可能なことを表します。
        input wire  M_AXIS_TREADY
    );
    // 転送データの総数です。byte数ではなくワード数(転送サイクル回数)
    localparam NUMBER_OF_OUTPUT_WORDS = 8;                                               
                                                                                         
    // clogb2関数は整数の値を返します。               
    // 入力された値は2を低としたlogを計算しceilした値を返します。                                           
    function integer clogb2 (input integer bit_depth);                                   
      begin                                                                              
        for(clogb2=0; bit_depth>0; clogb2=clogb2+1)                                      
          bit_depth = bit_depth >> 1;                                                    
      end                                                                                
    endfunction                                                                          
                                                                                         
    // WAIT_COUNT_BITS はcounterのビット幅です。                             
    localparam integer WAIT_COUNT_BITS = clogb2(C_M_START_COUNT-1);                      
                                                                                         
    // FIFOのアドレスを計算するために、アドレスのビット幅を計算します、.  
    localparam bit_num  = clogb2(NUMBER_OF_OUTPUT_WORDS);                                
                                                                                         
    // ステートマシンの状態を宣言します                                               
    // 制御ステートマシンはFIFOへの入力ストリーミングデータの書き込みを監視し、
    // FIFOからストリーミングデータを出力します。
    parameter [1:0] IDLE = 2'b00,        // 初期化・アイドルステート               
                                                                                         
                    INIT_COUNTER  = 2'b01, // カウンタの初期化ステート、一度
                                    // カウンタがC_M_START_COUNT までカウントすると,        
                                    // ステートマシンは SEND_STREAMへ遷移します。     
                    SEND_STREAM   = 2'b10; // このステートでは
                                         // ストリームデータはM_AXIS_TDATAを介して出力されます。   
    // ステートマシンの状態変数                                                            
    reg [1:0] mst_exec_state;                                                            
    // FIFOの読み出しポインタです。                                                  
    reg [bit_num-1:0] read_pointer;                                                      

    // AXI Stream の内部信号
    //wait counter.転送前にマスターが側が通信準備で待つ
    reg [WAIT_COUNT_BITS-1 : 0]    count;
    //転送するデータが準備できていることを表します。
    wire      axis_tvalid;
    //axis_tvalidを1サイクル遅らせるためのレジスタです。
    reg   axis_tvalid_delay;
    //転送するデータ(パケット)の終わりを表します。
    wire      axis_tlast;
    //axis_tlastを1サイクル遅らせるためのレジスタです。
    reg   axis_tlast_delay;
    //FIFO implementation signals
    reg [C_M_AXIS_TDATA_WIDTH-1 : 0]   stream_data_out;
    wire      tx_en;
    //FIFOデータをすべて送りだした時に朝―とされます。
    reg   tx_done;


    // I/O ピンと内部信号の接続

    assign M_AXIS_TVALID = axis_tvalid_delay;
    assign M_AXIS_TDATA  = stream_data_out;
    assign M_AXIS_TLAST  = axis_tlast_delay;
    assign M_AXIS_TSTRB  = {(C_M_AXIS_TDATA_WIDTH/8){1'b1}};


    // 制御用のステートマシンです。                             
    always @(posedge M_AXIS_ACLK)                                             
    begin                                                                     
      if (!M_AXIS_ARESETN)                                                    
      // 同期リセット(active low)                                       
        begin                                                                 
          mst_exec_state <= IDLE;                                             
          count    <= 0;                                                      
        end                                                                   
      else                                                                    
        case (mst_exec_state)                                                 
          IDLE:                                                               
            // ストリーミングデータの存在を示すスレーブは                          
            // tvalidがアサ―トされると、                         
            // スレーブはtdataの受信を開始します。       
            //if ( count == 0 )                                                 
            //  begin                                                           
                mst_exec_state  <= INIT_COUNTER;                              
            //  end                                                             
            //else                                                              
            //  begin                                                           
            //    mst_exec_state  <= IDLE;                                      
            //  end                                                             
                                                                              
          INIT_COUNTER:                                                       
            // ストリーミングデータの存在を示すスレーブは                          
            // tvalidがアサ―トされると、                         
            // スレーブはtdataの受信を開始します。                           
            if ( count == C_M_START_COUNT - 1 )                               
              begin                                                           
                mst_exec_state  <= SEND_STREAM;                               
              end                                                             
            else                                                              
              begin                                                           
                count <= count + 1;                                           
                mst_exec_state  <= INIT_COUNTER;                              
              end                                                             
                                                                              
          SEND_STREAM:                                                        
            // サンプルデザインは マスターが出力のdataを駆動し 
            // FIFOとスレーブをFIFOから、A_AXIS_DATAのストアが完了すると
            // ストリーム機能が開始されます    (何言ってるかよくわかりませんがこのステートでは通信が実行されます)
            if (tx_done)                                                      
              begin                                                           
                mst_exec_state <= IDLE;                                       
              end                                                             
            else                                                              
              begin                                                           
                mst_exec_state <= SEND_STREAM;                                
              end                                                             
        endcase                                                               
    end                                                                       


    //tvalid の生成
    //axis_tvalid はステートマシンの状態変数(mst_exec_state)がSEND_STREAM かつ
    //read_pointerがNUMBER_OF_OUTPUT_WORDS以下の時アサ―トされます。
    assign axis_tvalid = ((mst_exec_state == SEND_STREAM) && (read_pointer < NUMBER_OF_OUTPUT_WORDS));
                                                                                                   
    // AXI tlast の生成
    // axis_tlast はread_pointerがNUMBER_OF_OUTPUT_WORDS-1のときアサートされます。          
    // (0 to NUMBER_OF_OUTPUT_WORDS-1) 
    assign axis_tlast = (read_pointer == NUMBER_OF_OUTPUT_WORDS-1);                                
                                                                                                   
                                                                                                   
    // axis_tvalid と axis_tlast を1サイクル遅延させます。
    // これにより M_AXIS_TDATAの遅延と一致させます。                                                        
    always @(posedge M_AXIS_ACLK)                                                                  
    begin                                                                                          
      if (!M_AXIS_ARESETN)                                                                         
        begin                                                                                      
          axis_tvalid_delay <= 1'b0;                                                               
          axis_tlast_delay <= 1'b0;                                                                
        end                                                                                        
      else                                                                                         
        begin                                                                                      
          axis_tvalid_delay <= axis_tvalid;                                                        
          axis_tlast_delay <= axis_tlast;                                                          
        end                                                                                        
    end                                                                                            


    //read_pointer pointer

    always@(posedge M_AXIS_ACLK)                                               
    begin                                                                            
      if(!M_AXIS_ARESETN)                                                            
        begin                                                                        
          read_pointer <= 0;                                                         
          tx_done <= 1'b0;                                                           
        end                                                                          
      else                                                                           
        if (read_pointer <= NUMBER_OF_OUTPUT_WORDS-1)                                
          begin                                                                      
            if (tx_en)                                                               
              // FIFOが有効な時   
              // 1データ読み出すごとにread_pointerを1づつインクリメントします.                                   
              begin                                                                  
                read_pointer <= read_pointer + 1;                                    
                tx_done <= 1'b0;                                                     
              end                                                                    
          end                                                                        
        else if (read_pointer == NUMBER_OF_OUTPUT_WORDS)                             
          begin                                                                      
            // NUMBER_OF_OUTPUT_WORDS 個のストリームデータが出力されたときtx_doneがアサ―トされます
            // tx_doneがアサ―トされます
            tx_done <= 1'b1;                                                         
          end                                                                        
    end                                                                              


    //tx_en :FIFO の読み出し有効

    assign tx_en = M_AXIS_TREADY && axis_tvalid;   
                                                         
        // 転送対象のデータをFIFOから取り出します。       
        always @( posedge M_AXIS_ACLK )                  
        begin                                            
          if(!M_AXIS_ARESETN)                            
            begin                                        
              stream_data_out <= 1;                      
            end                                          
          else if (tx_en)// && M_AXIS_TSTRB[byte_index]  
            begin                                        
              stream_data_out <= read_pointer + 32'b1;   
            end                                          
        end                                              

    // ここにユーザーロジックを追加

    // ユーザーロジックここまで

    endmodule

ステートマシン

次のコードが通信全体を管理するステートマシンの抜粋です。

 // Control state machine implementation                             
    always @(posedge M_AXIS_ACLK)                                             
    begin                                                                     
      if (!M_AXIS_ARESETN)                                                    
      // Synchronous reset (active low)                                       
        begin                                                                 
          mst_exec_state <= IDLE;                                             
          count    <= 0;                                                      
        end                                                                   
      else                                                                    
        case (mst_exec_state)                                                 
          IDLE:                                                               
            // The slave starts accepting tdata when                          
            // there tvalid is asserted to mark the                           
            // presence of valid streaming data                               
            //if ( count == 0 )                                                 
            //  begin                                                           
                mst_exec_state  <= INIT_COUNTER;                              
            //  end                                                             
            //else                                                              
            //  begin                                                           
            //    mst_exec_state  <= IDLE;                                      
            //  end                                                             
                                                                              
          INIT_COUNTER:                                                       
            // The slave starts accepting tdata when                          
            // there tvalid is asserted to mark the                           
            // presence of valid streaming data                               
            if ( count == C_M_START_COUNT - 1 )                               
              begin                                                           
                mst_exec_state  <= SEND_STREAM;                               
              end                                                             
            else                                                              
              begin                                                           
                count <= count + 1;                                           
                mst_exec_state  <= INIT_COUNTER;                              
              end                                                             
                                                                              
          SEND_STREAM:                                                        
            // The example design streaming master functionality starts       
            // when the master drives output tdata from the FIFO and the slave
            // has finished storing the S_AXIS_TDATA                          
            if (tx_done)                                                      
              begin                                                           
                mst_exec_state <= IDLE;                                       
              end                                                             
            else                                                              
              begin                                                           
                mst_exec_state <= SEND_STREAM;                                
              end                                                             
        endcase                                                               
    end  

それぞれのステートは次のような働きをします。

  • IDLE:初期状態のステートです。サンプルプログラムだと強制的にINIT_COUNTERに遷移するようになっています。
  • INIT_COUNTER:通信の初期化を行うステートです。countをインクリメントし続けて C_M_START_COUNT - 1に達したときにSEND_STREAMに遷移します。
  • SEND_STREAM:データ送信のステートです。tx_doneがアサ―トされる(通信が完了する)まで、SEND_STREAMのステートを維持します。

ステートマシン

プログラム中のステートマシンを上の図にまとめました。実質的にAXI-Streamに関わるのはSEND_STREAMだけなので、次からSEND_STREAMで何が起きていくかを記述していきます。

 AXI4-Streamのインタフェース

生成されたIPコアのインタフェースを見ると次のコードのようなポートが生成されていると思います。

// Global ports
input wire  M_AXIS_ACLK,
// 
input wire  M_AXIS_ARESETN,
// Master Stream Ports. TVALID indicates that the master is driving a valid transfer, A transfer takes place when both TVALID and TREADY are asserted. 
output wire  M_AXIS_TVALID,
// TDATA is the primary payload that is used to provide the data that is passing across the interface from the master.
output wire [C_M_AXIS_TDATA_WIDTH-1 : 0] M_AXIS_TDATA,
// TSTRB is the byte qualifier that indicates whether the content of the associated byte of TDATA is processed as a data byte or a position byte.
output wire [(C_M_AXIS_TDATA_WIDTH/8)-1 : 0] M_AXIS_TSTRB,
// TLAST indicates the boundary of a packet.
output wire  M_AXIS_TLAST,
// TREADY indicates that the slave can accept a transfer in the current cycle.
input wire  M_AXIS_TREADY

各々のポートの意味はこんな感じです。これら名称のポートがモジュールに存在するとVIVADOは自動でポートを認識してブロックデザインでAXI4-Streamにより結線することが可能になります。

信号名 Masterから見た方向 機能 ビット幅
M_AXIS_ACLK input クロック 1
M_AXIS_ARESETN input リセット 1
M_AXIS_TVALID output マスター側送信準備完了 1
M_AXIS_TDATA output データ word
M_AXIS_TSTRB output バイト選択 word/8
M_AXIS_TLAST output パケットの境界線 1
M_AXIS_TREADY input スレーブ受信可能 1

M_AXIS_TVALIDとM_AXIS_TREADYのハンドシェイク

AXI4-Streamの通信開始時にはまずM_AXIS_TVALIDかM_AXIS_TREADYのどちらかが先行して、もしくは同時に立ち上がります。

 tvalid

外部に接続されたtvalid信号は次のようにアサインされている。

assign M_AXIS_TVALID  = axis_tvalid_delay;
assign M_AXIS_TLAST  = axis_tlast_delay;`

これらの信号の生成過程を追う。

_delayとついていることから分かるようにこれらの信号は生成された信号を1クロック分遅れさせている。 axis_tvalid 信号はステートマシンの状態がSEND_STREAMの時に有効化される

//tvalid generation
//axis_tvalid is asserted when the control state machine's state is SEND_STREAM and
//number of output streaming data is less than the NUMBER_OF_OUTPUT_WORDS.
assign axis_tvalid = (mst_exec_state == SEND_STREAM);                                                                  

tlast信号は転送サイズよりカウンタが一つ小さい数値となった時だけ1になる。

// AXI tlast generation                                                                        
// axis_tlast is asserted number of output streaming data is NUMBER_OF_OUTPUT_WORDS-1          
// (0 to NUMBER_OF_OUTPUT_WORDS-1)                                                             
assign axis_tlast = (read_pointer == NUMBER_OF_OUTPUT_WORDS-1); 

この信号は次のように通信クロックの立ち上がり分遅らせるようになっている。

 // Delay the axis_tvalid and axis_tlast signal by one clock cycle                              
    // to match the latency of M_AXIS_TDATA                                                        
    always @(posedge M_AXIS_ACLK)                                                                  
    begin                                                                                          
      if (!M_AXIS_ARESETN)                                                                         
        begin                                                                                      
          axis_tvalid_delay <= 1'b0;                                                               
          axis_tlast_delay <= 1'b0;                                                                
        end                                                                                        
      else                                                                                         
        begin                                                                                      
          axis_tvalid_delay <= axis_tvalid;                                                        
          axis_tlast_delay <= axis_tlast;                                                          
        end                                                                                        
    end       

 転送

tx_enはreadyとvalidが両方有効な場合なので通信が有効であるということです。この二つがアサ―トされているときだけデータの転送が行われます。 tream_data_outは出力に接続されている。

サンプルでは出力に読み出しポインタが接続されており、リセット時には1を、それ以外では読み出しポインタ+1が出力される。。

//FIFO read enable generation 

assign tx_en = M_AXIS_TREADY && axis_tvalid;   
                                                         
// Streaming output data is read from FIFO       
always @( posedge M_AXIS_ACLK )                  
    begin                                            
      if(!M_AXIS_ARESETN)                            
        begin                                        
          stream_data_out <= 1;                      
        end                                          
      else if (tx_en)// && M_AXIS_TSTRB[byte_index]  
        begin                                        
          stream_data_out <= read_pointer + 32'b1;   
        end                                          
    end                                              
// Add user

read_pointer を制御しているは下記のようになっている、リセット時にはtx_doneとread_pointerは初期化されread_pointer <= NUMBER_OF_OUTPUT_WORDS-1かつtx_enが立っている状態の時だけread_pointerをインクリメントさせる。そして、read_pointer == NUMBER_OF_OUTPUT_WORDSに達するとtx_doneをアサートさせるようになっている。

    //read_pointer pointer

    always@(posedge M_AXIS_ACLK)                                               
    begin                                                                            
      if(!M_AXIS_ARESETN)                                                            
        begin                                                                        
          read_pointer <= 0;                                                         
          tx_done <= 1'b0;                                                           
        end                                                                          
      else                                                                           
        if (read_pointer <= NUMBER_OF_OUTPUT_WORDS-1)                                
          begin                                                                      
            if (tx_en)                                                               
              // FIFOが有効な時   
              // 1データ読み出すごとにread_pointerを1づつインクリメントします.                                   
              begin                                                                  
                read_pointer <= read_pointer + 1;                                    
                tx_done <= 1'b0;                                                     
              end                                                                    
          end                                                                        
        else if (read_pointer == NUMBER_OF_OUTPUT_WORDS)                             
          begin                                                                      
            // NUMBER_OF_OUTPUT_WORDS 個のストリームデータが出力されたときtx_doneがアサ―トされます
            // tx_doneがアサ―トされます
            tx_done <= 1'b1;                                                         
          end                                                                        
    end                                                                              

 どうやって改造してゆけば良いのか?

基本的にステートマシンに従って改造してゆけば良い、readyとvalidが両方1であるタイミングにおいて、stream_data_outレジスタもしくはM_AXIS_TDATAワイヤーにストリームデータを書き込まれるようにしてゆけばよい。

そしてデータ数であるがテンプレート上ではNUMBER_OF_OUTPUT_WORDSで指定されている。転送したいデータ数が固定であればこのlocalparam自体を直接書き換えてゆけばよい。もし、転送したいデータ数が可変であれば、NUMBER_OF_OUTPUT_WORDSと自体をレジスタに書き換え、localparam bit_num を十分な大きさを持つレジスタに書き換えてしまえばよい。

私がAXI4Streamでセンサからデータを読み出す場合以下のような構成の回路を記述することが多い。先ほども述べたように、AXI4Streamではマスター側で1回のデータ転送数はあらかじめ決定しておく必要があるが、これはスレーブ側であるPS側でDMA受け入れを許可する際にも同様のサイズを指定する必要がある。この転送サイズがどの程度のサイズが適切なのかは使用するアプリケーションのレイテンシや、バッファサイズ、必要な転送速度によって変化する。また、信号処理回路を含むモジュールであれば、フィルタなどのパラメータをソフトウェアで変更できる方がいちいち論理合成をする必要がなくなり、迅速な開発につながる。このような設定を行うAXI4Liteのインタフェースを別途設置し、転送数や信号処理回路の設定、バッファにたまっているデータの数などを監視できるようにすることで、AXI4Streamを使った柔軟な開発が実現できる。

Rocket chipのCPUログを逆アセンブルする

研究でRocketChipというRISC-Vの割とデファクトスタンダードな実装のCPUをいじっているのですが、エラーがよくわからなくて苦しんでいます。

https://github.com/chipsalliance/rocket-chip

このRocketChipは実行するとvcdの他にCPUの実行した命令や、その時のALUのRS1、RS2、プログラムカウンタを含むXX.outというファイルを出力してくれます。このログファイルは次のような感じです

using random seed 1658059470
This emulator compiled with JTAG Remote Bitbang client. To enable, use +jtag_rbb_enable=1.
Listening on port 34039
C0:         27 [1] pc=[00010040] W[r 0=00000208][1] R[r 0=00000000] R[r 0=00000000] inst=[7c105073] DASM(7c105073)
C0:         32 [1] pc=[00010044] W[r10=00000000][1] R[r 0=00000000] R[r 0=00000000] inst=[f1402573] DASM(f1402573)
C0:         33 [1] pc=[00010048] W[r11=00010048][1] R[r 0=00000000] R[r 0=00000000] inst=[00000597] DASM(00000597)
C0:         34 [1] pc=[0001004c] W[r11=00010080][1] R[r11=00010048] R[r 0=00000000] inst=[03858593] DASM(03858593)
C0:         35 [1] pc=[00010050] W[r 0=00000000][1] R[r 0=00000000] R[r 0=00000000] inst=[30405073] DASM(30405073)
C0:         40 [1] pc=[00010054] W[r 0=00000000][0] R[r 0=00000000] R[r 0=00000000] inst=[10500073] DASM(10500073)
C0:         43 [0] pc=[00010058] W[r 0=00000000][0] R[r 0=00000000] R[r 0=00000000] inst=[0000bff5] DASM(0000bff5)
C0:         70 [1] pc=[00000800] W[r 0=00000804][1] R[r 0=00000000] R[r 0=00000000] inst=[00c0006f] DASM(00c0006f)
C0:         72 [1] pc=[0000080c] W[r 0=00000000][0] R[r 0=00000000] R[r 0=00000000] inst=[0ff0000f] DASM(0ff0000f)

この中のDASMという項目がアセンブラなのでどんなプログラムが走っているか知りたいですよね。なのでこの項目に逆アセンブラを適応していきましょう。今回はRISCVのtoolchainに含まれているspike-dasmを使います。このコマンドは行内に含まれるDASM(0x????????)という16進数の記述を見つけると逆アセンブルして置き換えてくれるみたいです。便利ですね。

catにパイプ付けてそのまま実行出来るかと思ったら、なんかうまく行かなかったので次のように実行してます。以下の内容でshellで実行してください。僕の環境だとspike-dasmは/opt/riscv/bin/spike-dasm にありました。

cat <span style="color: #d32f2f">XXX.out</span>| while read line      
do       
/path/to/spike-dasm ${line}
done;

この結果が次のようになります。

This emulator compiled with JTAG Remote Bitbang client. To enable, use +jtag_rbb_enable=1.
Listening on port 34039
C0:         27 [1] pc=[00010040] W[r 0=00000208][1] R[r 0=00000000] R[r 0=00000000] inst=[7c105073] csrwi   unknown_7c1, 0
C0:         32 [1] pc=[00010044] W[r10=00000000][1] R[r 0=00000000] R[r 0=00000000] inst=[f1402573] csrr    a0, mhartid
C0:         33 [1] pc=[00010048] W[r11=00010048][1] R[r 0=00000000] R[r 0=00000000] inst=[00000597] auipc   a1, 0x0
C0:         34 [1] pc=[0001004c] W[r11=00010080][1] R[r11=00010048] R[r 0=00000000] inst=[03858593] addi    a1, a1, 56
C0:         35 [1] pc=[00010050] W[r 0=00000000][1] R[r 0=00000000] R[r 0=00000000] inst=[30405073] csrwi   mie, 0
C0:         40 [1] pc=[00010054] W[r 0=00000000][0] R[r 0=00000000] R[r 0=00000000] inst=[10500073] wfi
C0:         43 [0] pc=[00010058] W[r 0=00000000][0] R[r 0=00000000] R[r 0=00000000] inst=[0000bff5] c.j     pc - 4
C0:         70 [1] pc=[00000800] W[r 0=00000804][1] R[r 0=00000000] R[r 0=00000000] inst=[00c0006f] j       pc + 0xc
C0:         72 [1] pc=[0000080c] W[r 0=00000000][0] R[r 0=00000000] R[r 0=00000000] inst=[0ff0000f] fence

はい、とりあえず何の命令が実行されているか確認出来ますね。これでデバッグがかなり楽になります。 いままで大体1024番地のデバッグ用のレジスタの挙動が意味不明で動かないことが多かったのでそのへんも分かったらまとめていきます。 じゃあさよなら

書くのが辛かった論文を再投稿しようとしたら辛い気持ちもリストアされてしまった。

もとごとに取り組むにはモチベーションみたいなのが非常に大切だ、特に私は似たような作業でも気持ちの持ちようでパフォーマンスが全然変わってしまうという厄介な気質を持っている。

現在以前リジェクトされた論文を別の学会に投稿しようと準備しているところであるがとても辛い。思い返すとこの論文のためのデータを集める時点で既に辛かった。前回リジェクトされた以前にも一度別の学会に投稿しようとしたのだが、その時別に実験を並行で行っていて、そっちを対応していたせいで投稿出来なかった。しかもその対応箇所は私の担当箇所ですらなく別の人の面倒を見て論文が投稿出来なかったわけだ。その後しばらくは、実験の箇所を担当していた後輩にうまく接することができなくなってしまった。

もう一つは最初に論文を投稿した時点でのことだ。これは直前に別の学会に投稿した上で一週間後に投稿というハードスケジュールであった。そもそも、この研究は結構お金がかかっているので早く論文にしないといけないという焦燥感が結構あった。直前まで一生懸命データを集めながらもう一つの論文を必死に書いていたわけで、すでに疲労困憊であった。その状態で論文を書いたわけだが、全然書けず、半分先生に書いてもらうような結果となってしまった。これにはとても罪悪感を感じてしまい、さらに気分が落ち込んで作業を続けたいのに、一行書くごとに苦痛をすごく感じる状態になってしまい、なかなか進めるのに苦労した。また、急ピッチで論文を書いたため、図表もイマイチな出来て、何度も修正をもらい、それも辛かった。論文提出後のことをあまり良く覚えていない。なんだか辛かったことはよく覚えている。

そんなこんなで苦労して書いた論文はイマイチな出来てリジェクトされてしまった。今度はこの論文を別の学会に出すために準備している。ただ、論文を読み返すたびに当時の辛い気分が心に復元されてしまい、進めるのが辛い状態になっている。辛いなぁ、胸が痛いなぁ、まあ自業自得だし大人だし、なんとかするけど。

エンジニアのコーディング依存症について

最近際限なくコードを書いてしまい、気がつくと空が明るくなってきてしまうことが良くある。また、会議中に面白い仕様が決まると、会議が終わってないのにエディタを立ち上げてしまったこともある。依存症というくらいだから日常生活に支障をきたしている。

非常に不健全だ。なぜこのようなことになってしまうのかといえば僕がゲーム依存症的にプログラムを書いてしまうからであろう。みなさんはゲームをしていて続きが気になって続けてしまうことは無いだろうか?ゲームでステージをクリアするごとに出来ることや強さが増していく、それが面白くて際限なく続けてしまう。この自分の物語の続きが気になってやめられなくなることがゲームに依存してしまう原因の一つだと思っている。これと同様の物語がプログラミングには存在する。

特にデバッグ作業は物語性が強く謎が一つ解けるたびに答えに近づいていく、そして答えに到達した時の快感など非常にゲーム的だ。誰も知らないライブラリのバグに到達した時など非常に興奮するし、他人のプログラムと自分のプログラムが繋がった瞬間なども非常に心地よい。これが私を虜ににしてしまう。

当たり前であるが、エンジニアの仕事はコーディングだけでは無い、資料作成やリファクタリング、プログラムのバージョン管理や他人とのマージ、テストなど多肢にわたる。リサーチャーなら論文執筆などもある。コーディングだけしてればいいプログラマはおそらく存在しないだろう。

私はどうにかしてコーディング依存症から脱却しなければならない、だが仕事柄毎日コードを書かないわけにはいかない。今日もこうやって朝までコードを書いてしまい反省している。どうしたらええんやろうなあ

ChiselのBitpadとListLookupがまるでわからない?

命令デコーダって難しい

 CPUをVerilogで生成していたところ、命令デコーダの生成で作ったIf文にラッチができてしまいました。

 オペランドがそのままCase文でマルチプレクサなどの制御に使える単純なアーキテクチャであれば気にする必要はありませんが、RISC-Vは命令形式によって利用するビットフィールドが異なるため、複数の条件式を使ったIf文を書いて命令を特定し、その中で制御信号を設定したくなってしまいました。冷静に考えるとアホみたいな書き方です。また、「デジタル回路設計とコンピュータアーキテクチャ」を参照してもそのような書き方はされてません。

なんでこんな書き方をしてしまったかというと、「RSIC-VとChiselで学ぶ初めてのCPU自作」のListLookupが記憶に残っていたからだと思います。この本ではBitpadとListLookupという機能を使って命令デコーダを作っています。そこで実際にBitpadとListLookupで、どのような回路が生成されるか実験してみました。

Chiselコード

とりあえずBitpadとListLookアップを使った回路です

package lookup
import chisel3._
import chisel3.util._

class Top extends Module{
    val io = IO(new Bundle{
        val exit = Output(Bool())
        val outA = Output(UInt(4.W))
        val outB = Output(UInt(4.W))

    })

    val reg = RegInit(0.U(4.W))
    reg := reg + 1.U(4.W)

    val A = BitPat("b????0011")
    val B = BitPat("b????0111")
    val C = BitPat("b????1011")
    val D = BitPat("b????1111")
    val E = BitPat("b00??0000")
    val F = BitPat("b10??0000")
    val G = BitPat("b01??0000")
    val H = BitPat("b11??0000")
    
    val csignals = ListLookup(reg, List(1.U(4.W),1.U(4.W)),
        Array(
            A -> List(0.U(4.W),14.U(4.W)),
            B -> List(2.U(4.W),13.U(4.W)),
            C -> List(4.U(4.W),12.U(4.W)),
            D -> List(6.U(4.W),11.U(4.W)),
            E -> List(8.U(4.W),10.U(4.W)),
            F -> List(10.U(4.W),9.U(4.W)),
            G -> List(12.U(4.W),8.U(4.W)),
            H -> List(14.U(4.W),7.U(4.W))
        )
    )


    when(reg === 15.U(4.W)){
        io.exit := true.B
    }.otherwise{
        io.exit := false.B
    }

    val tmpA::tmpB::Nil = csignals 
    io.outA := tmpA
    io.outB := tmpB

}


object Elaborate extends App {
  chisel3.Driver.execute(args, () => new Top())
}

この回路からVerilogを生成すると次の回路のようになりました。

`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_INVALID_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_REG_INIT
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_MEM_INIT
`define RANDOMIZE
`endif

module Top(
  input        clock,
  input        reset,
  output       io_exit,
  output [3:0] io_outA,
  output [3:0] io_outB
);
  reg [3:0] reg$;
  reg [31:0] _RAND_0;
  wire [4:0] _T_8;
  wire [3:0] _T_9;
  wire  _T_31;
  wire  _T_35;
  wire  _T_39;
  wire  _T_43;
  wire [7:0] _GEN_1;
  wire [7:0] _T_46;
  wire  _T_47;
  wire  _T_51;
  wire  _T_55;
  wire  _T_59;
  wire [3:0] _T_60;
  wire [3:0] _T_61;
  wire [3:0] _T_62;
  wire [3:0] _T_63;
  wire [3:0] _T_64;
  wire [3:0] _T_65;
  wire [3:0] _T_66;
  wire [3:0] csignals_0;
  wire [3:0] _T_67;
  wire [3:0] _T_68;
  wire [3:0] _T_69;
  wire [3:0] _T_70;
  wire [3:0] _T_71;
  wire [3:0] _T_72;
  wire [3:0] _T_73;
  wire [3:0] csignals_1;
  wire  _T_75;
  assign _T_8 = reg$ + 4'h1;
  assign _T_9 = _T_8[3:0];
  assign _T_31 = 4'h3 == reg$;
  assign _T_35 = 4'h7 == reg$;
  assign _T_39 = 4'hb == reg$;
  assign _T_43 = 4'hf == reg$;
  assign _GEN_1 = {{4'd0}, reg$};
  assign _T_46 = _GEN_1 & 8'hcf;
  assign _T_47 = 8'h0 == _T_46;
  assign _T_51 = 8'h80 == _T_46;
  assign _T_55 = 8'h40 == _T_46;
  assign _T_59 = 8'hc0 == _T_46;
  assign _T_60 = _T_59 ? 4'he : 4'h1;
  assign _T_61 = _T_55 ? 4'hc : _T_60;
  assign _T_62 = _T_51 ? 4'ha : _T_61;
  assign _T_63 = _T_47 ? 4'h8 : _T_62;
  assign _T_64 = _T_43 ? 4'h6 : _T_63;
  assign _T_65 = _T_39 ? 4'h4 : _T_64;
  assign _T_66 = _T_35 ? 4'h2 : _T_65;
  assign csignals_0 = _T_31 ? 4'h0 : _T_66;
  assign _T_67 = _T_59 ? 4'h7 : 4'h1;
  assign _T_68 = _T_55 ? 4'h8 : _T_67;
  assign _T_69 = _T_51 ? 4'h9 : _T_68;
  assign _T_70 = _T_47 ? 4'ha : _T_69;
  assign _T_71 = _T_43 ? 4'hb : _T_70;
  assign _T_72 = _T_39 ? 4'hc : _T_71;
  assign _T_73 = _T_35 ? 4'hd : _T_72;
  assign csignals_1 = _T_31 ? 4'he : _T_73;
  assign _T_75 = reg$ == 4'hf;
  assign io_exit = _T_75;
  assign io_outA = csignals_0;
  assign io_outB = csignals_1;
`ifdef RANDOMIZE
  integer initvar;
  initial begin
    `ifndef verilator
      #0.002 begin end
    `endif
  `ifdef RANDOMIZE_REG_INIT
  _RAND_0 = {1{$random}};
  reg$ = _RAND_0[3:0];
  `endif // RANDOMIZE_REG_INIT
  end
`endif // RANDOMIZE
  always @(posedge clock) begin
    if (reset) begin
      reg$ <= 4'h0;
    end else begin
      reg$ <= _T_9;
    end
  end
endmodule

どうやら出力ごと、にいい感じに組み合わせ回路を合成してくれるようです。カルノー図で解くような最適化をしているのかな?

不可解な点

ただ、Bitpadが矛盾していてもListLookupはエラーを吐かずに回路を生成してしまいました。次のコードですと0b00000011が与えられた場合AとEの両方に該当してしまうと思うんですが、回路は生成されます。ちゃんとエラー出してほしいお

    val A = BitPat("b????0011")
    val B = BitPat("b????0111")
    val C = BitPat("b????1011")
    val D = BitPat("b????1111")
    val E = BitPat("b00??????")
    val F = BitPat("b10??0000")
    val G = BitPat("b01??0000")
    val H = BitPat("b11??0000")

 全部同じにしてみた

試しにBitpadの値を次のように全部同じにしてみました。

    val A = BitPat("b????1111")
    val B = BitPat("b????1111")
    val C = BitPat("b????1111")
    val D = BitPat("b????1111")
    val E = BitPat("b????1111")
    val F = BitPat("b????1111")
    val G = BitPat("b????1111")
    val H = BitPat("b????1111")
    

すると生成されるコードは次のようになりました。

odule Top(
  input        clock,
  input        reset,
  output       io_exit,
  output [3:0] io_outA,
  output [3:0] io_outB
);
  reg [3:0] reg$;
  reg [31:0] _RAND_0;
  wire [4:0] _T_8;
  wire [3:0] _T_9;
  wire  _T_31;
  wire [3:0] _T_60;
  wire [3:0] _T_61;
  wire [3:0] _T_62;
  wire [3:0] _T_63;
  wire [3:0] _T_64;
  wire [3:0] _T_65;
  wire [3:0] _T_66;
  wire [3:0] csignals_0;
  wire [3:0] _T_67;
  wire [3:0] _T_68;
  wire [3:0] _T_69;
  wire [3:0] _T_70;
  wire [3:0] _T_71;
  wire [3:0] _T_72;
  wire [3:0] _T_73;
  wire [3:0] csignals_1;
  wire  _T_75;
  assign _T_8 = reg$ + 4'h1;
  assign _T_9 = _T_8[3:0];
  assign _T_31 = 4'hf == reg$;
  assign _T_60 = _T_31 ? 4'he : 4'h1;
  assign _T_61 = _T_31 ? 4'hc : _T_60;
  assign _T_62 = _T_31 ? 4'ha : _T_61;
  assign _T_63 = _T_31 ? 4'h8 : _T_62;
  assign _T_64 = _T_31 ? 4'h6 : _T_63;
  assign _T_65 = _T_31 ? 4'h4 : _T_64;
  assign _T_66 = _T_31 ? 4'h2 : _T_65;
  assign csignals_0 = _T_31 ? 4'h0 : _T_66;
  assign _T_67 = _T_31 ? 4'h7 : 4'h1;
  assign _T_68 = _T_31 ? 4'h8 : _T_67;
  assign _T_69 = _T_31 ? 4'h9 : _T_68;
  assign _T_70 = _T_31 ? 4'ha : _T_69;
  assign _T_71 = _T_31 ? 4'hb : _T_70;
  assign _T_72 = _T_31 ? 4'hc : _T_71;
  assign _T_73 = _T_31 ? 4'hd : _T_72;
  assign csignals_1 = _T_31 ? 4'he : _T_73;
  assign _T_75 = reg$ == 4'hf;
  assign io_exit = _T_75;
  assign io_outA = csignals_0;
  assign io_outB = csignals_1;
`ifdef RANDOMIZE
  integer initvar;
  initial begin
    `ifndef verilator
      #0.002 begin end
    `endif
  `ifdef RANDOMIZE_REG_INIT
  _RAND_0 = {1{$random}};
  reg$ = _RAND_0[3:0];
  `endif // RANDOMIZE_REG_INIT
  end
`endif // RANDOMIZE
  always @(posedge clock) begin
    if (reset) begin
      reg$ <= 4'h0;
    end else begin
      reg$ <= _T_9;
    end
  end
endmodule

下4桁がFのときに_T_32が1になり、その後の条件式ではassign _T_60 = _T_31 ? 4'he : 4'h1かもしくわ assign csignals_0 = _T_31 ? 4'h0 : _T_66;では4'h0か4'h1が選ばれる回路になるようです。この二行間の回路は意味をなしてませんので論理合成時に消えるものと思われます。

 雑なまとめ

とりあえずListLookupは危険だなと思いました。chiselの公式Wikiを見れば詳しい挙動が書いてありそうですが、どこに書いてあるか見つけられない。

C API for PYNQ : C++でDMAする

C API for PYNQでDMAをする

PYNQでFPGAとOS上のアプリケーション通信間で高速に通信するにはDMAが有効です。前回の記事でも述べたようにPythonのスレッド間通信は低速なのでFPGA側からDMAで送られてきたデータを処理しようとすると速度不足になりやすいです。 今回はPYNQでAXIStreamによるDMAができたのその記事を書こうと思います。 github.com

README.MDにサンプルコードがあるのですが、実行するための回路を生成するTCLもビットファイルも無いので、回路を動かすためにどうしたらいいかわかりづらいと思います。そこで回路を作って実行してみました。

回路作成

以下のブログの「PS に接続されたメモリのデータを PL から読み書きしてみる」の回路を丸々拝借します。 www.acri.c.titech.ac.jp

出来上がったものがこちら

f:id:toriten1024:20211224213502p:plain
ブロック図
そして注目すべきところが作ったaxi_dmaにたいしてAXI_LITEで接続しているアドレス部分。
f:id:toriten1024:20211224215315p:plain
アドレスエディタ

このアドレス部分をソースコード中のPYNQ_openDMA(&dma, アドレス);と一致させる必要があります。

ソースコード

丸々引用のコードです

#include <stdio.h>
#include <pynq_api.h>

int main() {
  PYNQ_loadBitstream("/home/xilinx/my_bitstream.bit");
  
  PYNQ_SHARED_MEMORY shared_memory_1, shared_memory_2;
  PYNQ_allocatedSharedMemory(&shared_memory_1, sizeof(int)*10, 1);
  PYNQ_allocatedSharedMemory(&shared_memory_2, sizeof(int)*10, 1);
  
  int * d1=(int*)shared_memory_1.pointer;
  int * d2=(int*)shared_memory_2.pointer;
  for (int i=0;i<10;i++) {
    d1[i]=i;
    d2[i]=0;
  }
  
  PYNQ_AXI_DMA dma;
  PYNQ_openDMA(&dma, 0x40400000);
  
  PYNQ_writeDMA(&dma, &shared_memory_1, 0, sizeof(int) * 10);
  PYNQ_readDMA(&dma, &shared_memory_2, 0, sizeof(int) * 10);

  PYNQ_waitForDMAComplete(&dma, AXI_DMA_WRITE);
  PYNQ_waitForDMAComplete(&dma, AXI_DMA_READ);

  for (int i=0;i<10;i++) {
    printf("%d %d %d\n", i, d1[i], d2[i]);
  }

  PYNQ_closeDMA(&dma);
  PYNQ_freeSharedMemory(&shared_memory_1);
  PYNQ_freeSharedMemory(&shared_memory_2);
  return 0;
}

以下のように実行すると数を二倍した結果を得られます。

$ g++ -o my_dma DMA.cpp -lpynq  -lcma -lpthread -O3
$./my_dma
0 0 0
1 1 2
2 2 4
3 3 6
4 4 8
5 5 10
6 6 12
7 7 14
8 8 16
9 9 18

C API for PYNQ : C++でPYNQを使いたかった

PYNQの問題点

FPGAのDMA転送やレジスタ書き込みがPythonから行えるPYNQは通常のFPGA開発と比較してデバッグが非常に容易になり、プログラミングも楽になる素晴らしい環境です。めんどくさいレジスタの設定も自動でやってくれます。

ただ、Pythonというのは諸刃の剣であり、演算が遅かったりGILが邪魔だったりで、FPGAを使いたい場面だとPythonの性質に足を引っ張られます。例えば信号をマルチスレッドでPYNQからDMAで取得したデータをCPUでパイプラインを組んで処理したい!と思っても、Pythonのマルチプロセスのキューライブラリを使うと遅すぎてまともにプログラムが動かないなんてことに陥ります。このような性質はFPGAが得意とするリアルタイム性が用いられるケースと相性が最悪です。

このようにPYNQはPythonでサクサク書いてデバッグできるという性質とFPGAが必要な場面では遅すぎるという二面性があるわけです。

回避するためには通常C言語でZYNQのapiを使ってプログラミングすればよいのでしょうが、多くがubuntuなどのPYNQと同じLinux上で動かすことを想定しておらず、VIVADOのIDE上でプログラムを開発するものが多いので、PYNQで開発したものを同じ環境で高速化したいと思うとちょっとめんどくさいです。

本記事はPYNQのAPIと似たい関数をC言語で呼び出せるC API for PYNQを紹介します。このプロジェクトではC言語でPYNQの基本的な機能をC言語で実装しています。本稿はとりあえずC API for PYNQを使ってLEDを点滅させるまでたどり着いた作業メモです。

C API for PYNQのインストール

インストールは公式のGithubに従ってください

github.com

make
sudo make install

ライブラリは以上のコマンドをPYNQubuntu上で実行することでインストールができます

回路設計

適当に回路を作ります。AXI4Liteから4つのレジスタにアクセスして、それぞれのレジスタにLEDを割り当ててます。

適当なコード

myip_v1_0_S00_AXI.v

`timescale 1 ns / 1 ps

    module myip_v1_0_S00_AXI #
    (
        // Users to add parameters here

        // User parameters ends
        // Do not modify the parameters beyond this line

        // Width of S_AXI data bus
        parameter integer C_S_AXI_DATA_WIDTH   = 32,
        // Width of S_AXI address bus
        parameter integer C_S_AXI_ADDR_WIDTH   = 4
    )
    (
        // Users to add ports here

        // User ports ends
        // Do not modify the ports beyond this line

        // Global Clock Signal
        input wire  S_AXI_ACLK,
        // Global Reset Signal. This Signal is Active LOW
        input wire  S_AXI_ARESETN,
        // Write address (issued by master, acceped by Slave)
        input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_AWADDR,
        // Write channel Protection type. This signal indicates the
            // privilege and security level of the transaction, and whether
            // the transaction is a data access or an instruction access.
        input wire [2 : 0] S_AXI_AWPROT,
        // Write address valid. This signal indicates that the master signaling
            // valid write address and control information.
        input wire  S_AXI_AWVALID,
        // Write address ready. This signal indicates that the slave is ready
            // to accept an address and associated control signals.
        output wire  S_AXI_AWREADY,
        // Write data (issued by master, acceped by Slave) 
        input wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_WDATA,
        // Write strobes. This signal indicates which byte lanes hold
            // valid data. There is one write strobe bit for each eight
            // bits of the write data bus.    
        input wire [(C_S_AXI_DATA_WIDTH/8)-1 : 0] S_AXI_WSTRB,
        // Write valid. This signal indicates that valid write
            // data and strobes are available.
        input wire  S_AXI_WVALID,
        // Write ready. This signal indicates that the slave
            // can accept the write data.
        output wire  S_AXI_WREADY,
        // Write response. This signal indicates the status
            // of the write transaction.
        output wire [1 : 0] S_AXI_BRESP,
        // Write response valid. This signal indicates that the channel
            // is signaling a valid write response.
        output wire  S_AXI_BVALID,
        // Response ready. This signal indicates that the master
            // can accept a write response.
        input wire  S_AXI_BREADY,
        // Read address (issued by master, acceped by Slave)
        input wire [C_S_AXI_ADDR_WIDTH-1 : 0] S_AXI_ARADDR,
        // Protection type. This signal indicates the privilege
            // and security level of the transaction, and whether the
            // transaction is a data access or an instruction access.
        input wire [2 : 0] S_AXI_ARPROT,
        // Read address valid. This signal indicates that the channel
            // is signaling valid read address and control information.
        input wire  S_AXI_ARVALID,
        // Read address ready. This signal indicates that the slave is
            // ready to accept an address and associated control signals.
        output wire  S_AXI_ARREADY,
        // Read data (issued by slave)
        output wire [C_S_AXI_DATA_WIDTH-1 : 0] S_AXI_RDATA,
        // Read response. This signal indicates the status of the
            // read transfer.
        output wire [1 : 0] S_AXI_RRESP,
        // Read valid. This signal indicates that the channel is
            // signaling the required read data.
        output wire  S_AXI_RVALID,
        // Read ready. This signal indicates that the master can
            // accept the read data and response information.
        input wire  S_AXI_RREADY,
        
        output wire [3:0] LED

    );


    // AXI4LITE signals
    reg [C_S_AXI_ADDR_WIDTH-1 : 0]     axi_awaddr;
    reg   axi_awready;
    reg   axi_wready;
    reg [1 : 0]     axi_bresp;
    reg   axi_bvalid;
    reg [C_S_AXI_ADDR_WIDTH-1 : 0]     axi_araddr;
    reg   axi_arready;
    reg [C_S_AXI_DATA_WIDTH-1 : 0]     axi_rdata;
    reg [1 : 0]     axi_rresp;
    reg   axi_rvalid;

    // Example-specific design signals
    // local parameter for addressing 32 bit / 64 bit C_S_AXI_DATA_WIDTH
    // ADDR_LSB is used for addressing 32/64 bit registers/memories
    // ADDR_LSB = 2 for 32 bits (n downto 2)
    // ADDR_LSB = 3 for 64 bits (n downto 3)
    localparam integer ADDR_LSB = (C_S_AXI_DATA_WIDTH/32) + 1;
    localparam integer OPT_MEM_ADDR_BITS = 1;
    //----------------------------------------------
    //-- Signals for user logic register space example
    //------------------------------------------------
    //-- Number of Slave Registers 4
    reg [C_S_AXI_DATA_WIDTH-1:0]   slv_reg0;
    reg [C_S_AXI_DATA_WIDTH-1:0]   slv_reg1;
    reg [C_S_AXI_DATA_WIDTH-1:0]   slv_reg2;
    reg [C_S_AXI_DATA_WIDTH-1:0]   slv_reg3;
    wire   slv_reg_rden;
    wire   slv_reg_wren;
    reg [C_S_AXI_DATA_WIDTH-1:0]    reg_data_out;
    integer    byte_index;
    reg    aw_en;

    assign LED[0] = slv_reg0[0];
    assign LED[1] = ~slv_reg1[0];
    assign LED[2] = slv_reg2[0];
    assign LED[3] = ~slv_reg3[0];
    
    
    
    // I/O Connections assignments

    assign S_AXI_AWREADY = axi_awready;
    assign S_AXI_WREADY  = axi_wready;
    assign S_AXI_BRESP   = axi_bresp;
    assign S_AXI_BVALID  = axi_bvalid;
    assign S_AXI_ARREADY = axi_arready;
    assign S_AXI_RDATA   = axi_rdata;
    assign S_AXI_RRESP   = axi_rresp;
    assign S_AXI_RVALID  = axi_rvalid;
    // Implement axi_awready generation
    // axi_awready is asserted for one S_AXI_ACLK clock cycle when both
    // S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_awready is
    // de-asserted when reset is low.

    always @( posedge S_AXI_ACLK )
    begin
      if ( S_AXI_ARESETN == 1'b0 )
        begin
          axi_awready <= 1'b0;
          aw_en <= 1'b1;
        end 
      else
        begin    
          if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
            begin
              // slave is ready to accept write address when 
              // there is a valid write address and write data
              // on the write address and data bus. This design 
              // expects no outstanding transactions. 
              axi_awready <= 1'b1;
              aw_en <= 1'b0;
            end
            else if (S_AXI_BREADY && axi_bvalid)
                begin
                  aw_en <= 1'b1;
                  axi_awready <= 1'b0;
                end
          else           
            begin
              axi_awready <= 1'b0;
            end
        end 
    end       

    // Implement axi_awaddr latching
    // This process is used to latch the address when both 
    // S_AXI_AWVALID and S_AXI_WVALID are valid. 

    always @( posedge S_AXI_ACLK )
    begin
      if ( S_AXI_ARESETN == 1'b0 )
        begin
          axi_awaddr <= 0;
        end 
      else
        begin    
          if (~axi_awready && S_AXI_AWVALID && S_AXI_WVALID && aw_en)
            begin
              // Write Address latching 
              axi_awaddr <= S_AXI_AWADDR;
            end
        end 
    end       

    // Implement axi_wready generation
    // axi_wready is asserted for one S_AXI_ACLK clock cycle when both
    // S_AXI_AWVALID and S_AXI_WVALID are asserted. axi_wready is 
    // de-asserted when reset is low. 

    always @( posedge S_AXI_ACLK )
    begin
      if ( S_AXI_ARESETN == 1'b0 )
        begin
          axi_wready <= 1'b0;
        end 
      else
        begin    
          if (~axi_wready && S_AXI_WVALID && S_AXI_AWVALID && aw_en )
            begin
              // slave is ready to accept write data when 
              // there is a valid write address and write data
              // on the write address and data bus. This design 
              // expects no outstanding transactions. 
              axi_wready <= 1'b1;
            end
          else
            begin
              axi_wready <= 1'b0;
            end
        end 
    end       

    // Implement memory mapped register select and write logic generation
    // The write data is accepted and written to memory mapped registers when
    // axi_awready, S_AXI_WVALID, axi_wready and S_AXI_WVALID are asserted. Write strobes are used to
    // select byte enables of slave registers while writing.
    // These registers are cleared when reset (active low) is applied.
    // Slave register write enable is asserted when valid address and data are available
    // and the slave is ready to accept the write address and write data.
    assign slv_reg_wren = axi_wready && S_AXI_WVALID && axi_awready && S_AXI_AWVALID;

    always @( posedge S_AXI_ACLK )
    begin
      if ( S_AXI_ARESETN == 1'b0 )
        begin
          slv_reg0 <= 0;
          slv_reg1 <= 0;
          slv_reg2 <= 0;
          slv_reg3 <= 0;
        end 
      else begin
        if (slv_reg_wren)
          begin
            case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
              2'h0:
                for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
                  if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                    // Respective byte enables are asserted as per write strobes 
                    // Slave register 0
                    slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
                  end  
              2'h1:
                for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
                  if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                    // Respective byte enables are asserted as per write strobes 
                    // Slave register 1
                    slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
                  end  
              2'h2:
                for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
                  if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                    // Respective byte enables are asserted as per write strobes 
                    // Slave register 2
                    slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
                  end  
              2'h3:
                for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
                  if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                    // Respective byte enables are asserted as per write strobes 
                    // Slave register 3
                    slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
                  end  
              default : begin
                          slv_reg0 <= slv_reg0;
                          slv_reg1 <= slv_reg1;
                          slv_reg2 <= slv_reg2;
                          slv_reg3 <= slv_reg3;
                        end
            endcase
          end
      end
    end    
       


    // Implement write response logic generation
    // The write response and response valid signals are asserted by the slave 
    // when axi_wready, S_AXI_WVALID, axi_wready and S_AXI_WVALID are asserted.  
    // This marks the acceptance of address and indicates the status of 
    // write transaction.

    always @( posedge S_AXI_ACLK )
    begin
      if ( S_AXI_ARESETN == 1'b0 )
        begin
          axi_bvalid  <= 0;
          axi_bresp   <= 2'b0;
        end 
      else
        begin    
          if (axi_awready && S_AXI_AWVALID && ~axi_bvalid && axi_wready && S_AXI_WVALID)
            begin
              // indicates a valid write response is available
              axi_bvalid <= 1'b1;
              axi_bresp  <= 2'b0; // 'OKAY' response 
            end                   // work error responses in future
          else
            begin
              if (S_AXI_BREADY && axi_bvalid) 
                //check if bready is asserted while bvalid is high) 
                //(there is a possibility that bready is always asserted high)   
                begin
                  axi_bvalid <= 1'b0; 
                end  
            end
        end
    end   

    // Implement axi_arready generation
    // axi_arready is asserted for one S_AXI_ACLK clock cycle when
    // S_AXI_ARVALID is asserted. axi_awready is 
    // de-asserted when reset (active low) is asserted. 
    // The read address is also latched when S_AXI_ARVALID is 
    // asserted. axi_araddr is reset to zero on reset assertion.

    always @( posedge S_AXI_ACLK )
    begin
      if ( S_AXI_ARESETN == 1'b0 )
        begin
          axi_arready <= 1'b0;
          axi_araddr  <= 32'b0;
        end 
      else
        begin    
          if (~axi_arready && S_AXI_ARVALID)
            begin
              // indicates that the slave has acceped the valid read address
              axi_arready <= 1'b1;
              // Read address latching
              axi_araddr  <= S_AXI_ARADDR;
            end
          else
            begin
              axi_arready <= 1'b0;
            end
        end 
    end       

    // Implement axi_arvalid generation
    // axi_rvalid is asserted for one S_AXI_ACLK clock cycle when both 
    // S_AXI_ARVALID and axi_arready are asserted. The slave registers 
    // data are available on the axi_rdata bus at this instance. The 
    // assertion of axi_rvalid marks the validity of read data on the 
    // bus and axi_rresp indicates the status of read transaction.axi_rvalid 
    // is deasserted on reset (active low). axi_rresp and axi_rdata are 
    // cleared to zero on reset (active low).  
    always @( posedge S_AXI_ACLK )
    begin
      if ( S_AXI_ARESETN == 1'b0 )
        begin
          axi_rvalid <= 0;
          axi_rresp  <= 0;
        end 
      else
        begin    
          if (axi_arready && S_AXI_ARVALID && ~axi_rvalid)
            begin
              // Valid read data is available at the read data bus
              axi_rvalid <= 1'b1;
              axi_rresp  <= 2'b0; // 'OKAY' response
            end   
          else if (axi_rvalid && S_AXI_RREADY)
            begin
              // Read data is accepted by the master
              axi_rvalid <= 1'b0;
            end                
        end
    end    

    // Implement memory mapped register select and read logic generation
    // Slave register read enable is asserted when valid address is available
    // and the slave is ready to accept the read address.
    assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
    always @(*)
    begin
          // Address decoding for reading registers
          case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
            2'h0   : reg_data_out <= slv_reg0;
            2'h1   : reg_data_out <= slv_reg1;
            2'h2   : reg_data_out <= slv_reg2;
            2'h3   : reg_data_out <= slv_reg3;
            default : reg_data_out <= 0;
          endcase
    end

    // Output register or memory read data
    always @( posedge S_AXI_ACLK )
    begin
      if ( S_AXI_ARESETN == 1'b0 )
        begin
          axi_rdata  <= 0;
        end 
      else
        begin    
          // When there is a valid read address (S_AXI_ARVALID) with 
          // acceptance of read address by the slave (axi_arready), 
          // output the read dada 
          if (slv_reg_rden)
            begin
              axi_rdata <= reg_data_out;     // register read data
            end   
        end
    end    

    // Add user logic here

    // User logic ends

    endmodule

ラップしてるやつ

myip_v1_0.v

`timescale 1 ns / 1 ps

    module myip_v1_0 #
    (
        // Users to add parameters here

        // User parameters ends
        // Do not modify the parameters beyond this line


        // Parameters of Axi Slave Bus Interface S00_AXI
        parameter integer C_S00_AXI_DATA_WIDTH = 32,
        parameter integer C_S00_AXI_ADDR_WIDTH = 4
    )
    (
        // Users to add ports here

        // User ports ends
        // Do not modify the ports beyond this line


        // Ports of Axi Slave Bus Interface S00_AXI
        input wire  s00_axi_aclk,
        input wire  s00_axi_aresetn,
        input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_awaddr,
        input wire [2 : 0] s00_axi_awprot,
        input wire  s00_axi_awvalid,
        output wire  s00_axi_awready,
        input wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_wdata,
        input wire [(C_S00_AXI_DATA_WIDTH/8)-1 : 0] s00_axi_wstrb,
        input wire  s00_axi_wvalid,
        output wire  s00_axi_wready,
        output wire [1 : 0] s00_axi_bresp,
        output wire  s00_axi_bvalid,
        input wire  s00_axi_bready,
        input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_araddr,
        input wire [2 : 0] s00_axi_arprot,
        input wire  s00_axi_arvalid,
        output wire  s00_axi_arready,
        output wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_rdata,
        output wire [1 : 0] s00_axi_rresp,
        output wire  s00_axi_rvalid,
        input wire  s00_axi_rready,
        output LED0,
        output LED1,
        output LED2,
        output LED3

    );
// Instantiation of Axi Bus Interface S00_AXI
    myip_v1_0_S00_AXI # ( 
        .C_S_AXI_DATA_WIDTH(C_S00_AXI_DATA_WIDTH),
        .C_S_AXI_ADDR_WIDTH(C_S00_AXI_ADDR_WIDTH)
    ) myip_v1_0_S00_AXI_inst (
        .S_AXI_ACLK(s00_axi_aclk),
        .S_AXI_ARESETN(s00_axi_aresetn),
        .S_AXI_AWADDR(s00_axi_awaddr),
        .S_AXI_AWPROT(s00_axi_awprot),
        .S_AXI_AWVALID(s00_axi_awvalid),
        .S_AXI_AWREADY(s00_axi_awready),
        .S_AXI_WDATA(s00_axi_wdata),
        .S_AXI_WSTRB(s00_axi_wstrb),
        .S_AXI_WVALID(s00_axi_wvalid),
        .S_AXI_WREADY(s00_axi_wready),
        .S_AXI_BRESP(s00_axi_bresp),
        .S_AXI_BVALID(s00_axi_bvalid),
        .S_AXI_BREADY(s00_axi_bready),
        .S_AXI_ARADDR(s00_axi_araddr),
        .S_AXI_ARPROT(s00_axi_arprot),
        .S_AXI_ARVALID(s00_axi_arvalid),
        .S_AXI_ARREADY(s00_axi_arready),
        .S_AXI_RDATA(s00_axi_rdata),
        .S_AXI_RRESP(s00_axi_rresp),
        .S_AXI_RVALID(s00_axi_rvalid),
        .S_AXI_RREADY(s00_axi_rready),
        .LED({LED3,LED2,LED1,LED0})
    );

    // Add user logic here

    // User logic ends

    endmodule

まあ適当にブロック接続に投げ込んでください。 実際のLEDにはPYNQ-Z2の制約ファイルを探して接続してください

この辺が参考になるかも https://qiita.com/nishimuraatsushi/items/3e5f161eca4b2a0f06c8

プログラミング

PYNQの実行ではbitファイルとhwhファイルを扱うと思うのですがこのhwhファイルはXML形式なのでパーサが必要となります。今回はRapidXMLを使いました。

高速軽量XMLパーサ:RapidXMLを触ってみた (1/2):CodeZine(コードジン)

軽量ライブラリなのでincludeするだけです。

いよいよメインのLED点滅です。以下のコードを実行するとレジスタが1と0で書き代わり偶数版のLEDと奇数版のLEDが交互に点滅します。

#include "rapidxml.hpp"
#include "rapidxml_utils.hpp" // rapidxml::file
#include <iostream>
#include <stdio.h>
#include <thread>
#include <chrono>
//pynq_apiはC言語なのでこうじゃ
extern "C"{
    #include <pynq_api.h>
}

using namespace std;
namespace rx = rapidxml;
using std::this_thread::sleep_for;

constexpr int TIME_TO_SLEEP = 100;

//hwhファイルを差し替えても動くようにパーサを使ってアドレスを探せるようにします。汚くてごめんね
string get_ip_addr(string path) {
  rx::xml_document<> doc;
  rx::file<> input("Lchika_test.hwh");
  doc.parse<0>(input.data());  
  rx::xml_node<>* node = doc.first_node("EDKSYSTEM")->first_node("MODULES")->first_node("MODULE")->first_node("PARAMETERS");
  for ( rx::xml_node<>* child = node->first_node(); child != nullptr; child = child->next_sibling() ) {
    rx::xml_attribute<>* attr = child->first_attribute("NAME");
    if( attr->value() ==  std::string("C_BASEADDR")){
      return  child->first_attribute("VALUE")->value();
    }
  }  
  return nullptr;
}

int main(void){
  //hwhファイルからアドレスを探す 
  string addr_str = get_ip_addr("Lchika_test.hwh");
  int addr = strtol(addr_str.c_str(), NULL, 16);
  // ビットファイルを展開する
  PYNQ_loadBitstream("Lchika_test.bit");

  // メモリマップドIOを使って各レジスタにアクセスできるようにする。
  PYNQ_MMIO_WINDOW led;
  PYNQ_createMMIOWindow( &led, addr, 32);

  int val = 1;

  for(int i =0; i < 100; i++){
      // 点滅
      val = ~val;
      //ちょっと遅延
      sleep_for(std::chrono::milliseconds(TIME_TO_SLEEP));
      //各レジスタに書き込み
      PYNQ_writeMMIO(&led, &val, 0, 4);
      PYNQ_writeMMIO(&led, &val, 4, 4);
      PYNQ_writeMMIO(&led, &val, 8, 4);
      PYNQ_writeMMIO(&led, &val, 12, 4);
  }

  return 0;
}

以下のコマンドでコンパイルします。

g++ -o my_code main.cpp -lpynq  -lcma -lpthread -O3

公式のドキュメントでは-lpynq_apiでライブラリを呼べと書いてあるのですが、/usr/libを見るとlibpynqというファイル名なので-lpynqが正解だと思います。