Zephyr Development

DTS Introduction

Zephyr Configuration System

Zephyr compilation is divided into two phases:

  • Configuration phase: Execute cmake to process DTS and Kconfig

    • DTS is converted to build/zephyr/include/generated/zephyr/devicetree_generated.h

    • Kconfig is converted to: build/zephyr/include/generated/zephyr/autoconf.h

  • Build phase: Execute make or ninja tools to generate firmware

Refer to the following diagram for the main flow of the configuration phase:

../../_images/zephyr_configuration_overview.svg

DTS Configuration

DTS Basic Syntax

DTS consists of nodes, and each node contains several properties or child nodes.

  • Basic node form: node_label:node_name@unit-address{...}

  • Basic property form: prop_name = value;, which is a key-value pair (except for boolean type, which has only an identifier; its presence indicates True)

The following diagram illustrates the structure of a simple DTS file:

../../_images/zephyr_dts_grammar.svg

A Real Hardware Example

Assume the following hardware structure:

../../_images/zephyr_dts_small_hw_structure.svg

The DTS structure can be used to describe this hardware:

../../_images/zephyr_dts_small_dts_structure.svg

Write the DTS file as follows:

/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

The unit-address in a node can be omitted; when present, the corresponding node must contain the reg property, and its first element value must equal the unit-address

Attention

unit-address can serve as part of the node identifier, meaning two nodes with the same node_name but different unit-address values are allowed. However, if neither node has a unit-address, an error will occur.

Important Properties

reg
  • It is a sequence of (address, size) pairs, each describing a register block. The specific meaning varies by device. For example:

    • UART device: reg = <0x100000 0x100>, <0x200000 0x100>;, has two register blocks

    • CPU: reg = <0> typically represents the CPU number in multi-CPU systems, consistent with cpu@0

  • The bit widths of address and size in reg are described by the address-cells and size-cells properties in the parent node of the node they belong to. For example:

    / {
      clocks {
        #address-cells = <1>;
        #size-cells = <1>;
        main_clk: main_clk_node@41008000 {
          compatible = "realtek,ameba-rcc";
          reg = <0x41008000 0x400>;
        };
      };
    }
    
    • In the clocks node, #address-cells and #size-cells respectively constrain the reg in its direct child node (main_clk):
      • address consists of a 32-bit cell

      • size consists of a 32-bit cell

    • When #size-cells is 0, the size part is not needed in reg

compatible
  • The compatible property consists of one or more strings, each corresponding to a DTS Binding file that defines the programming model for nodes containing this property

  • Nodes using compatible must also comply with these programming models

  • The binding method between compatible strings and DTS Binding files is: the compatible field value in the DTS Binding file equals the string, so DTS scripts can automatically find the corresponding DTS Binding file through the compatible property value

  • When compatible has multiple values, the build script matches from left to right until it finds the first matching DTS Binding file; others are ignored

DTS Organization in Zephyr

  • In Zephyr, a board’s DTS description is completed by multiple dts(i) files

  • These files come from three layers divided by different abstraction levels:

    • Arch layer: Highest level of abstraction, defines hardware descriptions related to CPU instruction set architecture

    • SoC layer: Medium level of abstraction, typically hardware descriptions for a specific series provided by a vendor, including peripherals, clock control, interrupts, pins, etc.

    • Board layer: Lowest level of abstraction, corresponds to a specific hardware entity, specifically describing onboard peripherals configuration, pin mappings, etc.

  • The board layer DTS is the entry point for processing scripts, usually including the SoC layer DTS, which in turn includes the Arch layer DTS

  • The DTS processing script ultimately generates a complete dts file (build/zephyr/zephyr.dts)

Refer to the following diagram for DTS processing inputs and outputs:

../../_images/zephyr_dts_inputs_outputs.svg

DTS Binding

Note

Functionality:

  • DTS binding files define the programming model for DTS nodes that reference them, which can be simply understood as specifications or constraints, such as node property requirements (which properties exist, property types, allowed values, etc.), child node bindings, bus information, etc.

  • DTS binding supports referencing other files through include to achieve reuse functionality

DTS binding file location: By default, look in zephyr/dts/bindings (generally recommended), or:

  • CMakeLists.txt: list(APPEND DTS_ROOT /path/to/your/dts)

  • west command parameter: -DDTS_ROOT=/path/to/your/dts

DTS Binding Basic Syntax

DTS binding is in YAML format. The following diagram introduces each field with examples:

../../_images/zephyr_dts_binding_grammar.svg

The following details some of the more important fields:

include
  • The included binding file will be merged with the current binding file to form a complete binding file

  • If a field value in the current binding file differs from the same field in its include, an error will occur, except for the required field in properties

  • You can also selectively import some properties from the include file. Refer to the following example:

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
  • Primarily used to match the compatible property in DTS files to bind the binding file to the corresponding node

  • Typically takes the form: <vender>,<device>

properties
  • Primarily used to add descriptions and constraints to properties in DTS nodes. The complete syntax is as follows:

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: Set to true indicates that the property must be declared in the corresponding node, otherwise the script will report an error

  • type: Describes the property type. See DTS Property Data Types for details

  • deprecated: Set to true indicates that the property is deprecated; the tool will report a warning during compilation

  • default: Sets a default value for the property

  • enum: A list specifying the allowed values for the property; if a value outside this list is set in the node, an error will occur

  • const: Indicates that the property is a constant

Specifier Cell

The function of Specifier cell can be understood as specifying the init parameters for a device. They can pass parameters for initialization when the device is instantiated (phandle reference). The following example explains how it works. Assume a DTS file as follows:

/ {
  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";
    };
  };
}

Focus on the hardware logic defined in it:

  • The SOC node contains a DMA child node (line 3) and an i2c0 child node (line 9). The i2c0 device uses two DMA instances (lines 12, 13) named rx/tx respectively (line 14).

  • The dmas property is of phandle-array type, containing two DMA instances, each initialized using three parameters (4 22 0 and 5 21 0).

  • The specification for these three initialization parameters is defined through the Specifier cell field in the binding file. See the binding file 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
    

Where:

  • The "#dma-cells" property in property specifies the number of parameters

  • dma-cells describes the names of these three parameters, which can be accessed in C code by their corresponding names

Accessing these parameters in C code:

#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),

Where:

  • The index parameter is the index for iterating through multiple I2C instances

  • dir is the index for accessing dmas; here dir can be rx or tx

  • channel corresponds to the first parameter described in dma-cells in the binding file

  • So the final value of .dma_channel here is 4 or 5

Attention

The dmas property name in the i2c0 node in DTS is fixed; its singular form dma must match the prefix before -cells in the Specifier cell field name (e.g. <prefix>-cells) in the binding file

Extended Reading

DTS Property Data Types

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 Introduction

Interrupt Information

DTS Configuration

Set the driver’s interrupt properties as shown in the highlighted lines below. The interrupts property takes two parameters: the first is the interrupt number, and the second is the interrupt priority.

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

Getting Interrupt Properties

The macros for retrieving information from DTS are defined in the file zephyr/include/zephyr/devicetree.h.

Driver code needs to use the interrupt number when registering an interrupt. Here’s how to get it:

DT_INST_IRQN

#define DT_INST_IRQN(inst) DT_IRQN(DT_DRV_INST(inst))

Get the interrupt number for the DT_DRV_COMPAT device. Parameter description:

inst:

Device instance number

When registering an interrupt in driver code, use macros to retrieve the priority from DTS. Here’s how:

DT_INST_IRQ

#define DT_INST_IRQ(inst, cell) DT_INST_IRQ_BY_IDX(inst, 0, cell)

Get the value of the DT_DRV_COMPAT interrupt specifier. Parameter description:

inst:

Instance number

cell:

Cell name specifier

DT_INST_IRQ_BY_NAME

#define DT_INST_IRQ_BY_NAME(inst, name, cell) \
    DT_IRQ_BY_NAME(DT_DRV_INST(inst), name, cell)

