Timeline
Timeline
2026-01-04
init
This article introduces the basic concepts and hardware implementation of PWM (Pulse Width Modulation) technology under Linux. It first explains the definition of PWM, which is a technology that controls analog circuits by changing the duty cycle of high/low levels, and explains the two core parameters: period and duty cycle. It then describes the area equivalence principle, which states that pulses of equal area produce basically the same response for inertial elements, and gives examples showing that different average voltages can be output by adjusting the duty cycle. It also lists applications of PWM in motor control, lighting control, and signal modulation. Finally, it summarizes the features of the 16 hardware PWM channels built into the iTOP-RK3568 chip, including independent control, interrupt-driven operation, programmable prescaler, 32-bit timer/counter, capture mode, continuous/one-shot output modes, and reference mode, and specifically points out that 4 of them are suitable for infrared applications. The article also compares the advantages and disadvantages of hardware PWM and provides a PWM pin multiplexing function table, making it convenient for developers to select PWM channels according to actual needs.
Linux Driver Notes
| Table of Contents | Links |
|---|---|
| 1. Linux Driver Framework | |
| 2. Linux Driver Loading Logic | |
| 3. Character Device Basics | |
| 4. Concurrency and Race Conditions | |
| 5. Advanced Character Device Topics | |
| 6. Interrupts | |
| 7. Platform Bus | |
| 8. Device Tree | |
| 9. Device Model | |
| 10. Hotplug | |
| 11. pinctrl Subsystem | |
| 12. GPIO subsystem | |
| 13. Input subsystem | |
| 14. 1-Wire | |
| 15. I2C | |
| 16. SPI | |
| 17. UART | |
| 18. PWM | |
| 19. RTC | |
| 20. Watchdog | |
| 21. CAN | |
| 22. Network devices | |
| 23. ADC | |
| 24. IIO | |
| 25. USB | |
| 26. LCD |
PWM Basics
PWM The full name is Pulse Width Modulationtranslated into Chinese as pulse width modulation. It is a technology that uses digital signals to control analog circuits. By changing the duty cycle of high/low levels, it can control the average voltage or power, thereby achieving the purpose of controlling analog quantities.
A PWM signal consists of a rectangular wave, which includes high-level time and low-level time. A PWM signal can be described by two parameters: period and duty cycle. The specific descriptions of the two parameters are as follows:
- Period (T): refers to the time required for one complete high/low level cycle. Frequency is the reciprocal of the period, indicating how many cycles occur in 1 second, with the unit Hz. For example, if one period is 20ms, then there are 50 PWM cycles in one second.
- Duty Cycle: refers to the ratio of high-level time to the period, usually expressed as a percentage. For example, if the period is 20ms and the high-level time is 10ms, then the duty cycle is 50%.

Area Equivalence Principle
The area equivalence principle is defined as: even if the width and amplitude of pulse signals are different, as long as their areas (pulse width × pulse amplitude) are the same,the response of the inertial elementis also basically the same, as shown in the figure below.

For example: the high level of the RK3568 IO port is 3.3V, and the low level is 0V. If we want to output different analog voltages, we can use the PWM area equivalence principle to achieve this. By changing the duty cycle of the PWM signal, the average voltage of the output signal can be adjusted.
When the duty cycle is 50%, the high-level time and low-level time are equal, and the average output voltage is 1.65V (3.3V × 50%).
When the duty cycle is 75%, the high-level time accounts for 75% of the total period, and the average output voltage is 2.47V (3.3V × 75%).
In addition to voltage control, PWM also has the following application scenarios:
- Motor control: controls motor speed and torque.
- Lighting control: controls LED brightness.
- Signal modulation: used for modulation in radio communications.
iTOP-RK3568 PWM Interface
The RK3568 chip has 16 built-in PWM channels, which can independently control and output PWM waveforms. It also supports interrupt-driven operation and can flexibly respond to interrupt events.
- The PWM channels use a programmable prescaler, which can first divide the bus clock and then perform a second division.
- The PWM channels have a built-in 32-bit timer/counter function.
- PWM channels support capture mode, which can capture information such as the pulse width of external input signals.
- PWM channels support continuous output mode and one-shot output mode.
- PWM channels provide reference mode, which can output waveforms with various duty cycles.
- The four PWM channels PWM3, PWM7, PWM11, and PWM15 are optimized and particularly suitable for infrared applications.
The 16 PWM channels above are all hardware PWM, that is, PWM output is implemented by a dedicated PWM hardware module. The introduction to hardware PWM is as follows:
Hardware PWM:
- Implementation methodHardware PWM is a method of implementing PWM output through a dedicated PWM hardware module.
- Advantages:
- Low CPU usage: PWM output is automatically completed by the hardware module without CPU intervention.
- High PWM output frequency and resolution, capable of reaching MHz-level frequencies and ns-level resolution.
- The output waveform is stable and reliable, and is not easily affected by CPU load.
- Disadvantages:
- Requires a dedicated hardware PWM module, resulting in higher cost.
- PWM output pins are limited; output is only available on predefined pins.
The hardware PWM pin multiplexing function table for the iTOP-RK3568 is shown below. PWM can be selected according to this table:
| PWM Interface | pinctrl function | Net label | Corresponding GPIO | Function |
|---|---|---|---|---|
| PWM0 | PWM0_M0 | Working_LEDEN_H_GPIO0_B7 | GPIO0_B7 | LED control pin |
| PWM0_M1 | LCD0_PWREN_H_GPIO0_C7 | GPIO0_C7 | LVDS screen power supply enable pin | |
| PWM1 | PWM1_M0 | VGA_HPDIN_GPIO0_C0 | GPIO0_C0 | VGA hot-plug detection pin |
| PWM1_M1 | PCIE20_WAKEN_M0 | GPIO0_B5 | PCIE2.0 wake-up | |
| PWM2 | PWM2_M0 | PCIE20_PERSTn_GPIO0_C1 | GPIO0_C1 | PCIE2.0 reset pin |
| PWM2_M1 | TP_RST_L_GPIO0_B6 | GPIO0_B6 | MIPI touchscreen reset pin | |
| PWM3_IR | PWM3_IR | PWM3_IR | GPIO0_C2 | Infrared receiver |
| PWM4 | PWM4 | LCD0_BL_PWM4 | GPIO0_C3 | LVDS screen backlight |
| PWM5 | PWM5 | LCD1_BL_PWM5 | GPIO0_C4 | MIPI screen backlight |
| PWM6 | PWM6 | PWM_FAN | GPIO0_C5 | Cooling fan speed adjustment |
| PWM7_IR | PWM7_IR | RS485_DIR_GPIO0_C6 | GPIO0_C6 | 485 transceiver control pin |
| PWM8 | PWM8_M0 | UART4_RX_M1 | GPIO3_B1 | Serial port 4 receive pin |
| PWM8_M1 | SDMWC0_D0 | GPIO1_D5 | SD card data line | |
| PWM9 | PWM9_M0 | UART4_TX_M1 | GPIO3_B2 | UART4 TX pin |
| PWM9_M1 | SDMWC0_D1 | GPIO1_D5 | SD card data line | |
| PWM10 | PWM10_M0 | GPIO3_B5 | GPIO3_B5 | Unused |
| PWM10_M1 | SDMWC0_CMD | GPIO2_A1 | SD card CMD pin | |
| PWM11 | PWM11_IR_M0 | GPIO3_B6 | GPIO3_B6 | Unused |
| PWM11_IR_M1 | CIF_CLKOUT | GPIO4_C0 | Unused | |
| PWM12 | PWM12_M0 | GMAC0_RSTn_GPIO3_B7 | GPIO3_B7 | Ethernet 0 reset pin |
| PWM12_M1 | GPIO4_C5 | GPIO4_C5 | UART9 TX pin | |
| PWM13 | PWM13_M0 | GMAC0_INT/PMEB_GPIO3_C0 | GPIO3_C0 | Ethernet 0 interrupt pin |
| PWM13_M1 | GPIO4_C6 | GPIO4_C6 | UART9 RX pin | |
| PWM14 | PWM14_M0 | UART7_TX_M1 | GPIO3_C4 | UART7 TX pin |
| PWM14_M1 | 4G_DISABLE_GPIO4_C2 | GPIO4_C2 | CAN1 RX | |
| PWM15 | PWM15_IR_M0 | UART7_RX_M1 | GPIO3_C5 | UART7 RX pin |
| PWM15_IR_M1 | HDMI_RX_INT_L_GPIO4_C3 | GPIO4_C3 | CAN1 TX |
When hardware PWM is insufficient, GPIO can be used to simulate PWM, which can also be called software PWM. The introduction to software PWM is as follows:
Software PWM:
- Implementation methodSoftware PWM is a method of implementing PWM output through software programming. By using timer interrupts or loop counting, the switching time of the output pin’s high and low levels is controlled in software, thereby generating PWM waveforms.
- Advantages:
- It is highly flexible and can generate PWM waveforms on any GPIO pin.
- Low cost, no additional hardware PWM module is required.
- Disadvantages:
- High CPU usage, because the pin level needs to be controlled in real time in interrupt service routines or loops.
- The frequency and resolution of PWM output are affected by the CPU clock frequency and interrupt response time, making it difficult to achieve high frequency and high resolution.
- It has high requirements for CPU performance and real-time capability.
PWM subsystem framework
The PWM subsystem can be divided into three layers: user space, kernel space, and hardware layer.
Kernel space includes the PWM device driver layer, PWM core layer, and PWM adapter driver layer.

