Zephyr驱动程序框架简介

Posted 17岁boy想当攻城狮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zephyr驱动程序框架简介相关的知识,希望对你有一定的参考价值。

引言

Zephyr为驱动提供一套具体的驱动框架模型,开发者可根据这一套驱动框架模型来实现自己的驱动,这一套模型非常类似Linux内核的驱动实现,如果你对Linux内核驱动模型或有Linux内核驱动开发相关经验那么学习起来会非常轻松与简单。

驱动模型框架是使用了结构化的方式描述驱动,每个驱动都有等级,等级在Zephyr内部已经规定好了,每个等级对应不同的阶段,Zephyr在启动过程中会根据等级来依次初始化这些驱动,同时在不同的阶段下某些内核服务的可用性也是不同的。

一些通用类型的驱动 (常见驱动) Zephyr给出了具体实现,例如:I2CGPIOUSARTSPI..., 当然也存在一些非通用设备例如SPDIF、CAMERA、LCD-DSI、I2S..., 这些非通用设备设备并没有给出具体的实现,但给出了整体框架,开发者应遵守框架模型来完成自己的实现,类型Linux里的VFS子系统调用。

非通用设备也许在单片机/嵌入式领域比较少见,但在PC端是常见设备,针对不同的领域上这些设备的体现也不同,因为在PC端VideoCAMERAUSB这些是必备设备驱动

下图为Zephyr驱动框架模型图

开始之前的准备

本文需要对驱动有一定概念,在开始之前简单介绍一下什么是驱动,以及驱动(Drive)设备(Device)之间的关系。

驱动是为设备准备的,你可以把它理解为驾驶员,而设备就是车,驱动是用于控制设备的,例如单片机设备上有一个LED灯,如果想让它亮起来就需要有一段代码来对它进行控制,这段代码必须是以接口形式或服务的形式提供而非直接在main函数里跑的裸机代码,这段代码就称为驱动。

当然代码没有规定,刚刚提到的驱动代码形式规定是基于System的,你完全可以按照你的想法与风格来实现驱动,简而言之控制设备的代码称为驱动,驱动就是软件部分而设备就是硬件部分。

通常设备中也具有一些固件代码,通常驱动是修改设备提供的特殊功能寄存器里,而设备里的固件检测到这些电平的变化之后会去控制设备工作,例如USARTCamera... 当然也有一些设备是纯电路实现,没有固件例如小马达电机。

标准驱动

Zephyr在对芯片做适配时会完成标准驱动的实现,迄今为止Zephyr已经完成了许多芯片架构的适配,这些芯片上所支持的驱动可能有所不同但它们都具有标准驱动的功能,可以理解为Zephyr最小功能、基础功能:

Interrupt controller (中断控制器驱动):

为内核里中断管理子系统提供中断控制服务


Timer (计时器驱动):

为内核里的系统时钟、硬件时钟子系统提供服务


Serial communication (串行通讯驱动):

为内核TTY终端子系统提供服务、例如printk、log这些都属于TTY终端子系统提供的应用层API其子系统底层使用的是这个驱动


Entropy (熵驱动):

为内核随机数子系统提供服务, 用于生成随机数。

熵驱动应用层不能直接调用, 需使用应用层API, 这个与底层实现不同, 在硬件方面随机数有软件算法实现与硬件实现 (例如RNG)。

Zephyr建议应用层开发者不应直接调用熵驱动API, 应使用提供的应用层API, 应用层API对熵驱动做了封装内部会根据硬件环境来做不同的工作, 这样能确保随机数生成的准确性。

本段落引用Zephyr 3.3的开发者文档,未来可能会支持更多的标准驱动, 更多可以参考: Zephyr standard-drivers

初始化流程

在Zephyr启动过程中会跳转到C启动函数z_cstart, 在这个函数里里会对内核服务驱动初始化,在初始化过程中根据驱动的等级来决定驱动在什么阶段下初始化,下面是Zephyr定义的几种驱动等级:

INIT_LEVEL_EARLY:

最优先初始化的驱动等级,在这个过程中内核服务还没有初始化,这个过程中初始化的驱动不能使用Zephyr内核提供的服务

