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の使い方ではまっても責任はとれませんが…)


コメント