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:
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user