(H8移植編その2第6回)TCPを実装した

2010/10/21

あなたは 人目のお客様です.

えー,TCPの実装をチマチマ進めていたのだけど,ようやく動きました. 簡単なwebサーバが動きます.

内容としては,前回のコードに対してTCPプロトコルと httpサーバを実装したというものなのだけど,シミュレータ側にちょっとバグがあった ので直した.あとIPまわりとかも多少直してある.

以下,修正したソース.

シミュレータに修正を入れてあるので,GDBにもパッチを当てる必要がある. これは今までと同様に,前回までのgdbのコードに対して 上記ソースコードのgdbというフォルダにある device.c に対するパッチを当てて gdbをビルドしなおすこと.device.cをそのまま置き換えるのでもよい.

TCPの実装は説明し出すときりがないのでやめとく. シーケンス番号の扱いとかの細かいことがよくわからなかったのだけど, ここ とかを参考にして,あとは実際の通信をキャプチャして見てみることで, なんとか実装できた.TCPの処理スレッドとして tcp.c と,あとhttpサーバとして httpd.c が追加されている.

あと,その他のファイルで修正したものについてちょっと説明しておく.

まず,ip.cの修正.

diff -ruN -U 10 h8_05/os/ip.c h8_06/os/ip.c
--- h8_05/os/ip.c	Sun Oct 10 17:28:42 2010
+++ h8_06/os/ip.c	Thu Oct 21 21:04:03 2010
@@ -99,21 +100,21 @@
   iphdr->length = pkt->size;
   iphdr->id = id++;
   iphdr->fragment = 0;
   iphdr->ttl = 64;
   iphdr->protocol = pkt->option.ip.send.protocol;
 
   iphdr->src_addr = my_ipaddr;
   iphdr->dst_addr = pkt->option.ip.send.dst_addr;
 
   iphdr->checksum = 0;
-  iphdr->checksum = ip_calc_checksum(pkt->size, pkt->top);
+  iphdr->checksum = ip_calc_checksum(hdrlen, iphdr);
 
   pkt->cmd = ETHERNET_CMD_SEND;
   memset(pkt->option.ethernet.send.dst_macaddr, 0, MACADDR_SIZE);
   pkt->option.ethernet.send.type = ETHERNET_TYPE_IP;
   pkt->option.ethernet.send.dst_ipaddr = iphdr->dst_addr;
   kz_send(MSGBOX_ID_ETHPROC, 0, (char *)pkt);
 
   return 1;
 }
 
チェックサムの計算範囲が間違っていた.どうもIPのチェックサムはパケット全体 ではなくて,IPヘッダのみで計算すればいいみたい.ていうかなぜこれで今まで pingが通っていたのか謎.wiresharkで確認したときも,とくにエラーにはなって いなかったように思うのだが...謎.

あと,TCPスレッドにIPアドレスを通知する処理を追加.

@@ -146,19 +147,24 @@
 
 int ip_main(int argc, char *argv[])
 {
   struct netbuf *buf;
   int ret;
 
   buf = kz_kmalloc(sizeof(*buf));
   buf->cmd = ARP_CMD_IPADDR;
   buf->option.common.ipaddr.addr = IPADDR;
   kz_send(MSGBOX_ID_ARPPROC, 0, (char *)buf);
+
+  buf = kz_kmalloc(sizeof(*buf));
+  buf->cmd = TCP_CMD_IPADDR;
+  buf->option.common.ipaddr.addr = IPADDR;
+  kz_send(MSGBOX_ID_TCPPROC, 0, (char *)buf);
 
   while (1) {
     kz_recv(MSGBOX_ID_IPPROC, NULL, (char **)&buf);
     ret = ip_proc(buf);
     if (!ret) kz_kmfree(buf);
   }
 
   return 0;
 }
次に netdrv.c の修正.

実は今までは1回の割り込みで1つのパケットしか取り込んでいなかったため, パケットを短期間で複数受信したときに2つ目以降を取り込めず,そのまま滞留 していってしまうというバグがあった(TCPの通信テストをしていて発覚した).

ということで割り込み処理で,パケットの複数受信に対応する.

