March 2, 2014

Installation of Arch Linux ARM on QNAP TS-212


Goal

This guide details the installation of Arch Linux ARM on QNAP TS-212 and keep the system running with minimal effort.

Although not tested, it should work on every TS-21x/TS-22x with minimal adjustments.

You should pause here and read every bit of information over at Debian on QNAP TS-21x/TS-22x, as it is a formidable source of information. Many thanks to Martin Michlmayr for all the work. Also, he details particular steps as how to backup and restore your device's flash, which aren't covered here.

I chose not to include extra configuration steps like hard drives power management, time synchronisation and software RAID (just to name a few), as they are not specific to the hardware in question and are well documented elsewhere.

I'm in no way liable for any bricking, damage, etc. you inflict on your devices; execute at your own risk. Also, and in case you're wondering, this procedure will most surely void your device's warranty.

Please feel free to drop a comment if you have any correction or suggestion to this guide.

Hardware

CPUARMv5TE single core Marvell Feroceon (Rev 1) running at 1.2Ghz, on Marvell DB-88F6281A-BP LE (Kirkwood System-on-Chip MV88F6281, Rev 3)
DRAM256MB DDRII
Boot LoaderU-Boot 1.1.4 (March 2nd, 2011, 14:34:42 - Marvell version is 3.4.4)
Network1x Gigabit RJ-45 ethernet port
LEDsUSB, Status, HDD1, HDD2, LAN, Power
USB Ports3x USB 2.0
ButtonsPower, One Touch Copy, Reset
SATA2x 3.5" SATA I/II interfaces
Fan1x 6cm, 12V DC
Buzzer1x
Serial Port1x 3.3V TTL with JST PHR-4 female connector
Flash Memory16 MB Serial NOR Micron ST M25P128, layed out in the following style:
# MTD                Size in
Block Starts at  Bytes Long Words Contains
    0 0xF8000000  512K 0x020000   U-Boot
    4 0xF8080000  256K 0x010000   U-Boot Config
    5 0xF80C0000 1280K 0x050000   NAS Config
    1 0xF8200000    2M 0x080000   Kernel
    2 0xF8400000    9M 0x240000   RootFS1
    3 0xF8D00000    3M 0x0C0000   RootFS2

Considerations

To keep the energy consumption as low as possible, the hard drives should be sleeping whenever not in use. To minimise its usage, root file system will be put on an USB stick.

As 256MB of RAM is not really enough, swap will be necessary and will be put on the same USB stick.

Arch Linux ARM's Kirkwood kernel image is bigger that the original 2M partition. This means that the kernel will be housed in the 9MB partition.

This particular U-boot version lacks the capability of loading files from USB, so the kernel will be loaded from flash memory. As such, kernel must be flashed every time it changes.

