MikanOS に NIC ドライバを実装する - 送受信編

この記事は [自作OS Advent Calendar](https://adventar.org/calendars/6581) 21日目の記事です。

# 前回のおさらい
前回の[初期化編](https://qiita.com/Saza-ku/items/83d48ed82ffad2027514)では NIC の設定と初期化を行い、ドライバから NIC を操作できるようにしました。今回はついにそれを利用してバッファの送受信を行ってみましょう！

# 今回やること
送信処理、受信処理の順に書いていきます。受信処理では、バッファの受信はポーリングで行います。割り込みで受信するようにするのは次回に行います。

# 送信処理

### Nic::Send の実装
`Nic`クラスに`Send`メソッドを実装していきます。実装の概要は[準備編](https://qiita.com/Saza-ku/items/751bad3201da610d3199#送信処理)で見た通りです。

- 送信するバッファのアドレスと長さを取得する。
- アドレスと長さを `TAIL` が指す送信ディスクリプタに記述する。
- 送信ディスクリプタのその他のフィールド (command, status) を適切に初期化する。
- TAIL をインクリメントする。

実装は以下の通りです。

```cpp:kernel/net/nic/e1000.cpp
uint8_t Nic::Send(void *buf, uint16_t length) {
    // ディスクリプタリングを書き換える
    t_descriptor *desc = &this->desc_ring_addr_[this->tail_];
    desc->buffer_address = (uintptr_t)buf;
    desc->length = length;
    desc->cmd = desc->cmd | T_DESC_CMD_EOP;
    desc->sta = 0;

    // NIC の TAIL をインクリメントする
    this->tail_++;
    SetNicReg(TDT_OFFSET, this->tail_);

    // 送信処理の完了を待つ
    uint8_t send_status = 0;
    while(!send_status) {
      send_status = desc->sta & 0x0fu;
    }
    return send_status;
  }
```

- `T_DESC_CMD_EOP` は送信するバッファがパケットの終わりである時に立てるフラグですが、今回は 1 つのパケットを 1 つだけのバッファで送ることとするので、毎回 `T_DESC_CMD_EOP` を立てています。
- `status` の値は NIC の送信処理が完了した時に NIC が書き換えるので、これを見ることで送信が完了したかどうかを確認することができます。この値を `return` することは必要ではないです。(たぶん)

なんとこれだけでパケットが送信されます！ではメイン関数からこれを呼び出してみましょう！

### Nic::Send を呼び出す

```cpp:kernel/main.cpp
extern "C" void KernelMainNewStack(
    const FrameBufferConfig& frame_buffer_config_ref,
    const MemoryMap& memory_map_ref,
    const acpi::RSDP& acpi_table,
    void* volume_image,
    EFI_RUNTIME_SERVICES* rt) {
  // ...

  net::e1000::Initialize();
  char message_buffer[] = "Hello, World!";
  uint8_t sta = net::e1000::nic->Send(message_buffer, sizeof(message_buffer));
  printk("sta: %d\n", sta);

  // ...
}
```

これでネットワークに `Hello, World!` という文字列が流れることになります。ちゃんと送信処理がされているか確認してみましょう！

### 送信処理のデバッグ

QEMU を使ってデバッグします。QEMU の実行引数に、以下のような設定を追加します。

- E1000E (E1000 と互換性のある NIC) をデバイスに追加する。 [^1]
    - MAC アドレスを指定する。
- ホスト OS から MikanOS にポートフォワーディングする。(後の受信処理のため)
- E1000E が送受信するバッファを .pcap ファイルにダンプさせる。

```shell:osbook/devenv/run_image.sh
sudo qemu-system-x86_64 \
    -m 1G \
    -drive if=pflash,format=raw,readonly,file=$DEVENV_DIR/OVMF_CODE.fd \
    -drive if=pflash,format=raw,file=$DEVENV_DIR/OVMF_VARS.fd \
    -drive if=ide,index=0,media=disk,format=raw,file=$DISK_IMG \
    -device nec-usb-xhci,id=xhci \
    -device usb-mouse -device usb-kbd \
    -monitor stdio \
    -netdev user,id=net0,hostfwd=tcp:127.0.0.1:1234-:80 \
    -object filter-dump,id=fiter0,netdev=net0,file=dump.pcap \
    -device e1000e,netdev=net0,mac=52:54:00:12:34:56 \
    $QEMU_OPTS
```

これで MikanOS を起動してみると、dump.pcap が生成されます。

![スクリーンショット 2021-12-21 13.34.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/953258/0b547dd0-cd83-8e06-2285-d711eccefbe8.png)

確かに `Hello, World!` と送信されているようです！

# 受信処理
次に受信処理です。

### Nic::Receive の実装
`Nic`クラスに`Receive`メソッドを実装していきます。実装の概要は[準備編](https://qiita.com/Saza-ku/items/751bad3201da610d3199#受信処理)で見た通りです。

- 受信するバッファのポインタを引数に取る。
- TAIL が指すディスクリプタの次のディスクリプタが、受信しているかどうかを確認し、その受信バッファのアドレスと長さを取得する。
    - 受信したかどうかの判定は、ディスクリプタの `command` フィールドに `DD` というフラグがあり、これが立っているときは受信しているという合図になる。
- 引数に取ったバッファに、受信したバッファをコピーする。
- TAIL をインクリメントする。
- バッファの長さを返す。

実装は以下の通りです。

```cpp:kernel/net/nic/e1000.cpp
#define R_DESC_STA_DD 0b00000001
uint16_t Nic::Receive (void *buf) {
    uint32_t next_tail = (r_tail_ + 1) % R_DESC_NUM;
    r_descriptor *desc = &r_desc_ring_addr_[next_tail];
    uint16_t len = 0;

    if (desc->status & R_DESC_STA_DD) {
      len = desc->length + 1;
      memcpy(buf, (void *)desc->buffer_address, len);

      desc->status = 0;

      r_tail_++;
      SetNicReg(RDT_OFFSET, r_tail_);
    }

    return len;
  }
```

呼び出し側は、以下のようにしてこのメソッドを使います。

- 受信するバッファを用意する。
- これを `Nic::Receive` の引数に渡す。
- `Nic::Receive` の返り値の長さだけバッファを読み出す。

### Nic::Receive を呼び出す
ではこれもメイン関数から呼び出すことでデバッグしてみましょう！

```cpp:kernel/main.cpp
extern "C" void KernelMainNewStack(
    const FrameBufferConfig& frame_buffer_config_ref,
    const MemoryMap& memory_map_ref,
    const acpi::RSDP& acpi_table,
    void* volume_image,
    EFI_RUNTIME_SERVICES* rt) {
  // ...
  net::e1000::Initialize();

  char packet_buffer[PACKET_SIZE];
  uint32_t len = 0;
  while(len == 0) {
    len = net::e1000::nic->Receive(packet_buffer);
  }
  printk("Received: 0x%x", packet_buffer);

  // ...
}
```

### 受信処理のデバッグ
以下の手順でデバッグします。

- MikanOS を起動 (QEMU の起動オプションをさっきと同様)
- ホスト OS のターミナルから `curl localhost:1234` を叩く

すると、MikanOS がなんらかのパケットを受け取ったことを画面に表示します。

![スクリーンショット 2021-12-21 14.36.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/953258/1ef12c51-3132-fc27-d9f2-5410f7f5b413.png)

また、ダンプされた .pcap ファイルを見ることでも確認できます。MikanOS は ARP リクエストを受け取ったようです。

![スクリーンショット 2021-12-21 14.45.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/953258/3373a230-a39e-1c9c-905f-092229cd9b58.png)

# まとめ
以上でバッファの送受信ができるようになりました！次回は受信したことを割り込みで通知させるような設定を行います。

次回: まだ

[^1]: E1000 ではなく E1000E を使うのは、QEMU の E1000 だと割り込みが行われないからです。より厳密には、E1000 には MSI のシミュレーションが実装されていないからです。