Zephyr 开发

DTS 介绍

Zephyr 配置系统

zephyr 编译分为两个阶段:

  • configuration 阶段: 执行 cmake, 处理 DTS, Kconfig

    • DTS 会被转换为 build/zephyr/include/generated/zephyr/devicetree_generated.h

    • Kconfig 会被转为: build/zephyr/include/generated/zephyr/autoconf.h

  • build 阶段: 执行 make 或者 ninja 工具生成固件

configuration 阶段主要流程参考下图:

../../_images/zephyr_configuration_overview.svg

DTS 配置

DTS 基本语法

DTS 由节点组成, 每个节点中包含若干属性或子节点.

  • 节点基本形式: node_label:node_name@unit-address{...}

  • 属性基本形式: prop_name = value;, 是一个键值对(bool 类型除外, 它仅有一个标识符, 出现即表示 True)

下图示例了一个简单的 DTS 文件的结构:

../../_images/zephyr_dts_grammar.svg

一个实际硬件的例子

假设有如下图所示的硬件结构:

../../_images/zephyr_dts_small_hw_structure.svg

则可使用如下 DTS 结构描述该硬件:

../../_images/zephyr_dts_small_dts_structure.svg

编写 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 中 addresssize 两个字段位宽是由 其所属节点的父节点 中的 address-cellssize-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 model

  • compatible 字符串与 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 处理的输入输出参考下图:

../../_images/zephyr_dts_inputs_outputs.svg

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 格式, 下图是其中各字段的介绍及示例:

../../_images/zephyr_dts_binding_grammar.svg

下面对其中比较重要的一些字段进行详述:

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 05 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 可以取 rxtx 即可

  • 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

a-string = "hello, world!";

int

between angle brackets (< and >)

an-int = <1>;

boolean

for true, with no value (for false, use /delete-property/)

my-true-boolean;

array

between angle brackets (< and >), separated by spaces

foo = <0xdeadbeef 1234 0>;

uint8-array

in hexadecimal without leading 0x, between square brackets ([ and ]).

a-byte-array = [00 01 ab];

string-array

separated by commas

a-string-array = "string one", "string two", "string three";

phandle

between angle brackets (< and >)

a-phandle = <&mynode>;

phandles

between angle brackets (< and >), separated by spaces

some-phandles = <&mynode0 &mynode1 &mynode2>;

phandle-array

between angle brackets (< and >), separated by spaces

a-phandle-array = <&mynode0 1 2>, <&mynode1 3 4>;

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 _isr_list))struct _isr_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_CONNECTIRQ_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 的中断向量表分成三部分,示意图如下:

../../_images/zephyr_nvic_interrupt_vector_table_position.svg

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 槽位中。调用时会抛出错误。

查表示意图如下:

../../_images/zephyr_nvic_interrupt_vector_table_query_zh.svg

_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_CONNECTirq_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_INTERRUPTSCONFIG_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 位中断号中分配一个字节(可配),使用此架构最多可支持四个中断级别,如下所示 (此处显示了三个中断级别):

../../_images/zephyr_nvic_multi_level_interrupts.jpg
  • “-”表示中断线,从 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 FLASHFatFS on SD 涉及的主要模块。

../../_images/zephyr_file_system_diagram.svg

zephyr file system diagram (LittleFS on FLASH & FatFS on SD)

FatFS

FatFS 是一个专为小型嵌入式系统设计的通用 FAT/exFAT 文件系统模块,兼容多种 FAT 格式:FAT、FAT32 和 exFAT。 FatFS 是独立于平台和存储介质的文件系统层,它与物理设备(e.g., SD)完全分离。存储设备控制模块不属于 FatFs 模块,需要由实现者提供。 zephyr 对 FatFS 的适配位于 zephyr/modules/fatfs 目录。

zephyr/modules/fatfs/CMakeLists.txt 表明 zephyr 是如何集成 FatFs 的:

  • 使用 FatFS 原生的 ff.cffunicode.c.c

  • 重新实现 diskio.czfs_diskio.c

  • 重新实现 ffsystem.czfs_ffsystem.c

  • zephyr_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.yaml

  • LittleFS 的属性 zephyr,fstab,littlefs.yaml

common properties

属性

类型

描述

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 都保持不变。因此可以在需求变化时,灵活更换后端,而无需修改应用层代码。

使用样例可参考:

需要先调用 settings_subsys_init() 初始化 settings 子系统,调用 settings_register() 注册模块的 settings handler; 然后再调用 settings_load_xx 或者 settings_save_xx 获取或者存储配置。

DEBUG 介绍

离线调试

coredump 模块

coredump 模块用于转储 CPU 寄存器和内存内容,以便进行离线调试。当遇到致命错误时,将调用此模块, 并根据启用的后端打印或存储数据。也可以在需要生成 coredump 文件时按需调用此模块。

coredump 相关文件分布:

<zephyr>
├── arch/
│    └── arm/
│       └── core
│           └── cortex_m/
│               └── coredump.c  # 架构特定实现
├── include/
│   └── zephyr/
│       └── debug/              # 头文件
├── subsys/
│   └── debug/
│       └── coredump/           # debug 子系统主要内容,coredump api 和后端实现
├── drivers/
│   └── coredump/               # coredump 伪设备
└── scripts/
    └── coredump/               # 解析 coredump 的脚本
coredump 配置

请使用以下选项配置此模块:

  • DEBUG_COREDUMP : 启用 coredump 模块。

以下是启用 coredump 输出后端的选项:

  • DEBUG_COREDUMP_BACKEND_LOGGING : 使用日志模块获取 coredump 输出。

  • DEBUG_COREDUMP_BACKEND_FLASH_PARTITION : 使用 flash 分区进行 coredump 输出。

  • DEBUG_COREDUMP_BACKEND_IN_MEMORY : 使用 memory 分区进行 coredump 输出。

  • DEBUG_COREDUMP_BACKEND_OTHER : 使用自定义机制进行 coredump 输出。

以下是有关内存转储的选项:

  • DEBUG_COREDUMP_MEMORY_DUMP_MIN : 仅转储异常线程的堆栈、其线程结构以及一些其他最基本的必要数据,以支持在调试器中遍历堆栈。仅当需要转储绝对最少的数据时才使用此选项。

  • DEBUG_COREDUMP_MEMORY_DUMP_THREADS : 导出所有线程的线程结构和堆栈以及调试线程所需的所有数据。

  • DEBUG_COREDUMP_MEMORY_DUMP_LINKER_RAM : 转储 _image_ram_start[]_image_ram_end[] 之间的内存区域。这至少包括 data、noinit 和 BSS 段。这是默认设置。

