STM32F4: Generating a sine wave

Now that the basic peripherals of the STM32F4 have been configured, we can start to make them do useful things. The aim of this “mini-project” is to generate PSK31 modulated data. PSK31 is a very narrowband technique for sending data. Typically, it is used at VHF by ham radio operators, but we have adapted it in our recent IPSN2012 paper for use in underground mines using Magneto-Induction. At the moment, we are using the PWM to output analog values (after filtering), but it would be just as easy to use a DAC or the onboard CODEC.

We want to generate a sine wave at an arbitrary carrier frequency. The simplest way of doing this is to use a software Direct Digital Synthesis (DDS) technique. The basic concept is just to compute the wrapped phase at each sampling instant. In practice, what this means is that we use a free running sampling clock, say running at 100kHz to make life easy. Again, to make life easy, lets assume we wanted to generate a sine-wave with frequency 1kHz. 1kHz is equivalent to an angular frequency \omega=2000\pi radians per second. Our sampling period is 10 us, so in each discrete time step, the phase is advancing by \phi = 2000\pi \times 10^{-6} = 20 \pi \times 10^{-3} radians.

In general, for an arbitrary sampling frequency f_s and required frequency f_o, the phase must increase by R=2\pi \frac{f_o}{f_s} at each sampling instant. Because we are using fixed point numbers, and not reals (floating point), we let 2^{32} = 2\pi . If we were using a 16 bit phase accumulator, then obviously change 2^{32} to 2^{16}.  So all we need to calculate is:

R=2^{32}\frac{f_o}{f_s}

 

The overall procedure is quite simple:

  1. We keep a phase accumulator, which typically has a much higher resolution than the output DAC.
  2. At each sampling instant, we increment the phase accumulator by R.
  3. Sine of the current phase accumulator is then sent to the DAC.

The only “tricky” part in reality is scaling numbers from real (double) to integers to make things easier to compute. For speed, typically the angle-to-sine(angle) conversion is precalculated, in a lookup table (LUT). If you are really smart, you will note that only the first quadrant of the sine lookup needs to be specified, as the rest can be calculated through reflection or inversion. I am lazy, and the STM32 has tons of memory (it could actually easily calculate the true sine value on the fly), so I just precalculated the whole table.

The phase accumulator is stored as a 32 bit long. To get the 8 bit DAC value, the phase accumulator is shifted to the right by 24. This is then used as the index to the LUT.

The entire meat of the code lives in the Timer 2 ISR, which is called at a rate of 100 kHz. Code takes 300ns to execute, so it is taking about 3% of the STM’s brain power (clocked at 84MHz) to update this. This is obviously complete overkill just to generate a lowly sinewave, but it does it pretty well. Here is a screenshot of PD13 showing how long the processor takes in the ISR:

Measuring the ISR execution time

Measuring the ISR execution time

The code itself is simplicity itself, with the magic phase accumulator parameter R just being added on at each interval.

void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
GPIO_SetBits(GPIOD, GPIO_Pin_13);
PWM_SetDC(1,pulse_width);
// Calculate a new pulse width
phase_accumulator+=R;
angle=(uint8_t)(phase_accumulator>>24);
pulse_width = sinetable[angle];
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
GPIO_ResetBits(GPIOD, GPIO_Pin_13);
}
}

Before filtering, the PWM looks nothing like a sine wave!

PWM output – you can see the pulse width reducing towards the right. PWM frequency is ~ 325kHz.

After filtering with a single pole RC (R = 22k, C = 2.2nF), things look a whole lot better, and it looks like a pretty decent attempt at a sinewave. In this case, we are generating a 2500 Hz sinewave, so the magic number R = 107374182.

DDS generated sinewave at 2.5kHz

If we zoom in, we can see that the sinewave is not perfect. This is due to the RC filter not rejecting the high frequency PWM, leading to a slightly jagged waveform. This can be addressed using a higher order filter, such as cascaded LC sections. However, for our application of generating a low frequency magnetic field (where the coil itself acts as an LC tank), this is adequate for now. For a £10 dev board and a few hours work, I would say that this is quite successful!

Zoomed in sinewave, showing slightly rough edges due to inadequate filtering

Download the main listing.

 

5 thoughts on “STM32F4: Generating a sine wave

  1. Great stuff, I would like to know more about the magic number R.
    Looks like R is 1/10th of 2^30 ! bear with me if I go wrong. Thanks in advance.

    1. R is not entirely magic. Basically, what we have is a free running sampling clock which ticks at 100 kHz. We want to generate a sine wave with a frequency of 2.5 kHz (I have edited the post to clarify that we are generating 2.5 kHz, not the 1kHz example frequency, and also shown how to actually calculate R). 2.5 kHz/100 kHz is 1/40. So R is 1/40th of 2^32. You are completely correct that R is 1/10th of 2^30, but easier to stick to 32 bit integers!

  2. While looking for a solution to a sinewave generation problem I came across this doc. I immediately noticed your reference to PSK31 since I am a ham. It’s good to see that amateur radio technology can still find a place in our high tech world of corporate developed technology.

    BTW PSK31 is primarily used at HF in the 3 – 30 MHz range. I am very active on that mode and have been using it for 7 years.

    I read some on your animal tracking research. I wish I could use that system to track my groundhogs and find a way to get rid of them – what a nuisance! My English Pointer did a good job of that but he is no longer with us.

    Also, this doc was a real help on my sinewave problem – thanks.

    Jim WA4YWM

  3. Hello,
    Amazing application, but I wanted to increase the size of the LUT but I am afraid there is some problem.

    Here is what I did
    1) I calculated by LUT for a size of 4096
    2) right shifted 32 bit number by 20 to get the index of the table

    Please let me know where am I going wrong.

    1. Hi Sameer,
      Sorry for the delayed reply. Without seeing your code it is hard to say what is going on, but I would imagine the most likely thing is in this line:
      angle=(uint8_t)(phase_accumulator>>24);
      I have implicitly set the size of the LUT to 256 entries (8 bit number). What you will need to do is cast to uint16_t and make sure that you make the result modulo 4096 so you don’t step past the bounds of the table.
      Best,
      Andrew

Leave a Reply