INIT_LEVEL_PRE_KERNEL_1、INIT_LEVEL_PRE_KERNEL_2:

在这个阶段针对开发板的硬件服务完成了初始化还有一些内核服务完成了初始化,这个过程中初始化的驱动可以使用Zephyr内核提供的硬件服务与内核服务API (例如Printk)

INIT_LEVEL_POST_KERNEL:

这个阶段下内核服务可以说是完全初始化了,内核的子系统也全部初始化完成,可以使用Zephyr内存管理子系统,例如Malloc

INIT_LEVEL_APPLICATION:

这个是最后的阶段,在这个阶段下所有的服务以及堆栈都已经完成初始化,在进入应用层之前做最后一次初始化


以上这些阶段可以在Zephyr源代码目录里kernel/init.c:z_cstart里找到它们的初始化代码

z_cstart函数会完成对INIT_LEVEL_EARLY、INIT_LEVEL_PRE_KERNEL_1、INIT_LEVEL_PRE_KERNEL_2的初始化

FUNC_NO_STACK_PROTECTOR
FUNC_NORETURN void z_cstart(void)

        /* gcov hook needed to get the coverage report.*/
        gcov_static_init();

        /* initialize early init calls */
        z_sys_init_run_level(INIT_LEVEL_EARLY);

        /* perform any architecture-specific initialization */
        arch_kernel_init();

        LOG_CORE_INIT();

#if defined(CONFIG_MULTITHREADING)
        /* Note: The z_ready_thread() call in prepare_multithreading() requires
        ¦* a dummy thread even if CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAIN=y
        ¦*/
        struct k_thread dummy_thread;

        z_dummy_thread_init(&dummy_thread);
#endif
        /* do any necessary initialization of static devices */
        z_device_state_init();

          /* perform basic hardware initialization */
        z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_1);
        z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_2);

#ifdef CONFIG_MULTITHREADING
        switch_to_main_thread(prepare_multithreading());
#else
...

prepare_multithreading函数里会去启动bg_thread_main线程,这个线程里会完成INIT_LEVEL_POST_KERNEL、INIT_LEVEL_APPLICATION的初始化

__boot_func
static void bg_thread_main(void *unused1, void *unused2, void *unused3)

        ARG_UNUSED(unused1);
        ARG_UNUSED(unused2);
        ARG_UNUSED(unused3);

#ifdef CONFIG_MMU
        /* Invoked here such that backing store or eviction algorithms may
        ¦* initialize kernel objects, and that all POST_KERNEL and later tasks
        ¦* may perform memory management tasks (except for z_phys_map() which
        ¦* is allowed at any time)
        ¦*/
        z_mem_manage_init();
#endif /* CONFIG_MMU */
        z_sys_post_kernel = true;

        z_sys_init_run_level(INIT_LEVEL_POST_KERNEL);
#if CONFIG_STACK_POINTER_RANDOM
        z_stack_adjust_initialized = 1;
#endif
        boot_banner();

#if defined(CONFIG_CPP)
        void z_cpp_init_static(void);
        z_cpp_init_static();
 /* Final init level before app starts */
        z_sys_init_run_level(INIT_LEVEL_APPLICATION);

        z_init_static_threads();

#ifdef CONFIG_KERNEL_COHERENCE
        __ASSERT_NO_MSG(arch_mem_coherent(&_kernel));
#endif

#ifdef CONFIG_SMP
        if (!IS_ENABLED(CONFIG_SMP_BOOT_DELAY)) 
                z_smp_init();
        
        z_sys_init_run_level(INIT_LEVEL_SMP);
#endif
...

在每个不同的阶段下,还有不同优先级的初始化,Zephyr在实现这一点时设计方式很巧妙。

Zephyr里面定义了许多的,这些段用于存放不同的对象,驱动根据等级数量定义了同等数量的段,可以看到如下的Link脚本:

这段脚本是由Zephyr Build脚本整合到Build目录下的link.cmd文件里的

