ESP32-S3 Baremetal Support

esp32s3_pwm.c

PWM on the LEDC peripheral - a square wave with an adjustable duty cycle.

Eight channels, each on almost any pin. Once started a channel runs in hardware and needs no attention: no interrupt, no timer, nothing for the program to keep doing. Dim an LED, drive a servo, set a motor's speed, make a rough analogue voltage out of a resistor and a capacitor.

pwm_init(0, 6, 1000, 10);      // channel 0, GPIO6, 1 kHz, 10-bit steps
pwm_set_duty(0, 512);          // half on, half off

Frequency and resolution are a trade against each other, because both come out of the same 40 MHz count: the channel divides the clock down to get its period, and the leftover precision is what the duty steps are made of. The product has to fit, so 40000000 / frequency is the ceiling on the number of steps. At 1 kHz that allows about 15 bits, plenty; at 100 kHz it is nearer 8; at 1 MHz there is almost nothing left. pwm_init() reports which way it went rather than quietly producing the wrong frequency.

The eight channels share four timers, and a timer is what actually holds a frequency. Channels asking for the same frequency and resolution are put on the same timer automatically, so eight LEDs dimming at 1 kHz cost one timer between them. Four *different* combinations is the hard limit.

The count comes from the 40 MHz crystal, so the frequency does not shift when the CPU clock does.

API

Declared in esp32s3_pwm.h

Functions

int pwm_init(uint32_t channel, uint32_t pin, uint32_t freq_hz, uint32_t resolution_bits);

Starts a channel on a pin, at rest (duty 0) until pwm_set_duty() is called. The resolution is how many steps the duty cycle has, in bits: 8 gives 256 steps, 10 gives 1024. More steps mean finer control and a lower ceiling on the frequency.

channel
0 to 7
pin
the GPIO to drive
freq_hz
cycles per second, e.g. 1000, or 50 for a servo
resolution_bits
1 to 14 not fit together or all four timers are already spoken for
returns
1 if the channel started, 0 if that frequency and resolution do
void pwm_set_duty(uint32_t channel, uint32_t duty);

Sets how much of each cycle the pin spends high. Takes effect at the end of the cycle in progress, so the output never shows a runt pulse.

channel
0 to 7
duty
0 for always low, pwm_max_duty() for always high
returns
nothing
uint32_t pwm_max_duty(uint32_t channel);

The duty value that means always on, i.e. 2 to the resolution.

channel
0 to 7 started
returns
the top of the duty range, or 0 for a channel that was never
void pwm_stop(uint32_t channel, int idle_level);

Stops a channel and parks its pin at a fixed level. Use this rather than a duty of 0 when the pin must be reliably still - a duty of 0 is a wave whose high phase happens to have no width, which is nearly but not quite the same thing. Call pwm_init() again to start the channel back up; pwm_set_duty() on a stopped channel changes the duty but does not restart the output.

channel
0 to 7
idle_level
0 to park low, 1 to park high
returns
nothing

Source

board/esp32s3_pwm.c
board/esp32s3_pwm.c
177 lines
/*
 * PWM on LEDC: four timers hold the frequencies, eight channels hold the
 * duty cycles, and channels that want the same frequency share a timer.
 */

#include "esp32s3_pwm.h"
#include "esp32s3_clock.h"
#include "esp32s3_gpio.h"
#include "esp32s3_regs.h"

// The count LEDC runs on, selected in start_peripheral(). The crystal rather
// than anything derived from the CPU clock, so a clock switch elsewhere in
// the program cannot change a servo's pulse width.
#define PWM_SRC_HZ      (ESP32S3_XTAL_MHZ * 1000000u)

// The divider is a fixed-point number with 8 fractional bits, in an 18-bit
// field: so between 1.0 and just under 1024.0.
#define PWM_DIV_MIN     (1u << 8)
#define PWM_DIV_MAX     ((1u << 18) - 1u)

#define PWM_MAX_RES_BITS 14

#define LEDC_REG(off)   ESP32S3_REG(LEDC_BASE + (off))

static struct {
    uint32_t freq_hz;
    uint32_t resolution;
    int      in_use;
} pwm_timer[LEDC_TIMERS];

static uint8_t pwm_channel_res[LEDC_CHANNELS];    // 0 until the channel starts

static void start_peripheral(void)
{
    static int done;

    if (done) {
        return;
    }
    done = 1;

    periph_enable(SYSTEM_PERIP_CLK_EN0_REG, SYSTEM_PERIP_RST_EN0_REG, SYSTEM_LEDC_CLK_EN);
    LEDC_REG(LEDC_CONF_OFF) = LEDC_GLOBAL_CLK_EN | LEDC_APB_CLK_SEL_XTAL;
}

