2019/02/17(日)Raspberry Pi Zero Wを無線Wakeupできるガジェットにする(後編)

2019/02/22 14:59 PC(全般)
Raspberry Pi Zero Wを無線Wakeupできるガジェットにする(前編) - 色々日記(ざ・めも)の続き。

前回の記事で、Raspberry Pi Zero Wによる無線LAN経由のPCのwakeup(WOL)までは実現できた。

■課題
さて、これには2つ課題があった。

・USB-有線LAN変換アダプタの電力消費
 100BASEでも0.2W~1.2W。

・電源系と有線LANでRas Piからのケーブルが2本必要
 実際に繋いでみると変換ケーブルやらアダプタでかなり邪魔くさい

今は殆どのM/Bはスタンバイ時にUSBから電源供給ができるわけだし、OTGならデータも通るはずなのでUSBケーブル1本でできる構成を考えたい。

つまり、こう。
+----------+
|    PC    |
|  Windows |
+-----+----+
 USB  |
      |                  +-------------+
      +------------------+Ras Pi Zero W|
                 OTG Port|             |
                         +-------------+

■考察
考えた結果、Ras Pi Zero WがUSBの有線キーボード(マウス)と同じ振る舞いができればこれが達成できるという結論になった。

USBキーボードでPCをwakeupさせるのと同じイメージになる。

■HIDキーボード化&設定
ということで、下記参考サイト通りにRas Pi Zero WをUSB HIDキーボード化した。
Raspberry Pi Zero (W) をUSBキーボード(USBガジェット)にする - Qiita

他にもこことかが参考になる。
Raspberry Piを遠隔入力キーボードにする - とうふ荘の手記てき!

今回は文字列そのものを送信したいわけではないので、hardpass-sendHIDは不要。

で、wakeupさせたいので、bmAttributesをいじる必要がある。
$ sudo echo 0xa0 > /sys/kernel/config/usb_gadget/isticktoit/configs/c.1/bmAttributes
とする(初期値は0x80)。

[参考]
コンフィグレーションディスクリプタ - おなかすいたWiki!

USBからのwakeupはWOLのときと同様で、Windowsがサスペンドに入る際にそのポートからのwakeupを受け付ける状態にする必要がある模様。

・bmAttributesでbit 5が立っている
・Windowsのデバイスマネージャで「このデバイスでコンピュータのスタンバイ状態を解除できるようにする」にチェック

が必要条件。

また、当たり前だがこの構成だとサスペンド時にRaspberry Pi Zero Wに電源が供給されている必要がある。電源の設定で「USBセレクティブサスペンド」はOFFにしておく必要がありそう。

■ソース読んでパッチ当て
しかし、これだけではスタンバイ時にキーを送ってもホストPCはwakeupしなかった。

ここ、根本的に勘違いしていたのだが、そもそもUSBの場合はホスト側がデバイス側にキー情報を取りに来る。

当然サスペンド中には取りに来ないので、キーをバッファに貯めるだけではだめ。
wakeupさせる場合、USBの規格にあるwakeup用の信号パターンを送る必要がある。

昔BIOSとかで特定キーによるwakeup設定が存在していてそれが頭の中にあったのだが、PS/2限定だったのかも。追求していない。

[参考]
デカスギ電源ボタン - PukiWiki
↑Arduinoでやりたいことを実現できている!
USB Made Simple - Part 3
USB1.1仕様書

USBキーボードの実装では、USBポートの信号状態をチェック→ホストがサスペンドの場合はwakeup特殊信号を送る、と理解した。

もちろんGPIOとかを介せばやれることはわかるのだが、標準のUSBポートでやりたい。
だが、ガジェットHIDからwakeupを呼ぶ機能が見つからない。

しょうがないのでソースを読むところから。

Raspberry Piでカーネルをカスタムして構築 - karaage. [からあげ]
Kernel building - Raspberry Pi Documentation

を参考に、カーネルソースを取得する。

まずはgit, bc入れるところから。
$ sudo apt-get install git bc
ディレクトリ作って
$ cd ~
$ mkdir kernel
$ cd kernel
カーネル落とした。
この時点では、60feca6ea3fabfe870a731a48c254c6ca1593e0d(rpi-4.14.yブランチ)。
$ git clone --depth=1 https://github.com/raspberrypi/linux
最初に、config作ってやる。Pi Zero Wなので、bcmrpi_defconfigの方。
cd linux
KERNEL=kernel
make bcmrpi_defconfig

で、ソースはココらへんを読めば良いらしい。
$ lsmod
~~
udc_core               38945  3 dwc2,libcomposite,usb_f_hid
~~
ソースを読むと、hidg0に値を書き込んだときの動きは、HIDドライバ->gadget共通ドライバ->dwc2(DesignWaveのUSB2かな)ドライバだと思う。

結論から言うと、
・HIDドライバに実装がないし送る方法もなさそう
・gadget共通ドライバにはI/Fがある
・dwc2のガジェット部に実装がない

今回はキーを送る必要はなくwakeup専用デバイスにしたいので、WRITEする箇所で必ずwakeupを送る動きに変えてしまうことにする。

パッチを当てる。
当てる必要があるのは2ファイル。

