3.6. Analog output DMA


1. Introduction   

This tutorial will take you through the design of a sinewave signal generator. The basic idea is to output samples at regular time interval on the integrated Digital to Analog Converter (DAC) of the STM32F072 device.

Although samples could be calculated within the program itself by calling a sin() function available in the <math.h> library (as we did before), a different approach will be introduced here  which is more efficient in terms of CPU usage.

The proposed method consist in storing a set (array) of constant values in the flash memory (beside the program itself), and directly transferring these data from the memory to the DAC upon a timer event which therefore defines the sampling period.

 

2. Using Scilab® to build the wavetable

We need to store samples representing one single period of the signal we want to produce. Once a period has been generated, we can restart over the same array again and again to achieve a continuous sinewave generation.

A set of constants representing a function is commonly called a lookup table. You can use anything you like to build your lookup tables. Here, we will make use of Scilab® (very same thing can be achieved with Matlab®, Excel®, or even by hand).

Scilab® is a free alternative to Matlab® that you can download from: http://www.scilab.org/fr

Scilab® can exectute programs (scripts) the same way as Matlab® does with “.m” files. You will find many resources on Scilab® use on the web. You have to try by yourself as using Scilab® is way beyond scope of this tutorial series.

For now, you can just Copy/paste the following Scilab® program into Scilab® editor and launch the execution. The program produces a plot of calculated samples, and a file called “sinewave_table.h” containing 60 pre-calculated constants corresponding to a sinus function; with one sample every 6°; scaled in amplitude for a 12-bit unsigned DAC.

Take time to carefully examine the Scilab® script. It is much simpler than it looks at first glance. The more complicated stuff is probably tuning the plot figure, which is totally out of interest here (i.e. skip this section).

// Clear the workspace
clear;

// Wavetable parameters
resol =     6;          // One point every x°
nsample =   360/resol;  // number of samples
offset =    2048;       // 12 bit DAC half range = (2^12)/2
amplitude = 2040;       // Avoid DAC saturation

// Compute the sinewave (one point/5° from 0 to 355°)
deg=linspace(0, (360-resol), nsample);
rad = deg.*(%pi/180);
sinus = sin(rad);

// Scale to 12-bit interger values
usin12b = round((sinus*amplitude)+(offset));

// Plot the wavetable for verification
clf();
plot(deg, usin12b, 'r.');
set(gca(),"grid",[26 26]);
set(gca(),"tight_limits","on");
set(gca(),"data_bounds",[0,360,0,4096]);
yt = [0:512:4096]; ytl = string(yt);
xt = [0:30:360]; xtl = string(xt);
set(gca(),"x_ticks",tlist(["ticks","locations","labels"], xt, xtl));
set(gca(),"y_ticks",tlist(["ticks","locations","labels"], yt, ytl));
xlabel("angle (°)","fontsize",3,"color","black");
ylabel("12-bit unsigned samples","fontsize",3,"color","red");
title("Sine wavetable ("+string(nsample)+" samples)","color","red","fontsize",4);

// Open the file for writing
fd = mopen("sinewave_table.h", 'wt');

// Print the data array
index=1;
col=1;

while (index<=nsample)
    // Print sample in hex format
    mfprintf(fd, '0x%04x, ', usin12b(index));
    col = col+1;
    // New line every 8 data
    if (col>4)
        mfprintf(fd, '\n');
        col=1;
    end
    index = index+1;
end

// Close the file
mclose(fd);

 

image013.png
// 60 12-bit unsigned constants 
// defining one sinewave period

0x0800, 0x08d5, 0x09a8, 0x0a76, 
0x0b3e, 0x0bfc, 0x0caf, 0x0d55, 
0x0dec, 0x0e72, 0x0ee7, 0x0f48, 
0x0f94, 0x0fcb, 0x0fed, 0x0ff8, 
0x0fed, 0x0fcb, 0x0f94, 0x0f48, 
0x0ee7, 0x0e72, 0x0dec, 0x0d55, 
0x0caf, 0x0bfc, 0x0b3e, 0x0a76, 
0x09a8, 0x08d5, 0x0800, 0x072b, 
0x0658, 0x058a, 0x04c2, 0x0404, 
0x0351, 0x02ab, 0x0214, 0x018e, 
0x0119, 0x00b8, 0x006c, 0x0035, 
0x0013, 0x0008, 0x0013, 0x0035, 
0x006c, 0x00b8, 0x0119, 0x018e, 
0x0214, 0x02ab, 0x0351, 0x0404, 
0x04c2, 0x058a, 0x0658, 0x072b


 

 

