Zephyr Pinctrl -- 4状态模型和动态控制

本文说明Zephyr Pinctrl的状态模型和动态Pinctrl控制。

状态模型 链接到标题

为了使设备驱动正确运行,需要按照需求进行特定的引脚配置。 大多数设备驱动在初始化时使用静态配置进行设置,后续在运行时根据操作条件更改配置,例如:在挂起设备时启用低功耗模式会改变引脚的上拉。 Zephyr的状态是从Linux内核中的概念改编而来,每个设备驱动都拥有一组状态,每个状态有一个唯一的名称并包含一个完整的引脚配置集。这些状态彼此独立,不需要以任何特定顺序配置它们。 状态模型的另一个优点是它将设备驱动与引脚配置隔离开来。

标准状态 链接到标题

分配给引脚控制状态的名称或它们的数量取决于设备驱动的具体要求。 在大多数情况下,只需要初始化时的一状态就足够了,但在其他一些情况下需添加更多状态。 为了使保持一致,Zephyr为最常见的用例建立了命名约定: default: 默认状态,设备驱动处于正常工作时引脚的状态 sleep:睡眠状态,设备驱动处于低功耗或者睡眠时引脚的状态

自定义状态 链接到标题

某些设备驱动可能需要使用超出标准状态的自定义状态。 为此可以使用自定义字串,例如正常初始化的时候串口被映射到一组引脚上,而进入调试模式后串口被映射到另外一组引脚上。

设备树的表示 链接到标题

下面以nrf系列的设备树为例进行说明, 先定义了三组引脚配置

  • uart0_default 用于正常工作的串口配置
  • uart0_sleep 用于低功耗的串口配置
  • uart0_debug 用于调试模式的串口配置,其串口被映射到不同的引脚上
&pinctrl {
	uart0_default: uart0_default {
		group1 {
			psels = <NRF_PSEL(UART_TX, 0, 20)>,
				<NRF_PSEL(UART_RX, 0, 19)>,
				<NRF_PSEL(UART_RTS, 0, 5)>,
				<NRF_PSEL(UART_CTS, 0, 7)>;
		};
	};

	uart0_sleep: uart0_sleep {
		group1 {
			psels = <NRF_PSEL(UART_TX, 0, 20)>,
				<NRF_PSEL(UART_RX, 0, 19)>,
				<NRF_PSEL(UART_RTS, 0, 5)>,
				<NRF_PSEL(UART_CTS, 0, 7)>;
			low-power-enable;
		};
	};

    uart0_debug: uart0_debug {
		group1 {
			psels = <NRF_PSEL(UART_TX, 0, 7)>,
				<NRF_PSEL(UART_RX, 0, 8)>,
				<NRF_PSEL(UART_RTS, 0, 9)>,
				<NRF_PSEL(UART_CTS, 0, 10)>;
		};
	};
}

在和设备绑定时用pinctrl-x引用状态,用pinctrl-names列出状态命名

&uart0 {
	compatible = "nordic,nrf-uart";
	current-speed = <115200>;
	status = "okay";
	pinctrl-0 = <&uart0_default>;
	pinctrl-1 = <&uart0_sleep>;
    pinctrl-2 = <&uart0_debug>;
	pinctrl-names = "default", "sleep", "debug";
};

代码引用 链接到标题

zephyr/drivers/serial/uart_nrfx_uart.c中会在静态编译时将pinctrl-0, pinctrl-1, pinctrl-2通过PINCTRL_DT_INST_DEV_CONFIG_GET获得

#ifdef CONFIG_PINCTRL
	.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(0),
#else

通过如下代码进行实际的配置引用

pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);

PINCTRL_STATE_DEFAULT指定了使用pinctrl-0 = <&uart0_default>;的配置,在zephyr/include/zephyr/drivers/pinctrl.h中有如下定义

/** Default state (state used when the device is in operational state). */
#define PINCTRL_STATE_DEFAULT 0U
/** Sleep state (state used when the device is in low power mode). */
#define PINCTRL_STATE_SLEEP 1U

/** This and higher values refer to custom private states. */
#define PINCTRL_STATE_PRIV_START 2U

PINCTRL_STATE_DEFAULTPINCTRL_STATE_SLEEP对应到标准的状态defaultsleep, 而后面用户自定义的状态使用PINCTRL_STATE_{STATE_NAME}, 自定义的状态只从PINCTRL_STATE_PRIV_START开始,例如我们对于debug的自定义标识符可以为

#define PINCTRL_STATE_DEBUG PINCTRL_STATE_PRIV_START
//如果有新增其它自定义状态,依次递增
#define PINCTRL_STATE_DEBUG1 (PINCTRL_STATE_PRIV_START+1)
#define PINCTRL_STATE_DEBUG2 (PINCTRL_STATE_PRIV_START+2)

