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
2 changes: 1 addition & 1 deletion src/defib/profiles/data/hi3516ev300.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"name": "hi3516ev200", "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, 2, 18, 120, 86, 52, 18, 64, 1, 2, 18, 117, 106, 105, 122], "ADDRESS": ["0x04013000", "0x04010500", "0x41000000"], "FILELEN": ["0x0040", "0x6000"], "STEPLEN": ["0x0040", "0x0080"]}
hi3516ev200.json
79 changes: 70 additions & 9 deletions src/defib/protocol/hisilicon_standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ def set_continuous_ack(self, enabled: bool) -> None:
"""
self._continuous_ack = enabled

@property
def uses_frame_blast_handshake(self) -> bool:
"""Whether this chip uses sendFrameForStart (HEAD blast) as handshake.

When True, the caller should skip the separate handshake() call and
let send_firmware() handle it internally via _send_frame_for_start().
"""
return self._profile is not None and self._profile.prestep_data is not None

async def handshake(
self,
transport: Transport,
Expand Down Expand Up @@ -263,27 +272,68 @@ async def _send_tail(self, transport: Transport, seq: int) -> bool:
transport, frame, FRAME_SEND_RETRIES_SHORT, timeout=0.15
)

async def _send_frame_for_start(
self, transport: Transport, profile: SoCProfile
) -> bool:
"""HiTool-style handshake: blast 0xAA + HEAD(FILELEN0, ADDRESS0) until ACK.

Continuously sends 0xAA (to trigger bootrom download mode) followed
by the 14-byte HEAD frame for up to 30 seconds. The bootrom enters
download mode on 0xAA, then ACKs the HEAD frame.
"""
import time

addr = profile.ddr_step_address
head_frame = HeadFrame(length=64, address=addr).encode()
logger.debug(
"sendFrameForStart: blasting 0xAA+HEAD(64, 0x%08X) for 30s",
addr,
)

deadline = time.monotonic() + 30.0
attempt = 0
while time.monotonic() < deadline:
await transport.write(BOOTMODE_ACK + head_frame)
attempt += 1
try:
ack = await transport.read(1, timeout=0.05)
if ack == ACK_BYTE:
logger.debug(
"sendFrameForStart: ACK after %d blasts", attempt
)
await transport.flush_input()
return True
except TransportTimeout:
continue
logger.debug("sendFrameForStart: timeout after 30s (%d blasts)", attempt)
return False

async def _send_ddr_step(
self,
transport: Transport,
profile: SoCProfile,
on_progress: Callable[[ProgressEvent], None] | None = None,
) -> bool:
"""Send DDR initialization step (64 bytes to SRAM).
"""Send DDR initialization steps to SRAM.

If the profile has PRESTEP0 data, it is sent first. PRESTEP0
prepares the DDR controller before the actual init trigger.
Matches HiTool's exact sequence:
1. sendFrameForStart: blast HEAD(64, ADDRESS0) until ACK (handshake)
2. For each step (PRESTEP0, DDRSTEP0, PRESTEP1): HEAD+DATA+TAIL
"""
_emit(on_progress, ProgressEvent(
stage=Stage.DDR_INIT, bytes_sent=0, bytes_total=64,
message="Sending DDR step",
))

addr = profile.ddr_step_address

# PRESTEP0: pre-DDR init (e.g. write "NOWD" to DDR controller)
prestep = profile.prestep_data

# sendFrameForStart: blast HEAD as handshake (HiTool approach)
if prestep is not None:
if not await self._send_frame_for_start(transport, profile):
return False