3. Prepare project

Copy the generated header sinewave_table.h into your ‘my_project’ folder under app/inc. Then Refresh the Project Explorer.

image015.png

 

Open sinewave_table.h in the main editor and surround the constants table with correct C declaration of a constant. Take care of the very last ‘,’ that should be removed.

By using the const type, the linker understands that data has to be stored in the flash memory (ROM) instead of RAM.

/*
 * sinewave.h
 * 
 * 60 samples Sine function wavetable
 * - 1 sample every 6°
 * - 12-bit unsigned scale
 */

const uint16_t sinewave[60] = 
{
		0x0800, 0x08d5, 0x09a8, 0x0a76, 
		0x0b3e, 0x0bfc, 0x0caf, 0x0d55, 
		0x0dec, 0x0e72, 0x0ee7, 0x0f48, 
		0x0f94, 0x0fcb, 0x0fed, 0x0ff8, 
		0x0fed, 0x0fcb, 0x0f94, 0x0f48, 
		0x0ee7, 0x0e72, 0x0dec, 0x0d55, 
		0x0caf, 0x0bfc, 0x0b3e, 0x0a76, 
		0x09a8, 0x08d5, 0x0800, 0x072b, 
		0x0658, 0x058a, 0x04c2, 0x0404, 
		0x0351, 0x02ab, 0x0214, 0x018e, 
		0x0119, 0x00b8, 0x006c, 0x0035, 
		0x0013, 0x0008, 0x0013, 0x0035, 
		0x006c, 0x00b8, 0x0119, 0x018e, 
		0x0214, 0x02ab, 0x0351, 0x0404, 
		0x04c2, 0x058a, 0x0658, 0x072b
};

 

4. Peripherals setup

4.1. Timer setup

Let us first tune the timer time base configuration according the frequency we want for the output signal.

Having 60 samples to play for 1 signal period we can deduce:

image018.png

The frequency of timer update events is given by:

image020.png

Given that image022.png we have:

image024.png

The graph below plots the signal frequency image026.png according to image028.png for various settings of image030.png.

image040.png

Let say we want image032.png. You can achieve that with many combinations of image034.png. Note that the lower the prescaler is, the more resolution you have around the desired frequency. Let us choose for instance:

  • image036.png
  • image038.png = 200

Edit the BSP_TIMER_Timebase_Init()  function:

/*
 * BSP_TIMER_Timebase_Init()
 * TIM6 at 48MHz
 * Prescaler   = 4   -> Counting frequency is 12MHz
 * Auto-reload = 200 -> Update frequency is 60kHz
 */

void BSP_TIMER_Timebase_Init()
{
	// Enable TIM6 clock
	RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;

	// Reset TIM6 configuration
	TIM6->CR1 = 0x0000;
	TIM6->CR2 = 0x0000;

	// Set TIM6 prescaler
	// Fck = 48MHz -> /4 = 12MHz counting frequency
	TIM6->PSC = (uint16_t) 4 -1;

	// Set TIM6 auto-reload register for 60kHz
	TIM6->ARR = (uint16_t) 200 -1;

	// Enable auto-reload preload
	TIM6->CR1 |= TIM_CR1_ARPE;

	// Enable Interrupt upon Update Event
	TIM6->DIER |= TIM_DIER_UIE;

	// Start TIM6 counter
	TIM6->CR1 |= TIM_CR1_CEN;
}

 

4.2. DAC setup

Leave the DAC initialization function as is it from previous tutorial:

/*
 * DAC_Init()
 * Initialize DAC for a single output
 * on channel 1 -> pin PA4
 */

