SXXXXXXX_PyDownloadFwViaSRIO/tools/tftp_receiver.py
2026-01-22 17:10:05 +01:00

540 lines
20 KiB
Python

"""TFTP receiver emulator for testing SRIO-over-TFTP flash protocol.
This server emulates the target hardware: it decodes incoming TFTP filenames
that encode SRIO commands ($SRIO:<slot>/<address>+<len>) and simulates
the complete SRIO flash controller with registers (MODE_REG, CMD_REG, STATUS_REG, etc.).
Usage:
python tools/tftp_receiver.py --port 6969 --workdir ./tftp_root
Reference: dwl_fw.cpp, fpgaflashinterface.h, qgtftptargetsim.cpp
Features:
- Simulates SRIO flash controller registers (from dwl_fw.h)
- Handles op_init_qspi() initialization sequence
- Processes erase_section(), write_flash(), read_flash(), wait_flash()
- Logs all register operations for debugging
Note: Uses custom TFTP server implementation for Windows compatibility.
"""
from __future__ import annotations
import argparse
import re
import socket
import struct
import sys
import threading
import time
from pathlib import Path
from typing import Dict, Optional, Tuple
# SRIO Register addresses (from dwl_fw.h)
MODE_REG = 0x4700002C
CMD_REG = 0x47000030
ADDR_REG = 0x47000034
NUM_BYTE_REG = 0x47000038
CTRL_REG = 0x47000060
TX_FIFO_REG = 0x47000400
STATUS_REG = 0x47000864
RX_FIFO_REG = 0x47000C00
class SRIOFlashController:
"""Simulates SRIO flash controller with register state machine.
Emulates hardware behavior including:
- Register state (MODE_REG, CMD_REG, CTRL_REG, STATUS_REG, etc.)
- Flash operations (erase, write, read)
- Status bit transitions (busy, ack, error)
"""
def __init__(self, flash_image_path: Path, flash_size: int = 16 * 1024 * 1024) -> None:
"""Initialize SRIO flash controller simulator."""
self.flash_image_path = flash_image_path
self.flash_size = flash_size
# Initialize flash image file
if not flash_image_path.exists():
flash_image_path.parent.mkdir(parents=True, exist_ok=True)
with open(flash_image_path, "wb") as f:
f.write(b"\xFF" * flash_size)
print(f"Flash controller initialized: {flash_image_path} ({flash_size} bytes)")
# Register state (256-byte aligned blocks)
self.memory: Dict[int, bytearray] = {} # Stores 256-byte blocks
# Flash operation state
self.flash_busy = False
self.tx_fifo_buffer: bytes = b""
self.mode_reg = 0
self.cmd_reg = 0
self.addr_reg = 0
self.num_bytes_reg = 0
def _get_block_key(self, address: int) -> int:
"""Get 256-byte aligned block key."""
return address & 0xFFFFFF00
def _ensure_block(self, address: int) -> bytearray:
"""Ensure 256-byte block exists in memory."""
key = self._get_block_key(address)
if key not in self.memory:
self.memory[key] = bytearray(256)
return self.memory[key]
def write_data(self, address: int, data: bytes) -> None:
"""Write data to SRIO address (256-byte aligned)."""
if len(data) != 256:
print(f" WARNING: Write size {len(data)} != 256 bytes")
# Pad or truncate
if len(data) < 256:
data = data + b"\x00" * (256 - len(data))
else:
data = data[:256]
block = self._ensure_block(address)
block[:] = data
# Check if this is a register write
if self._is_register_address(address):
self._handle_register_write(address, data)
elif address == TX_FIFO_REG:
# TX FIFO data for flash write
self.tx_fifo_buffer = data
print(f" TX_FIFO: Stored 256 bytes for flash write")
def read_data(self, address: int, length: int) -> bytes:
"""Read data from SRIO address."""
if length != 256:
print(f" WARNING: Read size {length} != 256 bytes")
block = self._ensure_block(address)
# Check if this is a register read
if self._is_register_address(address):
self._handle_register_read(address, block)
return bytes(block[:length])
def _is_register_address(self, address: int) -> bool:
"""Check if address is a register."""
base = address & 0xFFFFFF00
offset = address & 0xFF
reg_bases = [
MODE_REG & 0xFFFFFF00,
CMD_REG & 0xFFFFFF00,
CTRL_REG & 0xFFFFFF00,
STATUS_REG & 0xFFFFFF00,
RX_FIFO_REG & 0xFFFFFF00,
]
return base in reg_bases or address == TX_FIFO_REG
def _handle_register_write(self, address: int, data: bytes) -> None:
"""Process register write and update state."""
base = address & 0xFFFFFF00
# Extract register values from 256-byte block
if base == (MODE_REG & 0xFFFFFF00):
offset = MODE_REG & 0xFF
self.mode_reg = struct.unpack('<I', data[offset:offset+4])[0]
print(f" MODE_REG = 0x{self.mode_reg:02X}")
if base == (CMD_REG & 0xFFFFFF00):
offset = CMD_REG & 0xFF
self.cmd_reg = struct.unpack('<I', data[offset:offset+4])[0]
print(f" CMD_REG = 0x{self.cmd_reg:02X}")
if base == (ADDR_REG & 0xFFFFFF00):
offset = ADDR_REG & 0xFF
self.addr_reg = struct.unpack('<I', data[offset:offset+4])[0]
print(f" ADDR_REG = 0x{self.addr_reg:08X}")
if base == (NUM_BYTE_REG & 0xFFFFFF00):
offset = NUM_BYTE_REG & 0xFF
self.num_bytes_reg = struct.unpack('<I', data[offset:offset+4])[0]
print(f" NUM_BYTE_REG = {self.num_bytes_reg}")
if base == (CTRL_REG & 0xFFFFFF00):
offset = CTRL_REG & 0xFF
ctrl_value = struct.unpack('<I', data[offset:offset+4])[0]
self._handle_ctrl_write(ctrl_value)
def _handle_ctrl_write(self, value: int) -> None:
"""Handle CTRL_REG write (triggers state machine)."""
print(f" CTRL_REG = 0x{value:02X}", end="")
status_block = self._ensure_block(STATUS_REG)
status_offset = STATUS_REG & 0xFF
if value == 0x01:
# START_REQUEST or part of WRITE_DATA sequence
# Set STATUS[11] = 1 (busy/ack)
status = struct.unpack('<I', status_block[status_offset:status_offset+4])[0]
status |= (1 << 11)
struct.pack_into('<I', status_block, status_offset, status)
print(" → START (STATUS[11]=1)")
elif value == 0x00:
# END_REQUEST
# Clear STATUS[11]
status = struct.unpack('<I', status_block[status_offset:status_offset+4])[0]
status &= ~(1 << 11)
# Clear error bits [1,4,5,6,10]
status &= ~((1 << 1) | (1 << 4) | (1 << 5) | (1 << 6) | (1 << 10))
struct.pack_into('<I', status_block, status_offset, status)
print(" → END (STATUS[11]=0, errors cleared)")
elif value == 0x03:
# WRITE_DATA / READ_DATA sequence
# Set STATUS[12] = 1 (tx_fifo_empty / ready)
status = struct.unpack('<I', status_block[status_offset:status_offset+4])[0]
status |= (1 << 12)
struct.pack_into('<I', status_block, status_offset, status)
print(" → WRITE/READ_DATA (STATUS[12]=1)")
# Execute flash operation if CMD is set
if self.cmd_reg != 0:
self._execute_flash_operation()
else:
print()
def _handle_register_read(self, address: int, block: bytearray) -> None:
"""Prepare register read response."""
base = address & 0xFFFFFF00
# Read STATUS_REG
if base == (STATUS_REG & 0xFFFFFF00):
offset = STATUS_REG & 0xFF
status = struct.unpack('<I', block[offset:offset+4])[0]
print(f" STATUS_REG = 0x{status:08X} (bit11={status>>11&1}, bit12={status>>12&1})")
# Read RX_FIFO_REG (flash status or read data)
if base == (RX_FIFO_REG & 0xFFFFFF00):
offset = RX_FIFO_REG & 0xFF
rx_data = struct.unpack('<I', block[offset:offset+4])[0]
print(f" RX_FIFO_REG = 0x{rx_data:08X}")
def _execute_flash_operation(self) -> None:
"""Execute flash operation based on CMD_REG."""
cmd = self.cmd_reg
addr = self.addr_reg
num_bytes = self.num_bytes_reg
# Prepare RX_FIFO response
rx_block = self._ensure_block(RX_FIFO_REG)
rx_offset = RX_FIFO_REG & 0xFF
if cmd == 0x06:
print(f" → Flash: Write Enable")
# No action needed
elif cmd == 0xD8:
print(f" → Flash: ERASE sector at 0x{addr:08X}")
self._flash_erase(addr, 65536)
# Set RX_FIFO[0] = 1 (busy), will clear on next wait_flash
struct.pack_into('<I', rx_block, rx_offset, 0x01)
self.flash_busy = True
elif cmd == 0x02:
print(f" → Flash: WRITE 256 bytes to 0x{addr:08X}")
self._flash_write(addr, self.tx_fifo_buffer)
struct.pack_into('<I', rx_block, rx_offset, 0x01)
self.flash_busy = True
elif cmd == 0x0B:
print(f" → Flash: READ {num_bytes} bytes from 0x{addr:08X}")
data = self._flash_read(addr, num_bytes)
# Store first 4 bytes in RX_FIFO
if len(data) >= 4:
value = struct.unpack('<I', data[:4])[0]
struct.pack_into('<I', rx_block, rx_offset, value)
print(f" RX_FIFO[0] = 0x{struct.unpack('<I', rx_block[rx_offset:rx_offset+4])[0]:08X}")
elif cmd == 0x05:
# Read flash status register
if self.flash_busy:
print(f" → Flash: READ STATUS (busy=1)")
struct.pack_into('<I', rx_block, rx_offset, 0x01)
self.flash_busy = False # Next read will show ready
else:
print(f" → Flash: READ STATUS (ready=0)")
struct.pack_into('<I', rx_block, rx_offset, 0x00)
elif cmd == 0x61:
print(f" → Flash: Write Volatile Config Register")
elif cmd == 0xB7:
print(f" → Flash: Enter 4-byte Address Mode")
else:
print(f" → Flash: Unknown CMD 0x{cmd:02X}")
def _flash_erase(self, address: int, length: int) -> None:
"""Erase flash sector (fill with 0xFF)."""
try:
with open(self.flash_image_path, "r+b") as f:
f.seek(address)
f.write(b"\xFF" * length)
except Exception as e:
print(f" ERROR: {e}")
def _flash_write(self, address: int, data: bytes) -> None:
"""Write to flash."""
try:
with open(self.flash_image_path, "r+b") as f:
f.seek(address)
f.write(data)
except Exception as e:
print(f" ERROR: {e}")
def _flash_read(self, address: int, length: int) -> bytes:
"""Read from flash."""
try:
with open(self.flash_image_path, "rb") as f:
f.seek(address)
return f.read(length)
except Exception as e:
print(f" ERROR: {e}")
return b"\xFF" * length
def parse_srio_filename(filename: str) -> Optional[Tuple[str, int, int]]:
"""Parse SRIO filename format: $SRIO:<slot>/<address>+<len>.
Returns:
(slot, address, length) or None if not a valid SRIO command.
Examples:
>>> parse_srio_filename("$SRIO:0x10/0x1000+256")
('0x10', 4096, 256)
"""
match = re.match(r"\$SRIO:([^/]+)/(0x[0-9A-Fa-f]+)\+(\d+)", filename)
if match:
slot = match.group(1)
address = int(match.group(2), 16)
length = int(match.group(3))
return (slot, address, length)
return None
class SRIOTFTPHandler:
"""TFTP handler that processes SRIO commands via flash controller."""
def __init__(self, workdir: Path, flash_controller: SRIOFlashController) -> None:
self.workdir = workdir
self.flash_controller = flash_controller
self.workdir.mkdir(parents=True, exist_ok=True)
def handle_read(self, filename: str) -> Optional[bytes]:
"""Handle TFTP GET (read) request."""
parsed = parse_srio_filename(filename)
if parsed:
slot, address, length = parsed
print(f"SRIO READ: slot={slot}, addr=0x{address:08X}, len={length}")
try:
data = self.flash_controller.read_data(address, length)
return data
except Exception as e:
print(f" ERROR: {e}")
return None
else:
# Normal file read
filepath = self.workdir / filename
if filepath.exists():
with open(filepath, "rb") as f:
return f.read()
return None
def handle_write(self, filename: str, data: bytes) -> bool:
"""Handle TFTP PUT (write) request."""
parsed = parse_srio_filename(filename)
if parsed:
slot, address, length = parsed
print(f"SRIO WRITE: slot={slot}, addr=0x{address:08X}, len={len(data)}")
try:
self.flash_controller.write_data(address, data)
return True
except Exception as e:
print(f" ERROR: {e}")
return False
else:
# Normal file write
filepath = self.workdir / filename
with open(filepath, "wb") as f:
f.write(data)
print(f"File written: {filepath}")
return True
class SimpleTFTPServer:
"""Minimal TFTP server (RFC 1350) for Windows compatibility."""
# TFTP opcodes
OP_RRQ = 1
OP_WRQ = 2
OP_DATA = 3
OP_ACK = 4
OP_ERROR = 5
def __init__(self, host: str, port: int, handler: SRIOTFTPHandler) -> None:
self.host = host
self.port = port
self.handler = handler
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind((host, port))
self.block_size = 512
def handle_rrq(self, filename: str, addr: tuple) -> None:
"""Handle Read Request (GET)."""
print(f"\nRRQ from {addr[0]}:{addr[1]}: {filename}")
transfer_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
transfer_sock.bind(('0.0.0.0', 0))
try:
data = self.handler.handle_read(filename)
if data is None:
packet = struct.pack("!HH", self.OP_ERROR, 1) + b"File not found\x00"
transfer_sock.sendto(packet, addr)
return
# Send data in 512-byte blocks
block_num = 1
offset = 0
while offset < len(data):
chunk = data[offset : offset + self.block_size]
packet = struct.pack("!HH", self.OP_DATA, block_num) + chunk
transfer_sock.sendto(packet, addr)
transfer_sock.settimeout(5.0)
try:
ack_packet, _ = transfer_sock.recvfrom(4096)
opcode = struct.unpack("!H", ack_packet[:2])[0]
if opcode != self.OP_ACK:
return
except socket.timeout:
print(f" Timeout waiting for ACK {block_num}")
return
offset += len(chunk)
block_num += 1
if len(chunk) < self.block_size:
break
print(f" ✓ RRQ completed: {len(data)} bytes sent\n")
finally:
transfer_sock.close()
def handle_wrq(self, filename: str, addr: tuple) -> None:
"""Handle Write Request (PUT)."""
print(f"\nWRQ from {addr[0]}:{addr[1]}: {filename}")
transfer_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
transfer_sock.bind(('0.0.0.0', 0))
try:
# Send initial ACK (block 0)
packet = struct.pack("!HH", self.OP_ACK, 0)
transfer_sock.sendto(packet, addr)
received_data = bytearray()
expected_block = 1
while True:
transfer_sock.settimeout(10.0)
try:
packet, data_addr = transfer_sock.recvfrom(4096)
opcode = struct.unpack("!H", packet[:2])[0]
if opcode == self.OP_DATA:
block_num = struct.unpack("!H", packet[2:4])[0]
data = packet[4:]
if block_num == expected_block:
received_data.extend(data)
ack_packet = struct.pack("!HH", self.OP_ACK, block_num)
transfer_sock.sendto(ack_packet, data_addr)
expected_block += 1
if len(data) < self.block_size:
break
except socket.timeout:
print(f" Timeout waiting for DATA block {expected_block}")
return
success = self.handler.handle_write(filename, bytes(received_data))
if success:
print(f" ✓ WRQ completed: {len(received_data)} bytes received\n")
finally:
transfer_sock.close()
def handle_request(self, packet: bytes, addr: tuple) -> None:
"""Handle incoming TFTP request."""
opcode = struct.unpack("!H", packet[:2])[0]
if opcode == self.OP_RRQ:
parts = packet[2:].split(b"\x00")
if len(parts) >= 2:
filename = parts[0].decode('ascii', errors='ignore')
self.handle_rrq(filename, addr)
elif opcode == self.OP_WRQ:
parts = packet[2:].split(b"\x00")
if len(parts) >= 2:
filename = parts[0].decode('ascii', errors='ignore')
self.handle_wrq(filename, addr)
def serve_forever(self) -> None:
"""Main server loop."""
print(f"TFTP server listening on {self.host}:{self.port}")
print("Press Ctrl+C to stop.\n")
while True:
try:
packet, addr = self.sock.recvfrom(4096)
thread = threading.Thread(target=self.handle_request, args=(packet, addr), daemon=True)
thread.start()
except KeyboardInterrupt:
print("\nShutting down TFTP server.")
break
def main() -> None:
parser = argparse.ArgumentParser(description="TFTP receiver emulator for SRIO flash protocol")
parser.add_argument("--port", type=int, default=6969, help="TFTP listen port (default: 6969)")
parser.add_argument(
"--workdir",
type=Path,
default=Path("./tftp_root"),
help="Working directory for TFTP files",
)
parser.add_argument(
"--flash-image",
type=Path,
default=Path("./tftp_root/target_flash.img"),
help="Flash memory image file",
)
parser.add_argument(
"--flash-size",
type=int,
default=16 * 1024 * 1024,
help="Flash size in bytes (default: 16 MB)",
)
args = parser.parse_args()
workdir = args.workdir
workdir.mkdir(parents=True, exist_ok=True)
flash_controller = SRIOFlashController(args.flash_image, args.flash_size)
handler = SRIOTFTPHandler(workdir, flash_controller)
print(f"Working directory: {workdir.absolute()}")
print(f"Flash image: {args.flash_image.absolute()}")
print(f"Flash size: {args.flash_size // (1024*1024)} MB\n")
server = SimpleTFTPServer("0.0.0.0", args.port, handler)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped.")
sys.exit(0)
if __name__ == "__main__":
main()