Close

USC CDC Streams and Serial and HAL Fixups, 002

A project log for Careless WSPR

A desultorily executed Weak Signal Propagation Reporter beacon.

ziggurat29ziggurat29 08/07/2019 at 14:252 Comments

Summary

The streamification of serial ports continues with the USB CDC peripheral.  This one require more HAL hacking than the UART.

Deets

The Blue Pill has a USB device peripheral that we have just been using for power up to this point, but I do want to make it a serial port that can be used for making settings.  As before, I want to abstract that serial port behind the stream interface that was built-up in the prior post.

With the UART, the HAL interface had some awkwardness that was worked around in user code.  In this case, though, the USB CDC driver has greater deficiencies, and we have to make modifications in the library code itself.  This has consequences:  modifications to any code outside of the 'USER CODE BEGIN ...' and USER CODE END ...' will be overwritten each time we re-run CubeMX.  The project is already exposed to this with the alternative heap implementation, so I created a batch file that restore these various fixups after running CubeMX.

The major sticking point in this case with the USB is that there is no way of knowing when a transmission has completed.  We need that so that we can continue to feed the transmission with data from our circular buffers until completed.  We had some callbacks in the case of the UART, but nothing of the sort in the case of USB.  So we create some of our own.

The first surgery is to:
Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h

In this case, we add add a new method TxComplete that we will use to receive notification of transmission completed.  This addition is put in the structure that is defined around line 100:

typedef struct _USBD_CDC_Itf
{
  int8_t (* Init)(void);
  int8_t (* DeInit)(void);
  int8_t (* Control)(uint8_t cmd, uint8_t *pbuf, uint16_t length);
  int8_t (* Receive)(uint8_t *Buf, uint32_t *Len);
/* USER CODE BEGIN MyCDCExt */
  void (* TxComplete)       (uint8_t *, uint32_t );
/* USER CODE END MyCDCExt */

} USBD_CDC_ItfTypeDef;

Note I made up my own 'USER CODE BEGIN MyCDCExt'.  This is purely for my eyeballs, as these are /not/ honored by CubeMX.  It seems CubeMX has an internal, hard-coded, set of tags and it disregards all others.

While I was in this code, I also made a non-critical change a few lines down:

typedef struct
{
/* USER CODE BEGIN MyCDCExt */
//hack; this chip is FS only, so why do I want to waste 448 bytes?
//  uint32_t data[CDC_DATA_HS_MAX_PACKET_SIZE/4];      /* Force 32bits alignment */
  uint32_t data[CDC_DATA_FS_MAX_PACKET_SIZE/4];      /* Force 32bits alignment */
/* USER CODE END MyCDCExt */
  uint8_t  CmdOpCode;
  uint8_t  CmdLength;
  uint8_t  *RxBuffer;
  uint8_t  *TxBuffer;
  uint32_t RxLength;
  uint32_t TxLength;

  __IO uint32_t TxState;
  __IO uint32_t RxState;
}
USBD_CDC_HandleTypeDef;

So, as noted, the out-of-box CDC always reserves some internal buffer as if for HS even if you are only supporting FS.  So that gained me another 448 bytes of RAM!

For the last hack in this file, I add a function definition:

/* USER CODE BEGIN MyCDCExt */
//hack to help remember to re-apply the hacks when code is regenerated.
void XXX_USBCDC_PresenceHack ( void );
/* USER CODE END MyCDCExt */

This function does absolutely nothing, but I call it early in main().  The whole purpose is to cause the build to fail if I forget to apply these hacks again.  Failing to apply these hack would otherwise build successfully, but simply not work, and I didn't want to be endlessly debugging a non-problem just because I forgot to run the script to apply the hacks.

The implementation side of these hacks goes in two places.  One file is at:
Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c

down at around line 677 is a function 'USBD_CDC_DataIn':