initlevel :
 
  __init_start = .;
  __init_EARLY_start = .; KEEP(*(SORT(.z_init_EARLY[0-9]_*))); KEEP(*(SORT(.z_init_EARLY[1-9][0-9]_*)));
  __init_PRE_KERNEL_1_start = .; KEEP(*(SORT(.z_init_PRE_KERNEL_1[0-9]_*))); KEEP(*(SORT(.z_init_PRE_KERNEL_1[1-9][0-9]_*)));
  __init_PRE_KERNEL_2_start = .; KEEP(*(SORT(.z_init_PRE_KERNEL_2[0-9]_*))); KEEP(*(SORT(.z_init_PRE_KERNEL_2[1-9][0-9]_*)));
  __init_POST_KERNEL_start = .; KEEP(*(SORT(.z_init_POST_KERNEL[0-9]_*))); KEEP(*(SORT(.z_init_POST_KERNEL[1-9][0-9]_*)));
  __init_APPLICATION_start = .; KEEP(*(SORT(.z_init_APPLICATION[0-9]_*))); KEEP(*(SORT(.z_init_APPLICATION[1-9][0-9]_*)));
  __init_SMP_start = .; KEEP(*(SORT(.z_init_SMP[0-9]_*))); KEEP(*(SORT(.z_init_SMP[1-9][0-9]_*)));
  __init_end = .;
  > RAM

在Zephyr里它会将驱动根据等级放入名为z_init_Levelprio的段里,例如你的驱动等级是PRE_KERNEL_1,优先级是5,那么就会放入到z_init_PRE_KERNEL_15段里,这些是通过GCC__attribute__((__section__))特性实现的,同时可以看到Link脚本里使用了LD函数,SORT,它会对段进行排序,排序的方式与SORT函数有关,SORT比较方法与STRCMP类似,它会将段名字的ASCII码相减,将结果从小到大进行排序。

它在排序时可以看到它用了正则参数:

KEEP(*(SORT(.z_init_PRE_KERNEL_1[0-9]_*))); KEEP(*(SORT(.z_init_PRE_KERNEL_1[1-9][0-9]_*)))

排序了两次,首先排序的是z_init_PRE_KERNEL_1[0-9]_*,这段代码代表排序9以内编号的段,然后在排序10->99之间编号,这就代表Zephyr将驱动分为了两组,一组是编号0-9以内的,另一组是10-99以内的,这也意味着我们驱动优先级最大是99

最后用__init_EARLY_start...指向这些段的首地址,然后在c语言里引用它们,引用函数在kernel/init.c:z_sys_init_run_level

static void z_sys_init_run_level(enum init_level level)

        static const struct init_entry *levels[] =  
                __init_EARLY_start,  
                __init_PRE_KERNEL_1_start,
                __init_PRE_KERNEL_2_start,
                __init_POST_KERNEL_start,
                __init_APPLICATION_start,
#ifdef CONFIG_SMP
                __init_SMP_start,
#endif
                /* End marker */    
                __init_end,
        ;
        const struct init_entry *entry;

        for (entry = levels[level]; entry < levels[level+1]; entry++) 
                const struct device *dev = entry->dev;
                int rc = entry->init(dev);

                if (dev != NULL) 
                        /* Mark device initialized.  If initialization
                         * failed, record the error condition.
                         */
                        if (rc != 0) 
                                if (rc < 0) 
                                        rc = -rc;
                                
                                if (rc > UINT8_MAX) 
                                        rc = UINT8_MAX;
                                
                                dev->state->init_res = rc;
                        
                        dev->state->initialized = true;
                
        

在引用时,段已经从小到大排序好了,所以只需要通过指针指向段首地址,像链表一样去调用,然后依次调用init函数就完成了驱动的初始化,需要值得注意的是Zephyr在调用init函数指针时并没有判断是否为空指针,也就意味着如果你注册的驱动如果没有给init会导致call 0的操作,从而引发地址异常中断导致内核崩溃。

驱动API

以下API仅供驱动使用, 驱动API存放于device.h文件里

DEVICE_DEFINE

作用

创建设备对象,同时将设备设置为启动初始化,需要开发者指定初始化函数,由这个宏创建的设备对象会在z_cstart函数里进行初始化,此宏函数不能用于基于设备树节点来创建的设备对象

函数原型

DEVICE_DEFINE (dev_id, name, init_fn, pm, data, config, level, prio, api)

