In-Depth Analysis of the Linux PWM Subsystem

In-Depth Analysis of the Linux PWM Subsystem

1 Basics of PWM Technology

1.1 Basic Principles and Working Mechanism of PWM

Pulse Width Modulation (PWM) is an effective technique that uses digital signals to simulate the control of analog circuits. The core idea is to equivalently obtain different analog voltage values by changing the duty cycle of the square wave. In embedded systems, PWM is widely used in motor control, LED dimming, power management, and other fields.

The working principle of PWM is similar to that of a high-speed switch faucet. Suppose we have a faucet that outputs 5 liters of water per second when fully open. If we quickly turn this faucet on and off within 1 second, keeping it open for half the time and closed for the other half, the average output per second would be 2.5 liters. This is equivalent to a PWM signal with a duty cycle of 50%. Similarly, if the faucet is open 75% of the time and closed 25% of the time, the average output would be 3.75 liters. In PWM technology, the speed of this “faucet” switch is the frequency, and the proportion of time it is open is the duty cycle.

From a mathematical perspective, assuming the PWM period is T and the high level time is Ton, the duty cycle δ is given by:

δ = (Ton / T) × 100%

When the PWM signal is applied to a load, the average output voltage is given by:

Vavg = δ × Vmax

where Vmax is the voltage value of the PWM signal when it is at a high level.

1.2 Detailed Explanation of Key PWM Parameters

The three key parameters of a PWM waveform: period, duty cycle, and polarity collectively determine its characteristics and application scenarios.

  • Period: Refers to the time required for the PWM signal to complete one full cycle, usually measured in nanoseconds (ns) or milliseconds (ms). The period and frequency are reciprocals, i.e., f = 1/T. For example, a PWM signal with a period of 20ms has a frequency of 50Hz.
  • Duty Cycle: Refers to the proportion of time at high level within one PWM period, usually expressed as a percentage. A 0% duty cycle means the entire period is at low level, while a 100% duty cycle means the entire period is at high level.
  • Polarity: Refers to the output phase of the PWM signal. Normal polarity indicates that the duty cycle corresponds to the high level time; inverted polarity indicates that the duty cycle corresponds to the low level time. Polarity control is very important in practical applications, for example, in certain motor drive scenarios, inverted polarity is required to ensure safe operation.

Table: Relationship between PWM Parameters and Equivalent Output Voltage (Assuming Vmax = 5V)

Duty Cycle High Level Time Low Level Time Equivalent Output Voltage
0% 0 T 0V
25% 0.25T 0.75T 1.25V
50% 0.5T 0.5T 2.5V
75% 0.75T 0.25T 3.75V
100% T 0 5V

1.3 Composition of PWM Hardware System

In embedded systems, a PWM system typically consists of the following components:

  • PWM Controller: This is the hardware foundation for generating PWM, usually a peripheral module within the SoC. Modern SoCs often contain multiple PWM controllers, each capable of providing multiple independent PWM channels.
  • Divider and Counter: The PWM controller contains a divider and a counter. The divider is used to divide the input clock to an appropriate frequency, while the counter generates periodic waveforms based on the divided clock.
  • Compare Register: Stores the comparison value; when the counter value reaches the comparison value, the PWM output level toggles, thereby controlling the duty cycle.
  • Output Pins: The output of the PWM controller is routed through GPIO pins, which can usually be reused for different functions, including PWM output.

For example, in the popular RK3568 processor, the distribution of PWM controllers is as follows:

Table: PWM Pin Distribution for LubanCat-RK Series Boards

Board Series Pin12 Pin32 Pin33 Pin35
LubanCat-Zero Series pwm3 pwm11 pwm8 pwm9
LubanCat-1 Series pwm8 pwm9 pwm10 pwm14
LubanCat-2 Series pwm8 pwm9 pwm10 pwm14

2 Linux PWM Subsystem Architecture

2.1 Overview of the Overall Architecture

The Linux PWM subsystem adopts a typical layered architecture design, abstracting PWM functionality into the Controller Driver Layer, Core Layer, and Consumer Driver Layer. This design achieves hardware independence and interface uniformity, allowing consumer drivers to use PWM services through standard interfaces without concerning themselves with the specific implementation of the underlying hardware.

The overall architecture of the PWM subsystem can be represented as:

PWM Consumer Driver
PWM Core Layer
PWM Controller Driver
PWM Device Management
PWM API Interface
PWM Device 1
PWM Device 2
PWM Device N
Hardware PWM Controller 1
Hardware PWM Controller 2
Hardware PWM Controller N
User Space SysFS

In this architecture, the PWM Core Layer acts as an intermediary bridge, providing registration and management mechanisms for PWM devices while offering a unified API interface for consumer drivers. The PWM Controller Driver is responsible for interacting with specific hardware, including generating PWM waves and configuring parameters. The PWM Consumer Driver focuses on using PWM functionality without needing to understand the details of the underlying hardware.

2.2 Core Data Structure Relationships

