1mSec delay for every call to app_usbd_cdc_acm_write().

I am using the nRF52840, nRF5_SDK_3.2.0, using example: usbd_cdc_acm.  I mofified the example so that I transmit characters from a serial port to the USB using a virtual COM port, and from USB, to  serial - both using COM ports in windows.

What I find is that transmitting using the function app_usbd_cdc_acm_write(), I had to add a one mSec delay for each call, or face missing characters.

Q1: Why is the delay necessary?

Q2: I found that while characters are received by the USB, transmitting using app_usbd_cdc_acm_write() at the same time, does not work well. I have to make sure I an not receiving, before I use app_usbd_cdc_acm_write().. Is that normal?

Parents Reply Children
  • When I want to send say 156 bytes, if I wait for APP_USBD_CDC_ACM_USER_EVT_TX_DONE after I send each byte, it gets blocked. I inserted back the 1mSec delay after each app_usbd_cdc_acm_write() call, and it started transmitting.

    I inserted counters, one for every byte I transmit, and one for each APP_USBD_CDC_ACM_USER_EVT_TX_DONE.

    I checked after 156 byte translittion and saw that APP_USBD_CDC_ACM_USER_EVT_TX_DONE occured 32 times, which means it is not for every byte transmitted. So I am still stuck with the 1 mSec, and I do not know if it is too little, or too much.

    Regarding the direction, I am being carefull to make sure reseption has stopped, before I do the transmittion.

    is there anywhere this theory explained?

  • USB Design Info - the information I found, which resolves my problem.

    thread How USB Transmission Works in

    • The function queues data for transmission over the bulk IN endpoint.
    • The USB stack uses EasyDMA to move data from RAM to the USB peripheral.
    • The event is triggered only after the USB host acknowledges receipt of the data.
    • However, only one transfer can be queued at a time—there’s no internal buffering for multiple writes.

    So if you call  again before the previous transfer completes, it will silently fail or drop data unless you check the return value.

    Tools️ Recommended Mechanism (No Delay Needed)

    Instead of using a delay, implement a state-driven transmission queue:

    1. Create a ring buffer or FIFO queue for outgoing data.
    2. Call only when the previous transfer is complete, i.e., inside the  handler.
    3. Check the return value of :
    • If , the data was accepted.
    • If , wait for the next event before retrying.

     

    static bool tx_in_progress = false;

    static uint8_t tx_buffer[64]; // or use a ring buffer for multiple packets

    void send_data(uint8_t* data, size_t length) {

        if (!tx_in_progress) {

            ret_code_t ret = app_usbd_cdc_acm_write(&cdc_acm, data, length);

            if (ret == NRF_SUCCESS) {

                tx_in_progress = true;

            }

            // else: buffer it for later

        }

    }

    void cdc_acm_user_ev_handler(app_usbd_class_inst_t const * p_inst,

                                 app_usbd_cdc_acm_user_event_t event) {

        switch (event) {

            case APP_USBD_CDC_ACM_USER_EVT_TX_DONE:

                tx_in_progress = false;

                // Check if more data is queued and send next chunk

                break;

            // Handle other events...

        }

    }

    You're encountering a classic bottleneck in USB CDC ACM transmission on the nRF52840. Here's what’s happening and how you can manage it more robustly than with a fixed delay:

    Zap USB CDC ACM Throughput Insights

    • Baud Rate Is Cosmetic: The 115,200 baud setting in your terminal is not a real constraint. USB CDC ACM ignores it unless you're bridging to a physical UART. The actual USB bulk transfer speed is much higher.
    • Realistic Throughput: Nordic’s internal tests show that USB CDC ACM on nRF52840 can handle up to ~1 MB/s, depending on host OS and buffering strategy.
    • Your Target: 4,224 bytes/sec = ~33.8 kbps — well within the capabilities.
    • brain Optimized Transmission Strategy
    • To reliably send large packets without delays or dropped data, use a double-buffered queuing system with flow control:
    • Use a Ring Buffer
    • Store outgoing data in a circular buffer. This allows you to queue multiple packets and retry if the USB stack is busy.
    • Track Transmission State
    • Use a flag like tx_in_progress to avoid calling app_usbd_cdc_acm_write() while a transfer is active.
    • Chunk Your Data
    • USB CDC ACM typically uses 64-byte endpoint packets. Break your 4,224-byte payload into 64-byte chunks and send them sequentially.
    • Use TX_DONE Event
    • Only send the next chunk when APP_USBD_CDC_ACM_USER_EVT_TX_DONE

     

    #define CHUNK_SIZE 64

    static uint8_t tx_buffer[4224];

    static size_t tx_offset = 0;

    static bool tx_in_progress = false;

     

    void send_next_chunk() {

        if (tx_offset >= sizeof(tx_buffer)) {

            tx_in_progress = false;

            tx_offset = 0;

            return;

        }

        size_t remaining = sizeof(tx_buffer) - tx_offset;

        size_t len = remaining > CHUNK_SIZE ? CHUNK_SIZE : remaining;

        ret_code_t ret = app_usbd_cdc_acm_write(&cdc_acm, &tx_buffer[tx_offset], len);

        if (ret == NRF_SUCCESS) {

            tx_in_progress = true;

            tx_offset += len;

        }

    }

    void cdc_acm_user_ev_handler(app_usbd_class_inst_t const * p_inst,

                                 app_usbd_cdc_acm_user_event_t event) {

        switch (event) {

            case APP_USBD_CDC_ACM_USER_EVT_TX_DONE:

                send_next_chunk();

                break;

            // Handle other events...

        }

    }

    void start_transmission(uint8_t* data, size_t length) {

        memcpy(tx_buffer, data, length);

        tx_offset = 0;

        send_next_chunk();

    }

    White check mark Behavior of the Final Chunk

    • If the last chunk is less than 64 bytes, it will still transmit correctly.
    • The USB CDC ACM driver handles variable-length packets gracefully.
    • The CHUNK_SIZE in the example simply defines the maximum size per transfer—not a fixed requirement.

    Package USB Bulk Transfer Nuance

    • USB bulk endpoints (like CDC ACM) use packet sizes up to 64 bytes on Full Speed.
    • If the final packet is less than 64 bytes, the host interprets it as the end of the transfer.
    • If the final packet is exactly 64 bytes, some protocols require a zero-length packet afterward to signal completion—but Nordic’s CDC ACM stack handles this internally.

    The final chunk can be any size ≤64 bytes, and the transmission will complete cleanly. No need to pad or force a delay.

    Books Reference

    You can find more details in Nordic’s USB CDC ACM module documentation and a helpful discussion on Nordic DevZone.

Related