参数

  • dev_id - 全局设备结构体名称,用作变量名


  • name - 设备的字符串名称,将存储在device结构体里的name变量中。此名称可用于使用device_get_binding函数查找绑定设备。这必须少于Z_DEVICE_MAX_NAME_LEN (截止目前Zephyr 3.3 这个宏定义值为48)个字符(包括终止NULL),才能从用户模式进行查找。


  • init_fn - 指向设备初始化函数,在初始化阶段会被调用,此函数指针不能为NULL, ini函数原型要求:int (*init_func)(const struct device *port)


  • pm - 指向电源管理资源,如果没有设置为空,如果有将会保存到device结构体里的pm变量中,可以为NULL


  • data - 指向设备的私有可变数据的指针,该数据将存储在device结构体里data变量中,可以为NULL


  • config - 指向设备的私有常量数据的指针,该数据将存储在device结构体里的config变量中,这个变量是常量一旦指定就不可变更,可以为NULL


  • level - 设备的初始化等级


  • prio - 设备在不同等级下的初始化优先级


  • api - 驱动接口指针,提供给应用层的API,可以为NULL

DEVICE_NAME_GET

作用

用于展开DEVICE_DEFINEdev_id定义的结构体全名,DEVICE_DEFINE内部在使用dev_id定义全局设备对象结构体变量名称时会做拼接,这个宏函数可以用来获取拼接后的名称,仅用于预编译阶段使用。

函数原型

DEVICE_NAME_GET (dev_id)

参数

  • dev_id - 设备标识符

返回值

返回展开后的全局设备对象变量名

DEVICE_GET

作用

根据dev_id获取指向DEVICE_DEFINE创建的全局设备对象结构体指针

函数原型

DEVIE_GET (dev_id)

参数

  • dev_id - 设备标识符

返回值

返回设备对象结构体指针

DEVICE_NAME_GET

作用

用于展开DEVICE_DEFINEdev_id定义的结构体全名,DEVICE_DEFINE内部在使用dev_id定义全局设备对象结构体变量名称时会做拼接,这个宏函数可以用来获取拼接后的名称,仅用于预编译阶段使用。

函数原型

DEVICE_NAME_GET (dev_id)

参数

  • dev_id - 设备标识符

返回值

返回展开后的全局设备对象变量名

DEVICE_DECLARE

作用

声明静态设备结构体对象,需要在代码顶层使用它,它的作用是定义一个与全局设备对象无关的静态设备对象,它只能在当前代码文件中使用,它的目的是为了解决循环依赖的问题,例如当你注册IRQ时,这个时候在DEVICE_DEFINE函数还没有定义全局设备对象时你是无法使用全局设备对象的,所以可以定义一个相当于私有的静态设备对象,供你使用,可以将中断与设备对象区分开,同时定义的静态设备结构体对象是不会参与初始化的,其实就相当于定义了一个静态的struct device结构体。

函数原型

DEVICE_DECLARE (dev_id)

参数

  • dev_id - 设备标识符

设备对象结构体

结构体定义

struct device 
      const char *name;
      const void *config;
      const void *api;
      void * const data;
;

成员变量作用

const char *name:

设备对象名称, 可用于device_get_binding函数查找绑定设备

const void *config:

设备配置常量数据结构体

const void *api:

设备API指针

void * const data:

设备数据结构体,该指针常量,但要求data为常量

驱动项目组成部分

在Zephyr驱动框架中,你的驱动应由如下几个部分组成:

  1. src (源代码目录) (必须)

  1. Kconfig (内核配置文件) (必须)

  1. CMakeLists.txt (编译结构描述) (必须)

驱动项目文件夹应存放在$ZEPHYR_HOME/drivers目录下

在Zephyr的drivers目录下有CMakeLists.txt文件,这个文件里定义了哪些子模块需要编译到Zephyr内核里去,它与driver目录下的Kconfig配合使用

第一个驱动: Hello Word

编写代码

首先在drivers目录下创建我们的driver

mkdir drivers/my_driver

创建第一个Hello word并进入到这个目录里构建我们的项目

mkdir drivers/my_driver/hello_word & cd driver/my_driver/hello_word

创建src目录

mkdir src

创建cmake文件和Kconfig