The core data structures of the PWM subsystem include <span>pwm_chip</span>, <span>pwm_device</span>, <span>pwm_ops</span>, and <span>pwm_state</span>, which collectively describe the hardware characteristics and operational state of PWM devices. The relationships between these data structures are as follows:

包含
使用
状态
1
1
1
1..*
1
1
pwm_chip

+struct device *dev

+const struct pwm_ops *ops

+unsigned int npwm

+int base

+struct pwm_device **pwms

+struct list_head list

+unsigned int of_pwm_n_cells

+bool can_sleep

+int (*of_xlate)(...)
pwm_device

+const char *label

+unsigned long flags

+unsigned int hwpwm

+unsigned int pwm

+struct pwm_chip *chip

+void *chip_data

+struct pwm_state state

pwm_ops

+int (*apply)(...)

+void (*get_state)(...)

+int (*request)(...)

+void (*free)(...)

+int (*config)(...)

+int (*set_polarity)(...)

+int (*enable)(...)

+void (*disable)(...)
pwm_state

+u64 period

+u64 duty_cycle

+enum pwm_polarity polarity

+bool enabled
  • pwm_chip Structure: Represents a PWM controller, which can contain multiple PWM channels (devices). It contains a pointer to the <span>pwm_ops</span> structure, which implements the operation functions for specific controller drivers.
  • pwm_device Structure: Represents a PWM channel device, which is associated with a <span>pwm_chip</span> and contains the current operational state <span>pwm_state</span>. Each PWM device has a unique identifier that can be independently addressed and controlled within the system.
  • pwm_ops Structure: Contains a set of callback function pointers implemented by the PWM controller driver for hardware operations. As kernel versions evolve, <span>apply</span> and <span>get_state</span> have become the recommended callbacks, replacing the previously scattered functions like config, enable, and disable.
  • pwm_state Structure: Encapsulates the runtime state of the PWM device, including period, duty cycle, polarity, and enabled state. This encapsulation makes it easier to apply PWM parameters atomically.

2.3 PWM Device Lifecycle Management

The lifecycle of a PWM device in the Linux kernel includes the following stages:

  1. 1. Registration Stage: After the PWM controller driver detects the hardware, it initializes the <span>pwm_chip</span> structure, sets the operation functions, and then calls the <span>pwmchip_add()</span> function to register the controller with the PWM subsystem. This process creates the corresponding device interface under the /sys/class/pwm directory.
  2. 2. Request Stage: When the consumer driver needs to use the PWM device, it requests the PWM device through the <span>pwm_get()</span> series of functions. This process increases the device’s reference count and may call the controller driver’s request callback for hardware-specific initialization.
  3. 3. Configuration Stage: The consumer driver configures the PWM parameters, including period, duty cycle, and polarity, through the <span>pwm_apply_state()</span> function. These parameters undergo validity checks, such as ensuring the duty cycle does not exceed the period.
  4. 4. Enable Stage: After configuration, the consumer driver calls the <span>pwm_enable()</span> function to start PWM waveform output. This calls the enable callback function of the controller driver, which operates the hardware to start generating the PWM wave.
  5. 5. Release Stage: When the consumer driver no longer needs to use the PWM device, it calls the <span>pwm_put()</span> function to release the device, reducing the reference count. When the reference count drops to zero, the controller driver’s disable callback is called to stop waveform output, and the free callback may be called for resource cleanup.
  6. 6. Unregistration Stage: When the PWM controller driver is unloaded or the controller device is removed, the <span>pwmchip_remove()</span> function is called to unregister the PWM controller and release all associated resources.

3 Implementation of PWM Controller Driver

3.1 Detailed Development Process

Developing a PWM controller driver requires following the Linux kernel’s device driver model and implementing the interfaces defined by the PWM subsystem. The specific development steps are as follows:

  1. 1. Define Device Structure: First, define a device-specific structure that includes the <span>pwm_chip</span> structure. This structure typically also contains device-specific private data, such as register base addresses, clock handles, power management information, etc.
  2. 2. Implement PWM Operation Functions: Implement all necessary callback functions defined in the <span>pwm_ops</span> structure, which serve as the bridge for interaction between the PWM subsystem and the hardware.
  3. 3. Initialize pwm_chip Structure: In the driver’s probe function, initialize the fields of the <span>pwm_chip</span> structure, including pointers to operation functions, the number of PWM channels, etc.
  4. 4. Register PWM Controller: Call the <span>pwmchip_add()</span> function to register the PWM controller with the subsystem.
  5. 5. Implement Power Management: Implement suspend and resume callback functions as needed to correctly handle the PWM controller during changes in system power states.
  6. 6. Handle Device Tree Binding: Implement device tree parsing functionality so that the driver can obtain hardware configuration information from device tree nodes.

3.2 Core Data Structure Implementation

The core of the PWM controller driver is the <span>pwm_chip</span> structure, which represents a PWM controller:

