Zero Client: Boot kernel and root filesystem from network with a Raspberry Pi2 or Pi3

A Zero Client is a computer that has nothing on its permanent storage but a bootloader. Rather, it loads everything from the network.

With the method presented in this article, you will be able to boot a Raspberry Pi into a full Debian OS with nothing more on the SD card other than the Raspberry firmware files and the u-boot bootloader on a FAT file system. The Linux kernel and the actual OS will be served over the local ethernet network.

We will only focus on the Raspberry Pi 3, but the instructions should work with minor adaptations also on a Pi 2.

The following instructions assume that you have already built…

  1. a full root file system for the Raspberry
  2. a u-boot binary, and
  3. a Linux kernel

… based on my previous blog post. Thus, you should already have the following directory structure:

~/workspace
  |- rpi23-gen-image
  |- linux
  |- u-boot
  |- raspberry-firmware

We will do all the work inside of the ~/workspace directory.

Preparation of the SD card

You will only need a small SD card with a FAT filesystem on it. The actual storage of files in the running OS will be transparently done over the network. Mount the filesystem on /mnt/sdcard and do the following:

Copy firmware

cp ./raspberry-firmware/* /mnt/sdcard

Copy u-boot bootloader

cp ./u-boot/u-boot.bin /mnt/sdcard

Create config.txt

config.txt is the configuration file read by the Raspberry firmware blobs. Most importantly, it tells the firmware what kernel to load. “Kernel” is a misleading term here, since we will boot u-boot rather than the kernel.

Create /mnt/sdcard/config.txt with the following contents:

avoid_warnings=2

# boot u-boot kernel
kernel=u-boot.bin

# run in 64bit mode
arm_control=0x200

# enable serial console
enable_uart=1

Make an universal boot script for the u-boot bootloader

To achieve maximum flexibility — to avoid the repetitive dance of manually removing the SD card, copying files to it, and re-inserting it — we will make an universal u-boot startup script that does nothing else than loading yet another u-boot script from the network. This way, there is nothing specific about the to-be-loaded Kernel or OS on the SD card at all.

Create a file boot.scr.mkimage  with the following contents:

setenv autoload no
setenv autostart no
dhcp

setenv serverip 192.168.0.250

tftp 0x100000 /netboot-${serial#}.scr

imi
source 0x100000

Replace the server IP with the actual static IP of your server. Note that this script does nothing else other than loading yet another script called netboot-${serial#}.scr  from the server. serial# is the serial number which u-boot extracts from the Raspberry Pi hardware. This is usually the ethernet network device HW address. This way, you can have separate startup scripts for several Raspberry Pi’s if you have more than one. To keep the setup simple, set the file name to something predictable.

Compile the script into an u-boot readable image:

./u-boot/tools/mkimage -A arm64 -O linux -T script \
-C none -a 0x00 -e 0x00 \
-d boot.scr.mkimage \
boot.scr

Copy boot.scr to the SD card:

cp boot.scr /mnt/sdcard

The SD card preparation is complete at this point. We will now focus on the serving of the files necessary for boot.

Preparation of the file server

Do all of the following as ‘root’ user on a regular PC running Debian 9 (“Stretch”). This PC will act as the “server”.  This server will serve the files necessary to network-boot the Raspberry.

The directory /srv/tftp will hold …

  • an u-boot start script file
  • the kernel uImage file
  • and the binary device tree file.

… to be served by a TFTP server.

mkdir /srv/tftp

The directory /srv/rootfs_rpi3 will hold our entire root file system to be served by a NFS server:

mkdir /srv/rootfs_rpi3

You will find installation instructions of both TFTP and NFS servers further down.

Serve the root file system

Let’s copy the pre-built root file system into the directory from where it will be served by the NFS server:

rsync -a ./rpi23-gen-image/images/stretch/build/chroot/ /srv/rootfs_rpi3

(notice the slash at the end of the source directory)

Fix the root file system for network booting

Edit /srv/rootfs_rpi3/etc/fstab  and comment out all lines. We don’t need to mount anything from the SD card.

When network-booting the Linux kernel, the kernel will configure the network device for us (either with a static IP or DHCP). Any userspace programs attempting to re-configure the network device will cause problems, i.e. a loss of conncection to the NFS server. Thus, we need to prevent systemd-networkd from managing the Ethernet device. Make the device unmanaged by removing the folowing ethernet configuration file:

rm /srv/rootfs_rpi3/etc/systemd/network/eth.network

If you don’t do that, you’ll get the following kernel message during boot:

nfs: server not responding, still trying

That is because systemd has shut down and then re-started the ethernet device. Apparently NFS transfers are sensitive to that.

In case you want to log into the chroot to make additional changes that can only be done from within (e.g. running systemctl scripts etc.), you can do:

cp /usr/bin/qemu-aarch64-static /srv/rpi3fs/usr/bin
LANG=C LC_ALL=C chroot /srv/rpi3fs

Serve Kernel uImage

In this step, we create a Linux kernel uImage that can be directly read by the u-boot bootloader. We read Image.gz directly from the Kernel source directory, and output it into the /srv/tftp directory where a TFTP server will serve it to the Raspberry:

./u-boot/tools/mkimage -A arm64 -O linux -T kernel \
-C gzip -a 0x80000 -e 0x80000 \
-d ./linux/arch/arm64/boot/Image.gz \
/srv/tftp/linux-rpi3.uImage

Serve device tree binary

The u-boot bootloader will also need to load the device tree binary and pass it to the Linux kernel, so copy that too into the /srv/tftp directory.

cp ./linux/arch/arm64/boot/dts/broadcom/bcm2837-rpi-3-b.dtb /srv/tftp/

Serve secondary u-boot script loading the kernel

Create a file netboot-rpi3.scr.mkimage with the following contents:

setenv autoload no
setenv autostart no
dhcp

setenv serverip 192.168.0.250

setenv bootargs "earlyprintk console=tty1 dwc_otg.lpm_enable=0 root=/dev/nfs rw rootfstype=nfs nfsroot=192.168.0.250:/srv/rpi3fs,udp,vers=3 ip=dhcp nfsrootdebug smsc95xx.turbo_mode=N elevator=deadline rootdelay cma=256M@512M net.ifnames=1 init=/bin/systemd loglevel=7 systemd.log_level=debug systemd.log_target=console"

tftp ${kernel_addr_r} linux-rpi3.uImage
tftp ${fdt_addr_r} bcm2837-rpi-3-b.dtb
bootm ${kernel_addr_r} - ${fdt_addr_r}

Replace the server IP with the static IP of your server PC. Then compile this script into an u-boot readable image and output it directly to the /srv/tftp directory:

./u-boot/tools/mkimage -A arm64 -O linux -T script \
-C none -a 0x00 -e 0x00 \
-d netboot-rpi3.scr.mkimage \
/srv/tftp/netboot-0000000012345678.scr

Make sure that the filename of the .scr file matches with whatever file name you’ve set in the universal .scr script that we’ve prepared further above.

Install a NFS server

The NFS server will serve the root file system to the Raspberry and provide transparent storage.

apt-get install nfs-kernel-server

Edit /etc/exports and add:

/srv/rootfs_rpi3  *(rw,sync,no_root_squash,no_subtree_check,insecure)

To apply the changed ‘exports’ configuration, run

exportfs -rv

Useful to know about the NFS server:

You can restart the NFS server by running service nfs-kernel-server restart

Configuration files are /etc/default/nfs-kernel-server  and /etc/default/nfs-common

Test NFS server

If you want to be sure that the NFS server works correctly, do the following on another PC:

apt-get install nfs-common

Mount the root file system (fix the static IP for your server):

mkdir /tmp/testmount
mount 192.168.0.250:/srv/rootfs_rpi3 /tmp/testmount
ls -al /tmp/testmount

Install a TFTP server

To install:

apt-get install tftpd-hpa

After installation, check if the TFTP server is running:

ps -ejHf | grep ftp

This command will tell you the default serving directory (/srv/tftp):

/usr/sbin/in.tftpd --listen --user tftp --address 0.0.0.0:69 --secure /srv/tftp

Here is another command that tells you if the TFTP server is listening:

netstat -l -u | grep ftp

To get help about this server: man tftpd

Test TFTP

If you want to be sure that the TFTP server works correctly, do the following on another PC:

apt-get install tftp-hpa

Then see if the server serves the Linux kernel we’ve installed before:

tftp 192.168.0.250
tftp> get linux-rpi3.uImage
tftp> quit

You now should have a local copy of the linux-rpi3.uImage file.

Completion

If you’ve done all of the above correctly, you can insert the prepared SD card into your Raspberry Pi and reboot it. The following will happen:

  1. The Raspberry Pi GPU will load the firmware blobs from the SD card.
  2. The firmware blobs will boot the image specified in config.txt. In our case, this is the u-boot binary on the SD card.
  3. The u-boot bootloader will boot.
  4. The u-boot bootloader loads and runs the universal boot.scr script from the SD card.
  5. The boot.scr downloads the specified secondary boot script from the network and runs it.
  6. The secondary boot script …
    • downloads the device tree binary from the network and loads it into memory.
    • downloads the Linux kernel from the network and loads it into memory
    • passes the device tree binary to the kernel, and boots the kernel
  7. the Linux kernel will bring up the ethernet device, connect to the NFS server, and load the regular OS from there.

Many things can go wrong in this rather long sequence, so if you run into trouble, check the Raspberry boot messages output on an attached screen or serial console, and the log files of the NFS and TFTP servers on your server PC.

Resources

https://www.raspberrypi.org/documentation/linux/kernel/building.md

http://www.whaleblubber.ca/boot-raspberry-pi-nfs/

https://cellux.github.io/articles/moving-to-nfs-root/

http://billauer.co.il/blog/2011/01/diskless-boot-nfs-cobbler/

https://www.kernel.org/doc/Documentation/filesystems/nfs/nfsroot.txt

http://wiki.linux-nfs.org/wiki/index.php/General_troubleshooting_recommendations

https://wiki.archlinux.org/index.php/NFS

Reading Raspberry Pi chip temperature with mainline Linux kernel

This tutorial is based on a previous article where we installed pure Debian 9 with a recent mainline/vanilla Linux kernel on a Raspberry Pi, and so differs from what would be done on a Raspbian Distribution with a Raspbian kernel. In this article, we will read the Raspberry Pi chip temperature. Here is my previous article:

https://blog.michael.franzl.name/2016/10/31/raspberry-pi-debian-stretch/

As of mainline Linux Kernel 4.9.0-rc3, the sysfs entry /sys/class/thermal for the Broadcom BCM283x chip found on Raspberry Pi’s is empty. You can apply the following patch to Linux kernel 4.9.0-rc3, even though it will soon be superfluous because it seems that currently there is ongoing work by Linux Kernel developers to add in the missing functionalities.

The patch which will get you the standard sysfs temperature node which you can read like this:

cat /sys/class/thermal/thermal_zone0/temp
40084

This is the chip temperature in thousandths of degrees cenigrade, i.e. 40.084 degrees Celsius.

The following patch will give you a new entry in the mainline Kernel config, under Drivers -> SoC -> BCM -> Raspberry Pi thermal sysfs driver, which you have to enable, then recompile your kernel.

I have actually submitted this patch to a Linux kernel developer, but the process to get code into the Linux kernel is quite elaborate, and he said that they are already working on it, so I let it drop and decided to write this blog post instead.

Copied drivers/thermal/bcm2835-thermal.c from Raspberry Pi
Foundation Linux kernel to drivers/soc/bcm/raspberry-thermal.c
and added respective entry to the device tree.

Tested and confirmed working on a Raspberry Pi 2 Model B V1.1 2014.

Signed-off-by: Michael Franzl <office@blog.michael.franzl.name>
Tested-by: Michael Franzl <office@blog.michael.franzl.name>

 arch/arm/boot/dts/bcm2835-rpi.dtsi    |   5 ++
 arch/arm/configs/multi_v7_defconfig   |   1 +
 drivers/soc/bcm/Kconfig               |   9 +++
 drivers/soc/bcm/Makefile              |   1 +
 drivers/soc/bcm/raspberrypi-thermal.c | 109 ++++++++++++++++++++++++++++++++++
 5 files changed, 125 insertions(+)
 create mode 100644 drivers/soc/bcm/raspberrypi-thermal.c

diff --git a/arch/arm/boot/dts/bcm2835-rpi.dtsi b/arch/arm/boot/dts/bcm2835-rpi.dtsi
index e9b47b2..389ee83 100644
--- a/arch/arm/boot/dts/bcm2835-rpi.dtsi
+++ b/arch/arm/boot/dts/bcm2835-rpi.dtsi
@@ -22,6 +22,11 @@
 			mboxes = <&mailbox>;
 		};
 
+		thermal: thermal {
+			compatible = "raspberrypi,bcm2835-thermal";
+			firmware = <&firmware>;
+		};
+
 		power: power {
 			compatible = "raspberrypi,bcm2835-power";
 			firmware = <&firmware>;
diff --git a/arch/arm/configs/multi_v7_defconfig b/arch/arm/configs/multi_v7_defconfig
index 11f37ed..1368e63 100644
--- a/arch/arm/configs/multi_v7_defconfig
+++ b/arch/arm/configs/multi_v7_defconfig
@@ -838,6 +838,7 @@ CONFIG_XILINX_XADC=y
 CONFIG_CM36651=m
 CONFIG_AK8975=y
 CONFIG_RASPBERRYPI_POWER=y
+CONFIG_RASPBERRYPI_THERMAL=y
 CONFIG_PWM=y
 CONFIG_PWM_ATMEL=m
 CONFIG_PWM_ATMEL_HLCDC_PWM=m
diff --git a/drivers/soc/bcm/Kconfig b/drivers/soc/bcm/Kconfig
index a39b0d5..c951de4 100644
--- a/drivers/soc/bcm/Kconfig
+++ b/drivers/soc/bcm/Kconfig
@@ -20,4 +20,13 @@ config SOC_BRCMSTB
 
 	  If unsure, say N.
 
+config RASPBERRYPI_THERMAL
+	tristate "Raspberry Pi thermal sysfs driver"
+	depends on ARCH_BCM2835 || (COMPILE_TEST && OF)
+	depends on RASPBERRYPI_FIRMWARE=y
+	depends on THERMAL
+	help
+	  This enables a thermal sysfs driver for the thermal sensor on
+	  Broadcom283x chips found on a Raspberry Pi.
+
 endmenu
diff --git a/drivers/soc/bcm/Makefile b/drivers/soc/bcm/Makefile
index dc4fced..41734e1 100644
--- a/drivers/soc/bcm/Makefile
+++ b/drivers/soc/bcm/Makefile
@@ -1,2 +1,3 @@
 obj-$(CONFIG_RASPBERRYPI_POWER)	+= raspberrypi-power.o
 obj-$(CONFIG_SOC_BRCMSTB)	+= brcmstb/
+obj-$(CONFIG_RASPBERRYPI_THERMAL)	+= raspberrypi-thermal.o
diff --git a/drivers/soc/bcm/raspberrypi-thermal.c b/drivers/soc/bcm/raspberrypi-thermal.c
new file mode 100644
index 0000000..34a829f
--- /dev/null
+++ b/drivers/soc/bcm/raspberrypi-thermal.c
@@ -0,0 +1,109 @@
+/*****************************************************************************
+* Copyright 2011 Broadcom Corporation.  All rights reserved.
+*
+* Unless you and Broadcom execute a separate written software license
+* agreement governing use of this software, this software is licensed to you
+* under the terms of the GNU General Public License version 2, available at
+* http://www.broadcom.com/licenses/GPLv2.php (the "GPL").
+*
+* Notwithstanding the above, under no circumstances may you combine this
+* software in any way with any other Broadcom software provided under a
+* license other than the GPL, without Broadcom's express prior written
+* consent.
+*****************************************************************************/
+
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/thermal.h>
+#include <soc/bcm2835/raspberrypi-firmware.h>
+
+static int bcm2835_thermal_get_property(struct thermal_zone_device *tz,
+					int *temp, u32 tag)
+{
+	struct rpi_firmware *fw = tz->devdata;
+	struct {
+		u32 id;
+		u32 val;
+	} packet;
+	int ret;
+
+	*temp = 0;
+	packet.id = 0;
+	ret = rpi_firmware_property(fw, tag, &packet, sizeof(packet));
+	if (ret) {
+		dev_err(&tz->device, "Failed to get temperature\n");
+		return ret;
+	}
+
+	*temp = packet.val;
+	dev_dbg(&tz->device, "%stemp=%d\n",
+		tag == RPI_FIRMWARE_GET_MAX_TEMPERATURE ? "max" : "", *temp);
+
+	return 0;
+}
+
+static int bcm2835_thermal_get_temp(struct thermal_zone_device *tz,
+				    int *temp)
+{
+	return bcm2835_thermal_get_property(tz, temp,
+					    RPI_FIRMWARE_GET_TEMPERATURE);
+}
+
+static struct thermal_zone_device_ops ops  = {
+	.get_temp = bcm2835_thermal_get_temp,
+};
+
+static int bcm2835_thermal_probe(struct platform_device *pdev)
+{
+	struct device_node *fw_np;
+	struct rpi_firmware *fw;
+	struct thermal_zone_device *tz;
+
+	fw_np = of_parse_phandle(pdev->dev.of_node, "firmware", 0);
+	if (!fw_np) {
+		dev_err(&pdev->dev, "Missing firmware node\n");
+		return -ENOENT;
+	}
+	fw = rpi_firmware_get(fw_np);
+	if (!fw)
+		return -EPROBE_DEFER;
+
+	tz = thermal_zone_device_register("bcm2835_thermal", 0, 0, fw, &ops,
+					  NULL, 0, 0);
+	if (IS_ERR(tz)) {
+		dev_err(&pdev->dev, "Failed to register the thermal device\n");
+		return PTR_ERR(tz);
+	}
+
+	platform_set_drvdata(pdev, tz);
+
+	return 0;
+}
+
+static int bcm2835_thermal_remove(struct platform_device *pdev)
+{
+	thermal_zone_device_unregister(platform_get_drvdata(pdev));
+
+	return 0;
+}
+
+static const struct of_device_id bcm2835_thermal_of_match_table[] = {
+	{ .compatible = "raspberrypi,bcm2835-thermal", },
+	{},
+};
+MODULE_DEVICE_TABLE(of, bcm2835_thermal_of_match_table);
+
+static struct platform_driver bcm2835_thermal_driver = {
+	.probe = bcm2835_thermal_probe,
+	.remove = bcm2835_thermal_remove,
+	.driver = {
+		.name = "bcm2835_thermal",
+		.of_match_table = bcm2835_thermal_of_match_table,
+	},
+};
+module_platform_driver(bcm2835_thermal_driver);
+
+MODULE_AUTHOR("Dorian Peake");
+MODULE_AUTHOR("Noralf Trønnes");
+MODULE_DESCRIPTION("Thermal driver for bcm2835 chip");
+MODULE_LICENSE("GPL");
-- 
2.1.4


