This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Help understanding USB CDC ACM read flow

Hey all, I'm trying very hard to understand how UART over USB works by following through the USB CDC ACM example in SDK 17.0.2 on the nRF52840DK, but I'm still new to this so I'm missing something.  I see that on APP_USBD_CDC_ACM_USER_EVT_PORT_OPEN, we so an initial app_usbd_cdc_acm_read which presumably writes to SIZE.EPOUT register like it shows in the datasheet (Figure 202 on page 540 in v1.2) then sets up the EasyDMA transfer with the buffer your pass to it and kicks off the STARTEPOUT task?  Then, I guess, we get the APP_USBD_CDC_ACM_USER_EVT_RX_DONE event on the ENDEPOUT event register and load up a new EasyDMA buffer and kick off another STARTEPOUT task?

I ask all this because I am not seeing the data I expect to see.  First, I have an array of 16 ArrayLists, each with a 64 byte buffer (same size as USB buffer).  I modified READ_SIZE from 1 to 64. After each app_usbd_cdc_acm_read I put an app_usbd_cdc_acm_write (the only change to cdc_acm_user_ev_handler) to echo back to the terminal.  When I send more than 64 bytes at a time, I see that all of the data appears in the buffer when I look in the debugger, but not in my terminal echo.  Instead I consistently seem to be missing the first 148 bytes in the echo when I send something like 660 bytes at a time.  Since I'm not able to echo those first bytes, I'm also not able to process with them, which is the thing that has me stymied.  Here is the pertinent code:

#define ARRAY_LIST_SIZE         16
#define READ_SIZE               64

typedef struct
{
  uint8_t buffer[READ_SIZE];
} ArrayList_t;

static ArrayList_t m_rx_buffer[ARRAY_LIST_SIZE];
static char m_tx_buffer[NRF_DRV_USBD_EPSIZE];
static uint16_t m_send_flag = 0;
static uint16_t m_cdc_buffer_idx = 0;

...

static void cdc_acm_user_ev_handler(app_usbd_class_inst_t const * p_inst,
                                    app_usbd_cdc_acm_user_event_t event)
{
    app_usbd_cdc_acm_t const * p_cdc_acm = app_usbd_cdc_acm_class_get(p_inst);

    switch (event)
    {
        case APP_USBD_CDC_ACM_USER_EVT_PORT_OPEN:
        {
            //bsp_board_led_on(LED_CDC_ACM_OPEN);

            /*Setup first transfer*/
            ret_code_t ret = app_usbd_cdc_acm_read(p_cdc_acm,
                                                   m_rx_buffer[m_cdc_buffer_idx].buffer,
                                                   READ_SIZE);
            app_usbd_cdc_acm_write(p_cdc_acm, m_rx_buffer[m_cdc_buffer_idx].buffer, READ_SIZE);
            if (++m_cdc_buffer_idx > ARRAY_LIST_SIZE) m_cdc_buffer_idx = 0;
            UNUSED_VARIABLE(ret);
            break;
        }
        case APP_USBD_CDC_ACM_USER_EVT_PORT_CLOSE:
            //bsp_board_led_off(LED_CDC_ACM_OPEN);
            break;
        case APP_USBD_CDC_ACM_USER_EVT_TX_DONE:
            //bsp_board_led_invert(LED_CDC_ACM_TX);
            break;
        case APP_USBD_CDC_ACM_USER_EVT_RX_DONE:
        {
            ret_code_t ret;
            //NRF_LOG_INFO("Bytes waiting: %d", app_usbd_cdc_acm_bytes_stored(p_cdc_acm));
            do
            {
                /*Get amount of data transfered*/
                size_t size = app_usbd_cdc_acm_rx_size(p_cdc_acm);
                //NRF_LOG_INFO("RX: size: %lu char: %c", size, m_rx_buffer);

                /* Fetch data until internal buffer is empty */
                ret = app_usbd_cdc_acm_read(p_cdc_acm,
                                            m_rx_buffer[m_cdc_buffer_idx].buffer,
                                            READ_SIZE);
            } while (ret == NRF_SUCCESS);

            /* echo */
            app_usbd_cdc_acm_write(p_cdc_acm, m_rx_buffer[m_cdc_buffer_idx].buffer, READ_SIZE);
            if (++m_cdc_buffer_idx > ARRAY_LIST_SIZE) m_cdc_buffer_idx = 0;
            break;
        }
        default:
            break;
    }
}

What I was hoping to do was send a file with a standard header of size ~600 bytes, grab 64 bytes at a time, and look for a few particular fields - one being the first few bytes as a validation and another that could appear at anywhere from 100 to 600 bytes from the start of the file.  That field contains the size of the remainder of the file that I would then write to some flash.  The same protocol also will be used to communicate with the host in 64byte messages.

