Cover image for Linux PWM

Linux PWM

Words 11.2k
Views
Visitors

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, disadvantages, and pin multiplexing of the hardware PWM interface on the iTOP-RK3568 platform.

Linux Driver Notes

Table of ContentsLink
1. Linux Driver Framework
2. Linux Driver Loading Logic
3. Character Device Basics
4. Concurrency and Race Conditions
5. Advanced Character Device
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 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

PWM The full name is Pulse Width Modulation, which translates to Pulse Width Modulation in Chinese. It is a technology that uses digital signals to control analog circuits. By changing the duty cycle of the 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 units: period and duty cycle. The specific descriptions of these two units are as follows:

  • Period (T): Refers to the time required for a complete high-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 the 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 definition of the area equivalence principle is that 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 to the inertial elementis also basically consistent, as shown in the figure below

Area Equivalence Principle
Area Equivalence Principle

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 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 communication.

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 channel uses a programmable prescaler, which can first divide the bus clock frequency, and then perform a second division.
  • The PWM channel has 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 a reference mode, which can output waveforms with various duty cycles.
  • These 4 PWM channels, PWM3, PWM7, PWM11, and PWM15, are optimized and particularly suitable for infrared applications.

The 16 PWM channels mentioned above all belong to hardware PWM, which means the PWM output is realized by a dedicated PWM hardware module. The introduction to hardware PWM is as follows:

Hardware PWM:

  1. Implementation: Hardware PWM is a method of realizing PWM output 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, which can reach frequencies up to the MHz level and resolutions at the ns level.
    • The output waveform is stable and reliable, and is not easily affected by CPU load.
  3. Disadvantages:
    • Requires a dedicated PWM hardware module, resulting in higher cost.
    • PWM output pins are limited and can only output on predefined pins.

The table of hardware PWM pin multiplexing functions for iTOP-RK3568 is shown below, and PWM can be selected based on this table:

PWM Interfacepinctrl functionNet labelCorresponding GPIOFunction
PWM0PWM0_M0Working_LEDEN_H_GPIO0_B7GPIO0_B7LED light control pin
PWM0_M1LCD0_PWREN_H_GPIO0_C7GPIO0_C7LVDS screen power supply 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 regulation
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_B5Unused
PWM10_M1SDMWC0_CMDGPIO2_A1SD card CMD pin
PWM11PWM11_IR_M0GPIO3_B6GPIO3_B6Unused
PWM11_IR_M1CIF_CLKOUTGPIO4_C0Unused
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_C5Serial port 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, which can also be called software PWM. The introduction to software PWM is as follows:

Software PWM:

  1. Implementation: Software PWM is a method of achieving PWM output through software programming. By utilizing timer interrupts or loop counting, the high-low level switching time of the output pin is controlled in software, thereby generating a PWM waveform.
  2. Advantages:
    • High flexibility, capable of generating PWM waveforms on any GPIO pin.
    • Low cost, no additional hardware PWM module required.
  3. Disadvantages:
    • High CPU usage, because it requires real-time control of pin levels in interrupt service routines or loops.
    • The frequency and resolution of PWM output are affected by the CPU main frequency and interrupt response time, making it impossible to achieve high frequency and high resolution.
    • 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.

The 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 the PWM controller it belongs to.
  • pwm_driver
    • Implements the driver for specific PWM devices.
    • Responsible for operations such as initialization, configuration, start/stop of PWM devices.
    • Interacts with PWM hardware devices 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 describes PWM device information, such as device name, controller it belongs to, etc. Provides a standardized PWM device access API for upper-layer applications.
  • PWM parameter configuration
    Providespwm_configInterface for configuring the working parameters of PWM devices, 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
    Providespwm_enableandpwm_disableInterface for controlling the enable and disable 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 enable and disable 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 initialization, state management, and power management tasks.

Hardware abstraction

The PWM adapter driver layer is responsible for abstractly 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-layer PWM core layer can indirectly control the underlying PWM hardware by calling these standard interfaces, without needing to pay attention to the specific hardware implementation details.

Hardware adaptation

The PWM adapter driver needs to implement the various 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-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 adapter 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 supply.

When the PWM device is registered, the adapter driver will perform 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, the current working parameters, etc. This state information will be fed back to the upper-layer PWM core layer to ensure that the core layer can correctly control and manage the PWM device.

So far, the explanation of the PWM subsystem framework is complete. In the next chapter, the method of operating PWM through the sysfs interface will be explained.

