The ASUS Chromebit CS10 (veyron_mickey, RK3288, 2 GB RAM, 16 GB eMMC) is a stick PC that ChromeOS no longer supports. The usual way to run Arch Linux ARM on it boots a kernel image that has to be re-signed for the Chromebook firmware after every kernel update.

This guide uses a different boot chain:

coreboot + depthcharge (stock firmware, untouched)
  -> signed U-Boot image on the kernel partition (written once)
  -> /boot/extlinux/extlinux.conf on the root filesystem
  -> /boot/zImage + device tree

After setup, pacman -Syu updates the kernel like on any other machine. Nothing is signed, flashed or hooked again.

Warning. This writes to the eMMC with dd. A mistake there is recoverable with a USB drive. The optional last section writes to the firmware SPI chip, which can brick the device if it goes wrong. Skip it if you can live with a 30 second boot screen.

What you need

  • A Chromebit in developer mode.
  • A USB drive with Arch Linux ARM that boots on it (the ArchLinuxARM-armv7-chromebook-latest.tar.gz image), and a USB hub if you want a keyboard and the drive at the same time. Boot it with Ctrl+U at the developer screen. If it does not boot, run crossystem dev_boot_usb=1 dev_boot_signed_only=0 once from a root shell.
  • A second Linux machine to build U-Boot. On Arch x86_64: pacman -S git make gcc dtc bison flex swig uboot-tools python-setuptools openssl gnutls libelf arm-none-eabi-gcc.
  • On the Chromebit: pacman -S vboot-utils (vbutil_kernel, cgpt).
  • The public development keys from the ChromiumOS vboot_reference repository (tests/devkeys): kernel.keyblock and kernel_data_key.vbprivk. They are public, so signing with them is not security. It only satisfies the firmware in developer mode. Put them in /root/devkeys on the Chromebit.

Things to know about this device

  1. The first 8 MB of the eMMC can be write protected (check with mmc writeprotect user get /dev/mmcblk0). The primary GPT then lies in that area and is unusable, so only the backup GPT at the end of the disk is used.
  2. Mainline Linux only reads the backup GPT when started with the gpt kernel parameter. It appears in extlinux.conf below.
  3. The firmware only boots a signed kernel partition, so U-Boot goes into one.

The disk layout used in this guide:

# Start sector Size (sectors) Type Purpose
1 16384 32768 (16 MiB) ChromeOS kernel (priority 10, tries 5, successful 1) signed U-Boot
2 49152 30736351 Linux ext4 root filesystem

Step 1: partition the eMMC

This destroys the existing partition table, including ChromeOS. Boot the USB system (Ctrl+U) and run, as root:

DISK=/dev/mmcblk0
S=$(( $(blockdev --getsz $DISK) ))          # disk size in 512-byte sectors
cgpt create $DISK
cgpt add -i 1 -t kernel -b 16384 -s 32768 -P 10 -T 5 -S 1 -l Kernel $DISK
cgpt add -i 2 -t data   -b 49152 -s $(( S - 33 - 49152 )) -l Root $DISK
cgpt show $DISK

cgpt show must list both partitions: a 16 MiB "ChromeOS kernel" partition at sector 16384 and a "Linux data" partition at sector 49152 that runs to the end of the disk (30736351 sectors on a 14.7 GB unit). On a unit whose first 8 MB is write protected, a warning that the primary GPT header is ignored is normal. The partitions start at 8 MiB so that they sit outside the protected area.

Step 2: root filesystem on the eMMC

Boot the USB system (Ctrl+U) and log in. It cannot see mmcblk0p* (it runs without gpt), so address the root partition through a loop device:

L=$(losetup -f --show -o $((49152*512)) --sizelimit $((30736351*512)) /dev/mmcblk0)
mkfs.ext4 $L
mount $L /mnt

Copy an Arch Linux ARM root filesystem into /mnt (untar the ALARM image, or rsync the running USB system). Then, inside it (arch-chroot /mnt, or after booting from it):

pacman -S linux-armv7        # /boot/zImage, /boot/dtbs/*, modules
mkdir -p /boot/extlinux
cat > /boot/extlinux/extlinux.conf <<'EOF'
default arch
timeout 10

label arch
  menu label Arch Linux ARM
  linux /boot/zImage
  fdt /boot/dtbs/rk3288-veyron-mickey.dtb
  append root=/dev/mmcblk0p2 rootwait rw noinitrd console=tty0 gpt
EOF

Do not install linux-armv7-chromebook. It only provides /boot/vmlinux.kpart and a "flash now?" prompt, which is what U-Boot replaces. If it is installed, pacman -R linux-armv7-chromebook removes only that file.

Set the root and alarm passwords, and unmount.