struct pwm_chip {
    struct device *dev;                // Associated device
    const struct pwm_ops *ops;         // PWM operation functions
    int base;                          // First PWM number, negative when dynamically allocated
    unsigned int npwm;                 // Number of PWM channels
    struct pwm_device **pwms;          // PWM device array
    struct list_head list;             // List node
    bool can_sleep;                    // Indicates whether the operation can sleep
};

The controller driver also needs to implement the callback functions defined in the <span>pwm_ops</span> structure:

struct pwm_ops {
    int (*apply)(struct pwm_chip *chip, struct pwm_device *pwm,
                 const struct pwm_state *state);
    void (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,
                      struct pwm_state *state);
    int (*request)(struct pwm_chip *chip, struct pwm_device *pwm);
    void (*free)(struct pwm_chip *chip, struct pwm_device *pwm);
    // Traditional API - new drivers should use apply
    int (*config)(struct pwm_chip *chip, struct pwm_device *pwm,
                  int duty_ns, int period_ns);
    int (*enable)(struct pwm_chip *chip, struct pwm_device *pwm);
    void (*disable)(struct pwm_chip *chip, struct pwm_device *pwm);
    int (*set_polarity)(struct pwm_chip *chip, struct pwm_device *pwm,
                        enum pwm_polarity polarity);
};

3.3 Example of a Virtual PWM Controller Driver

The following is a simplified implementation of a virtual PWM controller driver, demonstrating the basic framework of the driver:

#include &lt;linux/module.h&gt;
#include &lt;linux/platform_device.h&gt;
#include &lt;linux/pwm.h&gt;
#include &lt;linux/io.h&gt;
#include &lt;linux/clk.h&gt;

// Private data structure for the virtual PWM controller
struct virtual_pwm_chip {
    struct pwm_chip chip;
    void __iomem *base;          // Register base address
    struct clk *clk;             // Clock
    unsigned long rate;          // Clock frequency
};

// Convert pwm_chip to private data structure
static inline struct virtual_pwm_chip *to_virtual_chip(struct pwm_chip *chip)
{
    return container_of(chip, struct virtual_pwm_chip, chip);
}

// Apply PWM configuration
static int virtual_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
                             const struct pwm_state *state)
{
    struct virtual_pwm_chip *vpwm = to_virtual_chip(chip);
    u64 period_cycles, duty_cycles;
    u32 period_reg, duty_reg;
    
    // Calculate the number of clock cycles corresponding to period and duty cycle
    period_cycles = mul_u64_u32_div(state-&gt;period, vpwm-&gt;rate, NSEC_PER_SEC);
    duty_cycles = mul_u64_u32_div(state-&gt;duty_cycle, vpwm-&gt;rate, NSEC_PER_SEC);
    
    // Write to hardware registers
    period_reg = (u32)period_cycles;
    duty_reg = (u32)duty_cycles;
    
    writel(period_reg, vpwm-&gt;base + PERIOD_OFFSET);
    writel(duty_reg, vpwm-&gt;base + DUTY_OFFSET);
    
    // Enable or disable PWM based on state
    if (state-&gt;enabled)
        writel(1, vpwm-&gt;base + ENABLE_OFFSET);
    else
        writel(0, vpwm-&gt;base + ENABLE_OFFSET);
    
    return 0;
}

// Get current PWM state
static void virtual_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
                                  struct pwm_state *state)
{
    struct virtual_pwm_chip *vpwm = to_virtual_chip(chip);
    u32 period_reg, duty_reg, enable_reg;
    
    // Read current state from hardware registers
    period_reg = readl(vpwm-&gt;base + PERIOD_OFFSET);
    duty_reg = readl(vpwm-&gt;base + DUTY_OFFSET);
    enable_reg = readl(vpwm-&gt;base + ENABLE_OFFSET);
    
    // Convert register values to time values (nanoseconds)
    state-&gt;period = DIV_ROUND_UP_ULL((u64)period_reg * NSEC_PER_SEC, vpwm-&gt;rate);
    state-&gt;duty_cycle = DIV_ROUND_UP_ULL((u64)duty_reg * NSEC_PER_SEC, vpwm-&gt;rate);
    state-&gt;enabled = enable_reg &amp; 0x1;
    state-&gt;polarity = PWM_POLARITY_NORMAL;
}

// PWM operation function structure
static const struct pwm_ops virtual_pwm_ops = {
    .apply = virtual_pwm_apply,
    .get_state = virtual_pwm_get_state,
    .owner = THIS_MODULE,
};

