Skip to main content

PWM

In this chapter, we will learn how to use PWM at the application level.

Sample program : Code.zip

1. PWM

PWM, which stands for Pulse Width Modulation, is a technique used to generate analog-like signals by controlling the pulse width of a digital signal. It is commonly used in embedded systems and electronic devices for applications such as motor speed control, LED brightness control, audio signal generation, and more.

In the Linux operating system, PWM devices are typically managed and configured using the sysfs file system, and their device directories are usually located under the /sys/class/pwm/ directory.

2. PWM Control (Shell)

2.1 Pin Distribution

Due to pin multiplexing in LuckFox Pico, LuckFox Pico Mini A/B and LuckFox Pico Plus there can be pin function conflicts. Therefore, the default PWM pins for each board model are as follows:

  • LuckFox Pico:
    • PWM0_0 - Pin 34
    • PWM1_0 - Pin 4
    • PWM10_1 - Pin 54
    • PWM11_1 - Pin 55
  • LuckFox Pico Mini A/B:
    • PWM10_1 - Pin 54
    • PWM11_1 - Pin 55
  • LuckFox Pico Plus:
    • PWM0_0 - Pin 34
    • PWM1_0 - Pin 4
    • PWM10_1 - Pin 54
    • PWM11_1 - Pin 55
  1. LuckFox Pico Diagram:

  2. LuckFox Pico Mini A/B Diagram:

  3. LuckFox Pico Plus Diagram:

2.2 Enable PWM

Use luckfox-config to enable the relevant configuration:

  • Use the and keys on the keyboard to navigate through menu items, Enter to select, , , or Tab to choose between the OK and cancel buttons, Esc to cancel and go back, and the spacebar to select options. Any changes will take effect after a reboot. Note: When logging in via ADB, arrow keys and Tab cannot be used; only numeric keys can be used to select options, and Enter to confirm.
  1. Open the luckfox-config tool in the terminal:

    luckfox-config
  2. Select Advanced Options:
    img

  3. Select PWM:
    img

  4. Select the PWM interface you want to enable, here we use PWM7_M1 as an example:
    img

  5. Choose enable, and after selecting, press Enter to confirm, then press Esc to exit:
    img

  6. Reboot the development board.

    reboot

2.3 Device Directory

Under the /sys/class/pwm/ directory, each PWM device has its own separate subdirectory, usually named in the form pwmchipX, where X is the PWM device number. You can check this using the following command:

# ls -l /sys/class/pwm
lrwxrwxrwx 1 root root 0 pwmchip10 -> ../../devices/platform/ff490020.pwm/pwm/pwmchip10
lrwxrwxrwx 1 root root 0 pwmchip1 -> ../../devices/platform/ff350010.pwm/pwm/pwmchip1
lrwxrwxrwx 1 root root 0 pwmchip11 -> ../../devices/platform/ff490030.pwm/pwm/pwmchip11
lrwxrwxrwx 1 root root 0 pwmchip0 -> ../../devices/platform/ff350000.pwm/pwm/pwmchip0

2.4 Device Properties

After successfully exporting a channel of PWM, a subdirectory corresponding to that channel is created within the PWM's device directory. This subdirectory contains property files specific to that channel. Users can configure and control various parameters of a specific PWM channel by modifying the contents of these files, enabling precise control over the characteristics of the PWM signal. This provides a convenient interface for users to interact with the PWM hardware and adjust the PWM signal's properties.

  1. View the property files for a single channel, such as PWM10:

    # ls /sys/class/pwm/pwmchip10/pwm0/
    uevent capture duty_cycle output_type
    polarity enable power period
  2. Property Files

    By using these property files, you can easily configure and control the PWM channel to suit different application scenarios and requirements. Some key properties include:

    • enable: Used to enable or disable the PWM channel.
    • period: Sets the period time of the PWM signal.
    • duty_cycle: Sets the duty cycle of the PWM signal.
    • polarity: Configures the polarity of the PWM signal.
    • power/control: Enables or disables power management for the PWM channel (typically used for power-saving modes).

