Platform

  • SoC: Amlogic S905H, codenamed gxbb_p200
    Many features provided by the SoC are not utilized, however:
    • LAN: The SoC provides a 100Mb RGMII MAC, but no PHY is present on board
    • SD: The SoC provides a SDIO host controller, which supports Wi-FI and SD card, but only Wi-Fi is present on board
  • RAM: DDR3 1GiB (2 * 4Gb/512MiB chips)
  • EMMC: 4GiB
  • Bootloader: U-Boot (Modified by Xiaomi from the Amlogic fork, double modification, yeah)

Boot process

1. SoC initializes the device

  1. Basic platform information is gathered, along with those hard-coded into the SoC
  2. Memory is detected but not allocated, SoC labels itself as platform_codename_ramsize, i.e. gxbb_p200_1g

2. SoC initializes the memory

  1. If either one of the following conditions is not met, the SoC resets:
    • The memory chips must be of the specific type the SoC supports, i.e. DDR3
    • Multiple memory chips on the same memory channel must be of the same type, there’s only one memory channel though.
  2. Memory is not actually allocated yet, the SoC knows there’s memory but does not really know how to handle it in the fancier way

3. SoC tries to load bootloader

  1. Unlike SD on the mmc bus, the emmc bus is always activated.
  2. If it fails to find the emmc device under the emmc bus, if falls back to USB burning mode
  3. Else, if it can find the emmc device, if then tries to read the first 4MiB of the emmc device into memory address 0x1000000
  4. It loads whatever is at 0x1000000, and if it fails to run that, if falls back to USB burning mode

4. Bootloader (U-boot) initializes the memory

  1. Some address are hardcoded into the bootloader, these are recorded as 128 bit integers, which includes:
    • 0x0000000001000000, the address to load the device tree blob. Unlike the standard Amlogic S905 devices, which is usually 0x0000000001040000 and will read it from the default environment.
  2. I/O are mapped to the last part of the memory, which would be different if the RAM size differs (1GB -> 2GB). On the 1GB model, this would be range 0x3F000000 - 0x3FFFFFFF.
    • Canvas is mapped to 0x3F800000, for example

5. Bootloader (U-boot) reads dtb from reserved partition on emmc

(Unlike standard, user-changable devices, embedded devices like this tend to not waste any space on mainstream partition tables. Partitions is defined byte-to-byte when flashing a image, and then recorded on a hard-coded location on emmc)

  1. Unlike standard Amlogic gxbb devices, dtb location address on emmc is 32M smaller as a result of Xiaomi’s modification of U-boot mentioned earlier.
  2. dtb is read from 4M offset in the reserved partition, therefore the emmc offsets are respectively:
    • mibox3: 8M = 4M(reserved offset)+4M(dtb offset)
    • standard: 40M = 36M(reserved offset)+4M(dtb offset)
  3. Amlogic has a multiple-dtb format, this has extra head and tail. Multiple or not, dtbs are stored in two identical 256K copies to avoid data corruption
  4. If a multi-dtb is found, bootloader will choose the one meets the platform specification, namely SoC codename and RAM size, i.e. gxbb_p200_1g

    • amlogic_dt_id must be defined in dtb, otherwise it will be ignored
    • amlogic_dt_id must be gxb_p200_1g, otherwise it will be read but bootloader will either not choose it (from multiple dtbs) or refuse to use it (from single dtb)
  5. If no dtb is available, the bootloader falls back to u-boot shell.

6. Bootloader (U-boot) reads the partiton table from reserved partition on emmc

  1. As always as Xiaomi would do, they defined the gap between the bootloader and reserved partition as 0 unlike the original Amlogic solution, this moves the reserved partition offset to 4M, instead of standard 36M since by default the gap should be 32M.
  2. It tries to read the 2 copies of partition table from LBA 0x2000 and 0x2001, which is actually 0x400000 and 0x400200 since each LBA is 512 Bytes (0x200), the location is exactly 4M and 4M + 0.5K
  3. These 2 tables are compared side-by-side to determine if there’s any error
  4. If there’s any error, the device falls back to u-boot shell

7. Bootloader (U-boot) reads environment from env partition on emmc

  1. It reads the location of env partition from the partition table,
    • If there’s no env partition, it will use the default envs defined in the bootloader itself.
    • If there’s the env partition, it will reads it as a whole into memory
  2. The envs as a whole is then CRC verified to make sure there’s no error.
  3. If the CRC checksum failed, it will use the default envs.

8. Bootloader initialize several devices with the args defined in env

  1. stdin/stdout/stderr of u-boot itself are remapped if there’s any arg redefining them
  2. Video output is initialized using the resolution/framerate/color mode, by default it is 1080p60

