Zephyr 开发
DTS 介绍
Zephyr 配置系统
zephyr编译分为两个阶段:
configuration阶段: 执行cmake, 处理DTS, Kconfig
DTS会被转换为
build/zephyr/include/generated/zephyr/devicetree_generated.hkconfig会被转为:
build/zephyr/include/generated/zephyr/autoconf.h
build阶段: 执行make或者ninja工具生成固件
configuration阶段主要流程参考下图:
DTS 配置
DTS基本语法
DTS由节点组成, 每个节点中包含若干属性或子节点.
节点基本形式:
node_label:node_name@unit-address{...}属性基本形式:
prop_name = value;, 是一个键值对(bool类型除外, 它仅有一个标识符, 出现即表示True)
下图示例了一个简单的 DTS 文件的结构:
一个实际硬件的例子
假设有如下图所示的硬件结构:
则可使用如下 DTS 结构描述该硬件:
编写 DTS 文件如下:
/dts-v1/;
/ {
clocks {
fixed_clk: fixed_clk_node {
};
main_clk: main_clk_node@41008000 {
};
};
soc {
usb: usb@40140000 {
clocks = <&fixed_clk>;
};
uart0: serial@4100c000 {
clocks = <&main_clk UART0_CLK>;
};
spi0: spi@40124000 {
clocks = <&main_clk SPI0_CLK>;
};
}
};
unit-address
节点中的 unit-address 可缺省; 当存在时, 对应节点必须包含 reg 属性, 且其第一个元素值必须等于 unit-address
注意
unit-address 可以作为节点标志符的部分, 即两个节点如果 node_name 相同但 unit-address 不同是允许的, 但如果这两个节点没有 unit-address 则会报错
重要属性
reg
是若干组
(address, size)序列, 每一组描述一个寄存器块, 具体含义因设备不同而不同, 例如:uart设备:
reg = <0x100000 0x100>, <0x200000 0x100>;, 有两个寄存器块cpu:
reg = <0>通常在多cpu系统中表示cpu编号, 和cpu@0保持一致
reg中
address和size两个字段位宽是由 其所属节点的父节点 中的address-cells和size-cells两个属性描述的, 例如:/ { clocks { #address-cells = <2>; #size-cells = <1>; main_clk: main_clk_node@41008000 { compatible = "realtek,ameba-rcc"; reg = <0x12345678 0xABCD0000 0x400>; }; }; }
clock节点中#address-cells和#size-cells分别约束其 直接 子节点(main_clk)中的reg中:address由一个32bit cell组成size由一个32bit cell组成
当
#size-cells为0时,reg中不需要size部分
compatible
compatible属性是由一个或多个字符串组成, 每个字符串都对应某个 DTS binding 文件, 在该文件中定义了包含该属性的节点的programming model使用了
compatible的节点也就必须要遵守这些programming modelcompatible字符串与 DTS binding 文件绑定方法是: DTS binding 文件中compatible字段值等于该字符串, 这样DTS脚本会自动通过compatible属性值找到对应的 DTS binding 文件当
compatible具有多个值时, 编译脚本会从左到右依次匹配直到找到第一个匹配成功的 DTS binding 文件, 其他则忽略
Zephyr中DTS组织
在zephyr中一个板子DTS描述是由多个多个dts(i)文件完成的
这些文件来自三个按照不同抽象层次划分的层次:
arch层: 最高程度抽象, 定义CPU指令集架构等相关的硬件描述
soc层: 中等程度抽象, 通常是一个供应商提供的某个系列的硬件描述, 包括外设, 时钟控制, 中断, 引脚等
board层: 最低程度的抽象, 对应一个具体的硬件实物, 具体描述了配置板载外设, 引脚映射等
board层DTS是处理脚本入口, 通常会include soc层DTS, soc层DTS又会include arch层DTS
DTS处理脚本最终生成一个完整的dts(build/zephyr/zephyr.dts)
DTS 处理的输入输出参考下图:
DTS binding
备注
功能:
DTS binding文件定义了引用它的DTS节点的programming model, 可以简单理解为规范或者约束, 如节点属性要求(有哪些属性, 属性类型, 可取值等), 子节点的binding, bus信息等
DTS binding支持通过include引用其他文件, 实现复用功能
DTS binding文件位置: 默认在 zephyr/dts/bindings 下查询(一般建议放这里), 或:
- CMakeLists.txt: list(APPEND DTS_ROOT /path/to/your/dts)
- west命令参数: -DDTS_ROOT=/path/to/your/dts
DTS binding基本语法
DTS binding 是yaml格式, 下图是其中各字段的介绍及示例:
下面对其中比较重要的一些字段进行详述:
include
include的binding文件会和当前binding文件进行融合形成一个完整的binding文件
如果当前binding文件中某个字段的值和其include中的同一个字段值不同会报错, 除了属性中的
require字段还可以选择性导入include文件中的部分属性, 参考如下示例:
include:
- name: foo.yaml
property-allowlist: //Import including some specific properties
- i-want-this-one
- and-this-one
- name: bar.yaml
property-blocklist: //Import excluding some specific properties
- do-not-include-this-one
- or-this-one
compatible
主要用于和DTS文件中
compatible属性进行匹配将binding文件绑定到对应节点中通常取值为:
<vender>,<device>
properties
主要用于为DTS节点中的属性添加描述和约束, 完整语法如下
properties:
<property name>:
required: <true | false>
type: <string | int | boolean | array | uint8-array | string-array |
phandle | phandles | phandle-array | path | compound>
deprecated: <true | false>
default: <default>
description: <description of the property>
enum:
- <item1>
- <item2>
...
- <itemN>
const: <string | int>
required: 设为true表明该属性必须在对应节点中声明, 否则会脚本报错type: 描述属性类型, 详见 DTS属性数据类型deprecated: 设为true表明该属性代表已弃用,编译过程中工具将报告警告default: 为属性设置一个默认值enum: 一个list, 指定该属性的可取值, 如果节点中设置了之外的值则会报错const: 表明属性是一个常量
Specifier cell
Specifier cell的功能可以理解为指定device的init parameter, 他们可以在device被实例化(phandle引用)的时候传入参数进行初始化. 下面以一个例子讲解其工作原理. 假设一个DTS文件如下:
/ {
soc {
dma: dma@40110000 {
compatible = "realtek,ameba-gdma";
reg = <0x40110000 0x3C0>;
#dma-cells = <3>;
};
i2c0: i2c0@41108000 {
compatible = "realtek,ameba-i2c";
reg = <0x41108000 0x100>;
dmas = <&dma 4 22 0>,
<&dma 5 21 0>;
dma-names = "rx", "tx";
};
};
}
重点关注其中定义的硬件逻辑:
SOC节点包含了dma子节点(第3行), i2c0子节点(第9行), 其中i2c0这个设备用到两个dma实例(第12,13行)分别起名为rx/tx(第14行).
其中
dmas属性是一个phandle-array类型, 包含两个dma实例, 每个实例使用了三个参数进行初始化(4 22 0和5 21 0).这里三个初始化参数的规范就是通过binding文件中Specifier cell字段定义的, 具体见binding文件
realtek,ameba-gdma.yaml:compatible: "realtek,ameba-gdma" include: dma-controller.yaml properties: "#dma-cells": const: 3 dma-cells: - channel //Select channel for data transmitting - slot //Handshake interface index, ref to ameba_gdma.h - config //include direction/addr inc/data width/msize
其中:
property中的"#dma-cells"属性规定了参数个数dma-cells描述了这三个参数的名称, 在c代码中可以通过对应名称访问这三个参数
在c代码中访问这些参数:
#define I2C_DMA_CHANNEL_INIT(index, dir) \
.dma_dev = AMEBA_DT_INST_DMA_CTLR(index, dir), \
.dma_channel = DT_INST_DMAS_CELL_BY_NAME(index, dir, channel), \
.dma_cfg = AMEBA_DMA_CONFIG(index, dir, 1, i2c_ameba_dma_##dir##_cb),
其中:
index参数是遍历多个i2c的索引dir是访问dmas的索引, 这里dir可以取rx或tx即可channel即对应binding文件中的dma-cells中所描述的第1个参数所以这里
.dma_channel最终的值就是4或5
注意
DTS中i2c0节点中的 dmas 属性名是固定的, 其单数形式 dma 必须和binding文件中的Specifier cell - 前的名称一样
DTS 操作指引
待补充
扩展阅读
DTS属性数据类型
Property type |
How to write |
Example |
|---|---|---|
string |
Double quoted |
|
int |
between angle brackets ( |
|
boolean |
for |
|
array |
between angle brackets ( |
|
uint8-array |
in hexadecimal without leading |
|
string-array |
separated by commas |
|
phandle |
between angle brackets ( |
|
phandles |
between angle brackets ( |
|
phandle-array |
between angle brackets ( |
|
NVIC 介绍
中断信息
DTS 配置信息
如下高亮行所示设置驱动的中断属性, interrupts 有两个入参, 第一个是中断号, 第二个是中断优先级。
nvic: interrupt-controller@e000e100 { #address-cells = < 0x1 >; compatible = "arm,v8.1m-nvic"; reg = < 0xe000e100 0xc00 >; interrupt-controller; #interrupt-cells = < 0x2 >; arm,num-irq-priority-bits = < 0x3 >; phandle = < 0x1 >; }; timer0: counter@40819000 { compatible = "realtek,ameba-counter"; reg = <0x40819000 0x30>; clocks = <&rcc AMEBA_LTIM0_CLK>; interrupts = <7 0>; clock-frequency = <32768>; status = "disabled"; };
获取中断属性
从DTS中获取信息的宏定义在文件 zephyr/include/zephyr/devicetree.h 中。
驱动代码注册中断需要使用中断号, 获取方式如下:
DT_INST_IRQN
#define DT_INST_IRQN(inst) DT_IRQN(DT_DRV_INST(inst))
获取 DT_DRV_COMPAT 设备中断号, 参数说明如下:
- inst:
设备实例编号
驱动代码注册中断时使用宏从DTS中获取优先级, 获取方式如下:
DT_INST_IRQ
#define DT_INST_IRQ(inst, cell) DT_INST_IRQ_BY_IDX(inst, 0, cell)
获取 DT_DRV_COMPAT 中断说明符的值, 参数说明如下:
- inst:
实例编号
- cell:
单元名称说明符
DT_INST_IRQ_BY_NAME
#define DT_INST_IRQ_BY_NAME(inst, name, cell) \
DT_IRQ_BY_NAME(DT_DRV_INST(inst), name, cell)
通过名称获取 DT_DRV_COMPAT 中断说明符的值, 参数说明如下:
- inst:
实例编号
- name:
小写字母和下划线的中断说明符名称
- cell:
单元名称说明符
DT_INST_IRQ_BY_IDX
#define DT_INST_IRQ_BY_IDX(inst, idx, cell) \
DT_IRQ_BY_IDX(DT_DRV_INST(inst), idx, cell)
通过名称索引获取 DT_DRV_COMPAT 中断说明符的值, 参数说明如下:
- inst:
实例编号
- idx:
中断说明符数组的逻辑索引
- cell:
单元名称说明符
中断优先级说明
DTS中配置为 arm,num-irq-priority-bits = < 0x3 >, 即优先级寄存器有三位, 最大8个优先级。
优先级相关宏在不同架构会有不同的实现, Cortex-M的实现在文件 zephyr/include/zephyr/arch/arm/cortex_m/exception.h 中, 如下:
#if defined(CONFIG_CPU_CORTEX_M_HAS_PROGRAMMABLE_FAULT_PRIOS)
#define _EXCEPTION_RESERVED_PRIO 1
#else
#define _EXCEPTION_RESERVED_PRIO 0
#endif
#define _EXC_FAULT_PRIO 0
#define _EXC_ZERO_LATENCY_IRQS_PRIO 0
#define _EXC_SVC_PRIO COND_CODE_1(CONFIG_ZERO_LATENCY_IRQS, \
(CONFIG_ZERO_LATENCY_LEVELS), (0))
#define _IRQ_PRIO_OFFSET (_EXCEPTION_RESERVED_PRIO + _EXC_SVC_PRIO)
#define IRQ_PRIO_LOWEST (BIT(NUM_IRQ_PRIO_BITS) - (_IRQ_PRIO_OFFSET) - 1)
#define _EXC_IRQ_DEFAULT_PRIO Z_EXC_PRIO(_IRQ_PRIO_OFFSET)
/* Use lowest possible priority level for PendSV */
#define _EXC_PENDSV_PRIO 0xff
#define _EXC_PENDSV_PRIO_MASK Z_EXC_PRIO(_EXC_PENDSV_PRIO)
中断注册
ISR分两种类型: 普通ISR和直接ISR, 注册方式也不相同。普通ISR有两种注册方式, 一种是编译期注册, 一种是运行期注册, 主要取决于是否在编译期已知参数; 直接ISR的注册需要先声明, 声明之后才可注册。可以参考 中断向量表查询 以帮助理解两种ISR注册方式的区别。
普通ISR
编译期注册
使用宏 IRQ_CONNECT 进行编译期ISR注册, 当所有的参数都已经决定时, 采用编译期注册。
相关实现在文件 include/irq.h 中, 如下:
#define IRQ_CONNECT(irq_p, priority_p, isr_p, isr_param_p, flags_p) \
ARCH_IRQ_CONNECT(irq_p, priority_p, isr_p, isr_param_p, flags_p)
参数说明如下:
- irq_p:
中断号
- priority_p:
中断优先级
- isr_p:
中断服务程序的地址
- isr_param_p:
传递给中断服务程序的参数
- flags_p:
特定于体系结构的 IRQ 配置标志
不同的架构会有不同的实现, ARM架构的实现在文件 zephyr/include/zephyr/arch/arm/irq.h 中, 如下:
#define ARCH_IRQ_CONNECT(irq_p, priority_p, isr_p, isr_param_p, flags_p) \
{ \
BUILD_ASSERT(IS_ENABLED(CONFIG_ZERO_LATENCY_IRQS) || !(flags_p & IRQ_ZERO_LATENCY), \
"ZLI interrupt registered but feature is disabled"); \
_CHECK_PRIO(priority_p, flags_p) \
Z_ISR_DECLARE(irq_p, 0, isr_p, isr_param_p); \
z_arm_irq_priority_set(irq_p, priority_p, flags_p); \
}
#define Z_ISR_DECLARE(irq, flags, func, param) \
static Z_DECL_ALIGN(struct _isr_list) Z_GENERIC_SECTION(.intList) \
__used _MK_ISR_NAME(func, __COUNTER__) = \
{irq, flags, (void *)&func, (const void *)param}
备注
Z_ISR_DECLARE 生成一个 struct _isr_list 结构体变量放入 .intList 段中, 结构体变量中保存了中断号irq,
标志flags, 中断函数func和传入中断函数的参数param。
struct _isr_list 结构体定义如下:
struct _isr_list {
/** IRQ line number */
int32_t irq;
/** Flags for this IRQ, see ISR_FLAG_* definitions */
int32_t flags;
/** ISR to call */
void *func;
/** Parameter for non-direct IRQs */
const void *param;
};
如果传入的fun是uart_isr, 调用 Z_ISR_DECLARE 的次数是第5次, 那么 Z_ISR_DECLARE 最终内容展开就是:
static __aligned(__alignof(struct _ist_list))struct _ist_list __attribute__((section(".intList")))
__used __isr_uart_isr_irq_5 = {
irq,
0,
uart_isr,
param}
结构体变量 __isr_uart_isr_irq_5 将被放入 .intList。
运行期注册
由于 IRQ_CONNECT 宏要求在编译时已知其所有参数,因此在某些情况下这可能不可接受。也可以使用 irq_connect_dynamic() 在运行时安装中断,
相关实现在文件 zephyr/include/irq.h 中, 如下:
static inline int
irq_connect_dynamic(unsigned int irq, unsigned int priority,
void (*routine)(const void *parameter),
const void *parameter, uint32_t flags)
{
return arch_irq_connect_dynamic(irq, priority, routine, parameter,
flags);
}
参数说明如下:
- irq:
中断号
- priority:
中断优先级
- routine:
中断服务程序的地址
- parameter:
传递给中断服务程序的参数
- flags:
特定于体系结构的 IRQ 配置标志
Cortex-M架构函数实现在文件 zephyr/arch/arm/core/cortex_m/irq_manage.c 中, 如下:
int arch_irq_connect_dynamic(unsigned int irq, unsigned int priority,
void (*routine)(const void *parameter), const void *parameter,
uint32_t flags)
{
z_isr_install(irq, routine, parameter);
z_arm_irq_priority_set(irq, priority, flags);
return irq;
}
z_isr_install() 注册中断如下, 主要是动态修改 _sw_isr_table:
void __weak z_isr_install(unsigned int irq, void (*routine)(const void *),
const void *param)
{
unsigned int table_idx;
table_idx = z_get_sw_isr_table_idx(irq);
/* If dynamic IRQs are enabled, then the _sw_isr_table is in RAM and
* can be modified
*/
_sw_isr_table[table_idx].arg = param;
_sw_isr_table[table_idx].isr = routine;
}
直接ISR
声明
和普通ISR不一样, 直接ISR的函数需要先声明, 相关宏在文件 include/irq.h 中, 代码如下:
#define ISR_DIRECT_DECLARE(name) ARCH_ISR_DIRECT_DECLARE(name)
#define ISR_DIRECT_HEADER() ARCH_ISR_DIRECT_HEADER()
#define ISR_DIRECT_FOOTER(check_reschedule) \
ARCH_ISR_DIRECT_FOOTER(check_reschedule)
不同的架构会有不同的实现, ARM架构的实现在文件 zephyr/include/zephyr/arch/arm/irq.h 中,代码如下:
#define ARCH_ISR_DIRECT_DECLARE(name) \
static inline int name##_body(void); \
ARCH_ISR_DIAG_OFF \
__attribute__ ((interrupt ("IRQ"))) void name(void) \
{ \
int check_reschedule; \
ISR_DIRECT_HEADER(); \
check_reschedule = name##_body(); \
ISR_DIRECT_FOOTER(check_reschedule); \
} \
ARCH_ISR_DIAG_ON \
static inline int name##_body(void)
#define ARCH_ISR_DIRECT_HEADER() arch_isr_direct_header()
static inline void arch_isr_direct_header(void)
{
#ifdef CONFIG_TRACING_ISR
sys_trace_isr_enter();
#endif
}
#define ARCH_ISR_DIRECT_FOOTER(swap) arch_isr_direct_footer(swap)
static inline void arch_isr_direct_footer(int maybe_swap)
{
#ifdef CONFIG_TRACING_ISR
sys_trace_isr_exit();
#endif
if (maybe_swap != 0) {
z_arm_int_exit();
}
}
直接ISR的声明主要是在原中断服务函数前后添加了一个头和尾, 为所有的直接ISR添加了一些统一的操作。
备注
内核允许将中断处理程序 (ISR) 直接安装到向量表中,以尽可能降低中断延迟。这使得 ISR 可以直接调用,而无需经过软件中断表。
但是,退出 ISR 时仍需要执行一些内核工作,例如上下文切换。连接到软件中断表的 ISR 会通过包装器 _isr_wrapper 自动执行此操作,
连接到硬件向量表的直接 ISR 则通过 ISR_DIRECT_DECLARE 中的 ARCH_ISR_DIRECT_FOOTER 添加了此操作。
注册
直接ISR通过 IRQ_DIRECT_CONNECT 注册, 宏定义在文件 include/irq.h 中, 如下:
#define IRQ_DIRECT_CONNECT(irq_p, priority_p, isr_p, flags_p) \
ARCH_IRQ_DIRECT_CONNECT(irq_p, priority_p, isr_p, flags_p)
参数说明如下:
- irq_p:
中断号
- priority_p:
中断优先级
- isr_p:
中断服务程序的地址
- flags_p:
特定于体系结构的 IRQ 配置标志
备注
IRQ_DIRECT_CONNECT 比 IRQ_CONNECT 少一个参数isr_param_p, 直接ISR不需要参数, 可在硬件向量表中直接跳转执行。
不同的架构会有不同的实现, ARM架构的实现在文件 zephyr/include/zephyr/arch/arm/irq.h 中, 如下:
#define ARCH_IRQ_DIRECT_CONNECT(irq_p, priority_p, isr_p, flags_p) \
{ \
BUILD_ASSERT(IS_ENABLED(CONFIG_ZERO_LATENCY_IRQS) || !(flags_p & IRQ_ZERO_LATENCY), \
"ZLI interrupt registered but feature is disabled"); \
_CHECK_PRIO(priority_p, flags_p) \
Z_ISR_DECLARE_DIRECT(irq_p, ISR_FLAG_DIRECT, isr_p); \
z_arm_irq_priority_set(irq_p, priority_p, flags_p); \
}
#define Z_ISR_DECLARE_DIRECT(irq, flags, func) \
Z_ISR_DECLARE(irq, ISR_FLAG_DIRECT | (flags), func, NULL)
备注
直接ISR注册宏中也调用了 Z_ISR_DECLARE, 整体和普通的ISR很像, 只有一点差异就是 flag = ISR_FLAG_DIRECT, 也就是说对于直接ISR最后还是产生一个 struct _isr_list
结构体变量放入 .intList 中。
启用/关闭中断
启用中断
中断注册完成后, 还需要启用中断才能使中断正常响应, 使能中断函数如下:
#define irq_enable(irq) arch_irq_enable(irq)
参数说明如下:
- irq:
中断号
关闭中断
启用中断后, 如果希望中断不再继续响应, 可以关闭中断, 关闭中断的函数如下:
#define irq_disable(irq) arch_irq_disable(irq)
参数说明如下:
- irq:
中断号
中断响应
中断向量表
zephyr的中断向量表分成三部分,示意图如下:
exc_vector_table 中是前16个异常向量, _irq_vector_table 为硬中断向量表, _sw_isr_table 为软件中断向量表 。呈现方式如下:
/*系统异常装载*/
SECTION_SUBSEC_FUNC(exc_vector_table,_vector_table_section,_vector_table)
.word z_main_stack + CONFIG_MAIN_STACK_SIZE
.word z_arm_reset
.word z_arm_nmi
.word z_arm_hard_fault
.word z_arm_mpu_fault
.word z_arm_bus_fault
.word z_arm_usage_fault
.word z_arm_secure_fault
.word 0
.word 0
.word 0
.word z_arm_svc
.word z_arm_debug_monitor
.word 0
.word z_arm_pendsv
.word sys_clock_isr
/*硬件中断向量表*/
uintptr_t __irq_vector_table _irq_vector_table[80] = {
((uintptr_t)&_isr_wrapper),
((uintptr_t)&_isr_wrapper),
((uintptr_t)&_isr_wrapper),
((uintptr_t)&direct_isr_pwm1),
((uintptr_t)&_isr_wrapper),
((uintptr_t)&_isr_wrapper),
...
}
/*软件中断向量表*/
struct _isr_table_entry __sw_isr_table _sw_isr_table[80] = {
{(const void *)0x0, (ISR)z_irq_spurious}, /* 0 */
{(const void *)0x0, (ISR)z_irq_spurious}, /* 1 */
{(const void *)0x0, (ISR)z_irq_spurious}, /* 2 */
{(const void *)0x0, (ISR)z_irq_spurious}, /* 3 */
{(const void *)0x0, (ISR)z_irq_spurious}, /* 4 */
{(const void *)0x40815000, (ISR)0x2027de1}, /* 5 */
...
}
备注
硬件中断向量表 irq_vector_table 和软件中断向量表 sw_isr_table 最初是空的,编译过程中, 会根据编译期注册的中断生成新的中断向量表,
并在运行期将普通ISR的中断添加进软件中断表。
查询中断向量表
当中断发生时, 查询硬件中断向量表 _irq_vector_table, 可能的情况如下:
如果不是
_isr_wrapper(), 表示是直接中断, 直接进入ISR。如果是
_isr_wrapper(), 表示是普通中断, 进入_isr_wrapper(), 查表_sw_isr_table寻找相应的ISR, 如果不是z_irq_spurious(), 则会正常执行。如果普通中断, 但是在
_sw_isr_table中找到的ISR是z_irq_spurious(), 表示中断未注册, 执行报错。
备注
z_irq_spurious() 在启动时安装在所有 _sw_isr_table 槽位中。调用时会抛出错误。
查表示意图如下:
isr_wrapper() 在不同的架构会有不同的实现, Cortex-M的实现在文件 zephyr/arch/arm/core/cortex_m/isr_wrapper.c 中, 如下:
void _isr_wrapper(void)
{
#ifdef CONFIG_TRACING_ISR
sys_trace_isr_enter();
#endif /* CONFIG_TRACING_ISR */
int32_t irq_number = __get_IPSR();
irq_number -= 16;
struct _isr_table_entry *entry = &_sw_isr_table[irq_number];
(entry->isr)(entry->arg);
#if defined(CONFIG_ARM_CUSTOM_INTERRUPT_CONTROLLER)
z_soc_irq_eoi(irq_number);
#endif
#ifdef CONFIG_TRACING_ISR
sys_trace_isr_exit();
#endif /* CONFIG_TRACING_ISR */
z_arm_exc_exit();
}
在 isr_wrapper() 中, 可以统一做一些相同的操作, 比如打开关闭trace, 异常处理程序退出前的内核清理,
和直接中断声明时加的 ISR_DIRECT_HEADER() 和 ISR_DIRECT_FOOTER() 类似。
_sw_isr_table 不映射异常,只映射中断。 由于前16个中断号已经被系统异常使用, 所以读出的中断号还需要减去16。
其他相关介绍
共享中断
通过 CONFIG_SHARED_INTERRUPTS 启用。
每当尝试在同一中断线上注册第二个 ISR/参数对(使用 IRQ_CONNECT 或 irq_connect_dynamic())时,中断线就会变为共享中断,
这意味着每次触发中断时都会调用这两个 ISR/参数对(前一个和刚刚注册的那个)。
在共享中断上下文中使用中断线的实体称为客户端。单个中断允许的最大客户端数量由配置宏 CONFIG_SHARED_IRQ_MAX_NUM_CLIENTS 控制。
共享中断存储结构如下:
struct z_shared_isr_table_entry z_shared_sw_isr_table[];
struct z_shared_isr_table_entry {
struct _isr_table_entry clients[CONFIG_SHARED_IRQ_MAX_NUM_CLIENTS];
size_t client_num;
};
共享中断注册
void my_isr_installer(void)
{
IRQ_CONNECT(MY_DEV_IRQ, MY_DEV_IRQ_PRIO, my_first_isr, MY_FST_ISR_ARG, MY_IRQ_FLAGS);
IRQ_CONNECT(MY_DEV_IRQ, MY_DEV_IRQ_PRIO, my_second_isr, MY_SND_ISR_ARG, MY_IRQ_FLAGS);
}
备注
共享中断注册时宏和函数的使用方法与前面中断注册章节讲述的完全一样, 只是共享中断注册时可以调用多次注册宏或函数。
共享中断执行
共享中断处理时, z_shared_isr() 将替换 _sw_isr_table 中当前注册的 ISR。此特殊 ISR 将遍历已注册客户端列表并调用 ISR。
void z_shared_isr(const void *data)
{
size_t i;
const struct z_shared_isr_table_entry *entry;
const struct _isr_table_entry *client;
entry = data;
for (i = 0; i < entry->client_num; i++) {
client = &entry->clients[i];
if (client->isr) {
client->isr(client->arg);
}
}
}
备注
请注意,启用共享中断将导致二进制文件大小不可忽略的增加。请谨慎使用。
启用共享中断后, 共享中断表 z_shared_sw_isr_table 空间会按最大共享中断数量开好。
共享中断卸载
允许用户使用 irq_disconnect_dynamic() 在启用共享中断支持和动态中断支持的场景中动态断开 ISR。断开 ISR 连接后,每当触发其注册的中断线时,该 ISR 将不再被调用。
static inline int
irq_disconnect_dynamic(unsigned int irq, unsigned int priority,
void (*routine)(const void *parameter),
const void *parameter, uint32_t flags)
{
return arch_irq_disconnect_dynamic(irq, priority, routine,
parameter, flags);
}
参数说明如下:
- irq:
中断号
- priority:
中断优先级
- routine:
中断服务程序的地址
- parameter:
传递给中断服务程序的参数
- flags:
特定于体系结构的 IRQ 配置标志
调用 irq_disconnect_dynamic() 会删除 z_shared_isr_table_entry 中相应的 _isr_table_entry。
arch_irq_disconnect_dynamic 函数实现在文件 zephyr/arch/common/shared_irq.c 中, 如下:
int __weak arch_irq_disconnect_dynamic(unsigned int irq, unsigned int priority,
void (*routine)(const void *parameter),
const void *parameter, uint32_t flags)
{
ARG_UNUSED(priority);
ARG_UNUSED(flags);
return z_isr_uninstall(irq, routine, parameter);
}
int z_isr_uninstall(unsigned int irq,
void (*routine)(const void *),
const void *parameter)
{
struct z_shared_isr_table_entry *shared_entry;
struct _isr_table_entry *entry;
struct _isr_table_entry *client;
unsigned int table_idx;
size_t i;
k_spinlock_key_t key;
table_idx = z_get_sw_isr_table_idx(irq);
if (table_idx >= IRQ_TABLE_SIZE) {
return -EINVAL;
}
shared_entry = &z_shared_sw_isr_table[table_idx];
entry = &_sw_isr_table[table_idx];
key = k_spin_lock(&lock);
if (!shared_entry->client_num) {
if (entry->isr == routine && entry->arg == parameter) {
entry->isr = z_irq_spurious;
entry->arg = NULL;
}
goto out_unlock;
}
for (i = 0; i < shared_entry->client_num; i++) {
client = &shared_entry->clients[i];
if (client->isr == routine && client->arg == parameter) {
shared_irq_remove_client(shared_entry, i, table_idx);
goto out_unlock;
}
}
out_unlock:
k_spin_unlock(&lock, key);
return 0;
}
多级中断
硬件平台可以通过使用一个或多个嵌套中断控制器来支持比原生中断线路更多的中断线路。硬件中断源会被合并成一条线路,然后路由到父控制器。
如果支持嵌套中断控制器,则应启用 CONFIG_MULTI_LEVEL_INTERRUPTS 选项,并根据硬件架构配置 CONFIG_2ND_LEVEL_INTERRUPTS 和 CONFIG_3RD_LEVEL_INTERRUPTS 选项。
多级中断DTS配置示例如下:
test_cpu_intc: interrupt-controller {
compatible = "vnd,cpu-intc";
#interrupt-cells = < 0x01 >;
interrupt-controller;
};
test_l1_irq: interrupt-controller@bbbbcccc {
compatible = "vnd,intc";
reg = <0xbbbbcccc 0x1000>;
interrupt-controller;
#interrupt-cells = <2>;
interrupts = <11 0>;
interrupt-parent = <&test_cpu_intc>;
};
test_l2_irq: interrupt-controller@bbbccccc {
compatible = "vnd,intc";
reg = <0xbbbccccc 0x1000>;
interrupt-controller;
#interrupt-cells = <2>;
interrupts = <12 0>;
interrupt-parent = <&test_l1_irq>;
};
test_l1_irq_inc: interrupt-controller@bbbbdccc {
compatible = "vnd,intc";
reg = <0xbbbbdccc 0x10>;
interrupt-controller;
#interrupt-cells = <2>;
interrupts = <12 0>; /* +1 */
interrupt-parent = <&test_cpu_intc>;
};
系统会分配一个唯一的 32 位中断号,其中包含用于选择和调用正确中断服务程序 (ISR) 的信息。 每个中断级别都会在这个 32 位中断号中分配一个字节(可配),使用此架构最多可支持四个中断级别,如下所示 (此处显示了三个中断级别):
“-”表示中断线,从 0 (最右边) 开始编号。
级别 1 有 12 条中断线, 其中两条线 (2 和 9) 连接到嵌套控制器,并在第 4 行连接一个设备“A”。
其中一个级别 2 控制器的中断线 5 连接到级别 3 嵌套控制器,并在第 3 行连接一个设备“C”。
另一个级别 2 控制器没有嵌套控制器,但在第 2 行连接一个设备“B”。
级别 3 控制器在第 2 行连接一个设备“D”。
从低位开始,每一级中断号占用一个字节。我们以上面所示的四个中断为例,分别标记为 A、B、C 和 D, 则它们的中断号表示如下:
A -> 0x00000004
B -> 0x00000302
C -> 0x00000409
D -> 0x00030609
中断lock/unlock
可以使用 irq_lock() 和 irq_unlock(key) 打开和关闭中断锁定状态, 创建临界区保护, 中断锁定状态下
中断优先级数值大于等于 _EXC_IRQ_DEFAULT_PRIO 的中断都会被屏蔽。函数实现如下:
#define irq_lock() arch_irq_lock()
#define irq_unlock(key) arch_irq_unlock(key)
static ALWAYS_INLINE unsigned int arch_irq_lock(void)
{
unsigned int key;
#if defined(CONFIG_ARMV7_M_ARMV8_M_MAINLINE)
key = __get_BASEPRI();
__set_BASEPRI_MAX(_EXC_IRQ_DEFAULT_PRIO);
__ISB();
#endif
return key;
}
static ALWAYS_INLINE void arch_irq_unlock(unsigned int key)
{
#if defined(CONFIG_ARMV7_M_ARMV8_M_MAINLINE)
__set_BASEPRI(key);
__ISB();
#endif
}
零延迟中断
通过应用 IRQ 锁来阻止中断可能会增加观察到的中断延迟。然而,对于某些低延迟用例, 较高的中断延迟可能是不可接受的。
内核通过允许具有严格延迟限制的中断以不会被中断锁阻止的优先级执行来解决此类用例。
这些中断被定义为零延迟中断。对零延迟中断的支持需要启用 CONFIG_ZERO_LATENCY_IRQS。
任何配置为零延迟的中断也必须声明为直接 ISR(并且不得在其中使用 ISR_DIRECT_PM),
因为常规 ISR 会与内核交互。此外,必须将标志 IRQ_ZERO_LATENCY 传递给 IRQ_DIRECT_CONNECT 宏,
以将特定中断配置为零延迟。在某些架构上,可以将零延迟中断 ISR 声明为直接中断和动态中断。
零延迟中断的实现主要依赖于中断锁定的优先级低于或等于 _EXC_IRQ_DEFAULT_PRIO, 设置零延迟中断的优先级
高于 _EXC_IRQ_DEFAULT_PRIO 即可实现零延迟中断。代码中相关宏的实现如下:
/*如果配置了零延迟中断, lock时使用的_EXC_IRQ_DEFAULT_PRIO增加保留给零延迟中断的优先级偏移*/
#define _EXC_SVC_PRIO COND_CODE_1(CONFIG_ZERO_LATENCY_IRQS, \
(CONFIG_ZERO_LATENCY_LEVELS), (0))
#define _IRQ_PRIO_OFFSET (_EXCEPTION_RESERVED_PRIO + _EXC_SVC_PRIO)
#define _EXC_IRQ_DEFAULT_PRIO Z_EXC_PRIO(_IRQ_PRIO_OFFSET)
备注
为了降低闪存访问延迟,请考虑将 ISR 及其所有相关符号迁移到 RAM。
一些配置说明
- GEN_ISR_TABLES:
生成_isr_table
- GEN_IRQ_VECTOR_TABLE:
生成_irq_vector_table
- CONFIG_GEN_SW_ISR_TABLE:
生成_sw_isr_table
- CONFIG_ISR_TABLES_LOCAL_DECLARATION:
会在调用 IRQ_CONNECT 宏的位置本地创建表条目,然后使用链接器脚本将其定位到内存中的正确位置
- CONFIG_LTO:
启用链接时优化
- CONFIG_IRQ_VECTOR_TABLE_JUMP_BY_ADDRESS:
中断向量表跳转使用ISR地址跳转
- CONFIG_IRQ_VECTOR_TABLE_JUMP_BY_CODE:
中断向量表跳转使用跳转指令跳转
- CONFIG_NUM_IRQS:
配置中断数量
- CONFIG_SHARED_INTERRUPTS:
启用共享中断
- CONFIG_SHARED_IRQ_MAX_NUM_CLIENTS:
共享中断最大客户端数量
- CONFIG_MULTI_LEVEL_INTERRUPTS:
启用多级中断
文件系统 介绍
文件系统简介
zephyr 文件系统 是其服务层的重要组成部分。
zephyr支持多种文件系统类型(FatFS,LittleFS,Ext2),可根据应用需求灵活选择。其设计遵循模块化原则,开发者可通过Kconfig配置工具按需裁剪。
zephyr支持多个文件系统类型同时工作,由Kconfig中的 FILE_SYSTEM_MAX_TYPES 配置,默认值 2。
zephyr允许应用程序在不同的挂载点(例如/lfs和/fatfs)挂载多个文件系统(littlefs或fatfs),每个挂载点各自维护文件系统的实例化、挂载和文件操作等。
和文件系统有关的目录和文件如下所示:
nuwa $
├── modules
│ ├── fs # third party github
│ │ ├── fatfs
│ │ └── littlefs
└── zephyr
├── include
│ ├── zephyr
│ │ ├── fs # file system API
├── modules
│ ├── fatfs # fatfs adapter layer
│ ├── littlefs # littlefs adapter layer
├── subsys
│ ├── fs # zephyr file system subsystem
│ │ ├── ext2
│ │ ├── fcb
│ │ ├── nvs
│ │ ├── zms
│ │ ├── fs.c # file system API implementation
│ │ ├── fat_fs.c
│ │ ├── littlefs_fs.c
├── samples
│ ├── subsys
│ │ ├──fs # file system samples
├── tests
│ ├── subsys
│ │ ├──fs # file system tests
下图展示了 LittleFS on FLASH 和 FatFS on SD 涉及的主要模块。
zephyr file system diagram (LittleFS on FLASH & FatFS on SD)
FatFS
FatFS 是一个专为小型嵌入式系统设计的通用FAT/exFAT文件系统模块,兼容多种 FAT 格式:FAT、FAT32 和 exFAT。
FatFS 是独立于平台和存储介质的文件系统层,它与物理设备(eg:SD)完全分离。存储设备控制模块不属于 FatFs 模块,需要由实现者提供。
zephyr 对 FatFS 的适配位于 zephyr/modules/fatfs 目录。
zephyr/modules/fatfs/CMakeLists.txt 表明 zephyr 是如何集成 FatFs 的:
使用 FatFS 原生的
ff.c和ffunicode.c.c重新实现
diskio.c为zfs_diskio.c重新实现
ffsystem.c为zfs_ffsystem.czephyr_fatfs_config.h中配置的优先级高于ffconf.h
备注
zephyr_fatfs_config.h 中,通过一系列的 #undef 和 #define 操作来覆盖 ffconf.h 中的配置。
LittleFS
LittleFS 是专为微控制器设计的小型故障安全文件系统,具有断电恢复能力,支持动态磨损均衡,还可以检测坏块并进行修复。
zephyr/modules/littlefs/CMakeLists.txt 表明 zephyr 是如何集成 LittleFS 的:
使用 LittleFS 原生的
lfs.c重新实现 CRC 算法
提供
zephyr_lfs_crc.c的目的是为了后续更新 CRC目前和原生的
lfs_until.c中的 CRC 是一样的
zephyr_lfs_config.h配置替代了lfs_util.h
FatFS VS LittleFS
特性 |
FatFS |
LittleFS |
|---|---|---|
磨损均衡 |
不支持 |
支持 |
掉电保护 |
不支持 |
支持 |
兼容性 |
强(Windows/DOS 兼容 FatFS) |
弱 (Windows不兼容LittleFS) |
建议存储设备 |
SD 卡、U 盘等可移动存储设备 |
FLASH |
备注
SD卡、USB上建议使用 FatFS 文件系统类型。
FLASH上建议使用 LittleFS 文件系统类型。
文件系统API
zephyr文件系统API 函数统一以 fs_ 开头。
可以分为以下几类:
注册
fs_register()/fs_unregister()
由文件系统驱动自动完成
挂载
fs_mount()/fs_unmount()
描述符
fs_file_t_init()/fs_dir_t_init()
需要先初始化描述符,再执行文件/目录操作
文件操作
fs_open()/fs_read()/fs_write()/fs_close()/fs_seek()/fs_tell()/fs_truncate()/fs_sync()
目录操作
fs_opendir()/fs_closedir()/fs_mkdir()/fs_readdir()
API 的使用方式可以参考样例:
备注
无论底层使用 FatFS 还是 LittleFS, 应用层使用的都是 zephyr 封装过的以 fs_ 开头的API.
文件系统配置
文件系统的配置可分为三类文件:
设备树: 包括 .dts、.dtsi、.yaml、.overlay 类型的文件,用于定义DTS变量和属性规则。
Kconfig: 用于定义配置项。
conf 文件: 用于定义配置项的默认取值。
用户可以通过 .overlay 和 .conf 文件来修改系统的默认配置。
DTS 配置
文件系统的设备树配置位于 zephyr/dts/bindings/fs/ 目录下。
该目录下的 yaml 文件可以分为两类,一类是各个文件系统都具有的属性 zephyr,fstab-common.yaml,另一类是各个文件系统特有的属性。
FatFS 的属性
zephyr,fstab,fatfs.yamlLittleFS 的属性
zephyr,fstab,littlefs.yaml
属性 |
类型 |
描述 |
|---|---|---|
mount-point |
string |
挂载点的绝对路径 |
automount |
boolean |
在文件系统驱动的初始化期间,自动尝试挂载该分区 |
read-only |
boolean |
以只读模式挂载文件系统 |
no-format |
boolean |
挂载失败时,不格式化挂载分区 |
disk-access |
boolean |
该字段未设置时,默认使用 flash API; 设置该字段表示使用 disk access API; |
Kconfig 配置
需要使能 FILE_SYSTEM 配置项后,才可以使用文件系统的功能。
备注
文件系统在 menuconfig 中的配置路径为:
(Top) → Subsystems and OS Services → File Systems
LittleFS Kconfig 配置项位于 zephyr/subsys/fs/Kconfig.littlefs 中。一些配置说明:
- FILE_SYSTEM_LITTLEFS:
启用 LittleFS 文件系统。
- FS_LITTLEFS_FMP_DEV:
在FLASH上支持LittleFS。(使用flash_map API)
- FS_LITTLEFS_BLK_DEV:
在块设备(如SD卡)上支持LittleFS。
- FS_LITTLEFS_READ_SIZE:
块读取的最小大小(字节)。所有读操作都将是这个值的倍数。
- FS_LITTLEFS_PROG_SIZE:
块编程的最小大小(字节)。所有编程操作都将是这个值的倍数。
- FS_LITTLEFS_CACHE_SIZE:
LittleFS 有一个read cache,一个program cache,每个文件也有一个cache。
- FS_LITTLEFS_LOOKAHEAD_SIZE:
lookahead buffer的大小,必须是8的倍数。
- FS_LITTLEFS_BLOCK_CYCLES:
数据迁移前的擦除周期数。推荐值[100,1000]。设置为非正值可禁用均衡功能。
- FS_LITTLEFS_NUM_FILES:
同时打开的最大文件数。(根据应用需求调整)
- FS_LITTLEFS_NUM_DIRS:
同时打开的最大目录数。(根据应用需求调整)
FatFS Kconfig 配置项位于 zephyr/subsys/fs/Kconfig.fatfs 中。一些配置说明:
- FAT_FILESYSTEM_ELM:
启用 FatFS 文件系统。
- FS_FATFS_EXFAT:
支持 FatFS exFAT格式。使能 exFAT 时, 会自动使能 LFN。
- FS_FATFS_READ_ONLY:
只读模式。
- FS_FATFS_MKFS:
添加用于创建 FAT 文件系统磁盘的代码。
- FS_FATFS_MOUNT_MKFS:
允许 fs_mount() 在未找到文件系统的情况下尝试格式化卷(如新插入的SD卡)。
- FS_FATFS_MAX_ROOT_ENTRIES:
FAT文件系统根目录中的最大条目数。
- FS_FATFS_HAS_RTC:
启用文件系统时间戳,而不是使用硬编码日期用于所有操作。
- FS_FATFS_LFN:
启用长文件名(LFN)功能。
- FS_FATFS_MAX_LFN:
定义长文件名最大长度,范围 [12, 255]。
- FS_FATFS_MAX_SS:
最大扇区大小(512、1024、2048 或 4096)。
- FS_FATFS_MIN_SS:
最小扇区大小(512、1024、2048 或 4096)。
- FS_FATFS_NUM_FILES:
同时打开的最大文件数。(根据应用需求调整)
- FS_FATFS_NUM_DIRS:
同时打开的最大目录数。(根据应用需求调整)
关于配置的其它说明
LittleFS Kconfig 和 DTS 中设置的存储介质要一致,否则 automount 会失败。
存储介质 |
Kconfig配置 |
DTS配置 |
|---|---|---|
FLASH |
CONFIG_FS_LITTLEFS_FMP_DEV=y |
disk_access=false |
SD |
CONFIG_FS_LITTLEFS_BLK_DEV=y |
disk_access=true |
LittleFS Kconfig 和 DTS 中都有 cache-size,block-cycles,lookahead_size,disk_version 的配置,哪个生效?
DTS配置的优先级高于 Kconfig
当 DTS 中设置的值为 0 时,将使用 KConfig 的配置值。
文件系统样例
LittleFS on FLASH 样例
可参考 zephyr/samples/subsys/fs/littlefs 查看 LittleFS 的使用方式。
如果 LittleFS 使用的块设备是板级dts中已有的 storage_partition 区域。
则用户可在 overlay 中配置如下的 fstab:
/{
fstab {
compatible = "zephyr,fstab";
lfs1: lfs1 {
compatible = "zephyr,fstab,littlefs";
read-size = < 0x1 >;
prog-size = < 0x1 >;
cache-size = < 0x100 >;
lookahead-size = < 0x8 >;
block-cycles = < 0x200 >;
partition = < &storage_partition >;
mount-point = "/lfs1";
automount;
};
};
};
并确保 spic 处于使能状态:
&spic {
status = "okay";
};
如果需要新增一个 partition 用于 LittleFS, 则 overlay 可以设置如下。 其中 demo_storage_partition 是新增的 partition, 应该根据实际情况设置它的标签、地址和长度。
&spic {
status = "okay";
};
&flash0 {
partitions {
demo_storage_partition: partition@260000 {
label = "demo-storage";
reg = <0x00260000 DT_SIZE_K(64)>;
};
};
};
/ {
fstab {
compatible = "zephyr,fstab";
lfs1: lfs1 {
compatible = "zephyr,fstab,littlefs";
read-size = < 0x1 >;
prog-size = < 0x1 >;
cache-size = < 0x100 >;
lookahead-size = < 0x8 >;
block-cycles = < 0x200 >;
partition = <&demo_storage_partition>;
mount-point = "/lfs1";
automount;
};
};
};
conf 需要打开以下配置:
CONFIG_FILE_SYSTEM=y
CONFIG_FILE_SYSTEM_LITTLEFS=y
FatFS on SD 样例
可参考 zephyr/samples/subsys/fs/fs_sample 查看 FatFS on SD 的使用方式。
FatFS 通过 SDMMC 子系统来访问SD卡, 可如下配置 DTS:
&sdhc0 {
status = "okay";
sdmmc {
compatible = "zephyr,sdmmc-disk";
status = "okay";
disk-name = "SD";
};
};
conf 需要打开以下配置:
CONFIG_FILE_SYSTEM=y
CONFIG_FAT_FILESYSTEM_ELM=y
Settings 介绍
zephyr settings子系统提供了一种存储持久化设备配置和运行时状态的方式。该系统通过统一的API支持多种存储后端实现,包括FCB、NVS、ZMS或文件系统。 设置项以键值对的形式存储,通常按 "package/subtree" 的层级结构来命名键名。
zephyr提供了详细的 settings 文档。
应用层开发者可重点关注以下章节:
settings 配置
下面是一份 settings dts 配置样例, 可调整头部的 SIZE 宏来修改配置分区的大小。
#define DT_BT_CONFIG_SIZE DT_SIZE_K(4)
#define DT_WIFI_CONFIG_SIZE DT_SIZE_K(4)
/ {
chosen {
zephyr,settings-partition = &settings_partition;
};
};
&spic {
status = "okay";
};
&flash0 {
partitions {
settings_partition: partition@200000 {
label = "settings_storage";
reg = <0x00200000 ((DT_BT_CONFIG_SIZE+DT_WIFI_CONFIG_SIZE)*2)>;
};
};
};
备注
需要根据实际情况设置分区的标签、起始地址和大小。
当使用 ZMS 作为后端时,conf 需要打开以下配置:
CONFIG_SETTINGS=y
CONFIG_ZMS=y
CONFIG_ZMS_LOOKUP_CACHE=y
CONFIG_ZMS_LOOKUP_CACHE_SIZE=512
如果使用了 settings_runtime_xx() API,还需要打开:
CONFIG_SETTINGS_RUNTIME=y
settings 样例
settings系统 API 由 include/zephyr/settings/settings.h 提供。
备注
无论采用哪种后端,应用层使用的settings API都保持不变。因此可以在需求变化时,灵活更换后端,而无需修改应用层代码。
使用样例可参考:
zephyr/samples/subsys/settings
需要先调用 settings_subsys_init() 初始化 settings 子系统,调用 settings_register() 注册模块的settings handler;
然后再调用 settings_load_xx 或者 settings_save_xx 获取或者存储配置。