zephyrproject-rtos / zephyr

Primary Git Repository for the Zephyr Project. Zephyr is a new generation, scalable, optimized, secure RTOS for multiple hardware architectures.
https://docs.zephyrproject.org
Apache License 2.0
10.99k stars 6.69k forks source link

Generate interrupt handlers and vector tables from devicetree #67583

Open bjarki-andreasen opened 10 months ago

bjarki-andreasen commented 10 months ago

Introduction

The entire interrupt layout, including interrupt controllers, their interrupt lines, which lines are shared, and which lines are used, can be determined from the devicetree alone. Using the ordinals of interrupt controllers, interrupt generating devices, and the interrupt line numbers, we can synthesize symbols for every IRQ, including the handler and data passed to the handler, similar to encoding struct device with __device_dts_ord_<ord>.

This RFC details exactly how we can create a completely link optimizable interrupt controller subsystem, which can be entirely stored in ROM, while natively supporting shared interrupts and interrupt controller cascading (which may also be described as interrupt levels/aggregators in the current schema)

Note

The proposed solution has recently become possible after #66707 which allows us to get the interrupt controller of an IRQ from the devicetree, allowing us to bind an instance of an interrupt with an instance of a specific interrupt controller.

Problem description

Consider the following devicetree snippet:

/ {
    /* ordinal 0 */
    soc {
        /* ordinal 1 */
        intc: foo_intc {
            interrupt-controller;
            interrupt-cells = <2>;
            /* property used to generate appropriate number of interrupt handlers */
            interrupt-lines = <65>;
        };

        /* ordinal 2 */
        timer0: foo_timer0 {
            interrupts <0 0>;
            status = "enabled";
        };

        /* ordinal 3 */
        timer1: foo_timer1 {
            interrupts <0 0>;
            status = "enabled";
        };

        /* ordinal 4 */
        timer2: foo_timer2 {
            interrupts <1 0>;
            status = "disabled";
        };

        /* ordinal 5 */
        gpio0: foo_gpio {
            /* GPIO is also a general interrupt controller */
            gpio-controller;
            gpio-cells = <2>;
            interrupt-controller;
            interrupt-cells = <2>;
            interrupt-lines = <32>;

            /* Its interrupt parent is the <&intc> */
            interrupts <3 0>;
            status = "enabled";
        };

        /* ordinal 6 */
        i2c0: foo_i2c0 {
            interrupts <4 0>
                   <5 0>;
            status = "enabled";

                /* ordinal 7 */
                sensor0: foo_sensor {
                    interrupt-parent = <&gpio0>;
                    interrupts = <2 3>;
                    status = "enabled";
                };
        };
    };
};

From this we can determine the following:

Solution

The solution is split into steps which follow:

Step 1: Deduce information from devicetree

We require the following for information for each IRQ entry

and the number of interrupt lines for each interrupt controller.

We can get the first 4 properties directly from the devicetree, however, the application must define the IRQ handler and IRQ data. Just like struct devices, we can declare both the IRQ handler and IRQ data which shall be stored in the IRQ entry as externs, and allow the application to define them. This simply requires a linkable symbol, which we can deduce from the devicetree.

We will use the following properties to synthesize the linkable symbols:

We will need to add an additional devicetree property to interrupt-controllers to indicate the number of interrupt lines connected to generate the appropriate amound of IRQ handlers for each of them.

Step 2: Define all interrupt specifications

All interrupts in the system will be stored in a single const array. The mapping between an interrupt and its index in the array will be provided by definitions, synthesized from the interrupt's controller and line number.

#define SYS_DT_IRQN_<intc_ord>_<intc_irq> <index into array>

The array itself contains a list of struct sys_irq_spec interrupt request specifications

struct sys_irq_spec {
        const struct device *intc;
        uint16_t irq;
};

const struct sys_irq_spec __sys_irq_specs[] = {
        {
                /* timer0 and timer1 */
                .intc = &__device_ord_1,
                .irq = 0,
        },
        {
                /* gpio0 */
                .intc = &__device_ord_1,
                .irq = 3,
        },
        ...
};

For example, enabling interrupt 0 of intc0 (timer0 and timer1)

sys_irq_enable(SYS_DT_IRQN_1_0);

Naturally, SYS_DT_IRQN_1_0 will not be used directly, but generated from a macro like SYS_DT_INST_IRQN(inst)

