Generating a sawtooth wave using integers

Using floats to generate waves for a microcontroller is conceptually straightforward, but tends to have some drawbacks:

  • Your MCU might not even have an FPU. Computation will be expensive!
  • You can lose significant bits, which may be problematical when you’re accumulating a small number into a larger number. IEEE-754, for example, specifies 23 bits as the Mantissa of a 32-bit float.

There is one nice benefit of using integers, though: you get automatic wrap-around on overflows.

Let’s suppose you want to generate a sawtooth wave of frequency fw, with a sampling frequency fs, and you want the number of bits generated to be nbits.

So, maybe you’re producing a wave for a DAC. Typically we want to produce a wave of 500Hz (fw=500), at a sampling rate of 44100Hz (fs=44100) for a DAC that 12 bits of precision (nbits =12). 10 bits of precision are also common for DACs. You are likely to want to keep your sampling rate fixed. If a 44.1kHz is stetching the capabilities of your MCU, you might want to try 22kHz, 16kHz, or even 8kHz.

Code

I’m going to work with uint32_t (32-bit unsigned integers). I normalise the increment in volume value for each time step of the sample as follows:

uint32_t dy = ((1<<30)/fs)*2 * fw;

What’s going on here, then? Well, it is important to note that order of computation matters so that you’re not overflowing and getting bad results. Parentheses are your friend.

1<<30 is 2^31. This is our “1”, but scaled up as much as possible. We can’t use 1<<31, because that is 2^32, which is 0 for 1 32-bit integer.

We then divide by fs to obtain the time increment of each step. This is a scaled-up number that will have more accuracy than any 32-bit float representation.

Next, we multiply by 2. This is because our results our out by this factor. Earlier we wanted to use 2^32, but had to use 2^31, so now we want to shift left by 1 to preserve the unity scale.

What the above accomplishes is, in effect, the denominator of our equation. If we were using floats, we’d just write 1.0/fs for the equivalent expression.

Then we want to multiply by fw to account for the frequency of the wave we want to produce. The faster the frequency, the greater the increment.

To generate a value of nbits bits, we need to rescale the accumulated number by right-shifting by 31-nbits.

Here’s the code I used in a project:

uint32_t get_saw_value(void)
{
	const uint32_t dy = ((1<<30)/fs)*2 * fw;
	volatile static uint32_t y = 0;
	y+= dy;
	const int nbits = 12; // it's a 12-bit DAC
	return y >> (31-nbits);
}

I have a timer interrupt that calls this function at 44.1KHz, and stuffs the returned value into the DAC.

Speed Test

I tested the code on an STM32L432LC with the clocks and peripherals set at 80MHz. I set up a timer to fire at 44.1kHz. I found that the timer took 800ns to execute, or about 3.5% of the CPU time. I did stuff like pin toggling to perform these calculations, and also calibrate the precision of the timer. So the actual computation was undoubtedly less.

So it looks like we’re doing fine on our computational budget. You could argue that we’ve spent too long optimising the problem. If a 500Hz sawtooth wave was all we wanted to do, then sure. But what if you want to do some signal processing? You’ll suddenly find that you’re hogging a lot of CPU as you implement a number of filters, wave-folds , and other fancy bits. It’s also one reason why I try to eschew too much HAL infrastructure. That stuff can really eat into your computation budget.

Wrapping up

I hope you found this article useful. I notice on my analytics for the blog that I am mostly speaking into the void. Maybe one day that will change. It’s good to see what countries are interested in my blog. The UK and US are, of course, the prime readers. Still, there are places like Germany (guten tag!), some other European countries (rarely France, now that I think of it), and exotic places like India, South Korea and Singapore (sometimes China). I bid you well, wherever you come from.

(Refs: db07.29, L432/04)

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