// How far the source clock has to be divided so that one period is exactly
// 2^resolution counts long. Returns 0 if the pair does not fit the hardware,
// which is the frequency-versus-resolution trade running out.
static uint32_t divider_for(uint32_t freq_hz, uint32_t resolution)
{
    // The << 8 is the divider's fractional part, so this stays exact rather
    // than rounding the frequency to a whole number of ticks.
    uint64_t div = ((uint64_t)PWM_SRC_HZ << 8) / ((uint64_t)freq_hz << resolution);

    if (div < PWM_DIV_MIN || div > PWM_DIV_MAX) {
        return 0;
    }
    return (uint32_t)div;
}

// Finds a timer already producing this frequency and resolution, or sets a
// free one up to do it. Returns -1 when all four are busy with something else.
static int claim_timer(uint32_t freq_hz, uint32_t resolution)
{
    for (int t = 0; t < LEDC_TIMERS; t++) {
        if (pwm_timer[t].in_use
            && pwm_timer[t].freq_hz == freq_hz
            && pwm_timer[t].resolution == resolution) {
            return t;
        }
    }

    uint32_t div = divider_for(freq_hz, resolution);
    if (div == 0) {
        return -1;
    }

    for (int t = 0; t < LEDC_TIMERS; t++) {
        if (pwm_timer[t].in_use) {
            continue;
        }

        LEDC_REG(LEDC_TIMER_CONF_OFF(t)) =
            (resolution << LEDC_DUTY_RES_S) | (div << LEDC_CLK_DIV_S);
        LEDC_REG(LEDC_TIMER_CONF_OFF(t)) |= LEDC_TIMER_PARA_UP;

        // Start the counter from zero rather than from wherever it happened
        // to be, so the first cycle is a whole one.
        LEDC_REG(LEDC_TIMER_CONF_OFF(t)) |= LEDC_TIMER_RST;
        LEDC_REG(LEDC_TIMER_CONF_OFF(t)) &= ~LEDC_TIMER_RST;

        pwm_timer[t].freq_hz    = freq_hz;
        pwm_timer[t].resolution = resolution;
        pwm_timer[t].in_use     = 1;
        return t;
    }

    return -1;
}

int pwm_init(uint32_t channel, uint32_t pin, uint32_t freq_hz, uint32_t resolution_bits)
{
    if (channel >= LEDC_CHANNELS || freq_hz == 0
        || resolution_bits == 0 || resolution_bits > PWM_MAX_RES_BITS) {
        return 0;
    }

    start_peripheral();

    int timer = claim_timer(freq_hz, resolution_bits);
    if (timer < 0) {
        return 0;
    }

    // hpoint is the count at which the pin goes high; leaving it at zero puts
    // every channel's rising edge at the start of the period.
    LEDC_REG(LEDC_CH_HPOINT_OFF(channel)) = 0;
    LEDC_REG(LEDC_CH_DUTY_OFF(channel))   = 0;

    LEDC_REG(LEDC_CH_CONF0_OFF(channel)) =
        ((uint32_t)timer << LEDC_TIMER_SEL_S) | LEDC_SIG_OUT_EN;

    pwm_channel_res[channel] = (uint8_t)resolution_bits;

    // conf1 drives the fade generator. A scale of zero means no fade: the
    // duty simply becomes whatever was written and stays there.
    LEDC_REG(LEDC_CH_CONF1_OFF(channel)) =
        LEDC_DUTY_START | LEDC_DUTY_INC | (1u << LEDC_DUTY_NUM_S) | (1u << LEDC_DUTY_CYCLE_S);

    LEDC_REG(LEDC_CH_CONF0_OFF(channel)) |= LEDC_CH_PARA_UP;

    gpio_route_out(pin, LEDC_SIG(channel));
    return 1;
}

void pwm_set_duty(uint32_t channel, uint32_t duty)
{
    if (channel >= LEDC_CHANNELS || pwm_channel_res[channel] == 0) {
        return;
    }

    uint32_t max = 1u << pwm_channel_res[channel];
    if (duty > max) {
        duty = max;
    }

    // The duty register carries four fractional bits below the whole steps,
    // for a dithering mode this driver does not use.
    LEDC_REG(LEDC_CH_DUTY_OFF(channel)) = duty << LEDC_DUTY_FRAC_BITS;

    LEDC_REG(LEDC_CH_CONF1_OFF(channel)) |= LEDC_DUTY_START;
    LEDC_REG(LEDC_CH_CONF0_OFF(channel)) |= LEDC_CH_PARA_UP;
}

