NCS-OS系列5 :设备驱动模型
文章目录
前言
ncs 相关文章,部分为原始文档翻译,水平有限,如果有错误,欢迎指出。
本文参考链接:
https://docs.zephyrproject.org/latest/reference/drivers/index.html
简介
Zephyr内核支持多种设备驱动程序。驱动程序是否可用取决于板子和驱动程序。
Zephyr设备驱动模型为配置作为系统一部分的驱动程序提供了一致性的设备模型。设备模型负责初始化配置到系统中的所有驱动程序。
每种类型的驱动程序(例如UART、SPI、I2C)都由一个通用的API支持。
在这个模型中,驱动程序在驱动初始化期间填入指向结构体的指针,该结构体包含指向驱动API函数的函数指针。这些结构体按照初始化等级顺序遍历放置到RAM中。
标准驱动
下面的驱动程序在所有支持的板子上都存在:
Interrupt controller
: 中断控制器,用于内核的中断管理子系统Timer
: 定时器,用于内核的系统时钟和硬件时钟子系统。Serial communication
:串行通信,用于内核的系统控制台子系统。Entropy
:为随机数产生子系统提供了一个随机数源。
同步调用
Zephyr 对多种板子提供了一系列的设备驱动程序。除非硬件不支持任何中断,否则每个驱动都应当提供基于中断(而不是轮询)的实现。
通过特定于设备的api(如i2c.h或spi.h)访问的高级调用通常是同步的。因此,这些调用应该是阻塞的
驱动程序APIs
下面的驱动程序APIs通过device.h
提供,这些 API 只能用于设备驱动程序中,不能应用于应用程序中。
DEVICE_DEFINE()
创建设备对象和相关数据结构,在启动时调用它的初始化函数。
DEVICE_AND_API_INIT()
类似于DEVICE_DEFINE()
,但是不支持设备电源管理。
DEVICE_NAME_GET()
将设备标识符转换为设备对象的全局标识符。
DEVICE_GET()
根据设备名称获取一个指向设备对象的指针。
DEVICE_DECLARE()
声明一个设备对象。当需要对尚未定义的设备进行前向引用时,可以使用这个函数。
驱动的数据结构
设备初始化宏在编译时会填充一些只读和运行时可变的数据结构,在较高的层次上,有如下的结构:
struct device {
const char *name;
const void *config;
const void *api;
void * const data;};
结构体成员config
是在编译阶段设置的只读
的配置数据,例如IO地址映射的地址,IRQ号,或设备的其他固定物理特征,这是传递给宏 DEVICE_DEFINE()
或其他涉及到的宏的config
指针。
data
结构体部分保存在RAM中,用于驱动程序的每个实例的运行时管理,例如,它可能包含引用计数、信号量、缓冲区等。
api
结构体部分将通用的子系统APIS和特定设备的驱动实现进行映射,它通常是只读的,并且在编译的时候生成,下面一章节会更详细的进行介绍。
子系统和API 结构体
大多数驱动程序实现的是一个与设备独立的子系统API,应用程序可以直接使用这些通用的API,额不用关系特定的驱动是如何实现的。
一个典型的子系统API定义如下所示:
typedef int (*subsystem_do_this_t)(const struct device *device, int foo, int bar);
typedef void (*subsystem_do_that_t)(const struct device *device, 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 *device, int foo, int bar)
{
struct subsystem_api *api;
api = (struct subsystem_api *)device->api;
return api->do_this(device, foo, bar);
}
static inline void subsystem_do_that(const struct device *device, void *baz)
{
struct subsystem_api *api;
api = (struct subsystem_api *)device->api;
api->do_that(device, foo, bar);
}
实现特定子系统的驱动程序时将定义这些api的真正实现,并填充subsystem_api结构的实例:
static int my_driver_do_this(const struct device *device, int foo, int bar)
{
···
}
static void my_driver_do_that(const struct device *device, 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_AND_API_INIT()
。
注意
由于指向API函数的指针是在API结构体中引用的,所以即使未被使用,它们也会包含在二进制文件中;gc-sections
链接器选项总是会找到至少一个对它们的引用。在大多数情况下,使用驱动程序api提供link-time
的大小优化需要用到由Kconfig选项控制可选特性。
特定设备API扩展
一些设备可以被认为是驱动子系统(如GPIO)的实例,但提供了无法通过标准API公开的额外功能。这些设备将子系统操作与特定于设备的api相结合,在特定的设备的头文件中进行描述。
典型的特定设备的API定义如下所示:
#include <drivers/subsystem.h>
/* When extensions need not be invoked from user mode threads */
int specific_do_that(const struct device *device, int foo);
/* When extensions must be invokable from user mode threads */
__syscall int specific_from_user(const struct device *device, int bar);
/* Only needed when extensions include syscalls */
#include <syscalls/specific.h>
实现子系统扩展的驱动程序将定义子系统API和特定API的真正实现:
static int generic_do_this(const struct device *device, 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 *device, int foo)
{
...
}
/* syscall API passes through a translation */
int z_impl_specific_from_user(const struct device *device, int bar)
{
...
}
#ifdef CONFIG_USERSPACE
#include <syscall_handler.h>
int z_vrfy_specific_from_user(const struct device *device, int bar)
{
Z_OOPS(Z_SYSCALL_SPECIFIC_DRIVER(dev, K_OBJ_DRIVER_GENERIC, &api));
return z_impl_specific_do_that(device, bar)
}
#include <syscalls/specific_from_user_mrsh.c>
#endif /* CONFIG_USERSPACE */
应用程序通过子系统API和扩展的api使用设备。
注意
特定设备的扩展的公共API应该以其所应用的设备的compatible作为前缀。例如,如果添加特殊函数来支持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 *device);
struct my_driver_config {
DEVICE_MMIO_ROM;
my_driver_config_irq_t config_func;
};
在通用init函数的实现中:
void my_driver_isr(const struct device *device)
{
/* Handle interrupt */
...
}
int my_driver_init(const struct device *device)
{
const struct my_driver_config *config = device->config;
DEVICE_MMIO_MAP(device, K_MEM_CACHE_NONE);
/* Do other initialization stuff */
...
config->config_func(device);
return 0;
}
当声明特殊的实例时:
DEVICE_DECLARE(my_driver_0);
static void my_driver_config_irq_0(void)
{
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_AND_API_INIT(my_driver_0, MY_DRIVER_0_NAME, my_driver_init,
&my_data_0, &my_driver_config_0, POST_KERNEL,
MY_DRIVER_0_PRIORITY, &my_api_funcs);
#endif /* CONFIG_MY_DRIVER_0 */
注意使用DEVICE_DECLARE()
来避免产生循环依赖。
初始化等级
驱动程序可能依赖于先初始化的其他驱动程序,或者需要使用内核服务。DEVICE_DEFINE()
和相关的api允许用户指定在初始化函数在设备启动的时候被执行的时间,任何驱动程序将指定以下四个初始化级别中的一个:
PRE_KERNEL_1
PRE_KERNEL_2
POST_KERNEL
APPLICATION
PRE_KERNEL_1
用于那些没有任何依赖的设备,例如那些纯粹只需要处理器/SoC 上的硬件的设备。这些设备在配置期间不需要使用任何内内核服务,因为此时内核服务还未启动。不过,中断子系统会被配置,因此可以进行设置中断,在这个等级上的初始化函数运行在中断栈上面。
PRE_KERNEL_2
用于那些依赖于已被初始化的 PRE_KERNEL_1
等级的设备的设备。这些设备在配置期间不使用任何内核服务,因为此时内核服务还未启动。这个等级上的初始化函数运行在中断栈上面。
POST_KERNEL
用于配置过程中需要内核服务的设备,这个级别的初始化函数在内核主任务的上下文中运行。
APPLICATION
用于需要自动配置的应用程序组件(即非内核组件)。这些设备可以在配置期间使用内核提供的所有服务,这个级别的初始化函数运行在内核主任务上。
在每个初始化级别中,相对于同一初始化级别中的其他设备,你可以指定一个优先级。优先级为0到99之间的整数值,较低的值表示较早的初始化。优先级必须是一个没有前导0或符号(例如32)的十进制整数值,或者一个等价的符号名(例如\#define MY_INIT_PRIO 32
);不允许使用符号表达式(例如CONFIG_KERNEL_INIT_PRIORITY_DEFAULT + 5
)
驱动程序和其他系统程序可以使用k_is_pre_kernel()
函数确定启动是否仍然处于 pre-kernel
状态。
系统驱动
在某些情况下,可能只需要在引导时运行一个函数。SYS_*
宏可以映射到DEVICE_DEFINE()
调用。对于SYS_INIT()
,没有配置或运行时数据结构,也没有办法在以后通过名称获取设备指针。初始化级别和优先级的策略相同。
对于SYS_DEVICE_DEFINE()
,可以通过名称获取指针,参考电源管理部分。
SYS_INIT()
:在启动时以指定的优先级运行初始化函数。
SYS_DEVICE_DEFINE()
:类似于没有API表的DEVICE_DEFINE()
,从初始化函数名构造设备名。
错误处理
一般来说,最好使用__ASSERT()
宏而不是返回值,除非错误可能会在正常操作过程中发生(例如存储设备已满),错误的参数、编程错误、一致性检查、病态/不可恢复的故障等都应该由断言来处理。
当返回错误状态让调用者检查时,成功时应该返回0,失败时返回POSIX errno.h
错误码,见:
https://github.com/zephyrproject-rtos/zephyr/wiki/Naming-Conventions#return-codes