# PRESTEP0: HEAD+DATA+TAIL (HEAD sent again per HiTool's loop)
logger.debug(
"=== PRESTEP0 === address=0x%08X data=%d bytes",
addr, len(prestep),
Expand All @@ -294,7 +344,6 @@ async def _send_ddr_step(
return False
if not await self._send_tail(transport, 2):
logger.debug("PRESTEP0 TAIL not ACKed (non-fatal)")
await self._rehandshake(transport)

# DDRSTEP0: actual DDR init trigger
logger.debug(
Expand Down Expand Up @@ -372,6 +421,13 @@ async def _send_spl(
if not await self._send_tail(transport, len(chunks) + 1):
return False

# DDR training delay: HiTool sleeps 300ms after SPL transfer
# for files matching specific sizes (20224, 21504, 29696).
import asyncio as _asyncio
if spl_size in (20224, 21504, 29696):
logger.debug("DDR training delay: 300ms (spl_size=%d)", spl_size)
await _asyncio.sleep(0.3)

_emit(on_progress, ProgressEvent(
stage=Stage.SPL, bytes_sent=spl_size, bytes_total=spl_size,
message="SPL complete",
Expand Down Expand Up @@ -399,9 +455,14 @@ async def _send_uboot(
message=f"Sending {label}",
))

# After SPL runs DDR init, some SoCs (e.g. hi3516av200) re-send
# 0x20 bootmode markers requiring a fresh 0xAA handshake.
await self._rehandshake(transport)
# After SPL runs DDR init, some SoCs re-send 0x20 bootmode markers.
# For chips with PRESTEP0 (frame-blast), use the full blast approach
# since simple rehandshake isn't sufficient on av200.
if profile.prestep_data is not None:
if not await self._send_frame_for_start(transport, profile):
return False
else:
await self._rehandshake(transport)
head = HeadFrame(length=total, address=profile.uboot_address).encode()
if not await self._send_frame_with_retry(
transport, head, retries=64, timeout=0.15,
Expand Down
39 changes: 25 additions & 14 deletions src/defib/recovery/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from defib.protocol.hisilicon_standard import HiSiliconStandard
from defib.protocol.registry import find_protocol
from defib.recovery.events import (
HandshakeResult,
LogEvent,
ProgressEvent,
RecoveryResult,
Expand Down Expand Up @@ -91,10 +92,12 @@ async def run(
if on_log:
on_log(LogEvent(level="info", message=f"Loaded profile: {profile.name}"))

# Power cycle + handshake
# For devices with very short bootrom windows (<100ms), the
# handshake must start BEFORE power-on so continuous ACK mode
# can flood 0xAA while the bootrom is active.
# Determine if this chip uses frame-blast handshake (built into send_firmware)
frame_blast = (
isinstance(protocol, HiSiliconStandard) and protocol.uses_frame_blast_handshake
)

# Power cycle
if self._power and self._poe_port:
if on_log:
on_log(LogEvent(
Expand All @@ -120,28 +123,36 @@ async def run(
)

# Wait for power to actually be cut, then flush any warm-reboot
# garbage from the serial buffer before starting the handshake.
# garbage from the serial buffer.
import asyncio
await asyncio.sleep(1.0)
await transport.flush_input()

# Now start handshake — floods 0xAA so the bootrom sees it
# immediately when power returns from the cycle.
if on_progress:
on_progress(ProgressEvent(
stage=Stage.POWER_CYCLE, bytes_sent=1, bytes_total=1,
message="Power cycle complete",
))

# Handshake — skip for frame-blast chips (handled inside send_firmware)
if frame_blast:
if on_log:
on_log(LogEvent(
level="info",
message=f"Using sendFrameForStart handshake for {self.chip}",
))
handshake = HandshakeResult(success=True, message="Frame-blast (deferred)")
elif self._power and self._poe_port:
# Power-cycle mode with 0x20→0xAA handshake: flood 0xAA
if on_log:
on_log(LogEvent(
level="info",
message=f"Starting {self._protocol_cls.name()} handshake for {self.chip}",
))
import asyncio
handshake_task = asyncio.create_task(
protocol.handshake(transport, on_progress)
)

if on_progress:
on_progress(ProgressEvent(
stage=Stage.POWER_CYCLE, bytes_sent=1, bytes_total=1,
message="Power cycle complete",
))

handshake = await handshake_task
else:
# Manual power cycling — just start handshake and wait
Expand Down
Loading