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 不同是允许的,但如果两个 node_name 相同的节点都没有 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 = <1>;
        #size-cells = <1>;
        main_clk: main_clk_node@41008000 {
          compatible = "realtek,ameba-rcc";
          reg = <0x41008000 0x400>;
        };
      };
    }
    
    • clocks 节点中 #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.txtlist(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 中的同一个字段值不同会报错,除了属性中的 required 字段

  • 还可以选择性导入 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 = DEVICE_DT_GET(DT_INST_DMAS_CTLR_BY_NAME(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 文件中形如 <prefix>-cells 的 Specifier cell 字段名中 -cells 前的前缀一致

扩展阅读

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/zephyr/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(!(flags_p & IRQ_ZERO_LATENCY), \
            "ZLI interrupts must be registered using IRQ_DIRECT_CONNECT()"); \
    _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/zephyr/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() 注册中断如下(定义在 zephyr/arch/common/dynamic_isr.c 中),主要是动态修改 _sw_isr_table:

void __weak z_isr_install(unsigned int irq, void (*routine)(const void *),
             const void *param)
{
    unsigned int table_idx;

    /*
     * Do not assert on the IRQ enable status for ARM GIC since the SGI
     * type interrupts are always enabled and attempting to install an ISR
     * for them will cause the assertion to fail.
     */
#ifndef CONFIG_GIC
    __ASSERT(!irq_is_enabled(irq), "IRQ %d is enabled", irq);
#endif /* !CONFIG_GIC */

    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/zephyr/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/zephyr/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 */

    /* CONFIG_PM 相关代码块已省略:在启用电源管理时,该代码在 idle 唤醒处理期间会
     * 关闭非零延迟中断,以确保 idle 状态清除和时钟编程不被打断。 */

    int32_t irq_number = __get_IPSR();
    irq_number -= 16;

    const 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 文件系统 基于 VFS(虚拟文件系统)架构,通过文件系统注册机制将应用程序与具体的文件系统实现解耦,支持在不同挂载点同时挂载多个文件系统实例。 Zephyr 支持多种文件系统类型(FatFS, LittleFS, Ext2),可根据应用需求灵活选择。其设计遵循模块化原则,开发者可通过 Kconfig 配置工具按需裁剪。 Zephyr 支持多个文件系统类型同时工作,由 Kconfig 中的 FILE_SYSTEM_MAX_TYPES 配置,默认值 2

和文件系统有关的目录和文件如下所示:

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
    │   │   ├── 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 是独立于平台和存储介质的文件系统层,它与物理设备(例如 SD)完全分离。存储设备控制模块不属于 FatFS 模块,需要由实现者提供。 Zephyr 对 FatFS 的适配位于 zephyr/modules/fatfs 目录。

zephyr/modules/fatfs/CMakeLists.txt 展示了 Zephyr 对 FatFS 的集成方式:

  • 使用 FatFS 原生的 ff.cffunicode.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_util.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;
    };
  };
};

prj.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";
  };
};

prj.conf 中启用以下配置:

CONFIG_FILE_SYSTEM=y
CONFIG_FAT_FILESYSTEM_ELM=y

Settings 介绍

Zephyr settings 子系统用于持久化保存设备配置与运行时状态。它提供统一 API,支持 FCB、NVS、ZMS、文件系统等多种存储后端。设置项以键值对存储,键采用 package/subtree 的层级命名约定。更多详情请参考官方文档:Settings

备注

无论采用哪种后端,应用层使用的 settings API 都保持不变。因此可以在需求变化时,灵活更换后端,而无需修改应用层代码。

应用层开发者可重点关注以下章节:

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 作为后端时,在 prj.conf 或板卡配置文件中打开以下配置:

CONFIG_SETTINGS=y
CONFIG_ZMS=y
CONFIG_ZMS_LOOKUP_CACHE=y
CONFIG_ZMS_LOOKUP_CACHE_SIZE=512

如果使用了 settings_runtime_xx() API,还需在 prj.conf 或板卡配置文件中打开:

CONFIG_SETTINGS_RUNTIME=y

Settings 示例

settings 系统 API 由 include/zephyr/settings/settings.h 提供。

使用示例:

基本使用步骤:

  1. 调用 settings_subsys_init() 初始化 settings 子系统。

  2. 调用 settings_register() 注册模块的 settings handler。

  3. 调用 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 与具体的操作系统和硬件平台无关,通过平台提供的硬件抽象层访问 Flash、加密等底层功能。在 Zephyr 中可以方便地集成 MCUboot,并支持 RSIP 固件加密

启动过程

系统采用两级引导:

  1. 芯片 ROM 中的启动代码加载并启动 MCUboot。

  2. MCUboot 验证应用固件,并在验证通过后启动应用程序。

具体启动流程如下图所示:

RTL8721Dx:
../../_images/rtl8721dx_zephyr_mcuboot_boot_flow.svg

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

Flash Layout

