Understanding the Pinctrl Subsystem in the Linux Kernel Code with Concrete Examples

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:

  • Code Redundancy: Multiple drivers might require identical pin functionalities, leading to unnecessary code duplication.
  • Configuration Conflicts: The potential for different drivers to attempt configuring the same pin for disparate functions could result in system instability or failure.
  • Maintenance Difficulty: Modifications to hardware pin configurations would necessitate changes across numerous drivers, significantly increasing maintenance overhead.
  • Lack of Uniformity: Diverse platforms might employ varying pin configuration methodologies, complicating the process of driver porting.

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:

  • Pins: The physical connection points on the hardware. Each pin is uniquely identified.
  • Groups: Related pins are logically grouped together for easier management. For instance, an SPI interface might necessitate a specific group of pins.
  • Functions: Each pin can be configured to perform various functions, such as acting as a GPIO input, GPIO output, or a UART transmit line.
  • Configs: Beyond their primary function, pins can possess additional configuration attributes like pull-up/pull-down resistors, drive strength, and speed.
  • States: A device might require different pin configurations depending on its operational state (e.g., normal operation versus sleep mode). The pinctrl subsystem allows for the definition of distinct pin states.
  • Pinctrl Core: The central component of the subsystem, responsible for managing pin configuration information and providing APIs for drivers.
  • Pinctrl Driver: A driver specific to each hardware platform, responsible for describing the platform's pins, pin groups, functions, and their interrelationships.

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:

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

  • Code Modularization: Pin configuration details are isolated from device drivers, enhancing code organization and readability.
  • Conflict Avoidance: The pinctrl core acts as a central authority, preventing conflicting pin configurations between different drivers.
  • Ease of Maintenance: Modifications to hardware pin configurations primarily involve updating the Device Tree and the pinctrl driver, minimizing changes in other drivers.
  • Platform Independence: Drivers utilize a consistent pinctrl API, abstracting away the underlying hardware specifics and promoting driver portability across different platforms.
  • Power Management Integration: The pinctrl subsystem seamlessly integrates with power management frameworks, enabling automatic transitions between pin states during different power modes.

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.

To view or add a comment, sign in

More articles by David Zhu

Insights from the community

Others also viewed

Explore topics