1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
use RefCell;
use ;
use Mutex;
use CriticalSectionRawMutex;
use Signal;
use Status;
use ;
use heapless;
// ---------------------------------------------------------------------------
// Firmware update data registry
// ---------------------------------------------------------------------------
/// A reference-counted firmware binary and its pre-computed CRC-32.
/// Maximum number of peripherals with registered firmware.
const MAX_FW_SLOTS: usize = 4;
/// Global registry: peripheral ID → (`FirmwareSlot`).
///
/// Populated via [`set_firmware_update_data`].
/// Looked up by [`PeripheralManager`] on
/// connection to decide whether an update is needed.
static FW_SLOTS: =
new;
/// Register a peripheral firmware binary for automatic dfu_split updates.
///
/// The central calls this (typically at startup) so that
/// `PeripheralManager` can verify and, if needed, update the peripheral's
/// firmware when the split link is established.
///
/// `id` must match the peripheral index in `[[split.peripheral]]` (or the
/// `id` argument of `run_peripheral_manager`). `hash` is the CRC-32 of
/// the firmware binary — typically computed via [`crate::crc32::crc32`].
/// `id` must be unique; if a slot for the same `id` already exists, it will be replaced.
/// Every peripheral has only one firmware slot, given by its unique `id`.
///
/// Returns `Err(())` if the registry is full (max `MAX_FW_SLOTS` entries).
/// Retrieve the firmware binary and its expected CRC-32 for a given
/// peripheral ID, if one has been registered.
/// ── PASSTHROUGH QUEUE: USB ISR → async PeripheralManager ─────────
///
/// The central gets 512B DNLOAD blocks from the host, while the peripheral gets
/// 256B chunks over the split link. The USB ISR splits each block into two chunks
/// and in order to smooth out the flow, a small FIFO queue is used to decouple
/// the two contexts. To prevent the host from sending more data than the queue
/// can hold, the GETSTATUS reply is modified to return `dfuDNBUSY` while the
/// queue is not empty.
///
/// ```text
/// USB Host Central MCU
/// ──────── ──────────
///
/// dfu-util -a 1 -D fw.bin
/// │
/// │ USB Control Transfer (DNLOAD, 512 bytes)
/// v
/// ┌──────────────────────────┐
/// │ PassthroughDfuHandler │ USB ISR (synchronous)
/// │ .write(data) │ splits 512B → 2 × 256B chunks
/// │ │ calls passthrough_push() each
/// └──────────────────────────┘
/// │
/// │ passthrough_push(Chunk{offset, data})
/// v
/// ┌──────────────────────────┐
/// │ PASSTHROUGH_CMD │ heapless::Vec<Command, 4>
/// │ [ Chunk(0) ] │ FIFO-queue
/// │ [ Chunk(256) ] │ max 4 entries (QUEUE_SIZE)
/// │ [ ... ] │ protected by CriticalSectionMutex
/// │ [ free ] │
/// └──────────────────────────┘
/// │
/// │ PASSTHROUGH_TARGET = peripheral_id (doorbell)
/// v
/// ┌──────────────────────────┐
/// │ PeripheralManager │ async event loop (every 5 ms)
/// │ .handle_passthrough() │
/// │ │ while passthrough_pending(id):
/// │ 1. passthrough_take() │ cmd = queue.pop()
/// │ 2. send() over split │ send(FirmwareChunk) to peripheral
/// │ 3. wait for Ack │ wait(FirmwareChunkAck)
/// │ 4. clear doorbell │ if queue.empty(): target = MAX
/// └──────────────────────────┘
/// │
/// │ UART
/// │ (SplitMessage::FirmwareChunk)
/// v
/// ┌──────────────────────────┐
/// │ Peripheral │
/// │ SplitDfuHandler │
/// │ .write_chunk() │ flash erase + write, send Ack
/// └──────────────────────────┘
///
///
/// ── FLOW CONTROL ────────────────────────────────────────────────
///
/// While PASSTHROUGH_TARGET != MAX every GETSTATUS reply has
/// state = dfuDNBUSY (4). The host polls again after 50 ms.
/// Once the queue is drained and passthrough_done_if_empty()
/// clears the target, the real DFU state is returned and the
/// host sends the next DNLOAD block.
///
/// ```
/// DFU `Handler` used for **passthrough** alternate settings on the
/// central's USB DFU interface.
///
/// Runs inside the USB interrupt. Each incoming DNLOAD block is split
/// into 256-byte chunks and pushed into [`PASSTHROUGH_CMD`]. The
/// async `PeripheralManager` task drains the queue and forwards chunks
/// to the peripheral over the split link. GETSTATUS flow control
/// (cf. [`PASSTHROUGH_TARGET`]) ensures the host waits when the queue
/// is not empty.
pub
/// A single chunk of firmware data queued for passthrough.
pub
/// Commands flowing from the USB ISR
/// ([`PassthroughDfuHandler`]) to the async
/// [`PeripheralManager`](crate::split::driver::PeripheralManager).
pub
/// Maximum number of pending chunks in the fire-and-forget queue.
const PASSTHROUGH_QUEUE_SIZE: usize = 4;
/// Fire-and-forget command queue.
///
/// The USB DFU handler (ISR context) pushes; the async
/// `PeripheralManager` pops. Protected by a critical-section mutex
/// so it is safe from both contexts.
static PASSTHROUGH_CMD: = new;
/// Wakeup signal: set when a command is pushed to [`PASSTHROUGH_CMD`].
pub static PASSTHROUGH_SIGNAL: = new;
/// Doorbell atomic: set to a peripheral ID when there is work in
/// [`PASSTHROUGH_CMD`], `usize::MAX` when idle.
///
/// Read by [`RmkDfuInterface::control_in`] to inject `dfuDNBUSY` into
/// the GETSTATUS response (adaptive host-side flow control).
pub static PASSTHROUGH_TARGET: AtomicUsize = new;
/// Check whether a passthrough command is pending for the given
/// peripheral ID.
pub
/// Push a command into the queue (ISR-safe).
/// Pop the next pending command (async task).
pub
/// Clear the target doorbell if the queue is empty.
///
/// Called after every command is processed. If the queue still has
/// items the target stays set, keeping the host in `dfuDNBUSY` until
/// the PeripheralManager catches up.
pub
/// Drain all pending passthrough commands (e.g. on disconnect).
pub