coredump 代码

通常情况下,当遇到致命错误时,会在 z_fatal_error() 函数内部调用 coredump() 以生成 coredump 文件。 也可以在需要生成 coredump 文件时按需调用此函数。

void coredump(unsigned int reason, const struct arch_esf *esf,
          struct k_thread *thread)
{
    z_coredump_start();
    dump_header(reason);
    if (esf != NULL) {
        arch_coredump_info_dump(esf);
    }
#ifdef CONFIG_DEBUG_COREDUMP_THREADS_METADATA
    dump_threads_metadata();
#endif
#ifdef CONFIG_DEBUG_COREDUMP_MEMORY_DUMP_MIN
        dump_thread(thread);
#endif
    process_memory_region_list();
    z_coredump_end();
}

函数 coredump() 在文件 zephyr/subsys/debug/coredump/coredump_core.c 中,文件中其他的函数介绍如下:

API

Description

coredump()

z_fatal_error() 函数内部调用此函数以生成 coredump 文件。也可在需要生成 coredump 文件时调用此函数。

z_coredump_start()

coredump 开始。

z_coredump_end()

coredump 结束。

coredump_buffer_output()

此函数会将字节数组缓冲区输出到 coredump 后端。

coredump_cmd()

对 coredump 子系统执行命令。

coredump_query()

对 coredump 子系统执行查询。

coredump_memory_dump()

转储内存区域。

arch_coredump_info_dump()

转储架构特定信息。

dump_threads_metadata()

转储线程元数据。

process_coredump_dev_memory()

转储对 coredump 伪设备收集的内存区域。

文件 zephyr/subsys/debug/coredump/coredump_core.c 中的函数是顶层函数,会做一些收集处理信息的操作,最后会调用具体的底层后端 backend_api 执行最终操作。

后端的选择由上面 coredump 配置 中提到的配置决定,选择后端的代码如下:

#if defined(CONFIG_DEBUG_COREDUMP_BACKEND_LOGGING)
extern struct coredump_backend_api coredump_backend_logging;
static struct coredump_backend_api
   *backend_api = &coredump_backend_logging;
#elif defined(CONFIG_DEBUG_COREDUMP_BACKEND_FLASH_PARTITION)
extern struct coredump_backend_api coredump_backend_flash_partition;
static struct coredump_backend_api
   *backend_api = &coredump_backend_flash_partition;
#elif defined(CONFIG_DEBUG_COREDUMP_BACKEND_INTEL_ADSP_MEM_WINDOW)
extern struct coredump_backend_api coredump_backend_intel_adsp_mem_window;
static struct coredump_backend_api
   *backend_api = &coredump_backend_intel_adsp_mem_window;
#elif defined(CONFIG_DEBUG_COREDUMP_BACKEND_IN_MEMORY)
extern struct coredump_backend_api coredump_backend_in_memory;
static struct coredump_backend_api
   *backend_api = &coredump_backend_in_memory;
#elif defined(CONFIG_DEBUG_COREDUMP_BACKEND_OTHER)
extern struct coredump_backend_api coredump_backend_other;
static struct coredump_backend_api
   *backend_api = &coredump_backend_other;
#else
#error "Need to select a coredump backend"
#endif

每个后端都需要定义一个结构体 coredump_backend_api,结构体中有 5 个 api,对应 coredump 顶层的五个 api,分别是 coredump 开始、结束、输出、查询、执行命令。

struct coredump_backend_api {
    /* Signal to backend of the start of coredump. */
    coredump_backend_start_t        start;
    /* Signal to backend of the end of coredump. */
    coredump_backend_end_t          end;
    /* Raw buffer output */
    coredump_backend_buffer_output_t    buffer_output;
    /* Perform query on backend */
    coredump_backend_query_t        query;
    /* Perform command on backend */
    coredump_backend_cmd_t          cmd;
};

备注

如果要实现自定义后端,就需要定义结构体 coredump_backend_api,并实现其中的 5 个 api。

转储格式

coredump 二进制文件包含一个文件头、一个架构特定块、零个或一个线程元数据块以及多个内存块。

所有的后端都需要按照固定的格式输出打印或存储 coredump 数据,以便在解析的时候寻找相应数据块。

以下所有文件头中的数字均为小端字节序。

文件头

Field

Data Type

Description

ID

char[2]

ZE作为文件标识符。

Header version

uint16_t

确定头部信息的版本。每当头部信息结构体被修改时,该值都需要递增。避免错误解析头部信息。

Target code

uint16_t

指明目标(例如架构或 SoC )以便解析器可以实例化正确的寄存器块解析器。

Pointer size

uint8_t

大小以 uintptr_t 的2次方幂为准(例如,32位系统为5 ,64位系统为6)。

Flags

uint8_t

暂未使用

Fatal error reason

unsigned int

include/zephyr/fatal_types.h 中定义的致命错误的原因相同。

架构特定块

架构特定块包含特定于目标架构(例如 CPU 寄存器)的数据字节流。

由以下字段组成:

Field

Data Type

Description

ID

char

A表示这是一个特定于架构的模块。

Header version

uint16_t

确定此代码块的版本。该版本将由目标架构特定的代码块解析器进行解析。

Number of bytes

uint16_t

头部之后包含目标数据字节流的字节数。字节流的格式特定于目标,并且仅由目标解析器解析。

Register byte stream

uint8_t[]

包含目标架构特定的数据。

线程元数据块

线程元数据块包含调试线程所需的数据字节流。

由以下字段组成:

Field

Data Type

Description

ID

char

T表示这是一个线程元数据块。

Header version

uint16_t

确定头部信息的版本。每当头部信息结构体被修改时,该值都需要递增。避免错误解析头部信息。

Number of bytes

uint16_t

包含目标数据字节流的头部之后的字节数。

Byte stream

uint8_t[]

包含调试线程所需的数据。

备注

线程元数据内容来源: struct z_kernel _kernel

内存块

内存块包含起始地址、结束地址以及内存区域内的数据。

由以下字段组成:

Field

Data Type

Description

ID

char

M表示这是一个内存块。

Header version

uint16_t

确定头部信息的版本。每当头部信息结构体被修改时,该值都需要递增。避免错误解析头部信息。

Start address

uintptr_t

内存区域的起始地址。

End address

