ESP32 and PWM

I’m continuing reinventing wheels with my ESP32. In ESP32 and Mozzi I was using the built-in DAC for audio output. In this post I’m taking a detailed look at how to use PWM for audio output instead.

  • Part 1 – All the theory and research around PWM and the ESP32.
  • Part 2 – Generating different waveforms on multiple channels.
  • Part 3 – Introducing analog control and frequency modulation.

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

These are the key tutorials for the main concepts used in this project:

If you are new to microcontrollers, see the Getting Started pages.

Parts list

  • ESP32 WROOM Module
  • 2x 1kΩ resistor
  • 1x 10uF electrolytic capacitor
  • 1x 100nF capacitor
  • 1x TRS socket
  • Breadboard and jumper wires

ESP32 PWM

The ESP32 has a PWM peripheral in the shape of the LED PWM Controller – LEDC. This is fully detailed in section 14 of the ESP32 Technical Reference Manual (“LED PWM Controller (LEDC)”)and has the following key features:

  • There are 8 high-speed channels and 8 low-speed channels.
  • There are 4 high-speed timers and 4 low-speed timers.
  • This makes a total of 16 PWM channels.
  • Any OUTPUT GPIO pin can be configured via the IO_MUX to connect to any of these 16 PWM channels.

This makes for a very flexible PWM scheme.

On my third party ESP32 WROOM development board, this means that all but four GPIO pins can be configured for PWM output. The four that can’t are INPUT only pins (34, 35, 36, 39).

My plan is to configure several PWM audio channels using the same high speed timer and map those onto GPIO pins to generate independent PWM audio outputs that can then be externally mixed.

The PWM peripherals can be linked to either or two clock sources. I’m planning to use the 80MHZ ABP_CLK (system application peripheral clock, which is derived from the system clock which can be up to 240MHz for the ESP32). It also supports the use of a clock divider, but I’m not planning on using it directly.

There is one key tradeoff when using the LEDC however – PWM resolution vs PWM frequency. As the resolution increase the available range of frequencies decreases. This is all fully explained in the reference manual and there are several equations provided to work out the appropriate values.

The PWM audio library provides an additional equation to calculate the PWM frequency from the desired resolution:

The two sets of equations don’t seem to fully tally though from what I can see… I suspect there is some rounding going on with the provided “commonly-used frequencies and resolutions” table in the reference manual.

Anyway, here are some sample resolutions and frequencies that I’m considering:

8 bits (0 .. 255)312 kHz
9 bits (0 .. 511)156 kHz
10 bits (0 .. 1023)78 kHz
11 bits (0 .. 2047)39 kHz
12 bits (0 .. 4095)19 kHz

Any of these should be fine.

Esspresif’s Arduino ESP32 Core

A note on versions of the Arduino ESP32 Core…

At the time of writing the last official release was dated October 2023 at version 2.0.14. However there appear to have been a number of API changes since then that are reflected in the latest documentation which puts it quite out of sync with the SDK itself.

Rather than attempt to find the documentation associated with the last official release, I switched over to the “development” strand of the core by changing the board manager URL I used.

At the time of writing I’ve installed the “3.0.0-alpha3” version… wish me luck!

ESP32 PWM Libraries for Audio

As mentioned, the PWM peripheral on the ESP32 is the LEDC. There appear to be several levels of library/API associated with producing audio signals via PWM on the LEDC on an ESP32:

As a general rule, the Arduino default PWM library isn’t really suitable for audio output, so PWM for audio on an Arduino tends to be done independently.

The Espressif PWM IoT Solution Audio API does provide a relatively straight forward way to configure and use the LEDC peripheral on an ESP32 for PWM audio. But it doesn’t look like it was designed for use with Arduino, but the ESP-IDF.

The Arduino ESP32 LEDC library includes additional functions for producing tones for simpler use-cases too.

What isn’t clear to me however is if it is possible to configure a single PWM peripheral with several audio output channels via these APIs so I might end up using the Espressif SDK LEDC API directly myself.

Checking PWM Frequencies

One of the example sketches provided with the ESP32 core will run through all possible frequency combinations and print out a table of valid frequencies. Find it under Examples -> ESP32 -> AnalogOut -> ledcFrequency

There will be loads of “invalid frequency” messages scrolling past the serial monitor (default baud is 115200) whilst it runs through all the values, but at the end there will be a table something like the following, which was the output for me:

Bit resolution | Min Frequency [Hz] | Max Frequency [Hz]
1 | 489 | 40078277
2 | 245 | 20039138
3 | 123 | 10019569
4 | 62 | 5009784
5 | 31 | 2504892
6 | 16 | 1252446
7 | 8 | 626223
8 | 4 | 313111
9 | 2 | 156555
10 | 1 | 78277
11 | 1 | 39138
12 | 1 | 19569
13 | 1 | 9784
14 | 1 | 4892
15 | 2 | 2446
16 | 1 | 1223
17 | 1 | 611
18 | 2 | 305
19 | 1 | 152
20 | 1 | 76

