学习链接:哔哩哔哩
讲解清晰,逻辑清楚。
为什么zephyr火了?
因为zephyr把开发流程和驱动文件以及API接口都规范化了,不同平台的移植更方便了。zephyr为大部分常用外设都设计了一套内核driver框架以及与该driver对应的用户态API,拿到一块新开发的芯片,驱动开发时只需要填充driver框架中要求你实现的接口就行了。但麻烦也就麻烦在这一点,你想直接使用一款板子的某个外设,你需要熟悉三点,First:先去先得去 zephyr官网 学一下这个外的驱动框架,如果有的话。Second:根据设备树的compatible在zephyr/drivers找对应的driver看看这个driver有哪些ops。Third:去zephyr/include/zephyr/drivers/找到外设在用户态的系统调用接口有哪些。
一、学习目标
- 学习使用线上文档,zephyr官网链接中文文档
- 官网的 zephyr API查询
- 官网的 zephyr Peripherals介绍,这个网站好多东西来自
zephyr/include/zephyr/drivers/
目录 - 学习zephyr目录结构体,各目录文件的作用,知道怎么看代码(三个vim窗口看代码,其实也不用,学会了cscope跳转其实挺快的,只有在自己确定了调用路径的情况下)。
- 理解zephyr的核心概念如
CMake, Kconfig, Devicetree
等。 - 理解
build, fluch, debug
等工作流程。自己写一个 eDMA,TPM timer 工程。
二、线上文档介绍
线上英文文档,介绍详细,包括zephyr是什么,在不同的OS上怎么搭环境,怎么编译,编译原理,支持哪些厂家的demo板子,厂家对特定的demo板已经对哪些外设做了支持,外设的编程框架以及相关API怎么用。等等,太多了。
三、zephyr的核心概念
- Devicetree:【链接】如果了解Linux kernel设备树的用法,会好理解。如果在设备树中设置了 status = okay 后,这个设备的驱动就被编译进入kernel了(详情看
zephyr/drivers
目录中对应的宏的作用),之后就可以通过zephyr提供的用户态API去调用内核驱动接口。 - Kconfig 与 CMake: 首先 Kconfig 根据用户的配置选项
prj.conf
生成最后的.config
,随后执行编译过程时,查看用户的CMakeLists.txt
文件可知依据.config
中的选项选择要编译的文件。这两个过程的进行都依赖CMake工具。 - 代码分层: 你自己写的代码算应用层,
zephyr/drivers
中代码算驱动层,modules/hal/
中的代码算 hal 层。 - west工具: zephyr中有30多个git仓库,而west可以通过一行命令对多个目录进行管理。west还可实现编译功能。详情看官网West (Zephyr’s meta-tool)。
【必须注意】:如何依据dts
找到对应zephyr/drivers
目录中的哪个驱动程序呢?
搜compatible
, 但是需要把所有的非字母字符改为下划线,如设备树中的“compatible = "nxp,tpm-timer"”
这个TPM设备对应的驱动程序你就需要去zephyr/drivers
目录找“nxp_tpm_timer”
这个字符串。
驱动程序如何编译进zephyr内核的? 去看吧,只要设备树打开了,宏就编译了,一大堆宏就工作了,贼麻烦。
四、zephyr 编译环境搭建(待补充)
五、zephyr 目录结构(待补充)
zephyrproject 中有:build ,modules, zephyr
等目录。
build/zephyr/
中有zephyr.elf以及zephyr.bin编译结果
modules/hal/nxp 是个由nxp维护的git仓库,随着zephyr的版本在更新【好像:如果芯片厂家驱动更新了,west update 就能完成更新,方便在这个地方】modules/hal/nxp/mcux/mcux-sdk/drivers/
中有NXP支持的各种 SAI,I2C, EDMA 等底层hal层驱动,直接读写寄存器。zephyr/
是个git仓库,最为重要。 依据real_time_edge_UserGuide需要切换到我们指定的branch【注意DN有两个工程,一个是纯zephyr, 一个是集合了zephyr和FreeRTOS】.zephyr/test
中有各种外设的使用测试用例如DMA, SAI,I2C,帮助你熟悉zephyr的各种接口使用。可以在这个目录拷贝修改测试工程。(也可也在zephyr/目录创建修改自己的工程。需要补充prj.conf
,sample.yaml
,src
等文件。)zephyr/samples/
也有一些测试用例。zephyr/drivers/
驱动程序目录,通过把dts
中的compatible
中对应的非字符字符串转换为下划线后可找到设备对应的驱动程序。该层代码会调用hal
层驱动。zephyr/include/zephyr/drivers/
存放可以调用driver中提供的给ops的使用接口API,这样用户态代码就跟驱动程序解耦了。
《dts相关文件》
zephyr/boards/nxp/
中有nxp厂商维护了board级别dts。zephyr/dts/arm64/nxp/
nxp厂商维护的芯片级别dts。zephyr/dts/bindings/
通过yaml
这种描述语言进行检测dts中的描述是否合规,根 Linux kernel 中 Documentation/devicetree/bindings/ 目录的yaml一样。
【问题1】:如果使用某款nxp开发板,使用zephyr/drivers/
中nxp家的驱动,他调用modules/hal/nxp/
中的哪个文件呢?
答:drivers层驱动通过include调用hal层驱动的头文件,比如:
zephyr/drivers/counter/counter_mcux_tpm.c
通过#include <fsl_tpm.h>
使用hal库, 该文件其位于modules/hal/nxp/mcux/mcux-sdk/drivers/tpm/fsl_tpm.h
。(mcux/mcux-sdk/devices/MIMX9352/drivers/edma4/
目录中也有对特定芯片的驱动,我有点晕)
【问题2】:如何查询dts中参数的解释呢?
去“”目录查询对应的compatible, 找到对应外设的yaml文件。 比如imx93的dts的tpm外设有compatible=“nxp,tpm-timer”, 就可以grep找到yaml对应zephyr/dts/bindings/timer/nxp,tpm-timer.yaml。
六、IMX9352 上创建 zephyr project(待补充)
需要了解:
分为zephyr目录外的工程和zephyr目录内的工程。
创建文件:app.overlay(设备树调整), prj.conf(CONFIG选项设置), CMakeLists.txt(工程名,源文件名,板子名称设置),src/main.c(源文件)。
需要了解各个文件的作用。官网有介绍。
七、运行测试(待补充)参考realtimedegeUG
先按照官网的方法安装:https://github.com/nxp-real-time-edge-sw/heterogeneous-multicore
python3.10 -m venv ~/zephyrproject/.venv //创建了一个名为 .venv 的虚拟环境,位于 ~/zephyrproject/ 目录下
source ~/zephyrproject/.venv/bin/activate //激活虚拟环境
pip install west
pip install pyelftools
cd ~/frdm_demo/workspace/
git clone https://github.com/nxp-mcuxpresso/FreeRTOS-Kernel -b manifest-rev
git clone ssh://git@bitbucket.sw.nxp.com/~nxf24402/mcux-sdk.git -b rtedge-v3.0-frdm //在west update后这个会自动还原,需手动调整
cd middleware
git clone https://github.com/nxp-mcuxpresso/lwip -b manifest-rev // 5e1d1678
git clone https://github.com/nxp-real-time-edge-sw/soem -b manifest-rev // 85793e8
git clone https://github.com/nxp-mcuxpresso/mcux-sdk-middleware-multicore -b manifest-rev multicore // 5b87729
cd multicore
git clone https://github.com/nxp-mcuxpresso/rpmsg-lite -b manifest-rev // 436596c
mkdir frdm_demo/workspace/modules/hal/nxp
cd frdm_demo/workspace/modules/hal/nxp
git clone https://github.com/nxp-zephyr/hal_nxp -b manifest-rev // cf6917ac
cd ~/frdm_demo/workspace/
git clone ssh://git@bitbucket.sw.nxp.com/~nxf24402/zephyr.git -b Real-Time-Edge-v3.0-202412-frdm //在west update后这个会自动还原,需手动调整
git clone ssh://git@bitbucket.sw.nxp.com/~nxf24402/heterogeneous-multicore.git -b rtedge-v3.0-frdm //在west update后这个会自动还原,需手动调整
上面这个资源,因为代码不能在串口显示,需要 git revert d5ccfdefcee13e82ef8dd66803d229bccc884ef0
cd heterogeneous-multicore
./all_compile.sh
或./frdm_servo -d 0x80
深入解读 - 设备树与设备驱动模型
1. 设备树概述(语法,如何配置硬件,c代码如何访问) driver的实现
讲解zephyr设备树的意义:单片机开发过程中,一般使用宏定义来表示硬件引脚,但是为了达成高内聚低耦合的软件架构,又设计了把初始化函数和中断放在一个board.c文件,其他按照功能分开存放。虽然已经很好了,但是切换硬件平台后还是不能很好的移植。(比如我们更换平台后,需要更换调用的GPIO library 库) 而且不同的人又有不同的代码风格,但是zephyr横空出世。规范了这一切,都得按照我的规矩来。这样导致平台移植非常方便,这简直是提高了人类的效率啊。
思考: zephyr把硬件都规定好了,他能不能自己生成board.c文件呢?
zephyr的设备树简单了解:芯片厂商会自己适配zephyr,客户可以根据需要修改,同时zephyr会调驱动把有设备树的外设初始化好。同时客户还能够调用和操作寄存器,控制管脚,好灵活。好牛逼啊感觉。
2. device tree的结构和语法
首先按照总线的主从关系,其次按照硬件的包含关系。(总线下面的节点都有地址,而如LED等外设虽然用到了GPIO但是没有地址,所以直接成为根节点下的子节点)
device tree 的适用范围:device tree 描述的是板卡级别的硬件信息,当一个办卡上有两个MCU他们不能公用device tree. 当一个MCU有两个独立固件的core,他们不能共用device tree,等。
设备树语法跟linux里面一样只是需要注意写法:device tree 的节点: name@address
节点的name可以是字母数字下划线,加减号标点等等。但是c代码得到设备树名称的时候会把不符合字母数字下划线的那部分特殊符号变成下划线。
如果有reg属性,则address必须和reg属性的第一个寄存器地址值相等。如果没有reg属性,则@address必须省略。
dts可以引用其他的dts或dtsi,这样板卡级dts就可以引用芯片厂商写好的芯片级dtsi文件。缩短dts开发时间。
dts还可以引用c头文件来使用一些枚举值或宏定义。
devicetree文件的位置:
1. 新建工程时可以选择板卡,板子的dts文件位于:zephyr/board/目录
2. 板卡引用的dtsi文件位于 zephyr/dts/arm/nordic 目录
3. 用户不需要自己写dts,直接在自己的工程目录中的board.overlay增删改节点属性。覆盖开发板的配置即可。
使用label指定节点并覆写其属性,/delete-property/ led1;可以删除属性,/delete-node/ leds;可以删除节点。
4. 最后,构建项目时,zephyr build system 会使用一系列脚本把board,soc,用户的overlay合并起来。如果构建目录的名称是build,最终合并后的完整文件位于:
${projector_folder}build/zephyr/zephyr.dts
5. 了解即可,zephyr最终会把zephyr.dts处理成c语言头文件,位于S{project_folder}/build/zephyr/include/generated/devicetree_generated.h。代码最后得到设备树的信息其实是通过这个文件。
3. device tree如何配置硬件信息
reg = <addr1 add1_leng addr2 add2_leng> 可见reg由多对总线上的地址和长度信息组合而成。
当一个节点定义的ranges属性,那他的子节点就可以使用相对地址而非绝对地址。 ranges = <子空间首地址 父空间首地址 长度信息>
status ,只需要注意"okay" 和 disabled, 决定是否初始化该外设。
compatible 设备会通过这个找到合适的驱动程序
设备树中的"域", 除了地址树以外,比如GPIO树其下面挂载了LED BUTTON等,中断树,ADC树下面挂载了很多ADC设备。 这些树的出现是为了更好的描述网状的硬件关系。这些虚拟概念上的树被称为"域"。每个域都有自己的根节点被称为controller, 控制器控制了整个域的相关硬件。控制器节点通过给自己一个*-controller的属性(也可能没有),如:gpio-controller, 然后这个GPIO域的子节点就可以通过前面介绍的phandle-array属性来使用这个gpio控制器,如:gpios = <&gpio0 0x18 0x11> 第一个数值指向控制器节点,后续的值是节点在这个域中的配置,被称为specifier。 使用哪一个gpio,后一个指定gpio的配置信息。
设备树有一些奇怪的规则,比如有的域控制器有controller有的没有,而且zephyr能自动检测你写的dts是不是符合这些规则的,怎么实现的呢?通过yaml这种描述语言进行检测的,比如这个 ./zephyr/dts/bindings/dma/nxp,mcux-edma.yaml ,这个就是device bingding 文件。 这个文件名字就是compatible属性,zephyr的编译器会用bing文件去dts检查符合这个compatible属性的设备树节点。这个binding文件可以约束子节点怎么写,还可以给specifier中的节点数值做含义解释。
zephyr build system 会从一下位置寻找binding文件:
./zephyr/dts/bindings/
${board_dir}/dts/bindings/
${project_dir}/dts/bindings/
也可以在Cmakelist.txt中,用list{APPEND DTS_ROOT /path/to/your/dts}增加binging文件的目录
也可以在编译时增加选项 west build -b <board_name> --DTS_ROOT=<path/to/your/dts>
还有一些特殊的节点:
chosen: 为device kernel 选择特定设备
aliase: 为节点起一个别名,别名是属性名
pinctrl: 直属于根节点,数字IO复用
/zephyr,usr: 方便用户开发的节点,直接写spicifier和配置项,不用写binding了
4. 如何在c代码中获取device tree??
通过API获取,需要包含头文件:#include <zephyr/device_tree.h>
为了获得节点属性,需要先获得节点id作为句柄(节点id就是node identifier),节点id本质上是devicetree_generated.h中的宏定义。方式如下:
DT_ROOT 得到根节点id
DT_PATH(soc,serial_40001000) 得到/soc/serial@40001000
DT_NODELABLE(serial1) 根据dts中定义的lable找到节点
DT_CHOSEN(zephyr_console) 根据chosen节点配置:zephyr,console=&uart0
还有很多子节点找父节点,父节点找子节点的方式。 还有一种方式,是通过实例ID的方式获取节点ID,如DT_INST(0, nordic_nrf_timer)对应的就是nordic,nrf_timer的第0个实例节点。如果节点中有多个节点有同一个comaptible,就是一个compatible对应多个实例,好处是什么呢就是可以放到for循环中遍历了呀。(但是注意不是c语言中的for,因为这些API都是预编译后的结果,括号里的当然也不是真正的输入参数,哈哈)
DT_PROP 宏可以得到普通属性
DT_REG_ADDR 和 DT_REG_SIZE 宏可以读取reg的地址和长度
如果一个节点的属性是其他节点可以通过DT_HANDLE_BY_IDX得到内个节点
上面说了zephyr提供的获取节点的ID虽然都不能传入遍历以便于通过for循环调用,但是zephyr提供了可以遍历这些节点的API,如DT_FOREACH_NODE(fn)为设备树中的每一个节点调用宏函数fn。 还有很多API请看 https://docs.zephyrproject.org/latest/build/dts/api/api.html#for-each-macros (这些API看似是循环,其实是宏)
这里这个fn宏函数是用代码模板。再看看
还有很多API方便你直接读取specifier等。具体参考
https://docs.zephyrproject.org/latest/build/dts/api/api.html#hardware-specific-apis
https://docs.zephyrproject.org/latest/hardware/index.heml
5. Zephyr Driver 的实现方式
什么是驱动程序?因为zephyr中驱动程序是“面向对象”的,他有个device结构体,结构体如下:
struct device {
const char *name;
const void *config;
const void *api;
struct device_state *state;
void *data;
......
};
可以看到这个结构体很宽泛,驱动程序需要在application启动之前把这个结构体填充好,然后application才能调用这些api。
根据zeyphr的启动流程,可以把驱动程序放在一下五个级别中的任何一个级别:
start up-> EARLY --> PRE_KERNEL_1 --> PRE_KERNEL_2 --> RTOS KERNEL 启动 --> POST_KERNEL --> APPLICATION -->MAIN
| 放这的不能有log | | 放这里可以打log |
Application需要得到这个device结构体才能操作外设对吧,那app如何得到device呢?两种方式:通过name, 通过设备树node id。
第一种方式中,在驱动程序中通过DEVICE_DEFINE宏来定义device结构体,其中第二个参数为该结构体的lable吧。在app中通过device_get_binding这个API即可得到这个device.
第二种方式中,在驱动程序中通过DEVICE_DT_DEFINE来定义结构体,并与节点绑定。在app中通过DEVICE_DT_GET(node id)宏来获得device.(node id可以用DT_PATH(节点名)得到)
prj.config中的CONFIG_*选项与dts中的status状态有什么关系?
前者决定是否编译进入固件(嵌入到硬件设备的软件代码),后者决定驱动程序使用宏遍历device结构体时能够为这个okay的节点创建device对象。只有两者都启用app才能操作这个节点。
6. Zephyr标准驱动
zephyr是个跨平台操作系统,少不了对标准硬件的跨平台支持。
详见:https://docs.zephyrproject.org/latest/hardware/peripherals/index.html
以DMA为例,在zephyr/include/zephyr/drivers/dma.h目录中规定好了dma驱动应该有哪些API。在zephyr/drivers/dma/目录下有各个厂家写好的对字节芯片的driver驱动。通过zephyr/drivers/dma/Kconfig.nxp_edma可以看到各个CONFIG选项的作用,在./zephyr/build/zephyr/.config设置好对应的CONFIG_*=y,则zephyr就会把板子对应厂商的dma驱动编译进来。
zephyr标准驱动支持硬件的全部功能吗???
zephyr只支持最基础最标准的硬件驱动,不支持各个厂商的硬件特性。如果想要硬件特性功能,需要使用厂商自己的driver library, 或者直接写寄存器。