NICはPCIで接続されており、NICを制御するための情報はPCIの管理領域(PCIコンフィグレーション空間)に並ぶレジスタにあります。この章では、NICを制御するための情報をPCIコンフィグレーション空間内のレジスタから取得する方法を、以下の流れで紹介します。
lspciはPCIの情報を取得するLinuxのコマンドです。この項では、自作OSを動作させる対象のマシン(VM含む)上でLinuxを起動し、lspciコマンドを使ってPCIのデバイス情報を取得し、マシンに搭載されているNICを把握します。
本書では一応、想定する開発環境をLinux(Debian)としているので、「開発マシン=自作OSを実行するマシン」の場合、そのマシン上で開発環境を起動させてください。「開発マシンと自作OSを実行するマシンが異なる」場合、実行マシンにLinuxがインストールされていない場合は、Ubuntu等のライブディスク/USBでLinuxをライブ起動してみてください。
それでは、lspciを実行してみます(図1.1)。
図1.1: lspciでPCIのデバイスを一覧表示
図1.1のスクリーンショットの最下行の"00:19.0 Ethernet controller: Intel Corporation Ethernet Connection (3) I218-V (rev 03)"が筆者のPCのNICです。
PCIコンフィグレーション空間から目的のデバイスの情報を取得するには、そのデバイスの「バス番号」・「デバイス番号」・「ファンクション番号」が分かっていれば良いです。そして、それは先程の出力結果の"00:19.0"に当たります。先頭の"00"が「バス番号」、真ん中の"19"がデバイス番号、最後の"0"がファンクション番号です。なお、これらの値は16進数表記なので、"19"は"0x19"です。
これで、PCIコンフィグレーション空間からNICの情報を読むために必要な情報は揃ったのですが、自作のコードでPCIコンフィグレーション空間を読んだ結果が正しいことを確認したいので、先にlspciでPCIコンフィグレーション空間を読んでみます。
lspciでPCIコンフィグレーション空間の情報も表示させるために、"-vx"オプションを指定してみます。"v"オプションが各デバイスのPCIコンフィグレーション空間の内容を解釈した結果を表示するオプションで、"x"がPCIコンフィグレーション空間の先頭64バイトを16進ダンプするオプションです。
試しに実行してみると図1.2の通りです。見たいデバイスは決まっているので、図1.2では"-s"オプションで先ほど確認したNICのバス番号・デバイス番号・ファンクション番号を指定しています。
図1.2: PCIコンフィグレーション空間の情報を表示
図1.2の後半の16進ダンプがPCIコンフィグレーション空間の生のデータ(の一部、先頭64バイト)で、それを解釈した結果が前半部分です。
ここで確認しておきたいのは、PCIコンフィグレーション空間の先頭4バイトのレジスタの内容です。PCIコンフィグレーション空間の先頭4バイトのレジスタには「ベンダーID(2バイト)」・「デバイスID(2バイト)」が格納されています。今回の場合、16進ダンプの"86 80"(リトルエンディアンなので0x8086)がベンダーID、"a3 15"(リトルエンディアンなので0x15a3)がデバイスIDです。この4バイトはデバイスに一意なIDなので、デバドラ等はこの値からどのベンダーの何というデバイスなのかを判別します。
これで、NICのPCIコンフィグレーション空間の先頭4バイトは「ベンダーID: 0x8086」・「デバイスID: 0x15a3」だと分かったので、以降でPCIコンフィグレーション空間を自作のコードで読んで見る際、この値が読めれば、読み方が間違っていないと言えます。
なお、表示されているその他の項目についてここで詳しく紹介はしません。「こんな感じの内容が格納されているのか」と雰囲気がつかめればOKです*1。
[*1] 一部で読む為にはスーパーユーザー権限が必要な項目もあり、"Capabilities"には"<access denied>"と表示されています。見てみたい場合はlspciにsudoを先頭に付ける等してスーパーユーザーで実行してください。
PCIコンフィグレーション空間は、ポインタ等でアドレス指定するようなメモリ空間とも、in/out命令でアクセスするIO空間とも別のアドレス空間です。PCIコンフィグレーション空間へは、「CONFIG_ADDRESSレジスタ(IOアドレス:0x0cf8 〜 0x0cfb の4バイトレジスタ)」へアクセスしたいPCIコンフィグレーション空間のアドレスを設定した上で、「CONFIG_DATAレジスタ(IOアドレス:0x0cfc 〜 0x0cff の4バイトレジスタ)」へread/writeすることで、CONFIG_ADDRESSレジスタへ指定したPCIコンフィグレーション空間のレジスタをread/writeできます。
そして、CONFIG_ADDRESSに指定するPCIコンフィグレーション空間のアドレスのフォーマットは図1.3の通りです。前項で確認した「バス番号」・「デバイス番号」・「ファンクション番号」でどのデバイスのPCIコンフィグレーション空間かが決まり、「レジスタアドレス(PCIコンフィグレーション空間先頭からのオフセット)」でPCIコンフィグレーション空間のどのレジスタかが決まります。
図1.3: CONFIG_ADDRESSのフォーマット
最上位ビットである「イネーブルビット」を1でCONFIG_ADDRESSにアドレスをセットすると、次のCONFIG_DATAへのアクセスは、PCIコンフィグレーション空間のレジスタへのアクセスとなります。
なお、「レジスタアドレス」は4バイトアライメント(4の倍数)で指定しますので、レジスタアドレスの下位2ビットは常に0です。そのため、例えばデバイスID(レジスタアドレス:0x02)にアクセスしたい場合に、CONFIG_ADDRESSのレジスタアドレスに0x02を指定することはできません。その場合、レジスタアドレスには0x00を指定して、CONFIG_DATAレジスタを0x0cfe(CONFIG_DATAレジスタの上位2バイト)から2バイト読み出すようにするか、CONFIG_DATAレジスタは0x0cfcから4バイト読み出した後で上位2バイトを取り出すようにします。
また、CONFIG_ADDRESSレジスタは32ビットアクセス限定です。CONFIG_ADDRESSの下位1バイトに当たるレジスタアドレスだけを書き換えようと0x0cfbに対して8ビットアクセスで書き込みを行ったとしても、それはCONFIG_ADDRESSではなく、同じIOアドレスにマップされた別のレジスタへのアクセスになります。
CONFIG_ADDRESS・CONFIG_DATAを使用してNICのベンダーID・デバイスIDを読んでみます。それぞれのレジスタの使い方は前項で説明した通りなので、早速サンプルコードを紹介します。
この項のサンプルディレクトリは「011_dump_vid_did」です。
まず、32ビット単位のin/out命令を実行するio_read32()とio_write32()を追加します(リスト1.1)。少なくともCONFIG_ADDRESSレジスタへは32ビットアクセスでなければならないので、CONFIG_DATAも必要がない限り32ビットでアクセスするようにします。
リスト1.1: 011_dump_vid_did/x86.c
/* ・・・ 省略 ・・・ */ inline void io_write(unsigned short addr, unsigned char value) { asm volatile ("outb %[value], %[addr]" :: [value]"a"(value), [addr]"d"(addr)); } /* 追加(ここから) */ inline unsigned int io_read32(unsigned short addr) { unsigned int value; asm volatile ("inl %[addr], %[value]" : [value]"=a"(value) : [addr]"d"(addr)); return value; } inline void io_write32(unsigned short addr, unsigned int value) { asm volatile ("outl %[value], %[addr]" :: [value]"a"(value), [addr]"d"(addr)); } /* 追加(ここまで) */ void gdt_init(void) /* ・・・ 省略 ・・・ */
バイト(8ビット)単位のio命令(inb/outb)を使うこれまでのio_read()/io_write()を元に、32ビット単位のio命令(inl/outl)を使う関数「io_read32()」・「io_write32()」を作成しました。
リスト1.1の変更に併せて、include/x86.hへio_read32()とio_write32()のプロトタイプ宣言を追加しておいてください。(コードを引用しての説明は省略します。)
ここでは動作確認としてmain.cに処理を追加してみます。
まず、使用する定数等を定義します(リスト1.2)。
リスト1.2: 011_dump_vid_did/main.c
/* ・・・ 省略 ・・・ */ #define INIT_APP "test" /* 追加(ここから) */ /* PCIの定義 */ #define PCI_CONF_DID_VID 0x00 #define CONFIG_ADDRESS 0x0cf8 #define CONFIG_DATA 0x0cfc union pci_config_address { unsigned int raw; struct __attribute__((packed)) { unsigned int reg_addr:8; unsigned int func_num:3; unsigned int dev_num:5; unsigned int bus_num:8; unsigned int _reserved:7; unsigned int enable_bit:1; }; }; /* NICの定義 */ #define NIC_BUS_NUM 0x00 #define NIC_DEV_NUM 0x19 #define NIC_FUNC_NUM 0x0 /* 追加(ここまで) */ /* ・・・ 省略 ・・・ */
定義している内容はこれまで説明してきた通りで、PCIに関する定数としては、PCIコンフィグレーション空間にアクセスするためのIOアドレス(CONFIG_ADDRESS/CONFIG_DATA)と、ベンダーID・デバイスIDのPCIコンフィグレーション空間内のオフセット(PCI_CONF_DID_VID)を定義しています。
また、CONFIG_ADDRESSに渡すデータの構造は、共用体と構造体で定義しています。(使い方は後ほど紹介します。)
そして、NICに関してはバス番号(NIC_BUS_NUM)、デバイス番号(NIC_DEV_NUM)、ファンクション番号(NIC_FUNC_NUM)を定義しています。
続いて、ベンダーIDとデバイスIDを読み出す処理を実装します(リスト1.3)。
リスト1.3: 011_dump_vid_did/main.c
/* ・・・ 省略 ・・・ */ void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi, void *_fs_start) { /* ・・・ 省略 ・・・ */ /* 周辺ICの初期化 */ pic_init(); hpet_init(); kbc_init(); /* 追加(ここから) */ /* NICのベンダーID・デバイスIDを取得 */ /* CONFIG_ADDRESSを設定 */ union pci_config_address conf_addr; conf_addr.raw = 0; conf_addr.bus_num = NIC_BUS_NUM; conf_addr.dev_num = NIC_DEV_NUM; conf_addr.func_num = NIC_FUNC_NUM; conf_addr.reg_addr = PCI_CONF_DID_VID; conf_addr.enable_bit = 1; io_write32(CONFIG_ADDRESS, conf_addr.raw); /* CONFIG_DATAを読み出す */ unsigned int conf_data = io_read32(CONFIG_DATA); /* 読み出したデータからベンダーID・デバイスIDを取得 */ unsigned short vendor_id = conf_data & 0x0000ffff; unsigned short device_id = conf_data >> 16; /* 表示 */ puts("VENDOR ID "); puth(vendor_id, 4); puts("\r\n"); puts("DEVICE ID "); puth(device_id, 4); puts("\r\n"); /* haltして待つ */ while (1) cpu_halt(); /* 追加(ここまで) */ /* システムコールの初期化 */ syscall_init(); /* ・・・ 省略 ・・・ */
「CONFIG_ADDRESSを設定」のコードブロックで、先程定義したpci_config_address共用体を使っています。まずメンバーのraw変数へ0を代入することでconf_addr全体をゼロクリアしています。次に、構造体部分の定義を利用して、バス番号・デバイス番号・ファンクション番号と、PCIコンフィグレーション空間内のアクセスしたいレジスタのアドレス(PCIコンフィグレーション空間先頭からのオフセット)をconf_addrへ代入しています。最後にenable_bitに1をセットして、CONFIG_ADDRESSに設定します。
この状態で、CONFIG_DATAにアクセスすると、CONFIG_ADDRESSに設定したPCIコンフィグレーション空間のレジスタへアクセスできます。今回の場合、ベンダーID・デバイスIDのレジスタを読み出しています。
読み出した32ビット(4バイト)の下位16ビットにベンダーIDが、上位16ビットにデバイスIDが格納されていますので、マスクしたり、ビットシフトしたりしてそれぞれを取り出し、表示しています。
このように、PCIコンフィグレーション空間へはCONFIG_ADDRESSとCONFIG_DATAを使用してアクセスします。
実行すると図1.4の様にNICのベンダーIDとデバイスIDが表示されます。
図1.4: 011_dump_vid_didの実行結果
前項で確認したものと同じ値が表示されているのでPCIコンフィグレーション空間へのアクセスは問題無さそうです。
この項の最後に、今後PCIコンフィグレーション空間の読み出し等を行いやすくするために、main.cに書いていた処理を関数化しておきます。
以降では関数化等を行ったコードをベースに説明していきます。「011_dump_vid_did_refactor」というディレクトリに作業済みのコードを置いていますので必要に応じて参照してみてください。
関数化したのはPCIコンフィグレーション空間からレジスタを読み出す関数「get_pci_conf_reg」と、それを使用してベンダーID・デバイスIDをダンプする関数「dump_vid_did」の2つで、pci.cというソースファイルを新たに作成しリスト1.4のように関数化しました。(その他にmain.cに書いていたPCI関係の定義もpci.cへ移動しましたが、割愛します。)
リスト1.4: 011_dump_vid_did_refactor/pci.c
/* ・・・ 省略 ・・・ */ unsigned int get_pci_conf_reg( unsigned char bus, unsigned char dev, unsigned char func, unsigned char reg) { /* CONFIG_ADDRESSを設定 */ union pci_config_address conf_addr; conf_addr.raw = 0; conf_addr.bus_num = bus; conf_addr.dev_num = dev; conf_addr.func_num = func; conf_addr.reg_addr = reg; conf_addr.enable_bit = 1; io_write32(CONFIG_ADDRESS, conf_addr.raw); /* CONFIG_DATAを読み出す */ return io_read32(CONFIG_DATA); } void dump_vid_did(unsigned char bus, unsigned char dev, unsigned char func) { /* PCIコンフィグレーション空間のレジスタを読み出す */ unsigned int conf_data = get_pci_conf_reg( bus, dev, func, PCI_CONF_DID_VID); /* 読み出したデータからベンダーID・デバイスIDを取得 */ unsigned short vendor_id = conf_data & 0x0000ffff; unsigned short device_id = conf_data >> 16; /* 表示 */ puts("VENDOR ID "); puth(vendor_id, 4); puts("\r\n"); puts("DEVICE ID "); puth(device_id, 4); puts("\r\n"); }
その他、主に行ったことは以下のとおりですが、main.cに書いていたものを移動させただけなのでコードを引用しての紹介は割愛します。
PCIコンフィグレーション空間内には対象デバイスのPCIに関わる設定やステータス確認のためのレジスタとして「コマンド」レジスタと「ステータス」レジスタがあります。
前項まではPCIコンフィグレーション空間の先頭の4バイトしか見ていなかったので特に出していませんでしたが、ここで、PCIコンフィグレーション空間のレジスタマップを示します(図1.5)。(本書で使用するレジスタのみに絞っています。)
図1.5: PCIコンフィグレーション空間(本書で使用するレジスタのみ抜粋)
図1.5の通り、ステータスとコマンドのレジスタはPCIコンフィグレーション空間の4バイト目です。
コマンドレジスタとステータスレジスタの内容をダンプしてみます。
この節のサンプルディレクトリは「012_dump_command_status」です。
ここでは、pci.cへ「dump_command_status」という関数を追加し、コマンド・ステータスそれぞれの値の16進ダンプを表示すると同時に、それぞれのレジスタに設定されているビット名を表示するようにしてみます。
前項の最後に、ベンダーIDとデバイスIDをダンプする関数をdump_vid_didという名前で作成したのと同じように、コマンドとステータスのレジスタの内容をダンプする関数を「dump_command_status」という名前でpci.cへ追加します。
まず、新たに使用する定義をinclude/pci.hへ追加します(リスト1.5)。
リスト1.5: 012-dump_command_status/pci.h
#pragma once #define PCI_CONF_DID_VID 0x00 /* 追加(ここから) */ #define PCI_CONF_STATUS_COMMAND 0x04 #define PCI_COM_IO_EN (1U << 0) #define PCI_COM_MEM_EN (1U << 1) #define PCI_COM_BUS_MASTER_EN (1U << 2) #define PCI_COM_SPECIAL_CYCLE (1U << 3) #define PCI_COM_MEMW_INV_EN (1U << 4) #define PCI_COM_VGA_PAL_SNP (1U << 5) #define PCI_COM_PARITY_ERR_RES (1U << 6) #define PCI_COM_SERR_EN (1U << 8) #define PCI_COM_FAST_BACK2BACK_EN (1U << 9) #define PCI_COM_INTR_DIS (1U << 10) #define PCI_STAT_INTR (1U << 3) #define PCI_STAT_MULT_FUNC (1U << 4) #define PCI_STAT_66MHZ (1U << 5) #define PCI_STAT_FAST_BACK2BACK (1U << 7) #define PCI_STAT_DATA_PARITY_ERR (1U << 8) #define PCI_STAT_DEVSEL_MASK (3U << 9) #define PCI_STAT_DEVSEL_FAST (0b00 << 9) #define PCI_STAT_DEVSEL_MID (0b01 << 9) #define PCI_STAT_DEVSEL_LOW (0b10 << 9) #define PCI_STAT_SND_TARGET_ABORT (1U << 11) #define PCI_STAT_RCV_TARGET_ABORT (1U << 12) #define PCI_STAT_RCV_MASTER_ABORT (1U << 13) #define PCI_STAT_SYS_ERR (1U << 14) #define PCI_STAT_PARITY_ERR (1U << 15) /* 追加(ここまで) */ unsigned int get_pci_conf_reg( /* ・・・ 省略 ・・・ */
「PCI_CONF_STATUS_COMMAND」でステータス・コマンドレジスタのPCIコンフィグレーション空間先頭からのオフセットを定義し、「PCI_COM_*」と「PCI_STAT_*」で、コマンドレジスタ・ステータスレジスタそれぞれのビットを定義しています。
コマンド・ステータスそれぞれのレジスタの各ビットについては、本書で意識すべき範囲のビットのみ、サンプルコードの実行結果を説明する際に簡単に紹介します。
次に、追加した定数を使用してコマンド・ステータスレジスタをダンプする関数(dump_command_status())を追加します(リスト1.6)。
リスト1.6: 012_dump_command_status/pci.c
/* ・・・ 省略 ・・・ */ void dump_vid_did(unsigned char bus, unsigned char dev, unsigned char func) { /* ・・・ 省略 ・・・ */ } /* 追加(ここから) */ void dump_command_status( unsigned char bus, unsigned char dev, unsigned char func) { /* PCIコンフィグレーション空間のレジスタを読み出す */ unsigned int conf_data = get_pci_conf_reg( bus, dev, func, PCI_CONF_STATUS_COMMAND); /* 読み出したデータからステータスとコマンド値を取得 */ unsigned short command = conf_data & 0x0000ffff; unsigned short status = conf_data >> 16; /* 表示 */ puts("COMMAND "); puth(command, 4); puts("\r\n"); if (command & PCI_COM_IO_EN) puts("IO_EN "); if (command & PCI_COM_MEM_EN) puts("MEM_EN "); if (command & PCI_COM_BUS_MASTER_EN) puts("BUS_MASTER_EN "); if (command & PCI_COM_SPECIAL_CYCLE) puts("SPECIAL_CYCLE "); if (command & PCI_COM_MEMW_INV_EN) puts("MEMW_INV_EN "); if (command & PCI_COM_VGA_PAL_SNP) puts("VGA_PAL_SNP "); if (command & PCI_COM_PARITY_ERR_RES) puts("PARITY_ERR_RES "); if (command & PCI_COM_SERR_EN) puts("SERR_EN "); if (command & PCI_COM_FAST_BACK2BACK_EN) puts("FAST_BACK2BACK_EN "); if (command & PCI_COM_INTR_DIS) puts("INTR_DIS"); puts("\r\n"); puts("STATUS "); puth(status, 4); puts("\r\n"); if (status & PCI_STAT_INTR) puts("INTR "); if (status & PCI_STAT_MULT_FUNC) puts("MULT_FUNC "); if (status & PCI_STAT_66MHZ) puts("66MHZ "); if (status & PCI_STAT_FAST_BACK2BACK) puts("FAST_BACK2BACK "); if (status & PCI_STAT_DATA_PARITY_ERR) puts("DATA_PARITY_ERR "); switch (status & PCI_STAT_DEVSEL_MASK) { case PCI_STAT_DEVSEL_FAST: puts("DEVSEL_FAST "); break; case PCI_STAT_DEVSEL_MID: puts("DEVSEL_MID "); break; case PCI_STAT_DEVSEL_LOW: puts("DEVSEL_LOW "); break; } if (status & PCI_STAT_SND_TARGET_ABORT) puts("SND_TARGET_ABORT "); if (status & PCI_STAT_RCV_TARGET_ABORT) puts("RCV_TARGET_ABORT "); if (status & PCI_STAT_RCV_MASTER_ABORT) puts("RCV_MASTER_ABORT "); if (status & PCI_STAT_SYS_ERR) puts("SYS_ERR "); if (status & PCI_STAT_PARITY_ERR) puts("PARITY_ERR"); puts("\r\n"); } /* 追加(ここまで) */
PCIコンフィグレーション空間上で読み出すレジスタがステータス・コマンド(オフセット:0x04)に変わっただけで、レジスタへのアクセスの仕方はdump_vid_did()と同じです。
表示の処理が少し長いですが、単に一つずつビットをチェックして、セットされているビットがあれば、そのビット名を表示するようにしているだけです。
dump_command_status()をmain.cから呼び出すように変更し、実行すると、筆者のPCでは図1.6のように表示されました。
図1.6: 012_dump_command_statusの実行結果
図1.6の結果から、ここでは初期値としてコマンドレジスタには「IO_EN」と「MEM_EN」そして「BUS_MASTER_EN」のビットが立っていることが確認できました。重要なのは「MEM_EN」と「BUS_MASTER_EN」で、MEM_ENがセットされていることからNICのレジスタへはメモリアクセスできる事が分かり、BUS_MASTER_ENもセットされているのでNICデバイスはバスマスターとして動作できる状態であることも分かります。コマンドレジスタは書き込み可能なレジスタなので、もしいずれかのフラグがセットされていなかった場合、次節で紹介するPCIコンフィグレーション空間のレジスタへの書き込み方法を参考にこれらのフラグをセットしてください。
なお、「IO_EN」と「MEM」はデバイス側が対応していない場合は1をセットしても読みだした時に0になるので、その場合は1がセットできるアクセス方法でNICレジスタへアクセスします。本書ではNICのレジスタへメモリアクセスできることを想定してサンプルプログラムを作っていますが、IOアクセスの場合の例も適宜紹介します。
また、図1.6の結果から、ステータスの初期値としては「MULT_FUNC」と「DEVSEL_FAST」がセットされていました。これは何かというと、MULT_FUNCは複数の機能(function)を持つデバイスである事を示していて、PCIコンフィグレーション空間へアクセスする際にファンクション番号だけを変更してアクセスすると別の機能にアクセスできます。また、DEVSEL_FASTは、バス上で特定のデバイスを選択するときの「DEVSEL」という信号に対する応答タイミングが「FAST」であることを示しています。ここらへんはデバイスの特性の問題で、今回NICドライバを作る上でどうこうする部分ではないので「そういうものか」と思っておけば良いです。
PCIコンフィグレーション空間への書き込みを試すことも兼ねて、コマンドレジスタの割り込み無効ビットを設定し、PCIレベルで割り込みを無効化しておきます。
この節のサンプルディレクトリは「013_set_command」です。
まず、PCIコンフィグレーション空間のレジスタへの設定を行う「set_pci_conf_reg()」をpci.cへ追加します(リスト1.7)。
リスト1.7: 013_set_command/pci.c
/* ・・・ 省略 ・・・ */ unsigned int get_pci_conf_reg( unsigned char bus, unsigned char dev, unsigned char func, unsigned char reg) { /* ・・・ 省略 ・・・ */ } /* 追加(ここから) */ void set_pci_conf_reg(unsigned char bus, unsigned char dev, unsigned char func, unsigned char reg, unsigned int val) { /* CONFIG_ADDRESSを設定 */ union pci_config_address conf_addr; conf_addr.raw = 0; conf_addr.bus_num = bus; conf_addr.dev_num = dev; conf_addr.func_num = func; conf_addr.reg_addr = reg; conf_addr.enable_bit = 1; io_write32(CONFIG_ADDRESS, conf_addr.raw); /* CONFIG_DATAへvalを書き込む */ io_write32(CONFIG_DATA, val); } /* 追加(ここまで) */ void dump_vid_did(unsigned char bus, unsigned char dev, unsigned char func) /* ・・・ 省略 ・・・ */
get_pci_conf_reg()とほとんど同じです。(引数や戻り値が変わったり、io_write32()を呼び出すように変えただけです。)
set_pci_conf_reg()を使用してコマンドレジスタへ割り込み無効ビットを設定してみます(リスト1.8)。
リスト1.8: 013_set_command/main.c
/* ・・・ 省略 ・・・ */ void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi, void *_fs_start) { /* フレームバッファ周りの初期化 */ fb_init(&pi->fb); set_fg(255, 255, 255); set_bg(0, 70, 250); clear_screen(); /* ACPIの初期化 */ acpi_init(pi->rsdp); /* CPU周りの初期化 */ gdt_init(); intr_init(); /* 周辺ICの初期化 */ pic_init(); hpet_init(); kbc_init(); /* 変更(ここから) */ /* 一旦、コマンドとステータスを読み出す */ 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); /* PCIコンフィグレーション空間からコマンドとステータスをダンプ */ dump_command_status(NIC_BUS_NUM, NIC_DEV_NUM, NIC_FUNC_NUM); /* haltして待つ */ while (1) cpu_halt(); /* 変更(ここまで) */ /* システムコールの初期化 */ syscall_init(); /* ・・・ 省略 ・・・ */
ステータスレジスタは、1が設定されているビットに1を書き込むと、そのビットをクリアします(0にします)。逆に0を書く場合は元のビットを変化させません。今回、ステータスレジスタを変化させる意図は無いため、読み出したコマンド・ステータスレジスタの内容を格納したconf_dataの上位16ビット(ステータスレジスタの領域)をゼロクリアしています。
そして、PCI_COM_INTR_DIS定数を使用してconf_dataのコマンドレジスタの領域(下位16ビット)に割り込み無効ビットをセットして、set_pci_conf_reg()で書き戻します。
実行すると図1.7のように表示されました。
図1.7: 013_set_commandの実行結果
「COMMAND」に新たに「INTR_DIS」と表示されているので、割り込み無効ビットが設定されていることが確認できました。
コマンドレジスタやステータスレジスタのその他のフラグについて詳しくは、本書末尾の「参考情報」で紹介している書籍か、あるいはネット上を検索してみてください。(十分に枯れた技術なので)
PCIの章の最後に、NICを制御するための情報、すなわちNIC自体のレジスタ群の先頭アドレス(ベースアドレス)を取得してみます。
デバイス固有のレジスタのベースアドレスは、PCIコンフィグレーション空間の「Base Address Register(BAR)」に格納されています。この項では、BARをPCIコンフィグレーション空間から取得し、その中からNICのレジスタのベースアドレスを取得します。
PCIコンフィグレーション空間内のBARの位置を確認するため、PCIコンフィグレーション空間のレジスタマップを再掲します(図1.8)。
図1.8: PCIコンフィグレーション空間(再掲)
BARのレジスタは、PCIコンフィグレーション空間の0x10以降です。
そして、BARの構成は図1.9の通りです。
図1.9: BARの構成
BARには、デバイスのレジスタのベースアドレスの他に、そのアドレスがIOアドレスなのかメモリアドレスなのか、32ビットなのか64ビットなのか、という情報が格納されています。それらの情報を踏まえて、BARから取得したアドレスを扱います。
アドレスがわかったので、BARをダンプしてみます。
この節のサンプルディレクトリは「014_dump_bar」です。
まず、pci.hにBARのアドレスと、BAR内のビットを定義します。併せてBARをダンプする関数「dump_bar」のプロトタイプ宣言も追加しておきます。(リスト1.9)
リスト1.9: 014_dump_bar/pci.h
/* ・・・ 省略 ・・・ */ #define PCI_STAT_PARITY_ERR (1U << 15) /* 追加(ここから) */ #define PCI_CONF_BAR 0x10 #define PCI_BAR_MASK_IO 0x00000001 #define PCI_BAR_MASK_MEM_TYPE 0x00000006 #define PCI_BAR_MEM_TYPE_32BIT 0x00000000 #define PCI_BAR_MEM_TYPE_1M 0x00000002 #define PCI_BAR_MEM_TYPE_64BIT 0x00000004 #define PCI_BAR_MASK_MEM_PREFETCHABLE 0x00000008 #define PCI_BAR_MASK_MEM_ADDR 0xfffffff0 #define PCI_BAR_MASK_IO_ADDR 0xfffffffc /* 追加(ここまで) */ unsigned int get_pci_conf_reg( unsigned char bus, unsigned char dev, unsigned char func, unsigned char reg); void set_pci_conf_reg(unsigned char bus, unsigned char dev, unsigned char func, unsigned char reg, unsigned int val); void dump_vid_did(unsigned char bus, unsigned char dev, unsigned char func); void dump_command_status( unsigned char bus, unsigned char dev, unsigned char func); /* 追加(ここから) */ void dump_bar(unsigned char bus, unsigned char dev, unsigned char func); /* 追加(ここまで) */
前節で確認したBARの構成を定義しました。
次に、pci.cにdump_bar()を実装します(リスト1.10)。
リスト1.10: 014_dump_bar/pci.c
/* ・・・ 省略 ・・・ */ void dump_command_status( unsigned char bus, unsigned char dev, unsigned char func) { /* ・・・ 省略 ・・・ */ } /* 追加(ここから) */ void dump_bar(unsigned char bus, unsigned char dev, unsigned char func) { /* PCIコンフィグレーション空間からBARを取得 */ unsigned int bar = get_pci_conf_reg(bus, dev, func, PCI_CONF_BAR); puts("BAR "); puth(bar, 8); puts("\r\n"); /* BARのタイプを確認しNICのレジスタのベースアドレスを取得 */ if (bar & PCI_BAR_MASK_IO) { /* IO空間用ベースアドレス */ puts("IO BASE "); puth(bar & PCI_BAR_MASK_IO_ADDR, 8); puts("\r\n"); } else { /* メモリ空間用ベースアドレス */ unsigned int bar_32; unsigned long long bar_upper; unsigned long long bar_64; switch (bar & PCI_BAR_MASK_MEM_TYPE) { case PCI_BAR_MEM_TYPE_32BIT: puts("MEM BASE 32BIT "); bar_32 = bar & PCI_BAR_MASK_MEM_ADDR; puth(bar_32, 8); puts("\r\n"); break; case PCI_BAR_MEM_TYPE_1M: puts("MEM BASE 1M "); bar_32 = bar & PCI_BAR_MASK_MEM_ADDR; puth(bar_32, 8); puts("\r\n"); break; case PCI_BAR_MEM_TYPE_64BIT: bar_upper = get_pci_conf_reg( bus, dev, func, PCI_CONF_BAR + 4); bar_64 = (bar_upper << 32) + (bar & PCI_BAR_MASK_MEM_ADDR); puts("MEM BASE 64BIT "); puth(bar_64, 16); puts("\r\n"); break; } if (bar & PCI_BAR_MASK_MEM_PREFETCHABLE) puts("PREFETCHABLE\r\n"); else puts("NON PREFETCHABLE\r\n"); } } /* 追加(ここまで) */
get_pci_conf_reg()でBARを取得し、BARの中からデバイスのレジスタのベースアドレスを取り出しています。
アドレスがIO空間のものなのかメモリ空間のものなのかを判別し、メモリ空間の場合はさらに32ビットなのか64ビットなのかを判定しています。
64ビットの場合、PCIコンフィグレーション空間の0x10からのBARの領域を図1.10のように使います。そのため、その場合には0x14からの4バイトも取得するようにしています。
図1.10: BAR(64ビット)
外から呼べるようにinclude/pci.hにdump_bar()のプロトタイプ宣言を追加しておいてください(コードは割愛)。
そして、main.cからリスト1.11のように呼び出します。
リスト1.11: 014_dump_bar/main.c
/* ・・・ 省略 ・・・ */ void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi, void *_fs_start) { /* ・・・ 省略 ・・・ */ nic_init(); /* 変更(ここから) */ /* BARをダンプ */ dump_bar(NIC_BUS_NUM, NIC_DEV_NUM, NIC_FUNC_NUM); /* 変更(ここまで) */ /* haltして待つ */ while (1) cpu_halt(); /* システムコールの初期化 */ syscall_init(); /* ・・・ 省略 ・・・ */
筆者のPCで実行してみると図1.11の結果となりました。
図1.11: 014_dump_barの実行結果
NICのレジスタのベースアドレスは、メモリ空間の32ビットのアドレスで、その値は0xf1300000であることが分かります。
実はこのアドレス情報はlspciで確認した際も表示されていました(図1.12)。
図1.12: PCIコンフィグレーション空間の情報を表示(再掲)
lspci出力4行目の「Memory at f1300000」がそうです*2。
[*2] 以降の行に「Memory at f133e000」や「I/O ports at 4080」とあるので、複数のメモリアドレスやIOアドレスとしてNICのレジスタへアクセスすることも可能なのかも知れません。(ただ、筆者未確認ですが。試してみると良いかも知れません。)
これで、取得したNICレジスタのベースアドレスも正しそうだと分かりました。
本章の最後に、BARからNICのレジスタのベースアドレスを取得する処理を「get_nic_reg_base」という名前で関数化しておきます。
この節のサンプルディレクトリは「015_get_nic_reg_base」です。
前節の確認を踏まえ、nic.cへリスト1.12のように関数化してみました。
リスト1.12: 015_get_nic_reg_base/nic.c
/* ・・・ 省略 ・・・ */ void nic_init(void) { /* ・・・ 省略 ・・・ */ } /* 追加(ここから) */ unsigned int get_nic_reg_base(void) { /* PCIコンフィグレーション空間からBARを取得 */ unsigned int bar = get_pci_conf_reg( NIC_BUS_NUM, NIC_DEV_NUM, NIC_FUNC_NUM, PCI_CONF_BAR); /* メモリ空間用ベースアドレス(32ビット)を返す */ return bar & PCI_BAR_MASK_MEM_ADDR; } /* 追加(ここまで) */
前節でNICのレジスタのベースアドレスは、32ビットのメモリアドレスであることを確認したので、それに従ってアドレスを抽出しています。もし、使用している環境で前節のdump_bar()が異なる結果を表示していた場合は、表示された結果に従ってget_nic_reg_base()を実装してください。その際は、dump_bar()の実装が参考になると思います。
最後に、この関数が外から呼べるようにinclude/nic.hへプロトタイプ宣言を追加しておいてください(コードは割愛)。