HIDドライバのWRITE時に、gadgetドライバのAPIを呼び出す
--- ~/kernel/linux/drivers/usb/gadget/function/f_hid.c.orig        2019-02-17 13:21:42.845143807 +0000
+++ ~/kernel/linux/drivers/usb/gadget/function/f_hid.c     2019-02-17 13:26:25.554009039 +0000
@@ -339,10 +339,14 @@
                            size_t count, loff_t *offp)
 {
        struct f_hidg *hidg  = file->private_data;
+       struct usb_composite_dev *cdev = hidg->func.config->cdev;
        struct usb_request *req;
        unsigned long flags;
        ssize_t status = -ENOMEM;

+       usb_gadget_wakeup(cdev->gadget);
+       pr_info("Wakeup!\n");
+
        if (!access_ok(VERIFY_READ, buffer, count))
                return -EFAULT;

dwc2ドライバのガジェット部にwakeup用の関数を追加してポインタにセット。
実装はLinux本家のdwc2_gadget_exit_hibernation()を参考にした。
https://github.com/torvalds/linux/blob/master/drivers/usb/dwc2/gadget.c
--- /home/pi/kernel/linux/drivers/usb/dwc2/gadget.c.orig        2019-02-09 06:00:07.027365942 +0000
+++ /home/pi/kernel/linux/drivers/usb/dwc2/gadget.c     2019-02-17 13:37:07.611431738 +0000
@@ -4445,6 +4445,30 @@
        return usb_phy_set_power(hsotg->uphy, mA);
 }

+static int dwc2_hsotg_wakeup(struct usb_gadget *gadget)
+{
+       u32 dctl;
+       struct dwc2_dregs_backup *dr;
+        struct dwc2_hsotg *dev;
+        unsigned long flags;
+        dev = container_of(gadget, struct dwc2_hsotg, gadget);
+       dr = &dev->dr_backup;
+
+       spin_lock_irqsave(&dev->lock, flags);
+       udelay(10);
+
+       /* Start Remote Wakeup Signaling */
+       dwc2_writel(dr->dctl | DCTL_RMTWKUPSIG, dev->regs + DCTL);
+       mdelay(12);
+       dctl = dwc2_readl(dev->regs + DCTL);
+       dctl &= ~DCTL_RMTWKUPSIG;
+       dwc2_writel(dctl, dev->regs + DCTL);
+
+       spin_unlock_irqrestore(&dev->lock, flags);
+
+       return 0;
+}
+
 static const struct usb_gadget_ops dwc2_hsotg_gadget_ops = {
        .get_frame      = dwc2_hsotg_gadget_getframe,
        .udc_start              = dwc2_hsotg_udc_start,
@@ -4452,6 +4476,7 @@
        .pullup                 = dwc2_hsotg_pullup,
        .vbus_session           = dwc2_hsotg_vbus_session,
        .vbus_draw              = dwc2_hsotg_vbus_draw,
+       .wakeup                 = dwc2_hsotg_wakeup,
 };

 /**
多少ソースが変わっても再パッチは簡単そう。

■ソースメモ
なお、usb_gadget_wakeup()の実装はdrivers/usb/gadget/udc/core.cで、usb_gadget_opsのwakeupを呼んでいる。

で呼び出し側の実体はチップごとのドライバ(Ras Pi Zero Wの場合dwc2配下のgadget.c)にあるということのようだ。

読むには、
~/kernel/linux/include/linux/usb/gadget.h
と、
~/kernel/linux/drivers/usb/dwc2/hw.h
あたりに定義がたくさんあるので参照しながら進める必要がある。

■カーネル再構築
パッチを当てたら、カーネル作り直す。
$ make -j4 zImage modules dtbs
$ sudo make modules_install
$ sudo cp arch/arm/boot/dts/*.dtb /boot/
$ sudo cp arch/arm/boot/dts/overlays/*.dtb* /boot/overlays/
$ sudo cp arch/arm/boot/dts/overlays/README /boot/overlays/
$ sudo cp arch/arm/boot/zImage /boot/$KERNEL.img
最後にリブート
$ sudo reboot

■カーネル再構築(部分コンパイルする場合)
今回、カーネルまるごと作り直したけど、Pi Zeroだと一晩かかるので部分モジュールコンパイルのほうが良かったかも。

部分コンパイルする場合は、インストールされているカーネルのバージョン調べてダウンロードした後、パッチ当てて、
$ cd ~/kernel/linux
$ make drivers/usb/gadget/function/usb_f_hid.ko
$ make drivers/usb/dwc2/dwc2.ko
でパッチ対象モジュールを作れる。
あとは手動コピーで良いはず(試してない)。

■使い方
これで、次回起動後、
$ echo -ne "\0\0\0\0\0\0\0\0" > /dev/hidg0
でwakeupがホストに行くようになる(hidg0の権限次第でsudoが必要)。

前編で作ったauthorized_keysの中のコマンドをこれに差し替えることで、同じ手順でUSB wakeupができるようになった。

キーを送る機能と共存できるかは試していないので、そこは各自研究してほしい。

なお、wakeup時に/var/log/messagesに"Wakeup!"が出力される。

気に食わない人は、f_hid.cパッチのpr_infoをコメントアウトなり好きなメッセージに変えること。
うまく動かないときは、dwc2側にもpr_infoを追加して動きを確認していくといいかもしれない。

なお、くれぐれもセキュリティにはご用心&自己責任で。

■おまけ
wakeup周りの実装がないのは多分Linuxから呼ぶIFが決まってないからだと思う。

今回、規格書見た段階ではD+ D-をちょろっと動かせばいいだけだと思ってなめてかかったのだが、dwc2ドライバの中身的にはもう一段階抽象化されていてチップのレジスタに何セットして送るの世界だった。

これだとデータシートかオシロがないと厳しい。
実装するにしても、既存ソースの識別子名から勘でトライ&エラーになってしまう。

データシートについては、個人では入手できないぽいとのこと。
Linux本家にwakeup絡みの実装が見つからなかったら取りあえず動くところまでたどり着けてなかったと思う。

何はともあれ動いてよかった。

動いちゃいるけど正しい信号の動きじゃないのでは疑惑が強いので、知見がある人がいたらツッコんでほしい。
OK キャンセル 確認 その他