mainRawCDC.c contains a minimal amount of code to configure the MCU hardware and USB device stack. It will allow the MCU to enumerate over USB as a virtual COM port when a micro-USB cable is plugged into CN1 (and goes to a USB host such as a PC) and power is applied through CN13. It will attempt to send two messages over USB: test and message:
- The USB stack is initialized by using the MX_USB_Device_Init() function after the hardware is fully initialized:
int main(void)
{
HWInit();=
MX_USB_DEVICE_Init();
- There is a single task that outputs two strings over USB, with a forced 100 tick delay after the second transmission using a naive call to usbd_cdc_if.c: CDC_Transmit_FS:
void usbPrintOutTask( void* NotUsed)
{
while(1)
{
SEGGER_SYSVIEW_PrintfHost("print test over USB");
CDC_Transmit_FS((uint8_t*)"test ", 5);
SEGGER_SYSVIEW_PrintfHost("print message over USB");
CDC_Transmit_FS((uint8_t*)"message ", 8);
vTaskDelay(100);
}
}
- After compiling and loading this application to our target board, we can observe the output of the USB port by opening a terminal emulator (Tera Term in this case). You'll likely see something similar to the following screenshot:
Since we were outputting a single line containing test and then a single line containing message, we would hope that the virtual serial port would contain that same sequence, but there are multiple test lines that aren't always followed by a message line.
Watching this same application run from SystemView shows that the code is executing in the order that we would expect:
Upon closer inspection of CDC_Transmit_FS, we can see that there is a return value that should have been inspected. CDC_Transmit_FS first checks to ensure that there isn't already a transfer being performed before overwriting the transmit buffer with new data. Here are the contents of CDC_Transmit_FS(automatically generated by STM Cube):
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;
}
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
/* USER CODE END 7 */
return result;
Data will only be transmitted if there isn't already a transfer in progress (indicated by hcdc->TxState). So, to ensure that all of the messages are transmitted, we have a number of options here.
- We could simply wrap each and every call to CDC_Transmit_FS in a conditional statement to check whether the transfer was successful:
int count = 10;
while(count > 0){
count--;
if(CDC_Transmit_FS((uint8_t*)"test ", 5) == USBD_OK)
break;
else
vTaskDelay(2);
}
There are several downsides to this approach:
-
- It is slow when attempting to transmit multiple messages back to back (because of the delay between each attempt).
- If the delay is removed, it will be extremely wasteful of CPU, since the code will essentially poll on transmission completion.
- It is undesirably complex. By forcing the calling code to evaluate whether a low-level USB transaction was valid, we're adding a loop and nested conditional statements to something that could potentially be very simple. This will increase the likelihood that it is coded incorrectly and reduce readability.
- We could write a new wrapper based on usbd_cdc_if.c that uses FreeRTOS stream buffers to efficiently move data to the USB stack. This approach has a few caveats:
- To keep the calling code simple, we'll be tolerant of dropped data (if space in the stream buffer is unavailable).
- To support calls from multiple tasks, we'll need to protect access to the stream buffer with a mutex.
- The stream buffer will effectively create a duplicate buffer, thereby consuming additional RAM.
- We could use a FreeRTOS queue instead of a stream buffer. As seen in Chapter 10, Drivers and ISRs, we would receive a performance hit when using a queue (relative to a stream buffer) since it would be moving only a single byte at a time. However, a queue wouldn't require being wrapped in a mutex when used across tasks.
Let's now have a look at how options 2 and 3 look, relative to the STM HAL drivers already present:
For this driver, we'll be modifying the stubbed out HAL-generated code supplied by ST (usbd_cdc_if.c) as a starting point. Its functionality will be replaced by our newly created VirtualCommDriver.c. This will be detailed in the next section.
We'll also make a very small modification to the CDC middleware supplied by STM (usbd_cdc.c/h) to enable a non-polled method for determining when transfers are finished. The USBD_CDC_HandleTypeDef struct in usbd_cdc.h already has a variable named TxState that can be polled to determine when a transmission has completed. But, to increase efficiency, we'd like to avoid polling. To make this possible, we'll add another member to the struct – a function pointer that will be called when a transfer is complete: usbd_cdc.h (additions in bold):
typedef struct
{
uint32_t data[CDC_DATA_HS_MAX_PACKET_SIZE / 4U]; /* Force 32bits
alignment */
uint8_t CmdOpCode;
uint8_t CmdLength;
uint8_t *RxBuffer;
uint8_t *TxBuffer;
uint32_t RxLength;
uint32_t TxLength;
//adding a function pointer for an optional call back function
//when transmission is complete
void (*TxCallBack)( void );
__IO uint32_t TxState;
__IO uint32_t RxState;
}
USBD_CDC_HandleTypeDef;
We'll then add the following code to usbd_cdc.c. (additions in bold):
}
else
{
hcdc->TxState = 0U;
if(hcdc->TxCallBack != NULL)
{
hcdc->TxCallBack();
}
}
return USBD_OK;
}
This addition executes the function pointed to by TxCallBack if it has been provided (indicated by a non-NULL value). This happens when TxState in the CDC struct is set to 0. TxCallBack was also initialized to NULL in USBD_CDC_Init().
NOTE: More recent versions of HAL and STMCubeIDE include support for TxCallBack, so this modification won't be necessary if you're starting from scratch with the latest released code from ST.