As we can see, this matches the aforementioned calculation pretty closely.

PWM on the ESP32 in Practice

I’m going to be using the LEDC peripheral for the PWM output and one of the general purpose timers to trigger a direct digital synthesis process to output the values from a wavetable.

PWM can be configured as follows:

ledcAttach(PWM_PIN, PWM_FREQUENCY, PWM_RESOLUTION);

Recall that any OUTPUT pin can be used for PWM output.

The timer to output samples can be configured as follows:

#define TIMER_FREQ 10000000
#define TIMER_RATE 305
timer = timerBegin(TIMER_FREQ);
timerAttachInterrupt(timer, &timerIsr);
timerAlarm(timer, TIMER_RATE, true, 0);

So what’s going on here? This is configuring a timer to run at 10,000,000 Hz, triggering an alarm every 305 “ticks” (so every 30.5uS), which in turn will trigger an interrupt running the timerIsr() routine.

Why 305 ticks? Well this is to give me a sample rate of 32768Hz which has a period of 1 / 32768 = 30.5uS, so I need to be outputting a sample every 30.5uS to keep up.

I’ve chosen that sample rate as I have a 256 byte wavetable so I wanted a sample rate that was a multiple of 256 to keep calculations easy.

As with previous DDS projects, I have an accumulator which provides the index into the wavetable of the sample to play, and an increment which moves through the table in a way to give me the required frequency of output.

I’m using 8.8 fixed point arithmetic again as that gives additional accuracy for the accumulator whilst making it very easy to scale to use as an index. Also, by using a unsigned 16-bit type for my 8.8 fixed point format I get automatic wrapping around of the accumulator too.

# This code runs at the sample rate and assumes a 256 byte wavetable
acc += inc;
sample = wavetable[acc>>8];

So how is the increment calculated from the required frequency? Again this is a calculation I’ve used many times now:

// For direct digital synthesis from a wavetable
// we have an accumulator to store the index into
// the table and an increment based on the sample
// rate and frequency.
// Increment = Freq * (Number of Samples in wavetable / Sample Rate)
// Increment = Freq * (256 / 32768)
// Increment = Freq / 128
//
// But using a 8.8 fixed-point accumulator and increment:
// Increment = 256 * Freq / 128
// Increment = Freq * 2
//
#define FREQ2INC(f) (f*2)
uint16_t acc;
uint16_t inc;
void setFreq (unsigned freq) {
inc = FREQ2INC(freq);
}

Once the sample has been calculated it can be written out to the PWM hardware using:

ledcWrite (PWM_PIN, sample);

Choosing the PWM resolution and frequency

As discussed, there is a tradeoff between resolution and PWM frequency. As I only have a 8-bit wavetable (i.e. the sinewave is defined with values between 0 and 255) then additional resolution is probably a little over the top.

There are some additional considerations however. The full resolution will be equivalent to the full 3V3 range of the output voltage, so one easy way to get (approximate) audio line level outputs is to configure 10-bit resolution, but then only use my 8-bit wave table. This means that 10-bits (4095) corresponds to 3V3, but my highest value of 255 is only a quarter of that, giving me less then 1V peak to peak. But the slower frequency gives a less cleaner output.

Sticking with an 8-bit (255) resolution gives me the full 3V3 range peak to peak and a much clearer output with the higher PWM frequency.

In the following, the first is the 10-bit output and the second is the 8-bit output.

I’ve gone with the 8-bit version, but if I want to end up with audio line-levels I will need a voltage divider to bring the voltage down from 3V3.

The Circuit

I’ve used the above filter circuit for the PWM output. The 1k/1k resistor divider drops the 3V3 peak to peak output down to half that. Then taking an equivalence value of 500Ω into a filter calculator (details here) with a 100nF value gives me a cutoff value of just over 3kHz. That could be considered a little low for audio frequencies, but is fine for my purposes right now.

The Code

The final code puts all this together with the following key functions:

  • setFreq – calculates and sets the increment for the required frequency.
  • ddsUpdate – calculates the next sample value and outputs it to the PWM hardware. It is called by the timer interrupt routine.

As previously mentioned, I’m using a 256 value, 8-bit sine table and set everything up for a 10MHz timer triggering to give me 32768Hz sample rate, with 8-bit resolution for the PWM, giving me a PWM frequency of just over 313kHz.

The sample code just uses a fixed test frequency 440Hz tone for now.

Find it on GitHub here.

Closing Thoughts

When I started looking at this, I wasn’t quite sure how all the apparent complex parts fitted together, but actually once you get into it, it is relatively straight forward to set up audio PWM on an ESP32.

The higher PWM frequencies in particular allow for a very smooth output. It isn’t as clean as the DAC of course, but it isn’t bad. It certainly looks like it would be fine for most of what I’d want to do with it.

I’d now like to see what might be involved in getting several PWM outputs simultaneously, possibly with different waveforms, to make a simple signal generator for audio frequencies and levels.

Kevin

Leave a comment