Get the value of the DT_DRV_COMPAT interrupt specifier by name. Parameter description:

inst:

Instance number

name:

Name of the interrupt specifier, using lowercase letters and underscores.

cell:

Cell name specifier

DT_INST_IRQ_BY_IDX

#define DT_INST_IRQ_BY_IDX(inst, idx, cell) \
    DT_IRQ_BY_IDX(DT_DRV_INST(inst), idx, cell)

Get the value of the DT_DRV_COMPAT interrupt specifier by index. Parameter description:

inst:

Instance number

idx:

Logical index of the interrupt specifier array

cell:

Cell name specifier

Interrupt Priority Explanation

In the DTS, arm,num-irq-priority-bits = < 0x3 > is configured, meaning the priority register has three bits with a maximum of 8 priority levels. Priority-related macros have different implementations for different architectures. The Cortex-M implementation is in the file zephyr/include/zephyr/arch/arm/cortex_m/exception.h, as shown below:

#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)

Interrupt Registration

There are two types of ISRs: normal ISR and direct ISR, with different registration methods. Normal ISRs have two registration methods: compile-time registration and runtime registration, depending on whether parameters are known at compile time; Direct ISRs require declaration before registration. You can refer to Interrupt Vector Table Query to help understand the difference between the two ISR registration methods.

Normal ISR

Compile-time Registration

Use the IRQ_CONNECT macro for compile-time ISR registration. Compile-time registration is used when all parameters are already determined. The related implementation is in the file include/zephyr/irq.h, as shown below:

#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)

Parameter description:

irq_p:

Interrupt number

priority_p:

Interrupt priority

isr_p:

Address of the interrupt service routine

isr_param_p:

Parameter passed to the interrupt service routine

flags_p:

Architecture-specific IRQ configuration flags

Different architectures have different implementations. The ARM architecture implementation is in the file zephyr/include/zephyr/arch/arm/irq.h, as shown below:

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

Note

Z_ISR_DECLARE generates a struct _isr_list structure variable placed in the .intList section. The structure variable contains the interrupt number irq, flags, interrupt function func, and the parameter param passed to the interrupt function.

The struct _isr_list structure is defined as follows:

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

If the passed function is uart_isr and this is the 5th call to Z_ISR_DECLARE, then the expanded content of Z_ISR_DECLARE would be:

static  __aligned(__alignof(struct _isr_list))struct _isr_list  __attribute__((section(".intList")))
    __used __isr_uart_isr_irq_5 = {
        irq,
        0,
        uart_isr,
        param}

The structure variable __isr_uart_isr_irq_5 will be placed in .intList.

Runtime Registration

Since the IRQ_CONNECT macro requires all its parameters to be known at compile time, this may not be acceptable in some cases. You can also use irq_connect_dynamic() to install interrupts at runtime. The related implementation is in the file zephyr/include/zephyr/irq.h, as shown below:

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

Parameter description:

irq:

Interrupt number

priority:

Interrupt priority

routine:

Address of the interrupt service routine

parameter:

Parameter passed to the interrupt service routine

flags:

Architecture-specific IRQ configuration flags

The Cortex-M architecture function implementation is in the file zephyr/arch/arm/core/cortex_m/irq_manage.c, as shown below:

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() registers the interrupt as follows (defined in zephyr/arch/common/dynamic_isr.c), mainly by dynamically modifying _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;
}

Direct ISR

Declaration

Unlike normal ISRs, direct ISR functions need to be declared first. The related macros are in the file include/zephyr/irq.h, with the code below:

#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)

Different architectures have different implementations. The ARM architecture implementation is in the file zephyr/include/zephyr/arch/arm/irq.h, with the code below:

#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();
    }
}

The direct ISR declaration mainly adds a header and footer around the original interrupt service function, providing some common operations for all direct ISRs.

Note

The kernel allows interrupt handlers (ISRs) to be installed directly into the vector table to minimize interrupt latency as much as possible. This allows ISRs to be called directly without going through the software interrupt table. However, some kernel work is still required when exiting the ISR, such as context switching. ISRs connected to the software interrupt table perform this operation automatically through the wrapper _isr_wrapper, while direct ISRs connected to the hardware vector table add this operation through ARCH_ISR_DIRECT_FOOTER in ISR_DIRECT_DECLARE.

Registration

Direct ISRs are registered through IRQ_DIRECT_CONNECT. The macro is defined in the file include/zephyr/irq.h, as shown below:

#define IRQ_DIRECT_CONNECT(irq_p, priority_p, isr_p, flags_p) \
    ARCH_IRQ_DIRECT_CONNECT(irq_p, priority_p, isr_p, flags_p)

Parameter description:

irq_p:

Interrupt number

priority_p:

Interrupt priority

isr_p:

Address of the interrupt service routine

flags_p:

Architecture-specific IRQ configuration flags

Note

IRQ_DIRECT_CONNECT has one fewer parameter than IRQ_CONNECT: isr_param_p. Direct ISRs do not need parameters and can jump directly to execution in the hardware vector table.

Different architectures have different implementations. The ARM architecture implementation is in the file zephyr/include/zephyr/arch/arm/irq.h, as shown below:

#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)

Note

The direct ISR registration macro also calls Z_ISR_DECLARE, which is very similar to normal ISR registration overall. The only difference is that flag = ISR_FLAG_DIRECT, meaning that for direct ISRs, a struct _isr_list structure variable is still generated and placed in .intList.

Enable/Disable Interrupt

Enable Interrupt

After interrupt registration is complete, you need to enable the interrupt for it to respond normally. The interrupt enable function is as follows:

#define irq_enable(irq) arch_irq_enable(irq)

Parameter description:

irq:

Interrupt number

Disable Interrupt

After enabling an interrupt, if you want it to stop responding, you can disable the interrupt. The interrupt disable function is as follows:

#define irq_disable(irq) arch_irq_disable(irq)

Parameter description:

irq:

Interrupt number

Interrupt Response

Interrupt Vector Table

The Zephyr interrupt vector table is divided into three parts, as shown in the diagram below:

../../_images/zephyr_nvic_interrupt_vector_table_position.svg

exc_vector_table contains the first 16 exception vectors, _irq_vector_table is the hardware interrupt vector table, and _sw_isr_table is the software interrupt vector table. The representation is as follows:

/*System exception loading*/
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
/*Hardware interrupt vector table*/
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),
    ...
}
/*Software interrupt vector table*/
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 */
    ...
}

Note

The hardware interrupt vector table irq_vector_table and software interrupt vector table sw_isr_table are initially empty. During compilation, a new interrupt vector table is generated based on compile-time registered interrupts, and normal ISRs are added to the software interrupt table at runtime.

Query Interrupt Vector Table

When an interrupt occurs, the hardware interrupt vector table _irq_vector_table is queried. Possible scenarios are:

  • If it’s not _isr_wrapper(), it’s a direct interrupt, and the ISR is entered directly.

  • If it is _isr_wrapper(), it’s a normal interrupt. Enter _isr_wrapper() and look up _sw_isr_table to find the corresponding ISR. If it’s not z_irq_spurious(), it will execute normally.

  • If it’s a normal interrupt but the ISR found in _sw_isr_table is z_irq_spurious(), it means the interrupt is not registered, and an error will be reported.

Note

z_irq_spurious() is installed in all _sw_isr_table slots at startup. It throws an error when called.

The query diagram is as follows:

../../_images/zephyr_nvic_interrupt_vector_table_query.svg

_isr_wrapper() has different implementations for different architectures. The Cortex-M implementation is in the file zephyr/arch/arm/core/cortex_m/isr_wrapper.c, as shown below:

void _isr_wrapper(void)
{
#ifdef CONFIG_TRACING_ISR
    sys_trace_isr_enter();
#endif /* CONFIG_TRACING_ISR */

    /* CONFIG_PM block omitted for brevity: disables non-ZLI interrupts
     * during idle wakeup handling when power management is enabled. */

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

In _isr_wrapper(), you can uniformly perform some common operations, such as enabling/disabling trace and kernel cleanup before exception handler exit, similar to ISR_DIRECT_HEADER() and ISR_DIRECT_FOOTER() added when declaring direct interrupts.

_sw_isr_table does not map exceptions, only interrupts. Since the first 16 interrupt numbers are already used by system exceptions, the interrupt number read from the IPSR must be reduced by 16.

File System Introduction

File System Introduction

Zephyr File System is based on a VFS (Virtual File System) architecture that decouples applications from specific file system implementations through a registration mechanism, supporting multiple file system instances mounted at different mount points simultaneously. Zephyr supports multiple file system types (FatFS, LittleFS, Ext2), allowing flexible selection based on application requirements. Its design follows modular principles, enabling developers to customize through Kconfig configuration tools. Zephyr supports multiple file system types working simultaneously, configured by FILE_SYSTEM_MAX_TYPES in Kconfig, with a default value of 2.

The directories and files related to the file system are shown below:

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

The figure below shows the main modules involved in LittleFS on FLASH and FatFS on SD.

../../_images/zephyr_file_system_diagram.svg

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

FatFS

FatFS is a generic FAT/exFAT file system module designed specifically for small embedded systems, compatible with multiple FAT formats: FAT, FAT32, and exFAT. FatFS is a file system layer independent of platform and storage media, completely separated from physical devices (e.g., SD). The storage device control module is not part of the FatFs module and needs to be provided by the implementer. Zephyr’s adaptation of FatFS is located in the zephyr/modules/fatfs directory.

zephyr/modules/fatfs/CMakeLists.txt shows how Zephyr integrates FatFs:

  • Uses FatFS native ff.c and ffunicode.c

  • Re-implements diskio.c as zfs_diskio.c

  • Re-implements ffsystem.c as zfs_ffsystem.c

  • Configuration in zephyr_fatfs_config.h takes precedence over ffconf.h

Note

In zephyr_fatfs_config.h, a series of #undef and #define operations are used to override configurations in ffconf.h.

LittleFS

LittleFS is a small fail-safe file system designed for microcontrollers, with power-loss recovery capability, support for dynamic wear leveling, and the ability to detect and repair bad blocks.

zephyr/modules/littlefs/CMakeLists.txt shows how Zephyr integrates LittleFS:

  • Uses LittleFS native lfs.c

  • Re-implements CRC algorithm

    • The purpose of providing zephyr_lfs_crc.c is for future CRC updates

    • Currently it is the same as the CRC in native lfs_util.c

  • zephyr_lfs_config.h configuration replaces lfs_util.h

FatFS VS LittleFS

Feature

FatFS

LittleFS

Wear Leveling

Not supported

Supported

Power Loss Protection

Not supported

Supported

Compatibility

Strong (Windows/DOS compatible with FatFS)

Weak (Windows not compatible with LittleFS)

Recommended Storage Device

SD cards, USB drives and other removable storage devices

FLASH

Note

FatFS file system type is recommended for SD cards and USB.

LittleFS file system type is recommended for FLASH.

File System API

Zephyr File System API functions all start with fs_. They can be categorized into the following types:

  • Registration

    • fs_register()/fs_unregister()

    • Automatically completed by the file system driver

  • Mounting

    • fs_mount()/fs_unmount()

  • Descriptors

    • fs_file_t_init()/fs_dir_t_init()

    • Descriptors must be initialized before performing file/directory operations

  • File Operations

    • fs_open()/fs_read()/fs_write()/fs_close()/fs_seek()/fs_tell()/fs_truncate()/fs_sync()

  • Directory Operations

    • fs_opendir()/fs_closedir()/fs_mkdir()/fs_readdir()

For API usage examples, please refer to:

Note

Whether the underlying layer uses FatFS or LittleFS, the application layer uses the Zephyr-wrapped APIs starting with fs_.

File System Configuration

File system configuration can be divided into three types of files:

  • Device Tree: Includes *.dts, *.dtsi, *.yaml, *.overlay files, used to define DTS variables and attribute rules.

  • Kconfig: Used to define configuration items.

  • conf files: Used to define default values for configuration items.

Users can modify the system’s default configuration through .overlay and .conf files.

DTS Configuration

The file system device tree configuration is located in the zephyr/dts/bindings/fs/ directory. The yaml files in this directory can be divided into two categories: one is the common properties shared by all file systems zephyr,fstab-common.yaml, and the other is properties specific to each file system.

  • FatFS properties zephyr,fstab,fatfs.yaml

  • LittleFS properties zephyr,fstab,littlefs.yaml

common properties

Property

Type

Description

mount-point

string

Absolute path of the mount point

automount

boolean

Automatically attempt to mount the partition during file system driver initialization

read-only

boolean

Mount the file system in read-only mode

no-format

boolean

Do not format the mount partition if mounting fails

disk-access

boolean

When this field is not set, flash API is used by default; setting this field indicates using disk access API;

Kconfig Configuration

The file system functionality can only be used after enabling the FILE_SYSTEM configuration item.

Note

The configuration path for file system in menuconfig is:

(Top) → Subsystems and OS Services → File Systems

LittleFS Kconfig configuration items are located in zephyr/subsys/fs/Kconfig.littlefs. Some configuration descriptions:

FILE_SYSTEM_LITTLEFS:

Enable LittleFS file system.

FS_LITTLEFS_FMP_DEV:

Support LittleFS on FLASH. (uses flash_map API)

FS_LITTLEFS_BLK_DEV:

Support LittleFS on block devices (such as SD cards).

FS_LITTLEFS_READ_SIZE:

Minimum size of block reads (bytes). All read operations will be multiples of this value.

FS_LITTLEFS_PROG_SIZE:

Minimum size of block programming (bytes). All programming operations will be multiples of this value.

FS_LITTLEFS_CACHE_SIZE:

LittleFS has a read cache, a program cache, and each file also has a cache.

FS_LITTLEFS_LOOKAHEAD_SIZE:

Size of the lookahead buffer, must be a multiple of 8.

FS_LITTLEFS_BLOCK_CYCLES:

Number of erase cycles before data migration. Recommended value [100,1000]. Set to a non-positive value to disable wear leveling.

FS_LITTLEFS_NUM_FILES:

Maximum number of simultaneously open files. (Adjust according to application requirements)

FS_LITTLEFS_NUM_DIRS:

Maximum number of simultaneously open directories. (Adjust according to application requirements)

FatFS Kconfig configuration items are located in zephyr/subsys/fs/Kconfig.fatfs. Some configuration descriptions:

FAT_FILESYSTEM_ELM:

Enable FatFS file system.

FS_FATFS_EXFAT:

Support FatFS exFAT format. Enabling exFAT automatically enables LFN.

FS_FATFS_READ_ONLY:

Read-only mode.

FS_FATFS_MKFS:

Add code for creating FAT file system disks.

FS_FATFS_MOUNT_MKFS:

Allow fs_mount() to attempt formatting a volume if no file system is found (e.g., newly inserted SD card).

FS_FATFS_MAX_ROOT_ENTRIES:

Maximum number of entries in the FAT file system root directory.

FS_FATFS_HAS_RTC:

Enable file system timestamps instead of using hardcoded dates for all operations.

FS_FATFS_LFN:

Enable Long File Name (LFN) functionality.

FS_FATFS_MAX_LFN:

Define maximum long file name length, range [12, 255].

FS_FATFS_MAX_SS:

Maximum sector size (512, 1024, 2048, or 4096).

FS_FATFS_MIN_SS:

Minimum sector size (512, 1024, 2048, or 4096).

FS_FATFS_NUM_FILES:

Maximum number of simultaneously open files. (Adjust according to application requirements)

FS_FATFS_NUM_DIRS:

Maximum number of simultaneously open directories. (Adjust according to application requirements)

Other Configuration Notes

The storage medium set in LittleFS Kconfig and DTS must be consistent, otherwise automount will fail.

Storage Medium

Kconfig Configuration

DTS Configuration

FLASH

CONFIG_FS_LITTLEFS_FMP_DEV=y

disk_access=false

SD

CONFIG_FS_LITTLEFS_BLK_DEV=y

disk_access=true

LittleFS supports configuration of cache-size, block-cycles, lookahead-size, and disk-version in both Kconfig and DTS. The priority rules are:

  • DTS configuration has higher priority than Kconfig

  • When the value set in DTS is 0, the Kconfig configuration value will be used.

File System Examples

LittleFS on FLASH Example

You can refer to zephyr/samples/subsys/fs/littlefs to see how LittleFS is used.

If LittleFS uses the storage_partition partition already defined in the board-level dts, configure the fstab in the overlay as follows:

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

And ensure that spic is enabled:

&spic {
  status = "okay";
};

If you need to add a new partition for LittleFS, the overlay can be set as follows. Where demo_storage_partition is the newly added partition, its label, address and length must be set according to the actual situation.

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

Enable the following configurations in prj.conf:

CONFIG_FILE_SYSTEM=y
CONFIG_FILE_SYSTEM_LITTLEFS=y

FatFS on SD Example

You can refer to zephyr/samples/subsys/fs/fs_sample to see how FatFS on SD is used.

FatFS accesses SD cards through the SDMMC subsystem. DTS can be configured as follows:

&sdhc0 {
  status = "okay";

  sdmmc {
    compatible = "zephyr,sdmmc-disk";
    status = "okay";
    disk-name = "SD";
  };
};

Enable the following configurations in prj.conf:

CONFIG_FILE_SYSTEM=y
CONFIG_FAT_FILESYSTEM_ELM=y

Settings Introduction

The Zephyr settings subsystem stores persistent device configurations and runtime states. It provides a unified API supporting multiple storage backends, including FCB, NVS, ZMS, and file systems. Settings are stored as key-value pairs with keys following a package/subtree hierarchical naming convention. Refer to Settings for more details.

Note

Regardless of which backend is used, the settings API at the application layer remains unchanged. This allows you to switch backends as needs change without modifying application code.

Application developers should focus on the following sections:

Settings Configuration

Below is a sample settings DTS configuration. You can adjust the SIZE macros at the top to modify the size of the configuration partition.

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

Note

The partition label, start address, and size should be set according to actual requirements.

When using ZMS as the backend, add the following to prj.conf or a board-specific configuration file:

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

If the settings_runtime_xx() API is used, also add the following to prj.conf or a board-specific configuration file:

CONFIG_SETTINGS_RUNTIME=y

Settings Sample

The settings system API is provided by include/zephyr/settings/settings.h.

Usage Examples:

Basic Usage Steps:

  1. Call settings_subsys_init() to initialize the settings subsystem.

  2. Call settings_register() to register the module’s settings handler.

  3. Call settings_load_xx/settings_save_xx to retrieve or store configurations.

Debug Introduction

Offline Debugging

coredump Module

The coredump module is used to dump CPU registers and memory contents for offline debugging. When a fatal error is encountered, this module is called to print or store data according to the enabled backend. This module can also be called on demand when a coredump file needs to be generated.

The coredump-related files are organized as follows:

<zephyr>
├── arch/
│    └── arm/
│       └── core
│           └── cortex_m/
│               └── coredump.c  # Architecture-specific implementation
├── include/
│   └── zephyr/
│       └── debug/              # Header files
├── subsys/
│   └── debug/
│       └── coredump/           # Main content of debug subsystem, coredump api and backend implementation
├── drivers/
│   └── coredump/               # coredump pseudo device
└── scripts/
    └── coredump/               # Scripts for parsing coredump
coredump Configuration

Please use the following options to configure this module:

  • DEBUG_COREDUMP : Enable coredump module.

The following are options to enable coredump output backends:

  • DEBUG_COREDUMP_BACKEND_LOGGING : Use logging module for coredump output.

  • DEBUG_COREDUMP_BACKEND_FLASH_PARTITION : Use flash partition for coredump output.

  • DEBUG_COREDUMP_BACKEND_IN_MEMORY : Use memory partition for coredump output.

  • DEBUG_COREDUMP_BACKEND_OTHER : Use custom mechanism for coredump output.

The following are options regarding memory dump:

  • DEBUG_COREDUMP_MEMORY_DUMP_MIN : Dump only the stack of the exception thread, its thread structure, and some other minimal necessary data to support stack walking in a debugger. Use this option only when dumping the absolute minimum of data is required.

  • DEBUG_COREDUMP_MEMORY_DUMP_THREADS : Export thread structures and stacks for all threads along with all data needed for thread debugging.

  • DEBUG_COREDUMP_MEMORY_DUMP_LINKER_RAM : Dump memory regions between _image_ram_start[] and _image_ram_end[]. This includes at least data, noinit, and BSS sections. This is the default setting.

coredump Code

Typically, when a fatal error is encountered, coredump() is called inside the z_fatal_error() function to generate a coredump file. This function can also be called on-demand when generating a coredump file is required.

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

The function coredump() is located in file zephyr/subsys/debug/coredump/coredump_core.c. Other functions in this file are described below:

API

Description

coredump()

This function is called inside z_fatal_error() to generate a coredump file. Can also be called when generating a coredump file is required.

z_coredump_start()

coredump start.

z_coredump_end()

coredump end.

coredump_buffer_output()

This function outputs byte array buffer to the coredump backend.

coredump_cmd()

Execute command on coredump subsystem.

coredump_query()

Execute query on coredump subsystem.

coredump_memory_dump()

Dump memory region.

arch_coredump_info_dump()

Dump architecture-specific information.

dump_threads_metadata()

Dump thread metadata.

process_coredump_dev_memory()

Dump memory regions collected by coredump pseudo device.

The functions in zephyr/subsys/debug/coredump/coredump_core.c are top-level functions that coordinate the collection of crash information and finally call the selected low-level backend (backend_api) to perform the actual output operation.

Backend selection is determined by the configurations mentioned in coredump Configuration above. The code for selecting backend is as follows:

#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

Each backend needs to define a structure coredump_backend_api, which contains 5 APIs corresponding to the five top-level coredump APIs: coredump start, end, output, query, and command execution.

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

Note

To implement a custom backend, you need to define the structure coredump_backend_api and implement the 5 APIs within it.

Dump Format

The coredump binary file contains a file header, an architecture-specific block, zero or one thread metadata block, and multiple memory blocks.

All backends must print or store coredump data in a fixed format so that the corresponding data blocks can be found during parsing.

All numbers in the file header below are in little-endian byte order.

File Header

Field

Data Type

Description

ID

char[2]

ZE as file identifier.

Header version

uint16_t

Determines the version of header information. This value needs to be incremented whenever the header information structure is modified. Avoids incorrect header parsing.

Target code

uint16_t

Identifies the target (e.g., architecture or SoC) so that the parser can instantiate the correct register block parser.

Pointer size

uint8_t

Size as power of 2 of uintptr_t (e.g., 5 for 32-bit systems, 6 for 64-bit systems).

Flags

uint8_t

Currently unused

Fatal error reason

unsigned int

Same as fatal error reason defined in include/zephyr/fatal_types.h.

Architecture-Specific Block

The architecture-specific block contains a byte stream of data specific to the target architecture (e.g. CPU registers).

It consists of the following fields:

Field

Data Type

Description

ID

char

A indicates this is an architecture-specific module.

Header version

uint16_t

Determines the version of this code block. This version will be parsed by the target architecture-specific code block parser.

Number of bytes

uint16_t

Number of bytes containing the target data byte stream after the header. The format of the byte stream is target-specific and only parsed by the target parser.

Register byte stream

uint8_t[]

Contains target architecture-specific data.

Thread Metadata Block

The thread metadata block contains a byte stream of data needed for thread debugging.

It consists of the following fields:

Field

Data Type

Description

ID

char

T indicates this is a thread metadata block.

Header version

uint16_t

Determines the version of header information. This value needs to be incremented whenever the header information structure is modified. Avoids incorrect header parsing.

Number of bytes

uint16_t

Number of bytes after the header containing the target data byte stream.

Byte stream

uint8_t[]

Contains data needed for thread debugging.

Note

Thread metadata content source: struct z_kernel _kernel.

Memory Block

The memory block contains the start address, end address, and data within the memory region.

It consists of the following fields:

Field

Data Type

Description

ID

char

M indicates this is a memory block.

Header version

uint16_t

Determines the version of header information. This value needs to be incremented whenever the header information structure is modified. Avoids incorrect header parsing.

Start address

uintptr_t

Start address of the memory region.

End address

uintptr_t

End address of the memory region.

Memory byte stream

uint8_t[]

Contains memory content between start address and end address.

Below is an example of configuration using logging backend and the corresponding coredump content printed in the log.

Configuration is as follows:

CONFIG_DEBUG_COREDUMP=y                 # Enable coredump module
CONFIG_DEBUG_COREDUMP_BACKEND_LOGGING=y # Use logging module for coredump output
CONFIG_DEBUG_COREDUMP_MEMORY_DUMP_MIN=y # Dump only the stack of the exception thread, its thread structure, and some other minimal necessary data

Print output is as follows:

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#

When the logging backend prints coredump content, it adds prefix #CD:, start marker BEGIN# and end marker END#, as shown in the print output above, to facilitate extracting coredump data from the log during subsequent parsing. The content between the start and end markers is the coredump data.

  • 5a45 ASCII code letters correspond to ZE, the line starting with 5a45 is the file header;

  • 41 ASCII code letter corresponds to A, the block of data starting with 41 is the architecture-specific block;

  • 4d ASCII code letter corresponds to M, the segment of data starting with 4d is the memory block.

Parsing Steps

After enabling the coredump module, when a fatal error occurs, CPU registers and memory contents are printed or stored according to the enabled backend. This coredump data can be provided as a remote target to a custom GDB server. The parsing process includes the following steps:

  1. Obtain coredump information from the device based on the enabled backend.

  2. Convert the coredump information to a binary format that the GDB server can parse.

  3. Use a script to start a custom GDB server, passing the coredump binary file and Zephyr ELF file as arguments.

  4. Start a debugger corresponding to the target architecture.

The diagram is as follows:

../../_images/zephyr_debug_gdbserver.svg

Note

For usage examples, see Offline Debugging.

Example of viewing results with gdb commands:

../../_images/zephyr_debug_coredump_parse.png

Coredump Pseudo Device

The coredump device is a pseudo device driver that provides configuration and function interfaces, allowing users to customize memory regions to be saved during coredump.

Enable the Coredump pseudo device by turning on macro CONFIG_COREDUMP_DEVICE. There are two types of Coredump pseudo devices:

  • COREDUMP_TYPE_MEMCPY : During coredump, dump memory entries defined in device tree and memory entries registered at runtime.

  • COREDUMP_TYPE_CALLBACK : During coredump, execute registered callback functions and save an array region coredump_bytes[] .

The two types are defined in code as follows:

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

Device required configuration information:

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

Device required configuration information corresponding to DTS configuration:

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

Note

  • COREDUMP_TYPE_MEMCPY type device does not require memory-regions property in dts; memory regions to be saved for this type can be added using API in code;

  • COREDUMP_TYPE_CALLBACK type device must have memory-regions property, where the first value 0x0 is the memory start address but does not take effect; during actual use, the memory start address is obtained from &coredump_bytes[0]; the second value 0x4 is the size of memory to save, which will create coredump_bytes[] based on this value. The actual memory region saved during coredump is this coredump_bytes[].

Information needed during runtime:

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

Information needed during runtime can be registered using the following device APIs:

static DEVICE_API(coredump, coredump_api) = {
    .dump = coredump_impl_dump,                           // Called during a coredump to dump memory from the coredump pseudo device
    .register_memory = coredump_impl_register_memory,     // Register a memory region for a COREDUMP_TYPE_MEMCPY device
    .unregister_memory = coredump_impl_unregister_memory, // Unregister a memory region for a COREDUMP_TYPE_MEMCPY device
    .register_callback = coredump_impl_register_callback, // Register a callback function for a COREDUMP_TYPE_CALLBACK device
};

In the coredump() main flow, process_memory_region_list() is called to handle memory region dumps. Pseudo device dumps are also called in this function. The relevant implementation is as follows:

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

Note

Even if the DEBUG_COREDUMP_MEMORY_DUMP_MIN configuration is selected, additional memory information can be included in the dump file through one or more coredump devices.

Online Debugging

Online debugging refers to real-time control and observation of program execution through debugging interfaces in the target environment where the code actually runs, including setting breakpoints, single-stepping, viewing/modifying memory and registers, monitoring thread and peripheral status, etc.

Zephyr’s meta-tool west provides some debug-related commands that support connecting a debugger to a development board from a Zephyr build directory and opening a debug console (e.g., GDB session).

west debug Usage

You can use west debug -h to view help information, which includes explanations for each parameter.

zephyr/scripts/west_commands/debug.py implements three subclasses for debug functionality:

  • debug : Connect to the development board via debug protocol, program the flash, then direct the user to the debugger interface with symbol table loaded from the current binary, and block until the debugger exits. (Equivalent to starting gdbserver+gdb)

  • debugserver : Connect via board-specific debug protocol, then reset and halt the target. Ensures the user can now connect to the debug server with symbol table loaded from the binary. (Equivalent to starting gdbserver)

  • attach : Connect to the development board via debug protocol, then direct the user to the debugger interface with symbol table loaded from the current binary, and block until exit. Unlike ‘debug’ command, this command does not program the flash. (Equivalent to starting gdb)

west debug command usage examples:

west debug            // If the default debug runner is J-Link, this starts JLinkGDBServer and the GDB debugger.
west debug -i ip:port // Start JLinkGDBServer and connect it to a remote JLinkRemoteServer at ip:port.

When using GDB for online debugging, the development board must be connected to the host computer via a J-Link probe. The following diagram shows three typical scenarios based on different connection methods and tools used. For detailed debugging methods and commands, see Online Debugging.

  1. Code on Windows computer, development board connected to Windows computer via jlink, debug using west debug.

    ../../_images/zephyr_debug_online_win.svg
  2. Code on Linux server, development board connected to Windows computer via jlink, debug using gdb command line tool.

    ../../_images/zephyr_debug_online_win_linux.svg
  3. Code on Linux server, development board connected to Windows computer via jlink, debug using west debug command line.

    ../../_images/zephyr_debug_online_linux.svg

Note

The west debug command starts JLinkGDBServer and arm-none-eabi-gdb, so using the west debug command requires both to be present on the host.

MCUboot Introduction

MCUboot Introduction

MCUboot is a secure bootloader for 32-bit microcontrollers that supports application image verification, secure boot, and firmware upgrades.

MCUboot is independent of any specific operating system or hardware platform; it accesses low-level functions such as Flash and cryptography through the hardware abstraction layer provided by the platform. MCUboot can be easily integrated into Zephyr and supports RSIP image encryption.

Boot Process

The system uses two-stage boot:

  1. The boot code in the chip ROM loads and starts MCUboot.

  2. MCUboot verifies the application image and starts the application after verification passes.

The boot flow is shown in the following figure:

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

The figure above focuses on describing the main process from the Zephyr/MCUboot project perspective. For detailed functional information, please refer to FreeRTOS Boot Process .

Flash Layout

In the common single-image dual-slot upgrade scheme, Flash typically contains three partitions:

  • boot_partition: holds MCUboot;

  • slot0_partition: primary image slot, holds the currently running application image;

  • slot1_partition: secondary image slot, holds the pending upgrade image.

MCUboot can swap images between the primary and secondary slots via Swap_using_move or Swap_using_offset mode.

To adjust partition sizes or addresses, modify only the macro definitions at the top of the board configuration file; all other values are computed automatically:

  • FLASH_BASE_PHY: flash physical base address

  • XXX_LOGIC_ADDR: logical address of app firmware

  • BOOT_SLOT_BASE / BOOT_SLOT_SIZE: bootloader partition start offset and size

  • APP_SLOT_SIZE: size of a single slot; primary and secondary slots share the same size

RTL8721Dx:

File path: 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 */

Resulting DTS partitions:

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

Note

After modifying slot-related parameters, you should also update the values after each partition@ in spic/flash0/partitions to match their reg[0] values to avoid compilation warnings.

MCUboot Bootloader Firmware

The MCUboot bootloader firmware format and code execution address must meet the requirements of its upper-level bootloader ROM. After compilation, the bootloader firmware is therefore further processed into three parts concatenated together, each prefixed with a 32-byte header:

  • manifest: signing and verification information used by the ROM to validate the firmware

  • xip_boot.bin: the MCUboot code

  • ram_1.bin: the SRAM entry table (start table + valid pattern), which the ROM reads to obtain the bootloader entry point

The ROM uses the load address in each header to decide whether that part stays in flash for XIP execution or is copied into SRAM first. Where the entry table is placed therefore differs per chip:

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

xip_boot.bin executes in XIP mode directly from flash, while the entry table must run from SRAM. The two therefore have different load addresses and are placed in separate parts: the ROM copies ram_1.bin into SRAM, then jumps through the entry table into the XIP code.

MCUboot App Firmware

Before booting an app, MCUboot verifies firmware integrity and signature. The app firmware must conform to MCUboot’s defined format, including: Firmware Header, Firmware Body, TLV Area (Optional) and Firmware Trailer. The schematic diagram is shown below:

../../_images/zephyr_ota_image_formate.svg

Note

The diagram shows the format requirements for application firmware, not the firmware format of MCUboot itself (see MCUboot Bootloader Firmware for the bootloader firmware format).

Firmware Header

The Zephyr build system reserves 0x200 bytes at the start of the firmware for the header (ih_hdr_size = 0x200).

#define IMAGE_MAGIC                 0x96f3b83d
#define IMAGE_HEADER_SIZE           32

STRUCT_PACKED image_version {
   uint8_t iv_major;
   uint8_t iv_minor;
   uint16_t iv_revision;
   uint32_t iv_build_num;
};

/** Image header.  All fields are in little endian byte order. */
STRUCT_PACKED image_header {
   uint32_t ih_magic;
   uint32_t ih_load_addr;
   uint16_t ih_hdr_size;               /* Size of image header (bytes). */
   uint16_t ih_protect_tlv_size;       /* Size of protected TLV area (bytes). */
   uint32_t ih_img_size;               /* Does not include header. */
   uint32_t ih_flags;                  /* IMAGE_F_[...]. */
   struct image_version ih_ver;
   uint32_t _pad1;
};

TLV Area (Type-Length-Value)

The TLV area is located after the firmware body and is optional, typically storing fields such as hash, signature, and security counter.

When a protected TLV area exists, the IMAGE_TLV_PROT_INFO_MAGIC information header must be present, and this area is included in the hash calculation. If not present, the hash only covers “firmware header + firmware body”.

To ensure rollback protection is effective, IMAGE_TLV_SEC_CNT, 0x50 needs to be written to the protected TLV area. Using imgtool’s --security-counter option will place it in the protected TLV area.

TLV Format
#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). */
};
Common TLV Types Related to OTA
#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 */