void BSP_DAC_Init()
{
	// Enable GPIOA clock
	RCC->AHBENR |= RCC_AHBENR_GPIOAEN;

	// Configure pin PA4 as analog
	GPIOA->MODER &= ~GPIO_MODER_MODER4_Msk;
	GPIOA->MODER |= (0x03 <<GPIO_MODER_MODER4_Pos);

	// Enable DAC clock
	RCC->APB1ENR |= RCC_APB1ENR_DACEN;

	// Reset DAC configuration
	DAC->CR = 0x00000000;

	// Enable Channel 1
	DAC->CR |= DAC_CR_EN1;
}

 

5. Testing the signal generator

 

Now, edit the main() function:

...

#include "sinewave_table.h"

...

// Main program

void main()
{
	uint8_t		index;

	// Configure System Clock
	SystemClock_Config();

	// Initialize Console
	BSP_Console_Init();
	my_printf("Console Ready!\r\n");

	// Initialize 60kHz timebase
	BSP_TIMER_Timebase_Init();
	BSP_NVIC_Init();

	// Initialize DAC
	BSP_DAC_Init();
	
	// Initialize wavetable index
	index = 0;

	// Main loop
	while(1)
	{
		// Do every sampling period
		if(timebase_irq == 1)
		{
		    // Output a new sample
                    DAC->DHR12R1 = sinewave[index];
			
		    // Increment wavetable index
                    index++;
		    if (index==60) index = 0;
			
		    // Reset timebase flag
		    timebase_irq = 0;
		}
	}
}

Build the project and flash the target.

You can now probe the PA4 pin with an oscilloscope. You should see the 3.3Vpp sinewave generated at 1kHz.

image047.png

 

  Commit name "Sinewave generation"
  Push onto Gitlab

 

6. Releasing CPU load

Can we do the same thing without CPU intervention? Obviously, the answer is Yes!

 

6.1. DAC setup with DMA and Timer triggering

Edit the BSP_DAC_Init() function. What we need to add is:

  • A DMA configuration for Channel 3 that automatically conveys samples from the sine wavetable to the DAC output register. It is similar to what has been done before, but note that for the first time we want a Memory→Peripheral transfer direction
  • Enable the DAC DMA request, of course
  • A synchronous mechanism that triggers DAC output based on TIM6 update event, without CPU intervention. This is achieved by using TRGO internal signal that directly connects TIM6 to DAC.

 

/*
 * DAC_Init()
 * Initialize DAC for a single output
 * on channel 1 -> pin PA4
 */

extern const uint16_t sinewave[60];

void BSP_DAC_Init()
{
	// Enable GPIOA clock
	RCC->AHBENR |= RCC_AHBENR_GPIOAEN;

	// Configure pin PA4 as analog
	GPIOA->MODER &= ~GPIO_MODER_MODER4_Msk;
	GPIOA->MODER |= (0x03 <<GPIO_MODER_MODER4_Pos);

	// Enable DMA clock
	RCC->AHBENR |= RCC_AHBENR_DMA1EN;

	// Reset DMA1 Channel 3 (DAC) configuration
	DMA1_Channel3->CCR = 0x00000000;

	// Configure DMA1 Channel 3
	// Memory -> Peripheral
	// Peripheral is 16-bit, no increment
	// Memory is 16-bit, increment
	// Circular mode

	DMA1_Channel3->CCR |= (0x01 <<DMA_CCR_PSIZE_Pos) | (0x01 <<DMA_CCR_MSIZE_Pos) | DMA_CCR_MINC | DMA_CCR_CIRC | DMA_CCR_DIR;

	// Peripheral is DAC1 DHR12R1
	DMA1_Channel3->CPAR = (uint32_t)&DAC1->DHR12R1;

	// Memory is sine wavetable
	DMA1_Channel3->CMAR = (uint32_t)sinewave;

	// Set Memory Buffer size
	DMA1_Channel3->CNDTR = 60;

	// Enable DMA1 Channel 3
	DMA1_Channel3->CCR |= DMA_CCR_EN;

	// Enable DAC clock
	RCC->APB1ENR |= RCC_APB1ENR_DACEN;

	// Reset DAC configuration
	DAC->CR = 0x00000000;

	// Enable DAC Channel 1 DMA request
	DAC->CR |= DAC_CR_DMAEN1;

	// Select and enable TIM6 TRGO event as DAC trigger source
	DAC->CR |= (0x00 <<DAC_CR_TSEL1_Pos) | DAC_CR_TEN1;

	// Enable Channel 1
	DAC->CR |= DAC_CR_EN1;
}

 

