Swapping an oscilloscope's boot logo took twenty minutes. Getting the oscilloscope to boot again took the rest of the night.
Four Allwinner boot blocks and one region that cannot be replaced. Everything that follows is about getting at the third line without ever writing to the fifth.
The FNIRSI 1014D is a cheap tablet-shaped oscilloscope built on an Allwinner F1C100s — an ARM926EJ-S with USB recovery burned into its boot ROM. Its boot logo lives in SPI flash as a raw RGB565 framebuffer with a 40-byte header.
Everything about that block is documented, thanks to pecostm32's reverse engineering. So the plan was two commands and a coffee.
The first thing the scope did was disagree with the documentation.
geometry, read from the chip's own header
bitmap 0x013000 eGON.BMP 0x13000 splash: 298x130 RGB565 pixel data at 0x13028, 77480 bytes block capacity 77784 bytes (304 bytes spare)
Every public source says this block is 0xE600 and the image
is 298 × 98. On this scope it is 0x13000 and 298 × 130. The extra
32 rows are the line reading "Firmware version: V3.0" — drawn into the logo. The
block varies by firmware version, and the published 1014D dump differs from this one in
1,566,094 of 2,097,152 bytes.
The tooling read it correctly only because it parses the header instead of trusting the table. That is the whole lesson of the first hour, and it was free.
To write flash you need FEL, the Allwinner boot ROM's USB recovery mode. Every guide says the same thing: put a boot stub on a microSD card, insert it, power on.
The 1014D has no card slot. It has one USB-A port and it shipped with a USB-A to USB-A cable, the first one i have ever seen.
It is not a mistake. That port is a device port wired straight to the SoC. And there is a card — soldered inside, holding saved screenshots, served over USB as mass storage when you pick "USB connection" in the scope's own menu. The boot ROM reads it before SPI flash. So the way in is to write the stub to the scope's internal storage, through the scope's own firmware, and then reboot into the recovery mode that replaces that firmware.
byte 8192 — sector 16 of the internal card
$ lsblk -o NAME,SIZE,TRAN,VENDOR,MODEL sdb 7.5G usb ADS1014D USB DisK Disk $ sudo dd if=fel-sdboot.sunxi of=/dev/sdb bs=1024 seek=8 # power cycle, and: Bus 001 Device 033: ID 1f3a:efe8 Allwinner sunxi SoC in FEL mode
The FAT partition starts at byte 32256, so 8 KiB at byte 8192 lands in dead space with 15,872 bytes to spare.
From there the actual job was undramatic. Two full reads of the chip, compared, hashed.
A new image built and diffed offline until it changed only the 19 pages of the bitmap block.
A partial write — spiflash-write 0x13000 rather than upstream's write-the-whole-chip
— so the calibration sectors were never even erased. Then a full read-back that matched the
intended file across all 2,097,152 bytes.
Twenty minutes. The logo was on the scope. Then we tried to give the scope back.
The stub had to come off the card, or the scope would boot into silent USB recovery
forever. Removing it is one dd — the exact inverse of putting it there.
Except that command needs the card to appear as mass storage. Mass storage is served by the scope application. And the scope application never runs, because the stub sends the boot ROM into FEL before the application loads.
The 8 KiB that let us in was the same 8 KiB that locked us out of the only mechanism that could remove it.
Nothing was damaged. Flash was correct and verified, calibration untouched, the SoC answering FEL on every power cycle. The scope was in perfect health and completely useless, held there by eight kilobytes on a card we could no longer reach.
The obvious fix is to boot the application some other way, from FEL, just once.
sunxi-fel spl patches an SPL to
return when it finishes, which is exactly what you don't want here.
ERROR -1: FEL's own exception vectors live in that SRAM. It
overwrote itself mid-transfer.
0x80000000 and jump.
readl came back all zeros — in a bare FEL session DRAM is not initialised,
so this was a jump into nothing.
Five approaches, each one plausible, each one wrong. Somewhere in there the realisation arrived that we were solving the wrong problem. We did not need the oscilloscope to work. We needed 512 bytes of zeros to reach one sector of a card.
Upstream's bootloader already brings up clocks, DRAM, the display and the SD card. It
links in sd_card_write() and never calls it. It also contains a dead branch —
the "load firmware from card" path, useless to us, several hundred bytes of it, sitting at a
known address.
So: disassemble, find sd_card_init at 0x13a4 and
sd_card_write at 0x1b84, confirm the second one is really the
writer by checking it issues MMC CMD24/CMD25 rather than
CMD17/CMD18, and hand-assemble a hundred bytes into the dead
branch.
the payload, assembled at 0x4CF4
bl sd_card_init ; bring up the card cmp r0, #0 bne FAIL ldr r4, =0x81CBBA90 ; the bootloader's own DRAM buffer mov r6, #8192 ; zero all of it str r5, [r4], #4 subs r6, r6, #4 bne ZLOOP mov r0, #16 ; sector 16 = byte 8192 mov r1, #16 ; 16 blocks = the whole stub bl sd_card_write cmp r0, #0 bne FAIL mov r0, #0x00FF00 ; green means it worked b PAINT FAIL: mov r0, #0xFF0000 PAINT: bl display_set_fg_color bl display_fill_rect HALT: b HALT
No UART on this thing, no debugger, one shot per power cycle. So the
program reports its result the only way available: it paints the entire 800 × 480
panel green if sd_card_write returned success, red if it didn't, and then
stops forever.
Two cache-enable calls got replaced with nop on the way past, so the zeros
would actually be in DRAM rather than parked in the D-cache when the card controller went
looking for them. The eGON checksum was recomputed. Then in it went.
The screen turned green. Power cycle, and the boot ROM found nothing on the card, fell through to SPI flash, and the scope came up with its new logo.
The stock logo is a 298 × 130 banner floating on an 800 × 480 panel. Having come that far it seemed a waste to stop at FNIRSI's dimensions.
The block can't grow — the application starts immediately behind it at
0x27000. But the chip has 1,151,488 bytes of nothing between the end of
the application and the calibration region, and a full-screen image needs 768,000. So put
the bitmap in the empty space and tell the bootloader where it went.
the SPL, disassembled — everything needed is here
0x50c mov r5, 0x80000000 app load address 0x510 mov sb, 0x81000000 splash buffer — 11.5 MB free above it 0x514 add r7, r5, 0x1b00000 framebuffer 0x548 mov r0, #0x13000 bitmap address — an immediate 0x75c .word 0x13028 pixel address — a literal
Two operands. It reads width and height from the block header and centres
with (800−w)/2, (480−h)/2, so an 800 × 480 image lands at the
origin with no scaling and no bars. Seven bytes changed in the SPL, counting the
checksum.
Bitmap first, bootloader last — until that last write the SPL still points at the old block, so an interruption anywhere leaves a scope that boots normally. It read back byte-identical and came up full screen.
0x100000, dithered to RGB565. Decoded back out of
the flash image, not a mockup.One artifact came free: the scope still shows the small logo when you power it
down. The application draws the splash too, and its own reference still points at
0x13000. Which is a good argument for having left that block alone.
| status | finding |
|---|---|
| verified | Splash geometry varies by firmware version, not model |
| verified | Both writes read back byte-identical across the full 2 MiB |
| corrected | No SD slot exists; every guide saying otherwise is wrong for this model |
| corrected | 0x1FD000 is calibration and live config — the firmware rewrote 0x1FD014 during an ordinary boot |
| open | Why the v3.0 app won't start under upstream's bootloader |
| open | Where the application keeps its own pointer to the shutdown logo |
One more thing worth writing down, because it cost more time than every dead end
combined: a FEL session does not survive a write. After any
spiflash-write or spl, the next transfer fails with
usb_bulk_send() ERROR -7. It is not the cable, it is not the scope, and it is
not the flash. Power cycle between every single operation and it goes away.
Read as a failure the first three times, that error turns a working procedure into an evening of increasingly baroque theories about cache coherency.
what it actually was
A boot ROM that checks the SD card before the flash. A firmware that only serves the SD card while it is running. And a recovery mode that stops the firmware from running.
Three reasonable design decisions, arranged into a loop. The exit was never going to be a cleverer way to boot the scope — it was to stop asking the scope for anything and write the sector directly.
XECAZ