Setting I2C bus speed on a Raspberry Pi via Device Tree

This tutorial is based on my previous article where we installed pure Debian 9 with a recent mainline/vanilla Linux kernel, and so differs from what would be done on a Raspbian Distribution with a Raspbian kernel. In this article, we will set the I2C bus speed on a Raspberry Pi. Here is my previous article:

https://blog.michael.franzl.name/2016/10/31/raspberry-pi-debian-stretch/

Device Trees

The I2C bus on the Broadcom BCM283x chips found on Raspberry Pi’s is well and directly supported by the mainline/vanilla Linux kernel. Since with the Raspberry Pi we’re dealing with a System on a Chip (SoC), and not a regular PC, the hardware is configured with so-called device trees, which is a low-level description of the chip hardware compiled from text into binary format.

The rpi23-gen-image script mentioned in my previous tutorial installs the binary device tree into /boot/firmware/bcm2836-rpi-2-b.dtb. The U-Boot bootloader can read this file and pass it to the Linux kernel which interprets it and enables all the mentioned features in it.

The clock frequency for the I2C bus is configured in this .dtb file, and the default is 100kHz. There is a tool which allows you to inspect the .dtb file, outputting regular text. With this tool you also can make changes to the device configuration. Nowadays, this is the proper way to configure low-level devices on SoC’s!

