Cover image for Linux PWM

Linux PWM


Timeline

Timeline

2026-01-04

init

This article introduces the basic concepts of Linux PWM (Pulse Width Modulation), including period, duty cycle, and the area equivalence principle, and discusses in detail the characteristics, advantages and disadvantages, and pin multiplexing of the hardware PWM interface on the iTOP-RK3568 platform.

Linux Driver Notes

Table of ContentsLinks
1. Linux Driver Framework
2. Linux Driver Loading Logic
3. Character Device Basics
4. Concurrency and Competition
5. Advanced Character Device Progression
6. Interrupts
7. Platform Bus
8. Device Tree
9. Device Model
10. Hot Plug
11. pinctrl Subsystem
12. gpio Subsystem
13. Input Subsystem
14. Single Bus
15. I2C
16. SPI
17. UART
18. PWM
19. RTC
20. Watchdog
21. CAN
22. Network Device
23. ADC
24. IIO
25. USB
26. LCD

PWM Basics

PWMFull name isPulse Width Modulation, translated 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. The PWM signal can be described by two units: period and duty cycle. The specific descriptions of the two units are as follows:

  • Period (T): Refers to the time required for a complete high and low level cycle, while frequency is the reciprocal of the period, indicating how many cycles occur in 1 second, with the unit being Hz. For example, if a 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%.

PWM
PWM

Area Equivalence Principle

The area equivalence principle states that even if the width and amplitude of pulse signals differ, as long as their area (pulse width × pulse amplitude) is the same,the response of the inertial elementis also essentially the same, as shown in the figure below

Area Equivalence Principle
Area Equivalence Principle

For example: The RK3568 IO port has a high level of 3.3V and a low level of 0V. If we want to output different analog voltages, we can use the PWM area equivalence principle technology 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: controlling motor speed and torque.
  • Lighting control: controlling 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, allowing flexible response to interrupt events.

  • The PWM channels use a programmable prescaler, which can first divide the bus clock and then perform a secondary division.
  • The PWM channel has a built-in 32-bit timer/counter function.
  • The PWM channel supports capture mode, which can capture information such as the pulse width of external input signals.
  • The PWM channel supports continuous output mode and one-shot output mode.
  • The PWM channel provides a reference mode, capable of outputting 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 PWMs, meaning PWM output is implemented by a dedicated PWM hardware module. The introduction to hardware PWM is as follows:

Hardware PWM:

  1. Implementation method: Hardware PWM is a method where PWM output is implemented by a dedicated PWM hardware module.
  2. 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.
    • Output waveform is stable and reliable, not easily affected by CPU load.
  3. Disadvantages:
    • Requires a dedicated hardware PWM module, resulting in higher cost.
    • PWM output pins are limited and can only output on predefined pins.

The iTOP-RK3568 hardware PWM pin multiplexing function table is shown below. PWM can be selected according to this table:

PWM Interfacepinctrl functionNet LabelCorresponding GPIOFunction
PWM0PWM0_M0Working_LEDEN_H_GPIO0_B7GPIO0_B7LED Control Pin
PWM0_M1LCD0_PWREN_H_GPIO0_C7GPIO0_C7LVDS Screen Power Enable Pin
PWM1PWM1_M0VGA_HPDIN_GPIO0_C0GPIO0_C0VGA Hot Plug Detection Pin
PWM1_M1PCIE20_WAKEN_M0GPIO0_B5PCIE2.0 Wake-up
PWM2PWM2_M0PCIE20_PERSTn_GPIO0_C1GPIO0_C1PCIE2.0 Reset Pin
PWM2_M1TP_RST_L_GPIO0_B6GPIO0_B6MIPI Touch Screen Reset Pin
PWM3_IRPWM3_IRPWM3_IRGPIO0_C2Infrared Receiver
PWM4PWM4LCD0_BL_PWM4GPIO0_C3LVDS Screen Backlight
PWM5PWM5LCD1_BL_PWM5GPIO0_C4MIPI Screen Backlight
PWM6PWM6PWM_FANGPIO0_C5Cooling fan speed adjustment
PWM7_IRPWM7_IRRS485_DIR_GPIO0_C6GPIO0_C6485 transceiver control pin
PWM8PWM8_M0UART4_RX_M1GPIO3_B1Serial port 4 receive pin
PWM8_M1SDMWC0_D0GPIO1_D5SD card data line
PWM9PWM9_M0UART4_TX_M1GPIO3_B2Serial port 4 transmit pin
PWM9_M1SDMWC0_D1GPIO1_D5SD card data line
PWM10PWM10_M0GPIO3_B5GPIO3_B5Not used
PWM10_M1SDMWC0_CMDGPIO2_A1SD card CMD pin
PWM11PWM11_IR_M0GPIO3_B6GPIO3_B6Not used
PWM11_IR_M1CIF_CLKOUTGPIO4_C0Not used
PWM12PWM12_M0GMAC0_RSTn_GPIO3_B7GPIO3_B7Network card 0 reset pin
PWM12_M1GPIO4_C5GPIO4_C5Serial port 9 transmit pin
PWM13PWM13_M0GMAC0_INT/PMEB_GPIO3_C0GPIO3_C0Network card 0 interrupt pin
PWM13_M1GPIO4_C6GPIO4_C6Serial port 9 receive pin
PWM14PWM14_M0UART7_TX_M1GPIO3_C4Serial port 7 transmit pin
PWM14_M14G_DISABLE_GPIO4_C2GPIO4_C2CAN1 RX
PWM15PWM15_IR_M0UART7_RX_M1GPIO3_C5UART 7 receive pin
PWM15_IR_M1HDMI_RX_INT_L_GPIO4_C3GPIO4_C3CAN1 TX