2.5 Controlling PWM

  1. Test PWM. Export PWM10_M1 (GPIO1_C6_d) to user space.

    echo 0 > /sys/class/pwm/pwmchip10/export
  2. Set the PWM period in nanoseconds. For example, for a 1KHz frequency, the period is 1000000ns (Note: ensure that the period value is greater than or equal to the duty_cycle value).

    echo 1000000 > /sys/class/pwm/pwmchip10/pwm0/period
  3. Set the duty cycle (must be less than or equal to the period).

    echo 100000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 200000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 300000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 400000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 500000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 600000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 700000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 800000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 900000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 1000000 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
    echo 0 > /sys/class/pwm/pwmchip10/pwm0/duty_cycle
  4. Set PWM polarity to normal or inverted.

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

    echo 1 > /sys/class/pwm/pwmchip1/pwm0/enable
    echo 0 > /sys/class/pwm/pwmchip1/pwm0/enable
  6. Unexport PWM from user space.

    echo 0 > /sys/class/pwm/pwmchip10/unexport

3. PWM Control (Python Program)

In the previous section, we demonstrated how to control devices by using the echo command to modify device files. Next, we will implement PWM control using a Python program.

3.1 Sample Program

With the following program, PWM10 output can be controlled.

from periphery import PWM
import time

pwm = PWM(10, 0)

try:
pwm.frequency = 1000
pwm.duty_cycle = 0
pwm.polarity = "normal"
pwm.enable()

direction = 1

while True:
pwm.duty_cycle += 0.01 * direction
pwm.duty_cycle = round(pwm.duty_cycle, 2)
if pwm.duty_cycle == 1.0:
direction = -1
elif pwm.duty_cycle == 0.0:
direction = 1

time.sleep(0.05)

except KeyboardInterrupt:
pass

finally:
pwm.close()

3.2 Open PWM Device

This code segment uses the PWM class from the periphery library to create a PWM object. In this example, the PWM object is initialized for PWM10.

pwm = PWM(10, 0)

3.3 Configure PWM

In this code segment, some properties of the previously created PWM object are configured. The PWM frequency is set to 1000 Hz, the polarity is set to "normal," and the duty cycle is initialized to 0.

pwm.frequency = 1000
pwm.duty_cycle = 0
pwm.polarity = "normal"

3.4 PWM Output

This code segment implements a simple PWM output. By continuously adjusting the duty cycle in a loop, a breathing effect is achieved. The pwm.duty_cycle is increased by 0.01 each time (adjustable as needed), and the round function is used to keep it to two decimal places. When reaching the maximum or minimum value, the direction is reversed. After each PWM value update, the breathing speed is controlled using time.sleep.

while True:
pwm.duty_cycle += 0.01 * direction
pwm.duty_cycle = round(pwm.duty_cycle, 2)
if pwm.duty_cycle == 1.0:
direction = -1
elif pwm.duty_cycle == 0.0:
direction = 1

print(pwm.duty_cycle)
time.sleep(0.05)

3.5 Running the Program

  1. Use the vi tool in the terminal to create a Python file, paste the Python program, and save it.

    # nano pwm.py
  2. Run the program.

    # python3 pwm.py
  3. Experimental Observations

    Control PWM10 to realize breathing light
    image

4. PWM Control (C Program)

In the previous section, we demonstrated how to control PWM using the echo command and a Python program. Additionally, you can also use the vi editor to modify files, but be cautious about user permissions when making modifications. Furthermore, we can utilize C library functions or system calls to read and write device files for device control. Please note that to run programs on specific embedded systems, it is typically necessary to use cross-compilation tools to compile the code into an executable file that can run on the target development board. Let's explore the specific implementation steps together.

4.1 Complete Code

