2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

MikanOS に NIC ドライバを実装する - 初期化編

Last updated at Posted at 2021-12-08

前回のおさらい

前回の準備編では、NIC とどのようなやりとりをすることでバッファの送受信をするのかをざっくりと見ました。さぁ、今回からついに実装に取り掛かっていきます!

今回やること

準備編で説明したような NIC とのやりとりをする前に、NIC が動作したり、ドライバが NIC とやりとりしたりできるようにするための初期化処理や初期設定をする必要があります。ちょっと地味な作業になります...。

PCI バスから NIC を探す

みかん本の 6 章でもやりましたが、PCI デバイスを操作するにはまず PCI バスからそれを探し出してくるのが第一歩です。というわけでみかん本 6 章で書いたコードとほとんど同じものを書きます。早速実装を見てみましょう。

kernel/net/nic/e1000.cpp
  void Initialize() {
    pci::Device* nic_dev = nullptr;
    for (int i = 0; i < pci::num_device; ++i) {
      // NIC のクラスコードは 0x020000
      if (pci::devices[i].class_code.Match(0x02u, 0x00u, 0x00u)) {
        nic_dev = &pci::devices[i];
        // Intel 製の NIC を優先
        if (0x8086 == pci::ReadVendorId(*nic_dev)) {
          break;
        }
      }
    }

    if (nic_dev) {
      Log(kInfo, "NIC has been found: %d.%d.%d\n",
          nic_dev->bus, nic_dev->device, nic_dev->function);
    } else {
      Log(kError, "NIC has not been found\n");
      exit(1);
    }

    const uint8_t bsp_local_apic_id =
      *reinterpret_cast<const uint32_t*>(0xfee00020) >> 24;
    Error err = pci::ConfigureMSIFixedDestination(
        *nic_dev, bsp_local_apic_id,
        pci::MSITriggerMode::kLevel, pci::MSIDeliveryMode::kFixed,
        InterruptVector::kNic, 0);
    if (err) {
      Log(kError, "Error while NIC MSI configuration: %s:%d\n", err.File(), err.Line());
    }

    // BAR を読む
    const WithError<uint64_t> nic_bar = pci::ReadBar(*nic_dev, 0);
    Log(kDebug, "ReadBar: %s\n", nic_bar.error.Name());
    const uint64_t nic_mmio_base = nic_bar.value & ~static_cast<uint64_t>(0xf);
    Log(kDebug, "NIC mmio_base = %08lx\n", nic_mmio_base);

    nic = new Nic{nic_mmio_base};
    nic->Initialize();
  }

ここでやっていることは、

  • PCI デバイスを全探索して NIC を見つけ出す
  • PCI コンフィグレーション空間から MMIO のベースアドレスを取得
  • それを Nic インスタンスに渡して初期化処理を行う

という感じです。MMIO についてはみかん本 6 章で解説されています。

Nic クラスを作る

ここで、一旦 Nic クラスの実装を確かめておきましょう。

kernel/net/nic/e1000.hpp
  // 現時点での実装
  class Nic {
   public:
    Nic(uintptr_t mmio_base);
    void Initialize(bool accept_all);
    
   private:
    const uintptr_t mmio_base_;
    uint32_t GetNicReg(uint16_t reg_offset);
    void SetNicReg(uint16_t reg_offset, uint32_t value);
  };

コンストラクタは、mmio_base_ の値をセットするだけにしておきます。

kernel/net/nic/e1000.cpp
Nic::Nic(uintptr_t mmio_base) : mmio_base_{mmio_base} {}

ディスクリプタを作る

前回見た、ディスクリプタとかいうものを構造体として定義しておきます。

kernel/net/nic/e1000.hpp
  #define T_DESC_NUM     8
  #define R_DESC_NUM     16

  // 送信ディスクリプタ
  struct t_descriptor {
    uint64_t buffer_address;
    uint16_t length;
    uint8_t  checksum_offset;
    uint8_t  command;
    uint8_t  status : 4;
    uint8_t  reserved : 4;
    uint8_t  checksum_start_field;
    uint16_t special;
  } __attribute__((packed));

  // 受信ディスクリプタ
  struct r_descriptor {
    uint64_t buffer_address;
    uint16_t length;
    uint16_t reserved;
    uint8_t  status;
    uint8_t  errors;
    uint16_t special;
  } __attribute__((packed));