// Platform driver probe function
static int virtual_pwm_probe(struct platform_device *pdev)
{
    struct virtual_pwm_chip *vpwm;
    struct resource *res;
    int ret;
    
    // Allocate private data structure
    vpwm = devm_kzalloc(&amp;pdev-&gt;dev, sizeof(*vpwm), GFP_KERNEL);
    if (!vpwm)
        return -ENOMEM;
    
    // Get IO resources
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    vpwm-&gt;base = devm_ioremap_resource(&amp;pdev-&gt;dev, res);
    if (IS_ERR(vpwm-&gt;base))
        return PTR_ERR(vpwm-&gt;base);
    
    // Get clock
    vpwm-&gt;clk = devm_clk_get(&amp;pdev-&gt;dev, NULL);
    if (IS_ERR(vpwm-&gt;clk))
        return PTR_ERR(vpwm-&gt;clk);
    
    // Enable clock
    ret = clk_prepare_enable(vpwm-&gt;clk);
    if (ret)
        return ret;
    
    vpwm-&gt;rate = clk_get_rate(vpwm-&gt;clk);
    
    // Initialize pwm_chip structure
    vpwm-&gt;chip.dev = &amp;pdev-&gt;dev;
    vpwm-&gt;chip.ops = &amp;virtual_pwm_ops;
    vpwm-&gt;chip.npwm = 4;  // 4 PWM channels
    vpwm-&gt;chip.base = -1; // Dynamically allocated base ID
    
    // Register PWM controller
    ret = pwmchip_add(&amp;vpwm-&gt;chip);
    if (ret &lt; 0) {
        dev_err(&amp;pdev-&gt;dev, "failed to add PWM chip: %d\n", ret);
        clk_disable_unprepare(vpwm-&gt;clk);
        return ret;
    }
    
    platform_set_drvdata(pdev, vpwm);
    return 0;
}

// Platform driver remove function
static int virtual_pwm_remove(struct platform_device *pdev)
{
    struct virtual_pwm_chip *vpwm = platform_get_drvdata(pdev);
    
    pwmchip_remove(&amp;vpwm-&gt;chip);
    clk_disable_unprepare(vpwm-&gt;clk);
    
    return 0;
}

// Device tree matching table
static const struct of_device_id virtual_pwm_dt_ids[] = {
    { .compatible = "vendor,virtual-pwm" },
    { }
};
MODULE_DEVICE_TABLE(of, virtual_pwm_dt_ids);

// Platform driver structure
static struct platform_driver virtual_pwm_driver = {
    .driver = {
        .name = "virtual-pwm",
        .of_match_table = virtual_pwm_dt_ids,
    },
    .probe = virtual_pwm_probe,
    .remove = virtual_pwm_remove,
};
module_platform_driver(virtual_pwm_driver);

3.4 Device Tree Configuration Example

The device tree node configuration for the PWM controller is as follows:

pwm: pwm@10000000 {
    compatible = "vendor,virtual-pwm";
    reg = &lt;0x10000000 0x1000&gt;;
    clocks = &lt;&amp;pwm_clk&gt;;
    #pwm-cells = &lt;2&gt;;
    status = "okay";
};

Reference the PWM controller in the consumer device node:

fan {
    compatible = "pwm-fan";
    pwms = &lt;&amp;pwm 0 1000000&gt;;    // Use PWM channel 0, period 1ms
    pwm-names = "fan";
};

4 Implementation of PWM Consumer Driver

4.1 Programming Interface for Consumer Drivers

The PWM consumer driver uses a set of API functions provided by the PWM subsystem to utilize PWM services. These APIs can be categorized into the following types:

  • Device Acquisition and Release: Used to acquire and release PWM device handles
  • Configuration and Control: Used to configure PWM parameters and control PWM output
  • Status Query: Used to query the current status of PWM

4.2 Requesting and Releasing PWM Devices

Before using a PWM device, the consumer driver must first acquire a PWM device handle. The PWM subsystem provides various acquisition functions:

// Acquire PWM device via device tree phandle
struct pwm_device *pwm_get(struct device *dev, const char *con_id);

// Acquire via device tree phandle and optional name
struct pwm_device *devm_pwm_get(struct device *dev, const char *con_id);

// Acquire directly via device tree node
struct pwm_device *of_pwm_get(struct device_node *np, const char *con_id);

// Acquire directly by index (traditional method, not recommended)
struct pwm_device *pwm_request(int pwm, const char *label);

Usage example:

struct pwm_device *pwm;

// Acquire PWM device via device tree
pwm = devm_pwm_get(&amp;pdev-&gt;dev, NULL);
if (IS_ERR(pwm)) {
    dev_err(&amp;pdev-&gt;dev, "failed to get PWM: %ld\n", PTR_ERR(pwm));
    return PTR_ERR(pwm);
}

// Release PWM device after use (for devm_pwm_get, it will be released automatically)
pwm_put(pwm);

4.3 PWM Configuration and Control

After acquiring the PWM device, the consumer driver can configure PWM parameters and control output:

// Apply PWM state configuration
int pwm_apply_state(struct pwm_device *pwm, const struct pwm_state *state);

// Simplified function: configure PWM period and duty cycle
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);

// Set PWM polarity
int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity);

// Enable PWM output
int pwm_enable(struct pwm_device *pwm);

// Disable PWM output
void pwm_disable(struct pwm_device *pwm);

Usage example:

struct pwm_state state;
int ret;

