季節のe7UDP/IP IPコアのサンプル(二種)

技術紹介

e7UDP/IP IPコアを使うと,FPGAで簡単にUDPパケットをやりとりできます.

…と言っておきながら,分かりやすいサンプルがなかったので用意してみました.

  • ボタンを押したらUDPパケットを送信する回路
  • UDPで送られてきた文字列の大文字を小文字に変換して送り返す回路

の二種類です.

※ 実際に使用を試すにはコアと,トップモジュールのHDLが必要です.

長いですか?たしかに,一見長いように見えますが,実際にパケットを投げている部分は

          when SEND =>

            if to_integer(unsigned(tx_counter)) > 0 then  -- during packet sending
              tx_counter <= std_logic_vector(unsigned(tx_counter) + 1);
            elsif pO0Ack = '1' then     -- ready to send UDP packet
              pO0Request <= '0';
              pO0En      <= '1';        -- start to send a UDP packet
              tx_counter <= std_logic_vector(to_unsigned(1, tx_counter'length));
            else -- ack = '0' and tx_counter = 0 (= before packet sending)
              pO0Request <= '1';
            end if;
            
            case to_integer(unsigned(tx_counter)) is
              when 0 => pO0Data <= pMyIpaddr;
              when 1 => pO0Data <= pServerIpAddr;
              when 2 => pO0Data <= pMyPort & pServerPort;
              when 3 => pO0Data <= X"00000004";
              when 4 => pO0Data <= X"DEADBEEF";
              when others =>
                pO0En <= '0';
                state <= IDLE;
            end case;

という部分です.

この部分は大きく二つのパートで構成されていて,

前半のifで構成されているパートでは,

  • パケット送信中(tx_counter > 0)は毎サイクルパケット送信カウンタをインクリメントする
  • パケット送信開始前(tx_counter = 0)で,送信可能(Ack=’1′)なら送信を開始する
  • パケット送信開始前に送信可能でない(Ack=’0′)ならコアに送信リクエストをだす

という処理をしています.

後半のcaseで構成されているパートでは,クロックにあわせて,UDPパケットを送るための情報をコアに供給しています.

0〜3クロックではパケットヘッダに相当するデータを,4クロック目以降(今回は1クロックだけ)がペイロードに相当するデータを送っています.

パケットを受け取って送り返す回路

今度は,送り返すだけ,の簡単な回路を作ってみます.といっても単に折り返すだけでは,応用しようとする場合の例として分かりにくいので,

  • 受け取ったUPDパケットのデータをメモリに格納する
  • メモリから読み出しつつ,大文字を小文字に変換する

というサンプルを用意してみました.

このHDLでは大きく次の3つのステートを定義しています.一つずつ少し丁寧にみていきます.

  • パケットがくるまで待つ(IDLE)
  • 来たパケットを受信する(RECV)
  • パケットを送信する(SEND)

パケットがくるまで待つ

パケットが来るまでまつ部分のコードはこうです

          
          when IDLE =>
            if pI0En = '1' then         -- An UDP packet is arrived.
              state      <= RECV;
              cSrcIPAddr <= pI0Data;    -- 1st DWORD is source port
              rx_counter <= std_logic_vector(to_unsigned(1, rx_counter'length));
              pI0Ack     <= '0';  -- to make disabled of packet receiving during operation
            else
              rx_counter <= (others => '0');
              pI0Ack     <= '1';
            end if;
            tx_counter <= (others => '0');
            pO0Request <= '0';
            pO0En      <= '0';

e7UDP/IP IPコアは,コアにパケットが到着するとpI0Enを’1’にするので,このステートでは,単にそれを待ち続けています.待っている間は,コアにパケットが到着したらデータを送ってよい,という意味でpI0Ackを’1’にしているというのがポイントでしょうか.

パケットが到着したら,受信ステートに遷移します.

来たパケットを受信する

次は来たパケットを受信しながらメモリに書く部分をみてみます.

          when RECV =>
            pI0Ack <= '0';
            if pI0En = '0' then
              state <= SEND;
            else
              rx_counter <= std_logic_vector(unsigned(rx_counter) + 1);
              case to_integer(unsigned(rx_counter)) is
                when 0 => null; -- consume 1st DWORD at previous state
                when 1 => cDestIPAddr <= pI0Data;
                when 2 =>
                  cDestPort <= pI0Data(31 downto 16);
                  cSrcPort  <= pI0Data(15 downto  0);
                when 3 => cLength <= pI0Data; -- Length & CRC
                when others =>          -- receiving payload
                  ram_we    <= '1';
                  ram_waddr <= std_logic_vector(unsigned(rx_counter) - 4);
                  ram_wdata <= pI0Data;
              end case;
            end if;

コアは受信したデータをサイクル毎に32bitずつ出力してきます.なので,ここでは,受信ルーチンではその出力データを取りこぼすことなく処理する必要があります.送信するとき同様,0〜3サイクルの間はヘッダに関する情報を,4サイクル目以降でペイロードデータを出力してきます.

この回路では,4サイクル以降のデータだけをメモリに書き出しています.

コアは受信したUDPパケットの出力を終えると,pI0Enを’0’にするので,そのタイミングで受信を終えて,送信ステートに遷移しています.

パケットを送信する

最後は送信部分です.本質的には,ボタンを押してパケットを送る回路と変わりはありません.

          when SEND =>
            if to_integer(unsigned(tx_counter)) > 0 then  -- during packet sending
              tx_counter <= std_logic_vector(unsigned(tx_counter) + 1);
            elsif pO0Ack = '1' then     -- ready to send UDP packet
              pO0Request <= '0';
              pO0En      <= '1';        -- start to send a UDP packet
              tx_counter <= std_logic_vector(to_unsigned(1, tx_counter'length));
            else -- ack = '0' and tx_counter = 0 (= before packet sending)
              pO0Request <= '1';
            end if;
            
            case to_integer(unsigned(tx_counter)) is
              when 0 => pO0Data <= cSrcIPAddr;
              when 1 => pO0Data <= cDestIPAddr;
              when 2 => pO0Data <= cDestPort & cSrcPort;
                        ram_raddr <= (others => '0');  -- for next next state
              when 3 => pO0Data <= cLength;  -- length of payload (DWORD)
                        ram_raddr <= std_logic_vector(unsigned(ram_raddr) + 1);
              when others =>
                ram_raddr <= std_logic_vector(unsigned(ram_raddr) + 1);
                pO0Data   <= to_lower(ram_rdata(31 downto 24)) &
                             to_lower(ram_rdata(23 downto 16)) &
                             to_lower(ram_rdata(15 downto  8)) &
                             to_lower(ram_rdata( 7 downto  0));
                if to_integer(unsigned(rx_counter)) = to_integer(unsigned(tx_counter) + 1) then
                  state <= IDLE;
                end if;
            end case;

前半のifの部分では,送信を開始するまでと,送信中のカウンタを処理していて,caseの部分がデータをコアに供給している部分です.

4サイクル以降がペイロードなので,この部分でメモリから読み出したデータに大文字→小文字変換関数を適用した結果をコアに供給しています.

使ってみる

“ボタンを押したらパケットを送る”くらいであればtcpdumpでみればいいのですが,今回はパケットを入力してあげないといけないので,ちょっとプログラムを書いてみます.

RubyでUDP投げる,なら,こんな感じでしょうか.もっとエレガントにかけるかもしれませんが.

require "socket"

TARGET_ADDR = "10.0.0.1"
TARGET_PORT = 16384

def send_recv(socket, data)
  socket.send(data, 0, TARGET_ADDR, TARGET_PORT)
  return socket.recv(1000)
end

udp = UDPSocket.open()
puts send_recv(udp, ARGV[0])

 

引数に与えた文字列が,大文字が小文字に変換された出力されます.

最後に

卒論や修論に向けての実装も佳境を迎えるこの季節,e7UDP/IPを使って面倒なI/O部分は楽してみませんか?(e7UDP/IPの使い方ではまっても責任はとれませんが…)

 

コメント

タイトルとURLをコピーしました