uintptr_t

内存区域的结束地址。

Memory byte stream

uint8_t[]

包含起始地址和结束地址之间的内存内容。

下面给出一个使用日志后端的配置及对应的日志中打印的 coredump 内容。

配置如下:

CONFIG_DEBUG_COREDUMP=y                 #启用 coredump 模块
CONFIG_DEBUG_COREDUMP_BACKEND_LOGGING=y #使用日志模块获取 coredump 输出
CONFIG_DEBUG_COREDUMP_MEMORY_DUMP_MIN=y #仅转储异常线程的堆栈、其线程结构以及一些其他最基本的必要数据

打印如下:

10:44:13.502  Coredump: rtl8721f_evb
10:44:13.502  E: ***** USAGE FAULT *****
10:44:13.502  E:   Attempt to execute undefined instruction
10:44:13.502  E: r0/a1:  0x00000000  r1/a2:  0x00100000  r2/a3:  0x0010d22c
10:44:13.502  E: r3/a4:  0x00000000 r12/ip:  0x01010101 r14/lr:  0x02038cf9
10:44:13.502  E:  xpsr:  0x41000000
10:44:13.502  E: Faulting instruction address (r15/pc): 0x020388d6
10:44:13.502  E: >>> ZEPHYR FATAL ERROR 36: Unknown error on CPU 0
10:44:13.502  E: Current thread: 0x20007a50 (unknown)
10:44:13.502  E: #CD:BEGIN#
10:44:13.502  E: #CD:5a4502000300050024000000
10:44:13.502  E: #CD:4102004400
10:44:13.502  E: #CD:00000000000010002cd210000000000001010101f98c0302d688030200000041
10:44:13.502  E: #CD:40b7002000000000000000000000000000000000000000000000000000000000
10:44:13.502  E: #CD:00000000
10:44:13.502  E: #CD:4d0100507a0020087b0020
10:44:13.502  E: #CD:08850020ec9e0020000000000080ff0000000000000000000000000000000000
10:44:13.502  E: #CD:0000000000000000000000000000000000000000000000000000000000000000
10:44:13.502  E: #CD:0000000000000000000000000000000038b7002000000000a87a0020a87a0020
10:44:13.502  E: #CD:00000000e9880002000000000000000000000000b87c00200000000000000000
10:44:13.502  E: #CD:0000000000000000000000000000000000000000000000000000000000000000
10:44:13.502  E: #CD:58b300200004000000000000087400200000000000000000
10:44:13.502  E: #CD:4d010040b7002058b70020
10:44:13.502  E: #CD:0000000000000000000000000000000000000000aaaaaaaa
10:44:13.502  E: #CD:END#

日志后端打印 coredump 内容时,添加了前缀 #CD: 、开始标志 BEGIN# 和结束标志 END#,如上面打印中所示, 以便于后续解析时从日志中取出 coredump 数据。开始和结束标志中间的部分就是 coredump 数据。

  • 5a45 ASCII 码字母对应 ZE5a45 开头的行即是文件头;

  • 41 ASCII 码字母对应 A41 开头的一块数据即是架构特定块;

  • 4d ASCII 码字母对应 M4d 开头的一段数据即是内存块。

解析步骤

启用 coredump 模块后,在发生致命错误时,CPU 寄存器和内存内容会根据启用的后端进行打印或存储。 这些 coredump 数据可以作为远程目标提供给自定义的 GDB 服务器。解析过程包括以下步骤:

  1. 根据已启用的后端,从设备获取 coredump 信息。

  2. 将 coredump 信息转换为 GDB 服务器可以解析的二进制格式。

  3. 使用脚本启动自定义 GDB 服务器,并将 coredump 二进制文件和 Zephyr ELF 文件作为参数传递。

  4. 启动与目标架构对应的调试器。

示意图如下:

../../_images/zephyr_debug_gdbserver_zh.svg

备注

使用示例可查看 离线调试

gdb 命令查看结果示例:

../../_images/zephyr_debug_coredump_parse.png

Coredump 伪设备

coredump 设备是一种伪设备驱动程序,提供了一些配置和函数接口,允许用户自定义一些在 coredump 时需要保存的内存区域。

打开宏 CONFIG_COREDUMP_DEVICE 启用 Coredump 伪设备。Coredump 伪设备有两种类型:

  • COREDUMP_TYPE_MEMCPY : coredump 时转储设备树中定义的内存条目和运行中注册的内存条目。

  • COREDUMP_TYPE_CALLBACK : coredump 时执行注册的回调函数,保存一个数组区域 coredump_bytes[]

两种类型在代码中定义如下:

enum COREDUMP_TYPE {
    COREDUMP_TYPE_MEMCPY = 0,
    COREDUMP_TYPE_CALLBACK = 1,
};

设备所需配置信息:

struct coredump_config {
    /* Type of coredump device */
    enum COREDUMP_TYPE type;
    /* Length of memory_regions array */
    int length;
    /* Memory regions specified in device tree */
    size_t memory_regions[];
};

设备所需配置信息对应的 DTS 配置信息:

/ {
    coredump_device0: coredump-device0 {
        compatible = "zephyr,coredump";
        coredump-type = "COREDUMP_TYPE_MEMCPY";
        status = "okay";
    };
    coredump_devicecb: coredump-device-cb {
        compatible = "zephyr,coredump";
        coredump-type = "COREDUMP_TYPE_CALLBACK";
        status = "okay";
        memory-regions = <0x0 0x4>;
    };
};

备注

  • COREDUMP_TYPE_MEMCPY 类型设备在 dts 中可以不添加 memory-regions 属性,此类型要保存的内存区域可以在代码中使用 api 添加;

  • COREDUMP_TYPE_CALLBACK 类型设备必须添加 memory-regions 属性,并且第一个值 0x0 为内存起始地址,但不生效, 实际使用时,内存起始地址从 &coredump_bytes[0] 获取;第二个值 0x4 为要保存内存的大小,会根据此值创建 coredump_bytes[] , coredump 时实际保存的内存区域就是这个 coredump_bytes[]。

运行过程中需要获取的信息:

struct coredump_data {
    /* Memory regions registered at run time */
    sys_slist_t region_list;
    /* Callback to be invoked at time of dump */
    coredump_dump_callback_t dump_callback;
};

运行过程中需要获取的信息可以使用如下设备 api 注册获取:

static DEVICE_API(coredump, coredump_api) = {
    .dump = coredump_impl_dump,                           //coredump 时调用此 api 执行 coredump 伪设备的内存转储
    .register_memory = coredump_impl_register_memory,     //COREDUMP_TYPE_MEMCPY 类型设备注册一块内存区域
    .unregister_memory = coredump_impl_unregister_memory, //COREDUMP_TYPE_MEMCPY 类型设备取消注册一块内存区域
    .register_callback = coredump_impl_register_callback, //COREDUMP_TYPE_CALLBACK 类型设备注册回调函数
};

coredump() 主流程中调用了 process_memory_region_list() 处理内存区域的转储,伪设备的转储也在这个函数中被调用,相关实现如下:

void process_memory_region_list(void)
{
...
#if defined(CONFIG_COREDUMP_DEVICE)
#define MY_FN(inst) process_coredump_dev_memory(DEVICE_DT_INST_GET(inst));
     DT_INST_FOREACH_STATUS_OKAY(MY_FN)
#endif
}

#if defined(CONFIG_COREDUMP_DEVICE)
static void process_coredump_dev_memory(const struct device *dev)
{
     DEVICE_API_GET(coredump, dev)->dump(dev);
}
#endif

备注

即使选择了 DEBUG_COREDUMP_MEMORY_DUMP_MIN 配置,也可以通过一个或多个 coredump 设备将额外的内存信息包含在转储文件中。

在线调试

在线调试指在代码实际运行的目标环境中,通过调试接口实时控制和观察程序执行,包括设置断点、单步、查看/修改内存与寄存器、监控线程与外设状态等。

Zephyr 的元工具 west 提供了一些调试相关的命令,支持从 Zephyr 编译目录中,将调试器连接到开发板并打开调试控制台(例如 GDB 会话)。

west debug 用法介绍

可以使用 west debug -h 查看帮助信息,帮助信息中会有每一个参数的解释。

zephyr/scripts/west_commands/debug.py 中对于 debug 功能,实现了三个子类:

  • debug :通过调试协议连接到开发板,对 flash 进行编程,然后将用户引导至调试器界面,符号表已从当前二进制文件加载,并阻塞直到调试器退出。(相当于启动 gdbserver+gdb)

  • debugserver :通过特定于开发板的调试协议连接,然后重置并停止目标。确保用户现在能够连接到调试服务器,并且符号表已从二进制文件加载。(相当于启动 gdbserver)

  • attach :通过调试协议连接到开发板,然后将用户引导至调试器界面,其中符号表已从当前二进制文件加载,并阻塞直到退出。与 'debug' 命令不同,此命令不会对 flash 进行编程。(相当于启动 gdb)

west debug 命令使用示例:

west debug            //如默认 debug runner 是 jlink,会启动 JLinkGDBServer 和 gdb 调试器
west debug -i ip:port //启动 JLinkGDBServer 连接远端 JLinkRemoteServer

使用 gdb 进行在线调试的方法,需要使用 Jlink 连接开发板和电脑,根据连线方式和使用工具不同下图列出三种典型场景。 详细调试方法及命令见 在线调试

  1. 代码在 windows 电脑,开发板通过 jlink 与 windows 电脑连接,使用 west debug 调试。

    ../../_images/zephyr_debug_online_win.svg
  2. 代码在 linux 服务器,开发板通过 jlink 与 windows 电脑连接,使用 gdb 命令行工具调试。

    ../../_images/zephyr_debug_online_win_linux.svg
  3. 代码在 linux 服务器,开发板通过 jlink 与 windows 电脑连接,使用 west debug 命令行调试。

    ../../_images/zephyr_debug_online_linux.svg

备注

west debug 命令会启动 JLinkGDBServer 和 arm-none-eabi-gdb,因此使用 west debug 命令要求主机上两者必须同时存在。

MCUboot 介绍

MCUboot 简介

MCUboot 是用于 32 位微控制器的安全引导加载程序。它为微控制器系统上的引导加载程序和系统闪存布局定义了通用基础设施,并提供了可轻松进行软件升级的安全引导加载程序。 MCUboot 不依赖于任何特定的操作系统和硬件,而是依赖于与其配合使用的操作系统的硬件移植层。在 Zephyr 中可以很方便地使用 MCUboot,同时支持 使用 RSIP 加密

启动过程

系统启动流程由两级引导完成:ROM 引导 Bootloader,Bootloader 引导 app。MCUboot 做 bootloader 时启动流程见下图:

上图侧重从 zephyr/MCUboot 工程角度描述主要流程,其中所涉及功能细节可参考 Free RTOS 启动过程

Flash Layout

MCUBoot 对 Flash Layout 的设计要求主要是出于 支持 OTA 功能 以及安全稳定性的考量,在 Swap_using_moveSwap_using_offset OTA 模式下,每一个 APP 固件都需要两块 slot,详见 Flash 分区映射

修改 Flash Layout 只需要在对应 dts 文件中修改头部的宏定义即可,其他变量会自动计算得到。主要修改的值有:

  • FLASH_BASE_PHY: flash 物理基地址

  • ROM_JUMP_ADDR:ROM 跳转 bootloader 的地址,这个地址决定了 flash 逻辑基地址

  • IMG0_LOGIC_ADDR/IMG1_LOGIC_ADDR:app 固件的逻辑地址

  • BOOT_SLOT_BASE/BOOT_SLOT_SIZE:bootloader 的 offset(基于 flash 基地址)和 size

  • IMG0_SLOT_SIZE/IMG1_SLOT_SIZE:app 固件的 size。app 和 bootloader slot 紧密排列,所以基于 offset 和 size 即可计算得到每个 slot 的区间

文件路径:zephyr/boards/realtek/rtl8721f_evb/rtl8721f_evb_mcuboot.dts

/**
* BOOT refers to mcuboot bootloader, at smallest physical address
* IMG0 refers to km4tz, is primary image, at bigger physical address
* IMG1 refers to km4ns, is secondary image, at smaller physical address
*/

/* Parameters below are set based on the hardware/rom/custom by user */
#define FLASH_BASE_PHY 0x08000000
#define ROM_JUMP_ADDR 0x10400020

/*WARNING: BETTER keep consistent with ameba_layout.ld::KM4TZ_IMG2_XIP exclude header 0x20*/
#define IMG0_LOGIC_ADDR 0x04000000
/*WARNING: MUST keep consistent with ameba_layout.ld::KM4NS_IMG2_XIP exclude header 0x20*/
#define IMG1_LOGIC_ADDR 0x02000000

