前言
- 设备驱动模型是 Zephyr RTOS 的开发中的一个核心概念,它提供了一种统一的方式来管理硬件设备和实现硬件抽象层。
- Zephyr 的设备驱动模型通过硬件抽象层(HAL)提供了一种抽象接口,使得应用程序和系统代码可以以一种一致的方式访问不同的硬件设备。这样,驱动程序能够提供对底层硬件的具体实现,而上层应用程序则可以使用相同的 API 进行操作,从而减少了对硬件特性的直接依赖。
- 本文以一个虚假的 EEPROM 模块(fake eeprom)上手,了解如何在 Zephyr 上进行设备驱动开发。
准备工作
- 开发环境:Zephyr 3.2, Zephyr RTOS开发环境搭建
- 学习和使用设备树1:理解如何编写和使用设备树文件是成功开发 Zephyr 驱动程序的关键。设备树文件定义了硬件配置和属性,驱动程序需要读取这些信息来正确初始化设备。
- 参考现有驱动:Zephyr 提供了大量现有的驱动程序作为参考,这些驱动程序覆盖了常见的硬件设备。学习和参考这些驱动程序可以帮助我们更快地编写自己的驱动程序。
流程说明
- 动手编写一个 dts 模块,熟悉 dts 规则,bindings 文件
- 动手编写一个设备模块驱动
- 动手编写一个 app, 能够调用自己的设备模块驱动
关键实现点
- 创建自己的 bindings 文件
- dts 中描述一个 fake eeprom
- 驱动程序中,能通过 dts 获取到 fake eeprom 的 label 名
- 驱动程序中,实现自定义的 driver api, 包含 open, read,write, close 接口
- 驱动程序中,开启一块大小为 4096 的 RAM 给 buffer, buffer 存储在 driver data 中
- open 处理:记录模块为打开状态
- close 处理: 记录模块为关闭状态
- write 处理: 接收上层调用传递下来的参数,写入对应地址
- read 处理: 打印对应地址的数据长度
- 正确定义 Kconfig
- app 通过 pri.conf 打开对应模块,实现 fake eeprom 访问
- dts 中能获取到 IIC 总线的 device
- 实现自己的系统调用
- read write 等实现,有防呆处理
以 nuclo_g474re 板子为例
编写 nucleo_g474re.overlay 文件。新增加驱动需要修改对应板的设备树文件,通常选择以 .overlay 的形式增加或修改设备树节点
-
添加 fake-eeprom 节点
/* * Copyright (c) 2019 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ / { aliases { eeprom0 = &fake_eeprom; }; }; &i2c1 { status = "okay"; fake_eeprom: eeprom@77 { compatible = "fake-eeprom"; reg = <0x77>; size = <1024>; pagesize = <16>; address-width = <8>; timeout = <5>; buffer_size = <4096>; /* read-only; */ }; };
-
编写设备树的 bindings 文件,即 fake-eeprom.yaml 文件
- 必须位于 dts/bindings 目录下 ,可以是原有的 zephyr/dts/bindings ,也可以是自己建的(需要通过 dts_root 指定)
- 设置 compatible 为设备树节点的 compatible,即 “fake-eeprom”,这样设备树节点才能与 .yaml 绑定
- 其他内容主要是指定该设备树节点下的所有属性的约束条件,如指定类型,是否必须定义等
# Copyright (c) 2021 BrainCo Inc. # SPDX-License-Identifier: Apache-2.0 description: nucleo-g474re, fake-eeprom compatible: "fake-eeprom" include: [base.yaml, i2c-device.yaml] properties: size: type: int required: true description: Total EEPROM size in bytes pagesize: type: int required: true description: EEPROM page size in bytes address-width: type: int required: true description: EEPROM address width in bits timeout: type: int required: true description: EEPROM write cycle timeout in milliseconds read-only: type: boolean required: false description: Disable writes to the EEPROM buffer_size: type: int required: true description: EEPROM BUFFER
-
编写 Kconfig 文件,使系统可裁减 fake eeprom 驱动功能
# Copyright (c) 2019 Nordic Semiconductor # SPDX-License-Identifier: Apache-2.0 config FAKE_EEPROM bool "Support for the demonstration fake eeprom"
-
编写 CMakeLists.txt 文件
- 对于底层(驱动层)CMakeLists.txt,主要根据 Kconfig 的配置宏选择是否编译该驱动源文件
# SPDX-License-Identifier: Apache-2.0 if(CONFIG_FAKE_EEPROM) # Add fake_eeprom_driver.h to the set of global include paths. zephyr_include_directories(.) zephyr_library() zephyr_library_sources( fake_eeprom_driver.c ) endif()
- 对于顶层(应用层)CMakeLists.txt,主要指定程序入口,以及一些文件目录等
# SPDX-License-Identifier: Apache-2.0 # For the sake of demonstration, we add the driver directory as a zephyr module # by hand. If your driver is a project that's managed by west, you can remove this line. list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR} ) cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(fake_eeprom) target_sources(app PRIVATE src/main.c)
-
编写驱动源文件 fake_eeprom_driver.c
- 定义该驱动的 struct config,struct data,以及 driver api,并通过设备树初始化这些结构体,生成设备。设备树与驱动之间通过 compatible 属性进行绑定
#define DT_DRV_COMPAT fake_eeprom
- 驱动程序在系统启动时会注册其支持的设备,并进行初始化。这一过程通常通过设备树或代码中声明的设备实例来完成,从而确保所有设备在应用程序使用之前都已经正确配置。
/* * Copyright (c) 2019 Vestas Wind Systems A/S * * SPDX-License-Identifier: Apache-2.0 */ /** * @file * @brief Driver for FAKE EEPROMs. */ #include <zephyr/drivers/i2c.h> #include <zephyr/kernel.h> #include <zephyr/types.h> #include <zephyr/syscall_handler.h> #include <zephyr/logging/log.h> #include "fake_eeprom_driver.h" LOG_MODULE_REGISTER(fake_eeprom); struct eeprom_config { struct i2c_dt_spec i2c; size_t size; size_t pagesize; uint8_t addr_width; bool readonly; uint16_t timeout; }; struct eeprom_data { uint8_t *buffer; bool state; }; static int eeprom_open(const struct device *dev) { struct eeprom_data *data = dev->data; data->state = true; printk("kernel: fake eeprom open\n"); return 0; } static int eeprom_close(const struct device *dev) { struct eeprom_data *data = dev->data; data->state = false; printk("kernel: fake eeprom close\n"); return 0; } static int eeprom_read(const struct device *dev, off_t address, void *buf, size_t len) { const struct eeprom_config *config = dev->config; struct eeprom_data *data = dev->data; if (!len) { return 0; } if ((address + len) > config->size) { LOG_WRN("attempt to read past device boundary"); return -EINVAL; } if (!data->state) { printk("kernel: fake eeprom read fail, device is close\n"); return 0; } printk("kernel: fake eeprom read: address=%ld len=%d\n", address, len); return 0; } static int eeprom_write(const struct device *dev, off_t address, const void *buf, size_t len) { const struct eeprom_config *config = dev->config; struct eeprom_data *data = dev->data; if (config->readonly) { LOG_WRN("attempt to write to read-only device"); return -EACCES; } if (!len) { return 0; } if ((address + len) > config->size) { LOG_WRN("attempt to write past device boundary"); return -EINVAL; } if (!data->state) { printk("kernel: fake eeprom write fail, device is close\n"); return 0; } printk("kernel: fake eeprom write: address=%ld len=%d\n", address, len); return 0; } static int eeprom_init(const struct device *dev) { const struct eeprom_config *config = dev->config; return device_is_ready(config->i2c.bus); } static const struct fake_eeprom_driver_api eeprom_api = { .open = eeprom_open, .close = eeprom_close, .read = eeprom_read, .write = eeprom_write, }; #define DT_DRV_COMPAT fake_eeprom #define INST_DT_FAKE_EEPROM(inst) DT_INST(inst, fake_eeprom) #define FAKE_EEPROM_DEVICE(inst) \ static const struct eeprom_config eeprom_config_##inst = { \ .i2c = I2C_DT_SPEC_INST_GET(inst), \ .addr_width = DT_PROP(INST_DT_FAKE_EEPROM(inst), address_width), \ .readonly = DT_PROP(INST_DT_FAKE_EEPROM(inst), read_only), \ .timeout = DT_PROP(INST_DT_FAKE_EEPROM(inst), timeout), \ .size = DT_PROP(INST_DT_FAKE_EEPROM(inst), size), \ }; \ static uint8_t eeprom_buffer_##inst[DT_PROP(INST_DT_FAKE_EEPROM(inst), buffer_size)]; \ static struct eeprom_data eeprom_data_##inst = { \ .buffer = eeprom_buffer_##inst, \ .state = false, \ }; \ DEVICE_DT_INST_DEFINE(inst, &eeprom_init, \ NULL, &eeprom_data_##inst, \ &eeprom_config_##inst, POST_KERNEL, \ 10, \ &eeprom_api); DT_INST_FOREACH_STATUS_OKAY(FAKE_EEPROM_DEVICE) //POST_KERNEL APPLICATION
- 定义该驱动的 struct config,struct data,以及 driver api,并通过设备树初始化这些结构体,生成设备。设备树与驱动之间通过 compatible 属性进行绑定
-
编写驱动头文件 fake_eeprom_driver.h,主要实现 driver api 对应的 __syscall 函数
通常对于通用驱动接口,建议转换成 __syscall 去使用,方便 app 通过统一接口去使用不同设备
对于特定于某个驱动的接口,则通过 config api 去使用就行/* * Copyright (c) 2019 Vestas Wind Systems A/S * * Heavily based on drivers/flash.h which is: * Copyright (c) 2017 Nordic Semiconductor ASA * Copyright (c) 2016 Intel Corporation * * SPDX-License-Identifier: Apache-2.0 */ /** * @file * @brief Public API for EEPROM drivers */ #ifndef ZEPHYR_INCLUDE_DRIVERS_FAKE_EEPROM_H_ #define ZEPHYR_INCLUDE_DRIVERS_FAKE_EEPROM_H_ /** * @brief EEPROM Interface * @defgroup eeprom_interface EEPROM Interface * @ingroup io_interfaces * @{ */ // #include <zephyr/types.h> // #include <stddef.h> #include <sys/types.h> #include <zephyr/device.h> #ifdef __cplusplus extern "C" { #endif typedef int (*eeprom_api_open)(const struct device *dev); typedef int (*eeprom_api_close)(const struct device *dev); typedef int (*eeprom_api_read)(const struct device *dev, off_t offset, void *data, size_t len); typedef int (*eeprom_api_write)(const struct device *dev, off_t offset, const void *data, size_t len); __subsystem struct fake_eeprom_driver_api { eeprom_api_open open; eeprom_api_close close; eeprom_api_read read; eeprom_api_write write; }; /** * @brief Open EEPROM device * * @param dev EEPROM device * * @return 0 on success, negative errno code on failure. */ __syscall int fake_eeprom_open(const struct device *dev); static inline int z_impl_fake_eeprom_open(const struct device *dev) { const struct fake_eeprom_driver_api *api = (const struct fake_eeprom_driver_api *)dev->api; return api->open(dev); } /** * @brief Close EEPROM device * * @param dev EEPROM device * * @return 0 on success, negative errno code on failure. */ __syscall int fake_eeprom_close(const struct device *dev); static inline int z_impl_fake_eeprom_close(const struct device *dev) { const struct fake_eeprom_driver_api *api = (const struct fake_eeprom_driver_api *)dev->api; return api->close(dev); } /** * @brief Read data from EEPROM * * @param dev EEPROM device * @param offset Address offset to read from. * @param data Buffer to store read data. * @param len Number of bytes to read. * * @return 0 on success, negative errno code on failure. */ __syscall int fake_eeprom_read(const struct device *dev, off_t offset, void *data, size_t len); static inline int z_impl_fake_eeprom_read(const struct device *dev, off_t offset, void *data, size_t len) { const struct fake_eeprom_driver_api *api = (const struct fake_eeprom_driver_api *)dev->api; return api->read(dev, offset, data, len); } /** * @brief Write data to EEPROM * * @param dev EEPROM device * @param offset Address offset to write data to. * @param data Buffer with data to write. * @param len Number of bytes to write. * * @return 0 on success, negative errno code on failure. */ __syscall int fake_eeprom_write(const struct device *dev, off_t offset, const void *data, size_t len); static inline int z_impl_fake_eeprom_write(const struct device *dev, off_t offset, const void *data, size_t len) { const struct fake_eeprom_driver_api *api = (const struct fake_eeprom_driver_api *)dev->api; return api->write(dev, offset, data, len); } #ifdef __cplusplus } #endif /** * @} */ #include <syscalls/fake_eeprom_driver.h> #endif /* ZEPHYR_INCLUDE_DRIVERS_EEPROM_H_ */
-
编写 module.yml 文件
zephyr 依赖的外部项目主要通过 module 去构建系统集成,如果在 zephyr 定义目录之外的地方实现自己的 module 驱动,就需要实现对应的 module.yml ,将 module 目录加入到构建系统
# SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2019 Nordic Semiconductor build: cmake: zephyr kconfig: zephyr/Kconfig settings: dts_root: .
-
编写 app main.c 文件,通过 __syscall 函数调用驱动 api,验证驱动功能
/* * Copyright (c) 2021 Thomas Stranger * * SPDX-License-Identifier: Apache-2.0 */ #include <zephyr/kernel.h> #include <zephyr/sys/printk.h> #include <zephyr/device.h> #include "fake_eeprom_driver.h" #define EEPROM_SAMPLE_OFFSET 100 struct eeprom_values { uint32_t param_1; uint32_t param_2; }; /* * Get a device structure from a devicetree node with alias eeprom0 */ static const struct device *get_eeprom_device(void) { const struct device *const dev = DEVICE_DT_GET(DT_ALIAS(eeprom0)); if (!device_is_ready(dev)) { printk("\nError: Device \"%s\" is not ready; " "check the driver initialization logs for errors.\n", dev->name); return NULL; } printk("Found EEPROM device \"%s\"\n", dev->name); return dev; } void main(void) { const struct device *eeprom = get_eeprom_device(); struct eeprom_values values; int rc; if (eeprom == NULL) { return; } rc = fake_eeprom_read(eeprom, EEPROM_SAMPLE_OFFSET, &values, sizeof(values)); if (rc < 0) { printk("Error: Couldn't read eeprom: err: %d.\n", rc); return; } rc = fake_eeprom_open(eeprom); if (rc < 0) { printk("Error: Couldn't open eeprom: err: %d.\n", rc); } rc = fake_eeprom_read(eeprom, EEPROM_SAMPLE_OFFSET, &values, sizeof(values)); if (rc < 0) { printk("Error: Couldn't read eeprom: err: %d.\n", rc); return; } rc = fake_eeprom_write(eeprom, EEPROM_SAMPLE_OFFSET, &values, sizeof(values)); if (rc < 0) { printk("Error: Couldn't write eeprom: err:%d.\n", rc); return; } rc = fake_eeprom_close(eeprom); if(rc < 0) { printk("Error: Couldn't close eeprom: err: %d.\n", rc); } }
-
编写 prj.conf 文件
通常大型项目里,系统可以通过 menuconfig 去进行裁减,但功能多了查找起来就不方便了。
zephyr 提供了 .conf 机制,无需通过 menuconfig ,直接加载对应功能,.conf 是一种永久的配置CONFIG_FAKE_EEPROM=y CONFIG_APPLICATION_DEFINED_SYSCALL=y CONFIG_I2C=y
目录结构
不同文件在不同目录的处理是有差异的
- 对于 .yaml 文件,可以直接放在 zephyr/dts/bindings 下,其他位置需要通过 module.yml 指定 dts_root 路径
- 对于新驱动模块的相关文件(源文件,Kconfig 文件),可以直接放在 zephyr/drivers下,其他位置需要 CMakeLists.txt 指定扩展模块路径
整个示例的目录如下:
/zephyr/samples/application_development/fake_eeprom:
fake_eeprom
|--boards
| - nucleo_g474re.overlay
|--dts/bindings
| - fake-eeprom.yaml
|--src
| - main.c
|--zephyr
| - CMakeLists.txt(底层)
| - fake_eeprom_driver.c
| - fake_eeprom_driver.h
| - Kconfig
| - module.yml
|--CMakeLists.txt(顶层)
|--prj.conf
问题记录
- 编译出现 undefined reference to `__device_dts_ord_92’,通常是系统相关依赖功能未启用,如使用 eeprom,但没定义 CONFIG_I2C=y ,在 prj.conf 加入该定义解决
- 系统编译生成多个相同宏导致的重定义冲突如自动生成两个 K_OBJ_EEPROM,原因是在做 syscall 转换时定义了重复的 __subsystem struct eeprom_driver_api,__subsystem定义的结构体是全局使用的,名字必须唯一
- 编译设备树生成的 devicetree_generated.h头文件里没有生成设备树节点属性的宏,导致在驱动里获取设备树节点属性时报错,这通常是.yaml 文件不匹配导致的,因为是自己实现的 .yaml 文件放在其他目录导致系统没有加载该文件,需要在 module.yml 里指定 dts_root 路径解决
- 应用程序中驱动使用前需要先判断驱动是否就绪,未就绪,原因可能是驱动程序的 init 函数没处理好这个返回的结果
- …
运行效果
- 上电先检查 eeprom 设备是否就绪,读写前需要先打开 eeprom 设备才能操作,如图:
设备树(Device Tree)是 Zephyr 中用于描述硬件的机制,它使用一个设备树的源文件(.dts 文件)来定义系统中所有设备的配置信息。设备驱动模型和设备树一起工作,允许 Zephyr 在编译时或运行时读取设备的配置信息,从而动态地配置设备。这使得驱动程序能够根据设备树的信息自动配置硬件设备,减少了硬编码和配置错误的可能性。 ↩︎