The flashing procedure should be done synchronously with the "pacman -S" procedure, either in a mkinitcpio build hook script (which Arch Linux ARM kernels don't execute), a kernel-install script (idem) or using a pacman hook feature (still nonexistent).

One valid option is to use some file system event monitor (inotify-tools, for instance), wait for /boot/uImage to be modified and then act on it. This isn't a good choice, as one should see the flashing actually being executed.

The chosen solution is to "intercept" pacman's execution with a /usr/local/bin/pacman script, which will call /usr/bin/pacman and, upon its successful completion and asserting that uImage differ from flash contents, flash the kernel image.

Assumptions

You:
  • Know your way around Arch Linux, U-Boot, particularly tftp, char and block devices, fdisk, mkfs, mkswap, tar, fstab, screen and bash scripting;
  • Read Debian on QNAP TS-21x/TS-22x.
You have:
  • A QNAP TS-212, in any state (bootable into U-Boot), diskless;
  • A 3.3V TTL serial cable, equiped with a JST PHR-4 male connector. I use a TTL-to-USB "Cygnal Integrated Products, Inc. CP210x UART Bridge", patched with an old CDROM audio cable;
  • An USB stick, minimum 4GB. As USB support varies, you should use a branded one - and fast, too. I use a SanDisk Cruzer Fit 4GB, as it is unobtrusive;
  • A networked Linux PC with a TFTP server and terminal multiplexer with serial support (tftp-hpa and screen, for instance). I use Arch Linux x86-64;
  • A network with DHCP and no (valid) BOOTP.

Steps

On the Linux PC, act as root from now on, so:
$ sudo -i

Install needed packages:
# pacman -S tftp-hpa screen

Ensure the TFTP server is running:
# systemctl start tftpd.socket tftpd.service

Insert the USB stick and unmount every auto mounted partition:
# umount /dev/sdX?
Of course X must be replaced by the letter Linux generated for the particular device...

~1GB should be enough swap; repartition the USB stick:
# fdisk /dev/sdX

Welcome to fdisk (util-linux x.xx.x).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Command (m for help): o
Created a new DOS disklabel with disk identifier 0xe34cf4eg.

Command (m for help): n

Partition type:
   p   primary (0 primary, 0 extended, 4 free)
   e   extended
Select (default p): 

Using default response p.
Partition number (1-4, default 1): 
First sector (2048-7821311, default 2048): 
Last sector, +sectors or +size{K,M,G,T,P} (2048-7821311, default 7821311): +2700M

Created a new partition 1 of type 'Linux' and of size 2.7 GiB.

Command (m for help): n

Partition type:
   p   primary (1 primary, 0 extended, 3 free)
   e   extended
Select (default p): 

Using default response p.
Partition number (2-4, default 2): 
First sector (5531648-7821311, default 5531648): 
Last sector, +sectors or +size{K,M,G,T,P} (5531648-7821311, default 7821311): 

Created a new partition 2 of type 'Linux' and of size 1.1 GiB.

Command (m for help): t
Partition number (1,2, default 2): 
Hex code (type L to list all codes): 82

Changed type of partition 'Linux' to 'Linux swap / Solaris'.

Command (m for help): p
Disk /dev/sdX: 3.7 GiB, 4004511744 bytes, 7821312 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xgggggggg

Device    Boot     Start       End  Blocks  Id System
/dev/sdX1           2048   5531647 2764800  83 Linux
/dev/sdX2        5531648   7821311 1144832  82 Linux swap / Solaris

Command (m for help): w

The partition table has been altered.
Calling ioctl() to re-read partition table.
Copy & paste the disk identifier into a text file, as it will be needed ahead.

Now create "usbroot" ext4 file system in first partition and "usbswap" swap in second partition:
# mkfs.ext4 -m 1 -L usbroot /dev/sdX1

# mkswap -L usbswap /dev/sdX2

Mount the "usbroot" partition, download the root filesystem, extract it and copy the kernel container uImage to the TFTP server root:
# mount -o noatime /dev/sdX1 /mnt

# curl -OL http://archlinuxarm.org/os/ArchLinuxARM-kirkwood-latest.tar.gz

# tar -xzvC/mnt -fArchLinuxARM-kirkwood-latest.tar.gz

# cp /mnt/boot/uImage /srv/tftp/

Default fstab is empty; fill it with the partitions you created earlier:
# cat >> /mnt/etc/fstab << EOF
LABEL=usbroot /    ext4 noatime  0 1
LABEL=usbswap none swap defaults 0 0
EOF

As USB is a rather slow medium, swap usage should be minimised. Also, Linux's memory compaction doesn't work all that well out-of-the-box, so you must take an extra step to prevent future page allocation failures:
# cat > /mnt/etc/sysctl.d/90-vm.conf << EOF
vm.swappiness=5
vm.min_free_kbytes=16184
EOF

Kernel will be written to flash from Linux, so "mtdblock" module is necessary:
# cat > /mnt/etc/modules-load.d/mtdblock.conf << EOF
mtdblock
EOF

As previously explained, pacman calls will be "intercepted" by the following script:
# cat > /mnt/usr/local/bin/pacman << EOF
#!/bin/bash

/usr/bin/pacman "\$@" || exit \$?

/usr/local/bin/flash-kernel
EOF

# chmod 755 /mnt/usr/local/bin/pacman

yaourt and any other similar tool you intend to use will need the same treatment.

And this one will flash the kernel:
# cat > /mnt/usr/local/bin/flash-kernel << EOF
#!/bin/bash

if [ "\$('id' -u)" != "0" ]; then
  echo -n "\$('basename' \$0): This script must be executed with root privileges! "
  command -v sudo > /dev/null 2>&1 || { echo >&2 "However, sudo is not installed. Aborting!"; exit 1; }
  exec sudo -E "\$0" \${1+"\$@"}
fi

[ -f /boot/uImage ] || { echo >&2 "\$('basename' \$0): No kernel (/boot/uImage) installed. Aborting!"; exit 1; }

modprobe mtdblock > /dev/null 2>&1
[ -b /dev/mtdblock2 ] || { echo >&2 "\$('basename' \$0): No MTD (/dev/mtdblock2) available. Aborting!"; exit 1; }

[ "\$('file' -sb /boot/uImage)" != "\$('file' -sb /dev/mtdblock2)" ] || exit 0

echo -n "\$('basename' \$0): Flashing /boot/uImage to /dev/mtdblock2... "
cat /boot/uImage > /dev/mtdblock2
if [ "\$?" != "0" ]; then
  echo >&2 "Your system could be unusable!!! Please flash the kernel again!!!"
  exit 1
else
  echo "Ok!"
  /usr/local/bin/backup-mtd
fi
EOF

# chmod 755 /mnt/usr/local/bin/flash-kernel

It is recommended to keep at all times a backup copy of the flash contents:
# cat > /mnt/usr/local/bin/backup-mtd << EOF
#!/bin/bash

BACKUP_KEEP_AT_MOST=5
BACKUP_DIRECTORY=/var/backups
BACKUP_SUBDIRECTORY=mtdblock

if [ "\$('id' -u)" != "0" ]; then
  echo -n "\$('basename' \$0): This script must be executed with root privileges! "
  command -v sudo > /dev/null 2>&1 || { echo >&2 "However, sudo is not installed. Aborting!"; exit 1; }
  exec sudo -E "\$0" \${1+"\$@"}
fi

modprobe mtdblock > /dev/null 2>&1
[ -b /dev/mtdblock2 ] || { echo >&2 "\$('basename' \$0): No MTD (/dev/mtdblock*) available. Aborting!"; exit 1; }

TIME=\$(date +"%Y-%m-%d_%H-%M-%S")

rm -fr \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/\$TIME > /dev/null 2>&1
mkdir -p \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/\$TIME > /dev/null 2>&1

echo "\$('basename' \$0): Backing up /dev/mtdblock* partition(s) to \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/\$TIME..."
echo "Generated at \$TIME. Contents are as follows:" > \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/\$TIME/description

for PARTITION in \$('ls' -dtr1 /dev/mtdblock*); do
  FILE=\$('basename' \$PARTITION)

  echo -n "\$FILE... "
  cat \$PARTITION > \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/\$TIME/\$FILE
  echo "Ok!"

  echo -n "\$FILE: " >> \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/\$TIME/description
  'file' -b \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/\$TIME/\$FILE >> \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/\$TIME/description
done

echo -n "Keeping only last \$BACKUP_KEEP_AT_MOST backups... "
'ls' -dtr1 \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/*/ | 'head' -n -\$BACKUP_KEEP_AT_MOST | 'xargs' rm -fr
echo "Ok! Current backups are:"
'ls' -dtr1 \$BACKUP_DIRECTORY/\$BACKUP_SUBDIRECTORY/*/ | sort
echo "Done!"
EOF

# chmod 755 /mnt/usr/local/bin/backup-mtd

Unmount the USB stick and insert it in the TS-212.

Power on the TS-212 while holding the reset button until it beeps twice (approximately 8 seconds - this instructs it to enter recovery mode). It will beep once more after a while (meaning it couldn't get a BOOTP response - which is intended).

Connect the serial cable and start the terminal multiplexer on the serial port with 115200/8-N-1:
# screen /dev/ttyUSB0 115200 8N1

With these kind of devices, the NIC's MAC address is set by U-Boot from its configuration. While this is printed on a sticker on the board right next to the power button, it's easier to copy & paste it to a text file from:
Marvell>> printenv
The value is stored in the "ethaddr" variable; copy & paste it onto the text file.

When booting, U-Boot waits only a second for a key press on the serial port to stop automatic boot. Right after issuing the next commands, repeatedly press space until you are on the U-Boot prompt ("Marvell>>"):
Marvell>> resetenv
Marvell>> reset

Reset the MAC address with the value you kept earlier:
Marvell>> setenv ethaddr 'GG:GG:GG:GG:GG:GG'

Now issue:
Marvell>> setenv mainlineLinux 'yes'
Marvell>> setenv arcNumber '2139'
This will make U-Boot inform the kernel which hardware it is booting on.

Booting with "root=/dev/sdX1" isn't feasible, as the X will be volatile from the moment that an hard disk is added to the system. To overcome that, "root=PARTUUID=" will be used:
Marvell>> setenv bootargs 'console=ttyS0,115200 root=PARTUUID=GGGGGGGG-01 rootfstype=ext4 rootwait rw'
Replace "GGGGGGGG" with the disk identifier you kept on your text file (minus the initial "0x").

Automatic boot consists on loading the kernel from flash partition #2, and starting it:
Marvell>> setenv bootcmd 'cp.l 0xF8400000 0x800000 0x240000;bootm 0x800000'

Finally, save the U-Boot variables, reboot and stop automatic boot when instructed:
Marvell>> saveenv
Marvell>> reset

At this point, kernel isn't yet lodged on the flash, so it must be loaded from the TFTP server. Connect the TS-212 to the network if it isn't already.

Select a free IP address on your network and set it with:
Marvell>> set ipaddr '192.168.0.303'
This address is temporary; it will only be used by the TFTP client.

Set the IP address of the TFTP server with:
Marvell>> setenv serverip '192.168.0.302'

Load the kernel from TFTP and run it with:
Marvell>> tftp 0x800000 uImage;bootm 0x800000

When greeted by getty, log on as root; default password is "root" - you should change it now.

Graphical interface is unnecessary:
# systemctl set-default multi-user.target

Upgrade the system:
# pacman -Suyy

It the previous update didn't flash a kernel, flash it now:
# flash-kernel
Please be advised that this procedure should take over three minutes.

A backup of the flash will be created every time a new kernel is flashed.

qcontrol is a package that controls LEDs, buzzer, buttons and, more important, the fan. Install it:
# pacman -S qcontrol

Assign the right configuration:
# ln -s /etc/qcontrol/ts219.lua /etc/qcontrol.conf

Enable it and reboot:
# systemctl enable qcontrold.socket qcontrol.service

# reboot

Done!

Final Words

Kernel loading should output a "rtc-mv rtc-mv: internal RTC not ticking" message. This can safely be ignored, as this board uses Seiko Instruments S-35390A (rtc-s35390a) to keep the time - and it will be registered as /dev/rtc.

When installing a new kernel, depmod outputs an error related to hci_vhci module. That can also be ignored.