本文以eeprom驱动为例。
这是一个典型的驱动包含的文件:
zephyr_project/
├── drivers/
│ └── foo/
│ └── foo_vendor.c
├── include/
│ └── zephyr/
│ └── drivers/
│ └── foo.c
├── CMakeLists.txt
└── Kconfig
include/zephyr/drivers/foo.c
声明了驱动的API、内联函数、系统调用入口等
drivers/foo/foo_vendor.c
是具体厂商的驱动代码,这其中要定义对应具体厂商的驱动API
一. 驱动API
zephyr中每个设备都以struct device
的形式呈现,这个结构体包含设备的基本信息和操作接口。
而每个设备驱动都会定义一个API结构体,包含所有驱动操作的指针(有时也包括一些状态数据)。
例如一个eeprom设备的dev是一个这样的结构体:
zephyr/include/zephyr/device.h
struct device {
const char *name;
const struct device_api *api;
void *driver_data;
// ...
};
而其中的api则这样定义:
zephyr/include/zephyr/drivers/eeprom.h
__subsystem struct eeprom_driver_api {
eeprom_api_read read;
eeprom_api_write write;
eeprom_api_size size;
};
这就是设备驱动API
具体的函数在哪里?
一般来说具体的函数在类似zephyr/drivers/eeprom/eeprom_stm32.c
的位置
该位置下还有eeprom_stm32_api
其中指定了具体的API函数
static const struct eeprom_driver_api eeprom_stm32_api = {
.read = eeprom_stm32_read,
.write = eeprom_stm32_write,
.size = eeprom_stm32_size,
};
如:
static size_t eeprom_stm32_size(const struct device *dev)
{
const struct eeprom_stm32_config *config = dev->config;
return config->size;
}
在设备初始化时DEVICE_DT_INST_DEFINE
宏会将设备实例和设备驱动关联,并指定初始化函数:
在系统启动时,Zephyr 会自动调用设备的初始化函数,将设备实例注册到设备模型中
zephyr/drivers/eeprom/eeprom_stm32.c:121
DEVICE_DT_INST_DEFINE(0, NULL, NULL, NULL, &eeprom_config, POST_KERNEL,
CONFIG_EEPROM_INIT_PRIORITY, &eeprom_stm32_api);
该宏指定具体的设备驱动APIeeprom_stm32_api
, 这样我们的gen_syscalls.py
能找到实际的驱动函数
二.系统调用
Zephyr 支持用户空间和内核空间的分离,用户空间代码不能直接调用内核空间的函数。
为了实现这一点,Zephyr 使用系统调用机制,允许用户空间代码通过特定的接口调用内核空间的函数。这样也提升了稳定性和可维护性。一开始接触这个确实一脸懵b....
1. 什么是__syscall
?
__syscall
关键字用于标记一个函数为系统调用函数。它告诉编译器和 gen_syscalls.py
脚本,这个函数需要生成系统调用接口
gen_syscalls.py
脚本会生成一个内联函数,用于在用户空间调用该系统调用函数。
这个内联函数会检查是否在用户空间环境下运行,如果是,则通过系统调用机制转发到内核空间的z_impl_eeprom_read
函数。
也就是说当用户进程调用eeprom_read
时,实际调用的并不是z_impl_eeprom_read
或者在eeprom.h
中定义的eeprom_read
,而是通过系统调用机制间接调用的。
zephyr/build/zephyr/include/generated/zephyr/syscalls/eeprom.h
//这段代码由脚本自动生成
__pinned_func //固定该函数在内存中位置不变
static inline int eeprom_read(const struct device * dev, off_t offset, void * data, size_t len)
{
#ifdef CONFIG_USERSPACE
if (z_syscall_trap()) {
union { uintptr_t x; const struct device * val; } parm0 = { .val = dev };
union { uintptr_t x; off_t val; } parm1 = { .val = offset };
union { uintptr_t x; void * val; } parm2 = { .val = data };
union { uintptr_t x; size_t val; } parm3 = { .val = len };
return (int) arch_syscall_invoke4(parm0.x, parm1.x, parm2.x, parm3.x, K_SYSCALL_EEPROM_READ);
}
#endif
compiler_barrier(); //用于防止编译器对代码进行重排序优化。它确保在调用 compiler_barrier() 之前的所有指令在调用之后的指令之前执行
return z_impl_eeprom_read(dev, offset, data, len);
}
调用过程
- 检查用户空间环境:生成的内联函数检查是否在用户空间环境下运行。
- 系统调用转发:如果在用户空间环境下运行,系统调用会被转发到内核空间,通过
arch_syscall_invoke4
函数进行实际调用。 - 内核空间调用 [
z_impl_eeprom_read
]:如果不在用户空间环境下运行,内联函数会直接调用 [z_impl_eeprom_read
]函数。
2. 定义系统调用
首先,在驱动代码中定义系统调用函数
在 eeprom.h
文件这样定义 [z_impl_eeprom_read
] 函数及其系统调用:
__syscall int eeprom_read(const struct device *dev, off_t offset, void *data,
size_t len);
static inline int z_impl_eeprom_read(const struct device *dev, off_t offset,
void *data, size_t len)
{
const struct eeprom_driver_api *api =
(const struct eeprom_driver_api *)dev->api;
return api->read(dev, offset, data, len);
}
在构建过程中,CMake 会调用 gen_syscalls.py
脚本。这个脚本会扫描所有标记了 __syscall
宏的函数,并生成相应的系统调用接口文件。
gen_syscalls.py
脚本会生成两个主要文件:
- syscalls_list.h:包含所有系统调用的列表。
- syscalls.c:包含系统调用的实现代码。
此外,还会生成每个系统调用的具体接口文件,例如 [eeprom.h
]中的内容。
大体就是这样,之后再写具体如何编写一个伺服电机驱动