touch CMakeLists.txt Kconfig

创建include文件用来定义驱动结构体

touch my_driver.h

定义API结构体

typedef void (*Func) (void);
struct my_driver_api 
    Func open;
    Func write;
    Func close;
;

创建main文件并开始编写驱动

touch src/main.c & vim src/main.c

首先包含基础头文件

#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <my_dirver.h>

然后根据DEVICE_DEFINE函数的要求,创建API接口

在开发过程驱动过程中,驱动API的调用不应是轮询或异步实现的,应是同步的,同时如果驱动可以支持中断的情况下应尽量支持中断,除非硬件环境不支持

void my_driver_open (void) 

    printk("hello word open\\n");


void my_driver_write (void) 

    printk("hello word write\\n")


void my_driver_close(void) 

    printk("hello word close\\n")
 

static int my_driver_init(const struct device *port) 
    
    return 0;


static const struct my_driver_api api = 
    .open =  my_driver_open,
    .write =  my_driver_write,
    .close = my_driver_close,
;

最后使用DEVICE_DEFINE创建设备对象

DEVICE_DEFINE(my_driver_0, "my_driver", my_driver_init, NULL, NULL, NULL, POST_KERNEL, 0, &api);

编写构建脚本

修改CMakeLists.txt文件,输入如下内容

zephyr_library()
zephyr_include_directories_ifdef(CONFIG_MY_DRIVER include)
zephyr_library_sources_ifdef(CONFIG_MY_DRIVER src/main.c)

zephyr_library: 使用zephyr cmake库

zephyr_include_directories_ifdef: 添加头文件到构建环境

zephyr_library_sources_ifdef: 使用条件编译,如果定义了CONFIG_MY_DRIVER 则将main.c包含进来编译

然后在打开Kconfig文件,对内核进行配置

首先定义一个menuconfig告诉内核,这个CONFIG属性用于配置我们的driver,如果想详细学习Kconfig可以到Linux官网学习Kconfig语法

menuconfig MY_DRIVER
     bool "My driver"
     help
         This is my test driver

因为我们里面使用了LOG功能所以需要将LOG模块编译进来,如果应用层没有开启LOG模块会导致编译出现未定义的情况

config LOG
     default y

config LOG_PRINTK
     default y

加入到内核模块里

打开driver目录下的CMakeLists.txt文件在最后一行加入我们的驱动文件条件编译

add_subdirectory_ifdef(CONFIG_MY_DRIVER my_driver/hello_word)

最后将Kconfig加入到driver目录下的Kconfig文件里

source "drivers/my_driver/hello_word/Kconfig"

编写应用层

在应用层的prj.conf文件里加入我们的驱动条件宏

CONFIG_MY_DRIVER=y

编写调用代码

#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <my_driver.h>

void main(void) 

     const struct device *dev = device_get_binding("my_driver");
     if(dev == NULL) 
             printk("can't open my_driver\\n");
             return;
    
    
     struct my_driver_api *api = dev->api;
    api->open();
    api->write();
    api->close();

构建生成

west build -b qemu_x86 samples/my_test/test_driver

运行:

west build -t run

输出结果:

-- west build: running target run
[0/1] To exit from QEMU enter: 'CTRL+a, x'[QEMU] CPU: qemu32,+nx,+pae
SeaBios (version zephyr-v1.0.0-0-g31d4e0e-dirty-20200714_234759-fv-az50-zephyr)
Booting from ROM..
*** Booting Zephyr OS build zephyr-v3.3.0-475-g38f554ef4f99 ***
hello open
hello write
hello close

因为配置了Kconfig,你可以在build时使用menuconfig来查看你的驱动

在Device Driver菜单里

以上是关于Zephyr驱动程序框架简介的主要内容,如果未能解决你的问题,请参考以下文章

Zephyr单元测试框架:ztest/twister的使用和介绍

Zephyr单元测试框架:ztest/twister的使用和介绍

Zephyr:undefined reference to `__device_dts_ord_xx‘

Zephyr:undefined reference to `__device_dts_ord_xx‘

Zephyr RTOS -- 源树结构及软硬件配置过程简介

Zephyr RTOS -- 源树结构及软硬件配置过程简介