小規模マイクロコントローラ用TCP/IPプロトコルスタックuSockの作成


はじめに

AVRなどの,少量のROMやRAMしか持たないマイクロコントローラーをLANにつないで通信を行うための,TCPやUDPが扱えるソケットライブラリを作成しました.

特徴

このライブラリはC言語で書かれており,TCP/UDP/IP/ICMP(echo)/ARP/Ethernetのプロトコルを処理します.
特に,以下の3点を重視して開発しました:

ただし,これらの特徴を実現するため,いくつか設計上割り切って考えた制限事項もあります:

基本的な使い方

前述のとおり,このライブラリはusock_open(),usock_close(),usock_alloc(),usock_recv(),usock_send()の5つのAPIを持っています.
簡単に言えば,usock_open()はローカルホストとリモートホストの間に新しいコネクションを張り,usock_close()は既存のコネクションを閉じます.usock_recv()はリモートホストから送られるデータを受信し,usock_send()はリモートホストへデータを送信します.usock_alloc()は,usock_send()で送信するためのデータを置くバッファを確保します.

例えば,基本的なクライアントアプリケーションは次のような感じになります:

{
  usock_open(...); /* コネクションを新たに作る */
  usock_alloc(...); /* データ送信用のバッファを確保する */
  ..... /* データをバッファに移す */
  usock_send(...); /* データをリモートホストへ送信する */
  usock_recv(...); /* リモートホストからの返事を受信する */
  ..... /* 受け取ったデータを処理する */
  usock_close(...); /* コネクションを閉じる */
}

送信するデータを置いておくバッファや,受信したデータを蓄えておくバッファは,同一のメモリ領域を使いまわして,メモリを節約するようにしています.

APIの説明

このライブラリは,5つの関数の他に,いくつかのマクロと大域変数を利用することができます. usock_alloc()以外の4つの関数は,リモートホストとの通信が発生するため時間がかかることがあるため,いずれの関数も大域変数Usock_timeoutで指定された時間が経過するとタイムアウトするようになっています. また5つのいずれの関数を実行した場合も,大域変数Usock_dataとUsock_sizeで管理される送受信バッファの内容は破壊されます.

関数

int8_t usock_open(uint8_t mode, uint16_t lport, uint16_t rport, uint8_t *raddr)

ローカルホストとリモートホストの間に新しいコネクションを張ります.この関数はコネクションの作成に失敗した場合は-1を返し,成功した場合はソケットの記述子を返します.この記述子は,以降の通信でこのコネクションを指定するために利用されます. modeUSOCK_UDPUSOCK_TCPを指定することで,プロトコルとしてUDPかTCPのどちらを使うかを指定します. lportはローカルホストのポート番号,rportはリモートホストのポート番号,raddrはリモートホストのIPアドレスを表す4つの要素からなる配列です.lportに0を指定した場合は,ローカルホストのポート番号は,空いているポート番号から自動的に選択して割り当てます. rportに0以外の値を指定した場合はクライアントとして,0の値を指定した場合はサーバとしての動作になります.ここで クライアントというのはリモートホストのIPアドレスとポート番号が決まっており先に接続を行う側のホストで,サーバというのはリモートホストのIPアド レスとポート番号は決まっておらず,どのIPアドレスやポート番号からも接続できる状態で,接続されるのを待っている側のホストのことです.rportに0を指定した場合raddrの値は利用されません.特定の1つまたは複数のホストからだけの接続を受け付けるような動作はできないため,必要であればコネクションが成立した後でリモートホストのIPアドレスやポート番号を調べ,再接続するかどうかを判定するようにします.

UDPのサーバとしてこの関数を実行した場合,大域変数Usock_dataに最初に受信したデータの中身が,Usock_sizeにその長さが保存されます.

lportの値もrportの値も0を指定して呼び出した場合,この関数はまだデバイスの初期化が行われていなければその初期化を行い,-1を返します.そのため,デバイスの初期化だけを行う目的でこの関数を呼び出すこともできます.

void usock_close(int8_t sd)

既存のコネクションsdを閉じます.

void usock_alloc(int8_t sd)

大域変数Usock_dataに,コネクションsdで送信に使うためのバッファ領域を確保します.またバッファ領域のサイズが大域変数Usock_maxに格納されます. usock_send()でデータの送信を行う前に,毎回必ずこの関数を呼び出してバッファ領域を確保する必要があります.