Using the sysfs interface to operate PWM

The PWM core layer provides a sysfs interface for operating PWM. However, to operate the PWM, the corresponding IO port must first be configured 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 figure below:

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 the status from okay to disabled, and then set PWM12.

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

/sys/class/pwm/

First, go to the/sys/class/pwm/directory, as shown in the figure below:

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

The corresponding relationship is as follows

pwm
pwm

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

pwmchip3
pwmchip3

There are a total of seven files in this directory, which are deviceexportnpwmpowersubsystemuevent and unexport. The attribute files to pay attention to are export, npwm, and unexport.

  • npwm: It is a read-only attribute. Reading this file can tell you how many PWM outputs there are 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, you need to delete the exported PWM, for example:

unexport
unexport

It should be noted 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 below:

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

  • polarity: Used to view PWM polarity, with normal and inversed options available
    • normal means the PWM signal is positive polarity, that is, high level is the active signal
    • inversed means the PWM signal is negative polarity, that is, low level is the active signal.
  • period: Used to configure the PWM period, readable and writable; write a string number value in ns (nanoseconds)
  • duty_cycle: Used to configure the PWM duty cycle, readable and writable; write a string number value, also in ns.
  • enable: Enable PWM output. Usually, enable PWM after configuring it. Readable and writable.
    • Writing “0” means disabling PWM;
    • Writing “1” means enabling PWM

SG-90 Servo Introduction

The SG90 servo is a small servo motor widely used in robots, RC airplanes, RC cars, and other fields. It is highly favored for its compact size, lightweight, low power consumption, and high cost-performance ratio.

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 part (0.5ms to 2.5ms) corresponds to the servo’s rotation range from 0 to 180 degrees. The specific correspondence is shown below.

High-level durationRotation angle
0.5 ms
1.0 ms45°
1.5 ms90°
2.0 ms135°
2.5 ms180°

Hardware Connection

Cable ColorFunctionBackplane 20pin 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 in joints or other moving parts of robots, as its precise angle control makes it suitable for realizing multi-degree-of-freedom motion.
  • Remote control models: Used to control steering surfaces or wheel directions, achieving flexible maneuvering.

Example

Use the following commands to rotate the servo to 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 positive (normal)
echo normal > polarity
# Enable PWM output
echo 1 > enable

Analysis of PWM controller registration process

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 PWM driver path for Rockchip can be found in the kernel directory asdrivers/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 matching 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 matching
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 active state of 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 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)// 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_chiptype pointer variable, 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

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;// 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; // Identifies whether the VOP (Video Output Processor) also enables the PWM function
bool center_aligned;// Identifies whether the PWM controller supports center-aligned mode
bool oneshot;// Identifies whether the PWM controller supports single-shot mode
int channel_id;
int irq;
};