When hardware PWM is insufficient, GPIO can be used to simulate PWM, also known as software PWM. The introduction to software PWM is as follows:

Software PWM:

  1. Implementation method: Software PWM is a method of achieving PWM output through software programming. By using timer interrupts or loop counting, it controls the switching time of high and low levels on the output pin in software, thereby generating PWM waveforms.
  2. Advantages:
    • High flexibility, can generate PWM waveforms on any GPIO pin.
    • Low cost, no additional hardware PWM module required.
  3. Disadvantages:
    • High CPU usage, as it requires real-time control of pin levels in interrupt service routines or loops.
    • The frequency and resolution of PWM output are affected by CPU clock speed and interrupt response time, making high frequency and high resolution unattainable.
    • High demands on 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 subsystem framework
PWM subsystem framework

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 its associated PWM controller.
  • 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, and controlling the device node, applications can interact with the PWM device
    • 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
    Throughstruct pwm_deviceuniformly describe PWM device information, such as device name and associated controller. Provide a standardized PWM device access API for upper-layer applications.
  • PWM Parameter Configuration
    Providepwm_configinterface for configuring PWM device working parameters, such as period, duty cycle, polarity, etc. Convert upper-layer configuration parameters into a form understandable by the underlying hardware and set them through the adapter driver.
  • PWM Output Control
    Providepwm_enableandpwm_disableinterfaces for controlling the start and stop 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: Controls 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 PWM core layer. It is responsible for abstracting and encapsulating the underlying PWM hardware, adapting to different types of PWM devices, providing standardized hardware control interfaces to the upper layer, and also handling hardware-related tasks such as initialization, state management, and power management.

Hardware Abstraction

The PWM adapter driver layer is responsible for abstracting and encapsulating the underlying PWM hardware devices. It defines a set of standardizedpwm_chipinterfaces, including functions such as device initialization, parameter configuration, and output control.

The upper PWM core layer can indirectly control the underlying PWM hardware by calling these standard interfaces, without needing to focus on specific hardware implementation details.

Hardware Adaptation

The PWM adapter driver needs to implement the functions defined in the pwm_chip interface to adapt to the specific working methods of the underlying PWM hardware. These adaptation functions convert the standardized requests from the upper 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 adapter driver is responsible for completing the initialization of PWM hardware devices, including the configuration and initialization of related resources such as GPIO, clocks, and power.

When registering a PWM device, the adapter driver performs initialization operations to ensure 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 PWM core layer to ensure the core layer can correctly control and manage the PWM device.

This concludes the explanation of the PWM subsystem framework. In the next chapter, we will discuss how to operate PWM using 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 pin as a PWM function. The selected IO pin is pin 6 of the iTOP-RK3568 backplane 20-pin GPIO interface, as shown in the following figure:

Hardware Connection
Hardware Connection

According to the PWM pin multiplexing table, the PWM12_M1 function pin is multiplexed as UART 9 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.

1
2
3
4
&pwm12 {
status = "okay";
pinctrl-0 = <&pwm12m1_pins>;
};

/sys/class/pwm/

First, navigate to the/sys/class/pwm/directory, as shown in the following figure:

/sys/class/pwm/
/sys/class/pwm/

The correspondence is as follows:

pwm
pwm

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

pwmchip3
pwmchip3

In this directory, there are a total of seven files, which aredeviceexportnpwmpowersubsystemueventandunexport. The three attribute files that need attention are export, npwm, and unexport.

  • npwm: It is a read-only attribute. Reading this file reveals how many PWM outputs are available under this PWM controller, as shown below:

npwm
npwm

  • export: Before using PWM, export it through the export attribute, as shown below:

export
export

  • unexport: After using PWM, the exported PWM needs to be deleted, for example:

unexport
unexport