// Configure PWM state
pwm_get_state(pwm, &amp;state);
state.period = 20000000;     // 20ms period
state.duty_cycle = 1500000;  // 1.5ms high level time
state.polarity = PWM_POLARITY_NORMAL;
state.enabled = true;

ret = pwm_apply_state(pwm, &amp;state);
if (ret) {
    dev_err(dev, "failed to apply PWM state: %d\n", ret);
    return ret;
}

4.4 System Call Interface

In addition to kernel APIs, the PWM subsystem also provides user space interfaces through sysfs, allowing user-space programs to control PWM devices. The main interface files include:

  • <span>/sys/class/pwm/pwmchipN/export</span> – Export PWM channel
  • <span>/sys/class/pwm/pwmchipN/unexport</span> – Unexport PWM channel
  • <span>/sys/class/pwm/pwmchipN/pwmM/period</span> – Set PWM period
  • <span>/sys/class/pwm/pwmchipN/pwmM/duty_cycle</span> – Set duty cycle
  • <span>/sys/class/pwm/pwmchipN/pwmM/enable</span> – Enable/disable PWM

Usage example:

# Export PWM channel 0
echo 0 &gt; /sys/class/pwm/pwmchip0/export

# Set period to 20ms
echo 20000000 &gt; /sys/class/pwm/pwmchip0/pwm0/period

# Set duty cycle to 1.5ms
echo 1500000 &gt; /sys/class/pwm/pwmchip0/pwm0/duty_cycle

# Enable PWM output
echo 1 &gt; /sys/class/pwm/pwmchip0/pwm0/enable

5 User Space Control of PWM

5.1 Detailed SysFS Interface

The Linux PWM subsystem provides a complete control interface for user space through the SysFS file system. These interfaces are located under the <span>/sys/class/pwm/</span> directory, with each PWM controller corresponding to a <span>pwmchipN</span> directory (N being the controller number).

Each <span>pwmchipN</span> directory contains the following key files:

  • npwm: Read-only file that shows the number of channels supported by the PWM controller.
  • export: Write-only file used to export a specified PWM channel for user space use.
  • unexport: Write-only file used to unexport a PWM channel.

When a PWM channel is exported by writing the channel number (e.g., 0) to the <span>export</span> file, a new <span>pwmM</span> directory (M being the exported channel number) is generated under the <span>pwmchipN</span> directory, which contains the following control files:

  • period: The total period of the PWM signal, measured in nanoseconds (ns).
  • duty_cycle: The duration of the high level in the PWM signal, measured in nanoseconds (ns), must be less than or equal to the period value.
  • polarity: The polarity of the PWM signal, which can be set to “normal” or “inversed”.
  • enable: Control for enabling/disabling PWM output; writing “1” enables PWM output, while writing “0” disables it.
  • Please open in WeChat client

5.2 Command Line Operation Example

Command line operations on PWM devices can quickly test hardware functionality or perform simple controls. Here is a complete example of controlling an SG90 servo:

# 1. Export PWM channel 0
echo 0 &gt; /sys/class/pwm/pwmchip1/export

# 2. Set PWM period to 20ms
echo 20000000 &gt; /sys/class/pwm/pwmchip1/pwm0/period

# 3. Set duty cycle to 1.5ms
echo 1500000 &gt; /sys/class/pwm/pwmchip1/pwm0/duty_cycle

# 4. Set polarity to normal
echo "normal" &gt; /sys/class/pwm/pwmchip1/pwm0/polarity

# 5. Enable PWM output
echo 1 &gt; /sys/class/pwm/pwmchip1/pwm0/enable

# Wait for 2 seconds
sleep 2

# 6. Change duty cycle to 2ms
echo 2000000 &gt; /sys/class/pwm/pwmchip1/pwm0/duty_cycle

# Wait for 2 seconds
sleep 2

# 7. Stop PWM output
echo 0 &gt; /sys/class/pwm/pwmchip1/pwm0/enable

# 8. Unexport PWM channel
echo 0 &gt; /sys/class/pwm/pwmchip1/unexport

5.3 User Space C Language Programming Example

In addition to command line operations, PWM devices can also be controlled in C programs through file operations. Here is a complete example:

#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;unistd.h&gt;
#include &lt;errno.h&gt;

#define PWM_CHIP "pwmchip1"
#define PWM_CHANNEL 0