Firmware Trailer

Used to record swap/status metadata at the end of the slot; this space cannot be used to store firmware body.

The firmware Trailer structure is as follows (in bytes):

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)                       |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Parameter Description

BOOT_MAX_IMG_SECTORS:

Default value is the number of sectors occupied by slot0_partition.

min-write-size:

Flash minimum write granularity.

s:

s = 3 (Swap_using_move), s= 2 (Swap_using_offset)

Swap status:

Per-sector Swap Status record for recovery after power loss.

Encryption keys:

(Optional) Encryption keys, only present when encryption is enabled.

Swap size:

Total amount of data to be moved in this swap (covering firmware body + TLV area).

Swap info:

1 byte valid, BOOT_MAX_ALIGN aligned; lower 4 bits are Swap Type, upper 4 bits are firmware number.

Copy done:

1 byte valid, BOOT_MAX_ALIGN aligned; indicates copy phase completion status: 0x01=Set, 0xFF=Unset.

Image OK:

1 byte valid, BOOT_MAX_ALIGN aligned; indicates whether new firmware has been confirmed, unconfirmed will revert on next boot. 0x01=Set, 0xFF=Unset.

MAGIC:

16 bytes, marker for trailer area validity.

  • When BOOT_MAX_ALIGN=8, it is a fixed 16-byte pattern:

    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
       }
    };
    
  • Otherwise, the first 2 bytes are the alignment value, and the last 14 bytes are a fixed pattern:

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

