Understanding the Pinctrl Subsystem in the Linux Kernel Code with Concrete Examples
In modern embedded systems, a processor typically boasts a plethora of pins, each capable of being configured for diverse functionalities like GPIO (General Purpose Input/Output), I2C, SPI, UART, and more. The efficient management of these pin configurations is paramount for the system's proper operation. The pinctrl (pin control) subsystem within the Linux kernel emerges as the dedicated solution to this intricate challenge.
Background: The Challenges of Pin Management
In the absence of a standardized pin management framework, each device driver would shoulder the responsibility of handling its own pin configurations. This approach presented several significant hurdles:
The Pinctrl Subsystem: A Unified Solution
The pinctrl subsystem steps in as a unified framework designed to oversee the management of all pins within a system. It achieves this by decoupling pin configuration details from device drivers and providing a standardized set of APIs for pin configuration and management. Key concepts underpinning this subsystem include:
Device Tree Configuration (Concrete Example 1)
Let's consider a hypothetical embedded system built around a System-on-Chip (SoC) named "MySoC". The pin configurations for this SoC are typically described in the Device Tree. For example, to configure pins for an I2C interface (I2C0) and a GPIO pin for an LED, the Device Tree might look like this:
/dts-v1/;
/memories/ {
...
};
/soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "mysoc";
ranges;
pinctrl: pinctrl@10000000 {
compatible = "mysoc-pinctrl";
reg = <0x10000000 0x1000>;
#address-cells = <1>;
#size-cells = <0>;
i2c0_pins_default: i2c0_pins_default {
compatible = "gpio-pinctrl-pins";
pins = <PIN_I2C0_SDA 0x40 (PULL_UP | DRIVE_STRENGTH_2)>,
<PIN_I2C0_SCL 0x40 (PULL_UP | DRIVE_STRENGTH_2)>;
function = "i2c0";
};
led_pins_default: led_pins_default {
compatible = "gpio-pinctrl-pins";
pins = <PIN_GPIO_LED 0x20 (OUTPUT_LOW)>;
function = "gpio";
};
};
i2c0: i2c@20000000 {
compatible = "my-i2c-controller";
reg = <0x20000000 0x100>;
pinctrl-names = "default";
pinctrl-0 = <&i2c0_pins_default>;
status = "okay";
};
led-controller: gpio-leds {
compatible = "gpio-leds";
led@0 {
gpios = <&gpio_controller PIN_GPIO_LED GPIO_ACTIVE_LOW>;
label = "my-led";
};
};
gpio_controller: gpio@30000000 {
compatible = "my-gpio-controller";
reg = <0x30000000 0x100>;
#gpio-cells = <2>;
};
};
Here, i2c0_pins_default defines a group of two pins, PIN_I2C0_SDA and PIN_I2C0_SCL, configured for the "i2c0" function with specific configs like PULL_UP and DRIVE_STRENGTH_2. Similarly, led_pins_default defines a group with the PIN_GPIO_LED configured as a "gpio" function with an initial config of OUTPUT_LOW. The i2c0 device node links to the i2c0_pins_default group, indicating that upon the i2c0 driver's initialization, these pins should be configured for I2C0 functionality.
Driver Interaction (Concrete Example 2)
Now, let's examine how a simplified I2C driver might interact with the pinctrl subsystem in its code:
Recommended by LinkedIn
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/pinctrl/consumer.h>
#include <linux/platform_device.h>
struct my_i2c_device {
struct i2c_client *client;
struct pinctrl *pinctrl;
struct pinctrl_state *pins_default;
};
static int my_i2c_probe(struct platform_device *pdev)
{
struct my_i2c_device *my_dev;
int ret;
my_dev = devm_kzalloc(&pdev->dev, sizeof(*my_dev), GFP_KERNEL);
if (!my_dev)
return -ENOMEM;
/* Get the pinctrl handle for this device */
my_dev->pinctrl = devm_pinctrl_get(&pdev->dev);
if (IS_ERR(my_dev->pinctrl)) {
dev_err(&pdev->dev, "Failed to get pinctrl handle\n");
return PTR_ERR(my_dev->pinctrl);
}
/* Get the default pin state (defined as "default" in device tree) */
my_dev->pins_default = pinctrl_lookup_state(my_dev->pinctrl, "default");
if (IS_ERR(my_dev->pins_default)) {
dev_err(&pdev->dev, "Failed to lookup default pin state\n");
return PTR_ERR(my_dev->pins_default);
}
/* Activate the default pin state */
ret = pinctrl_select_state(my_dev->pinctrl, my_dev->pins_default);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to select default pin state\n");
return ret;
}
dev_info(&pdev->dev, "I2C driver probed and pins configured.\n");
/* ... rest of the I2C driver initialization ... */
return 0;
}
static int my_i2c_remove(struct platform_device *pdev)
{
struct my_i2c_device *my_dev = platform_get_drvdata(pdev);
/* Optionally, you could deactivate the pin state here if needed */
/* pinctrl_select_state(my_dev->pinctrl, NULL); */
dev_info(&pdev->dev, "I2C driver removed.\n");
return 0;
}
static const struct of_device_id my_i2c_of_match[] = {
{ .compatible = "my-i2c-controller", },
{ }
};
MODULE_DEVICE_TABLE(of_id, my_i2c_of_match);
static struct platform_driver my_i2c_driver = {
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.driver = {
.name = "my-i2c-driver",
.of_match_table = my_i2c_of_match,
},
};
module_platform_driver(my_i2c_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Example I2C driver using pinctrl");
In the my_i2c_probe function, the driver obtains a handle to the pinctrl subsystem using devm_pinctrl_get(). It then retrieves the pin state named "default" (corresponding to the i2c0_pins_default group) using pinctrl_lookup_state(). Finally, pinctrl_select_state() activates the configuration defined in that group, ensuring the I2C pins are correctly set up.
State Management (Concrete Example 3)
Consider a scenario where our "MySoC" supports a low-power sleep state. We might want to reconfigure the I2C pins during sleep to minimize power consumption. This involves defining a new pin state in the Device Tree:
/dts-v1/;
...
/soc {
...
pinctrl: pinctrl@10000000 {
...
i2c0_pins_default: i2c0_pins_default {
...
};
i2c0_pins_sleep: i2c0_pins_sleep {
compatible = "gpio-pinctrl-pins";
pins = <PIN_I2C0_SDA 0x00 (PULL_DOWN)>,
<PIN_I2C0_SCL 0x00 (PULL_DOWN)>;
function = "gpio"; /* Maybe configure as GPIO inputs in sleep */
};
led_pins_default: led_pins_default {
...
};
};
i2c0: i2c@20000000 {
compatible = "my-i2c-controller";
reg = <0x20000000 0x100>;
pinctrl-names = "default", "sleep";
pinctrl-0 = <&i2c0_pins_default>;
pinctrl-1 = <&i2c0_pins_sleep>;
status = "okay";
};
...
};
Here, we've added i2c0_pins_sleep with potentially different configurations (e.g., PULL_DOWN) and function ("gpio"). The i2c0 device node now has pinctrl-names defining two states: "default" and "sleep", with pinctrl-0 and pinctrl-1 referencing the corresponding pin groups.
The I2C driver (or a power management driver) can then switch between these states in its code:
#include <linux/pm_runtime.h> /* For power management related functions */
static int my_i2c_suspend(struct device *dev)
{
struct my_i2c_device *my_dev = dev_get_drvdata(dev);
int ret;
/* Get the "sleep" pin state */
struct pinctrl_state *pins_sleep = pinctrl_lookup_state(my_dev->pinctrl, "sleep");
if (!IS_ERR(pins_sleep)) {
/* Activate the sleep pin state */
ret = pinctrl_select_state(my_dev->pinctrl, pins_sleep);
if (ret < 0) {
dev_err(dev, "Failed to select sleep pin state\n");
return ret;
}
}
/* ... perform other suspend operations ... */
return 0;
}
static int my_i2c_resume(struct device *dev)
{
struct my_i2c_device *my_dev = dev_get_drvdata(dev);
int ret;
/* Get the "default" pin state */
struct pinctrl_state *pins_default = pinctrl_lookup_state(my_dev->pinctrl, "default");
if (!IS_ERR(pins_default)) {
/* Activate the default pin state */
ret = pinctrl_select_state(my_dev->pinctrl, pins_default);
if (ret < 0) {
dev_err(dev, "Failed to select default pin state on resume\n");
return ret;
}
}
/* ... perform other resume operations ... */
return 0;
}
static const struct dev_pm_ops my_i2c_pm_ops = {
.suspend = my_i2c_suspend,
.resume = my_i2c_resume,
};
static struct platform_driver my_i2c_driver = {
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.driver = {
.name = "my-i2c-driver",
.of_match_table = my_i2c_of_match,
.pm = &my_i2c_pm_ops, /* Register power management operations */
},
};
module_platform_driver(my_i2c_driver);
The driver now uses pinctrl_lookup_state() to retrieve the "sleep" state and pinctrl_select_state() to activate it during the suspend operation, and similarly reverts to the "default" state during resume.
Advantages
Leveraging the pinctrl subsystem offers numerous benefits:
Conclusion
The pinctrl subsystem in the Linux kernel is an indispensable component for managing the complexity of pin configurations in modern embedded systems. By providing a structured and unified approach, it enhances code maintainability, portability, and prevents configuration conflicts. Understanding the pinctrl subsystem's concepts and how device drivers interact with it is fundamental for anyone involved in Linux kernel driver development for embedded platforms.