6.2. Timer setup for autonomous DAC synchronization

We then need to enable TIM6 TRGO output trigger based on update events. You may also disable TIM6 interrupt by commenting the corresponding line as we want to demonstrate that CPU is not involved in the process:

/*
 * BSP_TIMER_Timebase_Init()
 * TIM6 at 48MHz
 * Prescaler   = 4   -> Counting frequency is 12MHz
 * Auto-reload = 200 -> Update frequency is 60kHz
 */

void BSP_TIMER_Timebase_Init()
{
	// Enable TIM6 clock
	RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;

	// Reset TIM6 configuration
	TIM6->CR1 = 0x0000;
	TIM6->CR2 = 0x0000;

	// Set TIM6 prescaler
	// Fck = 48MHz -> /4 = 12MHz counting frequency
	TIM6->PSC = (uint16_t) 4 -1;

	// Set TIM6 auto-reload register for 60kHz
	TIM6->ARR = (uint16_t) 200 -1;

	// Enable auto-reload preload
	TIM6->CR1 |= TIM_CR1_ARPE;

	// Enable Interrupt upon Update Event
	// TIM6->DIER |= TIM_DIER_UIE;

	// Enable TRGO on update event
	TIM6->CR2 |= (0x02 <<TIM_CR2_MMS_Pos);

	// Start TIM6 counter
	TIM6->CR1 |= TIM_CR1_CEN;
}

 

6.3. Testing the setup

Edit the main() function so that CPU is held into an infinite empty loop, after everything has been initialized:

...

// Main program

void main()
{
	// Configure System Clock
	SystemClock_Config();

	// Initialize 60kHz timebase
	BSP_TIMER_Timebase_Init();

	// Initialize DAC
	BSP_DAC_Init();

	// Main loop
	while(1)
	{
		// Do nothing...
	}
}

 

You can now try and run the project. The CPU is doing absolutely nothing and still, if you probe PA4 you should get a nice 1kHz sine wave output.

Note that there is no need of pointer in the wavetable, and that there is no interruption involved in the process (NVIC is even not initialized).

To make ideas even clearer, launch a debug session and step over initialization code while watching the oscilloscope. You’ll see the sine wave appearing as soon as you’ve step over the DAC initialization function, even with CPU halted by the debugger.

Isn't that Cool!

 

  Commit name "Sinewave generation with DMA"
  Push onto Gitlab

 

7. A little more on sampled waveforms…

Taking 60 samples per signal period produces a pretty smooth sinewave signal, although you can see the discrete samples quite well on the oscilloscope:

image052.png

 

You may (or may not) know that the sampling process produces noise and harmonic distortion because of the discretization process of both time (sampling period) and amplitude (DAC number of bits) scales.

 

7.1. A bit of theory

A common approach to measure noise and harmonic distortion of a signal is to analyze its spectrum. A signal spectrum represents the distribution of the signal power along the frequency axis. For this reason, we call “frequency-domain” such a signal representation. It differs from the classical “time-domain” representation you get from the oscilloscope.

For a pure sinusoidal signal, all the power in concentrated on a single frequency which is the signal frequency (f0 in the figure below). Therefore, the spectrum exhibits a single frequency compound f0. The amplitude of this compound corresponds to the RMS value of the sinusoidal signal. This is shown in the figure below (A). Note that although we usually represent only the positive side of the frequency axis, a negative side also exists, mirroring the positive side of the spectrum.

The sampling process is described in the situation (B) where the sinewave is only observed at uniformly spaced points in time. Just like snapshots. Doing this produces duplicates (or images) of the original spectrum (including its negative compound) every multiples of the sampling period fs.

Nevertheless, signal (B) is not the one we can see on the oscilloscope. In practice, the DAC is holding (maintaining) a sample value during a whole sampling period and until a new sample is available. Therefore, the signal is not only sampled, it is also held. This corresponds to the situation (C). In that case, the whole spectrum of (B) is multiplied in amplitude by a cardinal sine function (sinc), which mostly attenuates the spectrum images.