Step 3: build U-Boot

git clone --depth 1 https://source.denx.de/u-boot/u-boot.git
cd u-boot
make O=b/mickey CROSS_COMPILE=arm-none-eabi- chromebit_mickey_defconfig

Source change: skip the CPU voltage and clock bump

In board/google/veyron/veyron.c, board_early_init_r() calls veyron_init(), which sets the CPU supply to 1.4 V and the CPU clock to 1.8 GHz. When U-Boot is started by depthcharge instead of the boot ROM, that call hangs the board with no output. Replace the whole #if !CONFIG_IS_ENABLED(ROCKCHIP_BACK_TO_BROM) ... #endif block that calls it with:

/* hangs when chain-loaded; keep the settings coreboot left in place */
(void)veyron_init;

Configuration

scripts/config --file b/mickey/.config \
  -e POSITION_INDEPENDENT --set-val TEXT_BASE 0x02001000 \
  -e CMD_SYSBOOT --set-val BOOTDELAY 0 \
  --set-str BOOTCOMMAND 'sysboot mmc 0:2 any ${pxefile_addr_r} /boot/extlinux/extlinux.conf; setenv bootargs root=/dev/mmcblk0p2 rootwait rw noinitrd console=tty0 gpt; ext4load mmc 0:2 ${kernel_addr_r} /boot/zImage; ext4load mmc 0:2 ${fdt_addr_r} /boot/dtbs/rk3288-veyron-mickey.dtb; bootz ${kernel_addr_r} - ${fdt_addr_r}; sleep 2; reset'
make O=b/mickey CROSS_COMPILE=arm-none-eabi- olddefconfig
make -j"$(nproc)" O=b/mickey CROSS_COMPILE=arm-none-eabi- all
  • The build ends with an error about __rel_dyn_start in the SPL link. Ignore it. b/mickey/u-boot-nodtb.bin and b/mickey/u-boot.dtb are built before that point.
  • POSITION_INDEPENDENT means U-Boot does not need to be linked for the exact address depthcharge loads it at (0x02000000). It only has to start on a 4 KB boundary, which the FIT script below ensures.
  • The boot command uses sysboot, which reads extlinux.conf. The standard bootflow scan does not find the root partition on this GPT. The ext4load/bootz part is only a fallback.

Step 4: wrap U-Boot in a FIT image the firmware accepts

depthcharge only accepts a FIT that looks like the stock ChromeOS kernel image: a kernel node of type kernel_noload with no hash, an fdt-veyron-mickey node with a sha1 hash, and a configuration named conf-veyron-mickey. Save this as mickey.its:

/dts-v1/;

/ {
	description = "Chrome OS kernel image with one or more FDT blobs";
	#address-cells = <1>;
	/* PADDING_PLACEHOLDER */
	images {
		kernel {
			description = "kernel";
			type = "kernel_noload";
			arch = "arm";
			os = "linux";
			compression = "none";
			load = <0>;
			entry = <0>;
			data = /incbin/("U_BOOT_BIN");
		};

		fdt-veyron-mickey {
			description = "rk3288-veyron-mickey.dtb";
			type = "flat_dt";
			arch = "arm";
			compression = "none";
			data = /incbin/("U_BOOT_DTB");
			hash {
				algo = "sha1";
			};
		};
	};

	configurations {
		default = "conf-veyron-mickey";

		conf-veyron-mickey {
			kernel = "kernel";
			fdt = "fdt-veyron-mickey";
		};
	};
};

The U-Boot binary must start exactly 4 KB into the FIT file. This script adds padding until it does. Save it as make-fit.py:

#!/usr/bin/env python3
# usage: make-fit.py <build-dir> <out.fit> <its-template>
import subprocess, sys, os
bd, out, tpl = sys.argv[1:4]
nodtb = open(f'{bd}/u-boot-nodtb.bin', 'rb').read()
dtb = open(f'{bd}/u-boot.dtb', 'rb').read()
binpath = out + '.u-boot.bin'
open(binpath, 'wb').write(nodtb + dtb)
src = (open(tpl).read()
       .replace('U_BOOT_BIN', os.path.abspath(binpath))
       .replace('U_BOOT_DTB', os.path.abspath(f'{bd}/u-boot.dtb')))
probe = (nodtb + dtb)[:256]

def build(n):
    pad = 'padding = [' + ' '.join(['00'] * n) + '];' if n else ''
    its = out + '.its'
    open(its, 'w').write(src.replace('/* PADDING_PLACEHOLDER */', pad))
    subprocess.run([f'{bd}/tools/mkimage', '-f', its, out],
                   capture_output=True, check=True)
    return open(out, 'rb').read().find(probe)