Note that the export and unexport files are write-only and have no read permission.

Use the following command again to export the pwm0 directory. After successful export, enter the folder as shown in the figure below:

/sys/class/pwm/pwmchip3/pwm0
/sys/class/pwm/pwmchip3/pwm0

  • polarity: Used to view PWM polarity, with normal and inversed options
    • normal indicates that the PWM signal is positive polarity, meaning high level is the active signal
    • inversed indicates that the PWM signal is negative polarity, meaning 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 DurationRotation Angle
0.5 ms
1.0 ms45°
1.5 ms90°
2.0 ms135°
2.5 ms180°

Hardware Connection

Cable ColorFunctionBackplane 20-pin Pin NumberNet Label
Red WirePower Positive (5V)10/12VCC5V0_SYS
Brown WirePower Negative (GND)19/20GND
Orange WireControl Signal Line (PWM Signal)6UART9_TX_M1(GPIO4_C5)

Application Scenarios

  • Robots: Used for joints or other movable parts in robots, as its precise angle control makes it suitable for achieving 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:

1
2
3
4
5
6
7
8
# Set the PWM period to 20 ms
echo 20000000 > period
# Set the PWM high-level duration to 2 ms
echo 2000000 > duty_cycle
# Set the PWM polarity to normal
echo normal > polarity
# Enable PWM output
echo 1 > enable

PWM Controller Registration Process Analysis

In the rk3568.dtsi device tree, the PWM-related nodes are as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwm0: pwm@fdd70000 {
compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm";
reg = <0x0 0xfdd70000 0x0 0x10>;
#pwm-cells = <3>;
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>;
#pwm-cells = <3>;
pinctrl-names = "active";
pinctrl-0 = <&pwm1m0_pins>;
clocks = <&pmucru CLK_PWM0>, <&pmucru PCLK_PWM0>;
clock-names = "pwm", "pclk";
status = "disabled";
};

Searching 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

1
2
3
4
5
6
7
8
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, the specific content is as follows:

rockchip_pwm_probe()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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 Resource and Map IO Address
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 Pin Controller Active State
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 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 if 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)// Disable bus clock if PWM is not enabled
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. The details are as follows:

struct rockchip_pwm_chip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 multiplexing controller of the PWM controller
struct pinctrl_state *active_state;// Current pin state 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 single-shot trigger mode
int channel_id;
int irq;
};

The second line of whichstruct pwm_chipstructure 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 PWM framework. This structure is defined in theinclude/linux/pwm.hfile, and its specific content is as follows:

struct pwm_chip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 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;// Device to which the PWM controller belongs
const struct pwm_ops *ops;// Operation function set of the PWM controller
int base;// Register base address offset of the PWM controller
unsigned int npwm;// 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;// Number of attributes required for PWM property description in the device tree

/* only used internally by the PWM framework */
struct list_head list;// Global linked list where the PWM controller resides
struct pwm_device *pwms;// Array of PWM channel devices
};

Among thempwm_opsrepresents the operation function set of the PWM controller

struct pwm_ops

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 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.cfound in the filerockchip_pwm_opsoperation set, it can be seen that Rockchip only implements two functions: status acquisition and request, as shown below

1
2
3
4
5
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()called inpwmchip_addfunction adds the PWM controller to the PWM subsystem,pwmchip_addfunction defined indrivers/pwm/core.cfile, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
* 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 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
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 is successful, export the PWM controller in sysfs
pwmchip_sysfs_export(chip);

return ret;
}
EXPORT_SYMBOL_GPL(pwmchip_add_with_polarity);

The main function of this function is to initialize a new PWM controller and calllist_addfunction to add it to the management of the PWM subsystem.

PWM Subsystem API

pwm_config()

PWM Parameter Configuration Core Function

ItemDetails
Function Prototypeint pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);
Core FunctionConfigure the PWM signal’sduty cycleandperiod, is the core function for adjusting PWM output characteristics
Parameter Description1.pwm: Pointer to the target PWM device structure (must first be obtained viadevm_of_pwm_getto obtain)
2.duty_ns: Duty cycle duration, unitnanosecond (ns), indicating the duration of the high level within one PWM cycle
3.period_ns: Total period of the PWM signal, unitnanosecond (ns), indicating the total duration of high level + low level
Return ValueSuccess: returns 0; Failure: returns a negative error code (e.g., -EINVAL indicates invalid parameter)
Usage InstructionsMust be called after PWM enable (pwm_enable) before calling, 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_ns / period_ns) * 100%

pwm_set_polarity()

PWM Polarity Setting Function