With the following program, you can control PWM output.

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

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

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

FILE *period_file = fopen(PWM_PATH "/pwm0/period", "w");
if (!period_file) {
perror("Failed to open PWM period");
return 1;
}
fprintf(period_file, "%d", PERIOD_NS);
fclose(period_file);

FILE *enable_file = fopen(PWM_PATH "/pwm0/enable", "w");
if (!enable_file) {
perror("Failed to open PWM enable");
return 1;
}
fprintf(enable_file, "1");
fclose(enable_file);

int direction = 1; // 1 for increasing duty cycle, -1 for decreasing
int duty_cycle_ns = 0;

while (1) {
duty_cycle_ns += 10000 * direction;
if(duty_cycle_ns == MAX_DUTY_CYCLE_NS)
direction = -1;
else if(duty_cycle_ns == MIN_DUTY_CYCLE_NS)
direction = 1;

FILE *duty_cycle_file = fopen(PWM_PATH "/pwm0/duty_cycle", "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);

usleep(50000); // Adjust this value for desired breathing speed
}

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

return 0;
}

4.2 Export PWM Channel to User Space

This code exports the specified PWM channel for user space control by writing the device index (usually 0) to /sys/class/pwm/pwmchip10/export.

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

4.3 Set PWM Period

This code sets the PWM period to 1KHZ by writing the period value "1000000" (in nanoseconds) to /sys/class/pwm/pwmchip10/pwm0/period.

FILE *period_file = fopen(PWM_PATH "/pwm0/period", "w");
if (!period_file) {
perror("Failed to open PWM period");
return 1;
}
fprintf(period_file, "%d", PERIOD_NS);
fclose(period_file);

4.4 Enable PWM

This code enables the PWM signal output by writing the value "1" to the /sys/class/pwm/pwmchip10/pwm0/enable file.

FILE *enable_file = fopen(PWM_PATH "/pwm0/enable", "w");
if (!enable_file) {
perror("Failed to open PWM enable");
return 1;
}
fprintf(enable_file, "1");
fclose(enable_file);

4.5 Implement PWM-Based Breathing LED Effect

This code opens the /sys/class/pwm/pwmchip10/pwm0/duty_cycle file and achieves a breathing LED effect by continuously changing the duty_cycle_ns variable from 0% to 100%, and then from 100% to 0%. The duty_cycle_ns variable's changes are between 0 and 1000000, controlling the duty cycle variation. When the duty cycle reaches its minimum or maximum value, it reverses direction to create a back-and-forth motion. Additionally, the usleep(50000) function is used to control the speed of the breathing LED. You can adjust this value as needed to change the breathing speed.

int direction = 1; // 1 for increasing duty cycle, -1 for decreasing
int duty_cycle_ns = 0;

