Embedded Linux update
Why
I don’t think I need to explain why possibility of system update is important. Nearly every day I read about another device filled with security holes.
While bugs are inevitable – every system has some. It is extremely important to be able to fix them.
While some (most maybe) devices are containing full blown Linux distribution, with pacman/apt support, there are also devices working on minimal distribution built using Yocto or Buildroot. These distributions are working more close to the metal, without resources required to run decent package manager.
Update process itself is little different comparing to regular Linux machine. User usually cannot supervise it. He cant make decisions.
Firmware update contents are under full control of device maker. It is the manufacturer who decides what will be updated.
It is devices makers responsibility to ensure that update won’t „brick” the device.
For who
I expect intermediate knowledge about Linux and Yocto build system.
I am using Yocto, but solution can be used on any NAND-based embedded Linux project.
Hardware
I am using custom board based on Atmel’s SAMA5. You can use this article to build update system for any U-boot/NAND based board.
I will port this solution to NXP SOM’s devices in future.
Definitions
Yocto
Yocto is a widely used system for building complete embedded Linux images.
Unfortunately, learning curve is extremely steep. While documentation is rich and detailed, there are number of „gotchas”, which are making development process painful.
Device tree
ARM-based devices does not have a BIOS. There are no system which could tell Linux Kernel about hardware.
That is why Device Tree was introduced.
Device tree is a binary encoded hardware description. Linux kernel parse and analyze device tree.
Device tree usually is loaded in to RAM by bootloader, then bootloader loads and starts Linux Kernel.
NAND flash
While most of today using embedded system are using miniSD card or some kind of eMMC chip. There are some using NAND flash.
MiniSD and eMMC can be described as NAND with additional controller – specialized chip that can balance NAND flash sectors wearing. Also controller is able to detect bad sectors.
Because NAND flash has some special properties – ordinary file system cannot be used on them, that is why specialized file systems was invented (UBI, YAFFS and more)
NAND partitions
NAND flash can be divided into to partitions, similar to hard disk. But unlike hard disk – NAND does not contain partition table.
NAND partitions are defined using Device Tree or command line parameters.
UBI and UBIFS
UBIFS is relatively new file system. Designed to run on top of UBI image.
UBI (unsorted block image) is a layer placed on top of NAND partition.
UBI can be described as image that can contain many volumes.
UBI is much safer then raw NAND flash. It balances writes, and is able to detect bad sectors and restore its data. That is why it is safe to store volume table inside UBI.
Bootloader
Bootloader is a program thats job is to start operating system.
Bootloaders can be divided in to:
- First stage – those one initializes hardware and loads second stage bootloader.
- Second stage, these job is to start operating system.
Bootstrap
Bootstrap is a first stage bootloader made by Atmel.
It’s job is to initialize hardware, and load second stage bootloader.
U-boot
U-boot is a popular second stage bootloader.
Many hardware vendors are creating patches, to U-boot could support their chips.
U-boot has command-line interface. User can modify default behavior by modifying environment variables.
Environment variables are stored in NAND partitions. Usually these partitions are redundant. Even if one is damaged, second one should allow device boot.
U-boot default environment variables, location of environment variables partitions(s), NAND partitions layout etc. are compiled-in into U-boot binary.
In simplest scenario, U-boot loads kernel and device tree from NAND partitions into memory. Then starts kernel.
FIT
FIT is a single-file format supported by U-boot that can be used to store both kernel and device tree.
FIT also can be signed, U-boot will not run image without proper signature (optional).
Default Atmel SAMA5D3 NAND layout
Lets analyze Atmel SAMA5D3 default boot process.
Here is NAND layout:
+--------------------------------------+ | Bootstrap | +--------------------------------------+ | U-boot | +--------------------------------------+ | U-boot environment variables 1 | +--------------------------------------+ | U-boot environment variables 2 | +--------------------------------------+ | Device tree | +--------------------------------------+ | Kernel | +--------------------------------------+ | UBI/UBIFS rootfs | +--------------------------------------+
Boot process
* Bootstrap initializes hardware
* Bootstrap loads and starts U-boot.
* U-boot starts,
* Loads its environments variables from NAND partition (or uses defaults, if no valid environment variables partitions has been found).
* Executes /bootcmd/ environment variable commands which loads device tree and kernel into RAM.
* U-boot starts Linux kernel using comand-line parameters defined in /bootargs/ environment variable
Goal
My goal is to create redundant banks for kernel and root.
The bank is a pair of volumes. One for kernel, second for root.
There are two banks. Only one is in use. The other one is used during update process.
Update script writes kernel/rootfs images in to free bank volumes.
Then, U-Boot environment variables are modified, so on the next reboot, bootloader will start Linux using different bank.
Device configuration after modifications
This is how NAND/UBI partitions and volumes is going to look.
+--------------------------------------+ | Bootstrap | +--------------------------------------+ | U-boot | +--------------------------------------+ | U-boot environment variables 1 | +--------------------------------------+ | U-boot environment variables 2 | +--------------------------------------+ | UBI | | +----------------------------------+ | | | Kernel 1 (FIT) (Bank 1) | | | +----------------------------------+ | | | Kernel 2 (FIT) (Bank 2) | | | +----------------------------------+ | | | Rootfs 1 (UBIFS) (Bank 1) | | | +----------------------------------+ | | | Rootfs 2 (UBIFS) (Bank 2) | | | +----------------------------------+ | | | Data (UBIFS) | | | +----------------------------------+ | +--------------------------------------+
Modifications required
Overview of modifications required to implement
NAND
NAND layout has to be changed.
The problem is – there are more then one place where NAND partitions are defined. All of those definitions has to modified.
U-boot
U-Boot requires full information about NAND flash partitions layout. Also it has to know UBI volumes labels for kernel and rootfs.
Additionally, U-Boot has to be configured to support FIT files.
All modifications requires rather serious U-boot code modifications.
I suggest to fork U-boot git repository, and make changes.
Changing git repository locations requires altering Yocto recipe behavior by creating append file (.bbappend)
UBI volumes
By default UBI image with single volume is created.
Image creation process is implemented in image_types.bbclass file. Unfortunately modifying behavior defined in .bbclass file is not as easy as .bbrecipe. Recipes have simple inheritance mechanism, classes are not. Luckily there is a hack we can use to get the work done.
To create UBI image containing five volumes original image_types.bbclass file has to be copied and changed.
UBI image should contain five volumes. Each volume is created using UBIFS image file or raw image.
Kernel
Kernel can remain unchanged. But Yocto has to be instructed to generate FIT file containing both Kernel and device tree.
Rootfs
Rootfs should contain additional software packages:
- UBI volumes manipulation
- NAND partitions manipulation
- U-Boot environment variables manipulation
These packages needs to be added to image,
DATA
Rootfs should be read only. Usually we need some kind of read/write storage. That is why another UBI volume is needed. DATA volume should contain UBIFS file system, and has to be mounted a boot time.
Bootstrap
Bootstrap may be left unchanged.
Lets get to work
Yocto configuration
Custom layer
To customize way some Yocto recipes work we need custom layer.
Lets call it meta-arek. Create folder named ‚meta-arek’ next to other layers in yocto directory.
You should know how to create ‚build’ directory.
Inside build/conf directory find, and open bblayers.conf file. Add path to meta-arek layer directory to BBLAYERS variable.
Path to meta-arek should be first on list. I’ll explain later why.
Link to Yocto documentation:
https://www.yoctoproject.org/documentation
UBI image and volumes
We need to change image creation process. The code that creates images can be found in meta/classes/image_types.bbclass file. Unfortunately we cant override behavior of .bbclass files. That is why I’m using little hack.
Copy file to meta-arek/classes directory. Because ‚meta-arek’ layer directory is higher then ‚meta’, it’s files will be preferred by bitbake.
Now, lets modify meta-arek/classes/image_types.bbclass file
multiubi_mkfs() { local mkubifs_args="$1" local ubinize_args="$2" if [ -z "$3" ]; then local vname="" else local vname="_$3" fi echo -n > ubinize${vname}.cfg echo \[kernel1\] >> ubinize${vname}.cfg echo mode=ubi >> ubinize${vname}.cfg echo image=${DEPLOY_DIR_IMAGE}/fitImage >> ubinize${vname}.cfg echo vol_id=1 >> ubinize${vname}.cfg echo vol_type=dynamic >> ubinize${vname}.cfg echo vol_name=kernel1 >> ubinize${vname}.cfg echo vol_size=10MiB >> ubinize${vname}.cfg echo \[kernel2\] >> ubinize${vname}.cfg echo mode=ubi >> ubinize${vname}.cfg echo image=${DEPLOY_DIR_IMAGE}/fitImage >> ubinize${vname}.cfg echo vol_id=2 >> ubinize${vname}.cfg echo vol_type=dynamic >> ubinize${vname}.cfg echo vol_name=kernel2 >> ubinize${vname}.cfg echo vol_size=10MiB >> ubinize${vname}.cfg echo \[root1\] >> ubinize${vname}.cfg echo mode=ubi >> ubinize${vname}.cfg echo image=${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${vname}.rootfs.ubifs >> ubinize${vname}.cfg echo vol_id=3 >> ubinize${vname}.cfg echo vol_type=dynamic >> ubinize${vname}.cfg echo vol_name=root1 >> ubinize${vname}.cfg echo vol_size=30MiB >> ubinize${vname}.cfg echo \[root2\] >> ubinize${vname}.cfg echo mode=ubi >> ubinize${vname}.cfg echo image=${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${vname}.rootfs.ubifs >> ubinize${vname}.cfg echo vol_id=4 >> ubinize${vname}.cfg echo vol_type=dynamic >> ubinize${vname}.cfg echo vol_name=root2 >> ubinize${vname}.cfg echo vol_size=30MiB >> ubinize${vname}.cfg echo \[data\] >> ubinize${vname}.cfg echo mode=ubi >> ubinize${vname}.cfg echo vol_id=5 >> ubinize${vname}.cfg echo image=${DEPLOY_DIR_IMAGE}/empty.ubifs >> ubinize${vname}.cfg echo vol_type=dynamic >> ubinize${vname}.cfg echo vol_name=data >> ubinize${vname}.cfg echo vol_size=30MiB >> ubinize${vname}.cfg echo vol_flags=autoresize >> ubinize${vname}.cfg rm -rf ${DEPLOY_DIR_IMAGE}/empty/* mkdir -p ${DEPLOY_DIR_IMAGE}/empty mkfs.ubifs -r ${DEPLOY_DIR_IMAGE}/empty -o ${DEPLOY_DIR_IMAGE}/empty.ubifs ${mkubifs_args} mkfs.ubifs -r ${IMAGE_ROOTFS} -o ${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${vname}.rootfs.ubifs ${mkubifs_args} ubinize -o ${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${vname}.rootfs.ubi ${ubinize_args} ubinize${vname}.cfg # Cleanup cfg file mv ubinize${vname}.cfg ${DEPLOY_DIR_IMAGE}/ # Create own symlinks for 'named' volumes if [ -n "$vname" ]; then cd ${DEPLOY_DIR_IMAGE} if [ -e ${IMAGE_NAME}${vname}.rootfs.ubifs ]; then ln -sf ${IMAGE_NAME}${vname}.rootfs.ubifs \ ${IMAGE_LINK_NAME}${vname}.ubifs fi if [ -e ${IMAGE_NAME}${vname}.rootfs.ubi ]; then ln -sf ${IMAGE_NAME}${vname}.rootfs.ubi \ ${IMAGE_LINK_NAME}${vname}.ubi fi cd - fi }
As you see, we have changed way file ubinize.cfg is generated. Ubinize.cfg file is used by ‚ubinize’ tool, which creating UBI image. Ubuinize.cfg contains definitions of UBI volumes, and some NAND specific configuration values.
U-boot
I spent couple of days, searching for simple solution to modify U-boot. I did not found any.
Atmel’s manual for U-boot setup is terribly outdated. So I have to found solution myself.
U-boot configuration is probably hardest part. We have to modify U-boot source files.
That is why I recommend to fork U-boot repository, make changes in forked version, then change repository URI using by Bitbake to fetch U-boot source code.
If you forked U-boot repository, we can start work.
Open arch/arm/mach-at91/Kconfig file.
add:
config TARGET_SAMA5D3_AREK bool "SAMA5D3 Xplained board" select CPU_V7 select SUPPORT_SPL
And, at the end of arch/arm/mach-at91/Kconfig file:
source "board/atmel/sama5d3_arek/Kconfig"
Create file board/atmel/sama5d3_arek/Kconfig
if TARGET_SAMA5D3_AREK config SYS_BOARD default "sama5d3_xplained" config SYS_VENDOR default "atmel" config SYS_CONFIG_NAME default "sama5d3_arek" endif
Create file board/atmel/sama5d3_arek/MAINTAINERS
SAMA5D3_AREK BOARD M: Arek Marud <a.marud@post.pl> S: Maintained F: board/atmel/sama5d3_arek/ F: include/configs/sama5d3_arek.h F: configs/sama5d3_arek_defconfig F: configs/sama5d3_arek_defconfig
Create file configs/sama5d3_arek_defconfig
CONFIG_ARM=y CONFIG_ARCH_AT91=y CONFIG_TARGET_SAMA5D3_AREK=y CONFIG_SPL=y CONFIG_FIT=y CONFIG_SYS_EXTRA_OPTIONS="SAMA5D3,SYS_USE_NANDFLASH"
Create file include/configs/sama5d3_arek.h
#include "sama5d3_xplained.h" #define MTDIDS_DEFAULT "nand0=nand_flash" #define MTDPARTS_DEFAULT "mtdparts=nand_flash:256k(bootstrap)ro,512k(uboot)ro,256k(env1),256k(env2),-(sys)" #define CONFIG_BOOTARGS "ubi.mtd=4 root=ubi0:root1 rootfstype=ubifs console=ttyS0,115200 earlyprintk mtdparts=atmel_nand:256k(bootstrap)ro,512k(uboot)ro,256k(env1),256k(env2),-(sys)" #define CONFIG_BOOTCOMMAND "mtdparts default;ubi part sys;ubi read 0x22000000 kernel1;bootm 0x22000000#conf@1"
That’s it.
Please notice contents of file ‚include/configs/same5d3_arek.h’, MTDPARTS_DEFAULT describes NAND partitions layout for U-boot. CONFIG_BOOTARGS environment variable defines kernel command line parameters, and NAND partitions.
Finally CONFIG_BOOTCOMMAND variable stores command for kernel start.
U-boot Yocto changes
To make our changes work, we have to change git repository URI.
Create file ‚meta-arek/recipes-bsp/u-boot/u-boot-at91_git.bbappend’.
Add single line:
SRC_URI="git://gitserver.com/u-boot-custom.git.git;branch=master;protocol=http"
Of course you have to enter URI for your repository.
Machine
Default settings for image are stored in machine file. Let’s create one.
First we need to copy all required include files.
Copy files:
at91sam9.inc bootloaders.inc sama5d2.inc sama5d3.inc sama5d4.inc sama5.inc
to meta-arek/conf/machine/include directory.
Files can be found inside meta-atmel directory.
Create file meta-arek/conf/machine/mymachine.conf
require include/sama5d3.inc MACHINE_FEATURES = "kernel26 apm ext2 ext3 usbhost usbgadget camera ppp wifi iptables" # used by sysvinit_2 SERIAL_CONSOLES ?= "115200;ttyS0" ROOT_FLASH_SIZE = "256" IMAGE_FSTYPES += " ubi tar.gz" # NAND MKUBIFS_ARGS ?= " -e 0x1f000 -c 2048 -m 0x800 -x lzo" UBINIZE_ARGS ?= " -m 0x800 -p 0x20000 -s 2048" UBI_VOLNAME = "rootfs" UBOOT_MACHINE ?= "sama5d3_arek_config" UBOOT_ENTRYPOINT = "0x20008000" UBOOT_LOADADDRESS = "0x20008000" AT91BOOTSTRAP_MACHINE ?= "sama5d3_xplained" PREFERRED_PROVIDER_virtual/kernel = "linux-at91" PREFERRED_VERSION_linux-at91= "4.%" KERNEL_CLASSES += "kernel-fitimage" KERNEL_IMAGETYPE = "fitImage"
Last two lines forces FIT file creation. FIT file will be used during UBI image creation.
Test build
bitbake core-image-minimal
Directory build/tmp/deploy/images/mymachine should be populated
flash
It is time to flash files in to NAND flash memory.
Do to that, Atemel’s SAM-BA flashing tool is required. Download and unpack sam-ba. Find directory with sam-ba_64 file.
Create flash.tcl file:
global target puts "=== Initialize the NAND access ===" NANDFLASH::Init puts "=== Erase the NAND access ===" NANDFLASH::EraseAll puts "=== Send SPL ===" NANDFLASH::SendBootFileCmd "at91bootstrap.bin" puts "=== Send u-boot.bin ===" send_file {NandFlash} "u-boot.bin" 0x40000 0 puts "=== Send rootfs ===" send_file {NandFlash} "rootfs.ubi" 0x00140000 0 puts "=== DONE ==="
Create flash.sh file:
#!/bin/bash ./sam-ba_64 /dev/ttyACM0 at91sama5d3x-xplained flash.tcl
Make flash.sh executable:
chmod u+x flash.sh
Connect your SAMA5 based board to USB port.
Remember to enable NAND access mode (JP5 jumper on SAMA5D3 Xplained).
Power the device (with jumper connected). Disconnect jumper after 3-4 seconds and start ‚flash.sh’ script. Wait for flashing process to finish.
Restart your device. Linux should boot.
Linux modifications
Now, our system has to have ability to update itself. That means that Linux needs to have access to:
* U-boot environment variables. These variables has to be changed to "switch" active kernel and rootfs.
* UBI volumes. Kernel and rootfs images will be copied in to them.
Lets start with U-Boot environment variables.
First, lets add „u-boot-fw-utils” package in to the rootfs. The package contains utilities, that allows to modify U-Boot environment variables.
local.config
IMAGE_INSTALL_APPEND += " u-boot-fw-utils"
The package „u-boot-fw-utils” is built using main U-Boot source repository. Because we changed U-Boot repository to different server, it is recommended to do this also for „u-boot-fw-utils” package.
To modify git repository address, create file u-boot-fw-utils_2015.07.bbappend in directory meta-arek/recipes-bsp/u-boot
SRCREV = "<git comit hash>" LIC_FILES_CHKSUM = "file://Licenses/README;md5=a2c678cfd4a4d97135585cad908541c6" SRC_URI="git://yourgit.server.com/repository.git;branch=master;protocol=http"
Unfortunately u-boot-fw-utils_2015.07.bbrecipe file is using SRCREV parameter. I did not found the way to „nullify” SRCREV in bbappend file. So, the value has to be changed each time some significant commit was pushed in to git repository.
What SRCREV does, is to force usage of specified commit instead the last one.
SRC_URI should be the same like u-boot-at91_git.bbappend file.
OK. So now we have tool to manipulate U-Boot environment variables installed. Now we need to configure it.
Program „u-boot-fw-utils” requires information about placement of U-Boot environment variables. Environment variables are stored in MTD partitions. Because we added MTD partitions layout information to the kernel command line, Kernel should create device files for each partition. Files are named /dev/mtd1 /dev/mtd2 and so on.
Configuration for u-boot-fw-utils is stored in /etc/fw_env.config file.
Here is valid configuration for MTD layout:
/dev/mtd2 0x0 0x20000 0x20000 1 /dev/mtd3 0x0 0x20000 0x20000 1
Default content for /etc/fw_env.config file is stored in U-Boot repository, it has to be changed.
Easiest way to do it, is to modify U-Boot repository. Find default file, and modify it.
Unfortunately because git repository was changed, SRCREV in u-boot-fw-utils_2015.07.bbappend file has to be updated. Change SRCREV value to valid commit hash.
UBI volumes manipulation
One of the greatest Linux strengths is the „everything is a file” philosophy. Each hard disk, partition on that disk has its own device file.
That rule applies also to UBI volumes – where kernel and rootfs are stored.
Essentially, during update process rootfs and kernel image files are copied in to UBI volume device file.
But UBI volumes are not ordinary Linux block device, ‚dd’ command cannot be used. We need tool for this task.
Package „mtd-utils-ubifs” contains everything we need.
To add „mtd-utils-ubifs” to image, add:
IMAGE_INSTALL_APPEND += " mtd-utils-ubifs"
to local.config
Now, command ubiupdatevol can be used. To copy kernel image file to /dev/ubi0_1 UBI volume device file try command:
ubiupdatevol /dev/ubi0_1 kernel
Now we have all tools we need to update Linux.
There are, however few scripts that needs to be created.
Create update package file
The easiest way to do that, is to create .tar.gz file containing rootfs and kernel files.
Unpack update package
I think best location is somewhere in /tmp directory.
Determine currently used UBI volumes.
This can be done by parsing U-Boot environment variables, or kernel command line parameters contained in /proc/ cmdline.
Overwrite UBI volumes
Using ubiupdatevol, copy contents of kernel and rootfs files in to UBI volumes.
Modify bootloader
Use fw_setenv command to modify bootloader environment variables.
Variable „bootcmd” contains kernel location.
Variable „bootargs” contains rootfs location.
Unfortunately we need to change two variables. That means that „switch” will not be an atomic operation.
Mount „data” volume
Rootfs contents should not be changed. Best solution would be read-only rootfs, I tried that, but number of problems that needs to be fixes is huge.
System settings directory – /etc/ can be mounted using unionfs over tmpfs volume. But some services (dropbear for example) are changing files in other directories.
Anyway, all changes made on rootfs will be lost after update. That is why we need a place where modified files (configuration data for example) will be stored. There is UBI volume named ‚data’ for that purpose.
We can:
* Mount contents of ‚data’ volume in /mnt/data directory
* Mount unionfs over /etc and /mnt/data/etc directories to merge them. All modifications made on /etc will be stored in ‚data’ volume. That means that all settings will remain unchanged after update process.
Unfortunately /etc/fstab can’t be used to mount UBI volume. To mount volume create script that will be executed during boot time, and mount volume ‚by hand’ using ‚mount’ command.
Firmware update file
Easiest way to make firmware update package, is to place files named rootfs and kernel inside tar.gz file.
Archive can be extracted in to /tmp directory (/tmp in Yocto minimal image is mounted using tmpfs, it is basically a ramdisk).
I suggest to sign package using OpenSSL private key. Add public key to your rootfs, and verify update file before performing any modifications.
Scripts
At least one script is required to perform update process.
Script should:
* Verify package file against public key (optionally)
* Extract update package archive
* Check rootfs and kernel files existence
* Check currently used UBI volumes (read U-Boot environment variables using fw_printenv command)
* Use ubiupdatevol to modify UBI volumes
* Use fw_setenv command to modify U-Boot environment variables, to use new kernel and rootfs locations.