ESP32-S3 Baremetal Support

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 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

esp32s3/src/pwm.c
esp32s3/src/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 "pwm.h"
#include "clock.h"
#include "gpio.h"
#include "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;
}
esp32s3/include/pwm.hโ€” the header, in full
esp32s3/include/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 PWM_H
#define 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