FNIRSI 1014D · Allwinner F1C100s

2026-09-04

8 KiB from a brick

Swapping an oscilloscope's boot logo took twenty minutes. Getting the oscilloscope to boot again took the rest of the night.

0x000000eGON.BT0SPL — draws the splash, starts the app
0x006000eGON.EXEFEL helper
0x013000eGON.BMPthe boot logo — the target
0x027000eGON.EXEthe scope application
0x1FD000CALIBRATIONper-unit, unbuyable, do not touch

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 target

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.

the way in

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 trap

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.

five dead ends

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.

the way out

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.

and then, greedily, full screen

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.

before298 × 130
The stock FNIRSI boot splash: the FNIRSI wordmark, a red FNIRSI-1014D badge, Chinese company name, the fnirsi.cn URL and the text Firmware version V3.0.
Stock, extracted from the chip. The version line is why this firmware's block is 32 rows taller than every public description of it.
after800 × 480
The replacement boot splash filling the whole panel: a face with the eyes pixelated, and the text xecaz.com across the top.
Relocated to 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.

the ledger

What the evening established, and what it merely suggests.
statusfinding
verifiedSplash geometry varies by firmware version, not model
verifiedBoth writes read back byte-identical across the full 2 MiB
correctedNo SD slot exists; every guide saying otherwise is wrong for this model
corrected0x1FD000 is calibration and live config — the firmware rewrote 0x1FD014 during an ordinary boot
openWhy the v3.0 app won't start under upstream's bootloader
openWhere 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

2,097,152 bytes · calibration intact · logo mine

tooling, bench procedure and the sd-wipe payload: github.com/xecaz/FNIRSI-1014D