static uint8_t  USBD_CDC_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum)
{
  USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef *)pdev->pClassData;
  PCD_HandleTypeDef *hpcd = pdev->pData;

  if (pdev->pClassData != NULL)
  {
    if ((pdev->ep_in[epnum].total_length > 0U) && ((pdev->ep_in[epnum].total_length % hpcd->IN_ep[epnum].maxpacket) == 0U))
    {
      /* Update the packet total length */
      pdev->ep_in[epnum].total_length = 0U;

      /* Send ZLP */
      USBD_LL_Transmit(pdev, epnum, NULL, 0U);
    }
    else
    {
      hcdc->TxState = 0U;
/* USER CODE BEGIN MyCDCExt */
    ((USBD_CDC_ItfTypeDef *)pdev->pUserData)->TxComplete ( hcdc->TxBuffer, hcdc->TxLength );
/* USER CODE END MyCDCExt */
    }
    return USBD_OK;
  }
  else
  {
    return USBD_FAIL;
  }
}

so I simply added the calling of our newly added TxComplete() function.

The last hacks are to another file located along with the main project source:

Src/usbd_cdc_if.c

Here there are actually some spots that are in USER blocks.  At the top:

/* USER CODE BEGIN INCLUDE */
#include "serial_devices.h"

//(these are currently internal to serial_devices.c; may get moved out)
extern size_t XXX_Pull_USBCDC_TxData ( uint8_t* pbyBuffer, const size_t nMax );
extern size_t XXX_Push_USBCDC_RxData ( const uint8_t* pbyBuffer, const size_t nAvail );
/* USER CODE END INCLUDE */

And then around line 80 are some buffers definitions:

/* USER CODE BEGIN PRIVATE_DEFINES */
#define APP_RX_DATA_SIZE  CDC_DATA_FS_MAX_PACKET_SIZE
#define APP_TX_DATA_SIZE  CDC_DATA_FS_MAX_PACKET_SIZE
/* USER CODE END PRIVATE_DEFINES */

and then around 182 in CDC_Init_FS():

static int8_t CDC_Init_FS(void)
{
  /* USER CODE BEGIN 3 */
  /* Set Application Buffers */
    //for some reason we bind the TX buffer of zero length, but the generated
    //code never uses that buffer again (it instead binds user buffers hoping
    //they will remain stable for the lifetime of the transfer).
    USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0);
    USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS);
    
    //immediately 'arm' reception of data to prime the pump
    USBD_CDC_ReceivePacket(&hUsbDeviceFS);
    
  return (USBD_OK);
  /* USER CODE END 3 */
}

Down lower around line 280 in CDC_Receive_FS:

static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */
    USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
    USBD_CDC_ReceivePacket(&hUsbDeviceFS);
    size_t nPushed = XXX_Push_USBCDC_RxData ( &Buf[0], (size_t)*Len );
    if ( nPushed != *Len )
    {
        //horror; dropped data
    }
    
    USBCDC_DataAvailable();    //notify data is available

    return (USBD_OK);
    
  /* USER CODE END 6 */
}

and then down lower around line 324 in CDC_Transmit_FS:

uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
  uint8_t result = USBD_OK;
  /* USER CODE BEGIN 7 */
    USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
    if (hcdc->TxState != 0){
        return USBD_BUSY;
    }
    size_t nPulled = XXX_Pull_USBCDC_TxData ( UserTxBufferFS, APP_TX_DATA_SIZE );
    if ( 0 != nPulled )
    {
        USBD_CDC_SetTxBuffer ( &hUsbDeviceFS, UserTxBufferFS, nPulled );
        result = USBD_CDC_TransmitPacket ( &hUsbDeviceFS );
    }
    else
    {
        USBCDC_TransmitEmpty();    //notify transmit is empty
    }
    UNUSED(Buf);
    UNUSED(Len);
    
  /* USER CODE END 7 */
  return result;
}

And then near the bottom:

/* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */

static void CDC_TsComplete_FS (uint8_t* pbuf, uint32_t Len)
{
    //just kick off a new transmission if we can.
    CDC_Transmit_FS(NULL,0);    //Note, these parameters no longer have meaning
    UNUSED(pbuf);
    UNUSED(Len);
}

