Initial import: ESP32 ZiFi32 firmware with SMB2/3 VFS bridge

Based on https://github.com/tslabs/zx-evo/tree/master/pentevo/esp32,
extended with an SMB2/3 server that exposes the ZX Evolution's real SD
card as a network share (browse/download/upload/delete), plus assorted
stability and protocol-correctness fixes in vfs_bridge.cpp and the
vendored libsmb2 - see firmware/README.md for the detailed changelog
and current status.

Also adds:
- firmware/: ready-to-flash compiled binaries + flashing instructions
- hardware/: adapter board connection diagram, PCB photo, and Gerber
  files
- src/third_party/libsmb2: vendored from sahlberg/libsmb2 (LGPL-2.1,
  see its own COPYING/LICENCE files), de-submoduled to a plain
  snapshot with local patches applied directly

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 16:00:05 +03:00
co-authored by Claude Sonnet 5
commit 6d835bc44c
377 changed files with 189430 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

+80
View File
@@ -0,0 +1,80 @@
from pyftdi.spi import SpiController, SpiIOError
from pyftdi.ftdi import Ftdi as ft
from binascii import hexlify
# ---------------------------------------------------------------------
def spix(d):
a = spi.exchange(d, duplex=True)
# print(' '.join(['{:02x}'.format(b) for b in a]))
return a
def wr_cmd(cmd):
# wr ESP_SPI_CMD_WR_REGS
# wr REG_CMD
# dummy
# wr <cmd>
# wr ESP_ST_IDLE
buf = b'\x01\x08\x00'
buf = buf + cmd.to_bytes(1, 'little')
buf = buf + b'\x00'
a = spix(buf)
def rd_reg8(reg):
# wr ESP_SPI_CMD_RD_REGS
# wr <reg>
# dummy
# rd <data>
buf = b'\x02'
buf = buf + reg.to_bytes(1, 'little')
buf = buf + b'\x00\x00'
a = spix(buf)
return a[3]
def rd_reg32(reg):
# wr ESP_SPI_CMD_RD_REGS
# wr <reg>
# dummy
# rd <data>
buf = b'\x02'
buf = buf + reg.to_bytes(1, 'little')
buf = buf + b'\x00\x00\x00\x00\x00'
a = spix(buf)
return int.from_bytes(a[3:7], 'little')
def rd_dma(len):
# wr ESP_SPI_CMD_RD_DMA
# dummy
# dummy
# rd <data> ...
buf = b'\x04'
a = spi.exchange(buf, len + 3, duplex=True)
return a[3:]
def wait_data():
i = 20
while (i):
if (rd_reg8(0x09) == 0x05): # ESP_ST_DATA_S2M
break
i -= 1
# ---------------------------------------------------------------------
ft.show_devices()
spi = SpiController()
spi.configure('ftdi://ftdi:232h:0:1/1')
spi = spi.get_port(cs=0, freq=14000000, mode=0)
wr_cmd(0x01) # Send GET_INFO command
wait_data() # Wait for data
len = rd_reg32(0x04) # Read data length
print(rd_dma(len)) # Read info string
wr_cmd(0xD0) # Stop DMA transaction
wr_cmd(0xF2)
wait_data()
len = rd_reg32(0x04)
print(rd_dma(len))
wr_cmd(0xD0)
+7
View File
@@ -0,0 +1,7 @@
To get Python test working:
1. Run Zadig.
2. Select Single RS232-HS (List all devices).
3. Install libusb-win32 driver.
To restore driver which supports C++ libs, uninstall device removing drivers, and install FTDIBUS driver (https://ftdichip.com/drivers/d2xx-drivers/).
+243
View File
@@ -0,0 +1,243 @@
import time
import spidev
# -------------------- registers --------------------
ESP_REG_COMMAND = 0x00 # 1 byte
ESP_REG_STATUS = 0x01 # 1 byte
# String commands
ESP_REG_STRING_TYPE = 0x02 # 1 byte
ESP_REG_STRING_SIZE = 0x02 # 1 byte
ESP_REG_STRING_DATA = 0x03 # byte array
# Object/data commands
ESP_REG_OBJ_TYPE = 0x02 # 1 byte
ESP_REG_DATA_SIZE = 0x03 # 4 bytes
ESP_REG_DATA_OFFSET = 0x07 # 4 bytes
ESP_REG_OBJ_HANDLE = 0x0B # 1 byte
# Lib commands
ESP_REG_FUNC = 0x02
ESP_REG_OPT = 0x03
ESP_REG_ARG = 0x03 # 4 bytes
ESP_REG_RETVAL = 0x07 # 4 bytes
ESP_REG_ARR1_HANDLE = 0x0C
ESP_REG_ARR2_HANDLE = 0x0D
ESP_REG_ARR3_HANDLE = 0x0E
ESP_REG_LIB_HANDLE = 0x0F
# Network
ESP_REG_NETSTATE = 0x02
ESP_REG_IP = 0x2C
ESP_REG_OWN_IP = 0x30
ESP_REG_MASK = 0x34
ESP_REG_GATE = 0x38
# Stats
ESP_EXEC_TIME = 0x3C
# -------------------- commands --------------------
ESP_CMD_NOP = 0x00
ESP_CMD_GET_INFO_STR = 0x01
ESP_CMD_GET_NETSTATE = 0x11
ESP_CMD_WSCAN = 0x12
ESP_CMD_SET_AP_NAME = 0x13
ESP_CMD_SET_AP_PWD = 0x14
ESP_CMD_AP_CONNECT = 0x16
ESP_CMD_GET_IP = 0x19
ESP_CMD_HTTP_GET = 0x20
ESP_CMD_XM_INIT = 0xA1
ESP_CMD_XM_PLAY = 0xA3
ESP_CMD_XM_STOP = 0xA4
ESP_CMD_LOAD_ELF = 0xD0
ESP_CMD_RUN_FUNC0 = 0xD2
ESP_CMD_RUN_FUNC1 = 0xD3
ESP_CMD_RUN_FUNC2 = 0xD4
ESP_CMD_RUN_FUNC3 = 0xD5
ESP_CMD_MAKE_OBJECT = 0xE0
ESP_CMD_WRITE_OBJECT = 0xE1
ESP_CMD_READ_OBJECT = 0xE2
ESP_CMD_DELETE_OBJECT = 0xE3
ESP_CMD_KILL_OBJECTS = 0xE4
ESP_CMD_REBOOT = 0xED
ESP_CMD_RESET = 0xEE
ESP_CMD_GET_RND = 0xF0
ESP_CMD_DEHST = 0xF1
ESP_CMD_UNZIP = 0xF2
ESP_CMD_STREAM_UNZIP = 0xF3
# -------------------- options --------------------
ESP_OPT_DATA_SRAM = 0x01
ESP_OPT_RODATA_SRAM = 0x02
ESP_OPT_BSS_SRAM = 0x04
# -------------------- status --------------------
ESP_ST_IDLE = 0x00
ESP_ST_READY = 0x01
ESP_ST_BUSY = 0x02
ESP_ST_DATA_M2S = 0x03
ESP_ST_DATA_S2M = 0x04
ESP_ST_RESET = 0x7E
# -------------------- object types --------------------
OBJ_TYPE_NONE = 0
OBJ_TYPE_DATA = 1
OBJ_TYPE_DATAF = 2
OBJ_TYPE_ELF = 3
OBJ_TYPE_LIB = 4
OBJ_TYPE_XM = 5
OBJ_TYPE_WAV = 6
OBJ_TYPE_HST = 7
OBJ_TYPE_ZIP = 8
OBJ_TYPE_XMC = 9
# -------------------- spi slave commands --------------------
ESP_SPI_CMD_WR_REGS = 0x01
ESP_SPI_CMD_RD_REGS = 0x02
ESP_SPI_CMD_WR_DATA = 0x03
ESP_SPI_CMD_RD_DATA = 0x04
ESP_SPI_CMD_W_END = 0x07
ESP_SPI_CMD_R_END = 0x08
# -------------------- transfer limits --------------------
DMA_BUF_SIZE = 4000
# -------------------- helpers --------------------
def le32(v: int) -> bytes:
return bytes([(v >> 0) & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF, (v >> 24) & 0xFF])
def u32_from_le(b: bytes) -> int:
return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)
class EspSpi:
def __init__(self, bus=0, dev=0, speed_hz=14_000_000, mode=0):
self.spi = spidev.SpiDev()
self.spi.open(int(bus), int(dev)) # spidev0.0 => 0,0
self.spi.max_speed_hz = int(speed_hz)
self.spi.mode = int(mode)
self.spi.bits_per_word = 8
def rd_regs(self, reg: int, n: int) -> bytes:
tx = [ESP_SPI_CMD_RD_REGS, reg & 0xFF, 0x00] + [0x00] * n
rx = self.spi.xfer2(tx)
return bytes(rx[3:3 + n])
def wr_regs(self, reg: int, data: bytes) -> None:
tx = [ESP_SPI_CMD_WR_REGS, reg & 0xFF, 0x00] + list(data)
self.spi.xfer2(tx)
def rd_reg8(self, reg: int) -> int:
return self.rd_regs(reg, 1)[0]
def rd_reg32(self, reg: int) -> int:
b = self.rd_regs(reg, 4)
return u32_from_le(b)
def wr_reg8(self, reg: int, v: int) -> None:
self.wr_regs(reg, bytes([v & 0xFF]))
def wr_reg32(self, reg: int, v: int) -> None:
self.wr_regs(reg, le32(v))
def cmd(self, cmd: int) -> None:
self.spi.xfer2([ESP_SPI_CMD_WR_REGS, ESP_REG_COMMAND, 0x00, cmd & 0xFF, ESP_ST_IDLE])
def wait_busy(self, timeout_s: float) -> bool:
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
if self.rd_reg8(ESP_REG_STATUS) != ESP_ST_BUSY:
return True
return False
def wait_status(self, want: int, timeout_s: float) -> bool:
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
if self.rd_reg8(ESP_REG_STATUS) == want:
return True
return False
def wait_not_status(self, not_want: int, timeout_s: float) -> bool:
deadline = time.monotonic() + timeout_s
while time.monotonic() < deadline:
if self.rd_reg8(ESP_REG_STATUS) != not_want:
return True
return False
# WR_DATA for one chunk (<= DMA_BUF_SIZE) + W_END + wait_busy
def send_chunk(self, data: bytes, frag: int = 4096) -> None:
i = 0
first = min(len(data), frag)
self.spi.xfer2([ESP_SPI_CMD_WR_DATA, 0x00, 0x00] + list(data[:first]))
i = first
while i < len(data):
part = data[i:i + frag]
self.spi.xfer2(list(part))
i += len(part)
self.spi.xfer2([ESP_SPI_CMD_W_END, 0x00])
self.wait_busy(10.0)
def make_object(self, size: int, obj_type: int, timeout_s: float = 5.0) -> int:
self.wr_reg8(ESP_REG_OBJ_TYPE, obj_type)
self.wr_reg32(ESP_REG_DATA_SIZE, size)
self.cmd(ESP_CMD_MAKE_OBJECT)
if not self.wait_busy(timeout_s):
st = self.rd_reg8(ESP_REG_STATUS)
raise RuntimeError(f"MAKE_OBJECT timeout, status=0x{st:02X}")
st = self.rd_reg8(ESP_REG_STATUS)
if st != ESP_ST_READY:
raise RuntimeError(f"MAKE_OBJECT non-READY, status=0x{st:02X}")
return self.rd_reg8(ESP_REG_OBJ_HANDLE)
# CHUNKED WRITE_OBJECT
def write_object_stream(self, handle: int, data: bytes, offset0: int = 0, chunk_sz: int = DMA_BUF_SIZE) -> None:
self.wr_reg8(ESP_REG_OBJ_HANDLE, handle)
self.wr_reg32(ESP_REG_DATA_OFFSET, offset0)
pos = 0
total = len(data)
while pos < total:
chunk = data[pos:pos + chunk_sz]
self.wr_reg32(ESP_REG_DATA_SIZE, len(chunk))
self.cmd(ESP_CMD_WRITE_OBJECT)
if not self.wait_status(ESP_ST_DATA_M2S, 2.0):
st = self.rd_reg8(ESP_REG_STATUS)
raise RuntimeError(f"WRITE_OBJECT wait DATA_M2S timeout, status=0x{st:02X}")
self.send_chunk(chunk)
if not self.wait_not_status(ESP_ST_DATA_M2S, 2.0):
st = self.rd_reg8(ESP_REG_STATUS)
raise RuntimeError(f"WRITE_OBJECT wait end DATA_M2S timeout, status=0x{st:02X}")
st = self.rd_reg8(ESP_REG_STATUS)
if st != ESP_ST_READY:
raise RuntimeError(f"WRITE_OBJECT non-READY, status=0x{st:02X}")
pos += len(chunk)
def delete_object(self, handle: int, timeout_s: float = 5.0) -> None:
self.wr_reg8(ESP_REG_OBJ_HANDLE, handle)
self.cmd(ESP_CMD_DELETE_OBJECT)
if not self.wait_busy(timeout_s):
st = self.rd_reg8(ESP_REG_STATUS)
raise RuntimeError(f"DELETE_OBJECT timeout, status=0x{st:02X}")
st = self.rd_reg8(ESP_REG_STATUS)
if st != ESP_ST_READY:
raise RuntimeError(f"DELETE_OBJECT non-READY, status=0x{st:02X}")
+33
View File
@@ -0,0 +1,33 @@
--- 1. Enable SPI
sudo nano /boot/firmware/config.txt
Add or uncomment:
dtparam=spi=on
sudo reboot
ls -l /dev/spidev*
--- 2. Connect ESP-SPI
For ESP32-S3:
MOSI GPIO13
MISO GPIO11
SCLK GPIO12
CS GPIO10
For ESP32-P4:
MOSI GPIO20
MISO GPIO22
SCLK GPIO21
CS GPIO23
RPi:
MOSI = GPIO10 (pin 19)
MISO = GPIO9 (pin 21)
SCLK = GPIO11 (pin 23)
CS0 = GPIO8 (pin 24)
CS1 = GPIO7 (pin 26)
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
import time
import spidev
from esp_spi import *
def hexdump(b: bytes, width: int = 16) -> str:
lines = []
for i in range(0, len(b), width):
chunk = b[i:i+width]
lines.append(f"{i:02X} " + " ".join(f"{x:02X}" for x in chunk))
return "\n".join(lines)
def test_make_object_data(size_bytes: int = 1 * 1024 * 1024):
esp = EspSpi()
print(f"MAKE_OBJECT: {size_bytes} bytes, type DATA")
print("Sending ESP_CMD_MAKE_OBJECT")
esp.wr_reg8(ESP_REG_OBJ_TYPE, OBJ_TYPE_DATA)
esp.wr_reg32(ESP_REG_DATA_SIZE, size_bytes)
esp.cmd(ESP_CMD_MAKE_OBJECT)
if not esp.wait_busy(2.0):
st = esp.rd_reg8(ESP_REG_STATUS)
raise RuntimeError(f"MAKE_OBJECT timeout, status=0x{st:02X}")
st = esp.rd_reg8(ESP_REG_STATUS)
if st != ESP_ST_READY:
raise RuntimeError(f"MAKE_OBJECT returned non-READY status=0x{st:02X}")
h = esp.rd_reg8(ESP_REG_OBJ_HANDLE)
print(f"Handle: {h}")
print("Object created")
print("Sending ESP_CMD_DELETE_OBJECT")
esp.wr_reg8(ESP_REG_OBJ_HANDLE, h)
esp.cmd(ESP_CMD_DELETE_OBJECT)
if not esp.wait_busy(2.0):
st = esp.rd_reg8(ESP_REG_STATUS)
raise RuntimeError(f"DELETE_OBJECT timeout, status=0x{st:02X}")
st = esp.rd_reg8(ESP_REG_STATUS)
if st != ESP_ST_READY:
raise RuntimeError(f"DELETE_OBJECT returned non-READY status=0x{st:02X}")
print("Object deleted")
def xm_load_and_play(path: str):
esp = EspSpi(bus=0, dev=0, speed_hz=14_000_000, mode=0)
with open(path, "rb") as f:
xm = f.read()
print(f"XM: {path} size={len(xm)}")
esp.cmd(ESP_CMD_XM_STOP)
esp.wait_busy(1.0)
# create XM object and upload
h = esp.make_object(len(xm), OBJ_TYPE_XM)
print(f"XM handle: {h}")
esp.write_object_stream(h, xm, offset0=0, chunk_sz=DMA_BUF_SIZE)
print("XM uploaded")
# init + play
esp.wr_reg8(ESP_REG_OBJ_HANDLE, h)
esp.cmd(ESP_CMD_XM_INIT)
esp.wait_busy(2.0)
esp.wr_reg8(ESP_REG_OBJ_HANDLE, h)
esp.cmd(ESP_CMD_XM_PLAY)
esp.wait_busy(2.0)
input("Press Enter to stop...")
esp.cmd(ESP_CMD_XM_STOP)
esp.wait_busy(2.0)
esp.wr_reg8(ESP_REG_OBJ_HANDLE, h)
esp.cmd(ESP_CMD_DELETE_OBJECT)
esp.wait_busy(2.0)
print("Done")
def main():
dev = EspSpi(speed_hz=40_000_000, mode=0)
b = dev.rd_regs(0x00, 64)
print("Reg dump (0x00..0x3F):")
print(" 0 1 2 3 4 5 6 7 8 9 A B C D E F")
for row in range(4):
chunk = b[row*16:(row+1)*16]
print(f"{row*16:02X} " + " ".join(f"{x:02X}" for x in chunk))
test_make_object_data()
# xm_load_and_play("xm/astar.xm")
# xm_load_and_play("xm/AT4RE - MagicTweak 4.01kg.xm")
# xm_load_and_play("xm/future dreams.xm")
xm_load_and_play("xm/av_out.xm")
# xm_load_and_play("xm/alien_threat.xm")
if __name__ == "__main__":
main()