STM32 ADC and DAC with DMA

From Stm32World Wiki
Jump to navigation Jump to search

Most, if not all, STM32 MCUs include one or more Analog to Digital converters (ADCs) which can be used to measure analog voltage levels. Manu, but not all, also include one or more Digital to Analog converters (DACs) which can produce an analog voltage level. This page will document how to use these peripherals.

Overview

To be added

ADC

As mentioned earlier almost all [{STM32]] MCUs has got at least one ADC (they can have more - STM32F405 have 3) and each of those ADCs have multiple channels that can be sampled individually or in sequence.

Configuring the ADC peripheral in STM32CubeMX

The ADC peripheral can be configured through STM32CubeMX:

ADC1 Config.png

The input channels can be enabled and disabled individually. For ADC1 there are also some internal channels (not wired to a pin but wired internally). For this example we will enable all the 3 internal channels:

ADC1 internal channels.png

In this case we have enabled the internal temperature sensor, the internal reference voltage and the battery voltage.

We have also enabled "Scan Conversion Mode" (measure the 3 channels in sequence) and "DMA Continuous Requests".

Under Regular Conversion we have configured the use of 3 and rank each of these in turn:

ADC Regular Conversion config.png

Notice that the Sampling time can be configured per channel. The higher the value, the more precise the ADC.

Final configuration of the ADC is the DMA.

ADC DMA Configuration.png

We will here use a circular buffer.

The final part of the configuration is the timer (timer 8 as per above configuration):

Timer8 Configuration for ADC DMA.png

Code

For convenience we will first create some defines:

#define ADC_RESOLUTION 4096
#define DMA_SAMPLES 10
#define DMA_BUFFER_SIZE 3 * DMA_SAMPLES

Notice we will create the buffer 3 times the number of samples, as all 3 channels are written to the buffer on the scan.

We can now create the buffer itself:

uint16_t dma_buffer[2 * DMA_BUFFER_SIZE];

In our main code we can now fire up the timer and the ADC:

HAL_TIM_Base_Start_IT(&htim8);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*) &dma_buffer, 2 * DMA_BUFFER_SIZE);

The continuous run is now enabled and we can add our callbacks for the DMA circular buffer:

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) {
    process_buffer(&dma_buffer[0]);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    process_buffer(&dma_buffer[DMA_BUFFER_SIZE]);
}

For this example we will simply average the 10 samples of each channel:

void process_buffer(uint16_t *buffer) {

    uint32_t temp_sum = 0, vref_sum = 0, vbat_sum = 0;

    //if (cb % 50 == 0) HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);

    for (int i = 0; i < DMA_SAMPLES; ++i) {
        temp_sum += buffer[0];
        vref_sum += buffer[1];
        vbat_sum += buffer[2];
        buffer += 3;
    }

    temp_avg = temp_sum / DMA_SAMPLES;
    vref_avg = vref_sum / DMA_SAMPLES;
    vbat_avg = vbat_sum / DMA_SAMPLES;

    // VDDA can be calculated based on the measured vref and the calibration data
    vdda = (float) VREFINT_CAL_VREF * (float) *VREFINT_CAL_ADDR / vref_avg / 1000;

    // Knowing vdda and the resolution of adc - the actual voltage can be calculated
    vref = (float) vdda / ADC_RESOLUTION * vref_avg;

    // Temperature can be calculated based on the
    temp = (float) ((float) ((float) (TEMPSENSOR_CAL2_TEMP - TEMPSENSOR_CAL1_TEMP) / (float) (*TEMPSENSOR_CAL2_ADDR - *TEMPSENSOR_CAL1_ADDR)) * (temp_avg *TEMPSENSOR_CAL1_ADDR) + TEMPSENSOR_CAL1_TEMP);

}

Tutorial video

We have created a tutorial video describing how to use the ADC with DMA.

You can watch the video on youtube here: https://www.youtube.com/watch?v=rb3j78--7xU.

DAC

The Digital to analog converter peripheral can be used to generate analog values on a pin.

The range of voltages is between 0 and VDDA (usually 3.3 V) with a precision of 12 bit. 12 bit can be used for 4096 values meaning the analog output can be controlled within 0.8 mV.

Configuring the DAC peripheral in STM32CubeMX