//the DAV callback (we make to the user) is optional
__weak void USBCDC_DataAvailable ( void ) {}

//the TBMT callback (we make to the user) is optional
__weak void USBCDC_TransmitEmpty ( void ) {}

/* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */

We implement the 'presence' function at around line 143, just under CDC_Receive_FS() is added:

/* USER CODE BEGIN MyCDCExt */
static void CDC_TsComplete_FS (uint8_t* pbuf, uint32_t Len);
//this is a little hack to work around the fact that re-generating code with
//STM32CubeMX will overwrite our changes (since they have to be in a
//non-"USER CODE BEGIN" demarcated block.  Further, when it does overwrite
//those changes, the project will still build, but just not work.  This
//presence hack will force the linkage to fail, making it obvious that the
//changes need to be re-applied.
void XXX_USBCDC_PresenceHack ( void )
{
    volatile int i = 0;    //thou shalt not optimize away
    (void)i;    //thou shalt not cry
}
/* USER CODE END MyCDCExt */

and then a little lower at around line 166:

USBD_CDC_ItfTypeDef USBD_Interface_fops_FS =
{
  CDC_Init_FS,
  CDC_DeInit_FS,
  CDC_Control_FS,
  CDC_Receive_FS
/* USER CODE BEGIN MyCDCExt */
  , CDC_TsComplete_FS
/* USER CODE END MyCDCExt */
};

And that's it for the hacks!  Now we have the transmit complete notification we need, and so we can start to implement the stream interface.

In the 'serial_devices.h' we add a new device:

extern const IOStreamIF g_pifCDC;
void USBCDC_Init ( void );
void USBCDC_DataAvailable ( void );
void USBCDC_TransmitEmpty ( void );
unsigned int CDC_txbuff_max ( void );
unsigned int CDC_rxbuff_max ( void );

(I add those lines near the similar functions I already created for the UART1.)

In the 'serial_devices.c' we add the implementation:

#include "usb_device.h"
#include "usbd_cdc_if.h"

static void USBCDC_flushTtransmit ( const IOStreamIF* pthis );
static size_t USBCDC_transmitFree ( const IOStreamIF* pthis );
static void USBCDC_flushReceive ( const IOStreamIF* pthis );
static size_t USBCDC_receiveAvailable ( const IOStreamIF* pthis );
static size_t USBCDC_transmit ( const IOStreamIF* pthis, const void* pv, size_t nLen );
static size_t USBCDC_receive ( const IOStreamIF* pthis, void* pv, const size_t nLen );

const IOStreamIF g_pifCDC = {
    USBCDC_flushTtransmit,
    USBCDC_transmitFree,
    USBCDC_transmit,
    USBCDC_flushReceive,
    USBCDC_receiveAvailable,
    USBCDC_receive,
    Serial_transmitCompletely,
    Serial_receiveCompletely,
    NULL
};

size_t XXX_Pull_USBCDC_TxData ( uint8_t* pbyBuffer, const size_t nMax )
{
    size_t nPulled;
    UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();    //lock queue
    size_t nToPull = circbuff_count(&CDC_txbuff);    //max you could pull
    if ( nMax < nToPull )    //no buffer overruns, please
        nToPull = nMax;
    for ( nPulled = 0; nPulled < nToPull; ++nPulled )
    {
        circbuff_dequeue(&CDC_txbuff,&pbyBuffer[nPulled]);
    }
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);    //unlock queue
    return nPulled;
}

size_t XXX_Push_USBCDC_RxData ( const uint8_t* pbyBuffer, const size_t nAvail )
{
    size_t nPushed;
    UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();    //lock queue
    size_t nToPush = circbuff_capacity(&CDC_rxbuff) - circbuff_count(&CDC_rxbuff);    //max you could push
    if ( nAvail < nToPush )    //no buffer overruns, please
        nToPush = nAvail;
    for ( nPushed = 0; nPushed < nToPush; ++nPushed )
    {
        circbuff_enqueue ( &CDC_rxbuff, &pbyBuffer[nPushed] );
    }
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);    //unlock queue
    return nPushed;
}

