Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 106 additions & 4 deletions agent/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,102 @@ static void handle_flash_write(const uint8_t *data, uint32_t len) {
proto_send_ack(ACK_OK);
}

/*
* CMD_FLASH_PROGRAM: erase + program flash from RAM.
*
* The U-Boot approach: host writes data to RAM first (via CMD_WRITE),
* then sends this command to erase + program flash from that RAM buffer.
* Agent does the entire flash operation locally, sending per-sector
* progress so the host knows it's alive.
*
* Host sends: CMD_FLASH_PROGRAM [ram_addr:4LE] [flash_addr:4LE]
* [size:4LE] [expected_crc:4LE]
* Agent: verifies RAM CRC → erases sectors → programs pages
* Progress: RSP_DATA [sectors_done:2LE] [total_sectors:2LE] per sector
* Final: ACK_OK or ACK_CRC_ERROR/ACK_FLASH_ERROR
*/
static void handle_flash_program(const uint8_t *data, uint32_t len) {
if (len < 16) { proto_send_ack(ACK_CRC_ERROR); return; }
if (!flash_readable) { proto_send_ack(ACK_FLASH_ERROR); return; }

uint32_t ram_addr = read_le32(&data[0]);
uint32_t flash_addr = read_le32(&data[4]);
uint32_t size = read_le32(&data[8]);
uint32_t expected_crc = read_le32(&data[12]);

/* Validate */
if (size == 0 || flash_addr + size > flash_info.size) {
proto_send_ack(ACK_FLASH_ERROR);
return;
}
if (!addr_readable(ram_addr, size)) {
proto_send_ack(ACK_FLASH_ERROR);
return;
}

const uint8_t *src = (const uint8_t *)ram_addr;

/* Verify RAM data CRC before touching flash */
uint32_t actual_crc = crc32(0, src, size);
if (actual_crc != expected_crc) {
uint8_t err[9];
err[0] = ACK_CRC_ERROR;
write_le32(&err[1], actual_crc);
write_le32(&err[5], size);
proto_send(RSP_ACK, err, 9);
return;
}

proto_send_ack(ACK_OK); /* CRC verified, starting flash operation */

uint32_t sector_sz = flash_info.sector_size;
uint32_t page_sz = flash_info.page_size;

/* Round up to sector boundary for erase */
uint32_t erase_start = flash_addr & ~(sector_sz - 1);
uint32_t erase_end = (flash_addr + size + sector_sz - 1) & ~(sector_sz - 1);
uint32_t num_sectors = (erase_end - erase_start) / sector_sz;
uint32_t total_sectors = num_sectors;

/* Phase 1: Erase sectors */
for (uint32_t s = 0; s < num_sectors; s++) {
flash_erase_sector(erase_start + s * sector_sz);

/* Progress: [sectors_done:2LE] [total:2LE] */
uint8_t progress[4];
progress[0] = ((s + 1) >> 0) & 0xFF;
progress[1] = ((s + 1) >> 8) & 0xFF;
progress[2] = (total_sectors >> 0) & 0xFF;
progress[3] = (total_sectors >> 8) & 0xFF;
proto_send(RSP_DATA, progress, 4);
}

/* Phase 2: Program pages */
uint32_t offset = 0;
while (offset < size) {
uint32_t chunk = size - offset;
if (chunk > page_sz) chunk = page_sz;
flash_write_page(flash_addr + offset, &src[offset], chunk);
offset += chunk;

/* Progress every 64 pages (16KB) to keep host alive */
if ((offset % (page_sz * 64)) == 0 || offset >= size) {
uint8_t progress[4];
uint16_t done = (uint16_t)(total_sectors + offset / (page_sz * 64));
uint16_t total = (uint16_t)(total_sectors + size / (page_sz * 64));
progress[0] = (done >> 0) & 0xFF;
progress[1] = (done >> 8) & 0xFF;
progress[2] = (total >> 0) & 0xFF;
progress[3] = (total >> 8) & 0xFF;
proto_send(RSP_DATA, progress, 4);
}
}

/* Skip in-agent verify — the FMC memory window may have stale data
* after bulk programming (65536 soft resets). Host verifies separately. */
proto_send_ack(ACK_OK);
}

/*
* ARM32 position-independent trampoline (machine code).
* Copies r2 bytes from r1 to r0, then branches to r0-r2 (original dst).
Expand Down Expand Up @@ -694,14 +790,20 @@ int main(void) {
case CMD_SCAN:
handle_scan(cmd_buf, data_len);
break;
case CMD_FLASH_PROGRAM:
handle_flash_program(cmd_buf, data_len);
break;
case CMD_SET_BAUD:
handle_set_baud(cmd_buf, data_len);
break;
case CMD_REBOOT:
/* Rejected — watchdog reset re-enters bootrom on serial
* boot pin, killing the agent with no recovery. Use
* SELFUPDATE to reload, or physical power cycle. */
proto_send_ack(ACK_FLASH_ERROR);
/* ACK first, then system reset via sysctrl register.
* This is the standard HiSilicon reset (same as Linux
* hisi-reboot driver): write 0xdeadbeef to 0x12020004. */
proto_send_ack(ACK_OK);
for (volatile int i = 0; i < 100000; i++) {}
*(volatile uint32_t *)0x12020004 = 0xdeadbeef;
while (1) {}
break;
default:
proto_send_ack(ACK_CRC_ERROR);
Expand Down
1 change: 1 addition & 0 deletions agent/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#define CMD_SELFUPDATE 0x07
#define CMD_SET_BAUD 0x08
#define CMD_SCAN 0x09
#define CMD_FLASH_PROGRAM 0x0A