ItemDetails
Function Prototypeint pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity);
Core FunctionSet the polarity of the PWM signal, defining the logical meaning of high and low levels
Parameter Description1.pwm: Pointer to the target PWM device structure
2.polarity: Polarity enumeration value, only two types 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 ValueSuccess: returns 0; failure: returns negative error code
InstructionsSuitable for scenarios requiring reverse logic control (e.g., certain motor forward/reverse control, LED reverse drive circuits). It is recommended to callpwm_configafterpwm_enablebefore

pwm_enable()

the PWM enable function

ItemDetails
Function Prototypeint pwm_enable(struct pwm_device *pwm);
Core FunctionEnable the PWM controller to start outputting the PWM signal with configured parameters (duty cycle, period, polarity)
Parameter Descriptionpwm: Pointer to the target PWM device structure
Return ValueSuccess: returns 0; Failure: returns a negative error code
Instructionsmust bepwm_config(andpwm_set_polarity, if polarity needs to be configured) to ensure the output signal parameters are valid; after calling, PWM continues to output until callingpwm_disablestop

pwm_disable()

PWM shutdown function

ItemDetails
Function Prototypevoid pwm_disable(struct pwm_device *pwm);
Core FunctionShuts down the PWM controller and stops outputting PWM signals
Parameter Descriptionpwm: Pointer to the target PWM device structure
Return ValueNo return value
Usage InstructionsCall when PWM output is not needed (e.g., device sleep, function disabled) to save system power; after calling, PWM stops outputting signals, and re-enabling requires calling again.pwm_enable(No need to reconfigure parameters; parameters are retained.)

devm_of_pwm_get()

PWM Device Acquisition Function

ItemDetails
Function Prototypestruct pwm_device *devm_of_pwm_get(struct device *dev, struct device_node *np, const char *con_id);
Core FunctionObtains the PWM device handle from the Linux Device Tree, automatically managing device resources (no manual release required).
Parameter Description1.dev: Pointer to the device structure corresponding to the current driver (e.g., in platform driverstruct deviceinstance)
2.np: Pointer to the device tree node corresponding to the PWM device
3.con_id: Connection ID (string type), used to distinguish multiple PWM channels of the same device (if there is only one PWM channel, passNULL
Return ValueSuccess: Returns a pointer tostruct pwm_devicevalid pointer; on failure: returns an error pointer of type ERR_PTR (can be determined viaIS_ERR()macro)
Usage Instructionsis the preferred method for obtaining a PWM device in device drivers, based on thedevmcharacteristics of the series interface; when the device is unloaded, the kernel automatically releases the PWM device resources to avoid memory leaks; must be called before using other PWM functions (pwm_configetc.) to first obtain the device handle for subsequent operations

Example

First, modify the device tree, similarly set pwm12 multiplexing first:

1
2
3
4
&pwm12 {
status = "okay";
pinctrl-0 = <&pwm12m1_pins>;
};

Then add the sg90 node

1
2
3
4
sg90 {
compatible = "sg90";
pwms = <&pwm12 0 20000000 1>;
}
  • compatibleis used to specify the device’s compatible string; the kernel matches the appropriate driver based on this string.
  • pwmsis used to configure the PWM controller information.
    • &pwm12indicates the reference to the PWM controller
    • 0indicates the PWM channel number
    • 20000000: indicates the PWM period in nanoseconds, i.e., set the period to 20ms
    • 1is the polarity, indicating active high, meaning the high level of the PWM signal represents the ‘on’ state

Driver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/uaccess.h>
#include <linux/pwm.h>

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 device
static 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 500,000 nanoseconds, period of 20,000,000 nanoseconds
pwm_set_polarity(sg90_pwm_device, PWM_POLARITY_NORMAL); // Set PWM polarity to normal polarity
pwm_enable(sg90_pwm_device); // Start PWM
return 0;
}

// Callback function when writing to the device
static 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: pulse width adjusted based on user input data
pwm_config(sg90_pwm_device, 500000 + data[0] * 100000 / 9, 20000000);
return size;
}

// Callback function when releasing the device
static int cdev_test_release(struct inode *inode, struct file *file) {
printk("This is cdev test release\n");

pwm_config(sg90_pwm_device, 500000, 20000000); // Revert to initial PWM parameter configuration
pwm_disable(sg90_pwm_device); // Stop PWM
return 0;
}

// Define character device operation function set
static struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.write = cdev_test_write,
.release = cdev_test_release,
};

// Device probe function
static 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);
}

// Request 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 removal function
static 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 table
static const struct of_device_id sg90_of_device_id[] = {
{.compatible = "sg90"},
{},
};
MODULE_DEVICE_TABLE(of, sg90_of_device_id);

// Define platform driver
static struct platform_driver sg90_platform_driver = {
.driver = {
.name = "sg90",
.of_match_table = sg90_of_device_id,
},
.probe = sg90_probe,
.remove = sg90_remove,
};