// PWM configuration function
static int pwm_configure(const char *chip, int channel, 
                         unsigned int period, unsigned int duty_cycle, 
                         const char *polarity, int enable) {
    char path[128];
    char value[32];
    int fd, ret;
    
    // 1. Export PWM channel
    snprintf(path, sizeof(path), "/sys/class/pwm/%s/export", chip);
    fd = open(path, O_WRONLY);
    if (fd &lt; 0 &amp;&amp; errno != EBUSY) {
        perror("open export");
        return -1;
    }
    if (fd &gt;= 0) {
        snprintf(value, sizeof(value), "%d", channel);
        ret = write(fd, value, strlen(value));
        close(fd);
        if (ret &lt; 0) {
            perror("write export");
            return -1;
        }
    }
    
    // 2. Set period
    snprintf(path, sizeof(path), "/sys/class/pwm/%s/pwm%d/period", chip, channel);
    fd = open(path, O_WRONLY);
    if (fd &lt; 0) {
        perror("open period");
        return -1;
    }
    snprintf(value, sizeof(value), "%u", period);
    ret = write(fd, value, strlen(value));
    close(fd);
    if (ret &lt; 0) {
        perror("write period");
        return -1;
    }
    
    // 3. Set duty cycle
    snprintf(path, sizeof(path), "/sys/class/pwm/%s/pwm%d/duty_cycle", chip, channel);
    fd = open(path, O_WRONLY);
    if (fd &lt; 0) {
        perror("open duty_cycle");
        return -1;
    }
    snprintf(value, sizeof(value), "%u", duty_cycle);
    ret = write(fd, value, strlen(value));
    close(fd);
    if (ret &lt; 0) {
        perror("write duty_cycle");
        return -1;
    }
    
    // 4. Set polarity
    snprintf(path, sizeof(path), "/sys/class/pwm/%s/pwm%d/polarity", chip, channel);
    fd = open(path, O_WRONLY);
    if (fd &lt; 0) {
        perror("open polarity");
        return -1;
    }
    ret = write(fd, polarity, strlen(polarity));
    close(fd);
    if (ret &lt; 0) {
        perror("write polarity");
        return -1;
    }
    
    // 5. Enable/disable PWM
    snprintf(path, sizeof(path), "/sys/class/pwm/%s/pwm%d/enable", chip, channel);
    fd = open(path, O_WRONLY);
    if (fd &lt; 0) {
        perror("open enable");
        return -1;
    }
    snprintf(value, sizeof(value), "%d", enable);
    ret = write(fd, value, strlen(value));
    close(fd);
    if (ret &lt; 0) {
        perror("write enable");
        return -1;
    }
    
    return 0;
}

int main(int argc, char *argv[]) {
    printf("Starting PWM control example...\n");
    
    // Configure PWM: period 20ms, duty cycle 1.5ms, normal polarity, enable output
    if (pwm_configure(PWM_CHIP, PWM_CHANNEL, 20000000, 1500000, "normal", 1) &lt; 0) {
        fprintf(stderr, "Failed to configure PWM\n");
        return 1;
    }
    
    printf("PWM configured. Press Enter to change duty cycle...");
    getchar();
    
    // Change duty cycle to 2ms
    if (pwm_configure(PWM_CHIP, PWM_CHANNEL, 20000000, 2000000, "normal", 1) &lt; 0) {
        fprintf(stderr, "Failed to update PWM duty cycle\n");
        return 1;
    }
    
    printf("Duty cycle updated. Press Enter to stop PWM...");
    getchar();
    
    // Stop PWM output
    if (pwm_configure(PWM_CHIP, PWM_CHANNEL, 20000000, 2000000, "normal", 0) &lt; 0) {
        fprintf(stderr, "Failed to disable PWM\n");
        return 1;
    }
    
    printf("PWM disabled. Exiting.\n");
    return 0;
}

6 PWM Debugging and Diagnosis

6.1 Debugging Tools and Techniques

Debugging the PWM subsystem involves multiple levels of hardware and software. Here are some commonly used debugging tools and techniques:

  1. 1. debugfs Interface: The PWM subsystem provides detailed debugging information through debugfs, which can be viewed by the command:
    cat /sys/kernel/debug/pwm

    Example output:

    platform/ff420020.pwm, 1 PWM device
    pwm-0 (vdd-log ): requested enabled period: 24997 ns duty: 8243 ns polarity: inverse
    
    platform/ff420010.pwm, 1 PWM device
    pwm-0 ((null) ): period: 0 ns duty: 0 ns polarity: inverse
    
    platform/ff420000.pwm, 1 PWM device
    pwm-0 (backlight ): requested enabled period: 24997 ns duty: 10003 ns polarity: normal
  2. 2. sysfs Status Check: You can check the basic information of the PWM controller and the status of each channel through sysfs.
    # View PWM controller information
    ls /sys/class/pwm/
    cat /sys/class/pwm/pwmchip0/npwm
  3. 3. Oscilloscope Verification: Use an oscilloscope or logic analyzer to directly measure the PWM output pin and verify whether the waveform parameters meet expectations.
  4. 4. Dynamic Debugging: Enable the dynamic debugging feature of the kernel’s PWM subsystem to view the internal operation logs of the driver in real-time.
    # Enable PWM core debug messages
    echo 8 &gt; /proc/sys/kernel/printk
    # Enable dynamic debugging for a specific PWM driver
    echo -n 'file pwm-stm32.c +p' &gt; /sys/kernel/debug/dynamic_debug/control

6.2 Common Problems and Solutions

