このテキストはBSD Magazine No.16 特集「デバイスドライバを書こう!」において「ソースから読むNetBSDデバイスドライバAPI」のタイトルで掲載された記事の後半部分 bus space API解説部分の抜粋です。記事の前半部分はやや冗長かつ一部内容がobsoleteになっていて個別の解説として再掲する意味が低いのと、長くて公開用に整理するのがめんどくさいという理由で手をつけていません。すいません。
Webでの公開にあたり投げやりなhtml化を含め一部の表記は見直していますが、基本的にAPI解説としては当時のソースのままであり、最新のNetBSDバージョンにおける変更には追従していません。
本テキストの著作権は筒井泉が有しています。obsoleteな不正確な情報が拡散するのもあまりよろしくないので、転載は控えて下さい。
bus_space(9) のマニュアルの冒頭には次のように書かれている。
"The bus_space functions exist to allow device drivers machine-independent access to bus memory and register areas."
直訳すれば、「バス上のメモリおよびレジスタ領域に対する機種に依存しないアクセス方法を提供するために存在する」となる。このまま bus_space(9) のマニュアルの記述を借りて要点をまとめると次のようになる。
これらを解決するために
ということを目標に考案されたのが bus_space APIである。
bus_space APIのコンセプトbus_space APIの考え方はすでにman pageの bus_space(9) 内に詳しく述べられているが、ここで簡単に触れておく。前項で述べたように、マシン、バスおよびデバイスによってアクセスする方法を切り替えるために、 bus_space APIでは bus space tag と bus space handle という引数が用いられる。これらの引数の定義は各機種依存であり <machine/bus.h> (ソースの実際のありかは sys/arch/${MACHINE}/include/bus.h)の中で定義されている。
多くのデバイスのマニュアルを見ればわかるが、デバイスを操作する際に必要なのはデバイスのレジスタやメモリのアドレスではなくてそれらのオフセットだけである。また、i386でのI/Oアドレスの 0x3f8に対するアクセスもarcでのメモリアドレスの 0xe20003f8 にアクセスも同じISA上デバイスの ns16550a の先頭のレジスタに対するアクセスであるが、両者の違いはアクセスを行なうアドレス領域とそのアクセス方法である。つまり、各アクセスに使用する関数において、アクセス方法、アクセス領域、オフセットの3つをセットで指定してやれば、一つのソースで複数の機種あるいは複数のバスの同一のデバイスに対応できることになる。
bus_space APIではデバイスに対するアクセス方法を規定する bus space tag、デバイスの領域を指定する bus space handle を定義し、その2つとオフセットをアクセスに用いる関数に対して指定することで機種非依存なデバイスドライバの記述を実現している。これらの bus_space関連の定義は arch/<arch>/include/bus.h の中で定義される。
bus space tag および bus space handle の具体的実装上記のようなコンセプトは理解できても、 bus space tag や bus space handle は具体的に何なのか、ということはなかなか難しいと思う[注24]。デバイスドライバを記述するだけであれば今まで説明したように bus space tag はバス層の attach_args 経由(もしくは専用の関数)で取得できるし bus space handle は bus_space_map(9) などのマップ関数で設定されるのでその中身が何なのかということは気にする必要はないのだが、少しくらいは中身を理解していないと「なぜ bus_space APIを使わなければならないか」といったことが理解しにくいのではないかと思う。そこでここではいくつかの機種を例に挙げて実際の bus space tag と bus space handle の中身、そして bus_space_map(9) 関数の実装について触れてみることにする。
[注24] もともとがアクセス方法の抽象化がbus_space APIの目的なのだから、その中身がわかりにくいのは当然といえば当然である。
まずはもっともユーザーになじみが深いであろうi386の実装を見てみよう。
bus space tagi386用の bus.h の実体は arch/x86/include/bus.h にある[注25]。その中の bus space tag の記述は次のとおりである。
-------------------------------------------------------------------------------- /* * Values for the x86 bus space tag, not to be used directly by MI code. */ #define X86_BUS_SPACE_IO 0 /* space is i/o space */ #define X86_BUS_SPACE_MEM 1 /* space is mem space */ : /* * Access methods for bus resources and address space. */ typedef int bus_space_tag_t; --------------------------------------------------------------------------------
つまり bus space tag の種類、すなわちアクセス方法としては2種類のみ定義されているということである。
bus space handlearch/x86/include/bus.h 中の bus space handle の記述は以下のとおりである。
-------------------------------------------------------------------------------- /* * Access methods for bus resources and address space. */ : typedef u_long bus_space_handle_t; --------------------------------------------------------------------------------
つまり、bus space handle についてはこれだけを見ても u_long型(つまりi386では32ビット整数)であるという以上のことはわからない。bus space handle の物理的意味を知るためには、実際に bus space handle を指定する bus_space_map(9)関数と bus space handle を使う bus_space_read_N() および bus_space_write_N() 関数[注26]の実装を見る必要がある。
bus_space_map()関数arch/x86/include/bus.h では bus_space_map() は次のようにdefineされている。
-------------------------------------------------------------------------------- #define bus_space_map(t, a, s, f, hp) \ x86_memio_map((t), (a), (s), (f), (hp)) --------------------------------------------------------------------------------
つまりマップ関数は1種類のみである。x86_memio_map() の実体は arch/x86/x86/bus_space.c にあり次のような内容である。
--------------------------------------------------------------------------------
int
x86_memio_map(t, bpa, size, flags, bshp)
bus_space_tag_t t;
bus_addr_t bpa;
bus_size_t size;
int flags;
bus_space_handle_t *bshp;
{
[...略...]
/*
* For I/O space, that's all she wrote.
*/
if (t == X86_BUS_SPACE_IO) {
*bshp = bpa; ←(1)
return (0);
}
if (bpa >= IOM_BEGIN && (bpa + size) <= IOM_END) {
*bshp = (bus_space_handle_t)ISA_HOLE_VADDR(bpa); ←(2)
return(0);
}
/*
* For memory space, map the bus physical address to
* a kernel virtual address.
*/
error = x86_mem_add_mapping(bpa, size,
(flags & BUS_SPACE_MAP_CACHEABLE) != 0, bshp); ←(3)
[...略...]
return (error);
}
--------------------------------------------------------------------------------
bus_space_map() がI/O空間の bus space tag とともに呼ばれた場合には(1)にあるとおり引数として渡されたアドレスをそのまま bus space handle として返している。つまりI/O空間のデバイスについては bus space handle はI/Oアドレスそのものである。
bus_space_map() がメモリ空間の bus space tag とともに呼ばれた場合はそのアドレスにより(2)と(3)の場合がある[注27]が、いずれの場合も bus space handle に入れられる値は引数として渡された物理アドレスに対応するカーネルの仮想アドレスである。
bus_space_read_N() および bus_space_write_N()arch/x86/include/bus.h の bus_space_read_1() の定義は次のとおりである。
-------------------------------------------------------------------------------- #define bus_space_read_1(t, h, o) \ ((t) == X86_BUS_SPACE_IO ? (inb((h) + (o))) :\ (*(volatile u_int8_t *)((h) + (o)))) --------------------------------------------------------------------------------
ここで bus space tag はI/O空間およびメモリ空間の属性であり、アクセス方法として inb() もしくはメモリ直接のいずれかを選択するのに使われている。bus space handle はI/O空間、メモリ空間のいずれの場合もアクセスを行なうアドレスの値そのものであり、結局 bus space handle とオフセット値を足したアドレスに対するアクセスを行っていることになる。
[注25] arch/x86 は従来からあるi386とAMDの新しい64ビットプロセッサであるAMD64と共有できるソースファイルを置くためのディレクトリとして最近作成された。
[注26] ここで N はアクセスの際のバイト幅を示す数値で、API上規定されているのは 1, 2, 4, 8 のいずれかである。
[注27] 旧来のISAのメモリスペースは arch/i386/i386/locore.S の中で静的にカーネル仮想アドレスにマップされており、ISA_HOLE_VADDR() は arch/x86/include/isa_machdep.h で定義されているマクロで、対応するメモリスペースの物理アドレスをこの仮想アドレスに変換する。x86_mem_add_mapping() は同じ arch/x86/x86/bus_space.c 中にあるが、この関数では新たにカーネル仮想アドレスを割り当ててそこに指定された物理アドレスをマップしている。
次に普通のユーザーにはほとんど縁がないと思われるarcの実装を見てみる。arcはCPUがMIPSであるがISAを持っているとか、仮想アドレス空間は32ビットであるにも関わらず物理アドレス空間は64ビットありPCIのアドレス空間が物理アドレスの4G以降の領域に配置されているなど、普通でない部分が満載のマシンである[注28]。
bus space tagarch/arc/include/bus.h の bus space tag の定義は次のとおりである。
-------------------------------------------------------------------------------- /* * Access methods for bus resources and address space. */ typedef struct arc_bus_space *bus_space_tag_t; --------------------------------------------------------------------------------
ここで指定されている struct arc_bus_space は同じ bus.h 内で定義されているが、バス空間のアドレスおよびサイズ、物理アドレスおよび仮想アドレス、そして bus_space_map()系の関数が含まれている。ここから、arcではバスによってマップ系の関数は使い分ける必要があるが bus_space_read_N() などのアクセス系の関数は一つだけであるということが推測できる。
bus space handlearch/arc/include/bus.h の bus space handle の定義は次のとおりである。
-------------------------------------------------------------------------------- /* * Access methods for bus resources and address space. */ typedef u_int32_t bus_space_handle_t; --------------------------------------------------------------------------------
i386と同じく、bus space handle の物理的意味を知るためには bus_space_map()関数と bus_space_read_N() および bus_space_write_N() 関数の実装を見る必要がある。
bus_space_map()関数bus space tag の項で見たとおり、arcでの bus_space_map() はバスによって異なるものになる。arcのバスのうちMIPS MagnumのISAを例に挙げると、ISA用の bus space tag はI/O空間のtagとメモリ空間用のtagのどちらも arch/arc/arc/c_magnum.c などで arch_bus_space_init() により初期化されていて[注29]、bus_space_map()関数の実体は arch/arc/arc/bus_space.c の arc_bus_space_map() であることがわかる。arc_bus_space_map() は領域のチェックをした後にバス固有の関数である arc_bus_space_compose_handle() を呼んでいる。
--------------------------------------------------------------------------------
int
arc_bus_space_compose_handle(bst, addr, size, flags, bshp)
bus_space_tag_t bst;
bus_addr_t addr;
bus_size_t size;
int flags;
bus_space_handle_t *bshp;
{
bus_space_handle_t bsh = bst->bs_vbase + (addr - bst->bs_start);
[...略...]
if ((flags & BUS_SPACE_MAP_CACHEABLE) == 0) {
*bshp = bsh;
return (0);
}
[...略...]
if (bsh < MIPS_KSEG2_START) { /* KSEG1 */
*bshp = MIPS_PHYS_TO_KSEG0(MIPS_KSEG1_TO_PHYS(bsh));
return (0);
}
[...略...]
*bshp = bsh;
return (0);
}
--------------------------------------------------------------------------------
かなりややこしいが、結論だけいえば bus space handle として返されるのは先頭の
bsh = bst->bs_vbase + (addr - bst->bs_start)
の式である(bst は bus space tag である)。bs_vbase と bs_start は arc_bus_space_init() で指定される引数で、bs_vaddr はISAの各アドレス空間がマップされている領域の先頭のカーネル仮想アドレスであり、bs_start はゼロである。よってarcの bus_space_map() でも bus space handle は指定されたバス空間アドレスにあるデバイスをアクセスするのに必要なカーネル仮想アドレスであることがわかる。
bus_space_read_N() および bus_space_write_N()arch/arc/include/bus.h の bus_space_read_1() の定義は次のとおりである。
--------------------------------------------------------------------------------
#define bus_space_read(BYTES,BITS) \
static __inline __CONCAT3(u_int,BITS,_t) \
__CONCAT(bus_space_read_,BYTES)(bus_space_tag_t bst, \
bus_space_handle_t bsh, bus_size_t offset) \
{ \
return (*(volatile __CONCAT3(u_int,BITS,_t) *) \
(bsh + (offset << __CONCAT(bst->bs_stride_,BYTES)))); \
}
bus_space_read(1,8)
--------------------------------------------------------------------------------
arcのアクセス系の bus_space関数にはstride処理[注30]が含まれるため少々ややこしいが、bs_stride_1 がゼロであると考えれば結局は bus space handle に offset を足したアドレスを読み込んでいるだけである。
[注28] とはいえWindowsNTを動かすことを目的に開発されたマシンであるのだから、マルチプラットフォームOSとしてNetBSDが後塵を拝するわけにはいかないだろう;-p もっともWindowsNTが各プラットフォームでどこまでドライバのソースを共有していたのかは定かではないが。
[注29] arcのCPUであるMIPSにはi386のようなI/Oアドレス空間は存在しない。そのためISAのI/O空間もカーネルのメモリ空間上に配置されている。
[注30] stride処理とは、機種非依存部のドライバがレジスタのオフセットが 0, 1, 2, ..というように連続していることを仮定しているのに、実際にデバイスが接続されているバスでは 0, 4, 8, ...といった不連続なアドレスに配置されている場合に、機種非依存部のドライバを変更することなく使えるように機種依存部の bus_space APIに処理を追加したものである。ただしstride処理は bus_space APIには定義されておらずあくまでもバス依存の記述である。
bus_space の実装のまとめその他の機種における実装を見ると、bus space tag は関数へのポインタを含む構造体であるか単純な整数型であることが多く、bus space handle は多くの場合アクセスに用いるアドレスそのものである。また、bus_space_read_1() などのアクセス系関数についてはここで挙げた2つのポートではいずれもすべてのバスで共通のマクロであったが、機種によってはバスごとに異なるアクセス用関数を持つようになっているものも存在する。
2つの実装の紹介だけではまだまだイメージが湧かないかもしれないが、実装を追う場合の手順についてはわかってもらえたのではないかと思う。少なくともi386とarcという全く異なるマシンで同じデバイスに対するアクセスを同じソース記述で対応できるという原理の感触はつかめたのではないだろうか。