前章でNICのレジスタのベースアドレスを取得できました。
この章では、NICのレジスタを使用して、任意のイーサネットフレームを受け取ったり、特にフォーマットに従っていない「オレオレデータ」を送ってみたり、ということを手っ取り早く行ってみます。
なお、この章では適宜データシートの図を引用します。参照しているデータシートについては本書最後の「参考情報」をご覧ください。
まず、NICのレジスタの操作方法を紹介します。
ここでは、NICの割り込み設定の確認と無効化を通して、レジスタ値の取得と設定の方法を紹介します。
なお、データシートで「パケット(packet)」と呼んでいるレジスタ等は本書でも「パケット」と呼称します。これは「IPパケット」ではなく「データの塊」という意味の広義の「packet」で、現在のTCP/IP通信においてNICが送受信するデータの単位としては「イーサネットフレーム」です。NICが送受信するデータの塊を「パケット」と呼称すると「IPパケット」と混同してまぎらわしいので、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の実行結果
全てのビットがゼロなので、割り込みは設定されていませんでした。
前節でNICのレジスタ値の取得方法を紹介しました。この節ではレジスタ値の設定方法を紹介します。
前節で割り込みが設定されていない事(割り込みマスクが全て0)を確認しましたが、ここではレジスタ値の設定方法の紹介として、割り込みマスクを適当に設定し、それをクリアしてみます。
この節のサンプルディレクトリは「022_set_nic_reg」です。
割り込みマスクを設定するレジスタは2つあります。
ここでは、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の実行結果
この項の最後に、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()の見通しが良くなりました。
前項で、NICレジスタ値の取得方法と設定方法が分かりました。この項では、NICレジスタへイーサネットフレーム受信のための設定を行い、受信処理の動作確認を行います。
この項のサンプルディレクトリは「023_receive_frame」です。この項では、項全体を通してこのサンプルを作ります。
大枠を説明すると、NICは受信した各イーサネットフレームを「受信ディスクリプタ(Receive Descriptor)」というデータ構造でメモリ上のリングバッファへ順次格納していきます。ドライバはそのリングバッファから受信ディスクリプタの構造を解釈しながら読み出していく、という流れです。
もう少し詳しく説明します。まず「受信ディスクリプタ」のデータ構造をデータシートから引用します(図2.3*1)。
図2.3: 受信ディスクリプタのデータ構造
[*1] 3.2.3 Receive Descriptor Format - PCI/PCI-X Family of Gigabit Ethernet Controllers Software Developer's Manual
受信ディスクリプタは64ビット(8バイト)からなるデータ構造です。各フィールドの意味は以下の通りです。
「Buffer Address」の説明の通り、受信したイーサネットフレーム自体は受信ディスクリプタではなく、Buffer Addressに指定されているアドレスの場所に格納されます。そのため、受信ディスクリプタ用リングバッファだけでなく、イーサネットフレームを格納するバッファ用の領域も事前に確保しておく必要があります。
そして、フレームを受信する度にNICはこの受信ディスクリプタのデータ構造をリングバッファへ格納していきます。このリングバッファは、予めNICのレジスタへ「受信ディスクリプタ用リングバッファのベースアドレス」と「リングバッファの長さ(バイト)」を指定することで、NICは指定された領域をリングバッファとして使用するようになります。
リングバッファの構造と使われ方については図2.4*2の通りです。
図2.4: 受信リングバッファについて
[*2] 3.2.6 Receive Descriptor Queue Structure - PCI/PCI-X Family of Gigabit Ethernet Controllers Software Developer's Manual
図2.4の「Head」や「Tail」もNICのレジスタです。これらの値も初期化時に適切に初期化しておきます。(詳細は実装しながら説明します。)
NICは受信したフレームをHeadが指す場所からTailが指す場所に向かってリングバッファへ格納していくので、ドライバはHeadから順に読み出していきます。
(Tail - 1)が指す領域まで格納し終えると、仕様上NICはそこより先の領域はアクセスしません。受信したフレームが喪失してしまうので、ドライバでは、リングバッファからフレームを取得したら、取得したインデックスまでTailを進めるようにします。
以上が、NICが受信したフレームを処理する流れです。以降の節では、さっそくフレーム受信処理を実装し始めます。使用するレジスタ等、詳しくは実装を進めながら適宜紹介します。
ここでは、受信ディスクリプタのデータ構造等の定義を行い、受信ディスクリプタ用リングバッファと受信したイーサネットフレーム自体を格納するバッファのメモリ領域を確保します(リスト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。
[*3] ブートローダー(poiboot)にて、カーネルスタックのベースアドレスをカーネルをロードしたアドレスの1MB先にしているのと、カーネルのリンカスクリプト(kernel.ld)にてカーネルのメモリサイズを960KBとしているためです。詳しくは本シリーズ最初の「フルスクラッチで作る!x86_64自作OS(パート1)」を参照してください。
イーサネットフレーム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」はリングバッファ内の次に読む場所を覚えておくための変数です。詳しくは変数を使用する際に説明します。
前節で受信処理に使用する領域確保や定数の定義等を行いました。この節ではそれらを初期化します。
まずは受信ディスクリプタ用リングバッファのベースアドレス(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レジスタに設定できる各ビットを表す定数です。
[*4] 無差別(promiscuous)に受信する設定
[*5] CRCエラー、シンボルエラー、シーケンスエラー、長さのエラー、等のパケット
これでNICの受信に関する初期化処理を行うrx_init()は完成です。NIC全体の初期化処理を行う関数nic_init()から呼び出すようにしておきます(コード紹介は割愛)。
前節で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。
[*6] ポインタに整数Nを足した場合にポインタが参照する型のN個分先のアドレスとなる事を利用しています。
次に、受信ディスクリプタ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.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。
[*8] 動作確認を分かりやすくするためにも最初は極力色々な機能を動かさないで試す方針です。
前項でイーサネットフレームの受信を試しました。この項ではイーサネットフレームの送信を試してみます。
ここではあくまでもNICの動作確認として特にイーサネットフレームのフォーマットに従わないオレオレなフレームを送信してみます。
この項を通して「024_send_frame」のサンプルディレクトリの内容を作ります。
フレーム送信も受信と同じくディスクリプタ用のリングバッファとフレーム自体を置いておくためのバッファを使用して行います。
送信のディスクリプタ(Transmit Descriptor)のデータ構造は図2.6*9の通りです。
図2.6: 送信ディスクリプタのデータ構造
[*9] 3.3.3 Legacy Transmit Descriptor Format - PCI/PCI-X Family of Gigabit Ethernet Controllers Software Developer's Manual
また、各フィールドの意味については以下の通りです。
実は送信ディスクリプタには3種類あり、図2.6で紹介したものは「レガシー送信ディスクリプタ(Legacy Transmit Descriptor)」と呼ばれるものです。ただ、これは「古い」というよりは「オリジナル」としての意味合いであるとのことで、他の2つはまとめて「拡張」ディスクリプタと呼ばれる*10、とのことです(データシートより)。最初試すときはなるたけ簡単なやり方の方がトラブらなくて良いので、本書ではレガシー送信ディスクリプタで試します。
[*10] 拡張ディスクリプタには、「TCP/IPデータディスクリプタ」と呼ばれるレガシーディスクリプタを拡張したディスクリプタと、「TCP/IPコンテキストディスクリプタ」と呼ばれるNICの制御情報のみ(パケットデータ無し)のディスクリプタ、があります。
そして、送信ディスクリプタ用リングバッファの構造と使われ方は図2.7*11の通りです。
図2.7: 送信リングバッファについて
[*11] 3.4 Transmit Descriptor Ring Structure - PCI/PCI-X Family of Gigabit Ethernet Controllers Software Developer's Manual
特に受信の時と違いは無いです。
それでは、次の節から実装を始めます。
受信の時と同様に、今度は送信ディスクリプタのデータ構造の定義とグローバル変数による領域の確保を行います(リスト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つあれば十分です。また、イーサネットフレーム自体のバッファは確保していません。後述しますが送信用関数に渡されたデータ領域をそのまま使います。
[*12] 送信ディスクリプタの長さも受信ディスクリプタと同様に128バイトアライメントの制約があり、要素数としては8の倍数である必要があります。
その他は、受信の時と同様です。送信用に新たに使用するレジスタのアドレス定義等もinclude/nic.hへ追加していますがコード紹介しての説明は省略します。レジスタ自体については使用する際に適宜紹介します。
次に初期化処理「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); } /* 追加(ここまで) */ /* ・・・ 省略 ・・・ */
受信の時と異なる箇所としては、まず、各送信ディスクリプタの各フィールドの初期化処理です。各フィールドの初期化内容について以下にまとめます。
次に受信の時と異なる箇所としては、先頭(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を参照してください。
以上でtx_init()の説明は終了です。最後にtx_init()をnic_init()から呼び出すようにしておきます(コード紹介は割愛)。
送信したいイーサネットフレームのバッファへのポインタと長さを渡すと、そのデータを送信し、ステータスを返す関数「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が正常終了でそれ以外が異常終了を示します。
"BE"と出力した後、送信し、正常終了なら"EF"を、異常終了ならそれらのステータスに対応する2文字を出力するようにしてみました。
実行すると図2.8の様に"BEEF BEEF ..."と出力されます。
図2.8: 024_send_frameの実行結果
実際に対向PCと1対1で接続*13し、対向PC側でWireshark等でパケットキャプチャしている様子が図2.9です。
図2.9: 対向側で受信している様子
イーサネットフレームの先頭6バイトである宛先MACアドレス部に"0xbeefbeef"と書かれた謎のフレームが続々と受信されている事が確認できます。
[*13] 送信しているデータはイーサネットフレームの体を成していない(宛先MACアドレスや送信元MACアドレスが書いていない)ので、経路上にスイッチングハブ等が合った場合、おそらく間違いなく捨てられます。