image054.png

 
Another important theoretical result concerns the noise resulting from the quantization process in amplitude (i.e.  discrete values of the DAC output). In the case of an ideal DAC, one can demonstrate that the SNR (Signal-to-Noise Ratio) integrated over a bandwidth between DC and fs/2 cannot be better than:

image056.png

where N is the number of bits available with the DAC. In our case, with N=12, we have:

image058.png

Note that this is a purely theoretical limit, with ideal DAC and no other noise source than the quantization process. In practice, we always measure inferior SNR. Better SNR can still be obtained over a reduced bandwidth (i.e. using a low-pass filtering). That's what you get when you hit the ‘average’ button of the oscilloscope...

 

7.2. A bit of practice

If you have an oscilloscope with FFT (Fast Fourier Transform) capability (any oscilloscope nowadays) you can turn this function on. Also set the input signal channel to AC coupling in order to remove the Vdd/2 offset from the analysis.

FFT is a digital algorithm that takes time domain data as input (i.e. signals on oscilloscope channels) and compute frequency-domain data as output (i.e. spectrum).
Try to play with the settings (mostly time and amplitude scales). If you are good enough, you may get this:

image060.png

The main frequency compound is found at 1kHz (as expected) with an amplitude of about 1.2Vrms with corresponds to:

image062.png

Signal theory told us that the spectrum of the signal, when sampled, is imaged around each multiple of the sampling frequency. The sampling frequency in the tutorial was:

image064.png

In the screenshot above, with 1kHz/div on the FFT, the first spectrum replica at 60kHz is far on the right, beyond screen. Changing oscilloscope and FFT range reveals the expected first harmonic images around 60kHz, and 120kHz…

image066.png

 

Focus on the spectrum around 60kHz. We have:

  • The image of the negative part of the ideal sinewave at image068.png
  • The image of the positive part of the ideal sinewave at image070.png

We can compute the sinc function for each of these frequencies considering :

image072.png
image074.png
image076.png

If you look at the amplitude measured in the above screenshot (vertical grid unit is 5mV), you will see a very good match with these theoretical results. Do the same around 120kHz, you will get go good match again with amplitude close to 10mV.

Regarding SNR, you can get an idea by using the dB scale of FFT. SNR is the distance between the fundamental peak height and the noise floor. Here, it is close to 50dB. This is equivalent to the ideal SNR obtained with a 8-bit discretization process.

image081.png

Let us now make things bad, for fun…

Instead of having 60 samples per signal period, we build a new wavetable with only 8 samples (i.e. a sample every 45°):

const uint16_t sinewave_lowres[8] =
{
		0x0800, 0x0da2, 0x0ff8, 0x0da2,
		0x0800, 0x025e, 0x0008, 0x025e
};

/*
 * DAC_Init()
 * Initialize DAC for a single output
 * on channel 1 -> pin PA4
 */

extern const uint16_t sinewave[60];
extern const uint16_t sinewave_lowres[8];

void BSP_DAC_Init()
{
	...

	// Memory is sine wavetable
	DMA1_Channel3->CMAR = (uint32_t)sinewave_lowres;

	// Set Memory Buffer size
	DMA1_Channel3->CNDTR = 8;

	...
}

 

Leaving the timer settings as they are, we still have a sampling frequency of 60kHz, but now the signal frequency becomes:

image085.png

In this situation, spectrum replicas every 60kHz are quite noticeable with an amplitude of ≈150mVrms for the first one:

image087.png

 
Again, let us verify this result:

image089.png
image091.png

Good match again!

Signal theory is a wide topic that we are not going to cover here. At this point, it is just important that you get a basic feeling of what the sampling process involves in terms of harmonic distortion. The ratio between sampling frequency and signal frequency (i.e. the number of sample per signal period) is a determinant factor to pay attention for in order to reduce the distortion produced by the sampling process.

 

8. Summary

In this tutorial, you have learned how to output sample from the DAC without any load on the CPU by making a clever use of both DMA and timer TRGO capability.