9. Bootloader runs the bootcmd arg defined in env.

  1. Usually the bootcmd will loads kernel into specific memory address, and also loads dtb if neccessary, then run bootm to run the kernel loaded at the defined address.
  2. Unlike standard gxbb devices, though, dtb_mem_addr is not defined in env by default, so you need to either set it in the env or include it in the bootcmd, otherwise standard side-load scripts will fail since they rely on an already defined dtb_mem_addr
    • The dtb_mem_addr is different from the standard 0x1040000, it is modifed to 0x1000000 by Xiaomi

10. Kernel reads dtb, and initializes

  1. If not custom dtb is read, the kernel will of course use the one read in step#5
  2. It loads built-in drivers depending on device tree.
    • If the kernel is linux-Amlogic, the emmc partition and dtb virtual block will be initialized by Amlogic’s mmc driver, this will fail on official kernels however, thanks to Xiaomi’s modifications mentioned in step#5 and step#6
  3. It runs init

11. Init handles mount and other stuffs.

  1. Root partition is usually harded coded in init, e.g. strictly /dev/system as / for Android, /dev/system or LABEL=COREELEC as /flash + **/flash/SYSTEM as / **for CoreELEC.
    • The root partition mount source is therefore modifiable

Modification

1. Kernel (linux-amlogic)

For linux-amlogic to properly run, the mmc driver must be patched to meet Xiaomi’s modification:

  1. This patch will allow you to properly read the partition table from emmc
  2. This patch will allow you to properly initialize the dtb virtual block.

The mmc driver must be built-in(Y) instead of loadable(M) if you want to boot from emmc, since modules sitted in system partition on emmc is impossible to load as the partition tables won’t even be readable. It is the default behaviour when you compile CoreELEC/EmuELEC.

2. Device tree

  1. This device tree mimics the partition layouts of the stock firmware, the data partition will only be 1.7GB
  2. This device tree removes unrelated partitions that are only usable in Xiaomi’s stock image. The only existing boot and system partition are as small as possible, 0 bytes are wasted for Xiaomi’s nonsense, even boot logo is omitted. the data partition will be 3.4GB

Note that the partitions node in devide tree will be used by u-boot when flashing an image via USB Burning Tool, so the second device tree will only make sense if you craft a customized USB burning image.

3. Init

To mount root directly from system partition, init must be modified. This init script will mount /dev/system as /, instead of /dev/system as /flash + /flash/SYSTEM as /, which wastes a ton of space

Image

1. USB Burning

Amlogic provides a handy USB Burning Tool to allow you flash a image file to an Amlogic device via the USB update mode.
To force mibox3 into USB update mode, you need to short the emmc chip so the SoC can not find it. Technically you need to short the 5th and 6th pin of it to block the communication, but Xiaomi wired 2 test spots on the board, you can conviniently short them with a tweezer, just do as follows:

  1. Install Amlogic USB Burning Tool, v2.0.8 is recommended since it is what I use
  2. Take your mibox 3 apart
  3. Connect the board with your PC via a USB-A to USB-A cable
  4. Short the two copper test spots near the sheild and the IR senser on the back of the board
  5. Plug the power

2. Customization

The usb burning tool image is unpackable and repackable. You mainly need aml_image_packer to unpack and repack it, the unpacked .PARTITION files are those you want to modify.

.PARTITION files are Android sparse img, you want to use img2simg to convert between it and raw disk image.

If you want to play it dangerously, you can even make a CoreELEC/EmuELEC/Armbian/OpenWrt flashable image, the USB_Burning_Tool.img.xz in this release is a minimum overhead image, with only boot, system and data partitions.

Basically you convert compiled kernel and system images to sparse image, replace those in the unpacked stock image, and repack it. The size of data partition is hard to determine though, so you can either leave it small and resize it later, or flash and calculate then repack. The raw data partition itself can be created with simple truncate and mkfs.ext4, additionally you can populate the filesystem with -d argument

Check the following image.cfgs, notice how you can just delete an unwanted partition.

Stock

[LIST_NORMAL]
file="DDR.USB"          main_type="USB"         sub_type="DDR"  
file="UBOOT.USB"                main_type="USB"         sub_type="UBOOT"
file="aml_sdc_burn.ini"         main_type="ini"         sub_type="aml_sdc_burn"
file="meson1.dtb"               main_type="dtb"         sub_type="meson1"
file="platform.conf"            main_type="conf"                sub_type="platform"

[LIST_VERIFY]
file="boot.PARTITION"           main_type="PARTITION"           sub_type="boot"
file="bootloader.PARTITION"             main_type="PARTITION"           sub_type="bootloader"
file="data.PARTITION"           main_type="PARTITION"           sub_type="data"
file="logo.PARTITION"           main_type="PARTITION"           sub_type="logo"
file="recovery.PARTITION"               main_type="PARTITION"           sub_type="recovery"
file="system.PARTITION"         main_type="PARTITION"           sub_type="system"

Modified

[LIST_NORMAL]
file="DDR.USB"          main_type="USB"         sub_type="DDR"
file="UBOOT.USB"                main_type="USB"         sub_type="UBOOT"
file="aml_sdc_burn.ini"         main_type="ini"         sub_type="aml_sdc_burn"
file="meson1.dtb"               main_type="dtb"         sub_type="meson1"
file="platform.conf"            main_type="conf"                sub_type="platform"

