目录
同一 DT 节点中具有多个 MMIO 区域的设备模型驱动程序
介绍
Zephyr 内核支持多种设备驱动程序。是否有驱动取决于单板和驱动。Zephyr 设备模型为配置作为系统一部分的驱动程序提供了一致的设备模型。设备模型负责初始化配置到系统中的所有驱动程序。每种类型的驱动程序(例如 UART、SPI、I2C)都由通用类型 API 支持。在此模型中,驱动程序在驱动程序初始化期间填充指向结构的指针,该结构包含指向其 API 函数的函数指针。这些结构按初始化级别顺序放入 RAM 部分。
标准驱动程序
下面列出了所有受支持的板配置上存在的设备驱动程序。
-
中断控制器:该设备驱动程序由内核的中断管理子系统使用。
-
定时器:这个设备驱动程序被内核的系统时钟和硬件时钟子系统使用。
-
串行通信:此设备驱动程序由内核的系统控制台子系统使用。
-
熵:此设备驱动程序为随机数生成器子系统提供熵数源。
同步调用
Zephyr 为多个板提供了一组设备驱动程序。每个驱动程序都应该支持基于中断的实现,而不是轮询,除非特定硬件不提供任何中断。
通过特定于设备的 API 访问的高级调用,例如i2c.h或spi.h,通常是同步的。因此,这些调用应该是阻塞的。
驱动程序 API
以下设备驱动程序 API 由:file:`device.h`提供。这些 API 仅供在设备驱动程序中使用,不应在应用程序中使用。
创建设备对象和相关数据结构,包括将其设置为启动时初始化。
将设备标识符转换为设备对象的全局标识符。
按名称获取指向设备对象的指针。
声明一个设备对象。当您需要对尚未定义的设备的前向引用时使用它。
驱动数据结构
设备初始化宏在构建时填充一些数据结构,这些数据结构分为只读部分和运行时可变部分。在高层次上,我们有:
struct device {
const char *name;
const void *config;
const void *api;
void * const data;
};
该config
成员用于在构建时设置的只读配置数据。例如,基本内存映射 IO 地址、IRQ 行号或设备的其他固定物理特性。这是config
传递给DEVICE_DEFINE()
相关宏的指针。
该data
结构保存在 RAM 中,并由驱动程序用于每个实例的运行时管理。例如,它可能包含引用计数、信号量、暂存缓冲区等。
该api
结构将通用子系统 API 映射到驱动程序中特定于设备的实现。它通常是只读的,并在构建时填充。下一节将对此进行更详细的描述。
子系统和 API 结构
大多数驱动程序将实现与设备无关的子系统 API。应用程序可以简单地针对该通用 API 进行编程,并且应用程序代码并不特定于任何特定的驱动程序实现。
子系统 API 定义通常如下所示:
typedef int (*subsystem_do_this_t)(const struct device *dev, int foo, int bar);
typedef void (*subsystem_do_that_t)(const struct device *dev, void *baz);
struct subsystem_api {
subsystem_do_this_t do_this;
subsystem_do_that_t do_that;
};
static inline int subsystem_do_this(const struct device *dev, int foo, int bar)
{
struct subsystem_api *api;
api = (struct subsystem_api *)dev->api;
return api->do_this(dev, foo, bar);
}
static inline void subsystem_do_that(const struct device *dev, void *baz)
{
struct subsystem_api *api;
api = (struct subsystem_api *)dev->api;
api->do_that(dev, baz);
}
实现特定子系统的驱动程序将定义这些 API 的实际实现,并填充 subsystem_api 结构的实例:
static int my_driver_do_this(const struct device *dev, int foo, int bar)
{
...
}
static void my_driver_do_that(const struct device *dev, void *baz)
{
...
}
static struct subsystem_api my_driver_api_funcs = {
.do_this = my_driver_do_this,
.do_that = my_driver_do_that
};
然后驱动程序将my_driver_api_funcs
作为api
参数 传递给DEVICE_DEFINE()
.
笔记
由于在结构中引用了指向 API 函数的指针api
,因此即使未使用它们也将始终包含在二进制文件中; gc-sections
链接器选项将始终看到至少一个对它们的引用。在大多数情况下,使用驱动程序 API 提供链接时间大小优化需要由 Kconfig 选项控制可选功能。
特定于设备的 API 扩展
某些设备可以转换为驱动子系统(例如 GPIO)的实例,但提供无法通过标准 API 公开的附加功能。这些设备将子系统操作与特定于设备的 API 结合在一起,在特定于设备的标头中进行了描述。
特定于设备的 API 定义通常如下所示:
#include <zephyr/drivers/subsystem.h>
/* When extensions need not be invoked from user mode threads */
int specific_do_that(const struct device *dev, int foo);
/* When extensions must be invokable from user mode threads */
__syscall int specific_from_user(const struct device *dev, int bar);
/* Only needed when extensions include syscalls */
#include <syscalls/specific.h>
实现子系统扩展的驱动程序将定义子系统 API 和特定 API 的实际实现:
static int generic_do_this(const struct device *dev, void *arg)
{
...
}
static struct generic_api api {
...
.do_this = generic_do_this,
...
};
/* supervisor-only API is globally visible */
int specific_do_that(const struct device *dev, int foo)
{
...
}
/* syscall API passes through a translation */
int z_impl_specific_from_user(const struct device *dev, int bar)
{
...
}
#ifdef CONFIG_USERSPACE
#include <zephyr/syscall_handler.h>
int z_vrfy_specific_from_user(const struct device *dev, int bar)
{
Z_OOPS(Z_SYSCALL_SPECIFIC_DRIVER(dev, K_OBJ_DRIVER_GENERIC, &api));
return z_impl_specific_do_that(dev, bar)
}
#include <syscalls/specific_from_user_mrsh.c>
#endif /* CONFIG_USERSPACE */
应用程序通过子系统和特定 API 使用设备。
笔记
设备特定扩展的公共 API 应以其适用的设备为前缀。例如,如果添加特殊功能以支持 Maxim DS3231,则specific
上述示例中的标识符片段将为maxim_ds3231
.
单个驱动程序,多个实例
某些驱动程序可能在给定系统中被多次实例化。例如,可以有多个 GPIO 组或多个 UART。驱动程序的每个实例都将具有不同的config
结构和data
结构。
为多个驱动程序实例配置中断是一种特殊情况。如果每个实例需要配置不同的中断线,这可以通过使用每个实例的配置函数来完成,因为参数需要IRQ_CONNECT()
在构建时解析。
例如,假设我们需要配置两个实例my_driver
,每个实例都有不同的中断线。在drivers/subsystem/subsystem_my_driver.h
:
typedef void (*my_driver_config_irq_t)(const struct device *dev);
struct my_driver_config {
DEVICE_MMIO_ROM;
my_driver_config_irq_t config_func;
};
在common init函数的实现中:
typedef void (*my_driver_config_irq_t)(const struct device *dev);
struct my_driver_config {
DEVICE_MMIO_ROM;
my_driver_config_irq_t config_func;
};
然后当声明特定实例时:
#if CONFIG_MY_DRIVER_0
DEVICE_DECLARE(my_driver_0);
static void my_driver_config_irq_0(const struct device *dev)
{
IRQ_CONNECT(MY_DRIVER_0_IRQ, MY_DRIVER_0_PRI, my_driver_isr,
DEVICE_GET(my_driver_0), MY_DRIVER_0_FLAGS);
}
const static struct my_driver_config my_driver_config_0 = {
DEVICE_MMIO_ROM_INIT(DT_DRV_INST(0)),
.config_func = my_driver_config_irq_0
}
static struct my_data_0;
DEVICE_DEFINE(my_driver_0, MY_DRIVER_0_NAME, my_driver_init,
NULL, &my_data_0, &my_driver_config_0,
POST_KERNEL, MY_DRIVER_0_PRIORITY, &my_api_funcs);
#endif /* CONFIG_MY_DRIVER_0 */
请注意使用 来DEVICE_DECLARE()
避免循环依赖于提供 IRQ 处理程序参数和设备本身的定义。
初始化级别
驱动程序可能依赖于首先初始化的其他驱动程序,或者需要使用内核服务。:c:func:`DEVICE_DEFINE()`和相关 API 允许用户指定在引导序列期间的什么时间执行 init 函数。任何驱动程序都会指定四个初始化级别之一:
EARLY
在引导过程的早期使用,就在进入 C 域 ( z_cstart()
) 之后。这可用于扩展或实现架构代码的架构和 SoC,并使用必须在内核调用任何特定于架构的初始化代码之前初始化的驱动程序或系统服务。
PRE_KERNEL_1
用于没有依赖性的设备,例如那些仅依赖于处理器/SOC 中存在的硬件的设备。这些设备在配置期间不能使用任何内核服务,因为内核服务尚不可用。然而,中断子系统将被配置,因此可以设置中断。此级别的初始化函数在中断堆栈上运行。
PRE_KERNEL_2
用于依赖于初始化为关卡一部分的设备初始化的设备PRE_KERNEL_1
。这些设备在配置期间不能使用任何内核服务,因为内核服务尚不可用。此级别的初始化函数在中断堆栈上运行。
POST_KERNEL
用于在配置期间需要内核服务的设备。此级别的初始化函数在内核主任务的上下文中运行。
APPLICATION
用于需要自动配置的应用程序组件(即非内核组件)。这些设备可以在配置期间使用内核提供的所有服务。此级别的初始化函数在内核主任务上运行。
在每个初始化级别中,您可以指定一个优先级,相对于同一初始化级别中的其他设备。优先级指定为 0 到 99 范围内的整数值;较低的值表示较早的初始化。优先级必须是不带前导零或符号的十进制整数文字(例如 32),或等效的符号名称(例如 \#define MY_INIT_PRIO 32
);不允许使用符号表达式(例如 CONFIG_KERNEL_INIT_PRIORITY_DEFAULT + 5
)。
驱动程序和其他系统实用程序可以通过使用:c:func:`k_is_pre_kernel` 函数来确定启动是否仍处于预内核状态。
系统驱动程序
在某些情况下,您可能只需要在启动时运行一个函数。对于这种情况, 可以使用:c:macro:`SYS_INIT` 。此宏不采用任何配置或运行时数据结构,并且以后无法通过名称获取设备指针。适用于初始化级别和优先级的相同设备策略。
错误处理
通常,最好使用__ASSERT()
宏而不是传播返回值,除非预期故障会在正常操作过程中发生(例如存储设备已满)。错误的参数、编程错误、一致性检查、病态/不可恢复的故障等,应该通过断言来处理。
当返回错误条件以供调用者检查时,成功时应返回 0,失败时应返回POSIX :file:`errno.h`代码。 有关详细信息,请参阅 https://github.com/zephyrproject-rtos/zephyr/wiki/Naming-Conventions#return-codes 。
内存映射
在某些系统上,外围内存映射 I/O (MMIO) 区域的线性地址在构建时无法获知:
- 必须在运行时从总线探测 I/O 范围,例如使用 PCI express
- 内存管理单元 (MMU) 处于活动状态,MMIO 范围的物理地址必须映射到内核确定的某个虚拟内存位置的页表中。
这些系统必须在 RAM 中维护 MMIO 范围的存储,并在驱动程序的 init 函数中建立映射。其他系统不关心这个,可以直接从 DTS 使用 MMIO 物理地址,不需要任何基于 RAM 的存储。
对于可能需要处理这种情况的驱动程序,定义了一组 DEVICE_MMIO 范围下的 API,以及一个映射函数 :c:func:`device_map`。
具有一个 MMIO 区域的设备模型驱动程序
最简单的情况是需要维护一个 MMIO 区域的驱动程序。 这些驱动程序将需要在它们的和结构的定义中使用DEVICE_MMIO_ROM
和 宏,并 使用. 在 init 函数中调用:
DEVICE_MMIO_RAMconfig_infodriver_dataconfig_infoDEVICE_MMIO_ROM_INITDEVICE_MMIO_MAP()
struct my_driver_config {
DEVICE_MMIO_ROM; /* Must be first */
...
}
struct my_driver_dev_data {
DEVICE_MMIO_RAM; /* Must be first */
...
}
const static struct my_driver_config my_driver_config_0 = {
DEVICE_MMIO_ROM_INIT(DT_DRV_INST(...)),
...
}
int my_driver_init(const struct device *dev)
{
...
DEVICE_MMIO_MAP(dev, K_MEM_CACHE_NONE);
...
}
int my_driver_some_function(const struct device *dev)
{
...
/* Write some data to the MMIO region */
sys_write32(0xDEADBEEF, DEVICE_MMIO_GET(dev));
...
}
这些宏的具体扩展取决于配置。在没有 MMU 或 PCI-e 的设备上,DEVICE_MMIO_MAP
扩展 DEVICE_MMIO_RAM
为空。
具有多个 MMIO 区域的设备模型驱动程序
某些驱动程序可能有多个 MMIO 区域。此外,一些驱动程序可能已经实现了一种继承形式,它要求将一些其他数据首先放置在 config_info
和driver_data
结构中。
这可以通过变体宏来管理DEVICE_MMIO_NAMED
。这些需要定义DEV_CFG()
和DEV_DATA()
宏以获得指向驱动程序的 config_info 或 dev_data 结构的正确类型指针。例如:
struct my_driver_config {
...
DEVICE_MMIO_NAMED_ROM(corge);
DEVICE_MMIO_NAMED_ROM(grault);
...
}
struct my_driver_dev_data {
...
DEVICE_MMIO_NAMED_RAM(corge);
DEVICE_MMIO_NAMED_RAM(grault);
...
}
#define DEV_CFG(_dev) \
((const struct my_driver_config *)((_dev)->config))
#define DEV_DATA(_dev) \
((struct my_driver_dev_data *)((_dev)->data))
const static struct my_driver_config my_driver_config_0 = {
...
DEVICE_MMIO_NAMED_ROM_INIT(corge, DT_DRV_INST(...)),
DEVICE_MMIO_NAMED_ROM_INIT(grault, DT_DRV_INST(...)),
...
}
int my_driver_init(const struct device *dev)
{
...
DEVICE_MMIO_NAMED_MAP(dev, corge, K_MEM_CACHE_NONE);
DEVICE_MMIO_NAMED_MAP(dev, grault, K_MEM_CACHE_NONE);
...
}
int my_driver_some_function(const struct device *dev)
{
...
/* Write some data to the MMIO regions */
sys_write32(0xDEADBEEF, DEVICE_MMIO_GET(dev, grault));
sys_write32(0xF0CCAC1A, DEVICE_MMIO_GET(dev, corge));
...
}
同一 DT 节点中具有多个 MMIO 区域的设备模型驱动程序
某些驱动程序可能将多个 MMIO 区域定义到同一个 DT 设备节点中,使用属性reg-names
来区分它们,例如:
/dts-v1/;
{
a-driver@40000000 {
reg = <0x40000000 0x1000>,
<0x40001000 0x1000>;
reg-names = "corge", "grault";
};
};
这可以像上一节中看到的那样进行管理,但这次使用 DEVICE_MMIO_NAMED_ROM_INIT_BY_NAME
宏来代替。所以唯一的区别在于驱动程序配置结构:
const static struct my_driver_config my_driver_config_0 = {
...
DEVICE_MMIO_NAMED_ROM_INIT_BY_NAME(corge, DT_DRV_INST(...)),
DEVICE_MMIO_NAMED_ROM_INIT_BY_NAME(grault, DT_DRV_INST(...)),
...
}
不使用 Zephyr 设备模型的驱动程序
某些驱动程序或类似驱动程序的代码可能不使用 Zephyr 的设备模型,并且必须为 MMIO 数据安排替代存储。这方面的一个例子是定时器驱动程序或中断控制器代码。
这可以通过一DEVICE_MMIO_TOPLEVEL
组宏来管理,例如:
DEVICE_MMIO_TOPLEVEL_STATIC(my_regs, DT_DRV_INST(..));
void some_init_code(...)
{
...
DEVICE_MMIO_TOPLEVEL_MAP(my_regs, K_MEM_CACHE_NONE);
...
}
void some_function(...)
...
sys_write32(DEVICE_MMIO_TOPLEVEL_GET(my_regs), 0xDEADBEEF);
...
}
不使用 DTS 的驱动程序
某些驱动程序可能无法从 DTS 获取 MMIO 物理地址,例如 PCI-E。在这种情况下,可以直接使用:c:func:`device_map`函数:
void some_init_code(...)
{
...
struct pcie_bar mbar;
bool bar_found = pcie_get_mbar(bdf, index, &mbar);
device_map(DEVICE_MMIO_RAM_PTR(dev), mbar.phys_addr, mbar.size, K_MEM_CACHE_NONE);
...
}
对于这些情况,可以省略 DEVICE_MMIO_ROM 指令。
API参考
.. doxygengroup:: 设备模型