Reboot-to-bootloader: enter <RealTek> prompt from Linux¶
Overview¶
A single command from Linux SSH reboots the gateway into the <RealTek>
bootloader prompt, ready for TFTP firmware updates — no need to press
ESC on the serial console.
The boothold binary writes the HOLD magic word to a fixed location in
DRAM and exits. The flag is one-shot: the bootloader clears it
before entering download mode, so the next reboot boots Linux normally.
How it works¶
The mechanism uses a magic word in DRAM that survives the
watchdog reset triggered by reboot. No flash writes are involved.
- Linux writes
0x484F4C44("HOLD") to physical address0x01FFEFFCvia/dev/mem. - Linux triggers
reboot, which causes a watchdog reset. - The CPU restarts at
BFC00000(flash reset vector). The btcode re-initialises the DDR controller, but DRAM cell contents survive because the DDR2 retention time (~64-256 ms) exceeds the re-init delay (~1-2 ms). - The stage-2 bootloader checks
0xA1FFEFFC(KSEG1 uncached alias of physical0x01FFEFFC) for the magic word. - If it matches, the bootloader clears it and enters
download mode (
goToDownMode()). - If it doesn't match (normal boot, cold power-on), the bootloader proceeds to load and boot the kernel as usual.
A full power cycle (disconnect all cables) clears DRAM and restores normal boot.
Optional TFTP server-IP handoff (bootloader V2.7+)¶
boothold <A.B.C.D> writes an additional IP record into the same
reserved page, just below the HOLD magic:
| Phys / KSEG1 addr | Contents |
|---|---|
0x01FFEFFC / 0xA1FFEFFC |
HOLD magic 0x484F4C44 ("HOLD") |
0x01FFEFF8 / 0xA1FFEFF8 |
IP marker magic 0x49505634 ("IPV4") |
0x01FFEFF4 / 0xA1FFEFF4 |
IPv4 packed as (a<<24)|(b<<16)|(c<<8)|d |
When the bootloader sees a valid HOLD, it also checks the IP marker; if
present it uses the packed IPv4 as its download-mode TFTP server address
(see tftpd_entry() / g_tftp_server_ip), then clears all three words
(one-shot). The IP is honoured only alongside a valid HOLD — i.e. a
deliberate warm reboot from a running Linux. On a cold boot the page
holds garbage, the marker will not match, and the bootloader keeps its
compiled default (192.168.1.6). flash_remote.sh passes its BOOT_IP
this way so the gateway comes up on the expected address with no serial
IPCONFIG. Words are written value-first, marker-last, so the marker is
never valid over a stale value. Endianness: the SoC is big-endian and
boothold writes via htonl(), so the bootloader reads the words as
plain unsigned long.
Boot flow with boot-hold¶
setClkInitConsole()
initHeap()
initInterrupt()
initFlash()
showBoardInfo()
← check BOOTHOLD_RAM[0]
if match → clear, goToDownMode(), return
check_image()
doBooting()
DRAM address selection¶
Why 0x01FFEFFC (high DRAM, one page below the btcode stack)¶
The flag must satisfy three constraints:
- The bootloader (btcode + stage-2) must not write to it during early boot — otherwise it gets clobbered before the HOLD check runs.
- The Linux kernel must not write to it while running, nor during
shutdown / reboot — otherwise the magic written by
bootholdis lost before the watchdog reset takes effect. - The bootloader's HOLD check (KSEG1 uncached read) must agree with whatever path Linux uses to write it — otherwise cache coherency bites.
The page 0x01FFE000–0x01FFEFFF (4 KB at 32 MB − 8 KB) sits in a
narrow but reliable window:
- Above the kernel image, kernel BSS/data, and any low-DRAM
scratch the kernel touches during early init. The kernel is loaded
at phys
0x00500000and never grows past ~24 MB even with maximum rootfs/userdata cache pressure. - Below the btcode stack, which initialises at top-of-RAM
(
0x82000000) and grows down through0x81FFFFFF(= phys0x01FFFFFC). In practice the stack uses well under 4 KB, so the page just below the stack page is untouched. - Reserved in the device tree as
reserved-memorywithno-map, so the kernel page allocator skips it and there's no MMU mapping — no KSEG0/KSEG1 coherency conflict, no cache aliasing risk.
Why not 0x003FFFFC (the v2.x location)?¶
v2.x firmware (Linux 5.10) put the flag at 0x003FFFFC, near the
bottom of DRAM. This was reliable on the 5.10 kernel. Starting
with Linux 6.18 (v3.0.0+) it became unreliable:
| Stack | HOLD address | boothold && reboot reliability |
|---|---|---|
| v2.1.6 (Linux 5.10) | 0x003FFFFC | 30/30 = 100% |
| v3.0.0 (Linux 6.18) | 0x003FFFFC | 26/30 = 87% |
| v3.1.1 (Linux 6.18) | 0x003FFFFC | 22/30 = 73% |
| v3.2.0 (Linux 6.18) | 0x01FFEFFC | 30/30 = 100% |
On failed boots, the bootloader read either zero or kernel-code-like
values (e.g. 0xFFFFFFFC, 0x00001000) — symptoms of the kernel
scribbling low DRAM before the reserved-memory no-map declaration
takes effect. The 5.10 → 6.18 jump (with its accompanying toolchain
refresh) changed the early-init memory access patterns; the bisection
in the v3.2.0 fix narrowed the regression to that kernel transition.
Moving the flag to high DRAM eliminates the conflict regardless of which exact path the kernel takes through low memory during boot or shutdown.
Address safety map¶
| Region | Address range | Status |
|---|---|---|
| Exception vectors | 0x80000000 – 0x800001FF |
Avoid |
| DDR calibration | low DRAM scratch | Avoid |
| DDR size detection | power-of-2 offsets in 0xA0000000 window |
Avoid |
| Stage-1.5 piggy decompressor scratch | 0x80100000+ |
Avoid |
| LZMA workspace | 0x80300000 |
Avoid |
| Kernel image (loaded by btcode) | 0x80500000 – ~0x81E00000 |
Avoid |
| Boot-hold flag | 0x81FFEFFC (KSEG0) |
Used |
| btcode stack page (grows down from top) | 0x81FFF000 – 0x81FFFFFF |
Avoid |
Bootloader implementation¶
In boot/main.c, at file scope:
#define BOOTHOLD_MAGIC 0x484F4C44 /* "HOLD" */
#define BOOTHOLD_RAM ((volatile unsigned long *)0xA1FFEFFC)
In start_kernel(), after showBoardInfo():
if (BOOTHOLD_RAM[0] == BOOTHOLD_MAGIC) {
BOOTHOLD_RAM[0] = 0;
prom_printf("---Boot hold requested\n");
goToDownMode();
return;
}
The pointer uses KSEG1 (0xA1FFEFFC), not KSEG0 (0x81FFEFFC), so
both the read and the clear bypass the L1 D-cache and go directly to
DRAM. Without this, the clear (write 0) could stay in the cache and
be lost on power cycle, producing a false boot-hold on every cold
boot.
Upgrading from v3.1.1 or earlier¶
The HOLD address changed in v3.2.0 (bootloader V2.6). Bootloader and
boothold binary must be upgraded together; a mismatch (old bootloader
+ new boothold, or vice versa) leaves boothold && reboot
non-functional. Always upgrade via fullflash
(./flash_install_rtl8196e.sh -y <gateway-IP>) when crossing the
v3.1.1 → v3.2.0 boundary. Per-partition upgrade across this boundary
is unsupported.
Kernel device tree reservation¶
In arch/mips/boot/dts/realtek/rtl8196e.dts:
memory@0 {
device_type = "memory";
reg = <0x00000000 0x02000000>; /* 32MB */
};
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
boothold@1ffe000 {
reg = <0x01FFE000 0x1000>; /* 4KB reserved for boothold flag */
no-map;
};
};
The no-map property removes this page from memblock before the page
allocator starts — no runtime cost, no exception handling. On MIPS,
KSEG0/KSEG1 are hardware-mapped (no TLB), so /dev/mem access still
works. The 4 KB cost (0.01% of 32 MB) is negligible.
The kernel boot log confirms the reservation:
Linux-side usage¶
With boothold binary (recommended)¶
The boothold binary (installed in /usr/bin/) uses pwrite() on
/dev/mem to write the HOLD magic to physical address 0x01FFEFFC.
Manual / one-liner alternative¶
This works because the address is in a reserved-memory no-map page —
the kernel never caches or touches it from the page allocator side.
Experimental results¶
Tested on the Lidl Silvercrest gateway (RTL8196E, 32 MB DDR2):
| Test | Result |
|---|---|
| DRAM retention across watchdog reset | Survives — magic at 0x01FFEFFC preserved |
| Boot-hold from Linux SSH on Linux 6.18 | 30/30 = 100% at the new address |
| One-shot behavior (subsequent reboot) | Works — flag is cleared, Linux boots normally |
| Full power cycle (disconnect all cables) | Flag cleared — DRAM lost, normal boot |
Design alternatives considered¶
Top of DRAM (last page, 0x01FFF000–0x01FFFFFF)¶
Rejected: the btcode initialises the stack pointer at 0x82000000 and
pushes data into the page at 0x81FFFFFC during DDR calibration on
every boot (including cold power-on), producing false HOLD detections.
The page one below (the current location) is far enough that even deep
stack frames don't reach it.
Low DRAM (0x003FFFFC, the v2.x location)¶
Rejected on v3.x: see the regression analysis above. Unreliable on
Linux 6.18 due to the kernel scribbling low DRAM during early init or
shutdown, before the no-map reservation is enforced.
Flash-based flag¶
Write a 4-byte magic to flash (e.g. last sector of mtd0), bootloader reads and clears it via sector read-modify-write. Guaranteed to survive any DRAM corruption but causes one flash erase+write cycle per use — needless wear when DRAM works reliably at the new address.
Not implemented.