// Module initialization function
static 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 function
static void __exit modulecdev_exit(void) {
// Unregister platform driver
platform_driver_unregister(&sg90_platform_driver);
printk("bye bye\n");
}

// Declare module license and author
module_init(modulecdev_init);
module_exit(modulecdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

app

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <fcntl.h>  
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
// argc indicates the number of command-line arguments, including the program name itself
// argv is a string array that stores each command-line argument

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 indicates 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 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 1 byte of data from buf to the opened device file
if (write(fd, buf, 1) != 1) { // Write failed; output 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 program, where 90 represents the rotation angle of the servo.

Simulated PWM

When hardware PWM is insufficient, GPIO can be used to simulate PWM, also known as 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 high and low levels on the output pin in software, thereby generating a PWM waveform.

High-resolution timer

The implementation of software PWM relies on timer interrupts, and**Traditional periodic timers typically range from 100Hz to 1000Hz, providing only millisecond-level precision.**This is insufficient for applications requiring high time precision (such as simulated PWM).

To address this issue, the Linux kernel introduced the high-resolution timer (hrtimers) mechanism.

hrtimersIt uses dynamic clocks (CLOCK_MONOTONIC) and dynamic timestamps (ktime_t) as the reference time. By leveraging underlying hardware timers (such asHPET、LAPICtimers, etc.), it provides nanosecond-level time precision.

High-resolution timer structurestruct hrtimerThe content is as follows:

1
2
3
4
5
6
7
struct hrtimer {
struct list_head node; // Node of the timer in the clock base list
ktime_t _softexpires;// Soft expiration time; 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; // Clock base to which the timer belongs
}

High-resolution timer workflow

  1. Initialize the timer

Use thehrtimer_init()function to initialize astruct hrtimerobject, specifying the clock source and working mode. During initialization, a clock base (hrtimer_clock_base) is assigned to the timer. Each CPU has its own clock base manager.

  1. Set the timer
    Use thehrtimer_start()orhrtimer_set()The function sets the expiration time of the timer. An absolute time or a relative time can be specified. 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.

  2. Timer Expiration Handling
    When a system clock interrupt occurs, thehrtimer_interrupt()function is called.hrtimer_interrupt()It traverses the timer linked list of the clock base to check for expired timers. For expired timers, it calls their callback function (function) to handle the expiration event. If the timer has not expired, it updates the wake-up time of the next expired timer.

  3. Timer Management

    • hrtimer_cancel()The function is used to cancel a running timer.
    • hrtimer_forward()The function is used to roll forward the timer, i.e., recalculate the timer’s expiration time based on the current time.
    • hrtimer_start_range_ns()The function is used to set the expiration time range of the timer to improve timer precision.

High-Resolution Timer API

hrtimer_init

Timer Initialization

ItemDetails
Function Prototypevoid hrtimer_init(struct hrtimer *timer, clockid_t clockid, enum hrtimer_mode mode);
Core FunctionInitializationstruct hrtimerStructure, specifying the timer’s clock source and working mode
Parameter Description-timer: Pointer to thehrtimerstructure pointer to be initialized
-clockid: Clock source, common values:
-CLOCK_MONOTONIC: Monotonic clock (does not go back after system startup, recommended for timers)
-CLOCK_REALTIME: Real-time clock (can be modified, not recommended for high-precision timing)
-CLOCK_BOOTTIME: Monotonic clock including system suspend time
-mode: Working mode, common values:
-HRTIMER_MODE_ABS: Absolute time mode (timer triggers at specified absolute time)
-HRTIMER_MODE_REL: Relative time mode (timer triggers after a specified delay from current time)
Return ValueNone
InstructionsMust be called before using other hrtimer APIs; it is a prerequisite for timer operations.

hrtimer_set

Set timer expiration time

ItemDetails
Function prototypevoid hrtimer_set(struct hrtimer *timer, ktime_t time, const enum hrtimer_mode mode);
Core functionSets the expiration time for an initialized timer (usually used withhrtimer_start)
Parameter description-timer: Pointer tohrtimerstructure pointer
-time: Expiration time (ktime_ttype, must usektime_set(秒, 纳秒)Construction)
-mode: Time mode (HRTIMER_MODE_ABS/HRTIMER_MODE_REL, must be consistent withhrtimer_initthe pattern of)
Return valueNone
InstructionsCan set the time separately before callinghrtimer_start, or directly throughhrtimer_startthe parameters to set the time (more common)

hrtimer_start

Start the timer

ItemDetails
Function prototypeint hrtimer_start(struct hrtimer *timer, ktime_t time, const enum hrtimer_mode mode);
Core functionAdd the timer to the kernel timer queue and start timing (the expiration time can be set directly)
Parameter Description-timer: Pointer tohrtimerstructure pointer -time: Expiration time (ktime_ttype) -mode: Time mode (same ashrtimer_init
Return ValueReturns 0 on success; returns a negative error code on failure
Usage Instructionsis the core function for starting a timer, supporting the one-step completion of “set time + start” (replacinghrtimer_set+hrtimer_start

hrtimer_forward

Advance timer expiration time

ItemDetails
Function Prototypektime_t hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);
Core FunctionAdvance the expiration time in the timer callback function to achieveperiodic timer
Parameter Description-timer: pointer tohrtimerstructure pointer
-now: current time (usually obtained withhrtimer_cb_get_time(timer))
-interval: period time interval (ktime_ttype)
Return ValueReturns the new expiration time (ktime_ttype)
Usage InstructionsMust be called in the timer callback function, in conjunction withreturn HRTIMER_RESTARTImplement periodic triggering

hrtimer_cancel

Cancel timer

ProjectDetails
Function prototypeint hrtimer_cancel(struct hrtimer *timer);
Core functionCancel the started timer and remove it from the kernel queue
Parameter description-timer: Pointer tohrtimerstructure pointer
Return valueReturns 1 if the timer is active; returns 0 if it has been stopped
Usage instructionsUsed to terminate the timer early (e.g., when unloading a module or disabling a function)

Example

We use simulated PWM to achieve the LED breathing light effect:

LED schematic
LED schematic

The pin used by the LED isGPIO0_B7, which is registered in the device tree using the LED subsystem. In this chapter, since we need to control this pin with simulated PWM, the device tree must be modified:

Originally:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#if LED_PWM
leds { compatible = "pwm-leds";
work {
pwms = <&pwm0 0 500000 0>;
linux,default-trigger = "heartbeat";
default-state = "on";
};
};
#else
leds {
compatible = "gpio-leds";
work {
gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
default-state = "on";
};
};
#endif

Changed to:

1
2
3
4
leds {
compatible = "pwm-leds";
led-gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>;
};

Driver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/uaccess.h>
#include <linux/pwm.h>
#include <linux/gpio/consumer.h>
#include <linux/hrtimer.h>
#include <linux/slab.h>

dev_t dev_num;
struct cdev cdev_test;
struct class *class;
struct device *device;

// Define the PWM LED data structure
struct pwm_led_data {
int sum_count; // Total pulse count of the PWM period
int high_count; // Pulse count for PWM high level duration
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 structure
struct pwm_led_data *data;

// PWM timer callback function
enum 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 number of pulses, set GPIO to high level
if (timer_count == mydata->sum_count) {
gpiod_set_value(mydata->gpiod, 1);
timer_count = 0;
}
// If the counter reaches the number of high-level duration pulses, set GPIO to low level
if (timer_count == mydata->high_count) {
gpiod_set_value(mydata->gpiod, 0);
}
timer_count++;
// If the number of high-level duration pulses is 0, reset the counter to 0
if (mydata->high_count == 0) {
timer_count = 0;
}

// Move the timer forward by the time interval and restart
hrtimer_forward(timer, hrtimer_cb_get_time(timer), mydata->time);
return HRTIMER_RESTART;
}

// Character device open callback function
static int cdev_test_open(struct inode *inode, struct file *file) {
printk("This is cdev test open\n");
return 0;
}

// Character device write callback function
static 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 PWM LED data structure
data->sum_count = kbuf[0];
data->high_count = kbuf[1];
return size;
}

// Character device release callback function
static int cdev_test_release(struct inode *inode, struct file *file) {
printk("This is cdev test release\n");
return 0;
}

// Character device operation function set
static 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 function
static 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 function
static 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 PWM LED data structure memory
printk("led_remove successful\n");
return 0;
}

// Device tree match table
static const struct of_device_id led_of_device_id[] = {
{.compatible = "pwm-leds"},
{},
};
MODULE_DEVICE_TABLE(of, led_of_device_id);

// Platform device driver structure
static 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 function
static 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 function
static void __exit modulecdev_exit(void) {
// Unregister platform device driver
platform_driver_unregister(&led_platform_driver);
printk("bye bye\n");
}

// Module initialization and exit function registration
module_init(modulecdev_init);
module_exit(modulecdev_exit);

// Module license, author, and description information
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");

Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
int fd; // File descriptor
int buf[2]; // Buffer, stores two integer values

// Check if 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 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 in buffer
buf[0] = atoi(argv[1]);
buf[1] = atoi(argv[2]);

// Write buffer data to PWM GPIO device
if (write(fd, buf, sizeof(buf)) != sizeof(buf)) {
perror("write error");
close(fd);
return -1;
}

// Close device file
close(fd);
return 0;
}

