In my previous blog post link I was chasing the holy grail of toggling a pin with somewhat high frequency. I could achieve a 6.25 MHz which was great but the realtime aspect was missing.
The toggling can be affected by any heavy load on CPU and will not have the realtime aspect that I am chasing after. To fix this problem we need to look a bit on the peripherals provided to us by Raspberry Pi. One of them is PWM. A PWM is an extra HW inside the CPU that can work independently, therefor it is not affected by load on the CPU. PWM or Pulse Width Modulation peripheral can be used to generate a toggling signal on GPIO but the use case of PWM goes beyond just toggling a GPIO. For now I will only use it to toggle a GPIO with some desired duty cycle.
What is a Pulse Width Modulation #
Pulse Width Modulation (PWM) is a method used for controlling analog devices using a digital output. PWM works by rapidly switching the output voltage between high ON
and low OFF
states. The ratio of the ON
time to the total cycle time is known as the duty cycle, expressed as a percentage.
How to use Raspberry PI PWM peripheral #
The usage of PWM is bit tricky as somehow the description for configuring PWM clock registers is missing in the datasheet of the Raspberry PI CPU.
After some deep research I found some old 2013 documents that has this information. As setting up the CPU clock is important to configure the peripheral correctly. You can find the document here and elinux website. The important part to be noted is you need to configure a MASH noise shaping filter and that has put a limit on the maximum possible frequency of 25 MHz.


I will be using memory map method described in my previous blog to configure the PWM peripheral.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define RD(reg_base, offset) (*( (reg_base) + (offset) ))
#define WR(reg_base, offset, value) do { *((reg_base) + (offset)) = (value); } while(0)
// Raspberry Pi 4 [Check previous blog]
#define BCM_PERI_BASE 0xFE000000
#define GPIO_BASE_OFFSET 0x200000
#define PWM_BASE_OFFSET 0x20C000
#define CLK_BASE_OFFSET 0x101000
#define BLOCK_SIZE (4*1024)
#define GPFSEL1 1
#define PWM_CTL 0
#define PWM_STA 1
#define PWM_RNG1 4
#define PWM_DAT1 5
#define PWMCLK_CNTL 40
#define PWMCLK_DIV 41
#define GPIO_FSEL_ALT5 2
static volatile uint32_t *gpio_reg = NULL;
static volatile uint32_t *pwm_reg = NULL;
static volatile uint32_t *clk_reg = NULL;
volatile uint32_t *map_peripheral(off_t base) {
int mem_fd;
void *reg_map;
if ((mem_fd = open("/dev/mem", O_RDWR | O_SYNC)) < 0) {
perror("Error: can't open /dev/mem");
exit(EXIT_FAILURE);
}
reg_map = mmap(
NULL, // Let the system choose an address
BLOCK_SIZE, // Length of mapping
PROT_READ | PROT_WRITE, // Enable read/write
MAP_SHARED, // Allow shared mapping
mem_fd, // File descriptor for /dev/mem
base // Base address to map
);
close(mem_fd);
if (reg_map == MAP_FAILED) {
perror("Error: mmap failed");
exit(EXIT_FAILURE);
}
return (volatile uint32_t *)reg_map;
}
int main(void) {
gpio_reg = map_peripheral(BCM_PERI_BASE + GPIO_BASE_OFFSET);
pwm_reg = map_peripheral(BCM_PERI_BASE + PWM_BASE_OFFSET);
clk_reg = map_peripheral(BCM_PERI_BASE + CLK_BASE_OFFSET);
uint32_t reg_val = RD(gpio_reg, GPFSEL1);
reg_val &= ~(0b111 << 24);
reg_val |= (GPIO_FSEL_ALT5 << 24);
WR(gpio_reg, GPFSEL1, reg_val);
WR(pwm_reg, PWM_CTL, 0);
usleep(10);
WR(clk_reg, PWMCLK_CNTL, 0x5A000000 | (1 << 5));
usleep(110);
WR(clk_reg, PWMCLK_DIV, 0x5A000000 | (2 << 12));
WR(clk_reg, PWMCLK_CNTL, 0x5A000011);
usleep(110);
WR(pwm_reg, PWM_STA, 0x0F);
usleep(10);
const uint32_t range = 1024;
const uint32_t duty = 512;
WR(pwm_reg, PWM_RNG1, range);
usleep(10);
WR(pwm_reg, PWM_DAT1, duty);
usleep(10);
uint32_t pwm_ctl_val = (1 << 0) | (1 << 7);
WR(pwm_reg, PWM_CTL, pwm_ctl_val);
printf("PWM on GPIO18 is now enabled with Range=%u and Duty=%u (%.1f%% duty cycle).\n",
range, duty, (double)duty / range * 100);
printf("Press Ctrl+C to exit.\n");
return 0;
}
Lets deconstruct the program piece-by-piece.
If you have read my previous blog you will already know half of the code described here. But we go again in brief.
The first few macros are just helper routines to write and read registers
#define RD(reg_base, offset) (*( (reg_base) + (offset) ))
#define WR(reg_base, offset, value) do { *((reg_base) + (offset)) = (value); } while(0)
Then are all the registers needed to be configured
// Adjust the peripheral base address for your Raspberry Pi model:
// Pi 1 (BCM2835): 0x20000000
// Pi 2/3 (BCM2836/2837): 0x3F000000
// Pi 4 (BCM2711): 0xFE000000
#define BCM_PERI_BASE 0xFE000000
#define GPIO_BASE_OFFSET 0x200000
#define PWM_BASE_OFFSET 0x20C000
#define CLK_BASE_OFFSET 0x101000
#define BLOCK_SIZE (4*1024)
#define GPFSEL1 1
#define PWM_CTL 0
#define PWM_STA 1
#define PWM_RNG1 4
#define PWM_DAT1 5
#define PWMCLK_CNTL 40
#define PWMCLK_DIV 41
#define GPIO_FSEL_ALT5 2
BCM_PERI_BASE
is base address depending on your Raspberry Pi model.
GPIO_BASE_OFFSET
is base address of the GPIO
peripheral.
PWM_BASE_OFFSET
is base address of the PWM
peripheral.
PWMCLK_CNTL
and PWMCLK_DIV
is address offset for PWM clock register and divider register that are missing from datasheet as described in previous section.
PWM_CTL
,PWM_STA
,PWM_RNG1
,PWM_DAT1
are config descriptor for PWM
in RPi.

