tsutsuiの作業記録置き場

NetBSDとかPC-6001とかの作業記録のうち、Twitterの140字では収まらない内容や記事としてまとめるべき内容をとりあえず置いてみる予定

RaSCSI NetBSD用カーネルドライバ

とりあえず書いてみて動いたので Togetter 的まとめ

きっかけ

以前の「Raspberry Pi 3 + NetBSD/evbarm で RaSCSI」のエントリの最後で書いたとおり、NetBSD/evbarm で動かしている RaSCSI を NetBSD/sparc に接続するとエラーになるというツイートを投げていたのですが、

さっそく GIMONSさんから以下のコメントをいただきました。

GIMONSさんの書かれているとおり、やりたいことは「割り込み禁止の状態で、適切なタイミングで GPIO のレジスタを直接叩く」ということだけです。Raspberry Pi というハードは OS関係なく共通なので、ポインタ経由でレジスタを直叩きするならアクセス操作自体には Linux依存な内容はないはずです。よって、 NetBSDでもカーネルモジュールおよびデバイスドライバ的に固有な部分だけ書けばなんとかなるか、ということでやってみました。

結論からいうと、わりとあっさり動きました。

NetBSDにおけるカーネルモジュール実装

とりあえずぐぐってみると、以下でツイートしているリンク先の解説記事がわかりやすくて参考になりました。

上記ツイートのものを含め、 NetBSDカーネルモジュールについては以下のページが参考になると思います。

バイスレジスタアドレスマッピング

とりあえず下調べ

作業開始

ドライバ実装

超大雑把なまとめ

  • 基本は Writing a NetBSD kernel module のサンプルと同様の構成でいける
  • RaSCSI の場合、アドレスは決め打ちで、親デバイス (バス等) からアドレスその他の情報を受け取る必要はない
    ⇒通常のデバイスドライバのような foo_match()foo_attach() のような driver(9) の枠組みの関数は無くてもいい
  • なので、上記ブログ記事の通り foo_modcmd() での devsw_attach()devsw_detach() と、 read, write, ioctl のアクセス関数だけ書けばなんとかなる(はず)
  • read, write の関数も上記ブログの通りで uiomove(9) で転送
  • ioctl については、コマンドは直接渡されて、パラメータはポインタ経由で飛んでくるので、それらが取得できればやる処理は Linux用の実装と同じ

ioctl

いつも末端のデバイスドライバばかり書いていて、最下層のドライバの ioctl() の実装は何度か書いたことがあるものの、カーネルの上位層でユーザーランドからどう渡されてくるのかというのは実はちゃんと調べたことがありませんでした。

「とりあえず動かす」という最低限の実装をする場合に ioctl の番号をどうするかというところでちょっと悩みました。

と書いてみると、つぶやき駆動開発状態で沖さんに助言をいただきました。

RaSCSI の Linux用の ioctl 番号は単純(?)に 0x100, 0x101, 0x102 になっていました。

NetBSDの場合、たとえば src/sys/arch/luna68k/include/xpio.h の定義では以下のようになっています。

#define XPIOCDOWNLD	_IOW('x', 1, struct xp_download)

この _IOW() マクロは src/sys/sys/ioccom.h にあります。

/*
* Ioctl's have the command encoded in the lower word, and the size of
* any in or out parameters in the upper word. The high 3 bits of the
* upper word are used to encode the in/out status of the parameter.
*
* 31 29 28 16 15 8 7 0
* +---------------------------------------------------------------+
* | I/O | Parameter Length | Command Group | Command |
* +---------------------------------------------------------------+
*/
#define IOCPARM_MASK 0x1fff /* parameter length, at most 13 bits */
#define IOCPARM_SHIFT 16
#define IOCGROUP_SHIFT 8
#define IOCPARM_LEN(x) (((x) >> IOCPARM_SHIFT) & IOCPARM_MASK)
#define IOCBASECMD(x) ((x) & ~(IOCPARM_MASK << IOCPARM_SHIFT))
#define IOCGROUP(x) (((x) >> IOCGROUP_SHIFT) & 0xff)

/* [中略] */

#define _IOC(inout, group, num, len) \
((inout) | (((len) & IOCPARM_MASK) << IOCPARM_SHIFT) | \
((group) << IOCGROUP_SHIFT) | (num))
#define _IO(g,n) _IOC(IOC_VOID, (g), (n), 0)
#define _IOR(g,n,t) _IOC(IOC_OUT, (g), (n), sizeof(t))
#define _IOW(g,n,t) _IOC(IOC_IN, (g), (n), sizeof(t))
/* this should be _IORW, but stdio got there first */
#define _IOWR(g,n,t) _IOC(IOC_INOUT, (g), (n), sizeof(t))

歴史的経緯というか、BSD の ioctl では上記 ioccom.h のコメント内の図にあるとおり 32ビットの中に I/O方向、パラメータ長、ioctl種別、ioctl番号をすべて押し込む実装になっています。 ioctl(2)システムコールのエントリ処理においては、これらの情報を使って前処理をした上で各ドライバの ioctl を呼ぶ、という構成になっているようです。詳細は src/sys/kern/sys_generic.csys_ioctl() の実装を見ればなんとなくわかります。

とりあえず実装

ioctl 番号についても一応 _IOW() のマクロをちゃんと使うようにして、ああでもないこうでもないと関数構成に悩みつつ、 3時間くらいでとりあえずリンクできるところまで実装。

とりあえずテスト

とりあえずデバッグ

動かないところを確認したところで read, write, ioctl 等々の各関数にデバッグ用の printf() を適当に埋め込み。最初から入れとけと怒られる

