このテキストは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 tag
i386用の 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 handle
arch/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 tag
arch/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 handle
arch/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という全く異なるマシンで同じデバイスに対するアクセスを同じソース記述で対応できるという原理の感触はつかめたのではないだろうか。