Note

This area is located at the end of the application image slot (slot0_partition / slot1_partition), not immediately following the firmware body. Therefore, the slot size must reserve space for the Trailer and be aligned to a Flash sector boundary.

Twister Introduction

Twister Introduction

Twister is Zephyr’s built-in testing framework, used to automatically build and run test cases in batches, verifying the correctness and stability of Zephyr across different hardware platforms and configurations.

Twister identifies test suites by scanning the YAML configuration files (testcase.yaml, sample.yaml) in the source repository, and then handles building, flashing, on-device execution, and result evaluation, finally producing several test reports such as twister.json.

Zephyr provides detailed Twister documentation.

Twister Environment Setup

Before running Twister, you need to set up the Zephyr command-line development environment (toolchain, Python environment, and environment variables). For the complete steps on Ubuntu and Windows, refer to Build and Flashing.

This document recommends running Twister via nuwa.py. The first time you run against a platform (e.g. -p rtl8721f_evb), the toolchain is automatically downloaded to the rtk-toolchain directory, and the ZEPHYR_TOOLCHAIN_VARIANT and GNUARMEMB_TOOLCHAIN_PATH environment variables are configured automatically, with no manual setup required.

Once the environment is ready, run python nuwa.py twister --help from the nuwa workspace root. Seeing the Twister help message means the environment is set up correctly.