diff -ruN -U 10 h8_05/os/netdrv.c h8_06/os/netdrv.c
--- h8_05/os/netdrv.c	Sun Oct 10 17:28:42 2010
+++ h8_06/os/netdrv.c	Thu Oct 21 21:04:03 2010
@@ -7,35 +7,40 @@
 #include "ethernet.h"
 #include "netdrv.h"
 
 static unsigned char my_macaddr[MACADDR_SIZE];
 
 /* 割込みハンドラ */
 static void netdrv_intr(void)
 {
   struct netbuf *pkt;
   if (rtl8019_checkintr(0)) {
-    pkt = kx_kmalloc(DEFAULT_NETBUF_SIZE);
-    memset(pkt, 0, DEFAULT_NETBUF_SIZE);
-    pkt->cmd  = ETHERNET_CMD_RECV;
+    while (1) {
+      pkt = kx_kmalloc(DEFAULT_NETBUF_SIZE);
+      memset(pkt, 0, DEFAULT_NETBUF_SIZE);
+      pkt->cmd  = ETHERNET_CMD_RECV;
 
-    /*
-     * ethernetフレームが14バイトで4の倍数でないので,ethernetフレーム以降が
-     * 4バイトアラインメントされるように,データの先頭を2バイト空ける.
-     */
-    pkt->top  = pkt->data + 2;
+      /*
+       * ethernetフレームが14バイトで4の倍数でないので,ethernetフレーム以降が
+       * 4バイトアラインメントされるように,データの先頭を2バイト空ける.
+       */
+      pkt->top  = pkt->data + 2;
 
-    pkt->size = rtl8019_recv(0, pkt->top);
-    if (pkt->size > 0)
-      kx_send(MSGBOX_ID_ETHPROC, 0, (char *)pkt);
-    else
-      kx_kmfree(pkt);
+      pkt->size = rtl8019_recv(0, pkt->top);
+      if (pkt->size > 0) {
+	kx_send(MSGBOX_ID_ETHPROC, 0, (char *)pkt);
+      } else {
+	kx_kmfree(pkt);
+	break;
+      }
+    }
+    rtl8019_clearintr(0);
   }
 }
 
 static int netdrv_init(void)
 {
   rtl8019_init(0, my_macaddr);
   return 0;
 }
 
 /* スレッドからの要求を処理する */
RTL8019ドライバにもパケット複数受信の対応を入れる.
diff -ruN -U 10 h8_05/os/rtl8019.c h8_06/os/rtl8019.c
--- h8_05/os/rtl8019.c	Sun Oct 10 17:28:42 2010
+++ h8_06/os/rtl8019.c	Thu Oct 21 21:04:03 2010
@@ -282,21 +282,21 @@
 #if 0
   status = *H8_3069F_ISR;
   return (status & 0x20) ? 1 : 0;
 #else
   *NE2000_CR = NE2000_CR_P0 | NE2000_CR_RD_ABORT | NE2000_CR_STA;
   status = *NE2000_ISR;
   return (status & 0x01) ? 1 : 0;
 #endif
 }
 
-static int clear_irq(int index)
+int rtl8019_clearintr(int index)
 {
   if (rtl8019_checkintr(index)) {
     *H8_3069F_ISR = 0x00;
     *NE2000_CR    = NE2000_CR_P0 | NE2000_CR_RD_ABORT | NE2000_CR_STA;
     *NE2000_ISR   = 0xff;
   }
   return 0;
 }
 
 int rtl8019_recv(int index, char *buf)
@@ -321,30 +321,28 @@
   read_data(start * 256, 4, header);
 
   size = ((int)header[3] << 8) + header[2] - 4;
   read_data((start * 256) + 4, size, buf);
 
   *NE2000_CR = NE2000_CR_P0 | NE2000_CR_RD_ABORT | NE2000_CR_STA;
   if (header[1] == NE2000_RP_START)
     header[1] = NE2000_RP_STOP;
   *NE2000_BNRY = header[1] - 1;
 
-  clear_irq(index);
-
   return size;
 }
 
 int rtl8019_send(int index, int size, char *buf)
 {
   write_data(NE2000_TP_START * 256, size, buf);
         
-  if(size < 60)
+  if (size < 60)
     size = 60;
 
   *NE2000_CR    = NE2000_CR_P0 | NE2000_CR_RD_ABORT | NE2000_CR_STA;
   *NE2000_TBCR0 = size & 0xff;
   *NE2000_TBCR1 = (size >> 8) & 0xff;
   *NE2000_TPSR  = NE2000_TP_START;
   *NE2000_CR    = NE2000_CR_P0 | NE2000_CR_RD_ABORT | NE2000_CR_TXP | NE2000_CR_STA;
   while ((*NE2000_CR & NE2000_CR_TXP) != 0)
     ;
 
さらにこれはシミュレータの話なのだけど, パケット受信時にしか割り込みフラグを立てる処理を行っていなかったため, 割り込み無効の状態でパケット受信すると,その後割り込み有効にしても 割り込みが入らないというバグがあった.

まあ実際にはRTL8019を割り込み無効にしているタイミングは動作中は無いので 実害は無いと思われるのだが,一応修正しておく.

--- device.c.old4	Thu Oct 21 21:05:27 2010
+++ device.c	Thu Oct 21 12:53:28 2010
@@ -558,7 +558,7 @@
   }
 
   /* 割り込みのクリア */