Step 3: Define IRQ handlers and data

IRQ handlers will be generated as static inline functions, which are called from the interrupt controllers. These are "normal" functions generated by the kernel, which must be called from a "safe" context (like _isr_wrapper or similar).

The IRQ handler's name is synthesized like this:

extern void __sys_irq_handler_<intc_ord>_<intc_irq>_<igd_ord>(void);

Note that this contains the devicetree ordinal of the interrupt generating device, this is to avoid naming conflicts in the case of shared interrupts. The interrupt handler (1 handler per interrupt line) called by the interrupt controller is synthesized like this:

extern void __sys_interrupt_handler_<intc_ord>_<intc_irq>(void);

This handler will call every individual IRQ connected the the interrupt line, and optionally any dynamically added IRQ (see last step).

The following is a snippet of the generated handlers and externs.

/* timer0 */
extern void __sys_irq_handler_1_0_2(void);

/* timer1 */
extern void __sys_irq_handler_1_0_3(void);

/* gpio0 */
extern void __sys_irq_handler_1_3_5(void);

/* timer0 and timer1 (shared interrupt) */
static inline void __sys_interrupt_handler_1_0(void)
{
        __sys_irq_handler_1_0_2();
        __sys_irq_handler_1_0_3();
}

/* No IRQ */
static inline void __sys_irq_handler_1_1(void)
{
        z_fatal_error(K_ERR_SPURIOUS_IRQ, NULL);
}

/* No IRQ */
static inline void __sys_interrupt_handler_1_2(void)
{
        z_fatal_error(K_ERR_SPURIOUS_IRQ, NULL);
}

/* gpio0 */
static inline void __sys_interrupt_handler_1_3(void)
{
        __sys_irq_handler_1_3_5();
}

The data and IRQ handler is defined by the application, just like a struct device, using a macro like INTC_DT_INST_IRQ_HANDLER_DEFINE(0, handler, data) which is used like:

static void my_handler(const void *data)
{
}

SYS_DT_INST_IRQ_HANDLER_DEFINE(0, DEVICE_DT_INST_GET(0), my_handler);

where SYS_DT_INST_IRQ_HANDLER_DEFINE for timer0 would expand to:

void __sys_irq_handler_1_0_2(void)
{
        my_handler(DEVICE_DT_INST_GET(0));
}

Notice that we don't need to store the data passed to the handler outside of the macro itself.

Step 4: Define IRQ vector tables

Defining IRQ vector tables will be "offloaded" to the interrupt controller drivers themselves.

A general interrupt controller would create a static const "software" table and populate it with pointers to the __sys_interrupt_handler_*_* functions like this:

/* example general interrupt controller with 4 interrupts and dts ord 4 */
typedef void (*intc_vector)(void);
static const intc_vector handler_table[4] = {
        __sys_interrupt_handler_4_0,
        __sys_interrupt_handler_4_1,
        __sys_interrupt_handler_4_2,
        __sys_interrupt_handler_4_3,
};

A vectored interrupt controller would populate the table with wrappers, containing pre/post work, calling the __sys_interrupt_handler_*_* function in between

/* example vectored interrupt controller with 4 interrupts and dts ord 4 */
__weak void __intc_irq_vector_4_0(void)
{
        intc_foo_header();
        __sys_interrupt_handler_4_0();
        intc_foo_pm();
        intc_foo_footer();
        intc_foo_swap();
}

__weak void __intc_irq_vector_4_1(void)
{
        intc_foo_header();
        __sys_interrupt_handler_4_1();
        intc_foo_pm();
        intc_foo_footer();
        intc_foo_swap();
}

...

typedef void (*intc_vector)(void);
static const intc_vector vector_table[4] = {
        __intc_irq_vector_4_0,
        __intc_irq_vector_4_1,
        __intc_irq_vector_4_2,
        __intc_irq_vector_4_3,
};

This allows the linker and compiler to fully optimize the interrupts from vector to handler.

Step 5: Defining direct IRQ handlers (vectors)

The names of the vectors in the previous step are not random :) For vectored interrupt controllers, their vectors are weakly defined, and named like this:

__intc_irq_vector_<intc_ord>_<intc_irq>

This allows for the application to strongly define (redefine) the interrupt vector, replacing the "default" wrapper. The application will use the following macro to redefine the IRQ vector:

INTC_DT_INST_DIRECT_IRQ_HANDLER_DEFINE(0)
{

}

This macro will use the devicetree compatible of the interrupt controller to automatically include the header/footer if required by the specific interrupt controller.

#define INTC_DT_<interrupt controller compat upper>_DIRECT_IRQ_HANDLER_DEFINE(vector_name)
{

}

If the interrupt controller happened to have the compat foo, dts ord 4, and the interrupt number was 0, the macro would expand to:

INTC_DT_FOO_DIRECT_IRQ_HANDLER_DEFINE(__intc_irq_vector_4_0)

Connecting interrupts

Disclaimer: We should not do this, but...

To manually add a handler to an interrupt not defined in the devicetree, we can add an iterable section of irq handlers, that follow the naming of the interrupt handlers, which we can then find using elf parsing, and add to the appropriate interrupt handlers, similar to how we currently do IRQ_CONNECT

Dynamic interrupts

Disclaimer: We should not do this, but...

Dynamic interrupts will be stored in a single linked lists stored in RAM, indexed by the SYS_DT_IRQN_<intc_ord>_<intc_irq> macro. This table would be iterated through by the __sys_interrupt_handler_*_* handlers after the statically defined interrupts, if any exist. The application will define a context to be added to the single linked lists:

struct sys_irq {
        sys_snode_t node;
        uint16_t irqn;
        void (*handler)(const void *data);
        const void *data;
};

struct sys_irq __sys_irq_lists[4];

/* gpio0 */
static inline void __sys_interrupt_handler_1_3(void)
{
        __sys_irq_handler_1_3_5();
        /* iterate through every added IRQ after the statically defined one */
        sys_dynamic_handlers(SYS_DT_IRQN_1_3);
}

Interrupt controller API

The interrupt controllers will expose an API to configure and control the individual interrupt lines. This API is a normal device driver API, which will be interacted with primarily through the System IRQ API.

__subsystem struct intc_driver_api {
    int (*configure_irq)(const struct device *dev, uint16_t irq, uint32_t flags);
    int (*enable_irq)(const struct device *dev, uint16_t irq);
    /* returns 1 if the IRQ was enabled before call, 0 if not */
    int (*disable_irq)(const struct device *dev, uint16_t irq);
    int (*trigger_irq)(const struct device *dev, uint16_t irq);
    /* returns 1 if the IRQ was triggered before call, 0 if not */
    int (*clear_irq)(const struct device *dev, uint16_t irq);
};

Interrupt configuration

The configuration of an interrupt line is pretty much as vendor/device specific as it gets. All configurations will be encoded into a uint32_t, similar to GPIO flags. This includes level, priority, fast/slow irq, sense, delivery mode, etc. To do this generically, we will use the same approach as the INTC_DT_INST_DIRECT_IRQ_HANDLER_DEFINE to encode the interrupt cells into a uint32_t. For example, if the interrupt controller with compatible foo defines the interrupt cells:

interrupt-cells:
  - irq
  - sense
  - priority

and using this specific definition as an example:

interrupts-extended = <&foo 3 3 15>;

the flags would be gotten from the devicetree using

INTC_DT_INST_FLAGS(inst)

which would expand into

INTC_DT_INST_FOO_FLAGS(inst)

which would expand to the device/vendor specific macro which encodes the interrupt cells.

#define INTC_DT_INST_FOO_FLAGS(inst) \
    ((DT_INST_PHA_BY_IDX(inst, prop, 0, sense) << 8) | \
     (DT_INST_PHA_BY_IDX(inst, prop, 0, priority) << 0))

note that the IRQ is not encoded into the flags, it is stored separately.

System IRQ API

Users will not use the intc API directly when managing IRQs, this will be performed through the sys_irq_* APIs. This is necessary to ensure safe concurrent interactions with the IRQs, especially when requesting/releasing dynamic IRQs, which has to be performed with the interrupt line disabled.

The system IRQ uses a global interrupt number assigned to each interrupt in the system. The following diagram shows how the global IRQ number maps to an actual interrupt line: interrupt_split drawio(1) Note that only enabled interrupts are included in the indexing, it is not possible to enable or configure a spurious interrupt :)

The following API is then used to configure and add/remove dynamic IRQs, taking the global interrupt request number:

/* Called once to configure interrupt line (calls intc_irq_configure()) */
int sys_irq_configure(uint16_t irqn, uint32_t flags);