int8_t usock_recv(int8_t sd)

コネクションsdのリモートホストからデータを受信して,受信したデータを大域変数Usock_dataに,その長さをUsock_sizeに格納します. 戻り値としては,正常に終了した場合は0を,タイムアウトした場合は1を返します.

uint16_t usock_send(int8_t sd)

コネクションsdのリモートホストへ,大域変数Usock_dataに格納されている長さがUsock_sizeのデータを送信します. 送信後にリモートホストから受信した確認応答(ACK)に含まれるデータがUsock_sizeに,その長さがUsock_sizeに格納されます. 戻り値としては,送信に成功したデータのサイズを返します.通信路の制限によりデータの先頭の一部しか送れなかった場合はUsock_sizeよりも小さい値を,タイムアウト等のエラーが起きたり送信方向の接続が切れている場合は0を返します.

TCPのコネクションの場合は,Usock_sizeに0を指定してこの関数を呼び出すと特別な意味を持ち,その場合送信方向の接続を切断します(shutdown).その場合でも,リモートホストからのデータの受信は行うことができます.

マクロ

int USOCK_RDEOF(sd)

コネクションsdの受信方向の接続が切断されていれば0以外を,そうでなければ0を返します.

int USOCK_WREOF(sd)

コネクションsdの送信方向の接続が切断されていれば0以外を,そうでなければ0を返します.

uint16_t USOCK_LPORT(sd)

コネクションsdのローカルホストのポート番号を返します.

uint16_t USOCK_RPORT(sd)

コネクションsdのリモートホストのポート番号を返します.

uint8_t *USOCK_RADDR(sd)

コネクションsdのリモートホストのIPアドレスの配列(4バイト)を返します.

大域変数

uint8_t Usock_ipaddress[4]

ローカルホストのIPアドレス.

uint8_t Usock_netmask[4]

ネットマスク.

uint8_t Usock_defaultrouter[4]

デフォルトルーターのIPアドレス.

uint16_t Usock_timeout

タイムアウト値.100ミリ秒単位で指定する.

uint8_t *Usock_data

データの送受信に使用するバッファ領域.

uint16_t Usock_size

Usock_dataに置かれたデータの長さ.

uint16_t Usock_max

Usock_dataに置くことのできるデータの最大長.

利用例

今回作成したプロトコルスタックの利用例です. いずれのサンプルプログラムもそれほど大きくないのでソースコードを全て記載していますが,ページ末尾からダウンロードできるファイルにも含まれています. 下記の4つの例のうち,Linux上では全てのプログラムを,AVR(ATmega8)上ではhttpdとtftpdの動作を確認しています.なお,ATmega8用のバイナリのサイズは,httpdが7,254バイト,tftpdが7,284バイトでした(uSock version 0.5.WinAVR-20100110を使用).確認には,昔製作したハードを使用しました. なお下記のサンプルプログラムのうち,httpとdnsをLinux上でTAPを利用して動かす場合は,IPマスカレードの設定が必要になる場合があります.例えば, # iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE ; echo 1 > /proc/sys/net/ipv4/ip_forward などとしておきます.

TCPサーバの例(httpd)

非常に簡単なWWWサーバの例です.本文が"hello, world"と書かれたページを返すだけのサーバです.

#include <string.h>
#include "usock.h"
int main(void) {
  int8_t sd;
  char *s;
  for (; ; ) {
    if ((sd = usock_open(USOCK_TCP, 80, 0, NULL)) < 0) continue;
    if (usock_recv(sd) == 0) {
      s = (char *)Usock_data;
      if (Usock_size > 4 && s[0] == 'G' && s[1] == 'E' && s[2] == 'T' && s[3] == ' ') {
        usock_alloc(sd);
        strcpy((char *)Usock_data, "HTTP/1.0 200 OK\r\nContent-Length: 66\r\n\r\n<html><title>sample page</title><body>hello, world</body></html>\r\n");
        Usock_size = strlen((char *)Usock_data);
        usock_send(sd);
      }
    }
    usock_close(sd);
  }
  return 0;
}

実行例