panic している read 関数のデバッグ printf は表示されるのに、 rascsi コマンド起動時に呼ばれているはずの ioctl についてはドライバ中の printf が表示されないという状態に。GPIO レジスタの仮想アドレス設定は ioctl で行うので、それが動かないと何も動かないということになってしまいます。

ioctl 調査

rascsi コマンドの gpiobus.c では ioctl(2) の返り値をチェックしていなかったのでそれを見てみると -1 でエラーの応答。

warnx(3) を使って strerror(errno) でエラーを表示させてみると、 Bad addressEFAULT のエラー。

ioctl(2) の記載からすると EFAULT が返るのは "argp points outside the process's allocated address space." という状態。

というわけで、 gpiobus.cpp の中の ioctl(2) 呼び出しについてパラメータ引数をポインタ渡しにすると、ようやくドライバ内の ioctl まで呼ばれるように。

ioctl のコマンド番号について、実装上は
_IOW('R', 0x102, sizeof long)
としていたのですが、上記ツイートの画面写真ではなぜか
_IOW('S', 2, 4)
になってしまっています。これは以下が原因だったので ioctl 番号について単純に 0, 1, 2 に修正。

レジスタアドレスのI/O空間マップ

オリジナルの Linux用ドライバで ioremap_nocache() が使われていた部分について、上記の通り pmap_direct_mapped_phys() は存在するっぽかったので当初それを使って書いていました。しかし、前項の ioctl デバッグツイートの画面写真にあるとおりで pmap_direct_mapped_phys() の呼び出しはエラーになってしまっていました。要は pmap_direct_mapped_phys() の実装はあるものの GPIOレジスタのあるI/O領域に対しては使えないということのようです。

最初のツイートで書いていたとおり uvm_km_alloc(9)pmap_kenter_pa(9) で書くのもなんかだるいなー、と思っていたのですが、ふと bus_space_map(9) の実装を見てみれば良いんじゃないかと思いついたので調べてみると、 src/sys/arch/arm/arm32/armv7_generic_space.c に以下の実装がありました。

int
armv7_generic_bs_map(void *t, bus_addr_t bpa, bus_size_t size, int flag,
bus_space_handle_t *bshp)
{
u_long startpa, endpa, pa;
const struct pmap_devmap *pd;
int pmapflags;
vaddr_t va;

if ((pd = pmap_devmap_find_pa(bpa, size)) != NULL) {
/* Device was statically mapped. */
*bshp = pd->pd_va + (bpa - pd->pd_pa);
return 0;
}

要は、すでに static にマップされている I/O空間の仮想アドレスをそのまま返す実装です。安直に上記の pmap_devmap_find_pa() のコードをほぼそのまま使ってデバイスマップのコードを書くと、エラーが返ってくることもなくそれなりの仮想アドレスが返ってきたので、それをそのまま使用することにしました。

なお、 Raspberry Piカーネルでは src/sys/arch/arm/arm32/armv7_generic_space.c ではなく src/sys/arch/arm/broadcom/bcm283x_platform.cbcm283x_bs_map() が使われているようですが、実装は armv7_generic_space() のものと同様です。

uiomove(9) のユーザー空間⇔カーネル空間データコピー

read および write の各ルーチンにおいて、オリジナルの RaSCSI の Linuxカーネルドライバ実装では copy_to_user() および copy_from_user() の各関数でデータを SCSI転送の 1バイト毎にコピーしていました。

最初は上記の copy_to_user() および copy_from_user() の実装をそのまま uiomove(9) に置き換えてみたのですが、前項の ioctl のデバッグと並行して実装していたせいかボトボト落ちてしまっていました。

uiomove(9) の転送を1バイトずつに分割してうまく動くのかどうかという検証をするのが面倒だったのと、処理内容的にもいったん別のバッファにまとめてコピーしてしまっても実装上の不都合はないように見えたので、以下のように書き換えて実装しました。

  • read については、GPIO からの 1バイトごとの SCSI受信データを仮バッファに入れて、最後にまとめて uiomove(9)uio 構造体にコピー
  • write については、先に uiomove(9)uio 構造体から仮バッファに全体をコピーして、仮バッファから 1バイトずつ GPIO に書き込み

そして動作

実際にはドライバの構成をどうするかとかいろいろ考えたのですが、途中の試行錯誤の過程をだらだら書いてもあまり得るものがなく、最終的な構成を見れば十分という感じなのでそのあたりは省略。
そもそもが ツイートするほどでもない内容 ということなので思い出すのも困難

というわけで、 NetBSD的なデバイスドライバ実装の作法およびデバイスレジスタ空間のマップという 2点でややハマりましたが、動いてみればあっさり書けた、という印象です。実際、 NetBSD固有部分の実装は copyright のコメントを含め 199行しかありませんし、コード的には Writing a NetBSD kernel module のサンプルとほとんど変わりません。かかった時間としては、事前検討が 1〜2時間、試行錯誤含む実装が 3時間、デバッグが 2時間といったところ。

まとめ

カーネルモジュールを書いたのは今回が実は初めてだったのですが、 panic しない限りは毎回カーネルを再起動する必要もなくデバッグがやりやすい、単一ソースなのでビルドの時間もかからない、という具合で、わかってしまえばかなりお手軽という感想です。RaSCSI同様に「ユーザーランドから手軽にデバイスを直叩きしたい」という時にはわりと使えるかもしれません。

なお、カーネルモジュールドライバのソースコードについては、そのうち RaSCSI 本体の配布に含まれるかもしれません。詳細は rascsi.txt のライセンス項を参照
それより先に試してみたい、という場合は、個別に連絡をもらえれば検討します。