simple sound player: PWM audio output, with DMA, without timer interrupt

stevestrong
Sun Oct 29, 2017 9:20 am
I have a project in which I need some short sound effects, without reading WAV files from SD card.

To output PWM audio, current solutions use timer interrupt to update the new value in the compare output register.
This version uses the DMA (channel 7) triggered by timer (4) update event to do the same, so the processor has now plenty of time to do other tasks.
Speaker out on PB6 (timer 4 ch 1).
No interrupts are used, although one could use the DMA transfer end IRQ to do some stuff.

EDIT: – I added the possibility to use DMA interrupt. For this just enable the define in line 10.

This example uses a half second long WAV file (converted to C code) with 8 bit resolution, 16 kHz sampling rate, stored in the flash (~8kB data).
The timer prescaler and preload registers are optimized in order to have a maximum peek to peek output range for 0-255 PWM values.

Have fun.

Needed patches:
https://github.com/rogerclarkmelbourne/ … 2/pull/366
https://github.com/rogerclarkmelbourne/ … 2/pull/367

[quiz: what does the sound say?]

RogerClark
Sun Oct 29, 2017 9:57 pm
Thanks Steve…

Does this run on the F103?


stevestrong
Sun Oct 29, 2017 10:21 pm
Yes, it runs on blue pill.
It can be adapted for F4 as well, the DMA part needs some change for that.

RogerClark
Mon Oct 30, 2017 12:33 am
Thanks

I’ll try it when I get from free time


ChrisMicro
Mon Oct 30, 2017 3:04 pm
Some time ago I also made some sound player. for the STM32F103 in the GENERIC repo.
It has no DMA but the sound quality was OK.
It is only a sine wave but could easily be extended for playing a wav.

stevestrong
Mon Oct 30, 2017 3:11 pm
Yes, I saw that, and also the bluepill delta-sigma examples.
By those examples, however, I still don’t understand how do you convert the sampling frequency to bit output frequency.
It would help a lot if you could make at least a hand-written scheme of the working principle (not for the pure delta-sigma, but of the whole concept).

Anyway, the main point here is that my example does NOT use the timer ISR, just the DMA. ;)


ChrisMicro
Mon Oct 30, 2017 3:53 pm
Anyway, the main point here is that my example does NOT use the timer ISR, just the DMA.
Yes, it is a good idea to use the DMA to avoid processing load.
If it would have been earlier available, I would have used your example ;-)

stevestrong
Mon Oct 30, 2017 4:17 pm
I had some more thoughts on this.

The audio signal level, given by the WAV sample value, is actually the duty-cycle of the PWM.
Now, it is desirable to move the PWM base frequency from the sampling one to an upper frequency region in order to avoid PWM frequency disturbance on the audio output.
That is why good PWM DACs are using frequencies of 140kHz and above.

I think we could do the same trick, at least for 8 bit resolution signals, using SPI.
Outputting an array of 8 x 32bit words (= 256 bits) with high clock frequency we could simulate the same duty cycle of the audio signal.
On each audio sample frequency tick we just have to update the array values correspondingly to the required duty cycle.
Using SPI1 with 36MHz clock we could achieve 36MHz / 256 = 140.625 kHz PWM base frequency for 8 bit resolution.

I will try this.


ChrisMicro
Mon Oct 30, 2017 5:57 pm
I think the ear can’t hear frequencies above 20 kHz. Therefore PWM frequencies above 20kHz are always enough.
It might have other reasons when they use 140kHz in the PWM Audio-DACs.
But what really matters: the resolution. The more bits, the better.

There are various tricks in the mozzi library to increase the DAC resolution.
Can you record the results of your various DAC implementations and place it somewhere?


stevestrong
Mon Oct 30, 2017 6:25 pm
The reason to increase PWM frequency is to reduce distortions, see: http://www.openmusiclabs.com/learning/d … dac.1.html, chapter 4.
So, if you have a 30kHz carrier, and you are creating a 5kHz tone, you will have signals at 30kHz +/- 5kHz, 10kHz, 15kHz, etc.