# ./httpd &
# wget -q -O - http://192.168.1.2/
<html><title>sample page</title><body>hello, world</body></html>

TCPクライアントの例(http)

WWWサーバからWebページを取得するクライアントプログラムの例です.

#include <stdio.h>
#include <string.h>
#include "usock.h"
static uint8_t daddr[] = {64, 170, 98, 32}; /* IP address of WWW server*/
int main(void) {
  int8_t sd;
  if ((sd = usock_open(USOCK_TCP, 0, 80, daddr)) < 0) return 1;
  usock_alloc(sd);
  strcpy((char *)Usock_data, "GET / HTTP/1.0\r\n\r\n");
  Usock_size = strlen((char *)Usock_data);
  if (usock_send(sd) == 0) return 1;
  do {
    fwrite(Usock_data, sizeof(uint8_t), Usock_size, stdout);
  } while (!usock_recv(sd) && Usock_size != 0);
  usock_close(sd);
  return 0;
}

実行例

# ./http
HTTP/1.1 302 Found
Date: Thu, 15 Apr 2010 15:10:37 GMT
Server: Apache/2.2.4 (Linux/SUSE) mod_ssl/2.2.4 OpenSSL/0.9.8e PHP/5.2.6 with Suhosin-Patch mod_python/3.3.1 Python/2.5.1 mod_perl/2.0.3 Perl/v5.8.8
Location: http://www.ietf.org/
Content-Length: 204
Connection: close
Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.ietf.org/">here</a>.</p>
</body></html>

UDPサーバの例(tftpd)

UDPを使用してファイルのやりとりを行うtftpのサーバの例です. このtftpサーバは,sampleというファイルをgetする動作にだけ対応しています.

#include <string.h>
#include "usock.h"
int main(void) {
  int8_t i;
  int8_t sd;
  for (; ; ) {
    if ((sd = usock_open(USOCK_UDP, 69, 0, (void *)0)) < 0) continue;
    if (Usock_size < 9 || memcmp(Usock_data, "\x00\x01sample\x00", 9) != 0) {
      usock_alloc(sd);
      memcpy(Usock_data, "\x00\x05\x00\x02Unsupported operation\x00", 26);
      Usock_size = 26;
      usock_send(sd);
      usock_recv(sd); /* ACK */
      usock_close(sd);
      continue;
    }
    for (i = 0; i < 10; i++) { /* retry */
      usock_alloc(sd);
      memcpy(Usock_data, "\x00\x03\x00\x01hello, world\n", 17);
      Usock_size = 17;
      usock_send(sd);
      if (usock_recv(sd)) continue;
      if (Usock_size == 4 && memcmp(Usock_data, "\x00\x04\x00\x01", 4) == 0) break;
    }
    usock_close(sd);
  }
  return 0;
}

実行例

# tftpd &
# tftp 192.168.1.2
tftp> get sample
tftp> quit
# cat sample
hello, world

UDPクライアントの例(dns)

UDPの代表的なアプリケーションであるDNSのクライアントの例です. ドメイン名に対応するIPアドレスをDNSサーバに問い合わせて,結果を表示します.

