Skip to content

Commit 4fd8c05

Browse files
committed
daplink_flash: Add config zone support and hardware test.
Add clear_config(), write_config() and read_config() methods for the 1 KB persistent config zone in the F103 internal flash. Add config_zone.py hardware test example with 10 test cases.
1 parent 43f0a58 commit 4fd8c05

4 files changed

Lines changed: 329 additions & 0 deletions

File tree

lib/daplink_flash/daplink_flash/const.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
CMD_CLEAR_FLASH = const(0x10)
1414
CMD_WRITE_DATA = const(0x11)
1515
CMD_READ_SECTOR = const(0x20)
16+
CMD_WRITE_CONFIG = const(0x30)
17+
CMD_READ_CONFIG = const(0x31)
18+
CMD_CLEAR_CONFIG = const(0x32)
1619

1720
# Registers
1821
REG_STATUS = const(0x80)
@@ -31,5 +34,6 @@
3134
MAX_WRITE_CHUNK = const(30)
3235
SECTOR_SIZE = const(256)
3336
MAX_SECTORS = const(32768)
37+
CONFIG_SIZE = const(1024)
3438
FILENAME_LEN = const(8)
3539
EXT_LEN = const(3)

lib/daplink_flash/daplink_flash/device.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,65 @@ def read(self, length=None):
171171
result.append(data[i])
172172
sector += 1
173173
return bytes(result)
174+
175+
# --------------------------------------------------
176+
# Config zone (1 KB persistent storage in F103 internal flash)
177+
# --------------------------------------------------
178+
179+
def clear_config(self):
180+
"""Erase the 1 KB config zone."""
181+
self._wait_busy()
182+
self._write_cmd(CMD_CLEAR_CONFIG)
183+
sleep_ms(100)
184+
self._wait_busy()
185+
186+
def write_config(self, data, offset=0):
187+
"""Write data to the config zone at the given offset.
188+
189+
The firmware performs a read-modify-write cycle so existing
190+
data outside the written range is preserved.
191+
192+
Args:
193+
data: bytes or str to store.
194+
offset: byte offset within the config zone (0-1023).
195+
"""
196+
if isinstance(data, str):
197+
data = data.encode()
198+
length = len(data)
199+
buf = bytearray(MAX_WRITE_CHUNK + 2)
200+
buf[0] = CMD_WRITE_CONFIG
201+
pos = 0
202+
while pos < length:
203+
self._wait_busy()
204+
chunk_len = min(MAX_WRITE_CHUNK - 3, length - pos)
205+
cur_offset = offset + pos
206+
buf[1] = (cur_offset >> 8) & 0xFF
207+
buf[2] = cur_offset & 0xFF
208+
buf[3] = chunk_len
209+
buf[4 : 4 + chunk_len] = data[pos : pos + chunk_len]
210+
for i in range(4 + chunk_len, len(buf)):
211+
buf[i] = 0
212+
self.i2c.writeto(self.address, buf)
213+
sleep_ms(50)
214+
pos += chunk_len
215+
self._wait_busy()
216+
217+
def read_config(self):
218+
"""Read config zone content.
219+
220+
Returns:
221+
bytes: config data up to first 0xFF, or b'' if empty.
222+
"""
223+
result = bytearray()
224+
for page_offset in range(0, CONFIG_SIZE, SECTOR_SIZE):
225+
self._wait_busy()
226+
hi = (page_offset >> 8) & 0xFF
227+
lo = page_offset & 0xFF
228+
self._write_reg(CMD_READ_CONFIG, bytes([hi, lo]))
229+
sleep_ms(100)
230+
data = self.i2c.readfrom(self.address, SECTOR_SIZE)
231+
for i in range(SECTOR_SIZE):
232+
if data[i] == 0xFF:
233+
return bytes(result)
234+
result.append(data[i])
235+
return bytes(result)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""Test suite for DAPLink internal flash config zone.
2+
3+
The config zone is a 1 KB area in the STM32F103 internal flash (gap
4+
between bootloader and interface firmware at 0x0800BC00). It survives
5+
interface firmware updates and clear_flash operations, making it
6+
suitable for factory data like board revision and sensor calibration.
7+
8+
Commands:
9+
- WRITE_CONFIG (0x30): offset(2) + len(1) + data(N)
10+
- READ_CONFIG (0x31): offset(2) -> 256 bytes
11+
- CLEAR_CONFIG (0x32): erase the entire 1 KB zone
12+
"""
13+
14+
from machine import I2C
15+
from time import sleep_ms
16+
17+
i2c = I2C(1)
18+
addr = 0x3B
19+
passed = 0
20+
failed = 0
21+
22+
23+
def read_status():
24+
return i2c.readfrom_mem(addr, 0x80, 1)[0]
25+
26+
27+
def read_error():
28+
return i2c.readfrom_mem(addr, 0x81, 1)[0]
29+
30+
31+
def wait_busy():
32+
for _ in range(200):
33+
if not (read_status() & 0x80):
34+
return
35+
sleep_ms(10)
36+
raise OSError("busy timeout")
37+
38+
39+
def check(name, condition):
40+
global passed, failed
41+
if condition:
42+
passed += 1
43+
print(" PASS:", name)
44+
else:
45+
failed += 1
46+
print(" FAIL:", name)
47+
48+
49+
def read_config(offset=0):
50+
wait_busy()
51+
i2c.writeto_mem(addr, 0x31, bytes([offset >> 8, offset & 0xFF]))
52+
sleep_ms(100)
53+
return i2c.readfrom(addr, 256)
54+
55+
56+
def write_config(offset, data):
57+
wait_busy()
58+
payload = bytearray([0x30, offset >> 8, offset & 0xFF, len(data)]) + data
59+
while len(payload) < 32:
60+
payload.append(0x00)
61+
i2c.writeto(addr, payload)
62+
sleep_ms(100)
63+
wait_busy()
64+
65+
66+
def clear_config():
67+
wait_busy()
68+
i2c.writeto(addr, bytearray([0x32]))
69+
sleep_ms(100)
70+
wait_busy()
71+
72+
73+
def clear_flash():
74+
wait_busy()
75+
i2c.writeto(addr, bytearray([0x10]))
76+
sleep_ms(1000)
77+
wait_busy()
78+
79+
80+
print("WHO_AM_I:", hex(i2c.readfrom_mem(addr, 0x01, 1)[0]))
81+
82+
# --- Test 1: Clear config ---
83+
print("\n--- Test 1: Clear config ---")
84+
clear_config()
85+
check("no error", read_error() == 0)
86+
data = read_config()
87+
check("all 0xFF", all(b == 0xFF for b in data))
88+
89+
# --- Test 2: Write and read back ---
90+
print("\n--- Test 2: Write and read back ---")
91+
config = b'{"rev":3,"name":"STeaMi-01"}'
92+
write_config(0, config)
93+
check("no error", read_error() == 0)
94+
data = read_config()
95+
end = 256
96+
for i in range(256):
97+
if data[i] == 0xFF:
98+
end = i
99+
break
100+
check("content matches", bytes(data[:end]) == config)
101+
102+
# --- Test 3: Write at offset ---
103+
print("\n--- Test 3: Write at offset ---")
104+
clear_config()
105+
write_config(100, b"HELLO")
106+
data = read_config()
107+
check("offset 0-99 is 0xFF", all(b == 0xFF for b in data[:100]))
108+
check("offset 100-104 is HELLO", bytes(data[100:105]) == b"HELLO")
109+
check("offset 105 is 0xFF", data[105] == 0xFF)
110+
111+
# --- Test 4: Write preserves existing data ---
112+
print("\n--- Test 4: Write preserves existing data ---")
113+
clear_config()
114+
write_config(0, b"FIRST")
115+
write_config(10, b"SECOND")
116+
data = read_config()
117+
check("FIRST preserved", bytes(data[0:5]) == b"FIRST")
118+
check("SECOND written", bytes(data[10:16]) == b"SECOND")
119+
check("gap is 0xFF", all(b == 0xFF for b in data[5:10]))
120+
121+
# --- Test 5: Clear flash does NOT erase config ---
122+
print("\n--- Test 5: Clear flash preserves config ---")
123+
clear_config()
124+
write_config(0, b"PERSIST")
125+
clear_flash()
126+
data = read_config()
127+
check("config survived clear_flash", bytes(data[:7]) == b"PERSIST")
128+
129+
# --- Test 6: Write at second 256-byte page ---
130+
print("\n--- Test 6: Read second page ---")
131+
clear_config()
132+
write_config(256, b"PAGE2")
133+
data_p1 = read_config(0)
134+
check("page 1 empty", all(b == 0xFF for b in data_p1))
135+
data_p2 = read_config(256)
136+
check("page 2 has data", bytes(data_p2[:5]) == b"PAGE2")
137+
138+
# --- Test 7: Large write (max single chunk) ---
139+
print("\n--- Test 7: Large write ---")
140+
clear_config()
141+
big = b"A" * 28
142+
write_config(0, big)
143+
data = read_config()
144+
check("28 bytes written", bytes(data[:28]) == big)
145+
check("byte 29 is 0xFF", data[28] == 0xFF)
146+
147+
# --- Test 8: Multiple sequential writes ---
148+
print("\n--- Test 8: Multiple sequential writes ---")
149+
clear_config()
150+
for i in range(5):
151+
write_config(i * 20, bytes([0x41 + i]) * 10)
152+
data = read_config()
153+
check("chunk 0 = AAAAAAAAAA", bytes(data[0:10]) == b"A" * 10)
154+
check("chunk 2 = CCCCCCCCCC", bytes(data[40:50]) == b"C" * 10)
155+
check("chunk 4 = EEEEEEEEEE", bytes(data[80:90]) == b"E" * 10)
156+
157+
# --- Test 9: Existing flash operations still work ---
158+
print("\n--- Test 9: Flash operations still work ---")
159+
wait_busy()
160+
i2c.writeto_mem(addr, 0x20, bytes([0x00, 0x00]))
161+
sleep_ms(200)
162+
sector = i2c.readfrom(addr, 256)
163+
check("read_sector OK", len(sector) == 256)
164+
check("who_am_i OK", i2c.readfrom_mem(addr, 0x01, 1)[0] == 0x4C)
165+
166+
# --- Test 10: USB stability after all operations ---
167+
print("\n--- Test 10: USB stability ---")
168+
for i in range(10):
169+
write_config(0, b"STRESS" + bytes([0x30 + i]))
170+
data = read_config()
171+
if bytes(data[:7]) != b"STRESS" + bytes([0x30 + i]):
172+
check("stress iteration %d" % i, False)
173+
break
174+
else:
175+
check("10 write/read cycles OK", True)
176+
177+
# --- Summary ---
178+
print("\n" + "=" * 40)
179+
print("Results: %d passed, %d failed" % (passed, failed))
180+
if failed == 0:
181+
print("ALL TESTS PASSED")
182+
else:
183+
print("SOME TESTS FAILED")

tests/scenarios/daplink_flash.yaml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,40 @@ tests:
126126
expect_true: true
127127
mode: [mock]
128128

129+
- name: "Clear config sends command"
130+
action: script
131+
script: |
132+
i2c.clear_write_log()
133+
dev.clear_config()
134+
log = i2c.get_write_log()
135+
sent_clear = any(reg is None and 0x32 in data for reg, data in log)
136+
result = sent_clear
137+
expect_true: true
138+
mode: [mock]
139+
140+
- name: "Write config sends command"
141+
action: script
142+
script: |
143+
i2c.clear_write_log()
144+
dev.write_config(b"test")
145+
log = i2c.get_write_log()
146+
sent_write = any(reg is None and data[0] == 0x30 for reg, data in log)
147+
result = sent_write
148+
expect_true: true
149+
mode: [mock]
150+
151+
- name: "Write config sends clear then write"
152+
action: script
153+
script: |
154+
i2c.clear_write_log()
155+
dev.write_config(b"hi")
156+
log = i2c.get_write_log()
157+
sent_clear = any(reg is None and 0x32 in data for reg, data in log)
158+
sent_write = any(reg is None and data[0] == 0x30 for reg, data in log)
159+
result = sent_clear and sent_write
160+
expect_true: true
161+
mode: [mock]
162+
129163
# ----- Hardware -----
130164

131165
- name: "Status register readable"
@@ -261,3 +295,49 @@ tests:
261295
result = len(content) == 0
262296
expect_true: true
263297
mode: [hardware]
298+
299+
# ----- Config zone -----
300+
301+
- name: "Write and read config round-trip"
302+
action: hardware_script
303+
script: |
304+
dev.clear_config()
305+
dev.write_config('{"test": 42}')
306+
content = dev.read_config()
307+
result = content == b'{"test": 42}'
308+
expect_true: true
309+
mode: [hardware]
310+
311+
- name: "Clear config erases config zone"
312+
action: hardware_script
313+
script: |
314+
dev.write_config("data")
315+
dev.clear_config()
316+
result = dev.read_config() == b""
317+
expect_true: true
318+
mode: [hardware]
319+
320+
- name: "Clear flash does not erase config"
321+
action: hardware_script
322+
script: |
323+
from time import sleep_ms
324+
dev.write_config("preserved")
325+
dev.clear_flash()
326+
sleep_ms(500)
327+
result = dev.read_config() == b"preserved"
328+
expect_true: true
329+
mode: [hardware]
330+
331+
- name: "Config survives data file operations"
332+
action: hardware_script
333+
script: |
334+
from time import sleep_ms
335+
dev.write_config('{"cal": true}')
336+
dev.set_filename("TEST", "CSV")
337+
dev.clear_flash()
338+
sleep_ms(500)
339+
dev.write_line("row1")
340+
sleep_ms(100)
341+
result = dev.read_config() == b'{"cal": true}'
342+
expect_true: true
343+
mode: [hardware]

0 commit comments

Comments
 (0)