U-bootによるネットワークブート

NFSルートを使うと、開発機とターゲットのRaspberry PIの間でデバッグの度にファイルをコピーする必要がなくなり、アプリケーション開発の効率化に繋がる。しかし、カーネルを変更した場合には、ビルドしたカーネルをSDカードに書き込まなくてはならないので、相変わらず面倒なコピー作業が必要となる。SDカードを何度も抜き差ししていると、故障の原因にもなってしまう(1台それで逝ってしまった…)。

Raspberry PIのLinuxを完全にネットワークブートできないかと調べてみたところ、U-BootのRaspberry PI対応版がリリースされていることを見つけた(元ページ)。何種類かの実装があるようだが、Mainlineのものを使ってみよう。

U-Bootのビルド

LaspbianのリポジトリにはDas U-Bootのパッケージがあるが、Raspberry PI用のバイナリは含まれていないので自分でビルドしなくてはならない。当然、クロス開発環境を構築するで述べたクロス開発環境を整備したマシン上で作業する。

まず、U-Bootのソースコードを持ってきて、クロスコンパイルして実機用のバイナリを作成する。

$ git clone git://git.denx.de/uboot.git U-boot
$ cd U-boot
$ make CROSS_COMPILE=arm-linux-gnueabihf- rpi_b_defconfig
  HOSTCC  scripts/basic/fixdep
  HOSTCC  scripts/kconfig/conf.o
  SHIPPED scripts/kconfig/zconf.tab.c
  SHIPPED scripts/kconfig/zconf.lex.c
  SHIPPED scripts/kconfig/zconf.hash.c
  HOSTCC  scripts/kconfig/zconf.tab.o
  HOSTLD  scripts/kconfig/conf
#
# configuration written to .config
#
$ make CROSS_COMPILE=arm-linux-gnueabihf- 
scripts/kconfig/conf --silentoldconfig Kconfig
  CHK     include/config.h
  GEN     include/autoconf.mk
  GEN     include/autoconf.mk.dep
  CHK     include/config/uboot.release
  UPD     include/config/uboot.release
    (途中省略)
  LD      examples/standalone/hello_world
  OBJCOPY examples/standalone/hello_world.srec
  OBJCOPY examples/standalone/hello_world.bin
  LDS     u-boot.lds
  LD      u-boot
  OBJCOPY u-boot.srec
  OBJCOPY u-boot.bin