Twister Usage

Twister tool supports setting parameters via command line, common parameters grouped by purpose are as follows.

Test selection

  • -p PLATFORM, --platform PLATFORM: Specify the test platform; can be used multiple times to specify multiple platforms.

  • -T TESTSUITE_ROOT, --testsuite-root TESTSUITE_ROOT: Root directory to recursively search for test suites; can be used multiple times to specify multiple directories. If not specified, the defaults are the samples/ and tests/ directories under the zephyr root directory.

  • -s TEST, --test TEST, --scenario TEST: Specify the test scenario; can be used multiple times to specify multiple scenarios.

  • -t TAG, --tag TAG: Filter which tests to run by tag; can be used multiple times to specify multiple tags. If not specified, no tag filtering is performed.

  • --test-config TEST_CONFIG: Specify the file path containing the test plan and configuration.

Test device

  • --device-testing: Run tests on device.

  • --port PORT: A Realtek Ameba custom option that specifies the board’s serial port (e.g., /dev/ttyUSB0 on Linux, COM12 on Windows). Once specified, Twister automatically configures the serial port, baud rate, flash runner, etc., so you do not need to set the low-level parameters below (such as --device-serial) manually.

  • --remote-server REMOTE_SERVER: A Realtek Ameba custom option that specifies the IP of the remote serial server, used together with --port when the board is attached to another PC running the AmebaRemoteService software for remote testing.

  • --device-serial DEVICE_SERIAL: Serial port device used to access the development board (e.g., COM12 on Windows, /dev/ttyUSB0 on Linux).

  • --device-serial-baud DEVICE_SERIAL_BAUD: Set serial port device baud rate; defaults to 115200 if not specified.

  • --west-flash [WEST_FLASH]: Additional flags passed to west flash (comma separated).

  • --flash-before: Flash first, then connect to serial port.

