#stm32 blue pill approx. 440Hz wave using PWM

Dissatisfied with the offerings from elsewhere, I have decided to write my own STM32 bare metal library from scratch. It is shaping up nicely. I’ve gotten GPIO, delays, interrupts, PWM, I2C and SPI working, so I’m well on the way of having a truly usable lightweight library which eschews piling on layers of abstractions. I haven’t investigated ADC or DMA yet.

It has not been an easy task. There is no way I could have achieved this much without the many online tutorials, videos, peeks at code and a bit of reverse engineering. I’m getting better at using datasheets, but for me anyway, they are a bad place to start. They can be impenetrable if you don’t have some conceptual hooks on which to hang the details.

I hope that other people find my growing library useful, if for no other reason than to use as a cheatsheet, or come away thinking “ah, so that’s how you do it.” I focus specifically on the stm32f103c8t6 (blue pill). That keeps the layers of abstraction to a minimum. You may well find that my libraries have limited portability outside of it, though.

Anyway, today I wanted to enhance the PWM capabilities of a previous example by producing a 440Hz triangular wave. Here’s the code:

include <gpio.h>
include <timers.h>
include <usart.h>

define FREQ 440
define NSAMPLES 16

char msg[40];

void printi(u32 v)
{
  itoa(v, msg, 10);
  print(msg);
}

void print_bin(u32 v)
{
  for(int i=0; i<8; i++) {
    print(i==0 ? " 0b" : "'");
    for(int j=0; j<4; j++) {
      putchar(v & (1<<31) ? '1' : '0');
    v <<= 1;
    }
  }
}

void TIM3_IRQHandler()
{
  // increment the PWM counter for when to set low
  static u32 count=0;
  if(count >= NSAMPLES) count = 0;
  TIM3->CCR1 = count++;

  TIM3->SR &= ~TIM_SR1_UIF; // reset interrupt flag
}

void setup_timer()
{
  RCC_APB1ENR |= RCC_APB1ENR_TIM3EN;
  gpio_mode(PA6, 0b1011); // output 50MHz, push-pull, alt function
  TIM3->ARR = NSAMPLES -1; 
  TIM3->PSC = 8000000UL/FREQ/NSAMPLES/NSAMPLES ; 
  printi(TIM3->PSC); 
  TIM3->CCER |= TIM_CCER_CC1E; // enable capture/compare   
  TIM3->CCMR1 |= TIM_CCMR1_OC1PE; // enable preload 
  TIM3->CCMR1 |= 0b110<<4; // output pwm compare mode 1 
  TIM3->CR1 |= TIM_CR1_CEN; // enable counter
}

void printn(u32 v)
{
  printi(v);
  print_bin(v);
  puts("");
}

void main()
{
  gpio_mode_out(BUILTIN_LED);
  init_serial();
  puts("12 pwm with interrupt");
  setup_timer(); 

  // setup interrupt 
  NVIC_ISER0 |= (1<<29); // position for   TIM3_IRQHandler 
  TIM3->DIER |= TIM_DIER_UIE; // enable interrupt (this wasn't in project 11) 
  enable_irq(); 
  beep(); 
  while(1);
}

And here’s some commentary:

Attach a speaker with one terminal to PA6, and one to ground. This project builds on project 11.

As we saw in project 11, it is possible to produce a PWM signal of a given frequency and duty cycle. This project takes things a step further: varying the duty cycle over each cycle. It is therefore possible to produce DAC-like behaviour using PWM. I am excited by this prospect, as it means (potentially) that we can ditch DACs and just use PWMs.

The ingredients added to this project are:

  • interrupts, where we increment the timer’s CCR1 (capture/compare register 1) on each interrupt
  • use "preloading" of CCR1 (aka "buffering" or using the "shadow register") so as to ensure that CCR1 is only updated at the beginning of an ARR (Auto Reload Reset) rather than at some arbitrary time

Near the top of main.c, we have:

#define FREQ 440
#define NSAMPLES 16

indicating that we want to generate a signal of 440Hz, and discretise the output "volumes" over 16 samples. Our timer will therefore need to be of a frequency of 7040Hz (=440*16), which is 16X faster than in project 11.

In setup_timer() we have:

TIM3->ARR = NSAMPLES -1;

for convenience sakes, and we’re going to get TIM3->CCR1 incrementally on each interrupt.

Continuing:

TIM3->PSC = 8000000UL/FREQ/NSAMPLES/NSAMPLES ;

We’re using an 8MHz clock. Recall that

PSC+1 = f\_clk/f\_pwm/NSAMPLES

where

f\_pwm = FREQ * NSAMPLES

yielding the code above.

Further down, we dispense with setting TIM3->CCR1, because we are going to do that in the interrupt. We do need to add the line:

TIM3->CCMR1 |= TIM_CCMR1_OC1PE; // enable preload

which will allow us to properly sync our new value of CCR1 with the ARR reset.

We initialise the interrupt:

NVIC_ISER0 |= (1<<29); // position for TIM3_IRQHandler
TIM3->DIER |= TIM_DIER_UIE; // enable interrupt (this wasn't in project 11)
enable_irq();

Here is the interrupt:

void TIM3_IRQHandler()
{
        // increment the PWM counter for when to set low
        static u32 count=0;
        if(count >= NSAMPLES) count = 0;
        TIM3->CCR1 = count++;

        TIM3->SR &= ~TIM_SR1_UIF; // reset interrupt flag
}

Note that TIM3 is used by pin PA6 on channel 1 for PWM. We could, theoretically, use a different timer to change CCR1, but there is not point.

TIM3_IRQHandler() triggers with a frequency 7040Hz, and we increment CCR1 by 1 each time, cycling when we get to a count of NSAMPLES. Note that this produces a triangular wave rather than a sawtooth one. A pin’s maximum volume occurs when the duty cycle is 50%, so as we increase the duty cycle beyojnd 50%, the volume actually goes down again.

Note that the final line of the interrupt handler reads

TIM3->SR &= ~TIM_SR1_UIF; // reset interrupt flag

which resets the interrupt so that it doesn’t end up being a one-shot.

The full code is available in its project directory on github.

I hope you find it useful. If there’s really strong demand for this kind of material, I may think of switching to a more useful platform. WordPress is OK if you want to produce flowery posts, but it is hard work for technical writing.

About mcturra2000

Computer programmer living in Scotland.
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s