とりあえず書いてみて動いたので Togetter 的まとめ
きっかけ
以前の「Raspberry Pi 3 + NetBSD/evbarm で RaSCSI」のエントリの最後で書いたとおり、NetBSD/evbarm で動かしている RaSCSI を NetBSD/sparc に接続するとエラーになるというツイートを投げていたのですが、
RaSCSI + SPARCstation でエラーになるのは RaSCSI 側が受け取るコマンドフェーズのデータが化けているっぽい。
— Izumi Tsutsui (@tsutsuii) 2018年5月1日
- コマンド $08 が $48 や $C8 になる
- LBA $00 $00 $xx が $08 $00 $xx になる 等
取り込みが速すぎるというか、フェーズ移行が速すぎて追いついてないとかかな https://t.co/WWVceQDJoz
さっそく GIMONSさんから以下のコメントをいただきました。
カーネルドライバを併用すると大丈夫だとおもうのですが…NetBSDだと厳しいかもしれませんね。
— GIMONS (@kugimoto0715) 2018年5月2日
カーネルドライバと言っても・・・
— GIMONS (@kugimoto0715) 2018年5月2日
// 割り込み禁止
local_irq_disable();
local_fiq_disable();
ごにょごにょ
// 割り込み禁止解除
local_fiq_enable();
local_irq_enable();
が使いたいだけなんですよね。
ユーザーアプリから使えないんですよ。
GIMONSさんの書かれているとおり、やりたいことは「割り込み禁止の状態で、適切なタイミングで GPIO のレジスタを直接叩く」ということだけです。Raspberry Pi というハードは OS関係なく共通なので、ポインタ経由でレジスタを直叩きするならアクセス操作自体には Linux依存な内容はないはずです。よって、 NetBSDでもカーネルモジュールおよびデバイスドライバ的に固有な部分だけ書けばなんとかなるか、ということでやってみました。
結論からいうと、わりとあっさり動きました。
NetBSDにおけるカーネルモジュール実装
とりあえずぐぐってみると、以下でツイートしているリンク先の解説記事がわかりやすくて参考になりました。
RaSCSI の NetBSD用カーネルドライバを書くために(メモ)
— Izumi Tsutsui (@tsutsuii) 2018年5月2日
- とりあえずこれを読む https://t.co/RvUZde14XJ
- デバイスノードは手動で作る必要がある
- 割り込み禁止は適当に splhigh() とかでいける?
- gpio レジスタマップは安直に mm(4) 同様 uvm_km_alloc(9) + pmap_kenter_pa(9) でいける?
上記ツイートのものを含め、 NetBSDのカーネルモジュールについては以下のページが参考になると思います。
デバイスレジスタアドレスマッピング
とりあえず下調べ
NetBSDだとデバドラの枠組みが厳格なので ioremap_nocache() みたいな何でもありな MI関数は無いような気がする (m68k の MDには physaccess() とかの似たようなのがあったけど)
— Izumi Tsutsui (@tsutsuii) 2018年5月2日
ラズパイだと pmap_direct_mapped_phys() が使えたりするんかな(調べずにつぶやくメモ手法)
— Izumi Tsutsui (@tsutsuii) 2018年5月2日
作業開始
NetBSD + Raspberry Pi のブログを書いただけで満足してしまっているけれど、気合いを入れなおして RaSCSI の NetBSD用カーネルモジュールを書く作業を始めてみる(始めるだけ)
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
思い出し中(メモの重要性) https://t.co/WJDmxRvDSg
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
ドライバ実装
超大雑把なまとめ
- 基本は 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 の NetBSD用カーネルドライバ進捗。
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
ioctl の引数というか ioctl 番号をどう定義すべきなのかというところで、ドライバの階層のことしかわかっていないということを痛感する(´・ω・`)
<sys/ioccom.h> の定義からすると方向や引数サイズもioctl番号の定義に含めないといけないように見えるが、それがデバイスノードの入り口側でどう処理されているのか……
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
と書いてみると、つぶやき駆動開発状態で沖さんに助言をいただきました。
方向と引数サイズは、ioctl共通部でカーネル空間とユーザー空間の間でのパラメータコピー(copyin/copyout)に使っています。
— 勝(まさる) (@masaru0714) 2018年5月5日
実際の末端のドライバの実装では IOCPARM_LEN() 等は使わずに「どの ioctlが何をするか」について暗黙知で copyin/copyout のサイズと方向を決めているようなので、それより上の層で見てるところあるのか(≒手抜きで長さ情報の入ってない番号を使うとハマるのか)が気になったのでした
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
長さ情報が入ってない場合、ユーザ空間でポインタ渡ししたstructの内容がカーネル空間にコピーされないので、読めないって感じです。書き戻すほうも逆方向で同様な感じです。パラメータが不要であれば長さ0でもちろん大丈夫です。
— 勝(まさる) (@masaru0714) 2018年5月5日
sys/kern/sys_generic.c の sys_ioctl() の処理ですね。ありがとうございます
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
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.c
の sys_ioctl()
の実装を見ればなんとなくわかります。
とりあえず実装
ioctl 番号についても一応 _IOW()
のマクロをちゃんと使うようにして、ああでもないこうでもないと関数構成に悩みつつ、 3時間くらいでとりあえずリンクできるところまで実装。
とりあえず超テキトーにカーネルドライバのガワだけは書けたけれど、どうやってデバッグするのかという話があるな……
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
とりあえずカーネルドライバロードして open しても即死することはないようだが…… pic.twitter.com/kLxiwRVY5o
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
とりあえずテスト
あいぼむ版でとりあえずテストしてみる pic.twitter.com/R8wao4MdMr
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
先は長いな (ヽ´ω`) pic.twitter.com/vu2SDWY4nw
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
とりあえずデバッグ
動かないところを確認したところで read, write, ioctl 等々の各関数にデバッグ用の printf()
を適当に埋め込み。最初から入れとけと怒られる
うーん。なんか read は呼ばれてるのに ioctl が呼ばれてないような。よくわからん……
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
panic している read 関数のデバッグ printf は表示されるのに、 rascsi コマンド起動時に呼ばれているはずの ioctl についてはドライバ中の printf が表示されないという状態に。GPIO レジスタの仮想アドレス設定は ioctl で行うので、それが動かないと何も動かないということになってしまいます。
ioctl
調査
rascsi コマンドの gpiobus.c
では ioctl(2)
の返り値をチェックしていなかったのでそれを見てみると -1 でエラーの応答。
warnx(3)
を使って strerror(errno)
でエラーを表示させてみると、 Bad address
の EFAULT
のエラー。
ioctl(2)
の記載からすると EFAULT
が返るのは "argp points outside the process's allocated address space." という状態。
ioctl が EFAULT を返しているということは何かをミスっているということだけれども……と思ったら Linux だと ioctl は値渡しだけど NetBSD だとアドレス渡しにする必要があるのか?
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
というわけで、 gpiobus.cpp
の中の ioctl(2)
呼び出しについてパラメータ引数をポインタ渡しにすると、ようやくドライバ内の ioctl
まで呼ばれるように。
とりあえずドライバまで ioctl は通った。まだいろいろおかしいけど pic.twitter.com/PHNuN4H14S
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
ioctl
のコマンド番号について、実装上は_IOW('R', 0x102, sizeof long)
としていたのですが、上記ツイートの画面写真ではなぜか_IOW('S', 2, 4)
になってしまっています。これは以下が原因だったので ioctl 番号について単純に 0, 1, 2 に修正。
ioctl 番号は 8ビットなので 0x101 とかだとはみ出すという問題
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
レジスタアドレスのI/O空間マップ
RPI2 のカーネルからすると pmap_direct_mapped_phys() はあるっぽい。
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
% nm netbsd | grep pmap_direct_mapped_phys
80026cd0 T pmap_direct_mapped_phys
オリジナルの 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.c
の bcm283x_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 に書き込み
そして動作
実際にはドライバの構成をどうするかとかいろいろ考えたのですが、途中の試行錯誤の過程をだらだら書いてもあまり得るものがなく、最終的な構成を見れば十分という感じなのでそのあたりは省略。
そもそもが ツイートするほどでもない内容 ということなので思い出すのも困難
キタッ!!!!
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
RaSCSI on NetBSD/evbarm with カーネルドライバでドライブ probe まで動きました。いえーい pic.twitter.com/8LNxoeDcSS
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
READコマンドで 512バイト読もうとしたら落ちた。ありがち。 orz pic.twitter.com/2oxDhZXlIw
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
バッファが 256バイトしか無かった(わはは
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
サクッとカーネルドライバの転送バッファ増やしたら RaSCSI on NetBSD/evbarm 8.0_RC1 with kernel driver + NetBSD/sparc on SPARCstation20 esp(4) SCSI でもエラー無く読めるようになった。転送速度も 1.3MB/s超え。完璧ですね! (ちなみに大量のデバッグメッセージ出力込みの速度) pic.twitter.com/hnZy4EP7t2
— Izumi Tsutsui (@tsutsuii) 2018年5月5日
というわけで、 NetBSD的なデバイスドライバ実装の作法およびデバイスレジスタ空間のマップという 2点でややハマりましたが、動いてみればあっさり書けた、という印象です。実際、 NetBSD固有部分の実装は copyright のコメントを含め 199行しかありませんし、コード的には Writing a NetBSD kernel module のサンプルとほとんど変わりません。かかった時間としては、事前検討が 1〜2時間、試行錯誤含む実装が 3時間、デバッグが 2時間といったところ。
まとめ
カーネルモジュールを書いたのは今回が実は初めてだったのですが、 panic しない限りは毎回カーネルを再起動する必要もなくデバッグがやりやすい、単一ソースなのでビルドの時間もかからない、という具合で、わかってしまえばかなりお手軽という感想です。RaSCSI同様に「ユーザーランドから手軽にデバイスを直叩きしたい」という時にはわりと使えるかもしれません。
なお、カーネルモジュールドライバのソースコードについては、そのうち RaSCSI 本体の配布に含まれるかもしれません。詳細は rascsi.txt のライセンス項を参照
それより先に試してみたい、という場合は、個別に連絡をもらえれば検討します。