"""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:/
+) 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(' 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(' None: """Prepare register read response.""" base = address & 0xFFFFFF00 # Read STATUS_REG if base == (STATUS_REG & 0xFFFFFF00): offset = STATUS_REG & 0xFF status = struct.unpack('>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(' 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('= 4: value = struct.unpack(' 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:/
+. 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()