Generating signals with STM32L4 timer, DMA and DAC

Generating arbitrary signals using an MCU is extremely useful. It can be used for example to play back any audio or make a modulator for a modem. The most needed MCU peripheral is of course a DAC, but it also needs other peripherals to efficiently play back the samples without loading the CPU.

This post shows how to implement a signal generator on an STM32L432 without using HAL libraries.

You can find a similar example for AVR XMEGA here.

The first thing is of course to have the audio samples stored somewhere. This example uses 77 samples of single cycle of a sinewave (keep reading why exactly 77). STM32L432 DAC can be use in 8-bit mode, so each sample is simply an uint8_t and is stored in flash.

The goal is to generate 2200 and 1200 Hz tones. So the samples have to be updated 1200*77 and 2200*77 times per second (because 77 samples make a single cycle). For the higher tone this effectively means an update rate of 169400 Hz which would put too much interrupt load on the CPU, so it is best handled using DMA. DMA just copies the consecutive samples from flash to the DAC register output address. DMA however does not know when to copy the samples (it could go as fast as the bus and memory subsystems allow), so yet another peripheral has to tell DMA when to do the transfer. In this example it is the general-purpose timer TIM2.

Not all timers can generate DMA requests. This is illustrated in the reference manual. In my case I just picked up DMA1 channel 1 (there are two separate DMA peripherals, each with their own channels), which means that I have to use timer TIM2 channel 3 (don’t confuse DMA channel with timer output compare channel).

Another issue that must be taken care of is the memory architecture. In case of STM32L4 this is not a problem, but in case of other STM32 you may be more limited (eg. only DMA2 could access the DAC memory region). It is usually shown in one of the first manual chapters:

The goal is to move data from flash to DAC. DAC is connected to APB1, which is connected to AHB1 (AHB is the “fast” system bus, APB is the slower bus). As you can see both DMA1 and DMA2 can access flash memory and both of them can also access AHB1 memory space.

Summing up:

  • Timer TIM2 output channel 3 will tick at the appropriate update rates
  • TIM2 output channel 3 will generate an event on overflow
  • This event will trigger DMA1 channel 1 transfer
  • DMA1 channel 1 will transfer data from flash to the DAC 8-bit output register
  • DMA1 channel 1 will use circular mode, so when it reached the last sample it will start from the beginning
  • The DAC knows nothing, it only has to be enabled and will update the voltage on every output register write

The code

Here is the code. It does not use HAL libraries and has no extra dependencies. APB1 frequency is 10 MHz (it is relevant for TIM2).

Magic numbers

There are some magic numbers involved like the 77 sine wave samples, timer reload values 59 and 108. Where they come from? First of all – the timer reload value. It specifies how long the timer should count until it overflows and starts from zero again (so the higher the reload value, the lower the sample rate and the lower the output frequency). The timer runs of APB1 clocked at 10 MHz. To generate 1200 Hz using 77 samples they have to be updated at the rate of 1200 * 77 Hz, ie. 92400 Hz. The timer must overflow at that rate, so to calculate the reload word, divide 10’000’000 Hz by 92400 Hz and you will get 108.225… Of course the timer reload register only accepts integer values so it has to be 108. The difference between the ideal value and the one that can be put into the register will lead to a small frequency error.

To minimize the error I made a simple spreadsheet that calculated the average error for both tones vs. number of samples and found out that for 10 MHz APB clock the minimum error(0.2%) is at 77 samples.

After recording the audio with a sound card and measuring the frequency in Audacity I found out the frequencies to be exactly twice as high, so I multiplied the timer reload values by 2 without further investigation.

This is the final result (as recorded by Audacity):

I release this code into the public domain.

Rate this post