ディスクリプタの構造や各フィールドの詳細はデータシートの 3 章を参照してください。
ざっと重要なフィールドの概要を説明しておくと、

  • buffer_address: バッファの Offset
  • length: バッファの Length

NIC の初期化・初期設定

nic->Initialize(); 実装を書いていきます。最初に書いた、void Initialize() の最後に出てきた、nic->Initialize(); の中身ですね。
ここでやることは以下の通りです。

  • ディスクリプタリングを作る
  • 受信バッファのメモリを確保する
  • 各ディスクリプタのフィールドを適切に初期化する
  • NIC を初期化する
  • 受信処理、送信処理の設定をする

ディスクリプタリングを作る

void Nic::Initialize() の実装を書いていきます。

kernel/net/nic/e1000.cpp
  #define T_DESC_NUM     8
  #define R_DESC_NUM     16

  void Nic::Initialize(bool accept_all) {
    // 送信ディスクリプタリングのメモリを確保
    static t_descriptor t_desc_ring_buf[sizeof(t_descriptor) * T_DESC_NUM]
      __attribute__((aligned(16)));
    t_desc_ring_addr_ = (t_descriptor *)t_desc_ring_buf;

    // 受信ディスクリプタのメモリを確保
    static r_descriptor r_desc_ring_buf[sizeof(r_descriptor) * R_DESC_NUM]
      __attribute__((aligned(16)));
    r_desc_ring_addr_ = (r_descriptor *)r_desc_ring_buf;

ここでの注意点は以下の通りです。

  • static をつけることで、その変数は静的領域に置かれます。こうしないとスタック領域に置かれてしまって return したときにメモリが解放されてしまうので注意です。
  • ディスクリプタリングのベースアドレスは 16 バイトアライメントである必要があるので、__attribute__((aligend(16))) をつけておきます。(詳細: データシート 13.4.25, 13.4.36)
  • ディスクリプタリングの要素数は、8 の倍数でないといけません。(詳細 データシート 13.4.27, 13.4.38)

上のコードでは、ディスクリプタリングを静的領域に確保して、そのベースアドレスを Nic インスタンスのプライベート変数 t_desc_ring_addr_r_desc_ring_addr_ に渡しています。

よって、ここでまた Nic クラスのフィールドを追加しておきます。

kernel/net/nic/e1000.hpp
  // 現時点での実装
  class Nic {
   // ...
   private:
    const uintptr_t mmio_base_;
    t_descriptor *t_desc_ring_addr_; // NEW!
    r_descriptor *r_desc_ring_addr_; // NEW!
    uint32_t GetNicReg(uint16_t reg_offset);
    void SetNicReg(uint16_t reg_offset, uint32_t value);
  };

受信バッファのメモリを確保する

kernel/net/nic/e1000.cpp
  #define PACKET_SIZE    2048
  void Nic::Initialize(bool accept_all) {
    // ...
    static uint8_t packet_buffer[R_DESC_NUM][PACKET_SIZE];
  }

これも静的領域に置きます。
PACKET_SIZE は 1 つの受信バッファのバイト数です。これはあとの受信処理の初期化で設定しますが、2048 Byte としておきます。

ディスクリプタのフィールドを適切に初期化する

前回見た通り、ディスクリプタには種々のフィールドが設けられているので、この値を初期化します。

送信ディスクリプタ

kernel/net/nic/e1000.cpp
    #define T_DESC_CMD_RS  0b00001000u
    // 送信ディスクリプタのフィールドを初期化する
    for (int i = 0; i < T_DESC_NUM; i++) {
      t_desc_ring_addr_[i].buffer_address = 0;
      t_desc_ring_addr_[i].length = 0;
      t_desc_ring_addr_[i].checksum_offset = 0;
      t_desc_ring_addr_[i].command = T_DESC_CMD_RS;
      t_desc_ring_addr_[i].status = 0;
      t_desc_ring_addr_[i].reserved = 0;
      t_desc_ring_addr_[i].checksum_start_field = 0;
      t_desc_ring_addr_[i].special = 0;
    }

送信ディスクリプタは、送信する際に設定する内容が多いので初期化時にはだいたい 0 にしておきます。例外は command です。ここに、NIC がそのディスクリプタが指すバッファを送信し終えたらドライバに対してその通知をさせるための設定をします。(詳細: データシート 3.3.3.1)

受信ディスクリプタ

kernel/net/nic/e1000.cpp
    static uint8_t packet_buffer[R_DESC_NUM][PACKET_SIZE];
    // 受信ディスクリプタのフィールドを初期化する
    for (int i = 0; i < R_DESC_NUM; i++) {
      r_desc_ring_addr_[i].buffer_address =
        (uint64_t)&packet_buffer[i];
      r_desc_ring_addr_[i].status = 0;
      r_desc_ring_addr_[i].errors = 0;
    }

受信ディスクリプタは、まず受信バッファのアドレスを設定します。そして statuserror を 0 に設定しておけば OK です。

MMIO の操作を抽象化する

ここからはついに NIC のレジスタをガチャガチャといじることで NIC を操作していきます。
MMIO を通じて NIC のレジスタにアクセスするので、この処理は先に関数にしておきましょう。

kernel/net/nic/e1000.cpp
  uint32_t Nic::GetNicReg(uint16_t reg_offset) {
    uintptr_t reg_addr = reg_offset + mmio_base_;
    return *(uint32_t *)reg_addr;
  }

  void Nic::SetNicReg(uint16_t reg_offset, uint32_t value) {
    uintptr_t reg_addr = (uintptr_t)(reg_offset + mmio_base_);
    *(uint32_t *)reg_addr = value;
  }
  • Nic::GetNicReg で指定したアドレスのレジスタから 32 bit 整数値を読み出します
  • Nic::SetNicReg で指定したアドレスのレジスタに 32 bit 整数値を書き込みます

Nic クラスのプライベートメソッドとして実装しておきました。mmio_base_ は Nic クラスのプライベートフィールドです。

NIC を初期化する

上で作った SetNicReg を使って設定を NIC の CTRL レジスタに書き込みます。CTRL レジスタの詳細はデータシートの 13.4.1 にあります。

kernel/net/nic/e1000.cpp
  #define CTRL_OFFSET    0x00000u
  #define CTRL_FD        0x00000001u // CTRL[0]
  #define CTRL_ASDE      0x00000020u // CTRL[5]
  #define CTRL_SLU       0x00000040u // CTRL[6]
  // NIC の初期化処理
    SetNicReg(CTRL_OFFSET,
      CTRL_FD | CTRL_ASDE | CTRL_SLU);

ここで行っている設定は以下の通りです。

  • CTRL_FD: 全二重モードで送受信します。
  • CTRL_ASDE: 送受信の速さを自動で検知します。

参考: データシート 14.3

Tail ポインタを NIC インスタンスに持たせる

前回見た通り、ディスクリプタリングは Head と Tail という二つのポインタを持っていて、Head は NIC が、Tail はドライバが動かすことになっていたのでした。
そのため、Tail の値は NIC レジスタに持たせて管理しましょう。

kernel/net/nic/e1000.hpp
  class Nic {
   // ...
   private:
    // ...
    uint32_t t_tail_
    uint32_t r_tail_;
    // ...
  };

そしてその値を初期化します。

kernel/net/nic/e1000.cpp
    t_tail_ = 0;
    r_tail_ = R_DESC_NUM - 1;

初期化値についてはデータシート 14.4, 14.5 にある通りです。
受信ディスクリプタについては

Software initializes the Receive Descriptor Head (RDH) register and Receive Descriptor Tail (RDT) with the appropriate head and tail addresses. Head should point to the first valid receive descriptor in the descriptor ring and tail should point to one descriptor beyond the last valid descriptor in the descriptor ring.

送信ディスクリプタについては

The Transmit Descriptor Head and Tail (TDH/TDT) registers are initialized (by hardware) to 0b after a power-on or a software initiated Ethernet controller reset. Software should write 0b to both these registers to ensure this.

送信処理の設定

kernel/net/nic/e1000.cpp
    #define TCTL_OFFSET    0x00400u
    #define TIPG_OFFSET    0x00410u
    #define TDBAL_OFFSET   0x03800u
    #define TDBAH_OFFSET   0x03804u
    #define TDLEN_OFFSET   0x03808u
    #define TDH_OFFSET     0x03810u
    #define TDT_OFFSET     0x03818u
    #define TCTL_EN        0x00000002u // TCTL[1]
    #define TCTL_PSP       0x00000008u // TCTL[3]
    #define TCTL_CT        0x00000100u // TCTL[11:4] = 10h
    #define TCTL_COLD      0x00400000u // TCTL[21:12] = 40h
    #define TIPG_IPGT      8u
    #define TIPG_IPGR1     8u
    #define TIPG_IPGR2     6u
    // 送信処理の設定
    SetNicReg(TCTL_OFFSET, TCTL_EN | TCTL_PSP | TCTL_CT | TCTL_COLD);
    SetNicReg(TIPG_OFFSET, TIPG_IPGT | TIPG_IPGR1 | TIPG_IPGR2);
    SetNicReg(TDBAL_OFFSET, (uintptr_t)t_desc_ring_addr_ & static_cast<uint64_t>(0xffffffff));
    SetNicReg(TDBAH_OFFSET, (uintptr_t)t_desc_ring_addr_ >> 32);
    SetNicReg(TDLEN_OFFSET, sizeof(t_descriptor) * T_DESC_NUM);
    SetNicReg(TDH_OFFSET, t_tail_);
    SetNicReg(TDT_OFFSET, t_tail_);

ここはデータシートの 14.5 の通りに設定しています。ただし、TIPG の値についてはセキュキャンの先生に言われた通りにしています。

ここはどうやら機種によって異なる値を入れる必要がありそうですね。QEMUはこの設定を完全に無視してるので入れなくても動きますが,Linuxとかの実装を見た感じIPGT=8, IPGR1=8, IPGR=6 とかを入れておくと良いと思います。

だそうです。

受信処理の設定

kernel/net/nic/e1900.cpp
    #define RCTL_OFFSET    0x00100u
    #define RDBAL_OFFSET   0x02800u
    #define RDBAH_OFFSET   0x02804u
    #define RDLEN_OFFSET   0x02808u
    #define RDH_OFFSET     0x02810u
    #define RDT_OFFSET     0x02818u
    #define RDTR_OFFSET    0x02820u
    #define RADV_OFFSET    0x0282Cu
    #define RAL_OFFSET     0x05400u
    #define RAH_OFFSET     0x05404u
    #define RCTL_EN        0x00000002u // RCTL[1]
    #define RCTL_BAM       0x00008000u // RCTL[15]
    #define RDTR_DELAY     0x00001000u // Delay Timer = 16^3
    #define RADV_DELAY     0x00001000u
    // 受信処理の設定
    uint32_t rctl_value = RCTL_BAM | RCTL_EN;
    SetNicReg(RCTL_OFFSET, rctl_value);
    SetNicReg(RDTR_OFFSET, RDTR_DELAY);
    SetNicReg(RADV_OFFSET, RADV_DELAY);
    SetNicReg(RDBAL_OFFSET, (uintptr_t)r_desc_ring_addr_ & static_cast<uint64_t>(0xffffffff));
    SetNicReg(RDBAH_OFFSET, (uintptr_t)r_desc_ring_addr_ >> 32);
    SetNicReg(RDLEN_OFFSET, sizeof(t_descriptor) * R_DESC_NUM);
    SetNicReg(RDH_OFFSET, 0);
    SetNicReg(RDT_OFFSET, r_tail_);

これもデータシートの 14.4 の通りの設定です。

まとめ

かなり地味でハードな回でした...。これをやらないと NIC が動いてくれないので仕方ないですね。
ついに次回からは NIC を操作してパケットの送受信処理を書いていきます!乞うご期待。

次回: 送受信編

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?