ChrisMicro
Tue Oct 31, 2017 6:55 am
The reason to increase PWM frequency is to reduce distortions, see: http://www.openmusiclabs.com/learning/d … dac.1.html, chapter 4.

This is really the best link I ever saw regarding this topic. Thanx .

Even the BluePill is mentioned. :D


RogerClark
Tue Oct 31, 2017 9:09 am
Steve

I’ve commited your patches manually so they both occurred in the same commit

https://github.com/rogerclarkmelbourne/ … b6315fed04


stevestrong
Tue Oct 31, 2017 9:12 am
OK, thanks.

Meanwhile I managed to generate a 70kHz PWM signal using MOSI (SPI2), but currently there is sometimes a glitch in the signal, I am trying to figure out where is it coming from. In the theory it should not be there :mrgreen:


RogerClark
Tue Oct 31, 2017 9:18 am
cool

victor_pv
Tue Oct 31, 2017 8:36 pm
Just as a note, on the F1 I was doing 8bit of resolution, with the PWM running at max about 132Khz (3 x 44.1), and coulnd’t hear any noise, and adding an RC filter didn’t change the sound at all.
I was also using DMA triggered by a timer in my latest version, and using the DMA ISR to reload the buffer from the sdcard, so cpu load from playing the samples was minimal, mostly the sdcard access was taking the time, but still all in all I think it was taking less than 20% cpu to read the file and reformat the samples (2 channels, 8 bits, from whatever the format was in the file), in the F1 at 72Mhz.

I see a main difference with what you are doing is that you are using the HardwareTimer class, and after a a few revisions I decided to skip it and just use the libmaple timer functions, and that I see the repeat counter to get the 3x frequency. You should look at that, it’s a feature in the timer DMA mode in which rather than send a DMA request each time the timer reloads, you can set a number of repetitions.
So as you are only using 8 bits of resolution as I was, you can run the timer much faster, but use the repetition value to output the same sample N times. At 16Khz, with a base frequency of 72Mhz, you could run the timer 17.5 times faster, and still get 8 bits of resolution in the PWM:
(72000000 / 16000)/17.5 = 257.1
You have to use integers in the repetition value, so you could set it to 18, 17, or other values, and calculate the max amplitude value you can use (250 ARR at 18 repeats). I think you get the idea.


stevestrong
Wed Nov 01, 2017 8:57 am
Victor, indeed, it seems a good idea to use the DMA burst transfer feature of the timers.
I was reading about it but couldn’t imagine before what is that good for. Now it makes sense ;)

If I set the prescaler to only a factor of that what is set now (basically divide it by an integer), I can multiply the PWM frequency by keeping the resolution. Clever.

I try to use as much as possible the HardwareTimer functions because it seems that they offer a wide scale of setup possibilities.
Now that Roger has included the DMA enable/disable functions, it became even more powerful.

My timer setup routine looks like this:
void TIMER_Setup()
{
pinMode(SPEAKER_PIN, PWM); // activate output
timer.pause();
// try to set the reload register to a value as close as possible to 256
// to get full range audio 0 - 3.3V for 0 - 255 PWM values (8 bit resolution)
uint32_t prescaler = F_CPU/SAMPLING_RATE_HZ/256;
timer.setPrescaleFactor(prescaler);
uint32_t overflow = F_CPU/SAMPLING_RATE_HZ/prescaler;
timer.setOverflow(overflow);
timer.enableDMA(0); // for update event use channel 0
}


RogerClark
Wed Nov 01, 2017 9:21 am
Steve

I’ve tried the code, and I can hear something coming from the speaker I attached via a 100nF capacitor, but its very quiet, and looking on my logic analyser, the waveform is not what I expected.

What I see is a 16Khz square wave, and on the falling edge of each half cycle, I see a burst of higher frequency square waves at 50Mhz !

