From af40384bd1df867fb6e92cfb4e3e689228fd0af0 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:21:04 +0300 Subject: [PATCH 1/2] hi3536dv100: flash agent, and the NBD8010S board's own SPL as a variant The Xiongmai NBD8010S-KL-V2 NVR, sold as "Hi3536E", is a Hi3536DV100 die (SCSYSID 0x3536D100). This makes it recoverable through the bootrom and gives it a flash agent. Agent (SOC=hi3536dv100). The memory map comes from vendor arch-hi3536dv100/platform.h: UART0 PL011 at 0x12080000, FMC 0x10000000 with its window at 0x14000000, CRG 0x12040000, FMC clock gate in CRG48 (+0xC0), SP805 WDT 0x12070000, reboot via sysctl+4. The PL011 runs off the fixed 24 MHz XTAL (CONFIG_PL011_CLOCK), so SET_BAUD reaches 921600 with no clock switching. That is unlike hi3520dv200 (#97), whose bootrom leaves the UART on a ~2 MHz reference. This SoC has no SPI pad block at 0x100C0000, and the vendor FMC driver never touches the pads, so SPI_PINS_SKIP leaves them as the bootrom set them. Variant hi3536dv100:nbd8010s. With the OpenIPC SPL, every bootrom stage is ACKed but the jump to 0x81000000 resets the chip: fresh 0x20 markers, then the stock U-Boot boots from flash. That looks like a successful burn. The OpenIPC SPL's DDR/PLL table (222 entries) was built for another board; this board's stock U-Boot has a different one (185 entries). The variant ships the stock SPL, flash bytes 0..0x5840. The SPL code runs right up to the LZMA payload at 0x5840, so FILELEN grows to 0x5840; the boundary scan would otherwise round down to 0x5800 and cut its tail. burn now honours SPL_BLOB. RecoverySession ignored it, so only the agent paths used a variant's SPL. It now passes the blob as spl_override, which lets `burn -c hi3536dv100:nbd8010s` put the OpenIPC U-Boot in RAM on top of the board's own DDR init. Verified on the NBD8010S-KL-V2 (W25Q128, ef4018, 16 MiB) with Tasmota power cycling: defib burn -c hi3536dv100:nbd8010s -p /dev/ttyUSB2 --power-cycle -> "OpenIPC #", U-Boot 2010.06 (May 09 2026) defib agent upload -c hi3536dv100:nbd8010s -p /dev/ttyUSB2 --power-cycle -> Agent ready, RAM 0x80000000, Flash 16384KB agent info: JEDEC ef4018, all capabilities full-chip read in 256 KiB chunks at 921600: 86 KB/s, 0 retries, CRC 0xaedae175, the same as stock U-Boot `sf read` + `crc32` (whole chip and each of the 16 MiB slices) agent scan: 256/256 sectors good write a pattern to blank sector 0xF50000, read back bit-exact, erase back to all-0xFF; neighbours and logo partition unchanged Without the variant, agent upload never gets READY and burn ends in the stock U-Boot. A single 16 MiB read stream at 921600 still drops one COBS frame in ~10k (the known seq-gap failure); 256 KiB chunks are clean. --- CLAUDE.md | 2 +- agent/Makefile | 21 +++++++++++++++- agent/README.md | 7 ++++++ agent/spi_flash.c | 5 +++- src/defib/agent/client.py | 1 + .../data/hi3536dv100-nbd8010s-spl.bin | Bin 0 -> 22592 bytes src/defib/profiles/data/hi3536dv100.json | 2 +- src/defib/recovery/session.py | 18 +++++++++++--- tests/test_agent_binary_lookup.py | 3 +++ tests/test_profiles.py | 13 ++++++++++ tests/test_recovery_session.py | 23 ++++++++++++++++++ 11 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 src/defib/profiles/data/hi3536dv100-nbd8010s-spl.bin diff --git a/CLAUDE.md b/CLAUDE.md index 3e4563e..fc076d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -128,7 +128,7 @@ through agent RAM buffers, see `agent/nand_layout.h` and `defib.agent.nand`). `a optional features through a capability bitmask (`client.py`). Backends: `spi_flash.c` (fmc100), `spi_flash_hisfc350.c` (V1-era parts), -`emmc_himci.c`. Twelve SoCs are supported; each has its own `ifeq` stanza in +`emmc_himci.c`. Thirteen SoCs are supported; each has its own `ifeq` stanza in `agent/Makefile` carrying `LOAD_ADDR` (and `SPI_DRIVER` where it differs). `link.ld` itself is generic — it just places `. = LOAD_ADDR`. diff --git a/agent/Makefile b/agent/Makefile index 5e9a915..d19de55 100644 --- a/agent/Makefile +++ b/agent/Makefile @@ -203,8 +203,26 @@ else ifeq ($(SOC),hi3520dv200) CRG_BASE = 0x20030000 SYSCTRL_REBOOT = 0x20050004 SPI_DRIVER = hisfc350 +else ifeq ($(SOC),hi3536dv100) + # DVR/NVR generation, Cortex-A7, V4-style map per vendor + # arch-hi3536dv100/platform.h. Sold by Xiongmai as "Hi3536E" + # (NBD8010S-KL-V2 reads SCSYSID 0x3536D100). UART is PL011 on a fixed + # 24 MHz clock (CONFIG_PL011_CLOCK), so SET_BAUD needs no CRG work. + # FMC clock gate is CRG48 (+0xC0); the vendor driver never touches + # the SPI pads, and there is no pad block at 0x100C0000, so skip them. + UART_BASE = 0x12080000 + UART_CLOCK = 24000000 + LOAD_ADDR = 0x81000000 + FLASH_MEM = 0x14000000 + FMC_BASE = 0x10000000 + RAM_BASE = 0x80000000 + WDT_BASE = 0x12070000 + CRG_BASE = 0x12040000 + SYSCTRL_REBOOT = 0x12050004 + FMC_CRG_OFFSET = 0x00C0 + SPI_PINS_SKIP = 1 else - $(error Unknown SOC: $(SOC). Supported: hi3516ev300 hi3516ev200 gk7205v200 gk7205v300 gk7605v100 gk7205v500 hi3516cv300 hi3516cv500 hi3518ev200 hi3516cv610 hi3519v101 hi3520dv200) + $(error Unknown SOC: $(SOC). Supported: hi3516ev300 hi3516ev200 gk7205v200 gk7205v300 gk7605v100 gk7205v500 hi3516cv300 hi3516cv500 hi3518ev200 hi3516cv610 hi3519v101 hi3520dv200 hi3536dv100) endif # Per-SoC CPU. V3 chips (cv300) are ARM926EJ-S (ARMv5TEJ); @@ -230,6 +248,7 @@ CFLAGS = -mcpu=$(CPU_TYPE) -marm -O2 -ffreestanding -nostdlib \ $(if $(FMC_CRG_CLK_BIT),-DFMC_CRG_CLK_BIT=$(FMC_CRG_CLK_BIT)) \ $(if $(SPI_PIN_BASE),-DSPI_PIN_BASE=$(SPI_PIN_BASE)) \ $(if $(SPI_PINS_CV6XX),-DSPI_PINS_CV6XX=1) \ + $(if $(SPI_PINS_SKIP),-DSPI_PINS_SKIP=1) \ -mno-unaligned-access -Wall -Wextra LDFLAGS = -nostdlib -T link.ld -Ttext=$(LOAD_ADDR) --defsym=LOAD_ADDR=$(LOAD_ADDR) diff --git a/agent/README.md b/agent/README.md index ab2453c..c5b1b22 100644 --- a/agent/README.md +++ b/agent/README.md @@ -82,12 +82,19 @@ Requires `arm-none-eabi-gcc` (Arch: `pacman -S arm-none-eabi-gcc arm-none-eabi-n | hi3516cv610 | 0x11040000 | 0x14000000 | 0x40000000 | 0x41000000 | | hi3519v101 | 0x12100000 | 0x14000000 | 0x80000000 | 0x81000000 | | hi3520dv200 | 0x20080000 | 0x58000000 | 0x80000000 | 0x81000000 | +| hi3536dv100 | 0x12080000 | 0x14000000 | 0x80000000 | 0x81000000 | gk7205v500 also serves gk7205v510/v530. The V500 bootrom has no SPL stage: `defib agent upload` puts the agent in the boot-code slot of an OpenIPC u-boot-xmedia image (downloaded, or `-f` for your own), whose DDR-init code runs first; the bootrom then executes the slot in place at `0x41007000`. +hi3536dv100 is the DVR/NVR part Xiongmai sells as "Hi3536E". Its UART runs +off a fixed 24 MHz clock, so `SET_BAUD` reaches 921600 with no clock +switching. The OpenIPC SPL for it doesn't bring up DDR on the Xiongmai +NBD8010S-KL-V2 board; use `-c hi3536dv100:nbd8010s` there, which uploads the +board's own SPL instead. + Addresses from [qemu-hisilicon](https://github.com/OpenIPC/LoTool) hardware definitions. ### Where defib looks for a built binary diff --git a/agent/spi_flash.c b/agent/spi_flash.c index 773b027..7a5ad0d 100644 --- a/agent/spi_flash.c +++ b/agent/spi_flash.c @@ -168,7 +168,10 @@ static void fmc_enter_normal(void) { * 0x10260000 and uses different register offsets + values; the * dimerr/u-boot-hi3516cv610 driver fmc_hi3516cv610.c is the source * of truth for the CV6xx values. */ -#ifdef SPI_PINS_CV6XX +#if defined(SPI_PINS_SKIP) + /* The bootrom's pad setup is final on this SoC (hi3536dv100): + * vendor U-Boot leaves the SPI pads alone too. */ +#elif defined(SPI_PINS_CV6XX) /* CV6xx layout (per drivers/mtd/fmc_hi3516cv610.c, 3.3V single-CS): * 0x0C MOSI_IO0, 0x10 CLK, 0x14 HOLD_IO3, 0x18 CS0, * 0x1C MISO_IO1, 0x20 WP_IO2. */ diff --git a/src/defib/agent/client.py b/src/defib/agent/client.py index c084be8..1f945cb 100644 --- a/src/defib/agent/client.py +++ b/src/defib/agent/client.py @@ -163,6 +163,7 @@ def bad_block(self) -> list[SectorResult]: "hi3516cv608": "hi3516cv610", # cv6xx-family, same memory map "hi3518ev200": "hi3518ev200", "hi3520dv200": "hi3520dv200", # V1-era, HISFC350 SPI controller + "hi3536dv100": "hi3536dv100", "gk7205v500": "gk7205v500", "gk7205v510": "gk7205v500", # V500 family, same memory map "gk7205v530": "gk7205v500", diff --git a/src/defib/profiles/data/hi3536dv100-nbd8010s-spl.bin b/src/defib/profiles/data/hi3536dv100-nbd8010s-spl.bin new file mode 100644 index 0000000000000000000000000000000000000000..e3bb63ec4230eb440707224d60b3d82281aef745 GIT binary patch literal 22592 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 ZSn9(%=tub-o{VuC&ey=F+fyWs|35+T_22*i literal 0 HcmV?d00001 diff --git a/src/defib/profiles/data/hi3536dv100.json b/src/defib/profiles/data/hi3536dv100.json index 85caadd..94c3119 100644 --- a/src/defib/profiles/data/hi3536dv100.json +++ b/src/defib/profiles/data/hi3536dv100.json @@ -1 +1 @@ -{"name": "hi3536dv100", "PRESTEP0": [4, 224, 45, 229, 36, 0, 159, 229, 36, 16, 159, 229, 0, 16, 128, 229, 32, 0, 159, 229, 32, 16, 159, 229, 4, 16, 128, 228, 0, 224, 128, 229, 4, 240, 157, 228, 239, 190, 173, 222, 239, 190, 173, 222, 239, 190, 173, 222, 60, 1, 5, 18, 78, 87, 79, 68, 64, 1, 5, 18, 117, 106, 105, 122], "DDRSTEP0": [4, 224, 45, 229, 36, 0, 159, 229, 36, 16, 159, 229, 0, 16, 128, 229, 32, 0, 159, 229, 32, 16, 159, 229, 4, 16, 128, 228, 0, 224, 128, 229, 4, 240, 157, 228, 239, 190, 173, 222, 239, 190, 173, 222, 239, 190, 173, 222, 60, 1, 5, 18, 120, 86, 52, 18, 64, 1, 5, 18, 117, 106, 105, 122], "ADDRESS": ["0x04012000", "0x04010500", "0x81000000"], "FILELEN": ["0x0040", "0x5000"], "STEPLEN": ["0x0040", "0x0050"]} \ No newline at end of file +{"name": "hi3536dv100", "PRESTEP0": [4, 224, 45, 229, 36, 0, 159, 229, 36, 16, 159, 229, 0, 16, 128, 229, 32, 0, 159, 229, 32, 16, 159, 229, 4, 16, 128, 228, 0, 224, 128, 229, 4, 240, 157, 228, 239, 190, 173, 222, 239, 190, 173, 222, 239, 190, 173, 222, 60, 1, 5, 18, 78, 87, 79, 68, 64, 1, 5, 18, 117, 106, 105, 122], "DDRSTEP0": [4, 224, 45, 229, 36, 0, 159, 229, 36, 16, 159, 229, 0, 16, 128, 229, 32, 0, 159, 229, 32, 16, 159, 229, 4, 16, 128, 228, 0, 224, 128, 229, 4, 240, 157, 228, 239, 190, 173, 222, 239, 190, 173, 222, 239, 190, 173, 222, 60, 1, 5, 18, 120, 86, 52, 18, 64, 1, 5, 18, 117, 106, 105, 122], "ADDRESS": ["0x04012000", "0x04010500", "0x81000000"], "FILELEN": ["0x0040", "0x5000"], "STEPLEN": ["0x0040", "0x0050"], "variants": {"nbd8010s": {"SPL_BLOB": "hi3536dv100-nbd8010s-spl.bin", "FILELEN": ["0x0040", "0x5840"]}}} \ No newline at end of file diff --git a/src/defib/recovery/session.py b/src/defib/recovery/session.py index 6d04aab..1a8dbd0 100644 --- a/src/defib/recovery/session.py +++ b/src/defib/recovery/session.py @@ -87,10 +87,15 @@ async def run( protocol = self._protocol_cls() - # For standard protocol, load and attach the SoC profile + # For standard protocol, load and attach the SoC profile. A board + # variant's SPL_BLOB replaces the SPL carved from the U-Boot image: + # that SPL carries one board's DDR table, and on a board with other + # DDR the bootrom jump into U-Boot lands in dead memory. + spl_override: bytes | None = None if isinstance(protocol, HiSiliconStandard): profile = load_profile(self.chip) protocol.set_profile(profile) + spl_override = profile.spl_data if self._power: protocol.set_continuous_ack(True) if on_log: @@ -247,9 +252,14 @@ async def run( level="info", message=f"Sending {len(firmware)} bytes of firmware...", )) - send_result = await protocol.send_firmware( - transport, firmware, on_progress, - ) + if spl_override is not None and isinstance(protocol, HiSiliconStandard): + send_result = await protocol.send_firmware( + transport, firmware, on_progress, spl_override=spl_override, + ) + else: + send_result = await protocol.send_firmware( + transport, firmware, on_progress, + ) if send_result.success: # Mutate handshake variable so post-loop code sees success. handshake_succeeded_result = send_result diff --git a/tests/test_agent_binary_lookup.py b/tests/test_agent_binary_lookup.py index 55ad1da..52cd2e2 100644 --- a/tests/test_agent_binary_lookup.py +++ b/tests/test_agent_binary_lookup.py @@ -43,6 +43,9 @@ def test_a_variant_suffix_still_resolves(self, monkeypatch, tmp_path): (tmp_path / "agent-hi3516cv500.bin").write_bytes(b"\x00" * 16) assert get_agent_binary("hi3516av300:emmc") is not None + def test_a_board_variant_uses_the_chip_build(self): + assert agent_binary_for("hi3536dv100:nbd8010s") == "hi3536dv100" + def test_every_path_is_absolute(self): assert all(Path(p).is_absolute() for p in _agent_search_path("hi3516cv300")) diff --git a/tests/test_profiles.py b/tests/test_profiles.py index 77cddc9..2e8f330 100644 --- a/tests/test_profiles.py +++ b/tests/test_profiles.py @@ -220,6 +220,19 @@ def test_real_av300_emmc_variant_loads_spl_blob(self): # SPL extracted at 0x5000 (above the gzip boundary at ~0x5220) assert len(p.spl_data) == 0x5000 + def test_real_hi3536dv100_nbd8010s_variant_loads_spl_blob(self): + """The Xiongmai NBD8010S-KL-V2 NVR's DDR differs from the board the + OpenIPC hi3536dv100 SPL was built for, so its variant ships the SPL + from the board's own U-Boot. The blob runs right up to the LZMA + payload at 0x5840, and FILELEN grows to match so none of it is cut.""" + p = load_profile("hi3536dv100:nbd8010s", PROFILES_DIR) + assert p.spl_blob == "hi3536dv100-nbd8010s-spl.bin" + assert p.spl_data is not None + assert p.spl_data[:4] == bytes.fromhex("170800ea") + assert len(p.spl_data) == 0x5840 + assert p.spl_max_size == 0x5840 + assert load_profile("hi3536dv100", PROFILES_DIR).spl_data is None + class TestSplBlob: """SPL_BLOB lets a profile (typically a board variant) ship pre-built diff --git a/tests/test_recovery_session.py b/tests/test_recovery_session.py index adcaaba..92e95bf 100644 --- a/tests/test_recovery_session.py +++ b/tests/test_recovery_session.py @@ -54,3 +54,26 @@ async def test_full_standard_session(self): assert result.elapsed_ms > 0 assert len(log_events) > 0 assert len(progress_events) > 0 + + @pytest.mark.asyncio + async def test_variant_spl_blob_replaces_the_firmware_spl(self): + """`burn -c hi3536dv100:nbd8010s` must send the board's SPL_BLOB as + the SPL stage, then the whole U-Boot image to DDR.""" + from defib.profiles.loader import load_profile + + blob = load_profile("hi3536dv100:nbd8010s", PROFILES_DIR).spl_data + assert blob is not None + + transport = MockTransport(flush_clears_buffer=False) + transport.enqueue_rx(b"\x20" * 5) + transport.enqueue_rx(ACK_BYTE * 1000) + firmware = b"\x5a" * 0x8000 + + session = RecoverySession(chip="hi3536dv100:nbd8010s", firmware_data=firmware) + result = await session.run(transport) + + assert result.success + sent = b"".join(transport.tx_log) + # SPL DATA frames carry the blob in 1024-byte chunks after a 3-byte + # frame header; the first chunk is enough to tell whose SPL went out. + assert blob[:1024] in sent From f2b2d023482d49997f2e5d7c12d8f283fdd26a85 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:34:09 +0300 Subject: [PATCH 2/2] protocol: never pad an SPL override past its own length Qodo review on this branch. Now that burn forwards SPL_BLOB, the 0x5000-byte hi3516av300:emmc blob ran into the same sizing the agent paths already had. A blob carries no LZMA/gzip marker, so _detect_spl_size falls back to the inherited FILELEN of 0x6000, and ljust() zero-pads 4 KiB past the SPL into SRAM the bootrom still uses. The SPL size is now capped at the override's length. Boundary detection and the SRAM limit work as before. A full U-Boot passed as the override is always longer than its detected boundary, so it is unaffected. The session test now checks the SPL HEAD length for both variants: 0x5840 for hi3536dv100:nbd8010s and 0x5000 for hi3516av300:emmc. The emmc case fails without this change. Re-verified agent upload -c hi3536dv100:nbd8010s on the NBD8010S-KL-V2 (READY; flash 0..0xF80000 CRC 0xa63bd6ca = backup). hi3516av300:emmc now sends 0x5000 instead of 0x6000. That is the extracted SPL's real size, but it has not been re-run on an eMMC av300 here. --- src/defib/protocol/hisilicon_standard.py | 6 ++++++ tests/test_recovery_session.py | 23 +++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/defib/protocol/hisilicon_standard.py b/src/defib/protocol/hisilicon_standard.py index 31955cb..c1817c6 100644 --- a/src/defib/protocol/hisilicon_standard.py +++ b/src/defib/protocol/hisilicon_standard.py @@ -512,6 +512,12 @@ async def _send_spl( spl_size = self._detect_spl_size( scan_buf, profile.spl_max_size, sram_limit=profile.spl_sram_limit, ) + # A board SPL_BLOB has no compressed payload to mark its end, so the + # scan falls back to profile_max — on hi3516av300:emmc that is 0x6000 + # against a 0x5000 blob. Padding it out writes zeros past the SPL into + # SRAM the bootrom still uses; send only the bytes the blob holds. + if spl_override is not None and len(spl_override) < spl_size: + spl_size = len(spl_override) logger.debug( "=== SPL === address=0x%08X size=%d chunks=%d", profile.spl_address, spl_size, diff --git a/tests/test_recovery_session.py b/tests/test_recovery_session.py index 92e95bf..fc4387c 100644 --- a/tests/test_recovery_session.py +++ b/tests/test_recovery_session.py @@ -56,20 +56,29 @@ async def test_full_standard_session(self): assert len(progress_events) > 0 @pytest.mark.asyncio - async def test_variant_spl_blob_replaces_the_firmware_spl(self): - """`burn -c hi3536dv100:nbd8010s` must send the board's SPL_BLOB as - the SPL stage, then the whole U-Boot image to DDR.""" + @pytest.mark.parametrize("chip, spl_len", [ + ("hi3536dv100:nbd8010s", 0x5840), + # 0x5000 blob under an inherited FILELEN of 0x6000: must not be + # padded out to the profile maximum. + ("hi3516av300:emmc", 0x5000), + ]) + async def test_variant_spl_blob_replaces_the_firmware_spl(self, chip, spl_len): + """`burn -c :` must send the board's SPL_BLOB, exactly + as long as the blob, as the SPL stage.""" + import struct + from defib.profiles.loader import load_profile - blob = load_profile("hi3536dv100:nbd8010s", PROFILES_DIR).spl_data - assert blob is not None + profile = load_profile(chip, PROFILES_DIR) + blob = profile.spl_data + assert blob is not None and len(blob) == spl_len transport = MockTransport(flush_clears_buffer=False) transport.enqueue_rx(b"\x20" * 5) transport.enqueue_rx(ACK_BYTE * 1000) firmware = b"\x5a" * 0x8000 - session = RecoverySession(chip="hi3536dv100:nbd8010s", firmware_data=firmware) + session = RecoverySession(chip=chip, firmware_data=firmware) result = await session.run(transport) assert result.success @@ -77,3 +86,5 @@ async def test_variant_spl_blob_replaces_the_firmware_spl(self): # SPL DATA frames carry the blob in 1024-byte chunks after a 3-byte # frame header; the first chunk is enough to tell whose SPL went out. assert blob[:1024] in sent + head = b"\xfe\x00\xff\x01" + struct.pack(">II", spl_len, profile.spl_address) + assert head in sent