Raspberry Pi CM5 IoT In C - - PWM Using GPIO5
Written by Harry Fairhead   
Monday, 12 May 2025
Article Index
Raspberry Pi CM5 IoT In C - - PWM Using GPIO5
Pulse Density Mode
The PWM Registers
Working with PWM
What Else Can You Use PWM For?

Working with PWM

Extending Gpio5 to work with PWM is fairly easy but there is no point in trying to follow the Pico SDK as its PWM hardware is very different from the CM5.

Now we have the functions to set the clock running we can set the GPIO lines we want to use into PWM mode:

int pwm_setup(uint32_t gpio, enum pwm_mode_rp1 mode)
{
    int pwm = getPWM(gpio);
    if (pwm < 0)
        return -1;
    pwm_init_clock();
    if (gpio == 18 | gpio == 19){
        gpio_set_function(gpio, GPIO_FUNC_PWM2);
    }
    else{
        gpio_set_function(gpio, GPIO_FUNC_PWM1);
    }
    PWM[pwm].Ctrl = mode;
    pwm_disable(gpio);
}
enum pwm_mode_rp1
{
    Zero = 0x0,
    TrailingEdge = 0x1,
    PhaseCorrect = 0x2,
    PDE = 0x3,
    MSBSerial = 0x4,
    PPM = 0x5,
    LeadingEdge = 0x6,
    LSBSerial = 0x7
};

This allows you to setup a GPIO line to use PWM, but of course not all GPIO lines support PWM and the ones that do use particular pwm channels. The getPWM function returns the channel used or -1 if the GPIO line isn’t PWM compatible:

int getPWM(uint32_t gpio)
{
    switch (gpio)
    {
    case 12:
        return 0;
    case 13:
        return 1;
    case 14:
        return 2;
    case 15:
        return 3;
    case 18:
        return 2;
    case 19:
        return 3;
    }
    return -1;
}

Notice that this returns the correct PWM channels for GPIO18 and GPIO19 and the setup function uses the correct GPIO mode for this selection.

The PWM line is set to a disabled state so that it can have its range, duty and phase set before being enabled:

int pwm_enable(uint32_t gpio)
{
    int pwm = getPWM(gpio);
    if (pwm < 0)
        return -1;
    uint32_t temp = 1 << pwm;
    *PWMBase = *PWMBase | temp | 0x80000000;
}
int pwm_disable(uint32_t gpio)
{
    int pwm = getPWM(gpio);
    if (pwm < 0)
        return -1;
    uint32_t temp = 1 << pwm;
    temp = ~temp & 0xf;
    *PWMBase = (*PWMBase & temp) | 0x80000000;
}

Notice that no check is made to see if the PWM line has been configured. A check is made that the GPIO line is a valid PWM line. After this the GLOBAL_CTRL register is used to enable the PWM channel and set the top most bit to commit the change. Notice that this update is done so as not to change any other settings in the register.

Now all we need is a function that sets the range, duty and phase:

int pwm_set_range_duty_phase(int32_t gpio, 
uint32_t range,uint32_t duty, uint32_t phase) { int pwm = getPWM(gpio); if (pwm < 0) return -1; PWM[pwm].Range = range; PWM[pwm].Duty = duty; PWM[pwm].Phase = phase; *PWMBase = *PWMBase | 0x80000000; }

Notice that the high bit of the GLOBAL_CTRL register is set to commit the changes. Only the phase register actually needs this as the DUTY and RANGE registers update automatically at the end of the PWM cycle.

The set_range_duty_phase function works, but in many cases we would like to set a frequency and a duty cycle as a percentage:

int pwm_set_frequency_duty(int32_t gpio, int32_t freq,
                                      int dutyPercent)
{
    int32_t div = PWMCLK->PWM0_DIV_INT;
    int32_t frac = PWMCLK->PWM0_DIV_FRAC;
    int32_t pwmf = PWMClock / div;
    int32_t range = pwmf / freq-1;
    int32_t duty = range * dutyPercent / 1000+1;
    pwm_set_range_duty_phase(gpio, range, duty, 0);
}

The duty cycle is specified as a percentage*10 to allow it to specify a decimal part. For example setting duty to 25 sets the duty cycle to 2.5%

Notice that this function assumes that you have set the correct clock rate to give a good duty cycle resolution.

In chapter but not in extract

  • Controlling an LED
  • How Fast Can You Modulate?
  • Controlling a Servo


Last Updated ( Tuesday, 13 May 2025 )