Double-Buffered DMA from ADC

Analog Streaming Data Capture

Overview

If you want to do high-powered Analog to Digital Conversion in a microcontroller, the best way is to use two buffers and cycle between them, so you can be recording data into one buffer and processing the data already in the other buffer.  ST-micro's STM32F7 products, and others, make this possible.

While the API functions are available to do this, as of this writing it isn't all packaged together so you can do it easily. Here is what I found when I went through the process.

The STM32F746G Discovery board is capable of taking ADC samples at more than 1 million per second, so it could be useful as a low-bandwidth scope, and audio spectrum analyzer, and so on.

Project Setup

To sample ADC in streaming mode, start with a CubeMX project based on the Discovery board. I choose not to initialize all peripherals, and just add the ones I want, which seems simpler. I am using CubeIDE 1.5.1.

Some basic setup:

  • In System Core / CORTEX_M7, enable the Prefetch and Caches 
  • In System Core / RCC, set HSE to Crystal/Ceramic Resonator.
  • In Connectivity, set USART1 to Asynchronous mode.  By default it connects to PA9 and PB7, which sets up a serial connection via the ST-Link USB debug port.  The default baud rate is 115200.

Then setup clocking:

  • Set PLL Source to HSE and set the PLLM to /25 and the *N to 400 and leave /P at /2.

You can go higher than 400, up to 432. The SYSCLK runs at half that, and more is generally better. But if you are using the SRAM on the Discovery board you need to limit to 400, so I usually leave it there.


The ADC3 clock is based on PCLK2, so keep an eye on that value.  With this setup it ends up being 100 MHz. 


For the analog input, I have chosen to use PA0, which is mapped to the A0 Arduino pin.  It is accessed via ADC3 IN0. So in the Analog / ADC3 setup, check IN0.

In the Parameter Settings:

  • we will leave the prescaler at PCLK2 / 4 for now.  This means the ADC is clocked at 25 MHz, and samples take 600 ns.  Okay, that's too much, set it to /8 for 1.2us samples.
  • leave resolution at 12 bits
  • I like the Left alignment for data.  That way you can divide all samples by FFFF as the full scale reading, and the sample resolution doesn't matter.

These values aren't critical to the example working, just the timing of things and the way the data looks, so feel free to pick values you like.  

The settings that are critical to double buffering are:

  • Scan Disabled (the default)
  • Continuous Enabled
  • Discontinuous Disabled
  • DMA Continuous Requests Enabled - but it won't let you enable it just yet.

Go to the DMA Settings page tab and click Add, and select ADC3 under DMA Request. It should show the stream DMA2_Stream_0.

Now you can go back and enable DMA Continuous Requests.


That's it, save the project and generate code.

Software

The current STM API has a function in the ADC section that starts the ADC with a DMA, but it doesn't support the double-buffering setup. I wrote, and am providing here, a similar function that takes a second buffer address and enables double buffering.  The key software pieces then will be calling this new start function, and adding a completion callback to process the data.

If you aren't familiar with CubeMX code generation, as long as you follow the rules of staying between the USER CODE BEGIN and END markers (mostly in main.c), you can safely use CubeMX to update your configuration and re-generate code.  Since in almost all my projects I have to regenerate at some point, I find it very good to follow the rules.  One simple way is to put the guts of your code in separate source files.

So here are the updates to main.c to call the rest of the project software:

/* USER CODE BEGIN PFP */
extern void adc_process (void);
/* USER CODE END PFP */ 

and...

  /* USER CODE BEGIN WHILE */
  adc_process();
  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

The adc_process function is defined in adc.c. It also implements the buffer full callback:
void HAL_ADC_DB_ConvCpltCallback(ADC_HandleTypeDef* hadc, int which)
{
  buffer_to_process = which;
  go_ahead_process_buffer = TRUE;

  conversion_count[which]++;
}

The STM HAL code handles callbacks by implementing 'weak' functions, which you can override if you know the correct name, and it helps to know the parameter types. So this is the callback for the Double-Buffer process.  Note that it has a second parameter which indicates which buffer has just been filled.

The double-buffer start function is implemented in db_dma.c. I didn't create a header file, there's really only one new function:
extern HAL_StatusTypeDef HAL_ADC_Start_DB_DMA (ADC_HandleTypeDef* hadc, uint32_t* pData1, uint32_t* pData2, uint32_t Length);

You can see there are two data pointers for this function. I left the data pointers as uint32_t*, even though for ADC use uint16_t* would be more appropriate.  The Length value is in the count of entries in the buffer, which should be uint16_t entries normally.

That's about it.  

NOTE

With the original code, the sample time is 1.2us so a buffer is completed in 1.2ms, but the output is 30 characters (inside the processing loop), so each time it outputs text it holds up processing for 30 * 10 / 115200 = 2.6ms, so it is missing at least one full conversion cycle and probably 3. It was only showing buffer 0 completions even though it should show both, so I found the issue.  The simple way to fix should be to use DMA for the UART output.

Timing

A simple way to measure timing, if you have access to an oscilloscope, is to trigger a GPIO output and watch it on the scope. I was able to do this to confirm the cycle time of 1.2ms and the UART output time of 2.6ms

To Come

If you don't want to capture data at the max rate based on the clock, there are ways to add time between conversion. I will look at that in a follow-up post.

Addendum: UART Note 2

Turns out that interrupt mode is simpler than DMA for UARTs and works in much the way you would expect.  The three things you need to do are:
  1. Enable the interrupt in CubeMX in the NVIC tab for the USART,
  2. Change the Transmit to Transmit_IT, and remove the timeout parameter,
  3. Don't try to transmit again until the last one is complete.  This starts by implementing the HAL_UART_TxCpltCallback callback so that you get notified when transmission is complete.
Also make sure any buffers you use to hold data are valid and unchanged until transmission is complete.  If you want to get fancy, you can do more buffering in software by queueing up waiting transmissions.




Comments

Popular posts from this blog

FFT Analysis using CMSIS DSP Library

Basic Scan-Mode A/D DMA (STM32H743)