static void USBCDC_flushTtransmit ( const IOStreamIF* pthis )
{
    UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();    //lock queue
    circbuff_init(&CDC_txbuff);
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);    //unlock queue
}


static void USBCDC_flushReceive ( const IOStreamIF* pthis )
{
    UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();    //lock queue
    circbuff_init(&CDC_rxbuff);
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);    //unlock queue
}


static size_t USBCDC_transmit ( const IOStreamIF* pthis, const void* pv, size_t nLen )
{
    size_t nPushed;
    UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();    //lock queue
    size_t nToPush = circbuff_capacity(&CDC_txbuff) - circbuff_count(&CDC_txbuff);    //max you could push
    if ( nLen < nToPush )    //no buffer overruns, please
        nToPush = nLen;
    for ( nPushed = 0; nPushed < nToPush; ++nPushed )
    {
        circbuff_enqueue ( &CDC_txbuff, &((uint8_t*)pv)[nPushed] );
    }
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);    //unlock queue
    //notify to kick-start transmission, if needed
    CDC_Transmit_FS(NULL, 0);
    return nPushed;
}


static size_t USBCDC_receive ( const IOStreamIF* pthis, void* pv, const size_t nLen )
{
    size_t nPulled;
    UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();    //lock queue
    size_t nToPull = circbuff_count(&CDC_rxbuff);    //max you could pull
    if ( nLen < nToPull )    //no buffer overruns, please
        nToPull = nLen;
    for ( nPulled = 0; nPulled < nToPull; ++nPulled )
    {
        circbuff_dequeue(&CDC_rxbuff,&((uint8_t*)pv)[nPulled]);
    }
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);    //unlock queue
    return nPulled;
}


//what are the number of bytes available to be read now
static size_t USBCDC_receiveAvailable ( const IOStreamIF* pthis )
{
    size_t n;
    UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();    //lock queue
    n = circbuff_count(&CDC_rxbuff);
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);    //unlock queue
    return n;
}


//how much can be pushed into the transmitter buffers now
static size_t USBCDC_transmitFree ( const IOStreamIF* pthis )
{
    size_t n;
    UBaseType_t uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();    //lock queue
    n = circbuff_capacity(&CDC_txbuff) - circbuff_count(&CDC_txbuff);
    taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);    //unlock queue
    return n;
}


//our stub implementation of the optional notification callbacks
__weak void USBCDC_DataAvailable ( void ){}
__weak void USBCDC_TransmitEmpty ( void ){}

So, now, finally we have a unified stream interface on the UART1 and also the USB CDC that works and looks the same to the application.  In main.c some initialization for this new interface is added.  First, the presence hack is added just after HAL_Init() in main():

    XXX_USBCDC_PresenceHack();    //this does nothing real; do not delete

Since all the stuff above will be (partly) overwritten if/when you run CubeMX again -- and moreover will compile cleanly when that happens -- the 'presence hack' is simply to cause the build to fail to make that obvious.  It's unfortunate that the hacks are necessary, but there it is.  To make coping with such less painful, there is a '#fixups' directory which contains the hacks, and a 'fixup.bat' script that will re-apply them when needed.

Further down in StartDefaultTask(), right after where we are already init'ing the UART1:

    USBCDC_Init();    //CDC == monitor

And that's it!  Now it's time to use them!

Next

Implement the Monitor task on the USB CDC stream.

Discussions

Alan Green wrote 08/08/2019 at 20:52 point

Thank you for writing this up. I find dealing with vendor-provided generated code challenging, so this was instructive.

XXX_USBCDC_PresenceHack() is a really neat idea.

  Are you sure? yes | no

ziggurat29 wrote 08/09/2019 at 01:18 point

Yes! I'm glad that little hack was helpful.  And didn't you do a project some years back involving ultrasonics?

  Are you sure? yes | no