So I disconnected the speaker, and I see something different, so it looks like just connecting via a capacitor to a speaker is not a good idea

What did you connect to the speaker pin ?

e.g. Do you use a RC filter and connect to an amplifier ?


stevestrong
Wed Nov 01, 2017 9:45 am
Currently I have a simple piezo buzzer (without electronics) connected.
Yes, the sound is relatively quiet, although the audio wave has 90% peek-to-peek amplitude.
But I cannot explain the spikes on falling edges, it probably has to do with using the speaker with the capacitor.
I would recommend to use the speaker either directly (for speakers with 8 Ohm or more) or in series with a resistor.
I also put an RC filter to input a small amplifier, but as I have no speaker I cannot use it. But the audio signal after RC (2k2 & 150nF) is very smooth.

RogerClark
Wed Nov 01, 2017 10:00 am
I’ve just tried a small amplifier via a 5k pot and a capacitor, but I’m just getting a series of clicks

I’ll try a piezo tomorrow


stevestrong
Wed Nov 01, 2017 10:29 am
Victor,
As fas as I understand, the DMA burst transfer of the timer cannot be used to repeatedly output the same value to the same timer register, but to send a burst of values to a specific number of consecutive timer registers.
So if I want to update CCR1 and CCR2 within the same DMA transfer then I can set the burst length to 2 and then I can have stereo PWM signal on CC1 and CC2 output, wherein the samples are read in one shot (burst) by the DMA from consecutive memory locations.

I think the only way to repeatedly send the same value to CCR1 is to multiply it by software. Set the DMA buffer size to (let’s say) 17 and fill it up with the same sample value, triggered by the timer set to 17x the sample frequency (ex. 16kHz*17). This DMA buffer has to be updated with the new sample value on the next sample period (ex. 16kHz).
Or maybe using only a 4x multiplier, transform a linear buffer of
abcde…
to
aaaabbbbccccddddeeee…
Is this what you did (with 3x multiplier)?

Or did I completely misunderstand something?


victor_pv
Wed Nov 01, 2017 1:14 pm
It’s a different feature than the burst.
In the burst, you can update several timer registers at once, because the timer will generate several DMA requests instead of one.
The repeat feature is more like the opposite (although both can be used together), the timer will reach the reload value several times before if generates one request.
It’s like this:

Memory content:
abcde

Timer actions (if set for 3 repeats for example):
Timer generates dma to load first value “a”.
Timer counts until “a”, does not generate DMA request, and starts counting again
Timer counts until “a” for the second time, does not generate DMA request, and starts counting again.
Timer counts until “a” for the thrid time, now generates DMA requests to load “b”
Timer counts until “b”, similarly 3 times, and only in the third generates DMA event and loads “c”
Timer counts until “c” 3 times, and on the third triggers DMA to load “d”

All those times the timer compare events are happening, so you get your PWM signal with your duty cycle acccording to the values, but you get it 3 times before it loads the next one.

The effect is that you can set an ARR that produces 3 times your frequency )3 is just an example, ir all depends what frequency and how many bits or resolution you want to use for PWM). Frequency was 16Khz in the example, you can configure ARR to produces a 48Khz frequency, so the exact same pwm pulse will be produced 3 times, then load the next value and produce that pulse 3 times. The PWM duty is the want you wanted, since it’s a relation between ARR and the Timer compare value, but the frequency is 3 times higher, so your noise is concentrated at a much higher frequency and easier to filter.

The datasheet explains it better than me, sorry I’m not too good at explaining this things. The section is called “repetition counter” in the reference manual. It’s section 14.3.3, page 305.

EDIT: for reference, this is my old code before adding the DMA stuff, but it uses the same repetition register (RCR)
The value is calculated here (is precalculated above that for a few normal frequencies, but can be calculated on the fly with same results, and it does for unusual frequencies):
https://github.com/victorpv/Arduino_STM … #L339-L344
The value is set to the register here:
https://github.com/victorpv/Arduino_STM … cm.cpp#L94

