PIN控制器
-
在芯片中控制引脚功能映射,输入输出方向,上拉下拉的模块称为引脚控制器,它的主要操作对象是针对SOC上的片上外设。例如将SOC上的 I2C SDA 信号映射到 PX0。
-
不同的厂商对 PIN 控制器的实现上有所区别,例如在STM32和NXP中使用了一个PINMUX模块,所有引脚的配置参数,包括信号映射,都被该模块控制。
-
其他的一些厂商使用的方法有所不同,信号的映射被不同的外设所控制,例如 Nordic 的 nRF 系列
-
这些实现上的差异只体现在驱动中,对于用户而言没有区别,他们都会使用Zephyr提供的 pinctrl 接口进行操作。
状态模型
- 如果想正确的操作一个设备驱动,它必须要有一个正确的配置,一些驱动需要一个静态的配置,通常在系统启动后的初始化阶段完成,而另一些驱动需要动态的配置,需要在运行时做修改,例如在设备挂起时使能低功耗模式,这种情况需要使用状态来描述,在Linux内核中已经被广泛应用的一个概念。
- 每个设备驱动都一个状态集,每一个状态都有一个唯一的名字,并且包含了所有的引脚配置,意味着每一个状态都是独立的,另一个优点是这种方法将其从设备驱动的配置中独立出来。
标准状态
- 状态模型的名字或者是数字取决于设备驱动的需求,大部分情况下使用一个状态就足够了,但是在其他情况下可能需要更多的状态,为了保持一致性,在大多数情况下都使用了特定的命名风格,以下是Zephyr中的标准状态和对应的描述:
自定义状态
- 某些设备驱动会使用自定义状态而不是标准状态,这种情况下需要在驱动作用域内定义一个自定义状态标识符,标识符以命名风格如下: PINCTRL_STATE_{STATE_NAME} ,其中{STATE_NAME} 是大写的状态名称。
- 如果自定义状态需要在驱动外使用,例如进行动态的引脚配置,这种情况需要将节点标识符放到头文件中公开访问。
跳过状态
-
在大多数情况下,设备树中定义的状态都会被编译到固件中,然而在某些情况下节点中的状态依赖于特定的选项作为条件编译,以睡眠模式为例,它需要 CONFIG_PM_DEVICE 选项被使能,然而在某些设备上没有电源管理单元,这种情况下就需要将sleep状态移除,避免将不使用的状态编译到固件中占用存储空间。
-
这种情况下可以定义宏 PINCTRL_SKIP_{STATE_NAME} 用于跳过该状态。
动态引脚控制
-
动态引脚控制拥有在运行时修改配置的能力,这种特性可以应用在同一套程序需要运行在硬件配置有轻微差异的情况下,该特性通过使能 CONFIG_PINCTRL_DYNAMIC 开启。
-
动态引脚控制需要设备未被初始化,在操作时修改引脚配置可能产生意外的行为,Zephyr 并不支持去初始化操作,去初始化操作应该在早期启动阶段完成。
-
如果使用了动态修改引脚配置的功能,那么 pinctrl_dev_config 将会被放入到 RAM 而不是 ROM 中,用户可以使用 pinctrl_update_states() 更新存储在 pinctrl_dev_config 中的新状态。
设备树
状态
- 在一个设备中,引脚状态在设备树中是通过 pinctrl-N 属性表示的,N是一个从0开始的整数,pinctrl-names 属性中为每个状态分配了一个唯一的标识符,例如 pinctrl-0 的标识符即为 default
periph0: periph@0 {
...
/* state 0 ("default") */
pinctrl-0 = <...>;
...
/* state N ("mystate") */
pinctrl-N = <...>;
/* names for state 0 up to state N */
pinctrl-names = "default", ..., "mystate";
...
};
- 下面是Zephyr中预定义的两个状态
/** 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
- Zephyr 示例代码中对串口2的重新映射使用到了 pinctrl-0, 也就是默认状态下的配置,因为在其驱动程序中默认使用该组配置
&pinctrl {
uart2_default: uart2_default {
group1 {
pinmux = <UART2_TX_GPIO32>;
};
group2 {
pinmux = <UART2_RX_GPIO33>;
bias-pull-up;
};
};
};
&uart2 {
status = "okay";
current-speed = <57600>;
pinctrl-0 = <&uart2_default>;
pinctrl-names = "default";
fps {
#address-cells=<1>;
#size-cells=<0>;
grow_r502a@ffffffff {
compatible = "hzgrow,r502a";
reg = <0xffffffff>;
int-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
};
};
};
- 当串口初始化或者修改配置时会调用uart_esp32_configure
static int uart_esp32_init(const struct device *dev)
{
const struct uart_esp32_config *config = dev->config;
struct uart_esp32_data *data = dev->data;
int ret = uart_esp32_configure(dev, &data->uart_config);
...
}
static int uart_esp32_configure(const struct device *dev, const struct uart_config *cfg)
{
const struct uart_esp32_config *config = dev->config;
struct uart_esp32_data *data = dev->data;
int ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);
...
}
- 在 devicetree 中有多种方式来表示引脚配置,它们都包含了相同的信息:引脚多路复用和引脚配置参数。例如:UART_RX映射到PX0并启用上拉。表示形式的选择在很大程度上取决于每个供应商/SoC,因此引脚控制驱动程序的 设备树绑定文件是查找详细信息的最佳位置。
ESP32 pinctrl 设备树节点
-
下面以ESP32串口的输出端口为例讲解设备树中如何描述引脚配置
-
首先是soc的设备树源文件中的 pinctrl 节点,里面的compatible 属性会告诉我们如何找到绑定文件:
pinctrl: pin-controller {
compatible = "espressif,esp32-pinctrl";
status = "okay";
};
- 通过查找 espressif,esp32-pinctrl 找到了 espressif,esp32-pinctrl.yaml 文件,从下面可以看出首先需要一个子节点作为引脚配置状态节点,在该节点之下会有多个配置组,每个组下会有一个 pinmux 数组,该属性用于描述对应的引脚的多路复用,除此之外还有一些可选的属性用于控制引脚的输入输出配置。
compatible: "espressif,esp32-pinctrl"
include: base.yaml
child-binding:
description: |
Espressif pin controller pin configuration state nodes.
child-binding:
description:
Espressif pin controller pin configuration group.
include:
- name: pincfg-node.yaml
property-allowlist:
- bias-disable
- bias-pull-down
- bias-pull-up
- drive-push-pull
- drive-open-drain
- input-enable
- output-enable
- output-high
- output-low
properties:
pinmux:
required: true
type: array
description: |
Each array element represents pin muxing information of an individual
pin. The array elements are pre-declared macros taken from Espressif's
HAL.
- 引脚的多路复用功能取决于具体的硬件设计,对应的节点描述通常应该位于zephyr/boards目录下,或者是用户app的boards下的overlay文件中,下面是位于 zephyr/boards 目录下对串口0的默认配置,其中使用了uart0_default 描述串口0的接收和发送引脚。
/* zephyr/boards/xtensa/esp32/esp32-pinctrl.dtsi */
&pinctrl {
uart0_default: uart0_default {
group1 {
pinmux = <UART0_TX_GPIO1>;
};
group2 {
pinmux = <UART0_RX_GPIO3>;
bias-pull-up;
};
};
};
/* zephyr/boards/xtensa/esp32/esp32-pinctrl.dtsi */
uart0: uart@3ff40000 {
compatible = "espressif,esp32-uart";
reg = <0x3ff40000 0x400>;
interrupts = <UART0_INTR_SOURCE>;
interrupt-parent = <&intc>;
clocks = <&rtc ESP32_UART0_MODULE>;
status = "disabled";
};
/* zephyr/boards/xtensa/esp32/esp32.dts */
&uart0 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart0_default>;
pinctrl-names = "default";
};
- 下面是驱动头文件中的IOMUX配置,设备驱动中会通过该 pinmux 属性获取配置。
/* esp32-pinctrl.h */
#define UART0_TX_GPIO1 \
ESP32_PINMUX(1, ESP_NOSIG, ESP_U0TXD_OUT)
#define UART0_RX_GPIO3 \
ESP32_PINMUX(3, ESP_U0RXD_IN, ESP_NOSIG)
/* esp32-pinctrl-common.h */
#define ESP32_PINMUX(pin, sig_i, sig_o) \
(((pin & ESP32_PIN_NUM_MASK) << ESP32_PIN_NUM_SHIFT) | \
((sig_i & ESP32_PIN_SIGI_MASK) << ESP32_PIN_SIGI_SHIFT) | \
((sig_o & ESP32_PIN_SIGO_MASK) << ESP32_PIN_SIGO_SHIFT))
ESP32 pinctrl 驱动
- 在 pinctrl-esp32 中包含了对应的 pinctrl 驱动,每一个不同的 pinctrl 驱动都是通过实现 pinctrl_configure_pins 来完成配置修改的,该函数需要在驱动中由实现。
int pinctrl_configure_pins(const pinctrl_soc_pin_t *pins, uint8_t pin_cnt,
uintptr_t reg)
{
uint32_t pin_mux, pin_cfg;
int ret = 0;
ARG_UNUSED(reg);
for (uint8_t i = 0U; i < pin_cnt; i++) {
pin_mux = pins[i].pinmux;
pin_cfg = pins[i].pincfg;
ret = esp32_pin_configure(pin_mux, pin_cfg);
if (ret < 0) {
return ret;
}
}
return 0;
}
- 在zephyr/drivers/pinctrl.h 中 pinctrl_apply_state 间接调用了 pinctrl_configure_pins,在可以修改映射的外设中会调用该函数来配置对应的端口。
static inline int pinctrl_apply_state(const struct pinctrl_dev_config *config,
uint8_t id)
{
int ret;
const struct pinctrl_state *state;
ret = pinctrl_lookup_state(config, id, &state);
if (ret < 0) {
return ret;
}
return pinctrl_apply_state_direct(config, state);
}
static inline int pinctrl_apply_state_direct(
const struct pinctrl_dev_config *config,
const struct pinctrl_state *state)
{
uintptr_t reg;
#ifdef CONFIG_PINCTRL_STORE_REG
reg = config->reg;
#else
ARG_UNUSED(config);
reg = PINCTRL_REG_NONE;
#endif
return pinctrl_configure_pins(state->pins, state->pin_cnt, reg);
}
- 从设备驱动的角度来看使用 pinctrl 的API需要执行两个操作
- 首先需要使用PINCTRL_DT_DEFINE 或者PINCTRL_DT_INST_DEFINE 定义一个实例用于保存配置,里面包含了所有的状态和引脚。
- 其次,使用PINCTRL_DT_DEV_CONFIG_GET 或者 PINCTRL_DT_INST_DEV_CONFIG_GET 获取 pinctrl 中 pinctrl_dev_confing 的引用,因为稍后使用的API需要它。
- 下面是esp32串口驱动程序中的实现:
#define ESP32_UART_INIT(idx) \
\
PINCTRL_DT_INST_DEFINE(idx); \
\
static const DRAM_ATTR struct uart_esp32_config uart_esp32_cfg_port_##idx = { \
.clock_dev = DEVICE_DT_GET(DT_INST_CLOCKS_CTLR(idx)), \
.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(idx), \
.clock_subsys = (clock_control_subsys_t)DT_INST_CLOCKS_CELL(idx, offset), \
.irq_source = DT_INST_IRQN(idx), \
ESP_UART_DMA_INIT(idx)}; \
\
static struct uart_esp32_data uart_esp32_data_##idx = { \
.uart_config = {.baudrate = DT_INST_PROP(idx, current_speed), \
.parity = UART_CFG_PARITY_NONE, \
.stop_bits = UART_CFG_STOP_BITS_1, \
.data_bits = UART_CFG_DATA_BITS_8, \
.flow_ctrl = COND_CODE_1(DT_NODE_HAS_PROP(idx, hw_flow_control), \
(UART_CFG_FLOW_CTRL_RTS_CTS), \
(UART_CFG_FLOW_CTRL_NONE))}, \
.hal = \
{ \
.dev = (uart_dev_t *)DT_INST_REG_ADDR(idx), \
}, \
ESP_UART_UHCI_INIT(idx)}; \
\
DEVICE_DT_INST_DEFINE(idx, &uart_esp32_init, NULL, &uart_esp32_data_##idx, \
&uart_esp32_cfg_port_##idx, PRE_KERNEL_1, \
CONFIG_SERIAL_INIT_PRIORITY, &uart_esp32_api);
DT_INST_FOREACH_STATUS_OKAY(ESP32_UART_INIT);
总结
- 查找设备节点中的compatible属性获取yaml中的描述。
- 在pinctrl节点下添加子节点,根据yaml文件中的描述在子节点下添加具体配置。
- 使能设备节点,在设备节点中添加 pinctrl-0 属性,指定设备的配置节点,设置pinctrl-name中的状态名称。