zephyr中的驱动机制

ttwards / 2024-10-06 / 原文

本文以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);
}

调用过程

  1. 检查用户空间环境:生成的内联函数检查是否在用户空间环境下运行。
  2. 系统调用转发:如果在用户空间环境下运行,系统调用会被转发到内核空间,通过 arch_syscall_invoke4 函数进行实际调用。
  3. 内核空间调用 [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]中的内容。
大体就是这样,之后再写具体如何编写一个伺服电机驱动