The basic DAC configuration is done through STM32CubeMX:

DAC configuration.png

As can be seen, there are very few settings. The output buffer is essentially an OpAmp voltage follower capable of delivering more current than the regular DAC at the price of some precision (OpAmps will introduce a DC offset error). If just setting a fixed value from the code somewhere, the trigger is not necessary. The trigger is used in combination with a DMA buffer and in the above example the DAC is triggered from a timer channel.

The DMA is configured from the DMA Tab:

DAC DMA settings.png

For the above configuration a timer can be configured to generate the samples:

DAC Timer config.png

In this case, the prescaler will scale the 84 MHz APB1 clock frequency down to 1 MHz. Dividing that with 10 means 100000 samples per second (100 kHz).

Code

For convenience we will first set a few defines:

#define DMA_BUFFER_SIZE 64
#define SAMPLE_FREQ 100000
#define OUTPUT_MID 2048

We can now create a DMA buffer:

uint16_t dma_buffer[2 * DMA_BUFFER_SIZE];

The buffer contains half words (16-bit) sufficient for the 12 bit resolution of the DAC. Because we're using a circular buffer we create the buffer double of the number of samples.

We can now start the timer and the DAC:

HAL_TIM_Base_Start_IT(&htim6);

HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*) &dma_buffer, 2 * DMA_BUFFER_SIZE, DAC_ALIGN_12B_R);

At this point the DAC will start 100000 samples per second (all zeros at this point since the buffer contains all zeros). A callback will be executed when the buffer is half send and when the buffer is fully send. The idea is that when the half point is reached, it is safe to update the samples of the first half. When the final point is reached, it is safe to update the second half of the buffer.

inline void HAL_DAC_ConvCpltCallbackCh1(DAC_HandleTypeDef *hdac) {
    do_dac(&dma_buffer[DMA_BUFFER_SIZE]);
}

inline void HAL_DAC_ConvHalfCpltCallbackCh1(DAC_HandleTypeDef *hdac) {
    do_dac(&dma_buffer[0]);
}

IF we want a nice sine wave on the DAC output, we will need a few more variables, and we'll need to implement the do_dac function (which will be called 100000 / buffer_size times each second). First a few includes:

#include <math.h>
#include "arm_math.h"

The "arm_math.h" is a library optimized for incredible fast float math on ARM cores. We can now create some variables.

const float two_pi = 2 * M_PI;

float angle = 0;
float angle_change = 440 * (2 * M_PI / SAMPLE_FREQ);
float amplifier = 0.9;

The "angle" will change from 0 to 2 * PI and each sample will increase that angle by "angle_change". Here configured for a 440 Hz sine-wave.

We can now create the "do_dac" function:

static inline void do_dac(uint16_t *buffer) {
    for (int i = 0; i < DMA_BUFFER_SIZE; ++i) {
        buffer[i] = OUTPUT_MID - (amplifier * (OUTPUT_MID * arm_cos_f32(angle)));
        angle += angle_change;
        if (angle >= two_pi) {
            angle -= two_pi;
        }
    }
}

This is quite simple. We go through each sample in the buffer (half buffer to be exact) and for each sample we create the cos (using the fast arm_cos_f32) and use that to calculate the sample value. We finally increase the angle by "angle_change". Finally we reset the angle if it goes above a full circle (2 * PI).

And that is all there is to it. Quite simple really.

Note it is possible to run both DACs at the same time. Simply start the second DAC channel like this:

HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_2, (uint32_t*) &dma_buffer_2, 2 * DMA_BUFFER_SIZE, DAC_ALIGN_12B_R);

Of course with a separate DMA buffer.

The callbacks for the second DAC channel are:

void HAL_DACEx_ConvHalfCpltCallbackCh2(DAC_HandleTypeDef *hdac) {
    do_dac2(&dma_buffer_2[0]);
}

void HAL_DACEx_ConvCpltCallbackCh2(DAC_HandleTypeDef *hdac) {
    do_dac2(&dma_buffer_2[DMA_BUFFER_SIZE]);
}

Tutorial video

We have created a tutorial video describing how to use the DAC with DMA.

You can watch the video on youtube here: https://www.youtube.com/watch?v=0N4ECamZw2k.

Miscellaneous Links