U-bootディレクトリに作成された`u-boot.bin'が、ブートローダの実行ファイルとなる。

SDカードに置くファイル

SDカードのvfatでフォーマットされた第1パーティションに、予めLaspbianの/bootディレクトリにあるファイルを全てコピーしておく。もちろん、LaspbianのSDカードをそのまま使用しても構わない。以下のファイルをコピー、ないしは作成、編集する。

u-boot.bin

ダウンロードしたソースからビルドした、u-boot.binをそのままコピーする。

config.txt

オリジナルのブートローダ設定ファイルであるが、最後に以下の行を追加して、(Linuxカーネルではなくて)u-boot.binを最後に実行することを指定する。

kernel=u-boot.bin

uEnv.txt

u-boot用の変数を定義するためのテキストファイルである。u-bootが起動した時に、自動的に読み込まれる。ここでは、以下の変数を定義することにする。

server
NFSサーバのIPアドレスを指定する。
rootpath
NFSサーバにおけるルートファイルシステムのパス名を指定する。
kernelfile
ロードして実行するNFSサーバ上のカーネル・イメージ・ファイル名を、rootpathで指定したディレクトリからのパスで指定する。
fbwidth
コンソール(HDML接続のディスプレィ)の横解像度を指定する。詳細は後述。
fbheight
コンソール(HDML接続のディスプレィ)の縦解像度を指定する。詳細は後述。

まとめると次のようになる。

serverip=192.168.3.51
rootpath=/export/raspbian/20140909
kernelfile=/boot/kernel.img
fbwidth=1824
fbheight=984

コンソールの解像度は、SoCのFirmwareによって検出と決定が行われる。Laspbianの通常のブートローダでは、それらの値をカーネルパラメータとしてLinuxに引き渡しており、その値を使って画面解像度が初期化される。現在のU-Bootでは、Firmwareが決定した値を使用することができないので、環境に応じてユーザが指定することにした。

fbwidthとfbheightに指定する値を決定するためには、使用するHDMI接続のディスプレイを接続した状態でRaspbianを起動して、カーネル起動時に指定されたパラメータを参照する。

$ cat /proc/cmdline 
dma.dmachans=0x7f35 bcm2708_fb.fbwidth=1824 bcm2708_fb.fbheight=984 bcm2708.boardrev=0x10 bcm2708.serial=0x9aefa7a7 smsc95xx.macaddr=B8:27:EB:EF:A7:A7 bcm2708.disk_led_gpio=47 bcm2708.disk_led_active_low=0 sdhci-bcm2708.emmc_clock_freq=250000000 vc_mem.mem_base=0x1ec00000 vc_mem.mem_size=0x20000000  dwc_otg.lpm_enable=0 console=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait

ここで、bcm2708_fb.fbwidthに指定されている値をfbwidthに、bcm2708_fbheightに指定されている値をfbwidthにそれぞれ指定する。

boot.scr

U-Bootが起動した時に自動実行するコマンド・スクリプトのソースファイルである。まず、全体を示してから、個々に説明する。

  1	set autoload "no"
  2	set bootfile "${rootpath}${kernelfile}"
  3	set bootarg_def "dma.dmachans=0x7f35 bcm2708_fb.fbwidth=${fbwidth} bcm2708_fb.fbheight=${fbheight} bcm2708.boardrev=0x10 smsc95xx.macaddr=${usbethaddr} bcm2708.disk_led_gpio=47 bcm2708.disk_led_active_low=0 sdhci-bcm2708.emmc_clock_freq=250000000 vc_mem.mem_base=0x1ec00000 vc_mem.mem_size=0x20000000"
  4	set bootarg_opt "dwc_otg.lpm_enable=0 ip=dhcp console=ttyAMA0,115200 console=tty1 kgdboc=ttyAMA0,115200 root=/dev/nfs nfsroot=${serverip}:${rootpath} elevator=deadline rootwait"
  5	set bootargs "${bootarg_def} ${bootarg_opt}"
  6	
  7	set net_start "usb start; dhcp"
  8	set load_kernel "nfs"
  9	
 10	run net_start
 11	run load_kernel
 12	usb stop
 13	bootz
1行目
U-Bootの組み込み変数autloadをセットして、ブートデバイスを初期化(指定)した時に、ブートファイル(やカーネル)を自動的に読み込まないことを指定する。これを指定しておかないと、dhcpコマンドを実行した時に、tftpでファイルを探しに行ってしまう。
2行目
組み込み変数bootfileに、ロードするファイルの名前をセットする。uEnv.txtで指定した変数を使用して、ファイルサーバのIPアドレスとファイル名(フルパス)を組み合わせている。
3行目
Linuxカーネルに渡すパラメータの前半部分、すなわち、Raspbianのオリジナル・ブートシステムでは、自動的に渡されるパラメータ群を定義する。SDカードからブートしたRaspbianの/proc/cmdlineを参照して、必要な箇所を変数などで置き換えればよい。なお、usbethaddr変数だけは、U-BootがFirmwareが決定した値にセットしているので、そのまま引きわたす。どうせなら、SoCのシリアル番号(bcm2708.serialパラメータで指定する)も引き渡してくれれば、SDカードからブートしたものと同じ状態にできるのだが。bcm2708.serialパラメータを渡さなくても、/proc/cpuinfoで参照できるシリアル番号が0になるだけで実害はなさそうだ。
4行目
Linuxカーネルに渡すパラメータの後半部分、すなわち、Raspbianのオリジナル・ブートシステムでは、cmdline.txtファイルに書かれているパラメータ群を定義する。ここで、NFSルートを使用するためのパラメータを指定している。
5行目
自明。スクリプトはU-Bootに組み込まれたhushで実行されるので、簡単な変数置換や条件分岐を書くことができる。
7〜8行目
一種のマクロを定義する。
10行目
定義したマクロを実行する。usbコマンドでUSBデバイスを有効化すると、Raspberry PIの内臓Ethernetポートが使用可能となる。その後、そのEhtenetポートから、DHCPでネットワーク設定を行う。
11行目
定義したマクロを実行する。引数なしでnfsコマンドを実行すると、変数bootfileで指定したファイルを、NFSでメモリ上にロードする。
12行目
無くてもよいが、USBデバイスのサービスを停止する。Linxuカーネルが初期化するので、止めておいたほうがいいだろう程度のもの。
13行目
メモリにロードしたLinuxカーネル・イメージ(圧縮)を実行する。カーネルパラメータとして、変数bootargsが引き渡される。

なお、いろいろなサンプル・スクリプトではsetenvコマンドが使われていることが多いが、Raspberry PI版ではsetenvコマンドが使用できず、setコマンドしか認識されない。これに気づかなくて、かなりハマってしまった。

boot.scr.uimg

作成したboot.scrファイルを、U-Bootが理解出来るフォーマットに変換する。mkimageコマンドは、UbuntuでもRaspbianでも、u-boot-toolsパッケージとして提供されているので、あらかじめインストールしておく。

# mkimage -A arm -O linux -T script -C none -n "U-Boot script" -d boot.src boot.scr.img

生成されたboot.src.imgファイルを、SDカードにコピーする

NFS上のファイルシステムの調整

NFSサーバには、ブート・パーティション(vfat)に置かれている/boot以下のディレクトリも、エクスポートするディレクトリにコピーしておくとよいだろう。SDカードと同じディレクトリ構成としておけば、カーネルがアップデートされた場合などにも、カーネル・イメージをコピーするといった作業が必要なくなる。

次のように/etc/fstabの調整を調整しておく。

  • ルートファイルシステムのエントリは必要無いので削除する
  • SDカードの第1パーティション(vfat)をどこかにマウントしておくと、boot.scrの書き換えなどが便利。例えば/sdcardなどにマウントする

実行例

ネットワークブートした際に、コンソール(シリアルポート)に表示される起動メッセージを示しておく。

U-Boot 2014.10-00410-g3664816 (Oct 28 2014 - 18:49:34)

DRAM:  448 MiB
WARNING: Caches not enabled
MMC:   bcm2835_sdhci: 0
Using default environment

In:    serial
Out:   lcd
Err:   lcd
Net:   Net Initialization Skipped
No ethernet found.
reading /uEnv.txt
113 bytes read in 11 ms (9.8 KiB/s)
Hit any key to stop autoboot:  0 ←カウントダウン中(2秒)にキー入力すると、U-Bootのプロンプトに落ちる
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0...
Found U-Boot script /boot.scr.uimg
reading /boot.scr.uimg
757 bytes read in 13 ms (56.6 KiB/s)
## Executing script at 00000000
(Re)start USB...
USB0:   Core Release: 2.80a
scanning bus 0 for devices... 3 USB Device(s) found
       scanning usb for storage devices... 0 Storage Device(s) found
       scanning usb for ethernet devices... 1 Ethernet Device(s) found
Waiting for Ethernet connection... done.
BOOTP broadcast 1
BOOTP broadcast 2
DHCP client bound to address 192.168.3.56 (3625 ms)
Waiting for Ethernet connection... done.
Using sms0 device
File transfer via NFS from server 192.168.3.51; our IP address is 192.168.3.56
Filename '/export/raspbian/20140909/boot/kernel.img'.
Load address: 0x200000
Loading: T #################################################################
	 #################################################################
	 #################################################################
	 #################################################################
	 #################################################################
	 #################################################################
	 #################################################################
	 #################################################################
	 #################################################################
	 ###############################################
done
Bytes transferred = 3232856 (315458 hex)
stopping USB..
Kernel image @ 0x200000 [ 0x000000 - 0x315458 ]

Starting kernel ... ←ここから後は、Linuxカーネルからのメッセージ

Uncompressing Linux... done, booting the kernel.
[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Initializing cgroup subsys cpu
[    0.000000] Initializing cgroup subsys cpuacct
[    0.000000] Linux version 3.12.28+ (dc4@dc4-XPS13-9333) (gcc version 4.8.3 20140303 (prerelease) (crosstool-NG linaro-1.13.1+bzr2650 - Linaro GCC 2014.03) ) #709 PREEMPT Mon Sep 8 15:28:00 BST 2014
[    0.000000] CPU: ARMv6-compatible processor [410fb767] revision 7 (ARMv7), cr=00c5387d
[    0.000000] CPU: PIPT / VIPT nonaliasing data cache, VIPT nonaliasing instruction cache
[    0.000000] Machine: BCM2708
[    0.000000] cma: CMA: reserved 8 MiB at 1b800000
[    0.000000] Memory policy: ECC disabled, Data cache writeback
[    0.000000] Built 1 zonelists in Zone order, mobility grouping on.  Total pages: 113792
[    0.000000] Kernel command line: dma.dmachans=0x7f35 bcm2708_fb.fbwidth=1824 bcm2708_fb.fbheight=984 bcm2708.boardrev=0x10 smsc95xx.macaddr=b8:27:eb:0d:66:18 bcm2708.disk_led_gpio=47 bcm2708.disk_led_active_low=0 sdhci-bcm2708.emmc_clock_freq=250000000 vc_mem.mem_base=0x1ec00000 vc_mem.mem_size=0x20000000 dwc_otg.lpm_enable=0 ip=dhcp console=ttyAMA0,115200 console=tty1 kgdboc=ttyAMA0,115200 root=/dev/nfs nfsroot=192.168.29.51:/export/raspbian/20140909 elevator=deadline rootwait
[    0.000000] PID hash table entries: 2048 (order: 1, 8192 bytes)
(途中省略)
Raspbian GNU/Linux 7 netpi ttyAMA0

netpi login: 

うまくブートしないときは、U-Bootのプロンプトに入って、定義されている変数を確認する。また、スクリプトで実行するはずのコマンドを1つずつ入力して実行してみて、スクリプトの誤りを見つけよう。

以上である。Ethernetインターフェイスが100Mbpsなので、それほど早いとは感じないかもしれないが、大量の書き込みを行った場合には差が歴然になるだろう。何よりも、SDの寿命や容量を気にせずに使えるし、カーネルの書き換えもホスト機上で簡単に行えるのが一番のメリットである。

カーネル・イメージのフォーマットについて

ブートするカーネル・イメージ・ファイルのフォーマットが、2014年10月頃のアップデートで変更になっている。

  • 20140909の公式ディストリビューションまで: 圧縮されたカーネルイメージ(zImage)に、imagetool-uncompressed.pyというツールを使って32KBのヘッダを付加したもの。
  • それ以降: Raspbianのカーネル・イメージ (/boot/kernel.img)は、raspberrypi-bootfilesというパッケージに、ブートローダ/ファームウェアと共に含まれている。そのパッケージの2014年10月頃のアップデートで、kernel.imgは単純な圧縮されたカーネル・イメージ(zImage)になった。

ここまでの説明は、新しいzImageフォーマットのカーネルについて説明している。古いフォーマットのカーネル(Arch Linuxは「今のところ」古いフォーマットのままである)を使用する場合は、以下の変更が必要となる。

  1. kernel.imgをU-Bootが扱えるフォーマットに変換する。すなわち、mkimageコマンドでkernel.imgを処理して、U-Boot用のヘッダがついたファイルとする。
  2. U-Boot用のカーネル・イメージを、uEnv.txtで指定する。
  3. boot.scrスクリプト最後のコマンドを、bootz (圧縮されたLinuxカーネルをブート)ではなく、bootm (メモリ上のカーネルを実行する)に変更する。

検証が不十分なので、例は示さない。悪しからず。