Build and retry

  • --short-build-path: Create shorter build directory paths. Recommended to enable this option on Windows.

  • --retry-build-errors: Retry build errors.

  • --retry-failed RETRY_FAILED: Retry failed tests, up to the specified number of times.

Twister Usage Examples

All commands below are run from the nuwa workspace root. Replace the serial ports (--west-flash and --device-serial) and the platform (--platform) in the commands according to your actual situation.

Run all test scenarios in a directory (specified via -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/

The above command will run the 8 test scenarios defined in zephyr/tests/kernel/threads/dynamic_thread_stack/testcase.yaml.

Run only a single test scenario (specified via --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 provides two levels of batch tests, smoke and acceptance, in zephyr/tests/test_config.yaml. The smoke test set is smaller and is usually used as the first step of continuous integration. If the smoke test fails, it usually means there is a serious defect, and you can immediately abort subsequent more time-consuming tests. The acceptance test set includes smoke and is more comprehensive but more time-consuming than smoke.

Run the smoke batch 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 --test-config=zephyr/tests/test_config.yaml --level=smoke \
  --retry-failed 3 --retry-build-errors

Run the acceptance batch 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 --test-config=zephyr/tests/test_config.yaml --level=acceptance \
  --retry-failed 3 --retry-build-errors

Custom Batch Test Suite

If you want to execute a batch of specified test suites via Twister, you can refer to the format of zephyr/tests/test_config.yaml to customize a test configuration file, and then specify the file and level via --test-config and --level parameters like the smoke and acceptance batch tests above. In the test configuration file, you can define test levels (name under levels), level dependencies (inherits), and use regular expressions in adds to specify test suites.

The following example defines two test levels, smoke and acceptance, where acceptance inherits from 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 Configuration Files

Board Configuration File

The Twister board configuration file describes the hardware characteristics and configuration information of the development board, using YAML format, located in each board directory, usually named <board>.yaml. For example, the Twister board configuration file for rtl8721f_evb is zephyr/boards/realtek/rtl8721f_evb/rtl8721f_evb.yaml.

For the meaning of each field in the Twister board configuration file, refer to the official documentation Board Configuration .

testcase.yaml

testcase.yaml is the test definition file read by Twister. It lives in each test directory under tests/ and declares the test scenarios, run conditions, and pass/fail rules for that directory’s test code.

It can be used to:

  • Precisely control under what conditions, on which boards, and with what configuration a test runs, via fields such as filter, platform_allow, and platform_exclude.

  • Avoid flashing memory-heavy tests to resource-constrained boards, via fields such as min_ram.

  • Define how to determine test pass/fail from serial output, via harness and harness_config, enabling automated verification.

Common fields (see the official Test Scenario Identifier documentation for the full list):

  • tags: a set of string tags; works with the --tag and --exclude-tag command-line options to filter tests.

  • depends_on: the features a test depends on; each must be declared in the supported list of the board configuration file (see Board Configuration File), otherwise the test is filtered out.

  • platform_exclude: boards to exclude from the run. For example:

    platform_exclude:
        - rtl8721f_evb
        - rtl872xda_evb
    

sample.yaml

sample.yaml is used to describe a demo program located in the samples/ directory to Twister. The meaning of each configuration item in this file can be referred to testcase.yaml.

Twister Reports

When executing Twister, a twister-out directory is generated in the current directory by default to store binary files, logs, reports, etc.

  • testplan.json: Test plan, can be used to understand which test suites will be executed.

  • twister.json: Execution results of test suites.

  • twister.log: Execution process log of the Twister tool itself. Records compilation, flashing, execution steps and errors. Can be used to diagnose Twister execution or environment issues.

  • twister.xml, twister_report.xml, twister_suite_report.xml: Other forms of reports.

Below is an example of a successful test suite result in 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"
        }
    ]
}

Below is an example of a failed test suite result in twister.json, which records the reason for failure:

{
    "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",
    ......
}

For the meaning of test suite and test case statuses, see the official documentation Twister status.

Twister Test Failure Diagnosis

If a test suite fails, you can try the following methods to diagnose the problem:

  • Search for the test suite name in the twister.json report to confirm its execution status and failure reason, and get preliminary error information.

  • Go to the test suite’s output directory in twister-out and check the following log files:

    • build.log: Diagnose compilation errors

    • device.log and handler.log: Diagnose runtime issues, such as flashing errors, timeouts.

  • Re-run the failed scenario on its own by following the single-scenario example above, keeping the other options unchanged and replacing --test with the name of the failed scenario.

If a large number of test suites show Timeout errors during batch testing, a common reason is that a certain test suite failed to correctly respond to the device flashing command reboot uartburn after running, causing subsequent tests to fail to execute normally. You can follow these steps to identify which specific test suite caused the problem:

  1. Connect the tracetool log tool, then restart the development board.

  2. From the boot log above, find the value of the test suite’s RunID, for example:

    ......
    Unexpected fault during test
    ===================================================================
    RunID: 6ce8788b17745a48c339e75dfdf479b3
    PROJECT EXECUTION FAILED
    
  3. In the twister.json report, search for the test record that matches the above RunID value. The test suite corresponding to that record may be the root cause of the subsequent series of timeout issues.

CANbus Subsystem Introduction

Sample Project Introduction

Zephyr implements an ISO-TP protocol CAN bus subsystem sample project in the zephyr/samples/subsys/canbus/isotp directory.

This sample demonstrates how to use the ISO-TP library to exchange messages between two development boards:

  • A long message, sent using a block size (BS) of 8 frames.

  • A short message, with a minimum separation time (STmin) of 5 milliseconds.

The short message send function call is non-blocking; the long message send function call is blocking.

Note

The default configuration of the rtl8721f_evb development board has loopback mode enabled, so no additional wiring is required. A single development board can run the sample project normally.

Build Command

Use the following command to build the isotp sample project:

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

The default configuration can be built and run normally. For modifying configuration details, see isotp Sample Project Configuration.

Sample Output

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]

Note

The values in the above output may vary.

ISO-TP Protocol Introduction

ISO-TP (ISO 15765-2) is a “segmentation and reassembly” upper-layer protocol built on classic frames. It does not change the physical/link layer structure of classic frames, but instead places its own “protocol header/type field + data” into the Data Field of classic frames, and defines its own frame types (SF/FF/CF/FC) and protocol content within. It can be further divided into the following four frame types:

  1. Single Frame (SF)

  2. First Frame (FF)

  3. Consecutive Frame (CF)

  4. Flow Control Frame (FC)

Single Frame (SF)

If the data payload is 7 bytes or less, a single frame is used for data transmission in the ISO-TP protocol. The first byte of the data field is used for PCI (Protocol Control Information). This byte is further divided into two parts: the upper 4 bits of the first byte indicate the frame type, and the lower 4 bits indicate the DL (data length).

../../_images/zephyr_canbus_subsys_isotp_frame_SF.svg

Key values and encoding description:

  • DL (Data Length): Data length of this frame (1..7)

First Frame (FF)

The first frame is the initial message of a multi-frame message packet in the ISO-TP protocol. It is used when segmented data exceeding 7 bytes must be transmitted. The first frame contains the length of the complete data packet as well as the initial data. In FF, the first 2 bytes are used for PCI, where the upper 4 bits of the first byte are used for frame type, and the lower 4 bits together with the next 1 byte (total 8+4 = 12 bits) of the data field are used for DL (2^12 = 4095 data bytes). Therefore, in FF, only 6 bytes of data can be transmitted initially. This frame is responsible for sending information to the receiver about how many total data bytes will be sent.

../../_images/zephyr_canbus_subsys_isotp_frame_FF.svg

Key values and encoding description:

  • DL (Data Length): Total length of complete data (12 bits), including all subsequent CF data

Consecutive Frame (CF)

The main task of the transport protocol or ISO-TP protocol is to transmit such messages (which cannot be transmitted as a single protocol data unit (PDU) due to length) that are segmented into multiple separate PDU messages.