During PWM driver development and debugging, several issues may arise. Here are common problems and their solutions:

  1. 1. PWM Request Failure: When <span>pwm_request</span> or <span>pwm_get</span> returns <span>-EPROBE_DEFER</span> (error code -517), it indicates that the dependent resources are not yet ready.

    Solution: Check the status settings of the PWM controller in the device tree to ensure that related clocks, power, and other resources are correctly configured.

    &amp;pwm3 {
        status = "okay";  // Ensure status is okay
    };
  2. 2. No Signal Output from PWM:
  • • Check the PWM pin multiplexing configuration to ensure it is correctly set to PWM function
  • • Verify that the clock is enabled and the frequency is correct
  • • Confirm that PWM has been enabled through <span>pwm_enable</span> or the sysfs interface
  • 3. Parameter Configuration Not Taking Effect:
    • • Ensure that the period is set before the duty cycle
    • • Check that the duty cycle value is less than the period value
    • • Verify that the hardware supports the requested parameter range
  • 4. Resource Conflict: Conflicts occur when multiple drivers attempt to use the same PWM channel.

    Solution: Check the device tree configuration to ensure that each PWM channel is used by only one device.

  • 7 PWM Application Examples

    7.1 Example of PWM Fan Control Driver

    The following is a complete example of a PWM fan control driver, demonstrating how to implement a consumer driver based on the PWM subsystem:

    #include &lt;linux/module.h&gt;
    #include &lt;linux/platform_device.h&gt;
    #include &lt;linux/thermal.h&gt;
    #include &lt;linux/pwm.h&gt;
    #include &lt;linux/of.h&gt;
    #include &lt;linux/slab.h&gt;
    
    struct pwm_fan {
        struct pwm_device *pwm;
        unsigned int pwm_value;
        unsigned int max_pwm;
        struct thermal_cooling_device *cdev;
    };
    
    // Set PWM duty cycle
    static int pwm_fan_set_pwm(struct pwm_fan *fan, unsigned int pwm)
    {
        struct pwm_state state;
        int ret;
        
        pwm_get_state(fan-&gt;pwm, &amp;state);
        state.duty_cycle = pwm;
        state.enabled = (pwm &gt; 0);
        
        ret = pwm_apply_state(fan-&gt;pwm, &amp;state);
        if (ret) {
            dev_err(fan-&gt;cdev-&gt;dev, "Failed to set PWM: %d\n", ret);
            return ret;
        }
        
        fan-&gt;pwm_value = pwm;
        return 0;
    }
    
    // Cooling device state setting callback
    static int pwm_fan_set_cur_state(struct thermal_cooling_device *cdev,
                                     unsigned long state)
    {
        struct pwm_fan *fan = cdev-&gt;devdata;
        unsigned int pwm;
        
        if (!fan)
            return -EINVAL;
        
        // Convert cooling state to PWM value
        if (state == 0) {
            // Turn off fan
            pwm = 0;
        } else {
            // Linear mapping: state 1 corresponds to minimum PWM, maximum state corresponds to maximum PWM
            pwm = (state * fan-&gt;max_pwm) / THERMAL_MAX_STATE;
            if (pwm &lt; 1)
                pwm = 1;
            if (pwm &gt; fan-&gt;max_pwm)
                pwm = fan-&gt;max_pwm;
        }
        
        return pwm_fan_set_pwm(fan, pwm);
    }
    
    // Cooling device state retrieval callback
    static int pwm_fan_get_cur_state(struct thermal_cooling_device *cdev,
                                     unsigned long *state)
    {
        struct pwm_fan *fan = cdev-&gt;devdata;
        
        if (!fan)
            return -EINVAL;
        
        // Convert PWM value to cooling state
        if (fan-&gt;pwm_value == 0)
            *state = 0;
        else
            *state = (fan-&gt;pwm_value * THERMAL_MAX_STATE) / fan-&gt;max_pwm;
        
        return 0;
    }
    
    // Cooling device maximum state retrieval callback
    static int pwm_fan_get_max_state(struct thermal_cooling_device *cdev,
                                     unsigned long *state)
    {
        *state = THERMAL_MAX_STATE;
        return 0;
    }
    
    // Cooling device operation structure
    static const struct thermal_cooling_device_ops pwm_fan_cooling_ops = {
        .get_max_state = pwm_fan_get_max_state,
        .get_cur_state = pwm_fan_get_cur_state,
        .set_cur_state = pwm_fan_set_cur_state,
    };
    
    // Platform driver probe function
    static int pwm_fan_probe(struct platform_device *pdev)
    {
        struct device *dev = &amp;pdev-&gt;dev;
        struct device_node *np = dev-&gt;of_node;
        struct pwm_fan *fan;
        u32 max_pwm = 255;
        int ret;
        
        // Allocate fan control structure
        fan = devm_kzalloc(dev, sizeof(*fan), GFP_KERNEL);
        if (!fan)
            return -ENOMEM;
        
        // Get PWM device
        fan-&gt;pwm = devm_pwm_get(dev, NULL);
        if (IS_ERR(fan-&gt;pwm))
            return dev_err_probe(dev, PTR_ERR(fan-&gt;pwm), "Failed to get PWM\n");
        
        // Get maximum PWM value from device tree (optional)
        of_property_read_u32(np, "max-pwm", &amp;max_pwm);
        fan-&gt;max_pwm = max_pwm;
        
        // Initial setting to off state
        ret = pwm_fan_set_pwm(fan, 0);
        if (ret)
            return ret;
        
        // Register cooling device
        fan-&gt;cdev = thermal_of_cooling_device_register(np, "pwm-fan", fan,
                                                       &amp;pwm_fan_cooling_ops);
        if (IS_ERR(fan-&gt;cdev)) {
            dev_err(dev, "Failed to register cooling device\n");
            return PTR_ERR(fan-&gt;cdev);
        }
        
        platform_set_drvdata(pdev, fan);
        dev_info(dev, "PWM fan initialized, max PWM: %u\n", fan-&gt;max_pwm);
        
        return 0;
    }
    
    // Platform driver remove function
    static int pwm_fan_remove(struct platform_device *pdev)
    {
        struct pwm_fan *fan = platform_get_drvdata(pdev);
        
        thermal_cooling_device_unregister(fan-&gt;cdev);
        pwm_fan_set_pwm(fan, 0);  // Turn off fan
        
        return 0;
    }
    
    // Device tree matching table
    static const struct of_device_id of_pwm_fan_match[] = {
        { .compatible = "pwm-fan", },
        {},
    };
    MODULE_DEVICE_TABLE(of, of_pwm_fan_match);
    
    // Platform driver structure
    static struct platform_driver pwm_fan_driver = {
        .driver = {
            .name = "pwm-fan",
            .of_match_table = of_pwm_fan_match,
        },
        .probe = pwm_fan_probe,
        .remove = pwm_fan_remove,
    };
    module_platform_driver(pwm_fan_driver);
    
    MODULE_AUTHOR("PWM Driver Author");
    MODULE_DESCRIPTION("PWM Fan Control Driver");
    MODULE_LICENSE("GPL v2");

    The corresponding device tree node configuration is as follows:

    fan0: pwm-fan {
        compatible = "pwm-fan";
        pwms = &lt;&amp;pwm0 0 100000&gt;;  // Use PWM0, period 100us (10kHz)
        max-pwm = &lt;255&gt;;          // 8-bit PWM resolution
        #cooling-cells = &lt;2&gt;;
    };

    7.2 Analysis of Example Code

    The above PWM fan control driver demonstrates several key concepts:

    1. 1. Acquisition of PWM Device: The <span>devm_pwm_get()</span> function is used to acquire the PWM device, which automatically manages the resource lifecycle.
    2. 2. Management of PWM State: The <span>pwm_get_state()</span> and <span>pwm_apply_state()</span> functions are used to safely manage and apply PWM parameters.
    3. 3. Integration with Thermal Management: By registering the <span>thermal_cooling_device</span>, the fan control is integrated into the Linux thermal management framework, achieving adaptive control based on temperature.
    4. 4. Device Tree Configuration: The use of device trees for hardware description and configuration enhances the portability and flexibility of the driver.

    The workflow of this driver is as follows:

    • • When the system temperature rises, the thermal management framework calls <span>pwm_fan_set_cur_state()</span>
    • • The driver converts the cooling state into a PWM duty cycle value
    • • The hardware output corresponding to the duty cycle is configured through the PWM subsystem
    • • The fan speed varies with the PWM duty cycle, achieving temperature control

    This design reflects a typical application of the PWM subsystem in complex embedded systems, demonstrating how to seamlessly integrate hardware control with high-level system frameworks (such as thermal management).

    8 Conclusion

    The Linux PWM subsystem provides a unified, abstract hardware control interface, effectively isolating hardware characteristics from application logic through a layered architecture design. This article comprehensively analyzes the working principles, implementation mechanisms, and application methods of the PWM subsystem, covering the complete technology stack from hardware foundations to software frameworks.

    The core of PWM technology lies in controlling analog circuits through the duty cycle of digital signals, which has wide applications in embedded systems. The Linux kernel abstracts this hardware functionality through the PWM subsystem, providing a concise and consistent programming interface for consumer drivers. The architecture of this subsystem clearly distinguishes between controller drivers and consumer drivers, aligning with the design philosophy of the Linux kernel.

    As embedded systems continue to evolve, the PWM subsystem is also continuously evolving. In new versions of the kernel, unified interfaces like <span>pwm_apply_state()</span> are gradually replacing scattered configuration functions, improving code reliability and maintainability. Furthermore, the integration of PWM with other subsystems (such as thermal management and regulator frameworks) is becoming increasingly tight, showcasing the powerful system integration capabilities of the Linux embedded platform.

    By deeply understanding the PWM subsystem, developers can more efficiently implement various hardware control functions, building high-performance, stable, and reliable embedded systems. The technical analysis and example code provided in this article serve as practical references and guidance for development work in related fields.

    Leave a Comment