e7UDP/IP IPコアを使うと,FPGAで簡単にUDPパケットをやりとりできます.
…と言っておきながら,分かりやすいサンプルがなかったので用意してみました.
- ボタンを押したらUDPパケットを送信する回路
- UDPで送られてきた文字列の大文字を小文字に変換して送り返す回路
の二種類です.
※ 実際に使用を試すにはコアと,トップモジュールのHDLが必要です.
ボタンを押したらUDPパケットを投げる回路
何をさておき,まずはソースコードをおいておきます.
長いですか?たしかに,一見長いように見えますが,実際にパケットを投げている部分は
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の使い方ではまっても責任はとれませんが…)
コメント