#include <stdio.h>
#include <string.h>
#include "usock.h"
struct Dns {
  uint16_t id;
  uint8_t opcode;
  uint8_t rcode;
  uint16_t qdcount;
  uint16_t ancount;
  uint16_t nscount;
  uint16_t adcount;
  uint8_t data[1];
};
static uint8_t addr[] = {192, 168, 0, 1}; /* IP address of DNS server */
#define DOMAIN "www.w3.org"
int main(void) {
  int8_t i, j;
  int8_t sd;
  struct Dns *dns;
  if ((sd = usock_open(USOCK_UDP, 0, 53, addr)) < 0) return 1;
  usock_alloc(sd);
  dns = (struct Dns *)Usock_data;
  dns->id = 0;
  dns->opcode = 0x01;
  dns->rcode = 0x00;
  ((uint8_t *)&dns->qdcount)[0] = 0x00;
  ((uint8_t *)&dns->qdcount)[1] = 0x01;
  dns->ancount = 0;
  dns->nscount = 0;
  dns->adcount = 0;
  for (i = 0, j = -1; i < strlen(DOMAIN) + 1; i++) {
    if (i == strlen(DOMAIN) || DOMAIN[i] == '.') {
      dns->data[1 + j] = (i - j) - 1;
      j = i;
    } else {
      dns->data[1 + i] = DOMAIN[i];
    }
  }
  dns->data[i++] = 0x00; dns->data[i++] = 0x00; dns->data[i++] = 0x01; dns->data[i++] = 0x00; dns->data[i++] = 0x01;
  Usock_size = 12 + i;
  usock_send(sd);
  if (usock_recv(sd)) return 1;
  dns = (struct Dns *)Usock_data;
  if (dns->id != 0 || (dns->opcode & 0xfa) != 0x80 || (dns->rcode & 0x0f) != 0x00) return 1;
  if (dns->ancount == 0) return 1;
  i = 0;
  for (j = 0; j < 2; j++) {
    for (; ; ) {
      if (dns->data[i] == 0x00) {
        break;
      } else if (dns->data[i] < 0xc0) {
        i += 1 + dns->data[i];
      } else {
        i += 1;
        break;
      }
    }
    i += 5;
  }
  i += 6;
  printf("%s <-> %d.%d.%d.%d\n", DOMAIN, dns->data[i], dns->data[i + 1], dns->data[i + 2], dns->data[i + 3]);
  usock_close(sd);
  return 0;
}

実行例

# ./dns
www.w3.org <-> 128.30.52.51

デバイスドライバの作成について

このライブラリを新しいネットワークインターフェースデバイスで利用する場合は,デバイスドライバを新たに作成する必要があります.デバイスドライバは,device.hというファイルで宣言されている,下記の3つの関数を持つ必要があります.
詳しいことは,付属のLinux用のTAPドライバや,LANカードの3C589D用のドライバを参考にしてください.
また,本ライブラリで使用しているデバイスドライバのAPIはuIPで使用されているものとほとんど同じであるため(タイムアウトの時間やバッファ領域の渡し方は少し異なります),uIP用に開発されているデバイスドライバを移植するのは容易であると思われます.uIPの移植されたものには,メジャーなRTL8019やENC28J60もあるようです.

void dev_init(void)

これはデバイスを初期化する関数です.uSockのライブラリが使用される際に,最初に1度だけ呼ばれます.

uint16_t dev_recv(uint8_t *data, uint16_t size)

MACフレームを受信する関数です.受信したデータはdataに格納され,その長さを戻り値とします.sizedataに格納することのできるデータの最大長です.
100ミリ秒たってもデータが受信されない場合は,戻り値を0として関数から戻る必要があります.

void dev_send(uint8_t *data, uint16_t size)

MACフレームを送信する関数です.送信するデータはdataに,その長さはsizeに格納されています.

その他・現状の課題など

単純な比較はできませんが,現状でこのuSock本体のAVR用のバイナリファイル(約6kバイト)は,uIP(version 0.6)(約4kバイト)よりも2kバイトほどサイズが大きくなっています.IPアドレスやMACアドレスのコピーなどで無駄なコードが生成されていたりしてコードサイズの最適化がまだ十分できていませんが,コンパイラの最適化に依存する部分も大きくあまりプログラムを複雑にしたくないので,その点に関する改良はまだ未着手です.

現在チェックサムの計算は真面目に行っていますが,IPパケットを包んでいるEthernetフレームでもCRCを計算してチェックが行われているので,受信時の処理速度が問題になりそうな場合はチェックサムの計算を省略するという手もあるかと思います.

現状で見つかっている不具合として,gccでコンパイルする場合には"-fno-inline"オプションを指定しないと正常に動作しない事があります.このオプションを指定せずに"-O3"オプションを付けてコンパイルした場合のアセンブラリストを見たところ,usock.c中の

Ip->checksum = 0x0000;
Ip->checksum = ~chksum((uint8_t *)Ip, 20, 0);

というIPヘッダのチェックサムを計算している部分で,2行目がインライン展開されるとともに,1行目が消えてしまっていました.コンパイラで,1行目が無意味と判断されて消されているようですが,指定する最適化オプションやgccのバージョンにより問題が起こる場合も起こらない場合もあるようです.

ソースコード


[戻る]
2010-04-29 version 0.6にアップデート
2010-04-15 ページ作成
(2010-04 プログラム作成)
T. Nakagawa