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 _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_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.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 是独立于平台和存储介质的文件系统层,它与物理设备(eg: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/
│               └── cordump.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_gdbsever.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,会启动JLinkGDBSever和gdb调试器
west debug -i ip:port //启动JLinkGDBSever连接远端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命令要求主机上两者必须同时存在。

OTA 介绍

OTA 简介

OTA (Over-the-Air,空中下载技术),是指通过网络连接向远程设备传输新固件并完成更新的过程。 尽管其名称中包含 "空中"(暗示无线连接),但在实际应用中,通过有线连接(例如以太网)接收的更新,通常也被称为 OTA 更新。

Zephyr 支持多种 OTA 升级方案,如下表所示:

示例

路径

Golioth

示例位于 外部仓库

Eclipse hawkBit

zephyr/samples/subsys/mgmt/hawkbit/

UpdateHub

zephyr/samples/subsys/mgmt/updatehub/

SMP Server

zephyr/samples/subsys/mgmt/mcumgr/smp_svr/

Lightweight M2M (LWM2M)

zephyr/samples/net/lwm2m_client/

其中, SMP Server 示例提供了完整的 OTA 解决方案,结合 MCUboot 引导加载程序与 MCUmgr 设备管理框架,可实现安全、可靠的固件升级。下面将基于该示例介绍 OTA 升级过程。

OTA关键组件

实现一次安全的 OTA 更新,主要依赖以下三个核心部分协同工作:

  • MCUboot:一个开源的、安全的引导加载程序。它负责在设备启动时验证应用程序固件的完整性与真实性,并管理固件在 Flash 存储区域中的切换与更新流程。

  • SMP Server:内置于您应用程序中的 Simple Management Protocol 服务。它负责接收并处理来自外部管理工具 MCUmgr 的各种命令,包括固件上传、状态查询等。

  • MCUmgr:运行在您开发主机上的客户端管理工具。您可以使用它通过 UART/BT/UDP 等多种方式,向设备端的 SMP Server 发送指令,从而触发和控制整个 OTA 流程。

OTA涉及模块层级图

实现安全 OTA 更新涉及以下 Zephyr 子模块,其层级关系如图所示:

../../_images/zephyr_ota_subsystem.svg

升级流程

使用上述 OTA关键组件,典型的升级流程图,如下图所示:

../../_images/zephyr_ota_upgrade_flow.svg

备注

Flash 分区映射

Flash map 是设备闪存的“分区表”,把设备上的 Flash 划分成若干独立的区域(flash area),并为每个区域分配一个数值 ID。 MCUboot 和应用程序通过这些 ID 找到具体的物理地址和大小,进行读写、擦除、升级等操作。

通常,我们会为每个固件划分两个槽位(Slots):主槽(Primary slot) 和 次槽(Secondary slot)。 以下是两个固件场景下的 Flash 分区配置示意:

设备树中 Flash 布局
&flash0 {
   reg = <0x103FF000 DT_SIZE_M(4)>;
   status = "okay";
   partitions {
      compatible = "fixed-partitions";
      #address-cells = <1>;
      #size-cells = <1>;

      /* flash area ID 0 */
      boot_partition: partition@0 {
         label = "bootloader";
         reg = <0x00000000 0x00014000>;
         read-only;
      };

      /* 为 AP(固件0)分配 主槽(slot0_partition)和 次槽(slot1_partition) */
      /* flash area ID 1 */
      slot0_partition: partition@14000 {
         label = "image-0";
         reg = <0x00014000 0x00080000>;
      };

      /* flash area ID 2 */
      slot1_partition: partition@94000 {
         label = "image-1";
         reg = <0x00094000 0x00080000>;
      };

      /* 为 NP(固件1) 分配 主槽(slot2_partition)和 次槽(slot3_partition) */
      /* flash area ID 3 */
      slot2_partition: partition@114000 {
         label = "image-2";
         reg = <0x00114000 0x00080000>;
      };

      /* flash area ID 4 */
      slot3_partition: partition@194000 {
         label = "image-3";
         reg = <0x00194000 0x00080000>;
      };
   };
};

固件格式

在 Zephyr 的 OTA 场景下,受 MCUboot 引导的应用固件必须符合 MCUboot 定义的固件格式,包含: 固件头部( Header 固件正文( Body TLV区( 可选 固件尾部 Trailer 。 示意图如下所示:

备注

  • 图示为应用固件的格式要求,而非 MCUboot 自身的固件格式。

TLV 区(类型-长度-值)

TLV 区位于固件正文之后,是可选项,通常存放哈希、签名、安全计数器等字段。

如果存在受保护 TLV 区,则需存在 IMAGE_TLV_PROT_INFO_MAGIC 信息头 ,且受保护 TLV 区并入哈希计算。如果不存在,那么哈希仅覆盖 “固件头 + 固件正文”。

为了确保回滚保护有效, IMAGE_TLV_SEC_CNT, 0x50 需写入受保护 TLV 区。使用 imgtool 的 --security-counter 选项会将其放入受保护 TLV 区。

TLV格式
#define IMAGE_TLV_INFO_MAGIC        0x6907
#define IMAGE_TLV_PROT_INFO_MAGIC   0x6908

/** Image TLV header.  All fields in little endian. */
STRUCT_PACKED image_tlv_info {
   uint16_t it_magic;
   uint16_t it_tlv_tot;                /* size of TLV area (including tlv_info header) */
};

/** Image trailer TLV format. All fields in little endian. */
STRUCT_PACKED image_tlv {
   uint16_t it_type;                   /* IMAGE_TLV_[...]. */
   uint16_t it_len;                    /* Data length (not including TLV header). */
};
与 OTA 相关的常用 TLV 类型
#define IMAGE_TLV_KEYHASH           0x01   /* hash of the public key */
#define IMAGE_TLV_SHA256            0x10   /* SHA256 of image hdr and body */
#define IMAGE_TLV_RSA2048_PSS       0x20   /* RSA2048 of hash output */
#define IMAGE_TLV_ECDSA224          0x21   /* ECDSA of hash output - Not supported anymore */
#define IMAGE_TLV_ECDSA_SIG         0x22   /* ECDSA of hash output */
#define IMAGE_TLV_RSA3072_PSS       0x23   /* RSA3072 of hash output */
#define IMAGE_TLV_ED25519           0x24   /* ED25519 of hash output */
#define IMAGE_TLV_ENC_RSA2048       0x30   /* Key encrypted with RSA-OAEP-2048 */
#define IMAGE_TLV_ENC_KW            0x31   /* Key encrypted with AES-KW-128 or 256 */
#define IMAGE_TLV_ENC_EC256         0x32   /* Key encrypted with ECIES-P256 */
#define IMAGE_TLV_ENC_X25519        0x33   /* Key encrypted with ECIES-X25519 */
#define IMAGE_TLV_SEC_CNT           0x50   /* security counter */

固件尾部( Trailer )

用于记录交换/状态元数据,记录在槽位的末尾,其空间不可用于存放固件正文。

固件 Trailer 结构如下(以字节为单位):

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
~                                                               ~
~    Swap status (BOOT_MAX_IMG_SECTORS * min-write-size * s)    ~
~                                                               ~
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                 Encryption key 0 (16 octets) [*]              |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    0xff padding as needed                     |
|  (BOOT_MAX_ALIGN minus 16 octets from Encryption key 0) [*]   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                 Encryption key 1 (16 octets) [*]              |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    0xff padding as needed                     |
|  (BOOT_MAX_ALIGN minus 16 octets from Encryption key 1) [*]   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      Swap size (4 octets)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    0xff padding as needed                     |
|        (BOOT_MAX_ALIGN minus 4 octets from Swap size)         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Swap info   |  0xff padding (BOOT_MAX_ALIGN minus 1 octet)  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Copy done   |  0xff padding (BOOT_MAX_ALIGN minus 1 octet)  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Image OK    |  0xff padding (BOOT_MAX_ALIGN minus 1 octet)  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    0xff padding as needed                     |
|         (BOOT_MAX_ALIGN minus 16 octets from MAGIC)           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       MAGIC (16 octets)                       |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
参数说明
BOOT_MAX_IMG_SECTORS:

默认值是 slot0_partition 所占扇区数。

min-write-size:

Flash 最小写入粒度。

s:

s = 3(swap_using_move),s= 2(swap_using_offset

Swap status:

逐扇区的 交换状态 记录,用于掉电后恢复。

Encryption keys:

(可选)加密密钥,仅当启用加密时存在。

Swap size:

本次交换需搬移的数据总量(涵盖固件正文 + TLV 区)。

Swap info:

1 字节有效,BOOT_MAX_ALIGN对齐;低 4 位为 交换类型,高 4 位为固件编号。

Copy done:

1 字节有效,BOOT_MAX_ALIGN对齐;表示拷贝阶段完成状态:0x01=Set,0xFF=Unset。

Image OK:

1 字节有效,BOOT_MAX_ALIGN对齐;表示新固件是否已确认,未确认将于下次启动回退。0x01=Set,0xFF=Unset。

MAGIC:

16 字节,尾区有效性的标记。

  • BOOT_MAX_ALIGN=8 时为固定 16 字节模式:

    const union boot_img_magic_t boot_img_magic = {
       .val = {
          0x77, 0xc2, 0x95, 0xf3,
          0x60, 0xd2, 0xef, 0x7f,
          0x35, 0x52, 0x50, 0x0f,
          0x2c, 0xb6, 0x79, 0x80
       }
    };
    
  • 否则,前 2 字节为对齐值,后 14 字节固定模式:

    const union boot_img_magic_t boot_img_magic = {
       .align = BOOT_MAX_ALIGN,
       .magic = {
          0x2d, 0xe1,
          0x5d, 0x29, 0x41, 0x0b,
          0x8d, 0x77, 0x67, 0x9c,
          0x11, 0x0f, 0x1f, 0x8a
       }
    };
    

备注

  • 该区域位于固件槽位的尾部,而不是紧跟固件正文。因此分配的槽位需要预留 Tailer 空间,并按扇区对齐。

OTA 升级方式

MCUboot 支持多种固件升级方式,如下表所示。详细可参考 MCUboot官网介绍

升级方式

特点

Upgrade only

将新固件直接覆盖旧固件,不进行交换,不可回滚

Swap using scratch

通过 scratch 分区做中转,完成新旧固件交换,可回滚

Swap using offset

依靠分区内的偏移布局进行交换,无需 scratch 分区,可回滚

Swap using move

通过移动数据完成交换,无需 scratch 分区,可回滚

Direct XIP

直接原地执行,无需交换

RAM load

将固件完整加载到 RAM 后执行

Firmware loader

通过专用加载器进行升级

当前支持以下两种升级方式:

swap_using_move

swap_using_move 是一种无需临时存储区(scratch)的固件交换算法,适用于双区(主槽/次槽)OTA 升级场景。 其核心是通过将主槽扇区整体后移一位,并交替复制主次槽内容,完成固件交换。

通过配置宏 CONFIG_BOOT_SWAP_USING_MOVE 可启用此升级方式。采用 --sysbuild 时需在应用程序 sysbuild.conf 中配置 SB_CONFIG_MCUBOOT_MODE_SWAP_USING_MOVE=y

工作原理
  1. 扇区下移:先将主槽所有扇区向下移动一个扇区位置。

  2. 交替复制:从第 1 个扇区开始,依次执行:

  3. 将次槽第 N 个扇区复制到主槽第 N 个扇区;

  4. 重复直至所有扇区交换完成。

    ../../_images/zephyr_ota_swap_using_move.svg

从图示可知,每个扇区的交换存在 3 个状态:

  1. 主槽[N] → 主槽[N+1]已完成

  2. 次槽[N] → 主槽[N] 已完成

  3. 主槽[N+1] → 次槽[N] 已完成

swap_using_offset

swap_using_offset 是一种无需临时存储区(scratch)的固件交换算法,为 swap_using_move 的增强版。 其核心是利用次槽的一个扇区作为缓冲区,并交替复制主次槽内容,完成固件交换。

通过配置宏 CONFIG_BOOT_SWAP_USING_OFFSET 启用;采用 --sysbuild 时需在应用程序 sysbuild.conf 中配置 SB_CONFIG_MCUBOOT_MODE_SWAP_USING_OFFSET=y

工作原理
  1. 固件放置:待升级的新固件必须从次槽的第二个扇区开始存放。

  2. 交替复制:从第 1 个扇区开始,依次执行:

  3. 将主槽第 N 个扇区复制到次槽第 N 个扇区;

  4. 将次槽第 N+1 个扇区复制到主槽第 N 个扇区;

  5. 重复直至所有扇区交换完成。

    ../../_images/zephyr_ota_swap_using_offset.svg

从图示可知,每个扇区的交换存在 2 个状态:

  1. 主槽[N] → 次槽[N] 已完成

  2. 次槽[N+1] → 主槽[N] 已完成

交换类型

在非恢复场景(升级过程未发生异常中断),MCUboot 会根据固件 trailer 中的标志位决定执行的交换类型(swap type)。 由于受限于 Flash 硬件特性,trailer 的设计并不直观,直接读出其字节很难还原设备状态。 因此,MCUboot 将“各种可能的 trailer 组合状态”映射为“交换类型”,并按优先级顺序判定。

State I 场景:在 swap_using_offset 执行 REVERT 前的风险窗口,记录 magic 和 copy-done 到次槽,下次启动会继续执行 REVERT。

State I (swap using offset only)
                 | primary slot | secondary slot |
-----------------+--------------+----------------|
           magic | Any          | Good           |
        image-ok | Any          | Unset          |
       copy-done | Any          | Set            |
-----------------+--------------+----------------'
 result: BOOT_SWAP_TYPE_REVERT                   |
-------------------------------------------------'

State II 场景:次槽固件合法且未确认,执行“测试交换”。系统将从次槽交换到主槽并启动。 新固件需在应用中设置 image-ok 进行确认;若未确认,下一次启动会回退(REVERT)。

State II
                 | primary slot | secondary slot |
-----------------+--------------+----------------|
           magic | Any          | Good           |
        image-ok | Any          | Unset          |
       copy-done | Any          | Any            |
-----------------+--------------+----------------'
 result: BOOT_SWAP_TYPE_TEST                     |
-------------------------------------------------'

State III 场景:次槽固件已预先标记为 image-ok,执行“永久交换”,升级后不会回退。

State III
                 | primary slot | secondary slot |
-----------------+--------------+----------------|
           magic | Any          | Good           |
        image-ok | Any          | 0x01           |
       copy-done | Any          | Any            |
-----------------+--------------+----------------'
 result: BOOT_SWAP_TYPE_PERM                     |
-------------------------------------------------'

State IV 场景:主槽已完成复制(copy-done=Set),但新固件未确认(image-ok=Unset),因此在本次启动执行回退,恢复到旧固件。

State IV
                 | primary slot | secondary slot |
-----------------+--------------+----------------|
           magic | Good         | Any            |
        image-ok | 0xff         | Any            |
       copy-done | 0x01         | Any            |
-----------------+--------------+----------------'
 result: BOOT_SWAP_TYPE_REVERT                   |
-------------------------------------------------'

如果上述 State I-IV 都没有匹配,则 MCUboot 不会尝试交换映像,而是执行 State V 表格中三种交换类型之一。

State V
                 | primary slot | secondary slot |
-----------------+--------------+----------------|
           magic | Any          | Any            |
        image-ok | Any          | Any            |
       copy-done | Any          | Any            |
-----------------+--------------+----------------'
 result: BOOT_SWAP_TYPE_NONE,                    |
         BOOT_SWAP_TYPE_FAIL, or                 |
         BOOT_SWAP_TYPE_PANIC                    |
-------------------------------------------------'

备注

  • State 编号越小优先级越高。一旦匹配某一状态,立即确定对应的交换类型并停止继续匹配。

  • 交换类型:

    • BOOT_SWAP_TYPE_NONE:无升级,直接启动主槽固件。

    • BOOT_SWAP_TYPE_TEST:试运行;如未确认(写 image-ok=0x01),下次启动执行回退。

    • BOOT_SWAP_TYPE_PERM:永久交换。

    • BOOT_SWAP_TYPE_REVERT:上次 TEST 未确认,回退至此前的旧固件。

    • BOOT_SWAP_TYPE_FAIL:目标固件无效(校验失败等)。

    • BOOT_SWAP_TYPE_PANIC:交换遇到不可恢复错误(终止)。

交换状态

交换状态(Swap status)用于掉电恢复,记录在主槽的 Trailer 。 当在固件交换过程中发生重启时,MCUboot 可依据已记录的交换状态恢复现场,继续完成未尽的交换操作。 该区域由一系列“单字节记录”组成,每条记录独立写入,并按照设备的最小写入粒度进行对齐填充。 正常的交换流程如下:

  1. 初始化: 同时擦除主、次槽 Trailer;随后在主槽 Trailer 写入 swap_info、swap_size 及 MAGIC 标记。

  2. 逐扇区交换:仅对固件实际占用的扇区(含 TLV)进行交换,并按选择的算法记录每扇区的状态。

  3. 收尾:完成后在主槽 Trailer 中写入 copy done,标记交换完成。

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|sec127,state 0 |sec127,state 1 |sec127,state 2 |sec126,state 0 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|sec126,state 1 |sec126,state 2 |sec125,state 0 |sec125,state 1 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|sec125,state 2 |                                               |
+-+-+-+-+-+-+-+-+                                               +
~                                                               ~
~               [Records for indices 124 through 1              ~
~                                                               ~
~               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
~               |sec000,state 0 |sec000,state 1 |sec000,state 2 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

备注

  • 图示假设 min-write-size = 1,每个“扇区索引”对应 3 个状态记录,适用于 swap_using_move 模式;而在 swap_using_offset 模式下,每个扇区仅有 2 个状态记录,所需总空间更小。

防版本回滚机制

为了防止设备被降级到有已知漏洞的固件版本,MCUboot 支持放版本回滚策略,分为 软件防回滚 与 硬件防回滚。 其中 软件防回滚 又可分为基于版本号(version) 和基于安全计数器(security counter) 两种。

软件防回滚(基于版本号)

基于版本号的软件防回滚,只有 !BOOT_DIRECT_XIP 升级方式下存在。该方式为纯软件防回滚,不防物理攻击(如对 Flash 擦除/回写)。

  1. 原理:

    • MCUboot 每次启动对固件版本号进行比较。

    • 如果新固件的版本号低于当前运行固件的版本号,则拒绝启动。

    • 如果新固件的版本号大于或等于当前运行固件的版本,则允许启动。

  2. 配置:

    CONFIG_MCUBOOT_DOWNGRADE_PREVENTION=y
    
  3. 固件签名时嵌入版本号:

    # 示例:在应用程序 prj.conf 或板卡特定配置中设置
    CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION="0.0.2"
    

软件防回滚(基于安全计数器)

基于 security counter 的软件防回滚,只有 (BOOT_SWAP_USING_MOVE || BOOT_SWAP_USING_SCRATCH || BOOT_SWAP_USING_OFFSET) 方式下存在。 该方式为纯软件防回滚,不防物理攻击(如对 Flash 擦除/回写)。

  1. 原理:

    • MCUboot 每次启动对 security counter 号进行比较。

    • 如果新固件的 security counter 小于当前运行固件的 security counter,则拒绝启动。

    • 如果新固件的 security counter 大于或等于当前运行固件的 security counter,则允许启动。

  2. 配置:

    CONFIG_MCUBOOT_DOWNGRADE_PREVENTION=y        # security counter 选项开启依赖于该配置
    CONFIG_MCUBOOT_DOWNGRADE_PREVENTION_SECURITY_COUNTER=y
    
  3. 固件签名时嵌入 security counter :

    # 示例:在应用程序 prj.conf 或板卡特定配置中设置
    CONFIG_MCUBOOT_IMGTOOL_SIGN_SECURITY_COUNTER=2
    

备注

  • 当前 NP 固件总是使用与 AP 相同的 security counter。

硬件防回滚(基于OTP)

  1. 原理:

    • MCUboot 将当前已接受的 security counter 保存到硬件计数器(OTP)中。

    • 如果新固件的 security counter 小于 OPT 记录值 ,则拒绝启动。

    • 如果新固件的 security counter 大于或等于 OPT 记录值,则允许启动。

  2. 配置:

    CONFIG_MCUBOOT_HW_DOWNGRADE_PREVENTION=y
    
  3. 固件签名时嵌入 security counter :

    # 示例:在应用程序 prj.conf 或板卡特定配置中设置
    CONFIG_MCUBOOT_IMGTOOL_SIGN_SECURITY_COUNTER=2
    

备注

  • 当前固件 NP 总是使用与 AP 相同的 security counter。

  • 固件 NP 与 AP 的硬件计数器均有上限,请仅在必要时更新,达到上限后可能造成设备启动失败。

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 添加32Byte的header得到

  • boot_ram.bin:是由空的body添加32Byte的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.xmltwsiter_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 值相匹配的测试记录。该记录所对应的测试套件,可能是导致后续一系列超时问题的根源。