Skip to main content

PWM

1. Introduction to PWM

PWM, or Pulse Width Modulation, is a technique used to generate analog signal output by controlling the pulse width of a digital signal. It is commonly employed in embedded systems and electronic devices for applications such as motor speed control, LED brightness adjustment, and audio signal generation. In Linux systems, PWM devices are typically managed and configured via the sysfs filesystem, with the device directories usually located under /sys/class/pwm/.

2. Controlling PWM (Shell)

2.1 Pin Distribution

Luckfox Omni3576 Pin Diagram:

2.2 Device Directory and Attributes

  1. The pwmchip0 (in this case, 2ade2000) is used for the screen backlight and is enabled by default:

    root@luckfox:/# ls -l /sys/class/pwm
    total 0
    lrwxrwxrwx 1 root root 0 Aug 25 23:07 pwmchip0 -> ../../devices/platform/2ade0000.pwm/pwm/pwmchip0
  2. When multiple PWM device tree overlays are enabled, the system assigns smaller pwmchip numbers to PWM controllers with smaller values. For instance, the default screen backlight may switch to pwmchip1.

    root@luckfox:/# ls -l /sys/class/pwm
    total 0
    lrwxrwxrwx 1 root root 0 Aug 26 00:59 pwmchip0 -> ../../devices/platform/2ade0000.pwm/pwm/pwmchip0
    lrwxrwxrwx 1 root root 0 Aug 26 00:59 pwmchip1 -> ../../devices/platform/2ade2000.pwm/pwm/pwmchip1
    NameDevice Tree NameAddress
    pwm2m0_ch0pwm2_8ch_02ade0000
    pwm2m0_ch2pwm2_8ch_22ade2000
  3. The /sys/class/pwm/pwmchipN/pwm0/ directory contains multiple attributes that can be configured to control the PWM.

    root@luckfox:/# ls /sys/class/pwm/pwmchip0/pwm0/
    capture duty_cycle enable period polarity power uevent
    • enable: Enables or disables the PWM channel
    • period: Sets the PWM signal period
    • duty_cycle: Sets the PWM signal duty cycle
    • polarity: Configures PWM signal polarity
    • power/control: Manages power settings for power-saving modes

2.3 Controlling PWM

  1. Export or Unexport PWM to/from User Space (e.g., GPIO0_D3_d).

    echo 0 > /sys/class/pwm/pwmchip0/export
    echo 0 > /sys/class/pwm/pwmchip0/unexport
  2. Set the PWM period unit to ns. For example, the period of a 1KHz frequency is 1000000ns (note that in any case, the value of period must be greater than or equal to the value of duty_cycle).

    echo 1000000 > /sys/class/pwm/pwmchip10/pwm0/period
  3. Set PWM polarity normal or inverted.

    echo "normal" > /sys/class/pwm/pwmchip0/pwm0/polarity
    echo "inversed" > /sys/class/pwm/pwmchip0/pwm0/polarity
  4. Enable and disable PWM.

    echo 1 > /sys/class/pwm/pwmchip1/pwm0/enable
    echo 0 > /sys/class/pwm/pwmchip1/pwm0/enable
  5. Set the duty cycle (less than or equal to period )

    echo 100000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 200000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 300000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 400000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 500000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 600000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 700000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 800000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 900000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
    echo 0 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle

3. Controlling PWM Using Python (python-periphery)

  1. The complete example code for PWM output using the python-periphery library is as follows:

    #!/usr/bin/python3

    from periphery import PWM
    import time
    import numpy as np

    pwm = PWM(0, 0)
    pwm.frequency = 100
    pwm.polarity = "normal"
    pwm.duty_cycle = 0
    pwm.enable()

    try:
    while True:
    for i in np.linspace(0, 2 * np.pi, 100):
    duty_cycle = (np.sin(i) + 1) / 2
    pwm.duty_cycle = duty_cycle
    time.sleep(0.02)
    except KeyboardInterrupt:
    pwm.disable()
    pwm.close()
  2. Program analysis.

    pwm = PWM(0, 0)         # Enable PWM chip 0, channel 0
    pwm.frequency = 100 # Set 100Hz frequency
    pwm.polarity = "normal" # Set PWM polarity
    pwm.duty_cycle = 0 # Initial duty cycle
    pwm.enable() # Enable PWM
    • Create a duty cycle loop that gradually increases the duty cycle to a maximum value and then gradually decreases it to a minimum value. This loop can use the np.sin() function to simulate a similar curve to produce smooth brightness changes.。
      while True:
    # Generate an array that changes smoothly from 0 to 2π. This range is used to control the input of the sin() function.
    for i in np.linspace(0, 2 * np.pi, 100):
    # (duty_cycle) changes smoothly between 0 and 1
    duty_cycle = (np.sin(i) + 1) / 2
    pwm.duty_cycle = duty_cycle
    time.sleep(0.02)
    • The output value of y = sinx will vary between -1 and 1 in the interval between 0 and 2Π. Since we want the duty cycle to vary smoothly between 0 and 1, the output range of (np.sin(i) + 1) / 2 is 0 to 1 (by dividing by 2, the result is limited to between 0 and 1).
  1. Run the Script:

    chmod +x pwm.py
    ./pwm.py
  2. Observe the output on Pin 12.

4. Control PWM (C program)