PWM_CTL
: PWM Control register
PWM_STA
: PWM Status register
PWM_RNG1
: PWM Channel 1 Range register
PWM_DAT1
: PWM Channel 1 Data register
As most of the registers are already described in the datasheet in adequate detail. I would only talk about the bits used in the code.
First we read the GPFSEL1 register and reset the FSEL18
bits and then set it to alternate function 5 which is PWM0
. I have described the process in my last Raspberry Pi blog.
uint32_t reg_val = RD(gpio_reg, GPFSEL1);
reg_val &= ~(0b111 << 24);
reg_val |= (GPIO_FSEL_ALT5 << 24);
WR(gpio_reg, GPFSEL1, reg_val);
Then we need to enable the PWM
peripheral and as shown in figure below PWM_CTL
can be used to disable or enable PWM channel 1 or 0.


But to enable the peripheral its needs to be disabled first and the following code snippet is doing the same.
WR(pwm_reg, PWM_CTL, 0);
usleep(10);
Then we need to reset the Clock
.
WR(clk_reg, PWMCLK_CNTL, 0x5A000000 | (1 << 5));
usleep(110);
Then configure it. THe divider value here is set to 2.
WR(clk_reg, PWMCLK_DIV, 0x5A000000 | (2 << 12));
usleep(110);
And then start the PWM clock with oscillator source (01) and without MASH
which is running at 25MHz.
WR(clk_reg, PWMCLK_CNTL, 0x5A000011);
usleep(110);
then we clear if any Status flags is set.
PWM Status reg:
WR(pwm_reg, PWM_STA, 0x0F);
usleep(10);

After setting the satus flag we configure PWM Channel 0:
- Set PWM period (range) to 1024 and initial duty cycle (data) 512
- Clear status flags in case any are set.
As you can see we set it PWM_DAT1
to half of PWM_RNG1
. This leads to 50% duty cycle on the PWM
.
const uint32_t range = 1024;
const uint32_t duty = 512;
WR(pwm_reg, PWM_RNG1, range);
usleep(10);
WR(pwm_reg, PWM_DAT1, duty);
usleep(10);
Please refer to Register description below
PWM Range reg:

PWM Data reg:

Finally we can Enable the PWM channel 0 in mark-space mode
// Bit 0: PWM channel enable.
// Bit 7: Mark-space mode enable.
uint32_t pwm_ctl_val = (1 << 0) | (1 << 7);
WR(pwm_reg, PWM_CTL, pwm_ctl_val);
to build and run the code just fire following script in the terminal of Raspberry Pi
gcc -o pwm pwm.c
sudo ./pwm
I tried with different range and duty cycle as shown bellow and got following result
- With range = 1024 and duty = 512

- With range = 1024 and duty = 256

- With range = 2 and duty = 1

As you can see in the last figure we could achieve a frequency of 13 MHz but the drawback is that we cannot get any other duty cycle than 50% as we are running on the edge of the frequency limit.
We have just scratched the surface and PWM peripheral is a beast and can do magical stuff. In my upcoming blog I might try to dig deeper on this topic.
DISCLAIMER: This kernel modules are provided for educational and demonstration purposes only.
USE AT YOUR OWN RISK.
The authors code disclaim any and all liability for any direct, indirect, incidental, special, or consequential damages arising out of or in connection with the use, modification, or distribution of this software. This module is provided “AS IS” without any express or implied warranties, including, but not limited to, the implied warranties of merchantability, fitness for a particular purpose, or non-infringement. By using this kernel module[s], you agree that you are solely responsible for ensuring that its use complies with all applicable laws, regulations, and licensing requirements. It is your responsibility to thoroughly test and verify the module in your environment prior to any production use. The author assumes no responsibility for any damage to your systems, data loss, or other adverse consequences that may result from the use or misuse of this module. If you do not agree with the terms of this disclaimer, do not use this kernel module provided above in any form.