../../_images/zephyr_canbus_subsys_isotp_frame_CF.svg

Key values and encoding description:

  • Sequence Number SN (lower 4 bits of CF): Values 0..15, sender increments SN = (SN+1) mod 16 after each CF; per specification, the first CF after FF has SN=1

Flow Control Frame (FC)

The flow control mechanism of the ISO-TP protocol is used to configure the sender to match the receiver’s attributes (timing, available receive buffers, receive readiness). Flow Control (FC) is always sent by the receiver to determine how the sender transmits data based on the receiver’s attributes.

../../_images/zephyr_canbus_subsys_isotp_frame_FC.svg

Key values and encoding description:

  • FlowStatus (FS, lower 4 bits of FC)

    • 0 = CTS (Clear To Send): Continue sending, with BS and STmin parameters

    • 1 = WAIT: Please wait, sender increments WFT count, abort if exceeds WFTmax

    • 2 = OVFLW: Receiver buffer insufficient, immediately abort this transmission

  • Block Size (BS)

    • 0: No block flow control (continuous CF from FF until completion, no intermediate FC needed)

    • 1..255: After sending BS number of CFs, sender must wait for new FC

  • STmin (FC byte 2)

    • 0x00..0x7F: Unit is milliseconds (ms)

    • 0xF1..0xF9: Unit is 100 microseconds (100 µs steps)

    • Other values reserved; if the implementation does not support microsecond encoding, typically treated as 0 (refer to specific stack implementation)

Typical Examples

(Classic frames, standard addressing, hexadecimal)

  • SF (3-byte PDU, data AA BB CC)

    03 AA BB CC 00 00 00 00
    
  • FF (total length 0x0014=20, first frame data 12 34 56 78 9A BC)

    10 14 12 34 56 78 9A BC
    
  • CF (SN=1, data 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
    

CAN Bus Subsystem Introduction

The directories and files related to the CAN bus subsystem in Zephyr are shown below:

nuwa $
   ├── modules
   │   ├── hal/realtek/ameba/amebag2/source/fwlib # hal
   │   └── lib/canopennode/                           # canopennode third-party library
   └── zephyr
       ├── include/zephyr/drivers/can.h               #macros, structs, _syscall api (calls driver)
       ├── drivers
       │   ├── can
       │   │   ├── can_ameba.c                        #driver_api
       │   │   ├── can_common.c                       #common api
       │   │   └── can_shell.c                        #shell command implementation
       │   └── net/canbus.c                           #canbus_api (calls _syscall api)
       ├── dts
       │   ├── arm/realtek/amebag2/amebag2.dtsi       #driver node
       │   └── bindings/can/realtek,ameba-can.yaml    #yaml
       ├── modules
       │   └── canopennode/                           #canopen protocol implementation and wrapper (calls _syscall api)
       ├── subsys
       │   ├── canbus/isotp/                          #implements ISO-TP session layer, including segmentation, flow control, etc. (calls _syscall api)
       │   └── net
       │         ├── ip/canbus_socket.c              #mapping and calling from "socket" to "L2 send entry" (bridges upper and lower layers)
       │         ├── l2/canbus/canbus_raw.c          #L2 link layer (calls canbus_api)
       │         └── lib/socket_can.c                #AF_can socket layer (calls IP layer interface)
       ├── samples
       │   ├── drivers/can/
       │   ├── modules/canopennode/                   #canopennode sample project
       │   └── subsys
       │       ├── canbus/isotp/                      #isotp send sample project
       │       └── net/socket/can/                    #socket sample project
       └── tests
           ├── drivers/can/
           └── subsys/canbus/

isotp Sample Project Configuration

Configuration can be divided into two types of files:

  • DTS: Includes *.dts, *.dtsi, *.yaml, *.overlay file types, used to define DTS variables and attribute rules.

  • Kconfig: *.conf file types are used to define default values for configuration items.

Users can modify the system’s default configuration by editing the rtl8721f_evb.overlay and rtl8721f_evb.conf files in the zephyr/samples/subsys/canbus/isotp/boards directory.

DTS Configuration

Device nodes are defined in 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";
        };
  }
}

Pins used by the device are defined in zephyr/boards/realtek/rtl8721f_evb/rtl8721f_evb-pinctrl.dtsi:

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

The device used by Zephyr’s isotp sample project is specified by the zephyr,canbus property in the chosen node, so you need to select a device in zephyr/samples/subsys/canbus/isotp/boards/rtl8721f_evb.overlay

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

&can0 {
    pinctrl-0 = <&can0_default>;
    pinctrl-names = "default";
    status = "okay";
};

Kconfig Configuration

Basic configurations are set in zephyr/samples/subsys/canbus/isotp/prj.conf. Refer to the configuration item descriptions below for the meaning of each configuration:

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

When building the image for the rtl8721f_evb development board, you can enable loopback mode in zephyr/samples/subsys/canbus/isotp/boards/rtl8721f_evb.conf. After enabling loopback mode, no additional wiring is required, and a single development board can run the sample project normally:

CONFIG_SAMPLE_LOOPBACK_MODE=y

The isotp Kconfig configuration items are located in zephyr/subsys/canbus/isotp/Kconfig. Below are descriptions of some configurations:

ISOTP_WFTMAX:

WFTmax, the maximum number of WAIT occurrences the sender can accept when the receiver returns FC=WAIT; transmission is aborted if exceeded.

ISOTP_BS_TIMEOUT:

Timeout for the sender while waiting for the next flow control frame (FC). The timer is started after sending an FF or after sending a complete block of CFs.

ISOTP_A_TIMEOUT:

As/Ar send/receive timeout (internal protocol stack protection timer for send operations and receive responses).

ISOTP_CR_TIMEOUT:

Timeout for the receiver while waiting for the next consecutive frame (CF).

ISOTP_REQUIRE_RX_PADDING:

Requires received SF, FC, and the last CF to be padded to full DLC; unused bytes must be padded.

ISOTP_ENABLE_TX_PADDING:

Sender pads frames as needed, using 0xCC as the padding value (Bosch recommendation).

ISOTP_RX_BUF_COUNT:

Number of receive data buffer blocks (blocks in net_buf pool), used to hold data during reassembly.

ISOTP_RX_BUF_SIZE:

Size of each receive data block (bytes).

ISOTP_RX_SF_FF_BUF_COUNT:

Number of pre-allocated buffers dedicated to receiving single frames (SF) and first frames (FF).

ISOTP_USE_TX_BUF:

When sending, the application data is copied to an internal net_buf; the caller can immediately release the original data.

ISOTP_TX_BUF_COUNT:

Number of send data buffer blocks (for ISOTP_USE_TX_BUF mode).

ISOTP_BUF_TX_DATA_POOL_SIZE:

Total size of the memory pool where send buffers are allocated (bytes).

ISOTP_ENABLE_CONTEXT_BUFFERS:

Enables “context buffers” (memory slab) to store send context, making “send-and-forget” possible.

ISOTP_TX_CONTEXT_BUF_COUNT:

Maximum number of concurrent/queued send contexts.

ISOTP_CUSTOM_FIXED_ADDR:

Enables custom “Fixed Addressing” ID encoding.

When the ISOTP_CUSTOM_FIXED_ADDR configuration is enabled, the following configurations also need to be set:

ISOTP_FIXED_ADDR_SA_POS:

Bit start position of source address (SA) in the identifier.

ISOTP_FIXED_ADDR_SA_MASK:

Mask for extracting source address.

ISOTP_FIXED_ADDR_TA_POS:

Bit start position of target address (TA).

ISOTP_FIXED_ADDR_TA_MASK:

Mask for extracting target address.

ISOTP_FIXED_ADDR_PRIO_POS:

Bit start position of priority field.

ISOTP_FIXED_ADDR_PRIO_MASK:

Mask for extracting priority field.

ISOTP_FIXED_ADDR_RX_MASK:

Mask for receive filtering, typically set to loose filtering that matches any priority and source address.