Zephyr 设备驱动与设备驱动实例

Zephyr-设备模型与设备驱动实现 一文中说明来 Zephyr 设备驱动的实现,设备驱动是通用的、可重用的代码模块。对于同一类设备,无论有多少个实例其驱动代码都是一份,例如 esp32 上有多个 adc 设备,这些 adc 在硬件上都是一样的,所以控制方式也一样,用相同的驱动逻辑对不同的 adc 寄存器组进行操作就可以完成对 esp32 下不同的 adc 硬件进行操作。这个过程就是 Zephyr 设备驱动的多实例–“一个驱动多个实例”。做个并不是很恰当的比喻驱动实现就是类,驱动实例就是对象。

基本概念 链接到标题

设备驱动的实现可以理解为一个静态的概念,就是一段可以驱动控制硬件设备的代码,设备驱动实例可以理解为一个动态的概念,就是 Zephyr 运行时,有哪些设备会工作并被设备驱动控制。基于上在 Zephyr 中有 2 点需要说明:

  1. 驱动实例的数量取决于 Devicetree 配置
  2. 哪种驱动被编译取决于 Devicetree 配置
  3. 驱动是否会被编译进 Zephyr 镜像取决于 Kconfig 配置

也就是说无论一片 Soc 上有多少个硬件设备,Zephyr 运行时其驱动实例是由 Devicetree 决定的。无论 Devicetree 中有多少设备,设备驱动的构建是由 Kconfig 控制。

从前面的文字描述可能还会有些模糊,下面的图就可以充分说明驱动和实例之间的关系:

多实例

rt1062 在硬件上有 4 个 i2c 设备,但只有一个设备驱动文件i2c_mcux_lpi2c.c的驱动实现,当使用 Devicetree 配置全部启用时,软件运行时将有 4 个 i2c 的驱动实例出现。

esp32-c3 在硬件上有 2 个 i2c 设备,也是对应一个设备驱动文件i2c_esp32.c的驱动实现,当使用 Devicetree 配置只启动一个 i2c,软件运行时就只有 1 个 i2c 驱动实例出现。

根据 Devicetree 是哪种 Soc,会选择编译链接i2c_mcux_lpi2c.c或是i2c_esp32.c

最后会根据 Kconfig 配置决定系统到底是不是使用 i2c,来决定是否执行编译 i2c 的驱动。

驱动实例 链接到标题

在 Zephyr 中将驱动实现和驱动实例的概念进行分类,驱动的实现是抽象出控制驱动的逻辑流程,而驱动实例是抽象出管理一个驱动设备的实际数据,因此在 Zephyr 中一份驱动的管理数据就可以理解为一个驱动实例。

这里以 esp32 的 adc 为例,说明 Zephyr 如何为驱动实例创建管理数据。

adc_esp32.c中可以看到 ADC 初始化实例的宏

#define ESP32_ADC_INIT(inst)							\
										\
	static const struct adc_esp32_conf adc_esp32_conf_##inst = {		\
		.unit = DT_PROP(DT_DRV_INST(inst), unit) - 1,			\
		.channel_count = DT_PROP(DT_DRV_INST(inst), channel_count),	\
		ADC_ESP32_CONF_GPIO_PORT_INIT                                   \
		ADC_ESP32_CONF_DMA_INIT(inst)                                   \
	};									\
										\
	static struct adc_esp32_data adc_esp32_data_##inst = {			\
	};									\
										\
DEVICE_DT_INST_DEFINE(inst, &adc_esp32_init, NULL,				\
		&adc_esp32_data_##inst,						\
		&adc_esp32_conf_##inst,						\
		POST_KERNEL,							\
		CONFIG_ADC_INIT_PRIORITY,					\
		&api_esp32_driver_api);

驱动的管理数据是全局变量 链接到标题

一个驱动实例由两部分管理数据构成:

  • 配置数据struct adc_esp32_conf
    • 从 Devicetree 读取硬件的配置
    • 运行时不会被改变
  • 运行时数据struct adc_esp32_data
    • 驱动代码运行时依赖该结构体保存过程和控制数据
    • 运行时会被修改

需要注意的是:虽然不同的硬件驱动都有这两部分数据,但其配置数据结构会根据硬件配置变化,而运行时数据结构会根据驱动的实现变化,并没有固定的形式,完全由驱动实现者自行定义。

一个驱动实例 链接到标题

还记得 Zephyr-设备模型与驱动 API 中提到过:“设备驱动都是通过struct device *”来访问的吗?这里一个struct device *就是指向一个驱动实例的指针,也就是设备驱动 API 对设备驱动实例struct device*进行的操作调用来对应的设备驱动。因此可以理解为struct device在 Zephyr 的设备驱动模型中代表一个实例。

在设备驱动模型中使用下面两个宏可以创建实例,也就是建立struct device的全局变量