Read the device tree

apt-get install device-tree-compiler
fdtdump /boot/firmware/bcm2836-rpi-2-b.dtb

This will output the decoded device tree as text. Regarding I2C, you will find i2c@-entries like this:

        i2c@7e205000 {
            compatible = "brcm,bcm2835-i2c";
            reg = <0x7e205000 0x0000005c>;
            interrupts = <0x00000002 0x000000e9>;
            clocks = <0x00000008 0x00000022>;
            #address-cells = <0x00000001>;
            #size-cells = <0x00000000>;
            status = "okay";
            clock-frequency = <0x000186a0>;
        };
        i2c@7e804000 {
            compatible = "brcm,bcm2835-i2c";
            reg = <0x7e804000 0x0000005c>;
            interrupts = <0x00000002 0x000000e9>;
            clocks = <0x00000008 0x00000022>;
            #address-cells = <0x00000001>;
            #size-cells = <0x00000000>;
            status = "okay";
            clock-frequency = <0x000186a0>;
        };

Change the device tree

The clock-frequency value is what we want to change. The value is a raw binary unsigned 32-bit int stored big-endian, unreadable for humans.

But you can use another tool fdtget to read just this value decoded:

fdtget /boot/firmware/bcm2836-rpi-2-b.dtb /soc/i2c@7e205000 clock-frequency

This will output 100000.

In my case, I wanted to set the I2C bus to the slowest frequency, to compensate for long cable lengths. I found that one of the lowest supported I2C clock frequencies is 4kHz. With fdtput you can set the clock-frequency property for each i2c device (there are 3 on the RPi):

fdtput --type u /boot/firmware/bcm2836-rpi-2-b.dtb /soc/i2c@7e205000 clock-frequency 4000

And that’s it. You have to reboot for these settings to take effect.

I used an oscilloscope to verify that the SCL pin of the GPIO of the RPi indeed toggled with 4kHz, and it did!