#define BOOT_SLOT_BASE 0x0
#define BOOT_SLOT_SIZE DT_SIZE_K(80)  /* 0x14000 */

#define IMG0_SLOT_SIZE DT_SIZE_K(512) /* 0x80000 */
#define IMG1_SLOT_SIZE DT_SIZE_K(512) /* 0x80000 */

备注

修改 slot 相关参数后要同步修改 spic/flash0/partitions 内各 partition@ 后面的值为其 reg[0] 的值,可以避免编译警告。

MCUboot bootloader 固件

MCUboot bootloader 固件格式和代码执行地址要满足其上级引导 ROM 的要求,所以和 ameba bootloader 类似。目前整个 bootloader 是 XIP 执行。

固件格式

代码编译结束之后,需要对得到的 zephyr.bin 做进一步处理,生成满足 ROM 要求的固件,各部分关系和结构如下图所示:

../../_images/zephyr_mcuboot_bootloder_structure.svg

由三部分拼接组成:

  • manifest.bin

  • boot_xip.bin:是由 zephyr.bin 添加 32 Byte 的 header 得到

  • boot_ram.bin:是由空的 body 添加 32 Byte 的 header 得到,其作用是满足 ROM 检查要求

MCUboot app 固件

MCUboot app 固件格式要遵循 MCUBoot 要求,详细可以参考 固件格式

Twister 介绍

twister 简介

twister 是 Zephyr 内置的测试框架,用于自动化地大规模编译和运行 Zephyr 中的测试样例,以保障 Zephyr 在不同硬件平台、配置下的正确性和稳定性。 twister 通过扫描源代码仓库中特定的 YAML 配置文件(如 testcase.yaml)来识别测试套件,并对其进行编译和执行。测试的最终状态会记录在 twister.json 和其它报告文件中。

Zephyr 官方提供了详细的 twister 文档。

twister windows 环境搭建

当在 Windows 上运行 twister 时,需要搭建运行环境,包括安装工具链、Python 环境、配置环境变量三个部分。

参考 环境准备 安装环境。安装完成后,可以在 rtk_toolchain 目录下看到工具链和 Python 环境的目录。

C:\rtk-toolchain
|-- asdk-12.3.1-4431        # 工具链
|-- prebuilts-win-1.0.3     # 含 Python 环境的工具包

配置以下用户环境变量:

变量名

变量值

GNUARMEMB_TOOLCHAIN_PATH

C:\rtk-toolchain\asdk-12.3.1-4431\mingw32\newlib

ZEPHYR_TOOLCHAIN_VARIANT

gnuarmemb

当使用系统环境时,Path 变量添加以下的路径:

  • C:\rtk-toolchain\prebuilts-win-1.0.3\bin

  • C:\rtk-toolchain\prebuilts-win-1.0.3\cmake\bin

  • C:\rtk-toolchain\prebuilts-win-1.0.3\python3

  • C:\rtk-toolchain\prebuilts-win-1.0.3\python3\Scripts

安装 twister 依赖的 Python 包:

python -m pip install -r .\zephyr\scripts\requirements.txt
python -m pip install -r .\tools\requirements.txt

备注

当 Python 环境设置正确后,Powershell 中,使用 python .\scripts\twister --help 可以成功看到 twister 帮助信息。

twister 使用方法

twister 工具支持通过命令行设置参数,常用参数如下:

  • -p PLATFORM, --platform PLATFORM: 指定测试的平台,可多次使用。

  • -T TESTSUITE_ROOT, --testsuite-root TESTSUITE_ROOT: 递归搜索测试套件的根目录,可多次使用。默认值为 zephyr 根目录下的 samples/tests/ 目录。

  • -s TEST, --test TEST, --scenario TEST: 仅运行指定的测试套件场景,可多次使用。

  • -t TAG, --tag TAG: 指定标签以限制要运行的测试。多次调用将被视为逻辑"或"关系。默认不进行任何标签过滤。

  • --test-config TEST_CONFIG: 指定包含测试计划和配置的文件路径。

  • --device-testing: 在设备上进行测试。

  • --device-serial DEVICE_SERIAL:用于访问开发板的串口设备(比如 COM12,/dev/ttyACM0)。

  • --device-serial-baud DEVICE_SERIAL_BAUD:设置串口设备波特率(默认值:115200)。

  • --west-flash [WEST_FLASH]: 配置 west flash 的参数。

  • --flash_before:先 flash,再连接串口。

  • --short_build_path:创建更短的编译目录路径。Windows 上建议打开该选项。

  • --retry-build-error: 重试编译错误。

  • --retry-failed RETRY_FAILED: 重试失败的测试,最多重试指定的次数。

twister 使用样例

通过 -T 指定测试目录,将执行目录下的所有测试套件:

nuwa\zephyr> python .\scripts\twister --short-build-path --device-testing `
  --flash-before --west-flash="--port=COM12" --device-serial COM12 --device-serial-baud 1500000 `
  --platform rtl8721f_evb `
  -T tests/kernel/threads/dynamic_thread_stack/

上述命令将运行 tests/kernel/threads/dynamic_thread_stack/tescase.yaml 中的 8 个测试套件。

可以通过 --test 进一步指定单个测试套件:

nuwa\zephyr> python .\scripts\twister --short-build-path --device-testing `
  --flash-before --west-flash="--port=COM12" --device-serial COM12 --device-serial-baud 1500000 `
  --platform rtl8721f_evb `
  -T tests/kernel/threads/dynamic_thread_stack/ `
  --test kernel.threads.dynamic_thread.stack.no_pool.no_alloc.no_user

zephyr 在 zephyr/tests/test_config.yaml 中提供了 smokeacceptance 两个等级的批量测试。 smoke 测试集较小,通常作为持续集成的第一步,如果 smoke 测试失败,通常意味着有严重缺陷,可以立即中止后续更耗时的测试。 acceptance 测试集包含了 smoke,比 smoke 更全面,更耗时。

zephyr smoke 批量测试命令:

nuwa\zephyr> python .\scripts\twister --short-build-path --device-testing `
  --flash-before --west-flash="--port=COM12" --device-serial COM12 --device-serial-baud 1500000 `
  --platform rtl8721f_evb `
  -T tests --test-config=".\tests\test_config.yaml" --level="smoke" `
  --retry-failed 3 --retry-build-errors

zephyr acceptance 批量测试命令:

nuwa\zephyr> python .\scripts\twister --short-build-path --device-testing `
  --flash-before --west-flash="--port=COM12" --device-serial COM12 --device-serial-baud 1500000 `
  --platform rtl8721f_evb `
  -T tests --test-config=".\tests\test_config.yaml" --level="acceptance" `
  --retry-failed 3 --retry-build-errors

备注

根据实际情况,替换上述命令中 --west-flash 和 --device-serial 指定的串口,--platform 指定的平台。

自定义批量测试集

如果想通过 twister 执行一批指定的测试套件,可以参考 zephyr/tests/test_config.yaml 的写法自定义测试配置文件,然后像上述的 smoke、acceptance 批量测试那样,通过 --test-config--level= 参数指定文件和等级。 在测试配置文件中,可以定义测试级别(levels 下的 name)、级别依赖关系(inherits),adds 中可以使用正则表达式指定测试套件。

下述样例中定义了 smoke 和 acceptance 两个测试级别,并且 acceptance 继承自 smoke。

platforms:
  override_default_platforms: false
  increased_platform_scope: true
levels:
  - name: smoke
    description: >
      A plan to be used verifying basic zephyr features on hardware.
    adds:
      - kernel.threads.*
      - kernel.timer.behavior
      - arch.interrupt
      - boards.*
      - drivers.gpio.1pin
      - drivers.console.uart
      - drivers.entropy
  - name: acceptance
    description: >
      More coverage
    inherits:
      - smoke
    adds:
      - kernel.*

twister 配置文件

板级配置文件

twister 板级配置文件描述了开发板的硬件特性和配置信息,采用 YAML 格式,位于每个开发板目录下,通常命名为 <board>.yaml。 例如 rtl8721f_evb 的 twister 板级配置文件是 zephyr/boards/realtek/rtl8721f_evb/rtl8721f_evb.yaml

关于 twister 板级配置文件中各个字段的含义,可以参考官方文档 Board Configuration

testcase.yaml

testcase.yamltests 目录中测试的定义文件,由 twister 读取,用于为当前目录下的测试代码声明测试场景、配置和运行规则。 它具有以下用途:

  • 通过 filter、platform_allow、platform_exclude 等配置控制测试范围和行为,精确指定一个测试在什么条件下、在哪些板子上、以何种配置运行。

  • 使用 min_ram 等字段,避免将大内存测试刷写到资源不足的开发板上。

  • 利用 tags 对测试进行分类,可以快速运行某一类测试。

  • 通过 harness 和 harness_config 定义如何从控制台输出中判断测试通过与否,实现自动化验证。

testcase.yaml 文件中各个配置项的含义,可以参考官方文档 Test Scenario Identifier 。 其中:

  • tags: <list of tags> (required),一组用于测试场景的字符串标签。可以通过 twister 命令行的 --tag--exclude_tag 筛选要运行的测试套件。

  • depends_on: <list of features>,依赖的特性需要在 板级配置文件supported 特性列表中支持,否则过滤测试套件。

  • platform_exclude: <list of platforms>,如果不想执行某个测试套件,可以通过 platform_exclude 排除。例如:

    platform_exclude:
        - rtl8721f_evb
        - rtl872xda_evb
    

sample.yaml

sample.yaml 用于向 twister 描述一个位于 samples/ 目录下的演示程序。该文件中各个配置项的含义可以参考 testcase.yaml

twister 报告

执行 twister 时,默认在当前目录下生成 twister-out 目录,用来存放二进制文件、日志、报告等。

  • testplan.json:测试计划,可用于了解哪些测试套件将被执行。

  • twister.json: 测试套件的执行结果。

  • twister.log: twister 工具本身的执行过程日志。记录编译、烧录、执行的步骤和错误。可以用于诊断 twister 自身执行或环境问题。

  • twister.xmltwister_report.xmltwister_suite_report.xml:其它形式的报告。

下方是 twister.json 中一个成功的测试套件的结果样例:

{
    "name":"tests/drivers/console/hello_world/drivers.console.uart",
    "arch":"arm",
    "platform":"rtl8721f_evb/rtl8721f",
    "path":"tests\\drivers\\console\\hello_world",
    "run_id":"852209a4da4005a76fee86f9a772c087",
    "runnable":true,
    "retries":0,
    "toolchain":"gnuarmemb",
    "status":"passed",
    "execution_time":"8.44",
    "build_time":"493.38",
    "testcases":[
        {
            "identifier":"drivers.console.uart",
            "execution_time":"8.44",
            "status":"passed"
        }
    ]
}

下方是 twister.json 中一个失败的测试套件的结果样例,会记录失败的原因:

{
    "name":"tests/kernel/mem_protect/syscalls/kernel.memory_protection.syscalls.timeslicing",
    "arch":"arm",
    "platform":"rtl8721f_evb/rtl8721f",
    "path":"tests\\kernel\\mem_protect\\syscalls",
    "run_id":"6ce8788b17745a48c339e75dfdf479b3",
    "runnable":true,
    "retries":0,
    "toolchain":"gnuarmemb",
    "status":"failed",
    "log":"",
    "reason":"Timeout",
    "execution_time":"198.54",
    "build_time":"651.04",
    ......
}

关于测试套件和测试样例状态的含义,详见官方文档 twister status

twister 测试失败定位

如果某个测试套件失败,可以尝试以下方法进行问题定位:

  • twister.json 报告中搜索测试套件的名称,确认其执行状态(status)、失败原因(reason),获取初步的错误信息。

  • 进入该测试套件在 twister-out 中的输出目录,检查以下日志文件:

    • build.log: 定位编译错误

    • device.loghandler.log:定位运行时的问题,比如烧录错误、超时。

  • 使用 twister --test <测试套件名> ..... 命令单独重新执行该测试。这有助于判断失败是测试套件自身的问题,还是由测试执行顺序、环境残留等因素引起的。

如果在批量测试过程中,大量测试套件出现 Timeout 错误,一个常见原因是某个测试套件运行后未能正确响应设备烧录命令 reboot uartburn,导致后续测试无法正常执行。 可按以下步骤定位并锁定具体是哪一个测试套件引发了问题:

  1. 连接 tracetool 日志工具,然后重新启动开发板。

  2. 从上述启动日志中,找到测试套件的 RunID 的值,例如:

    ......
    Unexpected fault during test
    ===================================================================
    RunID: 6ce8788b17745a48c339e75dfdf479b3
    PROJECT EXECUTION FAILED
    
  3. twister.json 报告中,搜索与上述 RunID 值相匹配的测试记录。该记录所对应的测试套件,可能是导致后续一系列超时问题的根源。

CANbus 子系统介绍

示例工程简介

zephyr 在 zephyr/samples/subsys/canbus/isotp 目录实现了一个 ISO-TP 协议的 canbus 子系统示例工程。

该示例展示如何使用 ISO-TP 库在两块开发板之间进行消息交换:

  • 一个长消息,使用块大小(BS)为 8 帧进行发送。

  • 一个短消息,最小分隔时间(STmin)为 5 毫秒。

短消息的发送函数调用是非阻塞的;长消息的发送函数调用是阻塞的。

备注

rtl8721f_evb 开发板的默认配置已打开回环模式,不需要额外连线,一块开发板就可以正常运行示例工程。

编译命令

使用以下命令编译 isotp 示例工程:

west build -b rtl8721f_evb zephyr/samples/subsys/canbus/isotp

默认配置是可以正常编译运行的,如需修改配置详情可见 isotp 示例工程相关配置

输出示例

Start sending data
[00:00:00.000,000] <inf> can_driver: Init of CAN_1 done
========================================
|   ____  ___  ____       ____  ____   |
|  |_  _|/ __||    | ___ |_  _||  _ \  |
|   _||_ \__ \| || | ___   ||  | ___/  |
|  |____||___/|____|       ||  |_|     |
========================================
Got 248 bytes in total
This is the sample test for the short payload
TX complete cb [0]

备注

以上输出的数值可能有所不同。

ISO-TP 协议简介

ISO-TP(ISO 15765-2)是建立在经典帧之上的“分片与重组”上层协议,它不改变经典帧的物理/链路层结构, 而是将自己的“协议头/类型字段+数据”放进经典帧的数据域(Data Field)中,并其中定义了自己的帧类型(SF/FF/CF/FC)和协议内容。 可以进一步分为以下四种帧类型:

  1. 单帧 SF【Single Frame】

  2. 首帧 FF【First Frame】

  3. 连续帧 CF【Consecutive Frame】

  4. 流控制帧 FC【Flow Control Frame】

单帧(SF)

如果数据有效载荷为 7 字节或小于 7 字节,则单帧将用于 ISO-TP 协议中的数据传输。其中数据字段的第一个字节用于 PCI(Protocol Control Information)。 该字节再次分为两部分,其中首字节高 4 位为帧类型,低 4 位为 DL(数据长度)。

../../_images/zephyr_canbus_subsys_isotp_frame_SF.svg

关键取值与编码说明:

  • DL(数据长度):本帧数据长度(1..7)

首帧(FF)

首帧是 ISO-TP 协议中多帧消息包的初始消息。当必须传送超过 7 个字节的分段数据时使用它。 第一帧包含完整数据包的长度以及初始数据。在 FF 中,前 2 字节用于 PCI,其中第 1 字节的高 4 位用于帧类型, 低 4 位和下一个 1 字节(总共 8+4 = 12 位) 数据字段用于 DL(2^12 = 4095 个数据字节)。所以在 FF 中, 第一次只能传输 6 个字节的数据。这个帧负责向接收者发送关于将要发送多少总数据字节的信息。

../../_images/zephyr_canbus_subsys_isotp_frame_FF.svg

关键取值与编码说明:

  • DL(数据长度):完整数据的总长度(12 bit),包含所有后续 CF 数据

连续帧(CF)

传输协议或 ISO-TP 协议的主要任务是传输这样的消息(由于长度而不能作为单个协议数据单元(PDU)传输的), 这样的消息被分段在多帧单独的 PDU 消息中。

../../_images/zephyr_canbus_subsys_isotp_frame_CF.svg

关键取值与编码说明:

  • 序号 SN(CF 的低 4 bit):取值 0..15,发送端在每个 CF 后 SN = (SN+1) mod 16;规范中 FF 后首个 CF 的 SN=1

流控制帧(FC)

ISO-TP 协议的流控制机制用于配置发送方以匹配接收方的属性(定时、可用接收缓冲区、接收准备情况)。 流控制(FC)始终由接收端发送,以便根据接收端的属性来决定发送端发送数据的方式。

../../_images/zephyr_canbus_subsys_isotp_frame_FC.svg

关键取值与编码说明:

  • FlowStatus(FS,FC 的低 4 bit)

    • 0 = CTS(Clear To Send):允许继续发送,附带 BS、STmin 参数

    • 1 = WAIT:请等待,发送端增加 WFT 计数,超过 WFTmax 则中止

    • 2 = OVFLW:接收端缓冲不足,立即中止本次传输

  • Block Size(BS)

    • 0:无块流控(从 FF 后一直 CF 到结束,不再需要中途 FC)

    • 1..255:每发送 BS 个 CF 后,发送端必须等待新的 FC

  • STmin(FC 字节 2)

    • 0x00..0x7F:单位为毫秒(ms)

    • 0xF1..0xF9:单位为 100 微秒(100 µs 步进)

    • 其他值保留;若实现不支持微秒编码,通常将其当作 0 处理(需参考具体栈实现)

典型示例

(经典帧,标准寻址,十六进制)

  • SF(3 字节 PDU,数据 AA BB CC)

    03 AA BB CC 00 00 00 00
    
  • FF(总长度 0x0014=20,首帧数据 12 34 56 78 9A BC)

    10 14 12 34 56 78 9A BC
    
  • CF(SN=1,数据 DE AD BE EF 01 02 03)

    21 DE AD BE EF 01 02 03
    
  • FC(CTS,BS=8,STmin=5ms)

    30 08 05 00 00 00 00 00
    

canbus 子系统简介

zephyr 中和 canbus 子系统有关的目录和文件如下所示:

nuwa $
   ├── modules
   │   ├── hal/realtek/ameba/amebaG2/source/fwlib # hal
   │   └── lib/canopennode/                           # canopennode 第三方库
   └── zephyr
       ├── include/zephyr/drivers/can.h               #宏、结构体、_syscall api(调用 driver)
       ├── drivers
       │   ├── can
       │   │   ├── can_ameba_a2c.c                    #driver_api
       │   │   ├── can_common.c                       #通用 api
       │   │   └── can_shell.c                        #shell 命令实现
       │   └── net/canbus.c                           #canbus_api(调用_syscall api)
       ├── dts
       │   ├── arm/realtek/amebaG2/amebaG2.dtsi       #driver node
       │   └── bindings/can/realtek,ameba_a2c.yaml    #yaml
       ├── modules
       │   └── canopennode/                           #canopen 协议的实现与封装(调用_syscall api)
       ├── subsys
       │   ├── canbus/isotp/                          #实现了 ISO-TP 的会话层,包括分段、流控等(调用_syscall api)
       │   └── net
       │          ├── ip/canbus_socket.c              #从“套接字”到“L2 发送入口”的映射与调用(承上启下)
       │          ├── l2/canbus/canbus_raw.c          #L2 链路层(调用 canbus_api)
       │          └── lib/socket_can.c                #AF_can 套接字层(调用 ip 层接口)
       ├── samples
       │   ├── drivers/can/
       │   ├── modules/canopennode/                   #canopennode 示例工程
       │   └── subsys
       │       ├── canbus/isotp/                      #isotp send 示例工程
       │       └── net/socket/can/                    #socket 示例工程
       └── tests
           ├── drivers/can/
           └── subsys/canbus/

isotp 示例工程相关配置

配置可分为两类文件:

  • DTS: 包括 *.dts*.dtsi*.yaml*.overlay 类型的文件,用于定义 DTS 变量和属性规则。

  • Kconfig: *.conf 类型的文件用于定义配置项的默认取值。

用户可以通过修改 zephyr/samples/subsys/canbus/isotp/boards 目录下的 rtl8721f_evb.overlayrtl8721f_evb.conf 文件来修改系统的默认配置。

DTS 配置

zephyr/dts/arm/realtek/amebaG2/amebaG2.dtsi 中定义设备节点:

/ {
    soc {
        a2c0: can@41005000 {
            compatible = "realtek,ameba-a2c";
            reg = <0x41005000 0x400>;
            clocks = <&rcc AMEBA_A2C0_CLK>;
            interrupts = <60 0>;
            status = "disabled";
        };
   }
}

zephyr/boards/realtek/rtl8721f_evb/rtl8721f_evb-pinctrl.dtsi 中定义设备使用的 pin:

&pinctrl {
    compatible = "realtek,ameba-pinctrl";
    a2c0_default: a2c0_default {
        group1 {
            pinmux = <AMEBA_PINMUX('A', 13, AMEBA_A2C0_TX)>,
                     <AMEBA_PINMUX('A', 12, AMEBA_A2C0_RX)>;
            bias-pull-up;
        };
    };
}

zephyr 的 isotp 示例工程中使用的设备是从 chosen 中的 zephyr,canbus 获取的, 因此要在 zephyr/samples/subsys/canbus/isotp/boards/rtl8721f_evb.overlay 中选择使用某个设备:

/ {
    chosen {
        zephyr,canbus = &a2c0;
    };
};

&a2c0 {
    pinctrl-0 = <&a2c0_default>;
    pinctrl-names = "default";
    status = "okay";
};

Kconfig 配置

zephyr/samples/subsys/canbus/isotp/prj.conf 中设置一些基础配置,配置含义可以参考后面的配置项说明:

CONFIG_LOG=y
CONFIG_CAN=y
CONFIG_CAN_MAX_FILTER=8
CONFIG_ISOTP=y
CONFIG_ISOTP_RX_SF_FF_BUF_COUNT=2

在编译 rtl8721f_evb 开发板的固件时,可以在 zephyr/samples/subsys/canbus/isotp/boards/rtl8721f_evb.conf 中打开回环模式, 打开回环模式之后,不需要额外连线,一块开发板就可以正常运行示例工程:

CONFIG_SAMPLE_LOOPBACK_MODE=y

isotp Kconfig 配置项位于 zephyr/subsys/canbus/isotp/Kconfig 中。下面是一些配置的说明:

ISOTP_WFTMAX:

WFTmax,接收端回 FC=WAIT 时,发送端可接受的 WAIT 次数上限;超过则中止传输。

ISOTP_BS_TIMEOUT:

等待下一个流控帧(FC)的超时,在发送 FF 或发送满一个块后等待 FC 时计时。

ISOTP_A_TIMEOUT:

As/Ar 发送/接收超时(协议栈内部对发送动作和接收响应的保护计时)。

ISOTP_CR_TIMEOUT:

接收端等待下一帧连续帧(CF)的超时。

ISOTP_REQUIRE_RX_PADDING:

要求接收的 SF、FC 和最后一个 CF 必须填充至完整 DLC,未用字节需填充。

ISOTP_ENABLE_TX_PADDING:

发送端按需填充帧,使用 0xCC 作为填充值(Bosch 推荐)。

ISOTP_RX_BUF_COUNT:

接收用数据缓冲块数量(net_buf 池中的块数),用于装载重组中的数据。

ISOTP_RX_BUF_SIZE:

每个接收数据块的大小(字节)。

ISOTP_RX_SF_FF_BUF_COUNT:

专用于接收单帧(SF)与首帧(FF)的预分配缓冲数量。

ISOTP_USE_TX_BUF:

发送时将应用数据复制到内部 net_buf,调用方可立即释放原始数据。

ISOTP_TX_BUF_COUNT:

发送数据缓冲块数量(用于 ISOTP_USE_TX_BUF 模式)。

ISOTP_BUF_TX_DATA_POOL_SIZE:

发送缓冲分配所在内存池的总大小(字节)。

ISOTP_ENABLE_CONTEXT_BUFFERS:

启用“上下文缓冲”(memory slab)保存发送上下文,使“发送即忘”(send-and-forget)成为可能。

ISOTP_TX_CONTEXT_BUF_COUNT:

可并发/排队的发送上下文数量上限。

ISOTP_CUSTOM_FIXED_ADDR:

启用自定义的“固定寻址(Fixed Addressing)”ID 编码。

在打开 ISOTP_CUSTOM_FIXED_ADDR 配置的情况下,还需要配置下面一些配置:

ISOTP_FIXED_ADDR_SA_POS:

源地址(SA)在标识符中的位起始位置。

ISOTP_FIXED_ADDR_SA_MASK:

提取源地址的掩码。

ISOTP_FIXED_ADDR_TA_POS:

目的地址(TA)位起始位置。

ISOTP_FIXED_ADDR_TA_MASK:

提取目的地址的掩码。

ISOTP_FIXED_ADDR_PRIO_POS:

优先级字段位起始位置。

ISOTP_FIXED_ADDR_PRIO_MASK:

提取优先级字段的掩码。

ISOTP_FIXED_ADDR_RX_MASK:

接收过滤用掩码,通常设置为匹配任意优先级与源地址的宽松过滤。