-  if ((page == 0) && (NE2000_ISR == 0xff))
+  if (NE2000_ISR == 0xff)
     NE2000_ISR = 0;
 
   /* 受信した */
@@ -601,11 +601,12 @@
 
       NE2000_CURR = new_curr;
 
-      if (NE2000_IMR & 0x01) {
-	NE2000_ISR |= 0x01;
-	vector = 17; /* IRQ5 */
-      }
+      NE2000_ISR |= 0x01;
     }
+  }
+
+  if ((NE2000_ISR & 0x01) && (NE2000_IMR & 0x01)) {
+    vector = 17; /* IRQ5 */
   }
 
   /* 送信処理 */
ここまでで修正は終わり.今回はシミュレータでデバッグをしていたのだけど, TCP処理部だけでなくシミュレータ側やパケット受信処理にバグがあって苦戦した. まあでもシミュレータでデバッグできて非常にたすかった.

では実行してみよう.まずいつもどおりファーム作成する. いつもどおりFreeBSDでシミュレータを起動してネットワークの設定 (TAPデバイスの設定とか)をしておく.このへんは前回までと, あとシミュレータの話のあたりを参照. KOZOSのhttpサーバは 192.168.10.16 で動作するので,PC側は 192.168.10.1 あたりに 設定しておく.

PCから telnet 192.168.10.16 80 とかすることで80番ポートに接続して, httpサーバの応答を試すことができる.

hiroaki@teapot:~>% telnet 192.168.10.16 80
Trying 192.168.10.16...
Connected to 192.168.10.16.
Escape character is '^]'.

うまく接続できた.

HTTPのGETコマンドを発行してみる.

hiroaki@teapot:~>% telnet 192.168.10.16 80
Trying 192.168.10.16...
Connected to 192.168.10.16.
Escape character is '^]'.
GET / HTTP/1.1
HTTP/1.0 200 OK
Server: KOZOS-httpd/1.0
Content-Type: text/html
Content-Length: 395  
...
おー,ちゃんとHTTP通信できている.

ブラウザで 192.168.10.16 に接続してみよう.

トップページが見れている.リンクをクリックしてみよう.

多少動作は鈍いが,ちゃんと見れている. KOZOSのhttpサーバで,KOZOSの説明のホームページが動作している. つまりKOZOS自身がKOZOSの説明をしているわけだ. すげー.感動.ちゃんと動いてる.

とりあえずはシミュレータでの動作しか確認していない.あとFreeBSDとの接続しか 試していないのだが,まあとりあえず動いている.

ちなみに現状のソースコード量は以下.

hiroaki@teapot>% wc -l rtl8019.[ch] netdrv.[ch] ethernet.[ch] arp.[ch] ip.[ch] tcp.[ch] httpd.c 
     350 rtl8019.c
      13 rtl8019.h
      84 netdrv.c
      92 netdrv.h
     115 ethernet.c
       8 ethernet.h
     234 arp.c
       9 arp.h
     170 ip.c
      10 ip.h
     642 tcp.c
      12 tcp.h
     319 httpd.c
    2058 total
hiroaki@teapot>% 
これは現状でのネットワーク関連のソースコードのステップ数だが, まあhttpdはHTMLドキュメントがあるからしかたないとして,多いのはRTLドライバと TCP通信部だ.RTLドライバはレジスタの定義が多いのでこれもしかたがないとして, TCPはやっぱりそれなりの量になってしまった.うーん,書籍化を考えたとき, これはちょっとなんとかしないとなあ.

まあこれでTCP/IPが動作した.OSC新潟では,今までのひとつの目標であった 「KOZOSでwebサーバ」の実演ができそうだ.やったー.


メールは kozos(アットマーク)kozos.jp まで