[LIST_VERIFY]
file="boot.PARTITION"           main_type="PARTITION"           sub_type="boot"
file="bootloader.PARTITION"             main_type="PARTITION"           sub_type="bootloader"
file="data.PARTITION"           main_type="PARTITION"           sub_type="data"
file="system.PARTITION"         main_type="PARTITION"           sub_type="system"

Partition layout (stock)

id name        offset     size flag
 0 bootloader       0   400000    0  
 1 reserved    400000   800000    0  
 2 cache       c00000 10000000    2  
 3 env       10c00000   400000    0  
 4 logo      11000000   400000    1  
 5 recovery  11400000  2000000    1  
 6 misc      13400000  2000000    1  
 7 boot      15400000  1400000    1  
 8 system    16800000 40000000    1  
 9 backup    56800000 20000000    4  
10 persist   76800000   800000    4  
11 panic     77000000   400000    4  
12 data      77400000 74c00000    4    

These are what would be created during the flashing if you are using the stock image. Different flag has different meanings:

  • 0 Raw data partition, no filesystem, directly accessed
  • 1 Read-only partiton, maybe have filesystem, mostly kernel image
  • 2 RW partition, but not accessible to user
  • 4 RW partition, accessable to user

Default env

baudrate=115200
boardver=1
boot_from_flash=setenv bootargs ${bootargs} usb=${usbmode}; if imgread kernel boot ${loadaddr}; then bootm ${loadaddr}; fi;
bootcmd=run boot_from_flash
bootdelay=1
btmac=00:00:00:00:00:00
cmdline_keys=keyman init 0x1234; 
cvbsmode=576cvbs
display_bpp=24
display_color_bg=0
display_color_fg=0xffff
display_color_index=24
display_height=1080
display_layer=osd1
display_width=1920
factory_reset_poweroff_protect=if test ${wipe_data} = failed; then run recovery_from_flash;fi; if test ${wipe_cache} = failed; then run recovery_from_flash;fi; 
fb_addr=0x3f800000
fb_height=1080
fb_width=1920
fdt_high=0x20000000
firstboot=1
hdmimode=1080p60hz
init_display=hdmitx hpd;osd open;osd clear;vout output ${outputmode};imgread pic logo bootup $loadaddr;bmp display $bootup_offset;bmp scale
initargs=rootfstype=ramfs init=/init console=ttyS0,115200 no_console_suspend earlyprintk=aml-uart,0xc81004c0
loadaddr=1080000
outputmode=1080p60hz
preboot=checkbootinfo;run upgrade_check;run cmdline_keys;setkeys;run init_display;run storeargs;run switch_bootmode;run factory_reset_poweroff_protect;
recovery_from_flash=setenv bootargs ${bootargs} usb=otg; if imgread kernel recovery ${loadaddr}; then bootm ${loadaddr}; fi
recovery_from_udisk=setenv bootargs ${bootargs} usb=otg; if fatload usb 0 ${loadaddr} aml_autoscript; then autoscr ${loadaddr}; fi;if fatload usb 0 ${loadaddr} recovery.img; then bootm ${loadaddr};fi;
serialno=123456789ABCDEF
storeargs=setenv bootargs ${initargs} logo=${display_layer},loaded,${fb_addr},${outputmode} hdmimode=${hdmimode} cvbsmode=${cvbsmode} hdmitx=${cecconfig} androidboot.btmac=${btmac} androidboot.wifimac=${wifimac} androidboot.firstboot=${firstboot} androidboot.serialno=${serialno} ramoops.mem_address=0x20000000 ramoops.mem_size=0x100000 ramoops.record_size=0xc0000 ramoops.dump_oops=0 boardver=${boardver}; 
switch_bootmode=get_rebootmode; echo reboot_mode=${reboot_mode};setenv bootargs ${bootargs} reboot_mode=${reboot_mode};if test ${reboot_mode} = factory_reset; then run recovery_from_flash;fi;if monitor_bt_cmdline; then run recovery_from_flash; fi;if irdetect; then run recovery_from_flash; fi;
update=run recovery_from_flash;
upgrade_check=echo upgrade_step=${upgrade_step}; if itest ${upgrade_step} == 3; then run init_display; run storeargs; setenv upgrade_step 2; saveenv; run update;else if itest ${upgrade_step} == 1; then env default -a; setenv upgrade_step 2; saveenv;else if itest ${upgrade_step} == 4; then resetenv_x; setenv upgrade_step 2; saveenv;fi;fi;fi;
upgrade_step=2
usbmode=slave
wifimac=00:00:00:00:00:00
wipe_cache=successful
wipe_data=successful

As long as you have UART connection, all it needs is a simple env default -a to reset the envs. Otherwise, you may need to download the default env, and the env_default function in this script