在常见的固件双槽升级方案中,Flash 通常包含以下三个分区:

  • boot_partition:存放 MCUboot;

  • slot0_partition:主槽,存放当前启动的固件;

  • slot1_partition:次槽,存放待升级的固件。

MCUboot 可通过 Swap_using_moveSwap_using_offset 模式,在主、次槽之间完成交换。

如需调整分区大小或地址,只需修改板级配置文件头部的宏定义,其他变量会自动计算:

  • FLASH_BASE_PHY:flash 物理基地址

  • XXX_LOGIC_ADDR:app 固件的逻辑地址

  • BOOT_SLOT_BASE / BOOT_SLOT_SIZE:bootloader 分区的起始 offset 和大小

  • APP_SLOT_SIZE:单个 slot 大小,primary 和 secondary 共用

RTL8721Dx:

文件路径:zephyr/boards/realtek/rtl872xda_evb/rtl872xda_evb_common.dts

/* BOOT: mcuboot bootloader */
#define BOOT_SLOT_BASE   0x0
#define BOOT_SLOT_SIZE   DT_SIZE_K(256)  /* 0x00040000 */

/* APP0: km4  APP1: km0 */
#define APP0_LOGIC_ADDR  0x0E000000
#define APP1_LOGIC_ADDR  0x0C000000
#define APP_SLOT_SIZE    DT_SIZE_K(512)  /* 0x00080000 */
#define APP_SLOT0_OFFSET (BOOT_SLOT_BASE + BOOT_SLOT_SIZE)   /* 0x00040000 */
#define APP_SLOT1_OFFSET (APP_SLOT0_OFFSET + APP_SLOT_SIZE)  /* 0x000C0000 */

对应的 DTS 分区定义:

boot_partition: partition@0 {
   label = "bootloader";
   reg = <BOOT_SLOT_BASE BOOT_SLOT_SIZE>;       /* 0x0, 256 KB */
};
slot0_partition: partition@40000 {
   label = "image-0";
   reg = <APP_SLOT0_OFFSET APP_SLOT_SIZE>;      /* 0x40000, 512 KB */
};
slot1_partition: partition@C0000 {
   label = "image-1";
   reg = <APP_SLOT1_OFFSET APP_SLOT_SIZE>;      /* 0xC0000, 512 KB */
};

备注

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

MCUboot bootloader 固件

MCUboot bootloader 的固件格式和代码执行地址需满足其上级引导 ROM 的要求。因此代码编译结束后,需要对 bootloader 固件做进一步处理,最终由三部分拼接组成,每部分前附加 32 Byte header:

  • manifest:签名与校验信息,供 ROM 验证固件

  • xip_boot.bin:MCUboot 代码

  • ram_1.bin:SRAM 入口表(start table + valid pattern),ROM 读取它以获得 bootloader 入口地址

ROM 依据各 header 中的载入地址,判断该部分是留在 flash 就地 XIP 执行,还是先拷贝到 SRAM。因此入口表放在哪一部分,各芯片不同:

RTL8721Dx:
../../_images/rtl8721dx_zephyr_mcuboot_bootloder_structure.svg

xip_boot.bin 从 flash 直接 XIP 执行,而入口表需在 SRAM 中运行。两者载入地址不同,故分处两个部分:ROM 先将 ram_1.bin 拷入 SRAM,再通过入口表跳入 XIP 代码。

MCUboot app 固件

MCUboot 在启动 app 前会校验固件的完整性和签名,因此 app 固件必须满足 MCUboot 规定的格式,包含:Firmware HeaderFirmware BodyTLV Area (Optional)Firmware Trailer。示意图如下所示:

../../_images/zephyr_ota_image_formate.svg

备注

图示为应用固件的格式要求,并非 MCUboot 自身的固件格式(MCUboot 固件格式见 MCUboot bootloader 固件)。

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
       }
    };
    

备注

该区域位于固件主/次槽(slot0_partition / slot1_partition)的末尾,不紧跟固件正文。因此槽位大小需预留 Trailer 空间,并按 Flash 扇区对齐。

Twister 介绍

Twister 简介

Twister 是 Zephyr 内置的测试框架,用于自动、批量地编译并运行测试用例,从而验证 Zephyr 在不同硬件平台和配置下的正确性与稳定性。

Twister 通过扫描源码仓库中的 YAML 配置文件(testcase.yamlsample.yaml)来识别测试套件,完成编译、烧录、设备执行、结果判定,最终输出 twister.json 等多份测试报告。

Zephyr 官方提供了详细的 Twister 文档

Twister 环境搭建

运行 Twister 前,需要先搭建 Zephyr 命令行开发环境(工具链、Python 环境和环境变量)。Ubuntu 和 Windows 的完整步骤请参考 编译和烧录

本文推荐通过 nuwa.py 运行 Twister。首次针对某平台(比如 -p rtl8721f_evb)运行时,工具链会自动下载到 rtk-toolchain 目录,ZEPHYR_TOOLCHAIN_VARIANTGNUARMEMB_TOOLCHAIN_PATH 两个环境变量也会自动配置,无需手动设置。

