386 lines
14 KiB
Python
386 lines
14 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)
|
|
"""Simulates a flash memory using a file on disk."""
|
|
|
|
def __init__(self, image_path: Path, size: int = 16 * 1024 * 1024) -> None:
|
|
self.image_path = image_path
|
|
self.size = size
|
|
if not image_path.exists():
|
|
# Create empty flash image
|
|
with open(image_path, "wb") as f:
|
|
f.write(b"\xFF" * size)
|
|
print(f"Flash emulator initialized: {image_path} ({size} bytes)")
|
|
|
|
def read(self, address: int, length: int) -> bytes:
|
|
"""Read from flash."""
|
|
if address + length > self.size:
|
|
raise ValueError(f"Read out of bounds: 0x{address:X}+{length} > {self.size}")
|
|
with open(self.image_path, "rb") as f:
|
|
f.seek(address)
|
|
return f.read(length)
|
|
|
|
def write(self, address: int, data: bytes) -> None:
|
|
"""Write to flash."""
|
|
length = len(data)
|
|
if address + length > self.size:
|
|
raise ValueError(f"Write out of bounds: 0x{address:X}+{length} > {self.size}")
|
|
with open(self.image_path, "r+b") as f:
|
|
f.seek(address)
|
|
f.write(data)
|
|
print(f" Flash write: 0x{address:08X} + {length} bytes")
|
|
|
|
def erase(self, address: int, length: int = 65536) -> None:
|
|
"""Erase flash region (fill with 0xFF)."""
|
|
if address + length > self.size:
|
|
raise ValueError(f"Erase out of bounds: 0x{address:X}+{length} > {self.size}")
|
|
with open(self.image_path, "r+b") as f:
|
|
f.seek(address)
|
|
f.write(b"\xFF" * length)
|
|
print(f" Flash erase: 0x{address:08X} + {length} bytes")
|
|
|
|
|
|
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:
|
|
"""Custom TFTP handler that processes SRIO commands."""
|
|
|
|
def __init__(self, workdir: Path, flash: FlashEmulator) -> None:
|
|
self.workdir = workdir
|
|
self.flash = flash
|
|
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.read(address, length)
|
|
return data
|
|
except Exception as e:
|
|
print(f" ERROR: {e}")
|
|
return None
|
|
else:
|
|
# Normal file read (not SRIO command)
|
|
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.write(address, data)
|
|
return True
|
|
except Exception as e:
|
|
print(f" ERROR: {e}")
|
|
return False
|
|
else:
|
|
# Normal file write (not SRIO command)
|
|
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.
|
|
|
|
Supports RRQ and WRQ operations with custom handler for SRIO commands.
|
|
"""
|
|
|
|
# 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 send_error(self, error_code: int, error_msg: str, addr: tuple) -> None:
|
|
"""Send TFTP ERROR packet."""
|
|
packet = struct.pack("!HH", self.OP_ERROR, error_code) + error_msg.encode() + b"\x00"
|
|
self.sock.sendto(packet, addr)
|
|
|
|
def send_ack(self, block_num: int, addr: tuple) -> None:
|
|
"""Send TFTP ACK packet."""
|
|
packet = struct.pack("!HH", self.OP_ACK, block_num)
|
|
self.sock.sendto(packet, addr)
|
|
|
|
def send_data(self, block_num: int, data: bytes, addr: tuple) -> None:
|
|
"""Send TFTP DATA packet."""
|
|
packet = struct.pack("!HH", self.OP_DATA, block_num) + data
|
|
self.sock.sendto(packet, addr)
|
|
|
|
def handle_rrq(self, filename: str, addr: tuple) -> None:
|
|
"""Handle Read Request (GET)."""
|
|
print(f"RRQ from {addr}: {filename}")
|
|
|
|
# Create dedicated socket for this transfer (RFC 1350)
|
|
transfer_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
transfer_sock.bind(('0.0.0.0', 0)) # Use ephemeral port
|
|
|
|
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)
|
|
|
|
# Wait for ACK with timeout
|
|
transfer_sock.settimeout(5.0)
|
|
try:
|
|
ack_packet, ack_addr = transfer_sock.recvfrom(4096)
|
|
opcode = struct.unpack("!H", ack_packet[:2])[0]
|
|
if opcode != self.OP_ACK:
|
|
print(f" Expected ACK, got opcode {opcode}")
|
|
return
|
|
ack_block = struct.unpack("!H", ack_packet[2:4])[0]
|
|
if ack_block != block_num:
|
|
print(f" ACK mismatch: expected {block_num}, got {ack_block}")
|
|
return
|
|
except socket.timeout:
|
|
print(f" Timeout waiting for ACK {block_num}")
|
|
return
|
|
|
|
offset += len(chunk)
|
|
block_num += 1
|
|
|
|
# Last block (less than 512 bytes)
|
|
if len(chunk) < self.block_size:
|
|
break
|
|
|
|
print(f" RRQ completed: {len(data)} bytes sent")
|
|
finally:
|
|
transfer_sock.close()
|
|
|
|
def handle_wrq(self, filename: str, addr: tuple) -> None:
|
|
"""Handle Write Request (PUT)."""
|
|
print(f"WRQ from {addr}: {filename}")
|
|
|
|
# Create dedicated socket for this transfer (RFC 1350)
|
|
transfer_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
transfer_sock.bind(('0.0.0.0', 0)) # Use ephemeral port
|
|
|
|
try:
|
|
# Send initial ACK (block 0)
|
|
packet = struct.pack("!HH", self.OP_ACK, 0)
|
|
transfer_sock.sendto(packet, addr)
|
|
|
|
# Receive data blocks
|
|
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
|
|
|
|
# Last block (less than 512 bytes)
|
|
if len(data) < self.block_size:
|
|
break
|
|
else:
|
|
print(f" Block mismatch: expected {expected_block}, got {block_num}")
|
|
# Re-send last ACK
|
|
ack_packet = struct.pack("!HH", self.OP_ACK, expected_block - 1)
|
|
transfer_sock.sendto(ack_packet, data_addr)
|
|
|
|
elif opcode == self.OP_ERROR:
|
|
error_code = struct.unpack("!H", packet[2:4])[0]
|
|
error_msg = packet[4:].decode('ascii', errors='ignore').rstrip('\x00')
|
|
print(f" Client error {error_code}: {error_msg}")
|
|
return
|
|
|
|
except socket.timeout:
|
|
print(f" Timeout waiting for DATA block {expected_block}")
|
|
return
|
|
|
|
# Process received data
|
|
success = self.handler.handle_write(filename, bytes(received_data))
|
|
if success:
|
|
print(f" WRQ completed: {len(received_data)} bytes received")
|
|
else:
|
|
print(f" WRQ failed")
|
|
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:
|
|
# Parse RRQ: opcode, filename\0, mode\0
|
|
parts = packet[2:].split(b"\x00")
|
|
if len(parts) >= 2:
|
|
filename = parts[0].decode('ascii', errors='ignore')
|
|
mode = parts[1].decode('ascii', errors='ignore')
|
|
self.handle_rrq(filename, addr)
|
|
|
|
elif opcode == self.OP_WRQ:
|
|
# Parse WRQ: opcode, filename\0, mode\0
|
|
parts = packet[2:].split(b"\x00")
|
|
if len(parts) >= 2:
|
|
filename = parts[0].decode('ascii', errors='ignore')
|
|
mode = parts[1].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)
|
|
# Handle each request in a separate thread
|
|
thread = threading.Thread(target=self.handle_request, args=(packet, addr), daemon=True)
|
|
thread.start()
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down TFTP server.")
|
|
break
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="TFTP receiver emulator for SRIO 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 = FlashEmulator(args.flash_image, args.flash_size)
|
|
handler = SRIOTFTPHandler(workdir, flash)
|
|
|
|
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("\nShutting down TFTP server.")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|