In addition to controlling the PWM pins through shell scripts and Python libraries, you can also use C language to operate sysfs to achieve the same function.

  1. Export to User Space.

    int pwm_export(int channel) {
    FILE *pwm_export = fopen(PWM_PATH "/export", "w");
    if (!pwm_export) {
    perror("Failed to open PWM export");
    return 1;
    }
    fprintf(pwm_export, "%d", channel);
    fclose(pwm_export);
    return 0;
    }
  2. Unexport userspace.

    int pwm_unexport(int channel) {
    FILE *pwm_unexport = fopen(PWM_PATH "/unexport", "w");
    if (!pwm_unexport) {
    perror("Failed to open PWM unexport");
    return 1;
    }
    fprintf(pwm_unexport, "%d", channel);
    fclose(pwm_unexport);
    return 0;
    }
  3. Set the PWM period.

    int pwm_set_period(int channel, int period_ns) {
    char period_path[128];
    snprintf(period_path, sizeof(period_path), PWM_PATH "/pwm%d/period", channel);

    FILE *period_file = fopen(period_path, "w");
    if (!period_file) {
    perror("Failed to open PWM period");
    return 1;
    }
    fprintf(period_file, "%d", period_ns);
    fclose(period_file);
    return 0;
    }
  4. Set the duty cycle.

    int pwm_set_duty_cycle(int channel, int duty_cycle_ns) {
    char duty_cycle_path[128];
    snprintf(duty_cycle_path, sizeof(duty_cycle_path), PWM_PATH "/pwm%d/duty_cycle", channel);

    FILE *duty_cycle_file = fopen(duty_cycle_path, "w");
    if (!duty_cycle_file) {
    perror("Failed to open PWM duty cycle");
    return 1;
    }
    fprintf(duty_cycle_file, "%d", duty_cycle_ns);
    fclose(duty_cycle_file);
    return 0;
    }
  5. Enable PWM.

    int pwm_enable(int channel, int enable) {
    char enable_path[128];
    snprintf(enable_path, sizeof(enable_path), PWM_PATH "/pwm%d/enable", channel);

    FILE *enable_file = fopen(enable_path, "w");
    if (!enable_file) {
    perror("Failed to open PWM enable");
    return 1;
    }
    fprintf(enable_file, "%d", enable);
    fclose(enable_file);
    return 0;
    }
  6. Complete example program.

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <math.h>

    #define PWM_PATH "/sys/class/pwm/pwmchip10"
    #define PERIOD_NS 1000000
    #define MAX_DUTY_CYCLE_NS 1000000
    #define MIN_DUTY_CYCLE_NS 0

    #define PI 3.141592653589793


    int pwm_export(int channel) {
    FILE *pwm_export = fopen(PWM_PATH "/export", "w");
    if (!pwm_export) {
    perror("Failed to open PWM export");
    return 1;
    }
    fprintf(pwm_export, "%d", channel);
    fclose(pwm_export);
    return 0;
    }


    int pwm_unexport(int channel) {
    FILE *pwm_unexport = fopen(PWM_PATH "/unexport", "w");
    if (!pwm_unexport) {
    perror("Failed to open PWM unexport");
    return 1;
    }
    fprintf(pwm_unexport, "%d", channel);
    fclose(pwm_unexport);
    return 0;
    }


    int pwm_set_period(int channel, int period_ns) {
    char period_path[128];
    snprintf(period_path, sizeof(period_path), PWM_PATH "/pwm%d/period", channel);

    FILE *period_file = fopen(period_path, "w");
    if (!period_file) {
    perror("Failed to open PWM period");
    return 1;
    }
    fprintf(period_file, "%d", period_ns);
    fclose(period_file);
    return 0;
    }


    int pwm_set_duty_cycle(int channel, int duty_cycle_ns) {
    char duty_cycle_path[128];
    snprintf(duty_cycle_path, sizeof(duty_cycle_path), PWM_PATH "/pwm%d/duty_cycle", channel);

    FILE *duty_cycle_file = fopen(duty_cycle_path, "w");
    if (!duty_cycle_file) {
    perror("Failed to open PWM duty cycle");
    return 1;
    }
    fprintf(duty_cycle_file, "%d", duty_cycle_ns);
    fclose(duty_cycle_file);
    return 0;
    }


    int pwm_enable(int channel, int enable) {
    char enable_path[128];
    snprintf(enable_path, sizeof(enable_path), PWM_PATH "/pwm%d/enable", channel);

    FILE *enable_file = fopen(enable_path, "w");
    if (!enable_file) {
    perror("Failed to open PWM enable");
    return 1;
    }
    fprintf(enable_file, "%d", enable);
    fclose(enable_file);
    return 0;
    }

    int main() {
    int channel = 0;


    if (pwm_export(channel)) {
    return 1;
    }


    if (pwm_set_period(channel, PERIOD_NS)) {
    return 1;
    }


    if (pwm_enable(channel, 1)) {
    return 1;
    }


    double step_size = (2 * PI) / 100.0;
    while (1) {
    for (double i = 0; i <= 2 * PI; i += step_size) {

    double duty_cycle_ratio = (sin(i) + 1) / 2;
    int duty_cycle_ns = (int)(duty_cycle_ratio * MAX_DUTY_CYCLE_NS);


    if (pwm_set_duty_cycle(channel, duty_cycle_ns)) {
    return 1;
    }

    usleep(20000);
    }
    }


    if (pwm_enable(channel, 0)) {
    return 1;
    }


    if (pwm_unexport(channel)) {
    return 1;
    }

    return 0;
    }
  7. Compile and run the program.

    gcc pwm.c -o pwm -lm

5. Device Tree Overview

  1. The PWM DTS source file is defined in kernel-6.10/arch/arm64/boot/dts/rockchip/rk3576.dtsi and can be directly utilized.

    pwm0_2ch_0: pwm@27330000 {
    compatible = "rockchip,rk3576-pwm";
    reg = <0x0 0x27330000 0x0 0x1000>;
    interrupts = <GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
    #pwm-cells = <3>;
    pinctrl-names = "active";
    pinctrl-0 = <&pwm0m0_ch0>;
    clocks = <&cru CLK_PMU1PWM>, <&cru PCLK_PMU1PWM>;
    clock-names = "pwm", "pclk";
    status = "disabled";
    };
  2. Example of enabling pwm2m0_ch0 in the device tree (luckfox-omni3576.dts): (Code snippet follows as required).

    &pwm2_8ch_0 {
    status = "okay";
    pinctrl-0 = <&pwm2m0_ch0>;
    };