Breathing light

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[]) {
int fd;
int buf[2];
int i;

// Check number of parameters
if (argc != 3) {
printf("Usage: %s <duty_cycle> <period>\n", argv[0]);
return -1;
}

// Open /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 infinite 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 device
close(fd);

return 0;
}

PWM input capture driver

Rockchip only implemented two functions: status acquisition and request, but did not implement functions related to input capture. Input capture is also one of the common functions of PWM. To implement PWM input capture on the RK3568, we need to implement the PWM input capture driver ourselves.

Device Tree Modification

The RK3568 PWM hardware introduction mentions that only four PWM channels, PWM3, PWM7, PWM11, and PWM15, support input capture. We will use PWM3 for the input capture demonstration. By default, PWM3 functions as an infrared receiver. The specific hardware schematic is as follows:

IR Receiver
IR Receiver

Modify the device tree, comment out the pwm3 node, and then add:

1
2
3
4
5
6
&pwm3 {
status = "okay";
compatible = "pwm-capture";
pinctrl-names = "default";
pwm-channel = <3>;
};

Driver Program

rockchip_pwm_capture.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
/* SPDX-License-Identifier: GPL-2.0 */

#ifndef __RKXX_PWM_REMOTECTL_H__
#define __RKXX_PWM_REMOTECTL_H__