#define DEVICE_DT_DEFINE(node_id, init_fn, pm, data, config, level, prio, api, ...)  
#define DEVICE_DT_INST_DEFINE(inst, ...) 

从实现上来说DEVICE_DT_DEFINE只是对DEVICE_DT_DEFINE的封装,将从设备数中读出的实例号inst转换node_id,为方便通过实例号来创建驱动实例。

DEVICE_DT_DEFINE主要任务就是创建struct device全局变量,并为其初始化,从ESP32_ADC_INIT中的代码片段来分析

DEVICE_DT_INST_DEFINE(inst, &adc_esp32_init, NULL,				\
		&adc_esp32_data_##inst,						\
		&adc_esp32_conf_##inst,						\
		POST_KERNEL,							\
		CONFIG_ADC_INIT_PRIORITY,					\
		&api_esp32_driver_api);
  • init_fn 初始化函数使用adc_esp32_init
  • pm 是电源管理函数 NULL, 如果驱动要实现电源管理,就需要准备好struct pm_device
  • data 运行时管理数据 adc_esp32_data_##inst, ESP32_ADC_INIT已经准备好是struct adc_esp32_data类型
  • config 配置管理数据 adc_esp32_conf_##inst,ESP32_ADC_INIT已经准备好是struct adc_esp32_conf类型
  • level 该驱动的初始化等级 POST_KERNEL
  • prio 该驱动实例初始化的优先级 CONFIG_ADC_INIT_PRIORITY
  • api 驱动 API,api_esp32_driver_api,在Zephyr-设备模型与设备驱动实现 中有说明,这部分是驱动模型定义好的,对于 adc 来说就是struct adc_driver_api类型

DEVICE_DT_INST_DEFINE另外还会准备一个用于管理设备状态的struct device_state,用于存储设备实例的初始化结果和标记是否已经初始化。

这些准备好的全局变量用于创建和初始化一个struct device全局变量

struct device {
	const char *name;
	const void *config;
	const void *api;
	struct device_state *state;
	void *data;

#if defined(CONFIG_PM_DEVICE) || defined(__DOXYGEN__)
	union {
		struct pm_device_base *pm_base;
		struct pm_device *pm;
		struct pm_device_isr *pm_isr;
	};
#endif
};

struct device中各个成员都由前面准备好的全局数据初始化,name 是从设备树中读出。从struct device可以看出其管理的数据就是:

  • name 从设备树来,和驱动实现无关
  • config 驱动实例配置数据,被抽象为 void* 没有发生数据结构关系的绑定,无论哪种驱动的实例都有该成员
  • api 驱动实现的 API,被抽象为 void* 没有发生数据结构关系的绑定,无论哪种驱动的实现都有该成员
  • data 驱动实例管理数据,被抽象为 void* 没有发生数据结构关系的绑定,无论哪种驱动的实例都有该成员
  • state 驱动实例的初始化状态,所有实例都一样

综上,对于一个驱动实例它最基本的几个元素:

  • 驱动实例配置数据,全局变量,构建时决定内容,不可更改
  • 驱动实例管理数据,全局变量,运行时修改内容
  • 驱动实现 API 指针,全局变量,构建时句诶都能够,不可更改
  • 设备实例状态,全局变量,运行初始化时决定内容
  • 设备实例管理结构struct device 构建时决定内容,指向前面 4 部分内容

以 esp32 adc 为例,其一个实例在代码中最终体现为:

//驱动实现 API 指针
static const struct adc_driver_api api_esp32_driver_api = {
 .channel_setup = adc_esp32_channel_setup,
 .read = adc_esp32_read,

 .ref_internal = (1100),
};

//驱动实例配置数据
static const struct adc_esp32_conf adc_esp32_conf_0 = { 
	.unit = 1 - 1, 
	.channel_count = 5, 
}; 

//驱动实例管理数据
static struct adc_esp32_data adc_esp32_data_0 = { }; 

//驱动实例状态
static _attribute__((__aligned__(__alignof(struct device_state)))) struct device_state __devstate_dts_ord_54 __attribute__((__section__(".z_devstate")));

//驱动实例管理结构
struct device __device_dts_ord_54 __attribute__((section("." "_device" "." "static" "." "3_50_"))) __attribute__((__used__)) = { 
	.name = "adc@60040000", 
	.config = (&adc_esp32_conf_0), 
	.api = (&api_esp32_driver_api), 
	.state = (&__devstate_dts_ord_54), 
	.data = (&adc_esp32_data_0), 
}; 

多个实例 链接到标题

根据硬件和配置的不同 Zephyr 可以支持一个驱动多个实例,从上面也可以看得出来只用准备多个全局变量来保存驱动实例数据,实例状态,和实例管理结构即可。

在驱动中使用宏#define DT_INST_FOREACH_STATUS_OKAY(fn)可以访问 Devicetree 中该驱动对应的设备,只要 Devicetree 中status = "okay";的都会被fn执行。例如在adc_esp32.c中:

DT_INST_FOREACH_STATUS_OKAY(ESP32_ADC_INIT)

当 Devicetree 中开启了 2 个 adc 设备:

adc0: adc@60040000 {
			compatible = "espressif,esp32-adc";
			reg = < 0x60040000 0x4 >;
			unit = < 0x1 >;
			channel-count = < 0x5 >;
			#io-channel-cells = < 0x1 >;
			status = "okay";
			#address-cells = < 0x1 >;
			#size-cells = < 0x0 >;
			channel@0 {
				reg = < 0x0 >;
				zephyr,gain = "ADC_GAIN_1_4";
				zephyr,reference = "ADC_REF_INTERNAL";
				zephyr,acquisition-time = < 0x0 >;
				zephyr,resolution = < 0xc >;
			};
		};
		adc1: adc@60040004 {
			compatible = "espressif,esp32-adc";
			reg = < 0x60040004 0x4 >;
			unit = < 0x2 >;
			channel-count = < 0x2 >;
			#io-channel-cells = < 0x1 >;
			status = "okay";
			#address-cells = < 0x1 >;
			#size-cells = < 0x0 >;
			channel@0 {
				reg = < 0x0 >;
				zephyr,gain = "ADC_GAIN_1_4";
				zephyr,reference = "ADC_REF_INTERNAL";
				zephyr,acquisition-time = < 0x0 >;
				zephyr,resolution = < 0xc >;
			};
		};

最后构建预编译的时候除了前面的 adc0,还会生成 adc1 的相关实例数据

//驱动实例配置数据
static const struct adc_esp32_conf adc_esp32_conf_1 = { 
	.unit = 2 - 1, 
	.channel_count = 2, 
};  

//驱动实例管理数据
static struct adc_esp32_data adc_esp32_data_1 = { }; 

//驱动实例状态
static _attribute__((__aligned__(__alignof(struct device_state)))) struct device_state __devstate_dts_ord_56 __attribute__((__section__(".z_devstate")));

//驱动实例管理结构
struct device __device_dts_ord_56 __attribute__((section("." "_device" "." "static" "." "3_50_"))) __attribute__((__used__)) = { 
	.name = "adc@60040004", 
	.config = (&adc_esp32_conf_1), 
	.api = (&api_esp32_driver_api), 
	.state = (&__devstate_dts_ord_56), 
	.data = (&adc_esp32_data_1), 
}; 

驱动模型中驱动实例的工作 链接到标题

我们再来回顾一下驱动 API 的使用:

  • 先获取代表驱动实例的struct device *
  • struct device * 送到驱动 API 中

接下来就是开始通过struct device*取驱动控制具体的设备实例了

驱动 API 使用驱动实例数据过程 链接到标题

从 API 的执行过程中,会从驱动实例的管理结构struct devicedevice->api取出函数指针进行调用,例如

__syscall int adc_channel_setup(const struct device *dev,
				const struct adc_channel_cfg *channel_cfg);

static inline int z_impl_adc_channel_setup(const struct device *dev,
					   const struct adc_channel_cfg *channel_cfg)
{
	const struct adc_driver_api *api =
				(const struct adc_driver_api *)dev->api;

	return api->channel_setup(dev, channel_cfg);
}adc

驱动实现使用驱动实例数据过程 链接到标题

adc_channel_setup 会执行dev->api->channel_setup, 加入这里的 dev 传入的是__device_dts_ord_56, 对应的就会执行api_esp32_driver_api->channel_setup, 请回看 Zephyr-设备模型与设备驱动实现 一文,就知道它是adc_esp32_channel_setup

static int adc_esp32_channel_setup(const struct device *dev, const struct adc_channel_cfg *cfg)
{
	const struct adc_esp32_conf *conf = (const struct adc_esp32_conf *)dev->config;
	struct adc_esp32_data *data = (struct adc_esp32_data *) dev->data;

	//接下来就是相同的代码对不同实例的 conf/data 进行操作
	....
}

它的参数也含有struct device*,进入函数内就可以从其中取出该设备实例的配置数据struct adc_esp32_conf和管理数据struct adc_esp32_data,然后针对其操作的代码就是这类设备驱动实例的共有代码。

小结 链接到标题

这里可以将整个驱动模型串接起来总结为下图

alt text

对于一个驱动,构建完后静态视图为:

  • 驱动 API 和驱动实现:只有一份保存在。text 中
  • 驱动实例:以数据的形式有多份,每个实例的数据保存在。data

运行时的视图为:

  • 将。data 中的驱动实例配置数据搬运到内存中驱动实例配置数据的全局变量中
  • 按。bss 中驱动实例管理数据大小保留驱动实例管理数据的全局变量内存
  • 运行时将驱动实例配置数据的全局变量送到驱动实现中
  • 驱动代码对实例执行的管理数据保存到管理数据全局变量中

参考 链接到标题

https://docs.zephyrproject.org/4.0.0/kernel/drivers/index.html