STM32 ADC sampling with timer and DMA and send data to computer with USB

adcdmaprocessingsamplingstm32

I want to sample a 4kHz signal that is produced by a signal generator.

I read 2000 samples in a row and then I send it to computer via virtual serial that is provided by USB of STM32F103C8T6. I set timer 3 in order to trigger the ADC and then I set up a 2000 length buffer for DMA.

When I plot signal on the computer the data is not continuous and there is a problem between the signal.

I also stop ADC at the beginning of the PeriodElapsedCallback and start it again at the end of it.

Here is the image of my signal:

enter image description here

The ADC clock is 12MHz. The cycles is set to 28.5 and I use a 12bit ADC. My timer clock is 48MHZ and it counts 30 clocks to trigger the ADC. DMA is set to circular mode. I do not expect these disruptions in my signal because I am taking 2000 samples without any stop.

EDIT:
This is my code. As I mentioned before I stop the DMA in the beginning of the interrupt and start it again at the end.

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
  HAL_ADC_Stop_DMA(&hadc1);
  HAL_TIM_Base_Stop(&htim3);

  usb_put_arr_int(arr,ARRAY_LENGTH);
  HAL_GPIO_TogglePin(GPIOC, 1<<13);

  HAL_TIM_Base_Start(&htim3);
  HAL_ADC_Start_DMA(&hadc1, arr , ARRAY_LENGTH);

}


I also change the timer period from 30 to 400 and I got a better result but the problem still occurs. I am suspicious about DMA speed.

enter image description here

Based on my calculation, the timer triggers the interrupt every:
400/48000000 = 8.33us
and the speed of my ADC is about
(28.5+12.5+2.5)/12000000 = 3.62us

So the ADC speed is faster than the timer. Thus, I expect the timer cannot trigger the ADC before the last sampling had completed.


I changed the CDC function according to what @JiříMaier said (I did not change my interrupt code.)

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;
  }
  USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
  result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);

  for (uint32_t usbTimeout = 100000; usbTimeout > 0; usbTimeout--) {
    if (hcdc->TxState == 0)
      break;
  }



  /* USER CODE END 7 */
  return result;
}

enter image description here


I also changed my code structure and changed my interrupt code according to @JiříMaier advice and I got the following result. The problem is that if I do not open the serial plotter on the computer in the beginning of the microcontroller start, microcontroller gets stuck if I open the serial plotter later.

enter image description here


When I change the buffer size from 2000 to 1000 everything is OK but I do not know why it has problem with the size of 2000.

In order to complete the documentation, this is put_arr_int function:

void usb_put_arr_int(uint16_t * number,int len)
{
    #define batch_size 50

        for (int j=0; j < (len/batch_size) ; j++)
        {
            count += j*batch_size;
            char f[batch_size*7];
            sprintf(f,"%hu\n",number[0 + j*batch_size]);
            for(int i=1;i<(batch_size);i++)
            {
                sprintf(f,"%s%hu\n",f,number[i + j*batch_size]);
                count+=i;
            }
            CDC_Transmit_FS(f,strlen(f));
}

len is always bigger than batch size.

Best Answer

The way I would do this:

in the callback function, stop the timer and DMA and set some "flag" variable:

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
   HAL_ADC_Stop_DMA(&hadc1);
   HAL_TIM_Base_Stop(&htim3);

   samplingFinished = 1;
}

(samplingFinished is uint8_t global variable)

and in main while loop check if samplingFinished is 1.

while (1) {
  if(samplingFinished){
    samplingFinished = 0;
    
    // Send data
    
    // Start timer and DMA again

}

You can send data using CDC_Transmit_FS from HAL libraries. That function however doesn't wait until transmit is finished.

You can modify the function (in USB_DEVICE->App->usbd_cdc_if.c) so that it will wait until all data is transmitted.

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;
     }
     USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
     result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);

     // New code -------------------
     for (uint32_t usbTimeout = 100000; usbTimeout > 0; usbTimeout--) {
       if (hcdc->TxState == 0)
         break;
     } 
    // ---------------------------
   /* USER CODE END 7 */
   return result;
}

The code waits until hcdc->TxState is zero, which means it is finished. If device is not connected to PC (virtual com port is not open), it would never finish, that is why I use the for loop instead of infinite wait.

Related Topic