PWM device driver layer
The PWM device driver layer plays a key role in the Linux kernel, providing a unified, flexible, and efficient PWM device access interface for upper-layer applications.
pwm_device- Represents a slave device connected to the PWM bus.
- Contains information such as the PWM channel number of the slave device and the PWM controller it belongs to.
pwm_driver- Implements the driver for a specific PWM device.
- Responsible for operations such as initialization, configuration, start and stop of the PWM device.
- Interacts with the PWM hardware device through pwm_chip.
- Provides a standardized PWM device access interface to the upper layer.
/dev/xxxDevice Node- Provides an interface for upper-layer applications to access PWM devices.
- By opening/reading/writing/controlling device nodes, applications can interact with PWM devices.
- The kernel PWM subsystem is responsible for forwarding application operations to the corresponding pwm_chip driver.
PWM core layer
The PWM core layer is an important component in the Linux kernel responsible for managing and controlling PWM devices. Its main functions are as follows:
- PWM device management
Through thestruct pwm_deviceUniformly describe PWM device information, such as device name, associated controller, etc. Provide a standardized PWM device access API for upper-layer applications. - PWM parameter configuration
providepwm_configInterface for configuring the working parameters of the PWM device, such as period, duty cycle, polarity, etc. Converts upper-layer configuration parameters into a form understandable by the underlying hardware and sets them through the adapter driver. - PWM output control
providepwm_enableandpwm_disableInterface for controlling the enabling and disabling of PWM output. Responsible for calling the corresponding functions of the adapter driver to perform actual output control. - PWM sysfs interface
The PWM core layer also provides a set of sysfs-based interfaces for upper-layer applications to configure and control PWM devices. These sysfs interfaces include:/sys/class/pwm/pwmX/period: Set PWM period/sys/class/pwm/pwmX/duty_cycle: Set PWM duty cycle/sys/class/pwm/pwmX/polarity: Set PWM polarity/sys/class/pwm/pwmX/enable: Control the enabling and disabling of PWM output
PWM controller driver layer
The PWM controller driver layer is a key component connecting PWM hardware and the upper-layer PWM core layer. It is responsible for abstracting and encapsulating the underlying PWM hardware, adapting to different types of PWM devices, providing a standardized hardware control interface to the upper layer, and also handling tasks such as hardware-related initialization, state management, and power management.
Hardware abstraction
The PWM controller driver layer is responsible for abstracting and encapsulating the underlying PWM hardware devices. It defines a set of standardizedpwm_opsinterfaces, including device initialization, parameter configuration, output control, and other functions.
The upper-layer PWM core layer can indirectly control the underlying PWM hardware by calling these standard interfaces, without needing to pay attention to specific hardware implementation details.
Hardware Adaptation
The PWM controller driver needs to implementpwm_opsthe functions defined in the interface to adapt to the specific working mode of the underlying PWM hardware. These adaptation functions convert the standardized requests from the upper-layer PWM core layer into operation instructions that the underlying hardware can understand.
Through this adaptation layer, the PWM core layer can seamlessly support different types of PWM hardware devices.
Hardware Initialization
The PWM controller driver is responsible for completing the initialization of the PWM hardware device, including the configuration and initialization of related resources such as GPIO, clock, and power.
When the PWM device is registered, the adapter driver performs initialization operations to ensure that the hardware device is in an available state.
State Management
The adapter driver needs to maintain the current state of the PWM hardware device, such as whether the device has been enabled and the current operating parameters. This state information is fed back to the upper-layer PWM core layer to ensure that the core layer can correctly control and manage the PWM device.
At this point, the explanation of the PWM subsystem framework is complete. In the next chapter, we will explain how to operate PWM through the sysfs interface.
Using the sysfs Interface to Operate PWM
The PWM core layer provides a sysfs interface for operating PWM. However, to operate PWM, you first need to configure the corresponding IO port as a PWM function. The selected IO port is pin 6 of the 20-pin GPIO interface on the iTOP-RK3568 backplane, as shown in the following figure:

According to the PWM pin multiplexing table, the PWM12_M1 function pin is multiplexed as UART9 by default, so the device tree needs to be modified to set the multiplexing function to PWM.
Find the uart9 node, change its status from ‘okay’ to ‘disabled’, and then configure PWM12:
1234 | &pwm12 { status = "okay"; pinctrl-0 = <&pwm12m1_pins>;}; |
/sys/class/pwm/
First, go to the/sys/class/pwm/directory, as shown in the following figure:

The corresponding relationship is as follows

PWM12_M1 corresponds to pwmchip3. After entering the pwmchip3 directory, it is shown in the following figure

There are seven files in this directory, namely device、export、npwm、power、subsystem、uevent and unexportThe three attribute files that need attention are export, npwm, and unexport.
- npwmIt is a read-only attribute. Reading this file tells you how many PWM outputs are available under this PWM controller, as shown below:

- export: Before using PWM, export it via the export attribute, as shown below:

- unexport: After using PWM, you need to delete the exported PWM, for example:

Note that both the export and unexport files are write-only and have no read permission.
Then use the following command to export the pwm0 directory. After successful export, enter the folder as shown below:

