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.
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
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
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:
Implementation: Hardware PWM is a method of realizing PWM output by a dedicated PWM hardware module.
Advantages:
Low CPU usage; PWM output is automatically completed by the hardware module without CPU intervention.
High PWM output frequency and resolution, 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.
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 Interface
pinctrl function
Net label
Corresponding GPIO
Function
PWM0
PWM0_M0
Working_LEDEN_H_GPIO0_B7
GPIO0_B7
LED light control pin
PWM0_M1
LCD0_PWREN_H_GPIO0_C7
GPIO0_C7
LVDS screen power supply enable pin
PWM1
PWM1_M0
VGA_HPDIN_GPIO0_C0
GPIO0_C0
VGA hot-plug detection pin
PWM1_M1
PCIE20_WAKEN_M0
GPIO0_B5
PCIE2.0 Wake-up
PWM2
PWM2_M0
PCIE20_PERSTn_GPIO0_C1
GPIO0_C1
PCIE2.0 reset pin
PWM2_M1
TP_RST_L_GPIO0_B6
GPIO0_B6
MIPI touch screen reset pin
PWM3_IR
PWM3_IR
PWM3_IR
GPIO0_C2
Infrared receiver
PWM4
PWM4
LCD0_BL_PWM4
GPIO0_C3
LVDS screen backlight
PWM5
PWM5
LCD1_BL_PWM5
GPIO0_C4
MIPI screen backlight
PWM6
PWM6
PWM_FAN
GPIO0_C5
Cooling fan speed regulation
PWM7_IR
PWM7_IR
RS485_DIR_GPIO0_C6
GPIO0_C6
485 transceiver control pin
PWM8
PWM8_M0
UART4_RX_M1
GPIO3_B1
Serial port 4 receive pin
PWM8_M1
SDMWC0_D0
GPIO1_D5
SD card data line
PWM9
PWM9_M0
UART4_TX_M1
GPIO3_B2
Serial port 4 transmit pin
PWM9_M1
SDMWC0_D1
GPIO1_D5
SD card data line
PWM10
PWM10_M0
GPIO3_B5
GPIO3_B5
Unused
PWM10_M1
SDMWC0_CMD
GPIO2_A1
SD card CMD pin
PWM11
PWM11_IR_M0
GPIO3_B6
GPIO3_B6
Unused
PWM11_IR_M1
CIF_CLKOUT
GPIO4_C0
Unused
PWM12
PWM12_M0
GMAC0_RSTn_GPIO3_B7
GPIO3_B7
Network card 0 reset pin
PWM12_M1
GPIO4_C5
GPIO4_C5
Serial port 9 transmit pin
PWM13
PWM13_M0
GMAC0_INT/PMEB_GPIO3_C0
GPIO3_C0
Network card 0 interrupt pin
PWM13_M1
GPIO4_C6
GPIO4_C6
Serial port 9 receive pin
PWM14
PWM14_M0
UART7_TX_M1
GPIO3_C4
Serial port 7 transmit pin
PWM14_M1
4G_DISABLE_GPIO4_C2
GPIO4_C2
CAN1 RX
PWM15
PWM15_IR_M0
UART7_RX_M1
GPIO3_C5
Serial port 7 receive pin
PWM15_IR_M1
HDMI_RX_INT_L_GPIO4_C3
GPIO4_C3
CAN1 TX
When hardware PWM is insufficient, GPIO can be used to simulate PWM, which can also be called software PWM. The introduction to software PWM is as follows:
Software PWM:
Implementation: 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.
Advantages:
High flexibility, capable of generating PWM waveforms on any GPIO pin.
Low cost, no additional hardware PWM module required.
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 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
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.
First, go to the/sys/class/pwm/directory, as shown in the figure below:
/sys/class/pwm/
The corresponding relationship is as follows
pwm
PWM12_M1 corresponds to pwmchip3. After entering the pwmchip3 directory, it is as shown in the figure below.
pwmchip3
There are a total of seven files in this directory, which are device、export、npwm、power、subsystem、uevent 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
export: Before using PWM, export it through the export attribute, as shown below:
export
unexport: After using PWM, you need to delete the exported PWM, for example:
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
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 duration
Rotation angle
0.5 ms
0°
1.0 ms
45°
1.5 ms
90°
2.0 ms
135°
2.5 ms
180°
Hardware Connection
Cable Color
Function
Backplane 20pin Pin Number
Net label
Red wire
Power positive (5V)
10/12
VCC5V0_SYS
Brown wire
Power negative (GND)
19/20
GND
Orange wire
Control signal line (PWM signal)
6
UART9_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:
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
staticintrockchip_pwm_probe(struct platform_device *pdev) { conststructof_device_id *id;// Device tree matching ID structrockchip_pwm_chip *pc;// PWM chip structure structresource *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"); }
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; }
// 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);
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
structrockchip_pwm_chip { structpwm_chipchip;// Generic PWM controller chip interface structclk *clk;// Main clock of the PWM controller structclk *pclk;// Peripheral clock of the PWM controller structpinctrl *pinctrl;// Pin multiplexing controller of the PWM controller structpinctrl_state *active_state;// Pin state currently used by the PWM controller conststructrockchip_pwm_data *data;// PWM controller configuration information specific to Rockchip SoC void __iomem *base;// Kernel virtual address of the PWM controller register space unsignedlong 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 - 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 */ structpwm_chip { structdevice *dev;// The device to which the PWM controller belongs conststructpwm_ops *ops;// The operation function set of the PWM controller int base;// The register base address offset of the PWM controller unsignedint npwm;// The number of PWM channels provided by the PWM controller
structpwm_device * (*of_xlate)(structpwm_chip *pc, conststructof_phandle_args *args); unsignedint of_pwm_n_cells;// The number of attributes required for PWM property description in the device tree
/* only used internally by the PWM framework */ structlist_headlist;// The global linked list where the PWM controller resides structpwm_device *pwms;// PWM channel device array };
wherepwm_opsRepresents the operation function set of the PWM controller
/** * 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 */ structpwm_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, unsignedlong timeout); int (*apply)(struct pwm_chip *chip, struct pwm_device *pwm, conststruct 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); structmodule *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
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:
/** * 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. */ intpwmchip_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. */ intpwmchip_add_with_polarity(struct pwm_chip *chip, enum pwm_polarity polarity) { structpwm_device *pwm; unsignedint 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];
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
Item
Details
Function prototype
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);
Core Function
Configure the PWM signal’sDuty cycleandPeriod, which is the core function for adjusting PWM output characteristics
Parameter description
1.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
Must 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
Item
Details
Function prototype
int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity);
Core Function
Sets the polarity of the PWM signal, defining the logic meaning of high and low levels
Parameter description
1.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
Suitable 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
Item
Details
Function prototype
int pwm_enable(struct pwm_device *pwm);
Core Function
Enables the PWM controller, starting to output the PWM signal with configured parameters (duty cycle, period, polarity)
Must 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
Item
Details
Function prototype
void pwm_disable(struct pwm_device *pwm);
Core Function
Shut down the PWM controller and stop outputting PWM signals
Parameter description
pwm: Pointer to the target PWM device structure
Return value
No return value
Usage Instructions
Called 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)
Obtain the PWM device handle from the Linux Device Tree, automatically managing device resources (no need for manual release)
Parameter description
1.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 value
Success: Returns a pointer tostruct pwm_devicevalid pointer; failure: returns an ERR_PTR type error pointer (which can be judged by theIS_ERR()macro)
Usage Instructions
is the preferred method for obtaining PWM devices in device drivers, based ondevmthe characteristics of the series interface, when the device is unloaded, the kernel 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>; };
// Callback function when opening the device staticintcdev_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 return0; }
// Callback function when writing to the device staticssize_tcdev_test_write(struct file *file, constchar __user *buf, size_t size, loff_t *off) { int ret; unsignedchar 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 staticintcdev_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 return0; }
// Define the character device operation function set staticstructfile_operationscdev_test_ops = { .owner = THIS_MODULE, .open = cdev_test_open, .write = cdev_test_write, .release = cdev_test_release, };
// Device probe function staticintsg90_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"); return0; }
// Device removal function staticintsg90_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"); return0; }
// Module initialization function staticint __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");
intmain(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 unsignedchar 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] = (unsignedchar)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);
return0; // 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
structhrtimer { structlist_headnode;// The node of the timer in the clock base list ktime_t _softexpires;// Soft expiration time, the actual expiration time may be slightly later enumhrtimer_restart(*function)(struct hrtimer *); // Callback function when the timer expires unsignedlong state; // Status flags of the timer structhrtimer_clock_base *base;// The clock base to which the timer belongs }
High-precision timer workflow
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.
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.
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.
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.
Initializationstruct 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 value
none
Usage Instructions
Must be called before using other hrtimer APIs; it is a prerequisite step for timer operations
hrtimer_set
Set Timer Expiration Time
Item
Details
Function prototype
void hrtimer_set(struct hrtimer *timer, ktime_t time, const enum hrtimer_mode mode);
Core Function
Set 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 value
none
Usage Instructions
Can set the time separately before callinghrtimer_start, or directly viahrtimer_start’s parameters to set the time (more commonly used)
hrtimer_start
Start timer
Item
Details
Function prototype
int hrtimer_start(struct hrtimer *timer, ktime_t time, const enum hrtimer_mode mode);
Core Function
Adds the timer to the kernel timer queue, 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 value
Returns 0 on success; returns negative error code on failure
Usage Instructions
Is the core function to start the timer, supports “set time + start” in one step (replaceshrtimer_set+hrtimer_start)
Advancing 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 value
Returns the new expiration time (ktime_ttype)
Usage Instructions
Must be called in the timer callback function, in conjunction withreturn HRTIMER_RESTARTto implement periodic triggering
hrtimer_cancel
Cancel timer
Item
Details
Function prototype
int hrtimer_cancel(struct hrtimer *timer);
Core Function
Cancel the started timer and remove it from the kernel queue
Parameter description
-timer: a pointer tohrtimerPointer to the struct
Return value
Returns 1 when the timer is active; returns 0 when it has stopped
Usage Instructions
Used 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
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:
// 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 staticintcdev_test_open(struct inode *inode, struct file *file) { printk("This is cdev test open\n"); return0; }
// Character device write callback function staticssize_tcdev_test_write(struct file *file, constchar __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 staticintcdev_test_release(struct inode *inode, struct file *file) { printk("This is cdev test release\n"); return0; }
// Character device operation function set staticstructfile_operationscdev_test_ops = { .owner = THIS_MODULE, .open = cdev_test_open, .write = cdev_test_write, .release = cdev_test_release, };
// Platform device probe callback function staticintled_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); }
// Module initialization function staticint __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");
intmain(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; }
// 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);
return0; }
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
Modify the device tree, comment out the pwm3 node, and then add:
// Callback function for opening character device staticintcdev_test_open(struct inode *inode, struct file *file) { structrkxx_capture_drvdata *ddata; structpwm_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;
return0; }
// Callback function for reading character device staticssize_tcdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off) { structrkxx_capture_drvdata *ddata; int val, i, ret;
ddata = file->private_data;
if (!ddata) { printk(KERN_ERR "Device data is NULL\n"); return -EINVAL; }
// 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 staticintcdev_test_release(struct inode *inode, struct file *file) { printk("This is cdev_test_release\n"); return0; }
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 intcapture_probe(struct platform_device *pdev) { int ret; structrkxx_capture_drvdata *ddata; structresource *r; structclk *clk; structclk *p_clk; structdevice_node *np = pdev->dev.of_node; int pwm_channel; int irq; structpwm_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; }