Parents
  • After a little more testing, I think I am narrowing down the my question a little.  Here's the sequence:

    1. On PORT_OPEN, the first read is executed, the buffer is empty (since no data has been sent), and the index of the buffer is incremented by one, so m_cdc_buffer_idx = 1.
    2. When I send the 662 bytes of data (this is the header for a standard WAV file) and I set a break, I see the beginning of the data get read into m_rx_buffer[0], and subsequent data read accordingly into m_rx_buffer[1], etc.

    How does this work?  When I send the data I do the read with the buffer located at m_rx_buffer[m_cdc_buffer_idx] and m_cdc_idx = 1 - so shouldn't the beginning of my data appear in m_rx_buffer[1] instead of m_rx_buffer[0]?  I'm guessing that since the index gets increased but the data read into previous buffers, that's why it is not echoed to the terminal.

    Here is the test data I'm using:

    RIFFüakWAVEfmt      D¬  Ì   bext[                                                                                                                                                                                                                                                                  iZotope RX 7 Audio Editor       USIZT0037408408461551490069551812021-03-1115:51:49                                                                                                                                                                                                                                                                          datas_kÿÿÿ  

    I was planning on keeping track of which buffer I was on with the m_cdc_buffer_idx variable, but if it isn't on the current buffer, that kind of defeats the purpose.  How do other people process variable length data from USB CDC ACM?

  • Hi,

    the documentation for CDC ACM module explains these things quite clear.

    In APP_USBD_CDC_ACM_USER_EVT_PORT_OPEN handler, you're preparing to receive 64 bytes into the buffer. There is still no data, so you shouldn't call app_usbd_cdc_acm_write() here - it will send some garbage.

    When APP_USBD_CDC_ACM_USER_EVT_RX_DONE comes, you have 64 bytes ready in your buffer - here you should call app_usbd_cdc_acm_write(). Then in the loop, you're reading next chunks that may already have been received by USB peripheral. If the result of next app_usbd_cdc_acm_read() is NRF_SUCCESS, you can call app_usbd_cdc_acm_write() again. Otherwise it will return NRF_ERROR_IO_PENDING - data is not yet ready, but app_usbd_cdc_acm_read() prepares USBD to receive next 64 bytes - the following APP_USBD_CDC_ACM_USER_EVT_RX_DONE will have the buffer filled in.

    CDC ACM has two rx buffers internally, you don't need to switch buffers in your code.

    How do other people process variable length data from USB CDC ACM?

    Eother by reading one byte at a time, or with app_usbd_cdc_acm_read_any function.

  • Thanks , I think I'm getting there.  Just two little things I can't quite wrap my brain around. You say

    If the result of next app_usbd_cdc_acm_read() is NRF_SUCCESS, you can call app_usbd_cdc_acm_write() again.

    but the end of the do loop in the handler is while (ret == NRF_SUCCESS) so how can I call app_usbd_cdc_acm_write if I am stuck in the loop?

    Also,

    CDC ACM has two rx buffers internally, you don't need to switch buffers in your code.

    Sorry, but does this mean I just make one 64 byte buffer and just keep pointing it back into the app_usbd_cdc_acm_read?  What if I want to do some processing on it?

  • but the end of the do loop in the handler is while (ret == NRF_SUCCESS) so how can I call app_usbd_cdc_acm_write if I am stuck in the loop?

    The code should look something like this:

                do
                {
                    /* here the next 64 bytes are in the buffer */
                    app_usbd_cdc_acm_write(p_cdc_acm, m_rx_buffer[m_cdc_buffer_idx].buffer, READ_SIZE);
                    if (++m_cdc_buffer_idx > ARRAY_LIST_SIZE) m_cdc_buffer_idx = 0;
                    
                    /* Fetch data until internal buffer is empty */
                    ret = app_usbd_cdc_acm_read(p_cdc_acm,
                                                m_rx_buffer[m_cdc_buffer_idx].buffer,
                                                READ_SIZE);
                } while (ret == NRF_SUCCESS);
    
                /* last app_usbd_cdc_acm_read() returned IO_PENDING */

    Sorry, but does this mean I just make one 64 byte buffer and just keep pointing it back into the app_usbd_cdc_acm_read?  What if I want to do some processing on it?

    What I mean - the buffer is not touched by USBD until you call app_usbd_cdc_acm_read() again - you can process it directly in APP_USBD_CDC_ACM_USER_EVT_RX_DONE handler, while the next data are received into the internal buffer. Of course, if circular buffer is more convenient in your case, it's a good decision.

  • Ahhhh!  Once I see it I realize I'm dumb :-P  It's all so clear now.  Thank you so much !

Reply Children
No Data
Related