Where the second line’sstruct 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-layer PWM framework. This structure is defined ininclude/linux/pwm.hfile, the 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;// The device to which the PWM controller belongs
const struct pwm_ops *ops;// The operation function set of the PWM controller
int base;// The register base address offset of the PWM controller
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 attributes required for PWM property description 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

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.cfile to findrockchip_pwm_opsoperation set, it can be found that Rockchip only implemented two functions: state acquisition and application, the specific content is as follows

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 to add the PWM controller to the PWM subsystem,pwmchip_addThe function is defined indrivers/pwm/core.cfile, the specific content is as follows:

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 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 to 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_addthe function 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, which is the core function for adjusting PWM output characteristics
Parameter description1.pwm: A pointer to the target PWM device structure (must first be obtained throughdevm_of_pwm_getobtained)
2.duty_ns: Duty cycle duration, unitNanoseconds (ns), representing the duration of the high level in one PWM period
3.period_ns: Total period of the PWM signal, unitNanoseconds (ns), representing 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 enabled in PWM (pwm_enable) is called to ensure the output PWM signal parameters are correctly configured; by adjustingduty_nsthe output average voltage can be changed (such as controlling LED brightness, motor speed), duty cycle calculation formula:Duty Cycle (%) = (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 FunctionSets the polarity of the PWM signal, defining the logic meaning of high and low levels
Parameter description1.pwm: Pointer to the target PWM device structure
2.polarity: Polarity enumeration value, only two 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 valueSuccess: returns 0; Failure: returns negative error code
Usage InstructionsSuitable for scenarios requiring reverse logic control (such as some motor forward/reverse control, LED reverse drive circuits), it is recommended to call afterpwm_configand beforepwm_enableis called

pwm_enable()

PWM Enable Function

ItemDetails
Function prototypeint pwm_enable(struct pwm_device *pwm);
Core FunctionEnables the PWM controller, starting to output 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 negative error code
Usage InstructionsMust be called afterpwm_config(andpwm_set_polarity, if polarity needs to be configured) to ensure the output signal parameters are valid; after calling, PWM will continue to output until it is calledpwm_disableStop

pwm_disable()

PWM shutdown function

ItemDetails
Function prototypevoid pwm_disable(struct pwm_device *pwm);
Core FunctionShut down the PWM controller and stop outputting PWM signals
Parameter descriptionpwm: Pointer to the target PWM device structure
Return valueNo return value
Usage InstructionsCalled when PWM output is not needed (e.g., device sleep, function shutdown) to save system power consumption; after calling, PWM will no longer output signals, and re-enabling requires re-callingpwm_enable(No need to reconfigure parameters, parameters will be 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 FunctionObtain the PWM device handle from the Linux Device Tree, automatically managing device resources (no need for manual release)
Parameter description1.dev: Pointer to the device structure corresponding to the current driver (e.g., in platform driversstruct 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, you can passNULL
Return valueSuccess: Returns a pointer tostruct pwm_devicevalid pointer; failure: returns an ERR_PTR type error pointer (which can be judged by theIS_ERR()macro)
Usage Instructionsis the preferred method for obtaining PWM devices in device drivers, based ondevmthe characteristics of the series interface, when the device is unloaded, the kernel will automatically release the PWM device resources to avoid memory leaks; it must be called before using other PWM functions (pwm_configetc.), the device handle must be obtained first before subsequent operations can be performed

Example

First modify the device tree, similarly set the 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>;
}
  • compatibleUsed to specify the compatibility string of the device, the kernel will match the appropriate driver based on this string.
  • pwmsUsed to configure the information of the PWM controller.
    • &pwm12Represents the reference to the PWM controller
    • 0Represents the PWM channel number
    • 20000000: Represents the PWM period, in nanoseconds, i.e., setting the period to 20ms
    • 1is the polarity, representing active high, i.e., 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 is 500,000 nanoseconds, period is 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 is 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); // Return to the initial PWM parameter configuration
pwm_disable(sg90_pwm_device); // Stop PWM
return 0;
}

// Define the 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);
}

// Apply for 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 matching 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 represents the number of command-line arguments, including the program name itself
// argv is an array of strings that stores the individual command-line arguments

int fd; // File descriptor, used to identify the opened device file
unsigned char buf[1]; // Stores the 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 be written 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 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 error
perror("write");
close(fd);
return -1;
}

// Delay for 3 seconds to simulate device operation
sleep(3);
// Close device file
close(fd);

return 0; // Program executed successfully
}

Test

Use./app 90Run the compiled executable program, where 90 represents the angle of servo rotation

Simulate PWM

When hardware PWM is insufficient, GPIO can be used to simulate PWM, also known as software PWM. Software PWM is a method of achieving PWM output through software programming, using timer interrupts or loop counting to control the high/low level switching time of the output pin 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, and can only provide millisecond-level accuracy, for some application scenarios with high time precision requirements (such as PWM simulation), the precision is still not sufficient.

To solve this problem, the Linux kernel introduced high-precision timers (hrtimers) mechanism.

hrtimersUsing dynamic clock (CLOCK_MONOTONIC) and dynamic timestamp (ktime_t) as the base time. By utilizing underlying hardware timers (such asHPET、LAPICtimers, etc.) to provide nanosecond-level time precision.

High-precision timer structurestruct hrtimerThe content is as follows:

1
2
3
4
5
6
7
struct hrtimer {
struct list_head node; // The 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-precision timer workflow

  1. Initialize timer

    Usehrtimer_init()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.

  2. Setting the timer
    Usehrtimer_start()orhrtimer_set()function sets the expiration time of the timer. Absolute or 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.

  3. Timer expiration handling
    When a system clock interrupt occurs, it callshrtimer_interrupt()function.hrtimer_interrupt()traverses the timer linked list of the clock base, checking for expired timers. For expired timers, it calls their callback function (function) to handle the expiration event. If the timer has not yet expired, it updates the wake-up time of the next expiring timer.

  4. Timer Management