#include <linux/input.h>
#include <linux/pwm.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>

/* Maximum Number of Keys */
#define MAX_NUM_KEYS 60
/* Maximum Number of PWM Captures */
#define PWM_PWR_KEY_CAPURURE_MAX 10

/* PWM Register Definitions */
#define PWM_REG_CNTR 0x00 /* Counter Register */
#define PWM_REG_HPR 0x04 /* Period Register */
#define PWM_REG_LPR 0x08 /* Duty Cycle Register */
#define PWM_REG_CTRL 0x0c /* Control Register */
#define PWM3_REG_INTSTS 0x10 /* PWM3 Interrupt Status Register */
#define PWM2_REG_INTSTS 0x20 /* PWM2 Interrupt Status Register */
#define PWM1_REG_INTSTS 0x30 /* PWM1 Interrupt Status Register */
#define PWM0_REG_INTSTS 0x40 /* PWM0 Interrupt Status Register */
#define PWM3_REG_INT_EN 0x14 /* PWM3 Interrupt Enable Register */
#define PWM2_REG_INT_EN 0x24 /* PWM2 Interrupt Enable Register */
#define PWM1_REG_INT_EN 0x34 /* PWM1 Interrupt Enable Register */
#define PWM0_REG_INT_EN 0x44 /* PWM0 Interrupt Enable Register */

/* Control Register Bit Definitions */
#define PWM_ENABLE (1 << 0) /* PWM Enable */
#define PWM_DISABLE (0 << 0) /* PWM Disable */

/* Operation Mode */
#define PWM_MODE_ONESHOT (0x00 << 1) /* One-Shot Mode */
#define PWM_MODE_CONTINUOUS (0x01 << 1) /* Continuous Mode */
#define PWM_MODE_CAPTURE (0x02 << 1) /* Capture Mode */

/* Duty Cycle Output Polarity */
#define PWM_DUTY_POSITIVE (0x01 << 3) /* Positive Polarity */
#define PWM_DUTY_NEGATIVE (0x00 << 3) /* Negative Polarity */

/* Inactive state output polarity */
#define PWM_INACTIVE_POSITIVE (0x01 << 4) /* Positive polarity */
#define PWM_INACTIVE_NEGATIVE (0x00 << 4) /* Negative polarity */

/* Clock source selection */
#define PWM_CLK_SCALE (1 << 9) /* Clock division */
#define PWM_CLK_NON_SCALE (0 << 9) /* No clock division */

#define PWM_CH0_INT (1 << 0)
#define PWM_CH1_INT (1 << 1)
#define PWM_CH2_INT (1 << 2)
#define PWM_CH3_INT (1 << 3)
#define PWM_PWR_KEY_INT (1 << 7)

#define PWM_CH0_POL (1 << 8)
#define PWM_CH1_POL (1 << 9)
#define PWM_CH2_POL (1 << 10)
#define PWM_CH3_POL (1 << 11)

#define PWM_CH0_INT_ENABLE (1 << 0)
#define PWM_CH0_INT_DISABLE (0 << 0)

#define PWM_CH1_INT_ENABLE (1 << 1)
#define PWM_CH1_INT_DISABLE (0 << 1)

#define PWM_CH2_INT_ENABLE (1 << 2)
#define PWM_CH2_INT_DISABLE (0 << 2)

#define PWM_CH3_INT_ENABLE (1 << 3)
#define PWM_CH3_INT_DISABLE (0 << 3)

#define PWM_INT_ENABLE 1
#define PWM_INT_DISABLE 0

