Control Surface pin-t-adl
MIDI Control Surface library for Arduino
mbed-m0/PluggableUSBMIDI.cpp
Go to the documentation of this file.
1#if defined(ARDUINO_ARCH_MBED_RP2040)
2
3#include "PluggableUSBMIDI.hpp"
4#include <AH/Debug/Debug.hpp>
5
6#include <cstring> // memcpy
7
9
10#ifdef FATAL_ERRORS
11#define CS_MIDI_USB_ASSERT(a) MBED_ASSERT((a))
12#else
13#define CS_MIDI_USB_ASSERT(a) (void)(a)
14#endif
15
16using std::tie;
17
18// ------------------------- CONSTRUCTOR/DESTRUCTOR ------------------------- //
19
20PluggableUSBMIDI::PluggableUSBMIDI() : PluggableUSBModule(2) {
21 PluggableUSBD().plug(this);
22}
23
24PluggableUSBMIDI::~PluggableUSBMIDI() { PluggableUSBD().deinit(); }
25
26// ----------------------------- INITIALIZATION ----------------------------- //
27
28void PluggableUSBMIDI::init(EndpointResolver &resolver) {
29 bulk_in_ep = resolver.endpoint_in(USB_EP_TYPE_BULK, PacketSize);
30 bulk_out_ep = resolver.endpoint_out(USB_EP_TYPE_BULK, PacketSize);
31 CS_MIDI_USB_ASSERT(resolver.valid());
32}
33
34bool PluggableUSBMIDI::connected() const {
35 return usb_connected.load(std::memory_order_relaxed);
36}
37
38// --------------------------------- USB API -------------------------------- //
39
40void PluggableUSBMIDI::callback_state_change(DeviceState new_state) {
41 assert_locked();
42 usb_connected.store(new_state == USBDevice::Configured,
43 std::memory_order_relaxed);
44}
45
46uint32_t PluggableUSBMIDI::callback_request(const setup_packet_t *setup,
47 USBDevice::RequestResult *result,
48 uint8_t **data) {
49 assert_locked();
50 *result = USBDevice::PassThrough;
51 *data = nullptr;
52 return 0;
53}
54
55bool PluggableUSBMIDI::callback_request_xfer_done(const setup_packet_t *setup,
56 bool aborted) {
57 assert_locked();
58 return false;
59}
60
61bool PluggableUSBMIDI::callback_set_configuration(uint8_t configuration) {
62 assert_locked();
63
64 PluggableUSBD().endpoint_add(
65 bulk_in_ep, PacketSize, USB_EP_TYPE_BULK,
66 mbed::callback(this, &PluggableUSBMIDI::in_callback));
67 PluggableUSBD().endpoint_add(
68 bulk_out_ep, PacketSize, USB_EP_TYPE_BULK,
69 mbed::callback(this, &PluggableUSBMIDI::out_callback));
70
71 writing.send_timeout = nullptr;
72 writing.timeout.detach();
73 writing.buffers[0].size = 0;
74 writing.buffers[0].ready_to_send = false;
75 writing.buffers[1].size = 0;
76 writing.buffers[1].ready_to_send = false;
78 writing.sending = nullptr;
79
81 reading.read_idx = 0;
83 reading.reading = true;
85
86 return true;
87}
88
89void PluggableUSBMIDI::callback_set_interface(uint16_t interface,
90 uint8_t alternate) {
91 assert_locked();
92}
93
94// ---------------------------------- WRITING ----------------------------------
95
96void PluggableUSBMIDI::write(uint32_t msg) {
97 write_impl(&msg, 1, false); // blocking
98}
99
100void PluggableUSBMIDI::write(const uint32_t *msgs, uint32_t num_msgs) {
101 const uint32_t *end = msgs + num_msgs;
102 while (msgs != end)
103 msgs += write_impl(msgs, end - msgs, false); // blocking
104}
105
106uint32_t PluggableUSBMIDI::write_nonblock(const uint32_t *msgs,
107 uint32_t num_msgs) {
108 uint32_t total_sent = 0, sent = 1;
109 while (total_sent < num_msgs && sent != 0) {
110 sent = write_impl(msgs + total_sent, num_msgs - total_sent, true);
111 total_sent += sent;
112 }
113 return total_sent;
114}
115
117 this->lock();
118 uint32_t active_idx, size;
119 wbuffer_t *writebuf;
120 tie(active_idx, writebuf, size) = read_writebuf_size();
121 if (size > 0)
122 send_now_impl_nonblock(active_idx);
123 else
124 this->unlock();
125}
126
127/*
128 * write(...)
129 * - called by user
130 * - adds data to the transmit buffer
131 * - starts a timeout to send the data (even if the buffer isn't full yet)
132 * - sends the transmit buffer to the USB stack when full
133 * - blocks if there's no space left in the transmit buffers
134 *
135 * timeout_callback()
136 * - called from a timer interrupt, a fixed time after the first write to a
137 * buffer
138 * - sends the transmit buffer even if it isn't full yet
139 * - if a previous transmission is still in progress, schedules it to be sent
140 * immediately after the current transmission finishes.
141 *
142 * in_callback()
143 * - called by the USB stack when a transmission is complete
144 * - finishes the transmission
145 * - releases the “sending” lock
146 * - starts a new transmission if it was scheduled while the previous
147 * transmission was in progress
148 */
149
150/*
151 * - Both send buffers start out empty (size = 0), the first buffer is active
152 * (`writebuffer = 0`, which means that the first buffer is the one being
153 * filled with the outgoing data).
154 * - The `write()` function is called.
155 * - The message is added to the active buffer and the size is incremented.
156 * (Writing the data happens inside of a critical section.)
157 * - If the active buffer is now full, the packet is sent to the USB stack, and
158 * the buffers are swapped. If the previous buffer wasn't completely sent yet,
159 * it is scheduled to be sent when the previous transmission is complete.
160 * - If this was the first message in the buffer, a timeout is started.
161 * - If the active buffer wasn't full, nothing is actually sent, and the
162 * `write()` function exits.
163 * - When the timeout fires, `timeout_callback()` is called (from a timer
164 * interrupt).
165 * - If there is data in the active buffer, it's sent over USB. If the previous
166 * buffer wasn't completely sent yet, it's not possible to send now, and it's
167 * scheduled to be sent later.
168 * - When a buffer has been sent, `in_callback()` is called (from a USB
169 * interrupt).
170 * - The write is finished, the “sending” lock is released and the buffer size
171 * is reset to zero to let the main `write()` function know that it can write
172 * to this buffer. If the other buffer was scheduled to be sent while we were
173 * sending this packet, immediately send the next one.
174 */
175
176std::tuple<uint32_t, PluggableUSBMIDI::wbuffer_t *, uint32_t>
178 this->assert_locked();
179 uint32_t active_idx = writing.active_writebuffer;
180 wbuffer_t *writebuffer = &writing.buffers[active_idx];
181 uint32_t size = writebuffer->size;
183 return std::make_tuple(active_idx, writebuffer, size);
184}
185
186uint32_t PluggableUSBMIDI::write_impl(const uint32_t *msgs, uint32_t num_msgs,
187 bool nonblocking) {
188 // Index of the buffer currently being written to.
189 uint32_t active_idx;
190 // Pointer to buffer currently being written to.
191 wbuffer_t *writebuf;
192 // Current size of that buffer (i.e. the first index we can write to).
193 uint32_t size;
194
195 this->lock();
196 tie(active_idx, writebuf, size) = read_writebuf_size();
197
198 // Make sure that there's space in the buffer for us to write data
199 if (size >= PacketSize) {
200 // If there's no space
201 if (nonblocking) {
202 // either return without blocking
203 return this->unlock(), 0;
204 } else {
205 // Or wait until the active writing buffer changes to an empty one
206 auto old_idx = active_idx;
207 do {
208 this->unlock();
209 yield(); // spin
210 this->lock();
211 tie(active_idx, writebuf, size) = read_writebuf_size();
212 } while (size >= PacketSize);
213 CS_MIDI_USB_ASSERT(old_idx != active_idx);
214 CS_MIDI_USB_ASSERT(size == 0);
215 }
216 }
218
219 // Copy the data into the active buffer
220 uint32_t free_size = std::min(PacketSize - size, num_msgs * 4u);
221 CS_MIDI_USB_ASSERT(free_size % 4 == 0);
222 CS_MIDI_USB_ASSERT(size % 4 == 0);
223 size = (size / 4) * 4;
224 free_size = (free_size / 4) * 4;
225 memcpy(&writebuf->buffer[size], msgs, free_size);
226 uint32_t newsize = size + free_size;
227 writebuf->size = newsize;
228 this->unlock();
229
230 // If the buffer is now full, send it immediately (but don't block)
231 if (newsize == PacketSize) {
232 send_now_impl_nonblock(active_idx);
233 }
234 // If this is the first data in the buffer, start a timer that will send
235 // the buffer after a given timeout
236 else if (size == 0) {
237 this->lock();
238 wbuffer_t *old = std::exchange(writing.send_timeout, writebuf);
239 this->unlock();
240 CS_MIDI_USB_ASSERT(old == nullptr);
241 std::atomic_signal_fence(std::memory_order_release);
242 /* ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ [Timeout] ▼ */
243 auto cb = mbed::callback(this, &PluggableUSBMIDI::timeout_callback);
244 writing.timeout.attach(std::move(cb), writing.timeout_duration);
245 }
246
247 // How many messages were added to the transmit buffer
248 return free_size / 4u;
249}
250
255bool PluggableUSBMIDI::send_now_impl_nonblock(uint32_t active_idx) {
256 this->lock();
257 // Disable the timeout
258 auto old_timeout = std::exchange(writing.send_timeout, nullptr);
259
260 wbuffer_t *writebuffer = &writing.buffers[active_idx];
261
262 // Schedule the active buffer to be sent immediately after the previous one
263 bool old_ready = std::exchange(writebuffer->ready_to_send, true);
264 if (old_ready)
265 // If the send flag was already set, there's nothing left for us to do
266 return this->unlock(), true;
267
268 // If we were the ones who set the send flag, try to send the packet now
269
270 // Try to acquire the “sending” lock:
271 if (writing.sending != nullptr)
272 // If we failed to get the lock, the previous transmission is still in
273 // progress, so we can't send now, but sending of our buffer is
274 // scheduled using the ready_to_send flag, so we can just return.
275 return this->unlock(), false;
276 writing.sending = writebuffer;
277
278 // Swap the active write buffer
279 CS_MIDI_USB_ASSERT(writing.buffers[!active_idx].size == 0);
280 writing.active_writebuffer = !active_idx;
281
282 // Get the size of the buffer to send and atomically set it to reserved.
283 // This buffer cannot be sent because writing.sending == nullptr, so no
284 // previous transmission was in progress that could have started sending
285 // this buffer from an ISR.
286 // Similarly, the timeout callback couldn't have started sending it either
287 // because we own the writing.sending lock.
288 uint32_t size = std::exchange(writebuffer->size, SizeReserved);
290 CS_MIDI_USB_ASSERT(size != 0);
291
292 // Actually send the buffer
293 write_start_sync(writebuffer->buffer, size);
294 // “sending” lock is released in USB callback
295
296 this->unlock();
297 if (old_timeout != nullptr)
298 writing.timeout.detach();
299
300 return true;
301}
302
303void PluggableUSBMIDI::send_in_callback(uint32_t sendbuf_idx) {
304 // Only called in scenarios where we already own “sending” lock
305
306 // Get the size of the buffer to send and atomically set it to reserved
307 wbuffer_t *sendbuffer = &writing.buffers[sendbuf_idx];
308 uint32_t size = std::exchange(sendbuffer->size, SizeReserved);
310 CS_MIDI_USB_ASSERT(size != 0);
311
312 // Actually send the buffer
313 write_start_sync(sendbuffer->buffer, size);
314}
315
317 this->lock(); // Note: we're in an ISR
318 std::atomic_signal_fence(std::memory_order_acquire);
319 /* ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ [Timeout] ▲ */
320
321 // Check which buffer (if any) to send
322 wbuffer_t *sendbuffer = std::exchange(writing.send_timeout, nullptr);
323 if (sendbuffer == nullptr)
324 return this->unlock();
325
326 // Schedule the active buffer to be sent immediately after the previous one
327 bool old_ready = std::exchange(sendbuffer->ready_to_send, true);
328 if (old_ready)
329 // If the send flag was already set, there's nothing left for us to do
330 return this->unlock();
331
332 // Try to get the send lock
333 if (writing.sending != nullptr)
334 // If we failed to get the lock, he previous transmission is still in
335 // progress, so we can't send now, but sending of our buffer is
336 // scheduled using the ready_to_send flag, so we can just return.
337 return this->unlock();
338 writing.sending = sendbuffer;
339
340 // Swap the buffers
341 uint32_t sendbuf_idx = sendbuffer - writing.buffers;
342 CS_MIDI_USB_ASSERT(writing.buffers[!sendbuf_idx].size == 0);
343 writing.active_writebuffer = !sendbuf_idx;
344
345 // Reserve and send the buffer
346 send_in_callback(sendbuf_idx);
347 return this->unlock();
348}
349
350void PluggableUSBMIDI::write_start_sync(uint8_t *buffer, uint32_t size) {
352 // digitalWrite(LED_BUILTIN, HIGH);
353 std::atomic_signal_fence(std::memory_order_release);
354 /* ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ [Write-Start] ▼ */
355 write_start(bulk_in_ep, buffer, size);
356}
357
359 std::atomic_signal_fence(std::memory_order_acquire);
360 /* ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ [Write-Start] ▲ */
361
362 assert_locked();
363 // digitalWrite(LED_BUILTIN, LOW);
364 write_finish(bulk_in_ep);
365
366 // Release the “sending” lock
367 wbuffer_t *sent = std::exchange(writing.sending, nullptr);
368 uint32_t sent_idx = sent - writing.buffers;
369 uint32_t next_idx = !sent_idx;
370 wbuffer_t *next = &writing.buffers[next_idx];
371
372 // Check if the other buffer was scheduled to be sent immediately after the
373 // one we just sent
374 uint32_t next_ready = next->ready_to_send;
375 if (next_ready) {
376 // Acquire the “sending” lock again
377 wbuffer_t *old_sending = std::exchange(writing.sending, next);
378 // Since interrupt handlers are atomic blocks, acquiring the lock cannot
379 // fail. If multiple threads are used, an extra flag is needed to
380 // prevent the main thread and the timeout thread from stealing the lock
381 // after we released it.
382 CS_MIDI_USB_ASSERT(old_sending == nullptr);
383
384 // The transmission is done, reset the size of the buffer to 0 so the
385 // main thread can write to it again.
386 sent->ready_to_send = false;
387 uint32_t oldsize = std::exchange(sent->size, 0);
389
390 // Swap the buffers
391 writing.active_writebuffer = !next_idx;
392
393 // Send it
394 send_in_callback(next_idx);
395 } else {
396 // The transmission is done, reset the size of the buffer to 0 so the
397 // main thread can write to it again.
398 sent->ready_to_send = false;
399 uint32_t oldsize = std::exchange(sent->size, 0);
401 }
402}
403
404// ---------------------------------- READING ----------------------------------
405
406uint32_t PluggableUSBMIDI::read() {
407 this->lock();
408 // Check if there are any bytes available for reading
409 uint32_t available = reading.available;
410 if (available == 0)
411 return this->unlock(), 0;
412
413 // Get the buffer with received data
414 uint32_t r = reading.read_idx;
415 rbuffer_t &readbuffer = reading.buffers[r];
416
417 // Read the data from the buffer (data is at least as new as available)
418 uint32_t data;
419 memcpy(&data, &readbuffer.buffer[readbuffer.index], 4);
420
421 readbuffer.index += 4;
422 // If we've read all messages from this buffer
423 if (readbuffer.index == readbuffer.size) {
424 // Increment the read index (and wrap around)
425 r = (r + 1 == NumRxPackets) ? 0 : r + 1;
426 reading.read_idx = r;
427 reading.available -= 1;
428 // There is now space in the queue
429 // Check if the next read is already in progress
430 bool inprogress = std::exchange(reading.reading, true);
431 if (!inprogress) {
432 // If not, start the next read now
433 uint32_t w = reading.write_idx;
435 }
436 }
437 return this->unlock(), data;
438}
439
441 assert_locked();
443 // Check how many bytes were read
444 uint32_t num_bytes_read = read_finish(bulk_out_ep);
445 CS_MIDI_USB_ASSERT(num_bytes_read % 4 == 0);
446
447 // If no bytes were read, start a new read into the same buffer
448 uint32_t w = reading.write_idx;
449 if (num_bytes_read == 0) {
451 return;
452 }
453
454 // Otherwise, store how many bytes were read
455 rbuffer_t &writebuffer = reading.buffers[w];
456 writebuffer.index = 0;
457 writebuffer.size = num_bytes_read;
458 // Increment the write index (and wrap around)
459 w = (w + 1 == NumRxPackets) ? 0 : w + 1;
460 reading.write_idx = w;
461
462 // Update number of available buffers in the queue
463 uint32_t available = (reading.available += 1);
464 // If there's still space left in the queue, start the next read
465 if (available < NumRxPackets)
467 // Otherwise, release the “reading” lock
468 else
469 reading.reading = false;
470}
471
472constexpr uint32_t PluggableUSBMIDI::PacketSize;
473constexpr uint32_t PluggableUSBMIDI::SizeReserved;
474
476
477#endif
#define END_CS_NAMESPACE
#define BEGIN_CS_NAMESPACE
#define CS_MIDI_USB_ASSERT(a)
static constexpr uint32_t NumRxPackets
static constexpr uint32_t PacketSize
USB packet size.
uint32_t active_writebuffer
The index of the buffer that is currently being written to.
bool ready_to_send
Indicates that this buffer can be sent as soon as the previous one has been sent.
void send_in_callback(uint32_t activebuf_idx)
static constexpr uint32_t SizeReserved
struct PluggableUSBMIDI::Reading reading
struct PluggableUSBMIDI::Writing writing
uint32_t write_nonblock(const uint32_t *msgs, uint32_t num_msgs)
Send multiple MIDI USB messages without blocking.
void timeout_callback()
uint32_t read()
Try reading a 4-byte MIDI USB message.
void callback_state_change(DeviceState new_state) override
void send_now()
Try sending the buffered data now.
void write_start_sync(uint8_t *buffer, uint32_t size)
Buffer * send_timeout
Buffer to be sent in the timeout callback.
void callback_set_interface(uint16_t interface, uint8_t alternate) override
uint32_t write_impl(const uint32_t *msgs, uint32_t num_msgs, bool nonblocking)
Buffer * sending
Buffer that is being sent.
bool callback_set_configuration(uint8_t configuration) override
bool connected() const
Check if this class is connected and ready.
void init(EndpointResolver &resolver) override
struct PluggableUSBMIDI::Reading::Buffer buffers[NumRxPackets]
std::tuple< uint32_t, wbuffer_t *, uint32_t > read_writebuf_size()
std::atomic< bool > usb_connected
uint32_t size
How many bytes are in the buffer.
bool send_now_impl_nonblock(uint32_t activebuf_idx)
uint32_t callback_request(const setup_packet_t *setup, USBDevice::RequestResult *result, uint8_t **data) override
bool callback_request_xfer_done(const setup_packet_t *setup, bool aborted) override
struct PluggableUSBMIDI::Writing::Buffer buffers[2]
void write(uint32_t msg)
Send a MIDI USB message.
constexpr auto min(const T &a, const U &b) -> decltype(b< a ? b :a)
Return the smaller of two numbers/objects.
Definition: MinMaxFix.hpp:15