int sys_irq_enable(uint16_t irqn);
int sys_irq_disable(uint16_t irqn);
int sys_irq_trigger(uint16_t irqn);
int sys_irq_clear(uint16_t irqn);

/* Dynamic IRQ API (which we should not support) */
int sys_irq_request(struct sys_irq *irq, uint16_t irqn, sys_irq_handler handler,
            const void *data);

int sys_irq_release(struct sys_irq *irq);

Link-time optimization (LTO)

Very shortly, LTO allows the linker to change code like this

static void my_irq_handler(const void *data)
{
        /* do stuff */
}

static const struct device foo;
void __sys_irq_handler_0_3_1(void)
{
        my_irq_handler(&foo);
}

void __sys_interrupt_handler_0_3(void)
{
        __sys_irq_handler_0_3_1();
}

void __sys_interrupt_vector_0_3(void)
{
        foo_header();
        __sys_interrupt_handler_0_3();
        foo_footer();
}

To this

void __sys_interrupt_vector_0_3(void)
{
        foo_header();
        my_irq_handler(&foo);
        foo_footer();
}

by optimizing all modules as a single module, making symbols like foo and my_irq_handler visible from any module, allowing it to be inlined (at certain optimization levels) for example. This is only possible if symbols are preserved. This is one reason why the currently generated SW ISR table can not be optimized, since it's using addresses in memory, which the compiler/linker can not recognize.

Conclusion

This can completely replace the existing solution, providing even more ROM efficiency since we only need to define a const table of struct sys_irq_spec for the ones that are actually used, and we don't need to store the IRQ handlers or data passed to the them in tables, allowing the compiler to optimize them. The solution also removes the need for NUM_IRQS :)

The solution also creates a clear separation of responsibilities between the system, and the interrupt controllers, and supports any combination, number of, type of, and "level" of interrupt controllers under the INTC and SYS IRQ APIs.

rakons commented 10 months ago

Some input from my side:

  1. I do not think we should get priority nor flags from DT. The reason for this is that for some specific applications both of them may change during runtime. The edge on which we wish to react may change while code is executing. The same about priority - we usually are not doing that, but in some applications it may be reasonable to temporary boost or lower some IRQ priorities. So the flags and priority should be rather software defined.
  2. We need dynamic interrupts. Some of our clients are using, for example radio and are changing its functionality between Bluetooth and some of the proprietary communication. They wish to have a possibility to totally disconnect one and connect other implementation. For this kind of work dynamic interrupt implementation is very usefull and it would be hard to implemented in other way.
bjarki-andreasen commented 10 months ago

@rakons

  1. I do believe we should get the flags of an IRQ from the devicetree, given that the flags are not hardware agnostic. Priority is only supported by some interrupt controllers, and could be encoded into the flags as well, so not sure about that one, however, just as an initial value, I think it is still valuable to store it, the application can override it later.

  2. I can see how it would be neat to have the kernel allow that, but I do believe it could also be solved on a driver level using a switch statement, like:

    foo_irq_handler(const void *data)
    {
    switch (radio_mode) {
        case BLE:
            foo_ble_isr(data);
            break;
        case WIFI:
            foo_wifi_isr(data);
            break;
    };
    }

    which is entirely static :)

rakons commented 10 months ago

To be sure that we are using the same function for spurious vectors we could possibly create one function (like __sys_interrupt_spurious) and then generate weak aliases to that function

void __sys_interrupt_handler_1_2(void) __attribute__((weak, alias("__sys_interrupt_spurious"));

Note: static inline function cannot be weak. For the weak functions we have to have external linkage.

bjarki-andreasen commented 10 months ago

@rakons

The static inline handlers are not weak, they are strongly defined, its only the vectors which are weak :)

I think one level of misdirection is clearer than the attribute

static inline void __sys_interrupt_spurious(void)
{
        z_fatal_error(K_ERR_SPURIOUS_IRQ, NULL);
}

static inline void __sys_interrupt_handler_1_2(void)
{
        __sys_interrupt_spurious();
}

static inline void __sys_interrupt_handler_1_3(void)
{
        __sys_interrupt_spurious();
}

...
carlescufi commented 10 months ago

Architecture WG:

rruuaanng commented 1 week ago

Do we still need INTC_DT_INST_FLAGS? I can implement it.