while (1) {
duty_cycle_ns += 10000 * direction;
if(duty_cycle_ns == MAX_DUTY_CYCLE_NS)
direction = -1;
else if(duty_cycle_ns == MIN_DUTY_CYCLE_NS)
direction = 1;

FILE *duty_cycle_file = fopen(PWM_PATH "/pwm0/duty_cycle", "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);

usleep(50000); // Adjust this value for desired breathing speed
}

4.6 Unexport PWM Channel from User Space

This code cancels the export of the specified PWM channel to user space by writing the device index to /sys/class/pwm/pwmchip10/unexport.

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

4.7 Cross-Compilation

  1. Specify the Cross-Compilation Tool

    First, you need to add the path to the cross-compilation tool to the system's PATH environment variable so that you can use the cross-compilation tool from anywhere. You can add the following line to your shell configuration file (usually ~/.bashrc or ~/.bash_profile or ~/.zshrc, depending on your shell). Note that the path after PATH= should point to the directory where the cross-compilation tool is located.

    • gcc path

      <SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin/arm-rockchip830-linux-uclibcgnueabihf-gcc
    • Open the shell configuration file.

      nano ~/.bashrc 
    • Add the path of the cross-compilation tool to the system's PATH environment variable. Replace <SDK Directory> with your own SDK path, such as /home/luckfox/luckfox-pico/.

      export PATH=<SDK Directory>/tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/bin:$PATH
    • Reload the shell configuration file to apply the changes:

      source ~/.bashrc  
  2. Compile the Program Using the Cross-Compilation Tool

    arm-rockchip830-linux-uclibcgnueabihf-gcc pwm.c -o pwm
  3. After successful cross-compilation, an executable file that can run on the development board will be generated in the current directory.

    #ls
    pwm pwm.c

4.8 Running the Program

  1. File Transfer

    First, transfer the pwm program from the virtual machine to Windows, and then transfer it to the development board via TFTP or ADB. Here are the steps to transfer the file from Windows to the development board using ADB:

    adb push path_to_file destination_on_development_board

    eg: (Transferring the `pwm` file from the current directory to the root directory of the development board)
    adb push pwm /
  2. Running the Program

    Modify the permissions of the pwm file and then run the program:

    # chmod 777 pwm
    # ./pwm
  3. Experimental Observations

    Control PWM10 to realize breathing light
    image

5. Modify Device Tree

The PWM10_M1 in all models of development boards is enabled by default and can be used directly. Next, let's take a look at how to enable other PWM channels if needed.

5.1 Modify Device Tree File

  1. Device Tree File Paths

    • Development Board Configuration File

      In the <SDK directory>/project/cfg/BoardConfig_IPC/ directory, there are configuration files for different models of development boards. These files primarily include configuration parameters for various Luckfox Pico board models, covering aspects such as target architecture, boot medium, Uboot and kernel configurations, and partition settings. The SDK directory structure is as follows:

      ├── build.sh -> project/build.sh ---- SDK compilation script
      ├── media --------------------------- Multimedia encoding, ISP, and related algorithms (can be compiled independently in the SDK)
      ├── sysdrv -------------------------- U-Boot, kernel, rootfs directory (can be compiled independently in the SDK)
      ├── project ------------------------- Reference applications, compilation configurations, and script directories
      │ ├── cfg
      │ ├── BoardConfig_IPC
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk
      │ ├── BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico_Mini_A-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Mini_B-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1103_Luckfox_Pico_Plus-IPC.mk
      │ ├── BoardConfig-SPI_NAND-NONE-RV1106_Luckfox_Pico_Pro_Max-IPC.mk
      │ └── ...
      ├── output -------------------------- Directory for storing SDK compiled image files
      ├── docs ---------------------------- SDK documentation directory
      └── tools --------------------------- Image burning packaging tools and burning tools

      Among them, RK_KERNEL_DTS specifies the Device Tree Source (DTS) file for the kernel. Taking Luckfox Pico as an example, by opening the BoardConfig-EMMC-NONE-RV1103_Luckfox_Pico-IPC.mk file, we can see that the file pointed to by RK_KERNEL_DTS is rv1103g-luckfox-pico.dts

    • Device Tree File Path

      Based on RK_KERNEL_DTS, the device tree file path for Luckfox Pico can be determined as follows:

      <SDK directory>/sysdrv/source/kernel/arch/arm/boot/dts/rv1103g-luckfox-pico.dts
  2. Enable PWM0_M0

    In the device tree, the code snippet to enable PWM0_M0 is as follows:
    image

  3. Enable PWM0_M1

    If we want to use PWM0_M1 for output, we first need to uncomment the configuration statements for PWM0_M1 (please refer to line 201 in the code):
    image

    According to the interface diagram, PWM0_M1 (GPIO1_D2_d) has the default function of I2C3, so we need to comment out the I2C3 code snippet in the device tree:
    image

5.2 Compile the Kernel

  1. Recompile the kernel

    luckfox@luckfox:~/Luckfox-Pico/luckfox-pico$ ./build.sh kernel