- polarity: Used to view PWM polarity, with normal and inversed options.
- normal means the PWM signal is positive polarity, i.e., high level is the active signal.
- inversed means the PWM signal is negative polarity, i.e., low level is the active signal.
- period: Used to configure the PWM period, readable and writable; write a string numeric value, in ns (nanoseconds).
- duty_cycle: Used to configure the PWM duty cycle, readable and writable; write a string numeric value, also in ns.
- enable: Enable PWM output. Usually, after configuring PWM, enable PWM. Readable and writable.
- Writing “0” disables PWM;
- Writing “1” enables PWM.
SG-90 Servo Introduction
The SG90 servo is a small servo motor widely used in fields such as robotics, remote-controlled aircraft, and remote-controlled cars. It is favored for its compact size, light weight, low power consumption, and high cost-effectiveness.
Working Principle
The SG90 servo controls its position through Pulse Width Modulation (PWM) signals.The width of the PWM signal determines the rotation angle of the servo.。
The typical control signal period is 20ms, where the duration of the high-level portion (0.5ms to 2.5ms) corresponds to the servo’s rotation range from 0 to 180 degrees. The specific correspondence is as follows:
| High-level duration | Rotation angle |
|---|---|
| 0.5 ms | 0° |
| 1.0 ms | 45° |
| 1.5 ms | 90° |
| 2.0 ms | 135° |
| 2.5 ms | 180° |
Hardware Connection
| Cable color | Function | Backplane 20-pin pin number | Net label |
|---|---|---|---|
| Red wire | Power positive (5V) | 10/12 | VCC5V0_SYS |
| Brown wire | Power negative (GND) | 19/20 | GND |
| Orange wire | Control signal wire (PWM signal) | 6 | UART9_TX_M1(GPIO4_C5) |
Application scenarios
- Robotics: Used for joints or other movable parts in robots; its precise angle control makes it suitable for multi-degree-of-freedom motion.
- Remote control models: Used to control the direction of control surfaces or wheels, enabling flexible maneuvering.
Example
Use the following commands to rotate the servo 135 degrees:
12345678 | # Set the PWM period to 20 msecho 20000000 > period# Set the PWM high-level duration to 2 msecho 2000000 > duty_cycle# Set the PWM polarity to positive (normal)echo normal > polarity# Enable PWM outputecho 1 > enable |
PWM Controller Registration Process Analysis
The PWM-related nodes in the rk3568.dtsi device tree are as follows:
123456789101112131415161718192021 | pwm0: pwm@fdd70000 { compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm"; reg = <0x0 0xfdd70000 0x0 0x10>; pinctrl-names = "active"; pinctrl-0 = <&pwm0m0_pins>; clocks = <&pmucru CLK_PWM0>, <&pmucru PCLK_PWM0>; clock-names = "pwm", "pclk"; status = "disabled";};pwm1: pwm@fdd70010 { compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm"; reg = <0x0 0xfdd70010 0x0 0x10>; pinctrl-names = "active"; pinctrl-0 = <&pwm1m0_pins>; clocks = <&pmucru CLK_PWM0>, <&pmucru PCLK_PWM0>; clock-names = "pwm", "pclk"; status = "disabled";}; |
Based on the compatible property of the PWM node, the Rockchip PWM driver path can be found in the kernel directory:drivers/pwm/pwm-rockchip.c
12345678 | static struct platform_driver rockchip_pwm_driver = { .driver = { .name = "rockchip-pwm", .of_match_table = rockchip_pwm_dt_ids, }, .probe = rockchip_pwm_probe, .remove = rockchip_pwm_remove,}; |
Then let’s look at the probe function of this driver.rockchip_pwm_probe, the specific content is as follows:
rockchip_pwm_probe()
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 | static int rockchip_pwm_probe(struct platform_device *pdev){ const struct of_device_id *id;// Device tree match ID struct rockchip_pwm_chip *pc;// PWM chip structure struct resource *r;// Resource information u32 enable_conf, ctrl; bool enabled; int ret, count;// Return value and count variable // Check device tree match id = of_match_device(rockchip_pwm_dt_ids, &pdev->dev); if (!id) return -EINVAL; // Allocate PWM chip structure pc = devm_kzalloc(&pdev->dev, sizeof(*pc), GFP_KERNEL); if (!pc) return -ENOMEM; // Get memory resources and map IO addresses r = platform_get_resource(pdev, IORESOURCE_MEM, 0); pc->base = devm_ioremap(&pdev->dev, r->start, resource_size(r)); if (IS_ERR(pc->base)) return PTR_ERR(pc->base); // Get PWM bus clock pc->clk = devm_clk_get(&pdev->dev, "pwm"); if (IS_ERR(pc->clk)) { pc->clk = devm_clk_get(&pdev->dev, NULL); if (IS_ERR(pc->clk)) return dev_err_probe(&pdev->dev, PTR_ERR(pc->clk), "Can't get bus clk\n"); } // Get APB clock count = of_count_phandle_with_args(pdev->dev.of_node, "clocks", "#clock-cells"); if (count == 2) pc->pclk = devm_clk_get(&pdev->dev, "pclk"); else pc->pclk = pc->clk; if (IS_ERR(pc->pclk)) { ret = PTR_ERR(pc->pclk); if (ret != -EPROBE_DEFER) dev_err(&pdev->dev, "Can't get APB clk: %d\n", ret); return ret; } // Enable bus clock ret = clk_prepare_enable(pc->clk); if (ret) { dev_err(&pdev->dev, "Can't prepare enable bus clk: %d\n", ret); return ret; } // Enable APB clock ret = clk_prepare_enable(pc->pclk); if (ret) { dev_err(&pdev->dev, "Can't prepare enable APB clk: %d\n", ret); goto err_clk; } pc->channel_id = rockchip_pwm_get_channel_id(pdev->dev.of_node->full_name); if (pc->channel_id < 0 || pc->channel_id >= PWM_MAX_CHANNEL_NUM) { dev_err(&pdev->dev, "Channel id is out of range: %d\n", pc->channel_id); ret = -EINVAL; goto err_pclk; } if (IS_ENABLED(CONFIG_PWM_ROCKCHIP_ONESHOT)) { pc->irq = platform_get_irq(pdev, 0); if (pc->irq < 0) { dev_err(&pdev->dev, "Get oneshot mode irq failed\n"); ret = pc->irq; goto err_pclk; } ret = devm_request_irq(&pdev->dev, pc->irq, rockchip_pwm_oneshot_irq, IRQF_NO_SUSPEND | IRQF_SHARED, "rk_pwm_oneshot_irq", pc); if (ret) { dev_err(&pdev->dev, "Claim oneshot IRQ failed\n"); goto err_pclk; } } // Get pin controller pc->pinctrl = devm_pinctrl_get(&pdev->dev); if (IS_ERR(pc->pinctrl)) { dev_err(&pdev->dev, "Get pinctrl failed!\n"); ret = PTR_ERR(pc->pinctrl); goto err_pclk; } // Get the active state of the pin controller pc->active_state = pinctrl_lookup_state(pc->pinctrl, "active"); if (IS_ERR(pc->active_state)) { dev_err(&pdev->dev, "No active pinctrl state\n"); ret = PTR_ERR(pc->active_state); goto err_pclk; } // Set driver data platform_set_drvdata(pdev, pc); // Initialize PWM chip structure pc->data = id->data; pc->chip.dev = &pdev->dev; pc->chip.ops = &rockchip_pwm_ops; pc->chip.base = of_alias_get_id(pdev->dev.of_node, "pwm"); pc->chip.npwm = 1; pc->clk_rate = clk_get_rate(pc->clk); // If polarity setting is supported, use the of with polarity_pwm_xlate function if (pc->data->supports_polarity) { pc->chip.of_xlate = of_pwm_xlate_with_flags; pc->chip.of_pwm_n_cells = 3; } enable_conf = pc->data->enable_conf; ctrl = readl_relaxed(pc->base + pc->data->regs.ctrl); enabled = (ctrl & enable_conf) == enable_conf; // Check whether it is center-aligned mode pc->center_aligned = device_property_read_bool(&pdev->dev, "center-aligned"); // Add PWM chip ret = pwmchip_add(&pc->chip); if (ret < 0) { dev_err(&pdev->dev, "pwmchip_add() failed: %d\n", ret); goto err_pclk; } /* Keep the PWM clk enabled if the PWM appears to be up and running. */ if (!enabled)// If PWM is not enabled, disable the bus clock clk_disable(pc->clk); clk_disable(pc->pclk); return 0;err_pclk: clk_disable_unprepare(pc->pclk);err_clk: clk_disable_unprepare(pc->clk); return ret;} |
Created astruct rockchip_pwm_chippointer variable of type, which is mainly used to describe the hardware characteristics and configuration information of the PWM controller on Rockchip series SoCs, as shown below:
struct rockchip_pwm_chip
123456789101112131415 | struct rockchip_pwm_chip { struct pwm_chip chip;// Generic PWM controller chip interface struct clk *clk;// Main clock of the PWM controller struct clk *pclk;// Peripheral clock of the PWM controller struct pinctrl *pinctrl;// Pin mux controller of the PWM controller struct pinctrl_state *active_state;// Pin state currently used by the PWM controller const struct rockchip_pwm_data *data;// PWM controller configuration information specific to Rockchip SoC void __iomem *base;// Kernel virtual address of the PWM controller register space unsigned long clk_rate;// Clock frequency currently used by the PWM controller bool vop_pwm_en; // Indicates whether the VOP (Video Output Processor) also enables the PWM function bool center_aligned;// Indicates whether the PWM controller supports center-aligned mode bool oneshot;// Indicates whether the PWM controller supports one-shot mode int channel_id; int irq;}; |
The second line of whichstruct pwm_chipThe structure is an abstraction and encapsulation of the PWM controller in the PWM subsystem.
It centralizes the basic information and operation functions of the PWM controller into a structure, facilitating management and invocation by the upper-level PWM framework. This structure is defined ininclude/linux/pwm.hfile, and the specific content is as follows:
struct pwm_chip
1234567891011121314151617181920212223242526 | /** * struct pwm_chip - abstract a PWM controller * @dev: device providing the PWMs * @ops: callbacks for this PWM controller * @base: number of first PWM controlled by this chip * @npwm: number of PWMs controlled by this chip * @of_xlate: request a PWM device given a device tree PWM specifier * @of_pwm_n_cells: number of cells expected in the device tree PWM specifier * @list: list node for internal use * @pwms: array of PWM devices allocated by the framework */struct pwm_chip { struct device *dev;// The device to which the PWM controller belongs const struct pwm_ops *ops;// The operation function set of the PWM controller int base;// The first PWM channel number controlled by the chip unsigned int npwm;// The number of PWM channels provided by the PWM controller struct pwm_device * (*of_xlate)(struct pwm_chip *pc, const struct of_phandle_args *args); unsigned int of_pwm_n_cells;// The number of cells required to describe the PWM property in the device tree /* only used internally by the PWM framework */ struct list_head list;// The global linked list where the PWM controller resides struct pwm_device *pwms;// PWM channel device array}; |
wherepwm_opsRepresents the operation function set of the PWM controller
struct pwm_ops
12345678910111213141516171819202122232425262728293031323334353637 | /** * struct pwm_ops - PWM controller operations * @request: optional hook for requesting a PWM * @free: optional hook for freeing a PWM * @capture: capture and report PWM signal * @apply: atomically apply a new PWM config * @get_state: get the current PWM state. This function is only * called once per PWM device when the PWM chip is * registered. * @get_output_type_supported: get the supported output type of this PWM * @owner: helps prevent removal of modules exporting active PWMs * @config: configure duty cycles and period length for this PWM * @set_polarity: configure the polarity of this PWM * @enable: enable PWM output toggling * @disable: disable PWM output toggling */struct pwm_ops { int (*request)(struct pwm_chip *chip, struct pwm_device *pwm); void (*free)(struct pwm_chip *chip, struct pwm_device *pwm); int (*capture)(struct pwm_chip *chip, struct pwm_device *pwm, struct pwm_capture *result, unsigned long timeout); 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 (*get_output_type_supported)(struct pwm_chip *chip, struct pwm_device *pwm); struct module *owner; /* Only used by legacy drivers */ int (*config)(struct pwm_chip *chip, struct pwm_device *pwm, int duty_ns, int period_ns); int (*set_polarity)(struct pwm_chip *chip, struct pwm_device *pwm, enum pwm_polarity polarity); int (*enable)(struct pwm_chip *chip, struct pwm_device *pwm); void (*disable)(struct pwm_chip *chip, struct pwm_device *pwm);}; |
Then return todrivers/pwm/pwm-rockchip.cthe file and findrockchip_pwm_opsthe operation set, and it can be seen that Rockchip only implements two functions: state get and apply, and the specific content is as follows
12345 | static const struct pwm_ops rockchip_pwm_ops = { .get_state = rockchip_pwm_get_state, .apply = rockchip_pwm_apply, .owner = THIS_MODULE,}; |
pwmchip_add()
rockchip_pwm_probe()callspwmchip_addthe function to add the PWM controller to the PWM subsystem,pwmchip_addThe function is defined indrivers/pwm/core.cfile, and the specific content is as follows:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495 | /** * pwmchip_add() - register a new PWM chip * @chip: the PWM chip to add * * Register a new PWM chip. If chip->base < 0 then a dynamically assigned base * will be used. The initial polarity for all channels is normal. * * Returns: 0 on success or a negative error code on failure. */int pwmchip_add(struct pwm_chip *chip){ return pwmchip_add_with_polarity(chip, PWM_POLARITY_NORMAL);}EXPORT_SYMBOL_GPL(pwmchip_add);/** * pwmchip_add_with_polarity() - register a new PWM chip * @chip: the PWM chip to add * @polarity: initial polarity of PWM channels * * Register a new PWM chip. If chip->base < 0 then a dynamically assigned base * will be used. The initial polarity for all channels is specified by the * @polarity parameter. * * Returns: 0 on success or a negative error code on failure. */int pwmchip_add_with_polarity(struct pwm_chip *chip, enum pwm_polarity polarity){ struct pwm_device *pwm; unsigned int i; int ret; if (!chip || !chip->dev || !chip->ops || !chip->npwm)// Check the validity of the chip structure return -EINVAL; // Check the validity of the pwm_ops structure if (!pwm_ops_check(chip)) return -EINVAL; // Acquire the global pwm_lock mutex mutex_lock(&pwm_lock); // Allocate a PWM device index number for the chip ret = alloc_pwms(chip->base, chip->npwm); if (ret < 0) goto out; // Dynamically allocate chip->npwm pwm_device structures chip->pwms = kcalloc(chip->npwm, sizeof(*pwm), GFP_KERNEL); if (!chip->pwms) { ret = -ENOMEM; goto out; } // Save the allocated PWM device index number chip->base = ret; // Initialize each PWM device for (i = 0; i < chip->npwm; i++) { pwm = &chip->pwms[i]; pwm->chip = chip; pwm->pwm = chip->base + i; pwm->hwpwm = i; pwm->state.polarity = polarity; pwm->state.output_type = PWM_OUTPUT_FIXED; // Add the PWM device to the global PWM device tree radix_tree_insert(&pwm_tree, pwm->pwm, pwm); } // Mark the allocated PWM device as used. bitmap_set(allocated_pwms, chip->base, chip->npwm); // Add the PWM controller to the global PWM controller linked list. INIT_LIST_HEAD(&chip->list); list_add(&chip->list, &pwm_chips); ret = 0; // If the kernel has Device Tree support enabled, register the PWM controller with the Device Tree. if (IS_ENABLED(CONFIG_OF)) of_pwmchip_add(chip);out: mutex_unlock(&pwm_lock);// Release the global pwm_lock mutex. if (!ret)// If the addition succeeds, export the PWM controller in sysfs. pwmchip_sysfs_export(chip); return ret;}EXPORT_SYMBOL_GPL(pwmchip_add_with_polarity); |
The primary purpose of this function is to initialize a new PWM controller and add it to the global linked list of the PWM subsystem for management.
PWM subsystem API
pwm_config()
Core function for PWM parameter configuration
| Item | Details |
|---|---|
| Function prototype | int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns); |
| Core function | Configure the PWM signal’sDuty cycleandPeriodis the core function for adjusting PWM output characteristics. |
| Parameter description | 1.pwm: pointer to the target PWM device structure (must first be obtained viadevm_of_pwm_getobtained)2. duty_ns: duty cycle duration, unitnanoseconds (ns), indicating the duration of the high level within one PWM cycle3. period_ns: total period of the PWM signal, unitnanoseconds (ns), indicating the total duration of high level + low level |
| Return value | Success: returns 0; failure: returns a negative error code (e.g., -EINVAL indicates invalid parameters) |
| Usage instructions | Must be before PWM is enabled (pwm_enable) before calling, to ensure the output PWM signal parameters are correctly configured; by adjustingduty_nsthe output average voltage can be changed (e.g., controlling LED brightness, motor speed), duty cycle calculation formula:Duty cycle (%) = (duty_ns / period_ns) * 100% |
pwm_set_polarity()
PWM polarity setting function
| Item | Details |
|---|---|
| Function prototype | int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity); |
| Core function | Set the polarity of the PWM signal, defining the logical meaning of high and low levels |
| Parameter description | 1.pwm: pointer to the target PWM device structure2. polarity: polarity enumeration value, only two types are supported:- PWM_POLARITY_NORMAL: positive polarity (default), high level is “active/on” state, low level is “inactive/off” state- PWM_POLARITY_INVERSED: negative polarity, low level is “active/on” state, high level is “inactive/off” state |
| Return value | Success: returns 0; failure: returns a negative error code |
| Usage instructions | Suitable for scenarios requiring reverse logic control (such as certain motor forward/reverse control, LED reverse drive circuits), it is recommended topwm_configafter,pwm_enablecall before |
pwm_enable()
PWM enable function
| Item | Details |
|---|---|
| Function prototype | int pwm_enable(struct pwm_device *pwm); |
| Core function | Enable the PWM controller to start outputting the PWM signal with configured parameters (duty cycle, period, polarity) |
| Parameter description | pwm: pointer to the target PWM device structure |
| Return value | Success: returns 0; failure: returns a negative error code |
| Usage instructions | Must bepwm_config(andpwm_set_polarity, after calling (if polarity needs to be configured), ensure the output signal parameters are valid; after calling, PWM continues to output until callingpwm_disableStop |
pwm_disable()
PWM disable function
| Item | Details |
|---|---|
| Function prototype | void pwm_disable(struct pwm_device *pwm); |
| Core function | Disable the PWM controller and stop outputting PWM signals. |
| Parameter description | pwm: pointer to the target PWM device structure |
| Return value | No return value |
| Usage instructions | Call when PWM output is not needed (e.g., device sleep, function disabled) to save system power; after calling, PWM no longer outputs signals, and to re-enable, call againpwm_enable(no need to reconfigure parameters; parameters are retained) |
devm_of_pwm_get()
PWM device get function
| Item | Details |
|---|---|
| Function prototype | struct pwm_device *devm_of_pwm_get(struct device *dev, struct device_node *np, const char *con_id); |
| Core function | Obtains the PWM device handle from the Linux Device Tree and automatically manages device resources (no manual release required). |
| Parameter description | 1.dev: pointer to the device structure corresponding to the current driver (e.g., in a platform driverstruct deviceinstance)2. np: pointer to the device tree node corresponding to the PWM device3. con_id: connection ID (string type), used to distinguish multiple PWM channels of the same device (if there is only one PWM channel, you can passNULL) |
| Return value | Success: return pointerstruct pwm_devicea valid pointer; on failure: returns an error pointer of type ERR_PTR (which can be checked usingIS_ERR()macro check) |
| Usage instructions | is the preferred method for obtaining PWM devices in device drivers, based ondevmthe characteristics of the series interface; when the device is unloaded, the kernel automatically releases the PWM device resources, avoiding memory leaks; it must be called before using other PWM functions (pwm_configetc.) before calling; obtain the device handle first before subsequent operations. |
Example
First modify the device tree, and similarly set the pwm12 multiplexing first:
1234 | &pwm12 { status = "okay"; pinctrl-0 = <&pwm12m1_pins>;}; |
Then add the sg90 node
1234 | sg90 { compatible = "sg90"; pwms = <&pwm12 0 20000000 1>;} |
compatibleUsed to specify the device’s compatibility string, based on which the kernel matches the appropriate driver.pwmsUsed to configure the PWM controller information.&pwm12Represents a reference to the PWM controller0Represents the PWM channel number20000000Represents the PWM period in nanoseconds, i.e., set the period to 20ms1Indicates polarity, meaning active high, i.e., the high level of the PWM signal represents the ‘on’ state
Driver
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 | dev_t dev_num;struct cdev cdev_test;struct class *class;struct device *device;struct pwm_device *sg90_pwm_device;// Callback function when opening the devicestatic int cdev_test_open(struct inode *inode, struct file *file) { printk("This is cdev test open\n"); pwm_config(sg90_pwm_device, 500000, 20000000); // Configure PWM parameters: pulse width of 500000 nanoseconds, period of 20,000,000 nanoseconds pwm_set_polarity(sg90_pwm_device, PWM_POLARITY_NORMAL); // Set PWM polarity to active high pwm_enable(sg90_pwm_device); // Start PWM return 0;}// Callback function when writing to the devicestatic ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off) { int ret; unsigned char data[1]; printk("This is cdev test write\n"); // Copy data from user space to kernel space ret = copy_from_user(data, buf, size); if (ret) { printk("copy_from_user failed\n"); return -EFAULT; } // Update PWM parameters: adjust the pulse width based on user input data pwm_config(sg90_pwm_device, 500000 + data[0] * 100000 / 9, 20000000); return size;}// Callback function when releasing the devicestatic int cdev_test_release(struct inode *inode, struct file *file) { printk("This is cdev test release\n"); pwm_config(sg90_pwm_device, 500000, 20000000); // Restore the initial PWM parameter configuration pwm_disable(sg90_pwm_device); // Stop PWM return 0;}// Define the character device operation function setstatic struct file_operations cdev_test_ops = { .owner = THIS_MODULE, .open = cdev_test_open, .write = cdev_test_write, .release = cdev_test_release,};// Device probe functionstatic int sg90_probe(struct platform_device *pdev) { int ret; // Get PWM device sg90_pwm_device = devm_pwm_get(&pdev->dev, NULL); if (IS_ERR(sg90_pwm_device)) { printk("Failed to get PWM device\n"); return PTR_ERR(sg90_pwm_device); } // Allocate device number ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name"); if (ret < 0) { printk("alloc_chrdev_region is error\n"); return ret; } printk("alloc_chrdev_region is ok\n"); // Initialize character device cdev_init(&cdev_test, &cdev_test_ops); cdev_test.owner = THIS_MODULE; ret = cdev_add(&cdev_test, dev_num, 1);// Add character device class = class_create(THIS_MODULE, "test"); // Create device class device = device_create(class, NULL, dev_num, NULL, "sg90"); // Create device printk("sg90_probe successful\n"); return 0;}// Device remove functionstatic int sg90_remove(struct platform_device *pdev) { device_destroy(class, dev_num); // Delete device class_destroy(class); // Delete device class cdev_del(&cdev_test); // Delete character device unregister_chrdev_region(dev_num, 1); // Release device number printk("sg90_remove successful\n"); return 0;}// Device tree match tablestatic const struct of_device_id sg90_of_device_id[] = { {.compatible = "sg90"}, {},};MODULE_DEVICE_TABLE(of, sg90_of_device_id);// Define platform driverstatic struct platform_driver sg90_platform_driver = { .driver = { .name = "sg90", .of_match_table = sg90_of_device_id, }, .probe = sg90_probe, .remove = sg90_remove,};// Module initialization functionstatic int __init modulecdev_init(void) { int ret; // Register platform driver ret = platform_driver_register(&sg90_platform_driver); if (ret) { printk("platform_driver_register is error\n"); return ret; } printk("platform_driver_register is ok\n"); return 0;}// Module exit functionstatic void __exit modulecdev_exit(void) { // Unregister platform driver platform_driver_unregister(&sg90_platform_driver); printk("bye bye\n");}// Declare module license and authormodule_init(modulecdev_init);module_exit(modulecdev_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("topeet"); |
app
1234567891011121314151617181920212223242526272829303132333435363738394041 | int main(int argc, char *argv[]) { // argc represents the number of command-line arguments, including the program name itself // argv is a string array that stores the individual command-line arguments int fd; // File descriptor, used to identify the opened device file unsigned char buf[1]; // Store a single byte of data to be written to the device // If the number of command-line arguments is less than 2, it means the value to write is missing; print usage and return an error if (argc < 2) { printf("Usage: %s <value>\n", argv[0]); return -1; } // Open the device file "/dev/sg90" in write-only mode fd = open("/dev/sg90", O_WRONLY); if (fd < 0) { // Failed to open the device file; output an error message and return an error perror("open"); return -1; } // Convert the command-line argument to an integer and store it in buf[0] buf[0] = (unsigned char)atoi(argv[1]); // Write the 1-byte data from buf to the opened device file if (write(fd, buf, 1) != 1) { // Write failed; output an error message, close the file, and return an error perror("write"); close(fd); return -1; } // Delay for 3 seconds to simulate device operation sleep(3); // Close the device file close(fd); return 0; // Program executed successfully} |
Test
use./app 90Run the compiled executable; here 90 represents the angle of the servo rotation
Simulated PWM
When hardware PWM is insufficient, GPIO can be used to simulate PWM, which can also be called software PWM. Software PWM is a method of implementing PWM output through software programming, using timer interrupts or loop counting to control the switching time of the output pin’s high and low levels in software, thereby generating a PWM waveform.
High-precision timer
The implementation of software PWM relies on timer interrupts, andTraditional periodic timers are usually between 100Hz and 1000Hz, providing only millisecond-level precision, for some application scenarios that require high time precision (e.g., analog PWM), the precision is still insufficient.
To solve this problem, the Linux kernel introduced the high-resolution timer (hrtimers) mechanism.
hrtimersUsing a monotonic clock (CLOCK_MONOTONIC) and high-precision timestamps (ktime_t) as the base time, by utilizing underlying hardware timers (such asHPET、LAPICtimers, etc.) to provide nanosecond-level time precision.
High-resolution timer structurestruct hrtimerThe content is as follows:
1234567 | struct hrtimer { struct list_head node; // Node of the timer in the clock base list ktime_t _softexpires;// Soft expiration time; the actual expiration time may be slightly later. enum hrtimer_restart (*function)(struct hrtimer *); // Callback function when the timer expires unsigned long state; // Status flags of the timer struct hrtimer_clock_base *base; // The clock base to which the timer belongs} |
High-resolution timer workflow
Initialize timer
use
hrtimer_init()The function initializes astruct hrtimerobject, specifying the clock source and working mode to be used. During initialization, a clock base (hrtimer_clock_base) is assigned to the timer. Each CPU has its own clock base manager.Setting the timer
usehrtimer_start()The function sets the expiration time of the timer and starts the timer. You can specify an absolute time or a relative time. The timer expiration time is converted to the soft expiration time of the corresponding clock base (_softexpires). The timer is inserted into the timer linked list of the clock base, sorted by expiration time.Timer expiration handling
When a system clock interrupt occurs, it callshrtimer_interrupt()function.hrtimer_interrupt()it will traverse the timer linked list of the clock base to check whether there are expired timers. For expired timers, it calls their callback function (function) to handle the expiration event. If the timer has not expired, update the wake-up time of the next expired timer.Timer Management
hrtimer_cancel()This function is used to cancel a running timer.hrtimer_forward()This function is used to forward the timer, i.e., recalculate the timer’s expiration time based on the current time.hrtimer_start_range_ns()This function is used to set the expiration time range of the timer to improve timer precision.
High-Resolution Timer API
hrtimer_init
Timer Initialization
| Item | Details |
|---|---|
| Function prototype | void hrtimer_init(struct hrtimer *timer, clockid_t clockid, enum hrtimer_mode mode); |
| Core function | Initializestruct hrtimerStructure that specifies the timer’s clock source and working mode. |
| Parameter description | -timer: Points to the to-be-initializedhrtimerstructure pointer- clockid: Clock source, common values:- CLOCK_MONOTONIC: Monotonic clock (does not go backwards after system boot, recommended for timers)- CLOCK_REALTIME: Real-time clock (can be modified, not recommended for high-precision timing)- CLOCK_BOOTTIME: Monotonic clock that includes system suspend time- mode: Working mode, common values:- HRTIMER_MODE_ABS: Absolute time mode (timer triggers at a specified absolute time)- HRTIMER_MODE_REL: Relative time mode (timer triggers after a specified duration from the current time) |
| Return value | None |
| Usage instructions | Must be called before using other hrtimer APIs; it is a prerequisite step for timer operations. |
hrtimer_start
Start the timer
| Item | Details |
|---|---|
| Function prototype | int hrtimer_start(struct hrtimer *timer, ktime_t time, const enum hrtimer_mode mode); |
| Core function | Adds the timer to the kernel timer queue and starts the timer (the expiration time can be set directly). |
| Parameter description | -timer: points tohrtimerPointer to the structure -time: Expiration time (ktime_tType) -mode: time mode (samehrtimer_init) |
| Return value | Returns 0 on success; returns a negative error code on failure. |
| Usage instructions | It is the core function for starting a timer, allowing ‘set time + start’ to be completed in one step. |
hrtimer_forward
Advance the timer expiration time.
| Item | Details |
|---|---|
| Function prototype | ktime_t hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval); |
| Core function | Advance the expiration time in the timer callback function to implementperiodic timer |
| Parameter description | -timer: points tohrtimerpointer to the structure- now: current time (usually obtained usinghrtimer_cb_get_time(timer)obtained)- interval: period interval (ktime_ttype) |
| Return value | Returns the new expiration time (ktime_ttype) |
| Usage instructions | Must be called in the timer callback function, together withreturn HRTIMER_RESTARTto implement periodic triggering |
hrtimer_cancel
Cancel the timer
| Item | Details |
|---|---|
| Function prototype | int hrtimer_cancel(struct hrtimer *timer); |
| Core function | Cancel a started timer and remove it from the kernel queue. |
| Parameter description | -timer: points tohrtimerpointer to the structure |
| Return value | Returns 1 when the timer is active; returns 0 when it is stopped. |
| Usage instructions | Used to terminate the timer early (e.g., when the module is unloaded or the function is disabled) |
example
We use simulated PWM to achieve the LED breathing light effect:

The pin used by the LED isGPIO0_B7, and it is registered in the device tree using the LED subsystem. In this chapter, because we need to use simulated PWM to control this pin, we need to modify the device tree:
Originally:
123456789101112131415161718 | leds { compatible = "pwm-leds"; work { pwms = <&pwm0 0 500000 0>; linux,default-trigger = "heartbeat"; default-state = "on"; }; }; leds { compatible = "gpio-leds"; work { gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>; linux,default-trigger = "heartbeat"; default-state = "on"; }; }; |
Change to:
1234 | leds { compatible = "pwm-leds"; led-gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>;}; |
Driver
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 | dev_t dev_num;struct cdev cdev_test;struct class *class;struct device *device;// Define the PWM LED data structurestruct pwm_led_data { int sum_count; // Total pulse count of the PWM period int high_count; // PWM high-level pulse count struct gpio_desc *gpiod; // GPIO descriptor struct hrtimer pwm_timer; // High-resolution timer ktime_t time; // Timer interval};// Declare a pointer to the PWM LED data structurestruct pwm_led_data *data;// PWM timer callback functionenum hrtimer_restart pwm_timer_func(struct hrtimer *timer) { static int timer_count = 0; // Timer counter struct pwm_led_data *mydata = container_of(timer, struct pwm_led_data, pwm_timer); // If the counter reaches the total pulse count, set the GPIO to high level if (timer_count == mydata->sum_count) { gpiod_set_value(mydata->gpiod, 1); timer_count = 0; } // If the counter reaches the high-level pulse count, set the GPIO to low level if (timer_count == mydata->high_count) { gpiod_set_value(mydata->gpiod, 0); } timer_count++; // If the high-level pulse count is 0, reset the counter to 0 if (mydata->high_count == 0) { timer_count = 0; } // Advance the timer by the time interval and restart it hrtimer_forward(timer, hrtimer_cb_get_time(timer), mydata->time); return HRTIMER_RESTART;}// Character device open callback functionstatic int cdev_test_open(struct inode *inode, struct file *file) { printk("This is cdev test open\n"); return 0;}// Character device write callback functionstatic ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off) { int ret; int kbuf[2]; printk("This is cdev test write\n"); // Copy data from user space to kernel space ret = copy_from_user(kbuf, buf, size); if (ret != 0) { printk("copy_from_user failed\n"); return -EFAULT; } // Update the PWM LED data structure data->sum_count = kbuf[0]; data->high_count = kbuf[1]; return size;}// Character device release callback functionstatic int cdev_test_release(struct inode *inode, struct file *file) { printk("This is cdev test release\n"); return 0;}// Character device operation function setstatic struct file_operations cdev_test_ops = { .owner = THIS_MODULE, .open = cdev_test_open, .write = cdev_test_write, .release = cdev_test_release,};// Platform device probe callback functionstatic int led_probe(struct platform_device *pdev) { int ret; // Allocate memory for PWM LED data structure data = kmalloc(sizeof(struct pwm_led_data), GFP_KERNEL); if (!data) { printk("kmalloc failed\n"); return -ENOMEM; } // Initialize PWM LED data structure data->sum_count = 20; data->high_count = 10; // Dynamically allocate device number ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name"); if (ret < 0) { printk("alloc_chrdev_region is error\n"); kfree(data); return ret; } printk("alloc_chrdev_region is ok\n"); // Initialize character device cdev_init(&cdev_test, &cdev_test_ops); cdev_test.owner = THIS_MODULE; ret = cdev_add(&cdev_test, dev_num, 1); // Register character device with kernel if (ret) { printk("cdev_add is error\n"); unregister_chrdev_region(dev_num, 1); kfree(data); return ret; } class = class_create(THIS_MODULE, "test"); // Create device class if (IS_ERR(class)) { printk("class_create is error\n"); cdev_del(&cdev_test); unregister_chrdev_region(dev_num, 1); kfree(data); return PTR_ERR(class); } device = device_create(class, NULL, dev_num, NULL, "pwm-gpio"); // Create device node if (IS_ERR(device)) { printk("device_create is error\n"); class_destroy(class); cdev_del(&cdev_test); unregister_chrdev_region(dev_num, 1); kfree(data); return PTR_ERR(device); } data->gpiod = gpiod_get(&pdev->dev, "led", GPIOD_OUT_HIGH); // Get GPIO descriptor if (IS_ERR(data->gpiod)) { printk("gpiod_get is error\n"); device_destroy(class, dev_num); class_destroy(class); cdev_del(&cdev_test); unregister_chrdev_region(dev_num, 1); kfree(data); return PTR_ERR(data->gpiod); } gpiod_set_value(data->gpiod, 1); // Set GPIO to high level // Initialize high-resolution timer data->time = ktime_set(0, 1000000); // 1 ms hrtimer_init(&data->pwm_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); data->pwm_timer.function = pwm_timer_func; hrtimer_start(&data->pwm_timer, data->time, HRTIMER_MODE_REL); // Start high-resolution timer printk("led_probe successful\n"); return 0;}// Platform device remove callback functionstatic int led_remove(struct platform_device *pdev) { hrtimer_cancel(&data->pwm_timer); // Stop high-resolution timer gpiod_put(data->gpiod); // Release GPIO descriptor device_destroy(class, dev_num); // Delete device node class_destroy(class); // Delete device class cdev_del(&cdev_test); // Unregister character device from kernel unregister_chrdev_region(dev_num, 1); // Release device number kfree(data); // Free memory of PWM LED data structure printk("led_remove successful\n"); return 0;}// Device tree match tablestatic const struct of_device_id led_of_device_id[] = { {.compatible = "pwm-leds"}, {},};MODULE_DEVICE_TABLE(of, led_of_device_id);// Platform device driver structurestatic struct platform_driver led_platform_driver = { .driver = { .name = "pwm-leds", .of_match_table = led_of_device_id, }, .probe = led_probe, .remove = led_remove,};// Module initialization functionstatic int __init modulecdev_init(void) { int ret; // Register platform device driver ret = platform_driver_register(&led_platform_driver); if (ret) { printk("platform_driver_register is error\n"); return ret; } printk("platform_driver_register is ok\n"); return 0;}// Module exit functionstatic void __exit modulecdev_exit(void) { // Unregister platform device driver platform_driver_unregister(&led_platform_driver); printk("bye bye\n");}// Module initialization and exit function registrationmodule_init(modulecdev_init);module_exit(modulecdev_exit);// Module license, author, and description informationMODULE_LICENSE("GPL");MODULE_AUTHOR("topeet"); |
Test
12345678910111213141516171819202122232425262728293031323334353637383940 | int main(int argc, char *argv[]) { int fd; // file descriptor to be operated on int buf[2]; // Buffer, stores two integer values // Check whether the number of command-line arguments is correct if (argc != 3) { fprintf(stderr, "Usage: %s <sum_count> <high_count>\n", argv[0]); return -1; } // Open the PWM GPIO device file fd = open("/dev/pwm-gpio", O_WRONLY); if (fd < 0) { perror("open error"); return -1; } // Convert command-line arguments to integers and store them in the buffer buf[0] = atoi(argv[1]); buf[1] = atoi(argv[2]); // Write the data in the buffer to the PWM GPIO device if (write(fd, buf, sizeof(buf)) != sizeof(buf)) { perror("write error"); close(fd); return -1; } // Close the device file close(fd); return 0;} |
Breathing light
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 | int main(int argc, char *argv[]) { int fd; int buf[2]; int i; // Check the number of arguments if (argc != 3) { printf("Usage: %s <duty_cycle> <period>\n", argv[0]); return -1; } // Open the /dev/pwm-gpio device fd = open("/dev/pwm-gpio", O_WRONLY); if (fd < 0) { printf("Error opening /dev/pwm-gpio\n"); return -1; } // Set PWM parameters buf[0] = atoi(argv[1]); // Duty cycle buf[1] = atoi(argv[2]); // Period // Enter the loop while (1) { // Increment duty cycle for (i = 0; i <= buf[0]; i++) { buf[1] = i; write(fd, buf, sizeof(buf)); usleep(30000); // Delay 30 milliseconds } // Decrement duty cycle for (i = buf[0]; i > 0; i--) { buf[1] = i; write(fd, buf, sizeof(buf)); usleep(30000); // Delay 30 milliseconds } } // Close the device close(fd); return 0;} |
PWM input capture driver
Rockchip only implemented the status acquisition and application functions, but not the input capture related functions. Since input capture is also a common PWM feature, to implement PWM input capture on the RK3568, we need to implement the PWM input capture driver ourselves.
Device tree modification
In the RK3568 PWM hardware introduction, it is mentioned that only four PWM channels, PWM3, PWM7, PWM11, and PWM15, can perform input capture. We will use PWM3 for the input capture demonstration. By default, PWM3 is used for infrared reception. The specific hardware schematic is as follows:

Modify the device tree, comment out the pwm3 node, and then add:
123456 | &pwm3 { status = "okay"; compatible = "pwm-capture"; pinctrl-names = "default"; pwm-channel = <3>;}; |
Driver
rockchip_pwm_capture.h
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 | /* SPDX-License-Identifier: GPL-2.0 *//* Maximum number of keys *//* Maximum PWM capture count *//* PWM Register Definitions *//* Control Register Bit Definitions *//* Operating Modes *//* Duty Cycle Output Polarity *//* Inactive State Output Polarity *//* Clock source selection *//* Prescaler factor */enum pwm_div { PWM_DIV1 = (0x0 << 12), PWM_DIV2 = (0x1 << 12), PWM_DIV4 = (0x2 << 12), PWM_DIV8 = (0x3 << 12), PWM_DIV16 = (0x4 << 12), PWM_DIV32 = (0x5 << 12), PWM_DIV64 = (0x6 << 12), PWM_DIV128 = (0x7 << 12),};/* NEC protocol *//* PWM data structure */struct pwm_data { int period_ns; /* Period (nanoseconds) */ int duty_ns; /* Duty cycle (nanoseconds) */};/* PWM state enumeration */typedef enum _RMC_STATE { RMC_IDLE, /* Idle state */ RMC_IDLE1, /* Idle state 1 */ RMC_IDLE2, /* Idle state 2 */ RMC_GETDATA,/* Data acquisition state */ RMC_DONE, /* Completion state */} eRMC_STATE;/* PWM capture platform data structure */struct RKxx_remotectl_platform_data { int nbuttons; /* Button count */ int rep; /* Repeat */ int timer; /* Timer */ int wakeup; /* Wakeup */};/* PWM capture character device data structure */struct pwm_capture_cdev { dev_t dev_num; /* Device ID */ struct cdev cdev_test; /* Character device structure */ struct class *class; /* Device class */ struct device *device; /* Device structure */ struct rkxx_capture_drvdata *ddata; /* Driver data */};/* PWM capture driver data structure */struct rkxx_capture_drvdata { void __iomem *base; /* base address */ int irq; /* Interrupt number */ struct device dev; /* Device structure */ int pwm_freq_nstime; /* PWM frequency (nanoseconds) */ int pwm_channel; /* PWM channel */ int hpr; /* High level period */ int lpr; /* Low level period */ eRMC_STATE state; /* PWM status */ struct clk *clk; /* Clock */ struct clk *p_clk; /* Parent clock */ struct pwm_capture_cdev pwm_cdev; /* PWM capture character device */ struct pwm_data data; /* PWM data structure */};/* PWM interrupt control */static void rk_pwm_int_ctrl(void __iomem *pwm_base, uint pwm_id, int ctrl){ int val; if (pwm_id > 3) return; /* If PWM ID exceeds 3, return directly */ val = readl_relaxed(pwm_base + PWM_REG_INT_EN(pwm_id)); /* Read current interrupt enable status */ if (ctrl) { val |= PWM_CH_INT_ENABLE(pwm_id); /* Set interrupt enable */ writel_relaxed(val, pwm_base + PWM_REG_INT_EN(pwm_id)); /* Write interrupt enable register */ } else { val &= ~PWM_CH_INT_ENABLE(pwm_id); /* Clear interrupt enable */ writel_relaxed(val, pwm_base + PWM_REG_INT_EN(pwm_id)); /* Write interrupt enable register */ }}/* Initialize PWM capture */static void rk_pwm_capture_init(void __iomem *pwm_base, uint pwm_id){ int val; /* Disable PWM */ val = readl_relaxed(pwm_base + PWM_REG_CTRL); val = (val & 0xFFFFFFFE) | PWM_DISABLE; writel_relaxed(val, pwm_base + PWM_REG_CTRL); /* Set to capture mode */ val = readl_relaxed(pwm_base + PWM_REG_CTRL); val = (val & 0xFFFFFFF9) | PWM_MODE_CAPTURE; writel_relaxed(val, pwm_base + PWM_REG_CTRL); /* Set divider value */ val = readl_relaxed(pwm_base + PWM_REG_CTRL); val = (val & 0xFF0001FF) | PWM_DIV64; writel_relaxed(val, pwm_base + PWM_REG_CTRL); /* Enable interrupt */ rk_pwm_int_ctrl(pwm_base, pwm_id, PWM_INT_ENABLE); /* Here you can enable PWM capture (commented-out code) */ /* val = readl_relaxed(pwm_base + PWM_REG_CTRL); val = (val & 0xFFFFFFFE) | PWM_ENABLE; writel_relaxed(val, pwm_base + PWM_REG_CTRL); */} |
pwm-rockchip-capture.c
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 | // Callback function for opening the character devicestatic int cdev_test_open(struct inode *inode, struct file *file) { struct rkxx_capture_drvdata *ddata; struct pwm_capture_cdev *pcdev; printk("This is cdev_test_open\n"); // Get device data from inode pcdev = container_of(inode->i_cdev, struct pwm_capture_cdev, cdev_test); ddata = container_of(pcdev, struct rkxx_capture_drvdata, pwm_cdev); if (!ddata) { printk(KERN_ERR "Failed to get device data\n"); return -ENODEV; } // Save device data to file private data file->private_data = ddata; return 0;}// Callback function for reading the character devicestatic ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off) { struct rkxx_capture_drvdata *ddata; int val, i, ret; ddata = file->private_data; if (!ddata) { printk(KERN_ERR "Device data is NULL\n"); return -EINVAL; } // Initialize capture data ddata->lpr = 0; ddata->hpr = 0; ddata->state = RMC_IDLE1; // Enable PWM val = readl_relaxed(ddata->base + PWM_REG_CTRL); val = (val & 0xFFFFFFFE) | PWM_ENABLE; writel_relaxed(val, ddata->base + PWM_REG_CTRL); // Wait for data capture completion for (i = 0; i < 100; i++) { msleep(1); if (ddata->state == RMC_DONE && ddata->hpr && ddata->lpr) { printk("capture ok!\n"); break; } } // Disable PWM val = readl_relaxed(ddata->base + PWM_REG_CTRL); val = (val & 0xFFFFFFFE) | PWM_DISABLE; writel_relaxed(val, ddata->base + PWM_REG_CTRL); if (ddata->hpr == 0 || ddata->lpr == 0) { printk(KERN_ERR "Failed to capture PWM data\n"); return -EIO; } // Calculate period and duty cycle ddata->data.period_ns = (ddata->lpr + ddata->hpr) * ddata->pwm_freq_nstime; ddata->data.duty_ns = ddata->hpr * ddata->pwm_freq_nstime; // Copy data to user space ret = copy_to_user(buf, &ddata->data, size); if (ret) { printk(KERN_ERR "Failed to copy data to user space\n"); return -EFAULT; } printk("This is cdev_test_read\n"); ddata->state = RMC_IDLE; return size;}// Character device release callback functionstatic int cdev_test_release(struct inode *inode, struct file *file) { printk("This is cdev_test_release\n"); return 0;}// Character device operation function structurestatic struct file_operations cdev_test_ops = { .owner = THIS_MODULE, .open = cdev_test_open, .read = cdev_test_read, .release = cdev_test_release};// PWM capture interrupt handler functionirqreturn_t rk_pwm_capture(int irq, void *dev_id) { struct rkxx_capture_drvdata *ddata = dev_id; unsigned int channel = ddata->pwm_channel; int val, lpr, hpr; val = readl_relaxed(ddata->base + PWM_REG_INTSTS(channel)); if ((val & PWM_CH_INT(channel)) == 0) { return IRQ_NONE; } // Read lpr or hpr according to polarity if ((val & PWM_CH_POL(channel)) == 0) { if (ddata->state != RMC_DONE) { lpr = readl_relaxed(ddata->base + PWM_REG_LPR); ddata->lpr = lpr; } } else { if (ddata->state != RMC_DONE) { hpr = readl_relaxed(ddata->base + PWM_REG_HPR); ddata->hpr = hpr; } } // Clear interrupt status writel_relaxed(PWM_CH_INT(channel), ddata->base + PWM_REG_INTSTS(channel)); // State machine processing switch (ddata->state) { case RMC_IDLE1: ddata->hpr = 0; ddata->lpr = 0; ddata->state = RMC_IDLE2; break; case RMC_IDLE2: ddata->hpr = 0; ddata->lpr = 0; ddata->state = RMC_GETDATA; break; case RMC_GETDATA: printk("ddata->hpr is %d, ddata->lpr is %d\n", ddata->hpr, ddata->lpr); if (ddata->hpr && ddata->lpr) { ddata->state = RMC_DONE; } break; default: break; } return IRQ_HANDLED;}// Driver probe functionint capture_probe(struct platform_device *pdev){ int ret; struct rkxx_capture_drvdata *ddata; struct resource *r; struct clk *clk; struct clk *p_clk; struct device_node *np = pdev->dev.of_node; int pwm_channel; int irq; struct pwm_capture_cdev *pcdev; int freq; // Allocate driver data structure ddata = devm_kzalloc(&pdev->dev, sizeof(struct rkxx_capture_drvdata), GFP_KERNEL); if (!ddata) { dev_err(&pdev->dev, "Failed to allocate memory for driver data\n"); return -ENOMEM; } ddata->state = RMC_IDLE; // Get resources r = platform_get_resource(pdev, IORESOURCE_MEM, 0); ddata->base = devm_ioremap_resource(&pdev->dev, r); if (IS_ERR(ddata->base)) { dev_err(&pdev->dev, "Failed to map memory resource\n"); return PTR_ERR(ddata->base); } ddata->dev = pdev->dev; // Get clock clk = devm_clk_get(&pdev->dev, "pwm"); if (IS_ERR(clk)) { dev_err(&pdev->dev, "Failed to get PWM clock\n"); return PTR_ERR(clk); } ddata->clk = clk; p_clk = devm_clk_get(&pdev->dev, "pclk"); if (IS_ERR(p_clk)) { dev_err(&pdev->dev, "Failed to get peripheral clock\n"); return PTR_ERR(p_clk); } ddata->p_clk = p_clk; // Read PWM channel from device tree ret = of_property_read_u32(np, "pwm-channel", &pwm_channel); if (ret) { dev_err(&pdev->dev, "Failed to get PWM channel from device tree\n"); return ret; } pwm_channel %= 4; ddata->pwm_channel = pwm_channel; // Get interrupt number irq = platform_get_irq(pdev, 0); if (irq < 0) { dev_err(&pdev->dev, "Failed to get IRQ\n"); return irq; } ddata->irq = irq; // Set driver data platform_set_drvdata(pdev, ddata); // Request interrupt ret = devm_request_irq(&pdev->dev, irq, rk_pwm_capture, IRQF_NO_SUSPEND, "rk_pwm_capture_irq", ddata); if (ret) { dev_err(&pdev->dev, "Failed to request IRQ\n"); return ret; } // Enable clock ret = clk_prepare_enable(ddata->clk); if (ret) { dev_err(&pdev->dev, "Failed to enable PWM clock\n"); return ret; } ret = clk_prepare_enable(ddata->p_clk); if (ret) { clk_disable_unprepare(ddata->clk); dev_err(&pdev->dev, "Failed to enable peripheral clock\n"); return ret; } // Calculate PWM frequency freq = clk_get_rate(ddata->clk) / 64; ddata->pwm_freq_nstime = 1000000000 / freq; // Register character device pcdev = &ddata->pwm_cdev; ret = alloc_chrdev_region(&pcdev->dev_num, 0, 1, "alloc_name"); if (ret < 0) { dev_err(&pdev->dev, "alloc_chrdev_region error\n"); goto err_alloc_chrdev; } printk("alloc_chrdev_region success\n"); pcdev->cdev_test.owner = THIS_MODULE; cdev_init(&pcdev->cdev_test, &cdev_test_ops); ret = cdev_add(&pcdev->cdev_test, pcdev->dev_num, 1); if (ret) { dev_err(&pdev->dev, "Failed to add cdev\n"); goto err_cdev_add; } // Create device class pcdev->class = class_create(THIS_MODULE, "test"); if (IS_ERR(pcdev->class)) { ret = PTR_ERR(pcdev->class); dev_err(&pdev->dev, "Failed to create class\n"); goto err_class_create; } // Create device pcdev->device = device_create(pcdev->class, NULL, pcdev->dev_num, NULL, "capture"); if (IS_ERR(pcdev->device)) { ret = PTR_ERR(pcdev->device); dev_err(&pdev->dev, "Failed to create device\n"); goto err_device_create; } rk_pwm_capture_init(ddata->base, ddata->pwm_channel); return 0;err_device_create: class_destroy(pcdev->class);err_class_create: cdev_del(&pcdev->cdev_test);err_cdev_add: unregister_chrdev_region(pcdev->dev_num, 1);err_alloc_chrdev: clk_disable_unprepare(ddata->p_clk); clk_disable_unprepare(ddata->clk); return ret;}// Driver remove functionint capture_remove(struct platform_device *pdev){ struct rkxx_capture_drvdata *ddata = platform_get_drvdata(pdev); struct pwm_capture_cdev *pcdev = &ddata->pwm_cdev; device_destroy(pcdev->class, pcdev->dev_num); class_destroy(pcdev->class); cdev_del(&pcdev->cdev_test); unregister_chrdev_region(pcdev->dev_num, 1); clk_disable_unprepare(ddata->p_clk); clk_disable_unprepare(ddata->clk); return 0;}// Device tree match tableconst struct of_device_id capture_of_device_id[] = { {.compatible = "pwm-capture"}, {}};// Platform driver structurestruct platform_driver capture_platform_driver = { .driver = { .name = "pwm-capture", .of_match_table = capture_of_device_id, }, .probe = capture_probe, .remove = capture_remove,};// Module initialization functionstatic int __init modulecdev_init(void){ return platform_driver_register(&capture_platform_driver);}// Module exit functionstatic void __exit modulecdev_exit(void){ platform_driver_unregister(&capture_platform_driver);}module_init(modulecdev_init);module_exit(modulecdev_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("topeet"); |
Test code:
12345678910111213141516171819202122232425262728293031323334353637383940414243 | // Use fixed-width types to avoid ABI issues.struct __attribute__((packed)) pwm_data { int32_t period_ns; int32_t duty_ns;};int main(int argc, char *argv[]) { const char *dev_path = "/dev/capture"; int fd = open(dev_path, O_RDONLY); if (fd < 0) { fprintf(stderr, "无法打开设备 %s: %s\n", dev_path, strerror(errno)); return EXIT_FAILURE; } struct pwm_data data; ssize_t n = read(fd, &data, sizeof(data)); if (n < 0) { fprintf(stderr, "读取设备失败: %s\n", strerror(errno)); close(fd); return EXIT_FAILURE; } if (n != sizeof(data)) { fprintf(stderr, "读取数据不完整: 期望 %zu 字节,实际 %zd 字节\n", sizeof(data), n); close(fd); return EXIT_FAILURE; } printf("period_ns = %d ns, duty_ns = %d ns\n", data.period_ns, data.duty_ns); printf("占空比 = %.2f%%\n", (data.period_ns > 0) ? (100.0 * data.duty_ns / data.period_ns) : 0.0); close(fd); return EXIT_SUCCESS;} |
We need another PWM as the input signal, connect it to PWM3, and let it capture.

