第2章 NICを制御してイーサネットフレームを送受信する
第2章 NICを制御してイーサネットフレームを送受信する
前章でNICのレジスタのベースアドレスを取得できました。
この章では、NICのレジスタを使用して、任意のイーサネットフレームを受け取ったり、特にフォーマットに従っていない「オレオレデータ」を送ってみたり、ということを手っ取り早く行ってみます。
なお、この章では適宜データシートの図を引用します。参照しているデータシートについては本書最後の「参考情報」をご覧ください。
2.1 NICのレジスタの操作方法
まず、NICのレジスタの操作方法を紹介します。
ここでは、NICの割り込み設定の確認と無効化を通して、レジスタ値の取得と設定の方法を紹介します。
なお、データシートで「パケット(packet)」と呼んでいるレジスタ等は本書でも「パケット」と呼称します。これは「IPパケット」ではなく「データの塊」という意味の広義の「packet」で、現在のTCP/IP通信においてNICが送受信するデータの単位としては「イーサネットフレーム」です。NICが送受信するデータの塊を「パケット」と呼称すると「IPパケット」と混同してまぎらわしいので、NICが扱うデータの塊については「イーサネットフレーム(あるいは単にフレーム)」と呼称します。
2.1.1 NICのレジスタ値を取得してみる
BARから取得したベースアドレス以降にNICのレジスタが並んでいて、readアクセスすることでレジスタの値を取得できます。
NICにはイベント毎にいくつか割り込みがあり、それらの有効/無効は「IMS(Interrupt Mask Set/Read:オフセット0x00d0)」というレジスタの各ビットで管理しています。「割り込みマスク(Interrupt Mask)」という名の通り、1が設定されているビットに対応する割り込みが有効になっています。
この節では、IMSのレジスタ値を取得し、何らかの割り込みが設定されているのかどうかを見てみます。
この節のサンプルディレクトリは「021_dump_nic_reg」です。
まず、include/nic.hへIMSレジスタのアドレス(ベースアドレスからのオフセット)を定義します(リスト2.1)。
リスト2.1: 021_dump_nic_reg/include/nic.h
/* ・・・ 省略 ・・・ */
#define NIC_FUNC_NUM 0x0
#define NIC_REG_IMS 0x00d0 /* 追加 */
void nic_init(void);
unsigned int get_nic_reg_base(void);
unsigned int get_nic_reg(unsigned short reg); /* 追加 */
void dump_nic_ims(void); /* 追加 */
併せて、指定したNICレジスタの値を取得する関数「get_nic_reg」と、それを使用してIMSをダンプする関数「dump_nic_ims」のプロトタイプ宣言を追加しておきました。
それでは、それぞれの関数をnic.cに実装します(リスト2.2)。
リスト2.2: 021_dump_nic_reg/nic.c
/* ・・・ 省略 ・・・ */
unsigned int get_nic_reg_base(void)
{
/* ・・・ 省略 ・・・ */
}
/* 追加(ここから) */
unsigned int get_nic_reg(unsigned short reg)
{
unsigned long long addr = nic_reg_base + reg;
return *(volatile unsigned int *)addr;
}
void dump_nic_ims(void)
{
unsigned int ims = get_nic_reg(NIC_REG_IMS);
puts("IMS ");
puth(ims, 8);
puts("\r\n");
}
/* 追加(ここまで) */
前章にて、NICのレジスタのアドレスはメモリアドレスであることを確認していたので、get_nic_reg()ではメモリアクセス(ポインタによるアクセス)でレジスタ値を取得しています。
なお、NICレジスタのアドレスがIOアドレスの場合はin命令でレジスタ値を取得します。その場合は、get_nic_reg()は、in命令を使用するように実装してください。in命令で任意のアドレス先の値を取得する関数としてio_read32()を既に実装済なので、それを使用して例えばリスト2.3の様に実装できます。
リスト2.3: IOアドレス時のget_nic_reg()の例
unsigned int get_nic_reg(unsigned short reg)
{
return io_read32(reg);
}
main.cからdump_nic_ims()を呼び出すようにして作業完了です(リスト2.4)。
リスト2.4: 021_dump_nic_reg/main.c
/* ・・・ 省略 ・・・ */
void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
void *_fs_start)
{
/* ・・・ 省略 ・・・ */
/* 周辺ICの初期化 */
pic_init();
hpet_init();
kbc_init();
nic_init();
/* 変更(ここから) */
/* NICのIMS(Interrupt Mask Set/Read Register)レジスタをダンプ */
dump_nic_ims();
/* 変更(ここまで) */
/* haltして待つ */
while (1)
cpu_halt();
/* ・・・ 省略 ・・・ */
実行結果は図2.1の通りです。
図2.1: 021_dump_nic_regの実行結果
全てのビットがゼロなので、割り込みは設定されていませんでした。
2.1.2 NICのレジスタ値を設定してみる
前節でNICのレジスタ値の取得方法を紹介しました。この節ではレジスタ値の設定方法を紹介します。
前節で割り込みが設定されていない事(割り込みマスクが全て0)を確認しましたが、ここではレジスタ値の設定方法の紹介として、割り込みマスクを適当に設定し、それをクリアしてみます。
この節のサンプルディレクトリは「022_set_nic_reg」です。
割り込みマスクを設定するレジスタは2つあります。
- IMS(Interrupt Mask Set/Read:オフセット0x00d0)
- 割り込みを有効にしたいビットを1にしてレジスタに書き込む
- IMC(Interrupt Mask Clear:オフセット0x00d8)
- 割り込みを無効にしたいビットを1にしてレジスタに書き込む
- すると、IMSの該当ビットが0になる
ここでは、IMSで適当に「0x0000beef」を割り込みマスクに設定し、IMCでそれをクリアしてみます。
まずは、新たに使用するIMCの定義と、指定されたNICレジスタへ値を設定する関数「set_nic_reg」のプロトタイプ宣言をinclude/nic.hへ追加します(リスト2.5)。
リスト2.5: 022_set_nic_reg/include/nic.h
/* ・・・ 省略 ・・・ */
#define NIC_FUNC_NUM 0x0
#define NIC_REG_IMS 0x00d0
#define NIC_REG_IMC 0x00d8 /* 追加 */
void nic_init(void);
unsigned int get_nic_reg_base(void);
unsigned int get_nic_reg(unsigned short reg);
void set_nic_reg(unsigned short reg, unsigned int val); /* 追加 */
void dump_nic_ims(void);
そして、set_nic_reg()の実装をnic.cへ追加します(リスト2.6)。
リスト2.6: 022_set_nic_reg/nic.c
/* ・・・ 省略 ・・・ */
unsigned int get_nic_reg(unsigned short reg)
{
/* ・・・ 省略 ・・・ */
}
/* 追加(ここから) */
void set_nic_reg(unsigned short reg, unsigned int val)
{
unsigned long long addr = nic_reg_base + reg;
*(volatile unsigned int *)addr = val;
}
/* 追加(ここまで) */
void dump_nic_ims(void)
/* ・・・ 省略 ・・・ */
レジスタ値を取得する場合とは逆に、レジスタのアドレスが指す先へ値を書き込むようにしているだけです。
なお、IOアドレスの場合のset_nic_reg()の実装例はリスト2.7の通りです。
リスト2.7: IOアドレス時のset_nic_reg()の例
void set_nic_reg(unsigned short reg, unsigned int val)
{
io_write32(reg, val);
}
get_nic_reg()の場合と同様に、今度はio_write32()を使用して実装できます。
最後に、main.cから割り込みマスク設定とクリアを試してみます(リスト2.8)。
リスト2.8: 022_set_nic_reg/main.c
/* ・・・ 省略 ・・・ */
void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
void *_fs_start)
{
/* ・・・ 省略 ・・・ */
/* 周辺ICの初期化 */
pic_init();
hpet_init();
kbc_init();
nic_init();
/* 変更(ここから) */
/* クリアの確認用に適当な値をセット */
set_nic_reg(NIC_REG_IMS, 0x0000beef);
dump_nic_ims();
/* NICの割り込みをIMC(Interrupt Mask Clear Register)で全て無効化 */
set_nic_reg(NIC_REG_IMC, 0xffffffff);
/* NICのIMS(Interrupt Mask Set/Read Register)レジスタをダンプ */
dump_nic_ims();
/* 変更(ここまで) */
/* haltして待つ */
while (1)
cpu_halt();
/* システムコールの初期化 */
/* ・・・ 省略 ・・・ */
set_nic_reg()でIMSへ0x0000beefという値を割り込みマスクへ設定しdump_nic_ims()で確認した後、IMCへ全てのビットが1の値を設定することで割り込みマスクをクリアしています。
実行すると図2.2のように表示されます。
図2.2: 022_set_nic_regの実行結果
2.1.3 NIC関連の割り込み無効化処理を関数化する
この項の最後に、PCI側でのNICデバイスの割り込み無効化処理と、NIC内の割り込みマスクによる割り込み無効化処理を、一つの関数にまとめておきます。
この節のサンプルディレクトリは「022_set_nic_reg_refactor」です。
nic.cに「disable_nic_interrupt」という関数を追加してみました(リスト2.9)。
リスト2.9: 022_set_nic_reg_refactor/nic.c
/* ・・・ 省略 ・・・ */
static unsigned int nic_reg_base;
/* 追加(ここから) */
static void disable_nic_interrupt(void)
{
/* 一旦、コマンドとステータスを読み出す */
unsigned int conf_data = get_pci_conf_reg(
NIC_BUS_NUM, NIC_DEV_NUM, NIC_FUNC_NUM,
PCI_CONF_STATUS_COMMAND);
/* ステータス(上位16ビット)をクリア */
conf_data &= 0x0000ffff;
/* コマンドに割り込み無効設定 */
conf_data |= PCI_COM_INTR_DIS;
/* コマンドとステータスに書き戻す */
set_pci_conf_reg(NIC_BUS_NUM, NIC_DEV_NUM, NIC_FUNC_NUM,
PCI_CONF_STATUS_COMMAND, conf_data);
/* NICの割り込みをIMC(Interrupt Mask Clear Register)で全て無効化 */
set_nic_reg(NIC_REG_IMC, 0xffffffff);
}
/* 追加(ここまで) */
void nic_init(void)
{
/* 変更(ここから) */
/* NICのレジスタのベースアドレスを取得しておく */
nic_reg_base = get_nic_reg_base();
/* 変更(ここまで) */
/* NICの割り込みを全て無効にする */
disable_nic_interrupt();
}
unsigned int get_nic_reg_base(void)
/* ・・・ 省略 ・・・ */
割り込み無効化処理を関数化したことで、nic_init()の見通しが良くなりました。
2.2 フレームを受信する
前項で、NICレジスタ値の取得方法と設定方法が分かりました。この項では、NICレジスタへイーサネットフレーム受信のための設定を行い、受信処理の動作確認を行います。
この項のサンプルディレクトリは「023_receive_frame」です。この項では、項全体を通してこのサンプルを作ります。
2.2.1 NICのフレーム受信の流れ
大枠を説明すると、NICは受信した各イーサネットフレームを「受信ディスクリプタ(Receive Descriptor)」というデータ構造でメモリ上のリングバッファへ順次格納していきます。ドライバはそのリングバッファから受信ディスクリプタの構造を解釈しながら読み出していく、という流れです。
もう少し詳しく説明します。まず「受信ディスクリプタ」のデータ構造をデータシートから引用します(図2.3*1)。
図2.3: 受信ディスクリプタのデータ構造
受信ディスクリプタは64ビット(8バイト)からなるデータ構造です。各フィールドの意味は以下の通りです。
- Buffer Address
- 受信したイーサネットフレームを格納するバッファ(受信バッファ)のアドレス
- Length
- Packet Chesksum
- NICで自動計算された受信フレームのチェックサム
- 受信バッファのどこからどこまでのチェックサムであるかは、パケットの種類やその他のNICレジスタの設定による
- Status
- ディスクリプタのステータスを示す
- NICが自動的に設定する
- 本書で使用するフラグについては実装する際に説明する
- Errors
- 受信したフレームに関するエラー情報が格納される
- 本書では使用しない
- Special
- VLAN等の特殊用途用のフィールド
- こちらも本書では使用しない
「Buffer Address」の説明の通り、受信したイーサネットフレーム自体は受信ディスクリプタではなく、Buffer Addressに指定されているアドレスの場所に格納されます。そのため、受信ディスクリプタ用リングバッファだけでなく、イーサネットフレームを格納するバッファ用の領域も事前に確保しておく必要があります。
そして、フレームを受信する度にNICはこの受信ディスクリプタのデータ構造をリングバッファへ格納していきます。このリングバッファは、予めNICのレジスタへ「受信ディスクリプタ用リングバッファのベースアドレス」と「リングバッファの長さ(バイト)」を指定することで、NICは指定された領域をリングバッファとして使用するようになります。
リングバッファの構造と使われ方については図2.4*2の通りです。
図2.4: 受信リングバッファについて
図2.4の「Head」や「Tail」もNICのレジスタです。これらの値も初期化時に適切に初期化しておきます。(詳細は実装しながら説明します。)
NICは受信したフレームをHeadが指す場所からTailが指す場所に向かってリングバッファへ格納していくので、ドライバはHeadから順に読み出していきます。
(Tail - 1)が指す領域まで格納し終えると、仕様上NICはそこより先の領域はアクセスしません。受信したフレームが喪失してしまうので、ドライバでは、リングバッファからフレームを取得したら、取得したインデックスまでTailを進めるようにします。
以上が、NICが受信したフレームを処理する流れです。以降の節では、さっそくフレーム受信処理を実装し始めます。使用するレジスタ等、詳しくは実装を進めながら適宜紹介します。
2.2.2 各種の定義の用意とバッファの確保
ここでは、受信ディスクリプタのデータ構造等の定義を行い、受信ディスクリプタ用リングバッファと受信したイーサネットフレーム自体を格納するバッファのメモリ領域を確保します(リスト2.10)。
リスト2.10: 023_receive_frame/nic.c
/* ・・・ 省略 ・・・ */
/* 追加(ここから) */
#define RXDESC_NUM 80
#define ALIGN_MARGIN 16
struct __attribute__((packed)) rxdesc {
unsigned long long buffer_address;
unsigned short length;
unsigned short packet_checksum;
unsigned char status;
unsigned char errors;
unsigned short special;
};
/* 追加(ここまで) */
static unsigned int nic_reg_base;
/* 追加(ここから) */
static unsigned char rx_buffer[RXDESC_NUM][PACKET_BUFFER_SIZE];
static unsigned char rxdesc_data[
(sizeof(struct rxdesc) * RXDESC_NUM) + ALIGN_MARGIN];
static struct rxdesc *rxdesc_base;
static unsigned short current_rx_idx;
/* 追加(ここまで) */
static void disable_nic_interrupt(void)
/* ・・・ 省略 ・・・ */
受信ディスクリプタとイーサネットフレームの領域は、ここでは簡単に、グローバル変数で確保してみました。「rx_buffer」がイーサネットフレーム用バッファの領域で、「rxdesc_data」が受信ディスクリプタ用リングバッファの領域です。
なお、受信ディスクリプタのアドレスは16バイトアライメント(16の倍数)である必要があります。そのため、rxdesc_dataを確保する際、16バイト多めに確保しておきます。そして、rxdesc_dataの先頭から16バイトアライメントとなる最初のアドレスを受信ディスクリプタのベースアドレスとして使うようにします。そのベースアドレスを管理するために「rxdesc_base」変数を用意しています。イーサネットフレーム用のバッファにはアライメント制約はありませんので、配列として確保したrx_buffer変数をそのまま使用します。
受信ディスクリプタ用リングバッファの要素数(RXDESC_NUM)は、ここでは80にしてみました。この値はお好きなように決めて構いませんが、後述するリングバッファの長さを指定するレジスタの制約で、要素数は8の倍数である必要があります。また、本シリーズの現状の自作OSではカーネル本体(kernel.bin)で使用するメモリサイズが1MB未満である必要がありますのでご注意ください*3。
イーサネットフレーム1つ当たりのバッファサイズ(PACKET_BUFFER_SIZE)は、include/nic.hに定義していて、今回は1024バイトとしました(リスト2.11)。
リスト2.11: 023_receive_frame/include/nic.h
/* ・・・省略 ・・・ */
#define NIC_RDESC_STAT_PIF (1U << 7)
#define PACKET_BUFFER_SIZE 1024
#define PACKET_RBSIZE_BIT NIC_RCTL_BSIZE_1024B
void nic_init(void);
/* ・・・ 省略 ・・・ */
PACKET_RBSIZE_BITは、NICの振る舞いを設定するレジスタにフレームバッファ1つのサイズを設定する際の定数です。PACKET_BUFFER_SIZEを変更した際は併せて変更する必要があるため、近くに定義しています。
include/nic.hには他にも、使用するNICのレジスタとレジスタが持つビットの定義を全て書いています。単に定数を定義しているだけの内容なのでコードを引用しての紹介はしません。内容が気になる場合はサンプルコードを見てみてください。
最後に、「current_rx_idx」はリングバッファ内の次に読む場所を覚えておくための変数です。詳しくは変数を使用する際に説明します。
2.2.3 受信の初期化処理を実装する
前節で受信処理に使用する領域確保や定数の定義等を行いました。この節ではそれらを初期化します。
まずは受信ディスクリプタ用リングバッファのベースアドレス(rxdesc_base)を初期化します(リスト2.12)。
リスト2.12: 023_receive_frame/nic.c
/* ・・・ 省略 ・・・ */
static void disable_nic_interrupt(void)
{
/* ・・・ 省略 ・・・ */
}
/* 追加(ここから) */
static void rx_init(void)
{
/* rxdescの先頭アドレスを16バイトの倍数となるようにする */
unsigned long long rxdesc_addr = (unsigned long long)rxdesc_data;
rxdesc_addr = (rxdesc_addr + ALIGN_MARGIN) & 0xfffffffffffffff0;
/* rxdescの初期化 */
rxdesc_base = (struct rxdesc *)rxdesc_addr;
}
/* 追加(ここまで) */
/* ・・・ 省略 ・・・ */
「rxdesc_addr」という計算用の変数を使用して、rxdesc_data(リングバッファ用に確保した領域の先頭アドレス)にアライメントサイズである16を足してから、16バイトアライメントとなるように下位4ビットを0でマスクしています。アライメントサイズを足す前に下位4ビットを0でマスクしてしまうと、確保した領域より若いアドレスとなってしまう可能性があり、前節で領域を確保する際に16バイト多めに確保した意味が無くなってしまうので、アライメントサイズを足してからマスクします。
受信ディスクリプタ用リングバッファのベースアドレスを用意できたので、次はリングバッファ内の各受信ディスクリプタを初期化します(リスト2.13)。
リスト2.13: 023_receive_frame/nic.c
/* ・・・ 省略 ・・・ */
static void rx_init(void)
{
unsigned int i; /* 追加 */
/* rxdescの先頭アドレスを16バイトの倍数となるようにする */
unsigned long long rxdesc_addr = (unsigned long long)rxdesc_data;
rxdesc_addr = (rxdesc_addr + ALIGN_MARGIN) & 0xfffffffffffffff0;
/* rxdescの初期化 */
rxdesc_base = (struct rxdesc *)rxdesc_addr;
/* 追加(ここから) */
struct rxdesc *cur_rxdesc = rxdesc_base;
for (i = 0; i < RXDESC_NUM; i++) {
cur_rxdesc->buffer_address = (unsigned long long)rx_buffer[i];
cur_rxdesc->status = 0;
cur_rxdesc->errors = 0;
cur_rxdesc++;
}
/* 追加(ここまで) */
}
/* ・・・ 省略 ・・・ */
buffer_addressにイーサネットフレーム用バッファのアドレスを設定し、ステータスとエラーフラグを0でクリアしています。
次に、NICのリングバッファのベースアドレスとサイズをNICのレジスタへ設定します(リスト2.14)。
リスト2.14: 023_receive_frame/nic.c
/* ・・・ 省略 ・・・ */
static void rx_init(void)
{
unsigned int i;
/* rxdescの先頭アドレスを16バイトの倍数となるようにする */
unsigned long long rxdesc_addr = (unsigned long long)rxdesc_data;
rxdesc_addr = (rxdesc_addr + ALIGN_MARGIN) & 0xfffffffffffffff0;
/* rxdescの初期化 */
rxdesc_base = (struct rxdesc *)rxdesc_addr;
struct rxdesc *cur_rxdesc = rxdesc_base;
for (i = 0; i < RXDESC_NUM; i++) {
cur_rxdesc->buffer_address = (unsigned long long)rx_buffer[i];
cur_rxdesc->status = 0;
cur_rxdesc->errors = 0;
cur_rxdesc++;
}
/* 追加(ここから) */
/* rxdescの先頭アドレスとサイズをNICレジスタへ設定 */
set_nic_reg(NIC_REG_RDBAH, rxdesc_addr >> 32);
set_nic_reg(NIC_REG_RDBAL, rxdesc_addr & 0x00000000ffffffff);
set_nic_reg(NIC_REG_RDLEN, sizeof(struct rxdesc) * RXDESC_NUM);
/* 追加(ここまで) */
}
/* ・・・ 省略 ・・・ */
「NIC_REG_」と付いている定数がNICのレジスタのアドレス(NICレジスタベースアドレスからのオフセット)で、include/nic.hに定義しているものです。ここでは3つのレジスタ値を設定しています。
まず、NICはリングバッファのベースアドレスを64ビットアドレスで管理します。設定するレジスタとしては2つあり、RDBAH(Receive Descriptor Base High)に上位32ビットを設定し、RDBAL(Receive Descriptor Base Low)へ下位32ビットを設定します。
RDLEN(Receive Descriptor Length)にはリングバッファの長さ(バイト数)を設定します。このバイト数は128の倍数で指定することが決められており、前述の通り、要素数としては8の倍数となります。
そして、リングバッファのHeadとTailを管理するNICレジスタも初期化します(リスト2.15)。
リスト2.15: 023_receive_frame/nic.c
/* ・・・ 省略 ・・・ */
static void rx_init(void)
{
unsigned int i;
/* rxdescの先頭アドレスを16バイトの倍数となるようにする */
unsigned long long rxdesc_addr = (unsigned long long)rxdesc_data;
rxdesc_addr = (rxdesc_addr + ALIGN_MARGIN) & 0xfffffffffffffff0;
/* rxdescの初期化 */
rxdesc_base = (struct rxdesc *)rxdesc_addr;
struct rxdesc *cur_rxdesc = rxdesc_base;
for (i = 0; i < RXDESC_NUM; i++) {
cur_rxdesc->buffer_address = (unsigned long long)rx_buffer[i];
cur_rxdesc->status = 0;
cur_rxdesc->errors = 0;
cur_rxdesc++;
}
/* rxdescの先頭アドレスとサイズをNICレジスタへ設定 */
set_nic_reg(NIC_REG_RDBAH, rxdesc_addr >> 32);
set_nic_reg(NIC_REG_RDBAL, rxdesc_addr & 0x00000000ffffffff);
set_nic_reg(NIC_REG_RDLEN, sizeof(struct rxdesc) * RXDESC_NUM);
/* 追加(ここから) */
/* 先頭と末尾のインデックスをNICレジスタへ設定 */
current_rx_idx = 0;
set_nic_reg(NIC_REG_RDH, current_rx_idx);
set_nic_reg(NIC_REG_RDT, RXDESC_NUM - 1);
/* 追加(ここまで) */
}
/* ・・・ 省略 ・・・ */
RDH(Receive Register Head)にHeadを、RDT(Receive Register Tail)にTailのインデックスを設定します。RDHに0を、RDTに(要素数 - 1)を設定しておくことで、この後受信処理を有効化すると、RDHから順次リングバッファに受信ディスクリプタを格納していくようになります。
なお、RDHはNICが次の書き込み先のインデックスを保持するためのもので、この後受信処理の有効化を行いますが、それ以降はドライバ側から値の変更を行ってはなりません(データシートに明記されています)。ドライバ側でリングバッファのどこまでを読み出したのかを保持しておくためには「current_rx_idx」を用意しています(併せて初期化しています)。
対して、RDTは「リングバッファのここよりは書き込まないでくれ」ということをドライバがNICへ伝えるためのものなので、ドライバでリングバッファから値を読み出す都度、更新する必要があります。(でないといずれRDHがRDTへ追いつき、NICはリングバッファへの書き込みをやめてしまいます。)RDTの更新処理については、受信処理を実装する際に紹介します。
最後にNICの受信動作設定と受信処理の有効化を行います(リスト2.16)。
リスト2.16: 023_receive_frame/nic.c
/* ・・・ 省略 ・・・ */
static void rx_init(void)
{
unsigned int i;
/* rxdescの先頭アドレスを16バイトの倍数となるようにする */
unsigned long long rxdesc_addr = (unsigned long long)rxdesc_data;
rxdesc_addr = (rxdesc_addr + ALIGN_MARGIN) & 0xfffffffffffffff0;
/* rxdescの初期化 */
rxdesc_base = (struct rxdesc *)rxdesc_addr;
struct rxdesc *cur_rxdesc = rxdesc_base;
for (i = 0; i < RXDESC_NUM; i++) {
cur_rxdesc->buffer_address = (unsigned long long)rx_buffer[i];
cur_rxdesc->status = 0;
cur_rxdesc->errors = 0;
cur_rxdesc++;
}
/* rxdescの先頭アドレスとサイズをNICレジスタへ設定 */
set_nic_reg(NIC_REG_RDBAH, rxdesc_addr >> 32);
set_nic_reg(NIC_REG_RDBAL, rxdesc_addr & 0x00000000ffffffff);
set_nic_reg(NIC_REG_RDLEN, sizeof(struct rxdesc) * RXDESC_NUM);
/* 先頭と末尾のインデックスをNICレジスタへ設定 */
current_rx_idx = 0;
set_nic_reg(NIC_REG_RDH, current_rx_idx);
set_nic_reg(NIC_REG_RDT, RXDESC_NUM - 1);
/* 追加(ここから) */
/* NICの受信動作設定 */
set_nic_reg(NIC_REG_RCTL, PACKET_RBSIZE_BIT | NIC_RCTL_BAM
| NIC_RCTL_MPE | NIC_RCTL_UPE | NIC_RCTL_SBP | NIC_RCTL_EN);
/* 追加(ここまで) */
}
/* ・・・ 省略 ・・・ */
RCTL(Receive Control Register)がNICの受信時の振る舞いを設定するレジスタです。定数を使って設定している他にも設定できるビットはあるのですが、ここでは設定しているもののみ紹介します。(全てのビットフィールドが気になる方はデータシートを見てみてください。)なお、「NIC_RCTL_」が付いている定数はRCTLレジスタに設定できる各ビットを表す定数です。
- PACKET_RBSIZE_BIT
- イーサネットフレーム1つ当たりのバッファサイズを指定するビット
- 先述の通りnic.hにて「BSIZE_1024B」で再定義しており、1024バイト
- BAM(Broadcast Accept Mode)
- ブロードキャストパケットを受け入れるかどうかの設定
- この項のサンプルでは受信する全てをダンプするようにしてみたいので1(受け入れる)に設定
- MPE(Multicast Promiscuous Enabled)
- マルチキャストパケットに対するプロミスキャス・モード*4設定
- フィルタリングしてほしくないので1に設定します。
- UPE(Unicast Promiscuous Enabled)
- ユニキャストパケットについてのプロミスキャス・モード設定
- こちらも1を設定
- SBP(Store Bad Packets)
- BADパケット*5をバッファへ格納するかどうかの設定
- 無差別に格納して欲しいのでこちらも1を設定
- EN(Receiver Enable)
- 受信処理を有効化するビット
- このビットを1にするとNICの受信処理が始まる
これでNICの受信に関する初期化処理を行うrx_init()は完成です。NIC全体の初期化処理を行う関数nic_init()から呼び出すようにしておきます(コード紹介は割愛)。
2.2.4 受信処理を実装する
前節でNICは受信処理を開始し、用意したバッファへ受信したフレームを順次格納していくようになりました。
それでは、リングバッファとフレーム用のバッファから、フレームを取得する関数「receive_frame」を実装してみます(リスト2.17)。
リスト2.17: 023_receive_frame/nic.c
/* ・・・ 省略 ・・・ */
void dump_nic_ims(void)
{
/* ・・・ 省略 ・・・ */
}
/* 追加(ここから) */
unsigned short receive_frame(void *buf)
{
unsigned short len = 0;
struct rxdesc *cur_rxdesc = rxdesc_base + current_rx_idx;
if (cur_rxdesc->status & NIC_RDESC_STAT_DD) {
len = cur_rxdesc->length;
memcpy(buf, (void *)cur_rxdesc->buffer_address,
cur_rxdesc->length);
cur_rxdesc->status = 0;
set_nic_reg(NIC_REG_RDT, current_rx_idx);
current_rx_idx = (current_rx_idx + 1) % RXDESC_NUM;
}
return len;
}
/* 追加(ここまで) */
receive_frame()は、リングバッファに格納されている未取得のイーサネットフレームを一つ引数で指定されたポインタ(buf)へコピーし、コピーしたフレームの長さを戻り値として返します。
流れとしては、まず、cur_rxdescに、現在参照している(current_rx_idxが指す)受信ディスクリプタのポインタを設定します*6。
次に、受信ディスクリプタcur_rxdescのステータスにDD(Descriptor Done)が設定されているかどうかをif文でチェックしています。これはNICが受信ディスクリプタの処理を終えたことを示すビットで、このビットが設定されていればその受信ディスクリプタをドライバから参照して良いことになります。受信ディスクリプタのステータスには他にもフラグがありますが、本書ではDDしか使いません。なお、受信ディスクリプタのステータスにDDがセットされていない場合、まだフレームを受信していないということで、lenの初期値0を返して終了します。
受信ディスクリプタにDDがセットされていた場合、フレームの長さを受信ディスクリプタのlengthメンバーから取得し、戻り値用の変数lenへ設定、memcpy()を使用して引数bufへイーサネットフレーム用バッファの内容をコピーします。
その後は、ステータスをクリアし、RDTを今取得を完了したインデックス(current_rx_idx)で更新し、current_rx_idxを進めます。
それでは、receive_frame()を使用して受信したフレームをダンプする関数「dump_frame()」を実装してみます(リスト2.18)。
リスト2.18: 023_receive_frame/nic.c
/* ・・・ 省略 ・・・ */
unsigned short receive_frame(void *buf)
{
/* ・・・ 省略 ・・・ */
}
/* 追加(ここから) */
unsigned short dump_frame(void)
{
unsigned char buf[PACKET_BUFFER_SIZE];
unsigned short len;
len = receive_frame(buf);
unsigned short i;
for (i = 0; i < len; i++) {
puth(buf[i], 2);
putc(' ');
if (((i + 1) % 24) == 0)
puts("\r\n");
else if (((i + 1) % 4) == 0)
putc(' ');
}
if (len > 0)
puts("\r\n");
return len;
}
/* 追加(ここまで) */
表示する際の文字間隔は1行当たりの文字数を調整するために色々やっていますが、receive_frame()でbufへ取得したフレームをダンプしているだけです。
最後にdump_frame()をmain.cから呼び出すようにしてみます(リスト2.19)。
リスト2.19: 023_receive_frame/main.c
/* ・・・ 省略 ・・・ */
void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
void *_fs_start)
{
/* ・・・ 省略 ・・・ */
/* 周辺ICの初期化 */
pic_init();
hpet_init();
kbc_init();
nic_init();
/* 変更(ここから) */
/* 受信したフレームをダンプし続ける */
while (1) {
if (dump_frame() > 0)
puts("\r\n");
}
/* 変更(ここまで) */
/* ・・・ 省略 ・・・ */
2.2.5 動作確認
ビルドし、実行すると、図2.5の様に受信動作を確認できます。
図2.5: 023_receive_frameの実行結果
動作確認の際は、他のPCと1対1で接続し、pkttools*7等の様に任意のパケットを送信できるツールで試してみると分かりやすいです。なお、現在のたいていのPCはAuto-MDIXという機能に対応しているのでストレートケーブルで1対1に接続しても問題ないです。
図2.5はリスト2.20のフレームを2つとリスト2.21のフレームを1つ受信したところです。これはpkttoolsで任意のイーサネットフレームを送信する際に指定できるテキストフォーマットで、「SIZE」の行の次の行からが送信するイーサネットフレームのバイナリデータです(16進数表記)。
リスト2.20: 対向PCから送信したフレーム その1
-- 2 --
TIME: 1548948030.069473 Fri Feb 1 00:20:30 2019
SIZE: 4/4
000000: 01 23 45 67
==
リスト2.21: 対向PCから送信したフレーム その2
-- 2 --
TIME: 1548948030.069473 Fri Feb 1 00:20:30 2019
SIZE: 8/8
000000: 01 23 45 67 89 ab cd ef
==
なお、図2.5ではリスト2.20やリスト2.21で指定しているデータの他に、0のパディングの後、末尾に4バイトの何かのデータがくっついています。これは、FCS(Frame Check Sequence)と呼ばれるチェックサムです。これは、イーサネットフレームを送信する際の送信ディスクリプタの設定に応じてNICが計算して付けているものです。Linuxの場合(というか一般的なNICドライバの場合)、標準で付けるようにしていますが、次の項で紹介する送信処理ではこれを付けないようにしてみるので、その際は付きません*8。
2.3 オレオレフレームを送信する
前項でイーサネットフレームの受信を試しました。この項ではイーサネットフレームの送信を試してみます。
ここではあくまでもNICの動作確認として特にイーサネットフレームのフォーマットに従わないオレオレなフレームを送信してみます。
この項を通して「024_send_frame」のサンプルディレクトリの内容を作ります。
2.3.1 NICのフレーム送信の流れ
フレーム送信も受信と同じくディスクリプタ用のリングバッファとフレーム自体を置いておくためのバッファを使用して行います。
送信のディスクリプタ(Transmit Descriptor)のデータ構造は図2.6*9の通りです。
図2.6: 送信ディスクリプタのデータ構造
また、各フィールドの意味については以下の通りです。
- Buffer Address
- 送信するイーサネットフレーム本体を格納したバッファのアドレス
- Length
- 送信するイーサネットフレームの長さ(バイト数)
- 0を指定するとNICはそのディスクリプタのフレームを送信しない
- CSO(Checksum Offset)
- NICがチェックサムを自動挿入するモードが有効になっている時、チェックサムを挿入するフレーム先頭からの相対位置
- CMD(Command)
- このディスクリプタの振る舞いを指定するいくつかのコマンドをこのビットフィールドで指定できる
- 使用するコマンドについては使用する際に説明する
- STA(Status)
- このディスクリプタの状態を表すフィールド
- ドライバ側であらかじめゼロクリアしておき、NICが適宜設定する
- 使用するステータスについては使用する際に説明する
- RSV(Reserved)
- 予約領域
- 将来の互換性のため書き込む際は0を書くこと
- CSS(Checksum Start)
- チェックサムの計算を開始する位置(フレーム先頭からのオフセット)を指定する
- Lengthフィールド未満であること
- Special
- このイーサネットフレームに対して特殊な設定(VLAN設定等)を行うフィールド
- 本書では使用しないため説明は割愛
実は送信ディスクリプタには3種類あり、図2.6で紹介したものは「レガシー送信ディスクリプタ(Legacy Transmit Descriptor)」と呼ばれるものです。ただ、これは「古い」というよりは「オリジナル」としての意味合いであるとのことで、他の2つはまとめて「拡張」ディスクリプタと呼ばれる*10、とのことです(データシートより)。最初試すときはなるたけ簡単なやり方の方がトラブらなくて良いので、本書ではレガシー送信ディスクリプタで試します。
そして、送信ディスクリプタ用リングバッファの構造と使われ方は図2.7*11の通りです。
図2.7: 送信リングバッファについて
特に受信の時と違いは無いです。
それでは、次の節から実装を始めます。
2.3.2 各種の定義の用意とバッファの確保
受信の時と同様に、今度は送信ディスクリプタのデータ構造の定義とグローバル変数による領域の確保を行います(リスト2.22)。
リスト2.22: 024_send_frame/nic.c
/* ・・・ 省略 ・・・ */
#define RXDESC_NUM 80
#define TXDESC_NUM 8 /* 追加 */
#define ALIGN_MARGIN 16
struct __attribute__((packed)) rxdesc {
unsigned long long buffer_address;
unsigned short length;
unsigned short packet_checksum;
unsigned char status;
unsigned char errors;
unsigned short special;
};
/* 追加(ここから) */
struct __attribute__((packed)) txdesc {
unsigned long long buffer_address;
unsigned short length;
unsigned char cso;
unsigned char cmd;
unsigned char sta:4;
unsigned char _rsv:4;
unsigned char css;
unsigned short special;
};
/* 追加(ここまで) */
static unsigned int nic_reg_base;
static unsigned char rx_buffer[RXDESC_NUM][PACKET_BUFFER_SIZE];
static unsigned char rxdesc_data[
(sizeof(struct rxdesc) * RXDESC_NUM) + ALIGN_MARGIN];
static struct rxdesc *rxdesc_base;
static unsigned short current_rx_idx;
/* 追加(ここから) */
static unsigned char txdesc_data[
(sizeof(struct txdesc) * TXDESC_NUM) + ALIGN_MARGIN];
static struct txdesc *txdesc_base;
static unsigned short current_tx_idx;
/* 追加(ここまで) */
static void disable_nic_interrupt(void)
/* ・・・ 省略 ・・・ */
送信ディスクリプタ用リングバッファの要素数は最小*12の8にしました。後述しますが、送信処理の関数では渡されたフレームの送信完了を確認してからreturnするようにするため、リングバッファの要素数としては1つあれば十分です。また、イーサネットフレーム自体のバッファは確保していません。後述しますが送信用関数に渡されたデータ領域をそのまま使います。
その他は、受信の時と同様です。送信用に新たに使用するレジスタのアドレス定義等もinclude/nic.hへ追加していますがコード紹介しての説明は省略します。レジスタ自体については使用する際に適宜紹介します。
2.3.3 送信の初期化処理を実装する
次に初期化処理「tx_init」関数を実装します。
ほとんど受信の時と同じなので一気に紹介します(リスト2.23)。
リスト2.23: 024_send_frame/nic.c
/* ・・・ 省略 ・・・ */
static void rx_init(void)
{
/* ・・・ 省略 ・・・ */
}
/* 追加(ここから) */
static void tx_init(void)
{
unsigned int i;
/* txdescの先頭アドレスを16バイトの倍数となるようにする */
unsigned long long txdesc_addr = (unsigned long long)txdesc_data;
txdesc_addr = (txdesc_addr + ALIGN_MARGIN) & 0xfffffffffffffff0;
/* txdescの初期化 */
txdesc_base = (struct txdesc *)txdesc_addr;
struct txdesc *cur_txdesc = txdesc_base;
for (i = 0; i < TXDESC_NUM; i++) {
cur_txdesc->buffer_address = 0;
cur_txdesc->length = 0;
cur_txdesc->cso = 0;
cur_txdesc->cmd = NIC_TDESC_CMD_RS | NIC_TDESC_CMD_EOP;
cur_txdesc->sta = 0;
cur_txdesc->_rsv = 0;
cur_txdesc->css = 0;
cur_txdesc->special = 0;
cur_txdesc++;
}
/* txdescの先頭アドレスとサイズをNICレジスタへ設定 */
set_nic_reg(NIC_REG_TDBAH, txdesc_addr >> 32);
set_nic_reg(NIC_REG_TDBAL, txdesc_addr & 0x00000000ffffffff);
set_nic_reg(NIC_REG_TDLEN, sizeof(struct txdesc) * TXDESC_NUM);
/* 先頭と末尾のインデックスをNICレジスタへ設定 */
current_tx_idx = 0;
set_nic_reg(NIC_REG_TDH, current_tx_idx);
set_nic_reg(NIC_REG_TDT, current_tx_idx);
/* NICの送信動作設定 */
set_nic_reg(NIC_REG_TCTL, (0x40 << NIC_TCTL_COLD_SHIFT)
| (0x0f << NIC_TCTL_CT_SHIFT) | NIC_TCTL_PSP | NIC_TCTL_EN);
}
/* 追加(ここまで) */
/* ・・・ 省略 ・・・ */
受信の時と異なる箇所としては、まず、各送信ディスクリプタの各フィールドの初期化処理です。各フィールドの初期化内容について以下にまとめます。
- buffer_addressとlength
- 送信時に設定するため0指定
- buffer_addressに関してはヌルポインタ(0)を指定しておくとNICはそのディスクリプタを送信しないという効果もある
- cmd(Command)
- RS(Report Status)
- NICへフレーム送信完了後のステータスフィールドの更新を要求する
- EOP(End Of Packet)
- 一連のパケットの最終であることを示す
- 今回送信するフレームは一つ一つがそれ単体で完結しているものなので初期値として設定しておく
- cso(Checksum Offset)とcss(Checksum Start)
- 今回、チェックサムの自動挿入モードは使用しないため0
- チェックサムの自動挿入はcmdに「IC(Insert Checksum)」のビットを設定することで有効化される
- sta(Status)
- Special
次に受信の時と異なる箇所としては、先頭(TDH: Transmit Descriptor Headレジスタ)と末尾(TDT: Transmit Descriptor Tailレジスタ)のインデックス指定で、受信とは違い、共に0で初期化しています。送信の際、NICはリングバッファのTDHから(TDL - 1)の間を送信するため、送信するべきディスクリプタが無い初期状態では共に0番目を指すようにしておきます。
受信と異なる箇所の最後はNICの動作設定(TCTL: Transmit Control Register)です。設定しているビットについては以下の通りです。なお、これまで同様に「NIC_TCTL_」と先頭に付く定数はTCTLレジスタ内のビットフィールドを示す定数です。末尾に「_SHIFT」が付くものは2ビット以上のビットフィールドに対して、左シフトするビット数を定義しています。定義内容はinclude/nic.hを参照してください。
- COLD(Collision Distance)
- コリジョン時のCSMA/CD操作の為に経過しなければならないバイトタイムを指定する
- データシートに推奨値が書かれていて、ここではその通りに全二重推奨値である0x40を設定
- CT(Collision Threshold)
- コリジョン時に諦めるまでの再送信回数
- データシート曰く「半二重動作でのみ意味がある」とのこと
- ここもデータシートの推奨値0x0fを指定
- PSP(Pad Short Packets)
- このビットが指定されていると、NICに渡されたフレームの長さが64バイト未満の時、パディング(0)を加えて64バイトにしてくれる
- このビットが指定されていない場合、NICが送信できる最小の長さは32バイトなので、それ未満の長さのフレームは送信できない
- 後述するが試しに送信するフレームは0xbeefbeef(4バイト)なので、このビットを指定しておく
- EN(Transmit Enable)
以上でtx_init()の説明は終了です。最後にtx_init()をnic_init()から呼び出すようにしておきます(コード紹介は割愛)。
2.3.4 送信処理を実装する
送信したいイーサネットフレームのバッファへのポインタと長さを渡すと、そのデータを送信し、ステータスを返す関数「send_frame」を実装します(リスト2.24)。
リスト2.24: 024_send_frame/nic.c
/* ・・・ 省略 ・・・ */
unsigned short dump_frame(void)
{
/* ・・・ 省略 ・・・ */
}
/* 追加(ここから) */
unsigned char send_frame(void *buf, unsigned short len)
{
/* txdescの設定 */
struct txdesc *cur_txdesc = txdesc_base + current_tx_idx;
cur_txdesc->buffer_address = (unsigned long long)buf;
cur_txdesc->length = len;
cur_txdesc->sta = 0;
/* idx更新 */
current_tx_idx = (current_tx_idx + 1) % TXDESC_NUM;
set_nic_reg(NIC_REG_TDT, current_tx_idx);
/* 送信完了を待つ */
unsigned char send_status = 0;
while (!send_status)
send_status = cur_txdesc->sta & 0x0f;
return send_status;
}
/* 追加(ここまで) */
まず、送信ディスクリプタは、buffer_addressとlengthへ渡されたものをそのまま指定し、sta(Status)をゼロクリアしています。
後はTDT(Transmit Descriptor Tail)をインクリメントすることで、NICがそのディスクリプタの内容を送信します。
その後、送信ディスクリプタのsta(Status)は下位4ビットがステータスフラグとして使用されるビットなので、そこに何らかのビットが設定されるのを待ちます。設定されたらステータス値をそのまま返して終了です。
これを呼び出すmain.cとしてはリスト2.25の様に実装してみました。
リスト2.25: 024_send_frame/main.c
/* ・・・ 省略 ・・・ */
void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi,
void *_fs_start)
{
/* ・・・ 省略 ・・・ */
/* 周辺ICの初期化 */
pic_init();
hpet_init();
kbc_init();
nic_init();
/* 変更(ここから) */
/* 1秒周期で0xbeefbeefを送信し続ける */
unsigned int data = 0xbeefbeef;
unsigned short len = sizeof(data);
while (1) {
puts("BE");
unsigned char status = send_frame(&data, len);
switch (status) {
case NIC_TDESC_STA_DD:
puts("EF");
break;
case NIC_TDESC_STA_EC:
puts("EC");
break;
case NIC_TDESC_STA_LC:
puts("LC");
break;
case NIC_TDESC_STA_TU:
puts("TU");
break;
}
putc(' ');
sleep(1 * SEC_TO_US);
}
/* 変更(ここまで) */
/* ・・・ 省略 ・・・ */
「0xbeefbeef」という4バイトのオレオレイーサネットフレームを1秒周期で送信し続けるようにしてみました。
send_frame()が返す送信ディスクリプタのステータスフラグの意味については以下の通りです。
- DD(Descriptor Done)
- EC(Excess Collisions)
- TCTLレジスタのCTフィールドで指定されている最大衝突回数を超えたため送信されなかった事を示す
- LC(Late Collision)
- 半二重モードでの作業中にレイトコリジョンが発生したことを示す
- 全二重モード時は意味のないビット
- TU(Transmit Underrun)
- 送信アンダーランイベントが発生したことを示す
- Early Transmit(早期送信)機能が有効になっていた場合にバッファ内のデータ不足で早期送信を完了できなかった事を示す
- 今回は特に意識不要
DDが正常終了でそれ以外が異常終了を示します。
"BE"と出力した後、送信し、正常終了なら"EF"を、異常終了ならそれらのステータスに対応する2文字を出力するようにしてみました。
2.3.5 動作確認
実行すると図2.8の様に"BEEF BEEF ..."と出力されます。
図2.8: 024_send_frameの実行結果
実際に対向PCと1対1で接続*13し、対向PC側でWireshark等でパケットキャプチャしている様子が図2.9です。
図2.9: 対向側で受信している様子
イーサネットフレームの先頭6バイトである宛先MACアドレス部に"0xbeefbeef"と書かれた謎のフレームが続々と受信されている事が確認できます。
第2章 NICを制御してイーサネットフレームを送受信する