LCOV - code coverage report
Current view: top level - src/MIDI_Interfaces/USBMIDI/LowLevel - BulkTX.ipp (source / functions) Hit Total Coverage
Test: 3aaab29ec58d6e71a20fde08a40531cf32318591 Lines: 92 118 78.0 %
Date: 2024-05-18 12:29:05 Functions: 6 6 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : #include "BulkTX.hpp"
       2             : 
       3             : #include <AH/Arduino-Wrapper.h>
       4             : #include <AH/Containers/CRTP.hpp>
       5             : 
       6             : #include <algorithm>
       7             : 
       8             : #include <cassert>
       9             : #include <cstring>
      10             : 
      11             : #ifdef FATAL_ERRORS
      12             : #define CS_MIDI_USB_ASSERT(a) assert((a))
      13             : #else
      14             : #define CS_MIDI_USB_ASSERT(a)
      15             : #endif
      16             : 
      17             : BEGIN_CS_NAMESPACE
      18             : 
      19             : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
      20             : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::write(MessageType msg) {
      21             :     write(&msg, 1);
      22             : }
      23             : 
      24             : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
      25       67496 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::write(
      26             :     const MessageType *msgs, uint32_t num_msgs) {
      27       67496 :     const uint32_t *end = msgs + num_msgs;
      28      170412 :     while (msgs != end) msgs += write_impl(msgs, end - msgs);
      29       67496 : }
      30             : 
      31             : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
      32             : uint32_t BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::write_nonblock(
      33             :     const MessageType *msgs, uint32_t num_msgs) {
      34             :     uint32_t total_sent = 0, sent = 1;
      35             :     while (total_sent < num_msgs && sent != 0) {
      36             :         sent = write_impl(msgs + total_sent, num_msgs - total_sent, true);
      37             :         total_sent += sent;
      38             :     }
      39             :     return total_sent;
      40             : }
      41             : 
      42             : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
      43        8333 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::send_now() {
      44        8333 :     auto buffer = writing.send_later.exchange(nullptr, mo_acq);
      45        8333 :     if (buffer == nullptr)
      46             :         // Either the write function or the timeout_handler already cleared
      47             :         // the send_later flag.
      48        1827 :         return;
      49        6506 :     CRTP(Derived).cancel_timeout();
      50             : 
      51             :     // Indicate to any handlers interrupting us that we intend to send a buffer.
      52        6506 :     writing.send_now.store(buffer, mo_seq); //                               (5)
      53             : 
      54             :     // Try to acquire the sending lock.
      55        6506 :     wbuffer_t *old = nullptr;
      56        6506 :     if (!writing.sending.compare_exchange_strong(old, buffer, mo_seq)) //    (6)
      57             :         // If we couldn't get the lock, whoever has the lock will send the
      58             :         // data and clear send-now.
      59           0 :         return;
      60             : 
      61             :     // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq >>>sending<<<
      62             :     // We now own the sending lock
      63             : 
      64             :     // It is possible that the tx_callback ran between (5) and (6), so check
      65             :     // the send-now flag again.
      66        6506 :     auto send_now = writing.send_now.load(mo_seq);
      67        6506 :     if (send_now == nullptr) {
      68             :         // If the buffer was already sent, release the sending lock and return.
      69           0 :         writing.sending.store(nullptr, mo_seq); // ▲▲▲
      70           0 :         return;
      71             :     }
      72             : 
      73             :     // Us having the sending lock also means that the timeout and the
      74             :     // tx_callback cannot have concurrent access to writing.active_writebuffer
      75             :     // Therefore, acquiring it must succeed.
      76        6506 :     auto send_buffer = writing.active_writebuffer.exchange(nullptr, mo_seq);
      77             :     // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
      78             :     CS_MIDI_USB_ASSERT(send_buffer == buffer);
      79             :     CS_MIDI_USB_ASSERT(send_now == buffer);
      80             :     // Now that we own both the buffer and the sending lock, we can send it
      81        6506 :     writing.send_now.store(nullptr, mo_rlx);
      82             :     // Prepare the other buffer to be filled
      83        6506 :     auto next_buffer = other_buf(send_buffer);
      84        6506 :     next_buffer->size = 0;
      85             :     // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
      86        6506 :     writing.active_writebuffer.store(next_buffer, mo_rel);
      87             :     // Send the current buffer
      88        6506 :     CRTP(Derived).tx_start(send_buffer->buffer, send_buffer->size);
      89             :     // ------------------------------------------------------------------------- (sending lock is released by the tx_callback)
      90        6506 :     return;
      91             : }
      92             : 
      93             : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
      94             : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::reset(
      95             :     uint16_t packet_size) {
      96             :     writing.packet_size = packet_size;
      97             :     writing.buffers[0].size = 0;
      98             :     writing.buffers[1].size = 0;
      99             :     writing.active_writebuffer.store(&writing.buffers[0], mo_rlx);
     100             :     writing.send_later.store(nullptr, mo_rlx);
     101             :     writing.send_now.store(nullptr, mo_rlx);
     102             :     writing.sending.store(nullptr, mo_rlx);
     103             : }
     104             : 
     105             : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
     106           1 : bool BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::is_done() const {
     107           2 :     return writing.sending.load(mo_acq) == nullptr &&
     108           2 :            writing.send_later.load(mo_acq) == nullptr &&
     109           2 :            writing.send_now.load(mo_acq) == nullptr;
     110             : }
     111             : 
     112             : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
     113      102916 : uint32_t BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::write_impl(
     114             :     const MessageType *msgs, uint32_t num_msgs) {
     115      102916 :     if (num_msgs == 0)
     116           0 :         return 0;
     117             : 
     118             :     // Try to get access to an available buffer
     119      102916 :     wbuffer_t *buffer = writing.active_writebuffer.exchange(nullptr, mo_acq);
     120             :     // If that failed, return without blocking, caller may retry until we get
     121             :     // a buffer we can use
     122      102916 :     if (buffer == nullptr)
     123           0 :         return 0;
     124             : 
     125             :     // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
     126             :     // At this point we have a buffer, and we have exclusive access to it,
     127             :     // but it may still be full
     128             :     // CS_MIDI_USB_ASSERT(writing.send_now.load(mo_rlx) == nullptr);
     129             : 
     130      102916 :     auto size = buffer->size;
     131             :     CS_MIDI_USB_ASSERT(size != SizeReserved);
     132      102916 :     size_t avail_size = writing.packet_size - size;
     133      102916 :     auto copy_size_zu = std::min<size_t>(avail_size, num_msgs * sizeof(*msgs));
     134      102916 :     auto copy_size = static_cast<uint16_t>(copy_size_zu);
     135      102916 :     if (copy_size > 0) {
     136      102916 :         std::memcpy(buffer->buffer + size, msgs, copy_size);
     137      102916 :         buffer->size = size + copy_size;
     138             :     }
     139             :     // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
     140             :     // Release access to the buffer
     141      102916 :     writing.active_writebuffer.store(buffer, mo_seq);
     142             : 
     143      102916 :     if (copy_size > 0) {
     144             :         // If we completely filled this buffer in one go, send it now.
     145      102916 :         if (size == 0 && copy_size == writing.packet_size) {
     146             :             CS_MIDI_USB_ASSERT(writing.send_later.load(mo_rlx) == nullptr);
     147             :             CS_MIDI_USB_ASSERT(writing.send_now.load(mo_rlx) == nullptr);
     148       31751 :             writing.send_now.store(buffer, mo_rel); // TODO: can be relaxed
     149             :         }
     150             :         // If this is the first data in the buffer, schedule it to be sent later.
     151       71165 :         else if (size == 0) {
     152             :             CS_MIDI_USB_ASSERT(writing.send_later.load(mo_rlx) == nullptr);
     153       63186 :             writing.send_later.store(buffer, mo_rel);
     154       63186 :             CRTP(Derived).start_timeout();
     155             :         }
     156             :         // If this buffer was partially filled before and now full, send it now.
     157        7979 :         else if (size + copy_size == writing.packet_size) {
     158        5983 :             auto send_buffer = writing.send_later.exchange(nullptr, mo_acq);
     159        5983 :             if (send_buffer != nullptr) {
     160             :                 CS_MIDI_USB_ASSERT(writing.send_now.load(mo_rlx) == nullptr);
     161             :                 CS_MIDI_USB_ASSERT(send_buffer == buffer);
     162        5983 :                 CRTP(Derived).cancel_timeout();
     163        5983 :                 writing.send_now.store(buffer, mo_rel); // TODO: can be relaxed
     164             :             }
     165             :         }
     166             :     }
     167             : 
     168             :     // An interrupt may have attempted to send the buffer while we owned it.
     169      102916 :     if (writing.send_now.load(mo_seq) == nullptr) //                         (1)
     170             :         // If that's not the case, we can safely return.
     171       65182 :         return copy_size / sizeof(*msgs);
     172             : 
     173             :     // Otherwise, it's our job to send the data that the interrupt failed to
     174             :     // send.
     175             : 
     176             :     // Try to acquire the sending lock.
     177       37734 :     wbuffer_t *old = nullptr;
     178       37734 :     if (!writing.sending.compare_exchange_strong(old, buffer, mo_seq)) //    (2)
     179             :         // If we couldn't get the lock, whoever has the lock will send the
     180             :         // data and clear send-now.
     181           0 :         return copy_size / sizeof(*msgs);
     182             :     // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq >>>sending<<<
     183             :     // We now own the sending lock
     184             : 
     185             :     // It is possible that the tx_callback ran between (1) and (2), so check
     186             :     // the send-now flag again.
     187       37734 :     auto send_now = writing.send_now.load(mo_seq);
     188       37734 :     if (send_now == nullptr) {
     189             :         // If the buffer was already sent, release the sending lock and return.
     190           0 :         writing.sending.store(nullptr, mo_seq); // ▲▲▲
     191           0 :         return copy_size / sizeof(*msgs);
     192             :     }
     193             : 
     194             :     // Us having the sending lock also means that the timeout and the
     195             :     // tx_callback cannot have concurrent access to writing.active_writebuffer
     196             :     // Therefore, acquiring it must succeed.
     197       37734 :     auto send_buffer = writing.active_writebuffer.exchange(nullptr, mo_seq);
     198             :     // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
     199             :     CS_MIDI_USB_ASSERT(send_buffer == buffer);
     200             :     CS_MIDI_USB_ASSERT(send_now == buffer);
     201             :     // Now that we own both the buffer and the sending lock, we can send it
     202       37734 :     writing.send_now.store(nullptr, mo_rlx);
     203             :     // Prepare the other buffer to be filled
     204       37734 :     auto next_buffer = other_buf(send_buffer);
     205       37734 :     next_buffer->size = 0;
     206             :     // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
     207       37734 :     writing.active_writebuffer.store(next_buffer, mo_rel);
     208             :     // Send the current buffer
     209       37734 :     CRTP(Derived).tx_start(send_buffer->buffer, send_buffer->size);
     210             :     // ------------------------------------------------------------------------- (sending lock is released by the tx_callback)
     211             : 
     212             :     // TODO: if copy_size == 0, we could retry here
     213             : 
     214       37734 :     return copy_size / sizeof(*msgs);
     215             : }
     216             : 
     217             : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
     218       50697 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::timeout_callback() {
     219       50697 :     auto buffer = writing.send_later.exchange(nullptr, mo_acq);
     220       50697 :     if (buffer == nullptr)
     221             :         // Either the write function or the send_now function already cleared
     222             :         // the send_later flag.
     223           0 :         return;
     224             : 
     225             :     // Indicate to any handlers interrupting us that we intend to send a buffer.
     226       50697 :     writing.send_now.store(buffer, mo_seq); //                               (7)
     227             : 
     228             :     // Try to acquire the sending lock.
     229       50697 :     wbuffer_t *old = nullptr;
     230       50697 :     if (!writing.sending.compare_exchange_strong(old, buffer, mo_seq)) //    (8)
     231             :         // If we couldn't get the lock, whoever has the lock will send the
     232             :         // data and clear send-now.
     233           1 :         return;
     234             :     // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq >>>sending<<<
     235             :     // We now own the sending lock
     236             : 
     237             :     // It is possible that the tx_callback ran between (7) and (8), so check
     238             :     // the send-now flag again.
     239       50696 :     auto send_now = writing.send_now.load(mo_seq);
     240       50696 :     if (send_now == nullptr) {
     241             :         // If the buffer was already sent, release the sending lock and return.
     242           0 :         writing.sending.store(nullptr, mo_seq); // ▲▲▲
     243           0 :         return;
     244             :     }
     245             : 
     246             :     // Us having the sending lock also means that the timeout and the
     247             :     // tx_callback cannot have concurrent access to writing.active_writebuffer
     248             :     // Therefore, acquiring it must succeed.
     249       50696 :     if (auto act = writing.active_writebuffer.exchange(nullptr, mo_seq)) {
     250             :         // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
     251             :         CS_MIDI_USB_ASSERT(act == buffer);
     252             :         CS_MIDI_USB_ASSERT(send_now == buffer);
     253             :         // Now that we own both the buffer and the sending lock, we can send it
     254       50696 :         writing.send_now.store(nullptr, mo_rlx);
     255             :         // Prepare the other buffer to be filled
     256       50696 :         auto next_buffer = other_buf(act);
     257       50696 :         next_buffer->size = 0;
     258             :         // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
     259       50696 :         writing.active_writebuffer.store(next_buffer, mo_rel);
     260             :         // Send the current buffer
     261       50696 :         CRTP(Derived).tx_start_timeout(buffer->buffer, buffer->size);
     262             :         // --------------------------------------------------------------------- (sending lock is released by the tx_callback)
     263       50696 :         return;
     264             :     }
     265             :     // The write function owns the buffer.
     266             :     // TODO: only valid if this is an interrupt handler, see tx_callback comment
     267             :     // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel >>>sending<<<
     268           0 :     writing.sending.store(nullptr, mo_seq);
     269           0 :     return;
     270             : }
     271             : 
     272             : template <class Derived, class MessageTypeT, uint16_t MaxPacketSizeV>
     273       94937 : void BulkTX<Derived, MessageTypeT, MaxPacketSizeV>::tx_callback() {
     274             :     // ------------------------------------------------------------------------- (we still own the sending lock)
     275       94937 :     wbuffer_t *sent_buffer = writing.sending.load(mo_acq);
     276             :     CS_MIDI_USB_ASSERT(sent_buffer != nullptr);
     277             : 
     278             :     // Check if anyone tried to send the next buffer while the previous one
     279             :     // was still being sent
     280       94937 :     wbuffer_t *send_next = writing.send_now.load(mo_seq);
     281       94937 :     if (send_next) {
     282             :         CS_MIDI_USB_ASSERT(send_next != sent_buffer);
     283             :         // We already own the sending lock.
     284           1 :         if (auto act = writing.active_writebuffer.exchange(nullptr, mo_seq)) {
     285             :             // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
     286             :             CS_MIDI_USB_ASSERT(act == send_next);
     287           1 :             writing.send_now.store(nullptr, mo_rlx);
     288           1 :             sent_buffer->size = 0;
     289           1 :             writing.sending.store(send_next, mo_seq);
     290             :             // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
     291           1 :             writing.active_writebuffer.store(sent_buffer, mo_seq);
     292           1 :             CRTP(Derived).tx_start_isr(send_next->buffer, send_next->size);
     293           1 :             return;
     294             :             // ----------------------------------------------------------------- (sending lock is released by the next tx_callback)
     295             :         }
     296             :         // Someone else already holds the active_buffer lock.
     297             :         // We own the sending lock, but not the buffer to be sent. Since the
     298             :         // timeout and send_now can only own the buffer if they also own the
     299             :         // sending lock, it must be the write function that owns the buffer.
     300             :         // The write function always checks the send-now flag after releasing
     301             :         // the buffer, so we can safely release our sending lock and return.
     302             :         // In the case of interrupts/signal handlers, our release of the lock
     303             :         // happens-before the release of the release of the active buffer in
     304             :         // the write function, so there would be no need to check send_now
     305             :         // again after releasing. In different threads, we would need to check
     306             :         // again, because the write thread could try to acquire the sending
     307             :         // lock before we released it.
     308             :         // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel >>>sending<<<
     309           0 :         writing.sending.store(nullptr, mo_seq);
     310           0 :         return;
     311             : 
     312             :         // TODO: can we do it for the threaded case? At first sight, we should
     313             :         //       try to acquire the active write buffer first, and then try
     314             :         //       to acquire the sending lock again.
     315             :         //       Alternatively, we could add a “main-should-wait” flag to
     316             :         //       indicate that the write function should do a busy wait because
     317             :         //       we'll release the sending lock soon.
     318             :     }
     319             : 
     320             :     // Release the “sending” lock
     321             :     // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel >>>sending<<<
     322       94936 :     writing.sending.store(nullptr, mo_seq);
     323             : 
     324             :     // Someone may have tried sending the other buffer while we were still
     325             :     // holding the sending lock, so check the send_now flag (again).
     326       94936 :     send_next = writing.send_now.load(mo_seq); //                            (3)
     327       94936 :     if (send_next == nullptr)
     328             :         // No one tried to send before, and if they try to send later, they will
     329             :         // be able to get the lock to do so.
     330       94936 :         return;
     331             : 
     332             :     // Someone tried to send. We must try to send now.
     333           0 :     wbuffer_t *old = nullptr;
     334           0 :     if (!writing.sending.compare_exchange_strong(old, send_next, mo_seq)) // (4)
     335             :         // If this fails, someone else must have been able to acquire the
     336             :         // sending lock in the meantime, and will be able to make progress.
     337           0 :         return;
     338             : 
     339             :     // The send_now flag must still be true: either the timeout fired between
     340             :     // (3) and (4) and clears it, in which case it holds on to the sending lock,
     341             :     // or it failed to get both the sending lock and the buffer, and in that
     342             :     // case it doesn't clear send-now. Similar for the main program.
     343             :     CS_MIDI_USB_ASSERT(writing.send_now.load(mo_seq) == send_next);
     344             : 
     345             :     // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq >>>sending<<<
     346             :     // We now hold the sending lock (again)
     347             : 
     348             :     // This is the same as earlier
     349           0 :     if (auto act = writing.active_writebuffer.exchange(nullptr, mo_seq)) {
     350             :         // ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ acq <<<active_writebuffer>>>
     351             :         CS_MIDI_USB_ASSERT(act == send_next);
     352           0 :         writing.send_now.store(nullptr, mo_rlx);
     353           0 :         sent_buffer->size = 0;
     354           0 :         writing.sending.store(send_next, mo_seq);
     355             :         // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel <<<active_writebuffer>>>
     356           0 :         writing.active_writebuffer.store(sent_buffer, mo_seq);
     357           0 :         CRTP(Derived).tx_start_isr(send_next->buffer, send_next->size);
     358           0 :         return;
     359             :         // --------------------------------------------------------------------- (sending lock is released by the next tx_callback)
     360             :     }
     361             :     // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ rel >>>sending<<<
     362           0 :     writing.sending.store(nullptr, mo_seq);
     363             : 
     364             :     // TODO: same as above
     365             : }
     366             : 
     367             : END_CS_NAMESPACE
     368             : 
     369             : #undef CS_MIDI_USB_ASSERT

Generated by: LCOV version 1.15