Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ micropython-steami-lib/
│ │ ├── manifest.py # Manifest file for firmware inclusion
│ │ ├── bq27441/ # Driver source code
│ │ └── examples/ # Usage examples
│ ├── w2564jv/ # SPI flash memory W2564JV-DTR
│ ├── daplink_flash/ # DAPLink Flash bridge (I2C to W25Q64JV)
│ ├── ssd1327/ # OLED display controller SSD1327ZB
│ ├── mcp23009/ # I2C I/O expander MCP23009
│ ├── vl53l1cx/ # Distance sensor VL53L1CX
Expand Down Expand Up @@ -90,31 +90,33 @@ bq.capacity_full() # Full capacity in mAh
bq.capacity_remaining() # Remaining capacity in mAh
```

### W2564JV-DTR (SPI Flash Memory)
### DAPLink Flash (I2C Flash Bridge)

The W2564JV-DTR is a 64 Mbit SPI flash memory.
The STeaMi board has a W25Q64JV 64 Mbit SPI flash memory connected to the STM32F103 (DAPLink). The STM32WB55 accesses it via I2C through the DAPLink bridge.

#### Mounting and Running an Example

```bash
# Mount the driver and run the example
mpremote mount lib/w2564jv run lib/w2564jv/examples/flash_read_write.py
mpremote mount lib/daplink_flash run lib/daplink_flash/examples/write_csv.py
```

#### Basic API

```python
from w2564jv import W2564JV
from daplink_flash import DaplinkFlash

# Initialization with SPI
spi = machine.SPI(0)
cs = machine.Pin(5, machine.Pin.OUT)
flash = W2564JV(spi, cs)
# Initialization
i2c = machine.I2C(1)
flash = DaplinkFlash(i2c)

# Write a CSV file
flash.set_filename("DATA", "CSV")
flash.write_line("temperature;humidity")
flash.write_line("25.3;48.2")