/* Prescaler factor */
#define PWMCR_MIN_PRESCALE 0x00
#define PWMCR_MAX_PRESCALE 0x07

#define PWMDCR_MIN_DUTY 0x0001
#define PWMDCR_MAX_DUTY 0xFFFF

#define PWMPCR_MIN_PERIOD 0x0001
#define PWMPCR_MAX_PERIOD 0xFFFF

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 */
#define RK_PWM_TIME_PRE_MIN 4000
#define RK_PWM_TIME_PRE_MAX 5000

#define RK_PWM_TIME_PRE_MIN_LOW 8000
#define RK_PWM_TIME_PRE_MAX_LOW 10000

#define RK_PWM_TIME_BIT0_MIN 390
#define RK_PWM_TIME_BIT0_MAX 730

#define RK_PWM_TIME_BIT1_MIN 1300
#define RK_PWM_TIME_BIT1_MAX 2000

#define RK_PWM_TIME_BIT_MIN_LOW 390
#define RK_PWM_TIME_BIT_MAX_LOW 730

#define RK_PWM_TIME_RPT_MIN 2000
#define RK_PWM_TIME_RPT_MAX 2500

#define RK_PWM_TIME_SEQ1_MIN 95000
#define RK_PWM_TIME_SEQ1_MAX 98000

#define RK_PWM_TIME_SEQ2_MIN 30000
#define RK_PWM_TIME_SEQ2_MAX 55000

#define PWM_REG_INTSTS(n) ((3 - (n)) * 0x10 + 0x10)
#define PWM_REG_INT_EN(n) ((3 - (n)) * 0x10 + 0x14)
#define RK_PWM_VERSION_ID(n) ((3 - (n)) * 0x10 + 0x2c)
#define PWM_REG_PWRMATCH_CTRL(n) ((3 - (n)) * 0x10 + 0x50)
#define PWM_REG_PWRMATCH_LPRE(n) ((3 - (n)) * 0x10 + 0x54)
#define PWM_REG_PWRMATCH_HPRE(n) ((3 - (n)) * 0x10 + 0x58)
#define PWM_REG_PWRMATCH_LD(n) ((3 - (n)) * 0x10 + 0x5C)
#define PWM_REG_PWRMATCH_HD_ZERO(n) ((3 - (n)) * 0x10 + 0x60)
#define PWM_REG_PWRMATCH_HD_ONE(n) ((3 - (n)) * 0x10 + 0x64)
#define PWM_PWRMATCH_VALUE(n) ((3 - (n)) * 0x10 + 0x68)
#define PWM_PWRCAPTURE_VALUE(n) ((3 - (n)) * 0x10 + 0x9c)

#define PWM_CH_INT(n) BIT(n)
#define PWM_CH_POL(n) BIT(n + 8)

#define PWM_CH_INT_ENABLE(n) BIT(n)
#define PWM_PWR_INT_ENABLE BIT(7)
#define CH3_PWRKEY_ENABLE BIT(3)

/* 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,/* Get Data Status */
RMC_DONE, /* Completion Status */
} eRMC_STATE;

/* PWM Capture Platform Data Structure */
struct RKxx_remotectl_platform_data {
int nbuttons; /* Button Count */
int rep; /* Repeat */
int timer; /* Timer */
int wakeup; /* Wake Up */
};

/* PWM Capture Character Device Data Structure */
struct pwm_capture_cdev {
dev_t dev_num; /* Device Number */
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);

/* PWM Capture Can Be Enabled Here (Commented Out Code) */
/*
val = readl_relaxed(pwm_base + PWM_REG_CTRL);
val = (val & 0xFFFFFFFE) | PWM_ENABLE;
writel_relaxed(val, pwm_base + PWM_REG_CTRL);
*/
}

#endif

pwm-rockchip-capture.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/pwm.h>
#include <linux/uaccess.h>
#include <linux/clk.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include "rockchip_pwm_capture.h"

// Open Character Device Callback Function
static 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;
}

// Read Character Device Callback Function
static 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 to complete
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;
}

// Release callback function of character device
static int cdev_test_release(struct inode *inode, struct file *file) {
printk("This is cdev_test_release\n");
return 0;
}

// Character device operation function structure
static 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
irqreturn_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 based on 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 function
int 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 function
int 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 table
const struct of_device_id capture_of_device_id[] = {
{.compatible = "pwm-capture"},
{}
};

// Platform driver structure
struct platform_driver capture_platform_driver = {
.driver = {
.name = "pwm-capture",
.of_match_table = capture_of_device_id,
},
.probe = capture_probe,
.remove = capture_remove,
};

// Module initialization function
static int __init modulecdev_init(void)
{
return platform_driver_register(&capture_platform_driver);
}

// Module exit function
static 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

// 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 an input signal, connect it to PWM3, and let it capture.