环境就绪后,在 nuwa 工作区根目录下运行 python nuwa.py 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: 指定的测试场景(scenario);可重复使用以指定多个场景。

  • -t TAG, --tag TAG: 按标签过滤要运行的测试;可重复使用以指定多个标签。不指定时不进行标签过滤。

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

测试设备相关

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

  • --port PORT: Realtek Ameba 定制参数,指定开发板的串口(比如 Linux 上的 /dev/ttyUSB0,Windows 上的 COM12)。指定后 Twister 会自动配置串口、波特率、烧录器等,无需再手动设置下面的 --device-serial 等底层参数。

  • --remote-server REMOTE_SERVER: Realtek Ameba 定制参数,指定远程串口服务器的 IP,配合 --port 用于开发板接在另一台运行 AmebaRemoteService 软件的电脑上的远程测试场景。

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

  • --device-serial-baud DEVICE_SERIAL_BAUD:设置串口设备波特率;不指定时默认 115200。

  • --west-flash [WEST_FLASH]: 透传给 west flash 的额外参数(逗号分隔)。

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

编译与重试相关

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

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

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

Twister 使用示例

以下命令均在 nuwa 工作区根目录下运行。请根据实际情况替换命令中的串口(--west-flash--device-serial)和平台(--platform)。

运行某个目录下的所有测试场景(用 -T 指定目录):

Linux (Ubuntu):
./nuwa.py twister --device-testing \
  --flash-before --west-flash="--port=/dev/ttyUSB0" --device-serial /dev/ttyUSB0 --device-serial-baud 1500000 \
  --platform rtl8721f_evb \
  -T zephyr/tests/kernel/threads/dynamic_thread_stack/

上述命令将运行 zephyr/tests/kernel/threads/dynamic_thread_stack/testcase.yaml 中定义的 8 个测试场景。

只运行单个测试场景(用 --test 指定):

Linux (Ubuntu):
./nuwa.py twister --device-testing \
  --flash-before --west-flash="--port=/dev/ttyUSB0" --device-serial /dev/ttyUSB0 --device-serial-baud 1500000 \
  --platform rtl8721f_evb \
  -T zephyr/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 更全面,更耗时。

运行 smoke 批量测试

Linux (Ubuntu):
./nuwa.py twister --device-testing \
  --flash-before --west-flash="--port=/dev/ttyUSB0" --device-serial /dev/ttyUSB0 --device-serial-baud 1500000 \
  --platform rtl8721f_evb \
  -T zephyr/tests --test-config=zephyr/tests/test_config.yaml --level=smoke \
  --retry-failed 3 --retry-build-errors

运行 acceptance 批量测试

Linux (Ubuntu):
./nuwa.py twister --device-testing \
  --flash-before --west-flash="--port=/dev/ttyUSB0" --device-serial /dev/ttyUSB0 --device-serial-baud 1500000 \
  --platform rtl8721f_evb \
  -T zephyr/tests --test-config=zephyr/tests/test_config.yaml --level=acceptance \
  --retry-failed 3 --retry-build-errors

自定义批量测试集

如果想通过 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.yaml 是 Twister 读取的测试定义文件,位于 tests/ 下每个测试目录中,用于声明该目录测试代码的测试场景、运行条件和判定规则。

借助它可以:

  • filterplatform_allowplatform_exclude 等字段,精确控制测试在什么条件下、在哪些开发板上、以何种配置运行。

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

  • harnessharness_config 定义如何从串口输出判断测试是否通过,实现自动化验证。

常用字段(完整列表见官方文档 Test Scenario Identifier):

  • tags:一组字符串标签,可配合命令行的 --tag--exclude-tag 筛选测试。

  • depends_on:依赖的特性,需在板级配置文件(见 板级配置文件)的 supported 列表中声明,否则该测试被过滤。

  • 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:定位运行时的问题,比如烧录错误、超时。

  • 参考 单场景示例,保持其余参数不变,把 --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.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-can.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 {
        can0: can@41005000 {
            compatible = "realtek,ameba-can";
            reg = <0x41005000 0x400>;
            clocks = <&rcc AMEBA_CAN0_CLK>;
            interrupts = <60 0>;
            wakeup-source-id = <WAKE_SRC_CAN0>;
            status = "disabled";
        };
   }
}

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

&pinctrl {
    compatible = "realtek,ameba-pinctrl";
    can0_default: can0_default {
        group1 {
            pinmux = <AMEBA_PINMUX('A', 25, AMEBA_CAN0_TX)>,
                     <AMEBA_PINMUX('A', 26, AMEBA_CAN0_RX)>;
            bias-pull-up;
        };
        group2 {
            pinmux = <AMEBA_PINMUX('A', 3, AMEBA_GPIO)>;
            bias-pull-down;
        };
    };
}

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

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

&can0 {
    pinctrl-0 = <&can0_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:

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