/* Responses (device → host) */
#define RSP_INFO 0x81
Expand Down
88 changes: 55 additions & 33 deletions src/defib/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ACK_OK,
CMD_CRC32,
CMD_ERASE,
CMD_FLASH_PROGRAM,
CMD_INFO,
CMD_READ,
CMD_SCAN,
Expand Down Expand Up @@ -381,41 +382,59 @@ async def write_flash(
data: bytes,
on_progress: Callable[[int, int], None] | None = None,
) -> bool:
"""Write data to flash (assumes sectors already erased).
"""Write data to flash: upload to RAM, then erase + program.

Uses _write_block with per-packet backpressure for reliable
transfer. Agent receives to RAM staging, verifies CRC, programs
flash page-by-page, verifies readback.
Two-phase approach (like U-Boot sf write):
1. Upload data to RAM via write_memory (fast, reliable)
2. CMD_FLASH_PROGRAM: agent erases + programs from RAM locally
"""
total = len(data)
offset = 0
max_retries = 3
ram_addr = self._ram_base + 0x200000 # Staging area in DDR

# Phase 1: Upload to RAM
logger.info("Flash write: uploading %d bytes to RAM", total)
ok = await self.write_memory(
ram_addr, data,
on_progress=lambda d, t: (
on_progress(d // 2, total) if on_progress else None
),
)
if not ok:
logger.error("Flash write: RAM upload failed")
return False

while offset < total:
block_size = min(WRITE_MAX_TRANSFER, total - offset)
block = data[offset:offset + block_size]
# Phase 2: Tell agent to erase + program flash from RAM
logger.info("Flash write: programming flash from RAM")
self._clear_rx_buffers()
expected_crc = zlib.crc32(data) & 0xFFFFFFFF
payload = struct.pack("<IIII", ram_addr, addr, total, expected_crc)
await send_packet(self._transport, CMD_FLASH_PROGRAM, payload)

ok = False
for attempt in range(max_retries):
ok = await self._write_block(addr + offset, block)
if ok:
break
logger.warning(
"Flash write retry %d at offset %d/%d",
attempt + 1, offset, total,
)
import asyncio
await asyncio.sleep(0.1)
# Read initial ACK (CRC verified)
cmd, resp = await recv_response(self._transport, timeout=30.0)
if cmd != RSP_ACK or resp[0] != ACK_OK:
logger.error("Flash program rejected: 0x%02x", resp[0] if resp else -1)
return False

if not ok:
logger.error("Flash write failed at offset %d/%d", offset, total)
# Read progress packets until final ACK
while True:
cmd, data_pkt = await recv_packet(self._transport, timeout=60.0)
if cmd == RSP_READY:
continue
elif cmd == RSP_DATA and len(data_pkt) >= 4:
done = data_pkt[0] | (data_pkt[1] << 8)
total_steps = data_pkt[2] | (data_pkt[3] << 8)
if on_progress and total_steps > 0:
on_progress(total // 2 + (total // 2) * done // total_steps, total)
elif cmd == RSP_ACK:
if data_pkt[0] == ACK_OK:
return True
logger.error("Flash program failed: 0x%02x", data_pkt[0])
return False
else:
break

offset += block_size
if on_progress:
on_progress(offset, total)

return True
return False

async def crc32(self, addr: int, size: int) -> int:
"""Get CRC32 of a memory region from the device."""
Expand Down Expand Up @@ -654,9 +673,12 @@ async def set_baud(self, baud: int) -> bool:
return False

async def reboot(self) -> None:
"""Reboot is disabled — watchdog reset kills the agent on serial
boot pin with no recovery. Use selfupdate() to reload code."""
raise RuntimeError(
"Reboot disabled: watchdog reset re-enters bootrom on serial "
"boot mode, requiring physical power cycle. Use selfupdate() instead."
)
"""Reset the device via watchdog. Bootrom boots from flash if
valid firmware is present, otherwise enters serial download."""
self._clear_rx_buffers()
from defib.agent.protocol import CMD_REBOOT
await send_packet(self._transport, CMD_REBOOT)
try:
cmd, resp = await recv_response(self._transport, timeout=5.0)
except Exception:
pass # Agent may reset before ACK arrives
1 change: 1 addition & 0 deletions src/defib/agent/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
CMD_SELFUPDATE = 0x07
CMD_SET_BAUD = 0x08
CMD_SCAN = 0x09
CMD_FLASH_PROGRAM = 0x0A

# Responses
RSP_INFO = 0x81
Expand Down
Loading