    • hrtimer_cancel()Function used to cancel a running timer.
    • hrtimer_forward()Function used to forward the timer, i.e., recalculate the timer’s expiration time based on the current time.
    • hrtimer_start_range_ns()Function used to set the expiration time range of the timer to improve its accuracy.

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 clock source and operating mode of the timer
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-resolution timing)
-CLOCK_BOOTTIME: Monotonic clock including system suspend time
-mode: Operating 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 valuenone
Usage InstructionsMust be called before using other hrtimer APIs; it is a prerequisite step 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 FunctionSet the expiration time for an initialized timer (usually used withhrtimer_startuse )
Parameter description-timer: a pointer tohrtimerPointer to the struct
-time: Expiration time (ktime_ttype, requires usingktime_set(seconds, nanoseconds)to construct)
-mode: Time mode (HRTIMER_MODE_ABS/HRTIMER_MODE_REL, must matchhrtimer_init’s mode)
Return valuenone
Usage InstructionsCan set the time separately before callinghrtimer_start, or directly viahrtimer_start’s parameters to set the time (more commonly used)

hrtimer_start

Start timer

ItemDetails
Function prototypeint hrtimer_start(struct hrtimer *timer, ktime_t time, const enum hrtimer_mode mode);
Core FunctionAdds the timer to the kernel timer queue, starts timing (can directly set the expiration time)
Parameter description-timer: a pointer tohrtimerPointer to the struct -time: Expiration time (ktime_ttype) -mode: Time mode (same ashrtimer_init
Return valueReturns 0 on success; returns negative error code on failure
Usage InstructionsIs the core function to start the timer, supports “set time + start” in one step (replaceshrtimer_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 FunctionAdvancing the expiration time in the timer callback function to implementPeriodic timer
Parameter description-timer: a pointer tohrtimerPointer to the struct
-now: current time (usually obtained usinghrtimer_cb_get_time(timer)obtained)
-interval: periodic 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_RESTARTto implement periodic triggering

hrtimer_cancel

Cancel timer

ItemDetails
Function prototypeint hrtimer_cancel(struct hrtimer *timer);
Core FunctionCancel the started timer and remove it from the kernel queue
Parameter description-timer: a pointer tohrtimerPointer to the struct
Return valueReturns 1 when the timer is active; returns 0 when it has stopped
Usage InstructionsUsed to terminate the timer early (e.g., when unloading a module or shutting down a function)

Example

We use simulated PWM to achieve the LED breathing effect:

LED schematic diagram
LED schematic diagram

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

Originally it was:

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 PWM LED data structure
struct pwm_led_data {
int sum_count; // Total PWM period pulses
int high_count; // PWM high-level duration pulses
struct gpio_desc *gpiod; // GPIO descriptor
struct hrtimer pwm_timer; // High-resolution timer
ktime_t time; // Timer time interval
};

// Declare PWM LED data structure pointer
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 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 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 it
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 PWM LED data structure memory
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 the 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 removal callback function
static int led_remove(struct platform_device *pdev) {
hrtimer_cancel(&data->pwm_timer); // Stop high-resolution timer
gpiod_put(data->gpiod); // Release the 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 matching 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 parameter count
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 for status acquisition and application, and did not implement functions related to input capture. However, input capture is also one of the common functions of PWM. To implement PWM input capture on RK3568, we need to implement the PWM input capture driver ourselves.

Device Tree Modification

The RK3568 PWM hardware introduction mentions that only 4 PWM channels, PWM3, PWM7, PWM11, and PWM15, can perform input capture. We will use PWM3 for the input capture demonstration. By default, PWM3 functions as infrared reception. 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

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 PWM capture count */
#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 */

/* Operating mode */
#define PWM_MODE_ONESHOT (0x00 << 1) /* One-shot mode */
#define PWM_MODE_CONTINUMOUS (0x01 << 1) /* Continuous mode */
#define PWM_MODE_CAPTURE (0x02 << 1) /* Capture mode */

/* Duty cycle output polarity */
#define PWM_DUTY_POSTIVE (0x01 << 3) /* Positive polarity */
#define PWM_DUTY_NEGATIVE (0x00 << 3) /* Negative polarity */

/* Inactive state output polarity */
#define PWM_INACTIVE_POSTIVE (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 status enum */
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 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 state */
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 prescaler 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);
*/
}

#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"

// Callback function for opening character device
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;
}

// Callback function for reading character device
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;
}

// Callback function for releasing 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 function
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 matching 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.