off = build(0)
n = 0x1000 - off
for _ in range(10):
    off = build(n)
    if off == 0x1000:
        break
    n += 0x1000 - off
assert off == 0x1000, off
print('FIT ok, padding', n)
python3 make-fit.py b/mickey out.fit mickey.its
scp out.fit alarm@<chromebit-ip>:

Step 5: sign and write it to the kernel partition

On the Chromebit, as root, still booted from USB:

cd /root && echo test > dummy.txt
vbutil_kernel --arch arm \
  --keyblock /root/devkeys/kernel.keyblock \
  --signprivate /root/devkeys/kernel_data_key.vbprivk \
  --version 1 --config dummy.txt \
  --vmlinuz /home/alarm/out.fit --bootloader dummy.txt --pack u-boot.kpart
vbutil_kernel --verify u-boot.kpart

dd if=u-boot.kpart of=/dev/mmcblk0 bs=512 seek=16384 conv=notrunc,fsync oflag=direct
dd if=/dev/mmcblk0 bs=512 skip=16384 count=$(( $(stat -c %s u-boot.kpart) / 512 )) | cmp - u-boot.kpart && echo FLASH_OK

Do not reboot unless it prints FLASH_OK. Then reboot and do not press anything. After the developer screen (30 s, or about 2 s with the optional step below) U-Boot starts, loads /boot/zImage and you get a login prompt on the screen and on the network.

Updating the kernel

pacman -Syu

An update only replaces /boot/zImage, /boot/dtbs/* and the modules. The kernel partition and extlinux.conf are not owned by any package, so nothing overwrites U-Boot.

Recovery

  • Hold the power button until the unit is off, power on, and press Ctrl+U at the developer screen to boot the USB system. Check the eMMC from there with the loop device in step 2.
  • Keep a copy of a working kernel partition: dd if=/dev/mmcblk0p1 of=p1-good.img bs=1M. Restoring it from USB is the dd ... seek=16384 command from step 5 with that image.
  • If you see the ChromeOS recovery screen ("Please remove all external devices to begin recovery"), do not follow it. Power off and use Ctrl+U instead. The development keys cannot boot from the real recovery path.
  • Never press Space and Enter on the developer warning screen. That disables developer mode and wipes the device.

Optional: shorten the developer screen from 30 s to about 2 s

The delay is a firmware flag (GBB DEV_SCREEN_SHORT_DELAY, 0x1) in the SPI flash chip, which is write protected by a screw and by the chip's status register. This step is optional and is the only one that touches the firmware.

1. Remove the write-protect screw

The iFixit teardown of the CS10 shows it. After a reboot, crossystem wpsw_cur should print 0.

2. Back up the chip twice and compare

Copy spi1.bin somewhere safe afterwards.

pacman -S flashrom
flashrom -p linux_mtd -r spi1.bin
flashrom -p linux_mtd -r spi2.bin
cmp spi1.bin spi2.bin && echo BACKUP_OK

3. Unlock through spidev

flashrom -p linux_mtd --wp-disable fails on this chip, but the raw SPI route works:

modprobe spidev
echo spi2.0 > /sys/bus/spi/drivers/spi-nor/unbind
echo spidev > /sys/bus/spi/devices/spi2.0/driver_override
echo spi2.0 > /sys/bus/spi/drivers/spidev/bind
FR="flashrom -p linux_spi:dev=/dev/spidev2.0,spispeed=8000"
$FR --wp-disable
$FR --wp-range 0,0           # both commands are needed
$FR --wp-status              # expect: range none, mode disabled

4. Set the flag, check it, write it

Work on a copy of the backup, and check it before writing:

cp spi1.bin spi-new.bin
gbb_utility --set --flags=0x1 spi-new.bin
gbb_utility --get --flags spi-new.bin        # 0x00000001
cmp -l spi1.bin spi-new.bin | wc -l          # expect 1
$FR -r spi3.bin && cmp spi1.bin spi3.bin     # chip still equals the backup
$FR -w spi-new.bin                           # must end with VERIFIED

5. Optional: restore protection

Re-enable protection with $FR --wp-range 0x0,0x200000 --wp-enable and refit the screw.

On the next boot the developer screen lasts about 2 seconds and then the eMMC boots by itself.

Tested on

One CS10 with firmware Google_Veyron_Mickey.6588.168.0 and U-Boot built from master (2026.10): boot through extlinux.conf, pacman -Syu and a forced reinstall of linux-armv7 with the kernel partition left untouched, and the shortened developer screen. Not tested: a real kernel version upgrade, installing from a wiped disk in exactly this order, and other veyron boards. If you try it on something else, the useful details to report are the firmware version shown on the recovery screen and whether the veyron_init skip was needed.