如果需要从驱动外部访问自定义状态,例如执行动态引脚控制,则自定义标识符应放在可公开访问的头文件中。

跳过状态 链接到标题

在大多数情况下,Devicetree 中定义的状态都会编译进固件中使用。 但在某些情况下,特定的状态将根据编译标志有条件地使用。 例如启用了 CONFIG_PM_DEVICE,才会使用到sleep。 如果需要没有设备电源管理的固件,应该从 Devicetree 中删除睡眠状态,以免浪费 ROM 空间存储在这种未使用的状态。

zephyr/include/zephyr/drivers/pinctrl.h中有如下定义 pinctrl Devicetree 宏在没有电源管理的情况下可以跳过睡眠状态:

#ifndef CONFIG_PM_DEVICE
/** If device power management is not enabled, "sleep" state will be ignored. */
#define PINCTRL_SKIP_SLEEP 1
#endif

其生效主要体现在条件宏上

#define Z_PINCTRL_STATE_PINS_DEFINE(state_idx, node_id)			       \
	COND_CODE_1(Z_PINCTRL_SKIP_STATE(state_idx, node_id), (),	       \
	(static const pinctrl_soc_pin_t					       \
	Z_PINCTRL_STATE_PINS_NAME(state_idx, node_id)[] =		       \
	Z_PINCTRL_STATE_PINS_INIT(node_id, pinctrl_ ## state_idx)))

#define Z_PINCTRL_STATE_INIT(state_idx, node_id)			       \
	COND_CODE_1(Z_PINCTRL_SKIP_STATE(state_idx, node_id), (),	       \
	({								       \
		.id = Z_PINCTRL_STATE_ID(state_idx, node_id),		       \
		.pins = Z_PINCTRL_STATE_PINS_NAME(state_idx, node_id),	       \
		.pin_cnt = ARRAY_SIZE(Z_PINCTRL_STATE_PINS_NAME(state_idx,     \
								node_id))      \
	}))

当我们需要跳过自定义宏是需要定义宏PINCTRL_SKIP_{STATE_NAME}, 例如我们不启用前面的debug模式,就要定义

#define PINCTRL_SKIP_DEBUG 1

SKIP状态要被pinctrl.h引用才有效,因此自定义的SKIP宏需要放到pinctrl.h,如果和具体的soc有关,可以将其放在#include <pinctrl_soc.h>中,如果不想污染zephyr的目录,目前就只能通过app的cmake来加入全局宏。

动态控制 链接到标题

动态管脚控制是指在运行时改变管脚配置。 此功能适用于在相同固件用在略有不同的板上,每个板都有一个外设路由到不同的引脚集。 可以通过设置 CONFIG_PINCTRL_DYNAMIC=y 启用此功能。

**注意:**动态管脚控制只能用于尚未初始化的设备。 由于 Zephyr 尚不支持设备取消初始化,因此只能在早期引导阶段使用此功能。在设备运行时更改引脚配置可能会导致意外行为。

启用动态引脚控制的影响之一是pinctrl_dev_config 将存储在 RAM 而不是 ROM 中(但不是状态或引脚配置)。 然后用户可以使用 pinctrl_update_states() 来更新存储在 pinctrl_dev_config中的状态, 这些动作需要在设备驱动初始化之前完成,设备驱动初始化时将应用状态时应用存储在更新状态中的引脚配置。 下面的代码为pinctrl_update_states的实现,可以看到是先找出匹配的id,然后改变其状态

int pinctrl_update_states(struct pinctrl_dev_config *config,
			  const struct pinctrl_state *states,
			  uint8_t state_cnt)
{
	uint8_t equal = 0U;

	/* check we are inserting same number of states */
	if (config->state_cnt != state_cnt) {
		return -EINVAL;
	}

	/* 找到匹配的id */
	for (uint8_t i = 0U; i < state_cnt; i++) {
		for (uint8_t j = 0U; j < config->state_cnt; j++) {
			if (states[i].id == config->states[j].id) {
				equal++;
				break;
			}
		}
	}

	if (equal != state_cnt) {
		return -EINVAL;
	}

	/* 替换状态 */
	config->states = states;

	return 0;
}

下面是nrf的动态控制demo代码https://github.com/zephyrproject-rtos/zephyr/tree/v3.3-branch/samples/boards/nrf/dynamic_pinctrl,板子的uart0原始Pinctrl配置如下

	uart0_default: uart0_default {
		group1 {
			psels = <NRF_PSEL(UART_TX, 0, 6)>,
				<NRF_PSEL(UART_RTS, 0, 5)>;
		};
		group2 {
			psels = <NRF_PSEL(UART_RX, 0, 8)>,
				<NRF_PSEL(UART_CTS, 0, 7)>;
			bias-pull-up;
		};
	};

	uart0_sleep: uart0_sleep {
		group1 {
			psels = <NRF_PSEL(UART_TX, 0, 6)>,
				<NRF_PSEL(UART_RX, 0, 8)>,
				<NRF_PSEL(UART_RTS, 0, 5)>,
				<NRF_PSEL(UART_CTS, 0, 7)>;
			low-power-enable;
		};
	};

用户定义如下

/ {
	zephyr,user {
		uart0_alt_default = <&uart0_alt_default>;
		uart0_alt_sleep = <&uart0_alt_sleep>;
	};
};

	uart0_alt_default: uart0_alt_default {
		group1 {
			psels = <NRF_PSEL(UART_TX, 1, 5)>,
				<NRF_PSEL(UART_RTS, 1, 4)>;
		};
		group2 {
			psels = <NRF_PSEL(UART_RX, 1, 7)>,
				<NRF_PSEL(UART_CTS, 1, 6)>;
			bias-pull-up;
		};
	};

	uart0_alt_sleep: uart0_alt_sleep {
		group1 {
			psels = <NRF_PSEL(UART_TX, 1, 5)>,
				<NRF_PSEL(UART_RX, 1, 7)>,
				<NRF_PSEL(UART_RTS, 1, 4)>,
				<NRF_PSEL(UART_CTS, 1, 6)>;
			low-power-enable;
		};
	};

二者的主要不同就是引脚的映射配置不同,remap.c演示了动态切换这一过程

/* UART0 alternative configurations (default and sleep states) */
PINCTRL_DT_STATE_PINS_DEFINE(DT_PATH(zephyr_user), uart0_alt_default);
#ifdef CONFIG_PM_DEVICE
PINCTRL_DT_STATE_PINS_DEFINE(DT_PATH(zephyr_user), uart0_alt_sleep);
#endif

static const struct pinctrl_state uart0_alt[] = {
	PINCTRL_DT_STATE_INIT(uart0_alt_default, PINCTRL_STATE_DEFAULT),
#ifdef CONFIG_PM_DEVICE
	PINCTRL_DT_STATE_INIT(uart0_alt_sleep, PINCTRL_STATE_SLEEP),
#endif
};

static int remap_pins(const struct device *dev)
{
	....
	//当按下button后,使用alt中的配置
	if (gpio_pin_get_dt(&button)) {
		struct pinctrl_dev_config *uart0_config =
			PINCTRL_DT_DEV_CONFIG_GET(DT_NODELABEL(uart0));

		return pinctrl_update_states(uart0_config, uart0_alt,
					     ARRAY_SIZE(uart0_alt));

	}
}


SYS_INIT(remap_pins, PRE_KERNEL_1, CONFIG_REMAP_INIT_PRIORITY);

remap_pinsPRE_KERNEL_1阶段调用,优先级为CONFIG_REMAP_INIT_PRIORITY, 因此UART0设备驱动初始化的优先级要低于CONFIG_REMAP_INIT_PRIORITY才能按顺序生效, 代码中

BUILD_ASSERT((CONFIG_GPIO_INIT_PRIORITY < CONFIG_REMAP_INIT_PRIORITY) &&
	     (CONFIG_REMAP_INIT_PRIORITY < CONFIG_SERIAL_INIT_PRIORITY),
	     "Device driver priorities are not set correctly");

在文件Kconfig中配置了CONFIG_REMAP_INIT_PRIORITY为50

config REMAP_INIT_PRIORITY
	int "Remap routine initialization priority"
	default 50
	help
	  Initialization priority of the remap routine within the PRE_KERNEL1 level.
	  This priority must be greater than GPIO_INIT_PRIORITY and lower than
	  UART_INIT_PRIORITY.

在prj.conf中配置了CONFIG_SERIAL_INIT_PRIORITY为60,满足前面的条件

CONFIG_PINCTRL=y
CONFIG_PINCTRL_DYNAMIC=y
# configure serial and console to come after remap hook
CONFIG_SERIAL_INIT_PRIORITY=60
CONFIG_CONSOLE_INIT_PRIORITY=70

小结 链接到标题

动态控制pinctrl_update_states只是改变状态,最后要依赖于设备驱动对Pinctrl的初始化调用pinctrl_apply_state来完成,并没有达到真正意义的动态控制。其实在nrf和rt系列中由于pinctrl实现得得当,直接使用pinctrl_apply_state应用设备树中不同的预定义状态就可以达到动态控制的目的,但对于其他soc不一定可以。

参考 链接到标题

https://docs.zephyrproject.org/latest/hardware/pinctrl/index.html