uint32_t pwm_max_duty(uint32_t channel)
{
    if (channel >= LEDC_CHANNELS || pwm_channel_res[channel] == 0) {
        return 0;
    }
    return 1u << pwm_channel_res[channel];
}

void pwm_stop(uint32_t channel, int idle_level)
{
    if (channel >= LEDC_CHANNELS) {
        return;
    }

    uint32_t conf0 = LEDC_REG(LEDC_CH_CONF0_OFF(channel));
    conf0 &= ~(LEDC_SIG_OUT_EN | LEDC_IDLE_LV);
    if (idle_level) {
        conf0 |= LEDC_IDLE_LV;
    }

    LEDC_REG(LEDC_CH_CONF0_OFF(channel)) = conf0;
    LEDC_REG(LEDC_CH_CONF0_OFF(channel)) = conf0 | LEDC_CH_PARA_UP;
}
include/esp32s3_pwm.hโ€” the header, in full
include/esp32s3_pwm.h
71 lines
/*
 * PWM on the LEDC peripheral - a square wave with an adjustable duty cycle.
 *
 * Eight channels, each on almost any pin. Once started a channel runs in
 * hardware and needs no attention: no interrupt, no timer, nothing for the
 * program to keep doing. Dim an LED, drive a servo, set a motor's speed, make
 * a rough analogue voltage out of a resistor and a capacitor.
 *
 *     pwm_init(0, 6, 1000, 10);      // channel 0, GPIO6, 1 kHz, 10-bit steps
 *     pwm_set_duty(0, 512);          // half on, half off
 *
 * Frequency and resolution are a trade against each other, because both come
 * out of the same 40 MHz count: the channel divides the clock down to get its
 * period, and the leftover precision is what the duty steps are made of. The
 * product has to fit, so 40000000 / frequency is the ceiling on the number of
 * steps. At 1 kHz that allows about 15 bits, plenty; at 100 kHz it is nearer
 * 8; at 1 MHz there is almost nothing left. pwm_init() reports which way it
 * went rather than quietly producing the wrong frequency.
 *
 * The eight channels share four timers, and a timer is what actually holds a
 * frequency. Channels asking for the same frequency and resolution are put on
 * the same timer automatically, so eight LEDs dimming at 1 kHz cost one timer
 * between them. Four *different* combinations is the hard limit.
 *
 * The count comes from the 40 MHz crystal, so the frequency does not shift
 * when the CPU clock does.
 */

#ifndef ESP32S3_PWM_H
#define ESP32S3_PWM_H

#include <stdint.h>

// Starts a channel on a pin, at rest (duty 0) until pwm_set_duty() is called.
//
// The resolution is how many steps the duty cycle has, in bits: 8 gives 256
// steps, 10 gives 1024. More steps mean finer control and a lower ceiling on
// the frequency.
//
// - channel: 0 to 7
// - pin: the GPIO to drive
// - freq_hz: cycles per second, e.g. 1000, or 50 for a servo
// - resolution_bits: 1 to 14
// returns: 1 if the channel started, 0 if that frequency and resolution do
//          not fit together or all four timers are already spoken for
int pwm_init(uint32_t channel, uint32_t pin, uint32_t freq_hz, uint32_t resolution_bits);

// Sets how much of each cycle the pin spends high. Takes effect at the end of
// the cycle in progress, so the output never shows a runt pulse.
// - channel: 0 to 7
// - duty: 0 for always low, pwm_max_duty() for always high
// returns: nothing
void pwm_set_duty(uint32_t channel, uint32_t duty);

// The duty value that means always on, i.e. 2 to the resolution.
// - channel: 0 to 7
// returns: the top of the duty range, or 0 for a channel that was never
//          started
uint32_t pwm_max_duty(uint32_t channel);

// Stops a channel and parks its pin at a fixed level. Use this rather than a
// duty of 0 when the pin must be reliably still - a duty of 0 is a wave whose
// high phase happens to have no width, which is nearly but not quite the same
// thing. Call pwm_init() again to start the channel back up; pwm_set_duty()
// on a stopped channel changes the duty but does not restart the output.
// - channel: 0 to 7
// - idle_level: 0 to park low, 1 to park high
// returns: nothing
void pwm_stop(uint32_t channel, int idle_level);

#endif // ESP32S3_PWM_H