I hope seeing the formula to calculate it and the reference manual will make it easier to understand, my code is pretty nasty, specially that file.


stevestrong
Wed Nov 01, 2017 1:54 pm
Ah, thanks, I got the point about the “repetition counter”.
I couldn’t find it because I was looking to the general purpose timers, they don’t have this feature, only the advanced ones (TIM1 and 8).
This means that you are limited to the TIMER1 compare channel output pins (and the remapped ones). But this is not that critical.

RogerClark
Wed Nov 01, 2017 7:55 pm
Steve

can you briefly explain how the code you posted, is supposed to work ?

From looking at ou code, I think you are using DMA to control the timer PWM.
The DMA sends the values from the array ( which is data from a WAV file)

This appears to change the PWM proportion.

However I dont seem to be getting the resulst I exoected.

If I connect an 16 ohm headphone ( ear bud) via a 330 ohm resistor, I can hear something like the word “hello”, but when I look at the soeaker pin with a logic analyser, I see a very strznge waveform, where the falling edige of then output pwm appears to have a 50mhz pulse of square waves.
I think this must be ringing caused by the inductance of the speaker.

The other thing I noticed, is that the values in the wav array, dont seem to have a very wide range.
Are they 8 bit, 16 or 32 bit values ?

I initially thought they were 8 bit, but I need to doible check, as they may be 16 bit.


stevestrong
Wed Nov 01, 2017 7:59 pm
They are 8 bit values.
And congrats, you solved the quiz :) , it is indeed the hello word being said.

The timer 4 update event triggers the DMA which is writing the wav sample values to the CCR1 register. Thats all.


RogerClark
Wed Nov 01, 2017 8:01 pm
did you normalise the wav file to get max range of 8 bit values?

stevestrong
Wed Nov 01, 2017 8:05 pm
No. I was happy with the original amplitude which had 80% peek.

RogerClark
Wed Nov 01, 2017 8:08 pm
OK

I’ll do some more tests.


RogerClark
Wed Nov 01, 2017 8:16 pm
BTW.

Does the sine table in the code still work?

I tried to enable it, but didn’t get a tone

Edit.

Sine table just clicks a bit

I don’t think things are working the same for me as they do for you :-(

I’ll attach my logic analyser and look at the PWM pin PB6

Edit 2.

I found my piezo was a bit faulty. I tried just using digitalWrite to make a tone and it didnt work very well.

I’ve found the loose connection inside the piezo and its working a little better, but I think I may need to connect the piezo between Vcc and PB6 rather than ground and PB6 as this may stop the clicking.

I think however that the direct linkage between the wav data sample and the PWM is not ideal, as it needs lots of samples to get good audio. Probably 48kHz or higher would be better.

BTW.

Did you use this to create the data

http://ccgi.cjseymour.plus.com/wavtocode/wavtocode.htm


stevestrong
Wed Nov 01, 2017 9:14 pm
The sine table should work but its length is really short, more than a click is not hearble. But it can be seen with the scope ;)

The other click is due to audio level sudden jump from steady (low or high) to middle level when it starts playing.
It can be avoided by fading the in and out portions.

Yes, that is the converter. But meanwhile i think it can be generated more easily with HxD (hex editor), open file and save as code.

The audio is of couse better quality when sampled with 48kHz. But this was a quick feasibility test to check quality of short samples which should fit in flash.

Edit
In parallel i am evaluating the tone functions, you probably observed as i pushed a pr. But that is another story.


RogerClark
Wed Nov 01, 2017 11:48 pm
Hi Steve

Yes. I saw your PR for the tone library, I need to test before I can merge

One thing I noticed with tone that it didn’t work on the pin you are using i.e PB6

I’m not sure why that is.

I think ideally the tone library should work on all pins, even if we have to make it blocking, but thats a different matter.


Leave a Reply

Your email address will not be published. Required fields are marked *