# Reading and writing
data = b'Hello, STeaMi!'
flash.write(0, data)
read_data = flash.read(0, len(data))
# Erase flash
flash.clear_flash()
```

### SSD1327ZB (OLED Display Controller)
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions lib/daplink_flash/daplink_flash/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .device import DaplinkFlash

__all__ = ["DaplinkFlash"]
35 changes: 35 additions & 0 deletions lib/daplink_flash/daplink_flash/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from micropython import const

# I2C address (7-bit) — 0x76 in 8-bit (CODAL convention)
DAPLINK_FLASH_DEFAULT_ADDR = const(0x3B)

# WHO_AM_I expected value
DAPLINK_FLASH_WHO_AM_I_VAL = const(0x4C)

# Commands
CMD_WHO_AM_I = const(0x01)
CMD_SET_FILENAME = const(0x03)
CMD_GET_FILENAME = const(0x04)
CMD_CLEAR_FLASH = const(0x10)
CMD_WRITE_DATA = const(0x11)
CMD_READ_SECTOR = const(0x20)

# Registers
REG_STATUS = const(0x80)
REG_ERROR = const(0x81)

# Status register bits
STATUS_BUSY = const(0x80)

# Error register bits
ERROR_BAD_PARAM = const(0x01)
ERROR_FILE_FULL = const(0x20)
ERROR_BAD_FILENAME = const(0x40)
ERROR_CMD_FAILED = const(0x80)

# Protocol limits
MAX_WRITE_CHUNK = const(30)
SECTOR_SIZE = const(256)
MAX_SECTORS = const(32768)
FILENAME_LEN = const(8)
EXT_LEN = const(3)
173 changes: 173 additions & 0 deletions lib/daplink_flash/daplink_flash/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from time import sleep_ms

from daplink_flash.const import *


class DaplinkFlash(object):
"""MicroPython driver for the DAPLink Flash bridge (STM32F103 → W25Q64JV)."""

def __init__(self, i2c, address=DAPLINK_FLASH_DEFAULT_ADDR):
self.i2c = i2c
self.address = address
self._buffer_1 = bytearray(1)

# --------------------------------------------------
# Low level I2C
# --------------------------------------------------

def _read_reg(self, reg, n=1):
"""Read n bytes from register."""
if n == 1:
self.i2c.readfrom_mem_into(self.address, reg, self._buffer_1)
return self._buffer_1[0]
return self.i2c.readfrom_mem(self.address, reg, n)

def _write_reg(self, reg, data):
"""Write data bytes to register."""
self.i2c.writeto_mem(self.address, reg, data)

def _write_cmd(self, cmd):
"""Write a single command byte (no payload)."""
self._buffer_1[0] = cmd
self.i2c.writeto(self.address, self._buffer_1)

# --------------------------------------------------
# Device identification
# --------------------------------------------------

def device_id(self):
"""Read WHO_AM_I register. Expected: 0x4C."""
return self._read_reg(CMD_WHO_AM_I)

# --------------------------------------------------
# Status and error registers
# --------------------------------------------------

def _status(self):
"""Read raw status register."""
return self._read_reg(REG_STATUS)

def _error(self):
"""Read raw error register."""
return self._read_reg(REG_ERROR)

def busy(self):
"""Return True if flash is busy."""
return bool(self._status() & STATUS_BUSY)

def _wait_busy(self, timeout_ms=30000):
"""Poll busy bit until clear. Raises OSError on timeout."""
elapsed = 0
while self.busy():
sleep_ms(5)
elapsed += 5
if elapsed >= timeout_ms:
raise OSError("DAPLink Flash busy timeout")

# --------------------------------------------------
# Filename management
# --------------------------------------------------

def set_filename(self, name, ext):
"""Set 8.3 filename. name: max 8 chars, ext: max 3 chars."""
self._wait_busy()
n = name.upper().encode("ascii")[:FILENAME_LEN]
e = ext.upper().encode("ascii")[:EXT_LEN]
padded = n + b" " * (FILENAME_LEN - len(n)) + e + b" " * (EXT_LEN - len(e))
self._write_reg(CMD_SET_FILENAME, padded)

def get_filename(self):
"""Read current filename. Returns (name, ext) tuple, stripped."""
self._wait_busy()
raw = self._read_reg(CMD_GET_FILENAME, FILENAME_LEN + EXT_LEN)
name = bytes(raw[:FILENAME_LEN]).decode().rstrip()
ext = bytes(raw[FILENAME_LEN:]).decode().rstrip()
return (name, ext)

# --------------------------------------------------
# Flash operations
# --------------------------------------------------

def clear_flash(self):
"""Erase entire flash memory."""
self._wait_busy()
self._write_cmd(CMD_CLEAR_FLASH)

def write(self, data):
"""Append data to current file. data: bytes or str.

Returns the number of bytes written.
"""
if isinstance(data, str):
data = data.encode()
offset = 0
length = len(data)
buf = bytearray(MAX_WRITE_CHUNK + 2)
buf[0] = CMD_WRITE_DATA
while offset < length:
self._wait_busy()
chunk_len = min(MAX_WRITE_CHUNK, length - offset)
buf[1] = chunk_len
buf[2 : 2 + chunk_len] = data[offset : offset + chunk_len]
# Zero-pad remainder
for i in range(2 + chunk_len, len(buf)):
buf[i] = 0
self.i2c.writeto(self.address, buf)
offset += chunk_len
self._wait_busy()
err = self._error()
if err:
raise OSError("DAPLink Flash write error: 0x{:02X}".format(err))
return length
Comment thread
nedseb marked this conversation as resolved.

def write_line(self, text):
"""Append text + newline to current file."""
return self.write(text + "\n")

# --------------------------------------------------
# Read operations
# --------------------------------------------------

def read_sector(self, sector):
"""Read a 256-byte sector from flash.

Args:
sector: sector number (0-32767).

Returns:
bytes: 256 bytes of data.
"""
self._wait_busy()
self._write_reg(CMD_READ_SECTOR, bytes([sector >> 8, sector & 0xFF]))
# F103 processes the command in its 30ms hook, then sets up DMA.
# After DMA setup, the F103 is no longer in listen mode — only
# a plain readfrom() will work (no register-based status poll).
sleep_ms(200)
return self.i2c.readfrom(self.address, SECTOR_SIZE)

def read(self, length=None):
"""Read file content from flash.

Args:
length: max bytes to read. If None, reads until first 0xFF.

Returns:
bytes: file content.
"""
if length is not None and length <= 0:
return b""
result = bytearray()
sector = 0
while sector < MAX_SECTORS:
data = self.read_sector(sector)
for i in range(SECTOR_SIZE):
if length is not None:
result.append(data[i])
if len(result) >= length:
Comment thread
nedseb marked this conversation as resolved.
return bytes(result)
else:
if data[i] == 0xFF:
return bytes(result)
result.append(data[i])
sector += 1
return bytes(result)
18 changes: 18 additions & 0 deletions lib/daplink_flash/examples/erase_flash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Erase all data from the flash memory."""

from machine import I2C
from time import sleep_ms
from daplink_flash import DaplinkFlash

i2c = I2C(1)
flash = DaplinkFlash(i2c)

name, ext = flash.get_filename()
print("Current file: {}.{}".format(name, ext))

print("Erasing flash...")
flash.clear_flash()
sleep_ms(1000)

print("Done. Flash is empty.")
print("ERROR: 0x{:02X}".format(flash._error()))
16 changes: 16 additions & 0 deletions lib/daplink_flash/examples/flash_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Display DAPLink Flash bridge status and filename."""

from machine import I2C
from daplink_flash import DaplinkFlash

i2c = I2C(1)
flash = DaplinkFlash(i2c)

print("=== DAPLink Flash Info ===")
print("WHO_AM_I: 0x{:02X}".format(flash.device_id()))
print("STATUS: 0x{:02X}".format(flash._status()))
print("ERROR: 0x{:02X}".format(flash._error()))
print("Busy: ", flash.busy())

name, ext = flash.get_filename()
print("Filename: {}.{}".format(name, ext))
19 changes: 19 additions & 0 deletions lib/daplink_flash/examples/read_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Read and display the current file stored on flash."""

from machine import I2C
from daplink_flash import DaplinkFlash

i2c = I2C(1)
flash = DaplinkFlash(i2c)

name, ext = flash.get_filename()
print("Reading file: {}.{}".format(name, ext))
print()

content = flash.read()
if len(content) == 0:
print("(empty)")
else:
print(content.decode())
print("---")
print("{} bytes".format(len(content)))
Loading
Loading