diff --git a/boards/wch/ch32v003evt/ch32v003evt.dts b/boards/wch/ch32v003evt/ch32v003evt.dts index c81a00fd30b3..84289f32a92e 100644 --- a/boards/wch/ch32v003evt/ch32v003evt.dts +++ b/boards/wch/ch32v003evt/ch32v003evt.dts @@ -68,6 +68,10 @@ clocks = <&pll>; }; +&exti { + status = "okay"; +}; + &gpioc { status = "okay"; }; diff --git a/boards/wch/ch32v006evt/ch32v006evt.dts b/boards/wch/ch32v006evt/ch32v006evt.dts index 2cd0e20abeb0..34e3212f5005 100644 --- a/boards/wch/ch32v006evt/ch32v006evt.dts +++ b/boards/wch/ch32v006evt/ch32v006evt.dts @@ -74,6 +74,10 @@ clocks = <&pll>; }; +&exti { + status = "okay"; +}; + &gpioc { status = "okay"; }; diff --git a/boards/wch/linkw/linkw.dts b/boards/wch/linkw/linkw.dts index 50f749ef28cb..c01bb92736ee 100644 --- a/boards/wch/linkw/linkw.dts +++ b/boards/wch/linkw/linkw.dts @@ -68,6 +68,10 @@ clocks = <&pll>; }; +&exti { + status = "okay"; +}; + &gpioa { status = "okay"; }; diff --git a/drivers/gpio/Kconfig.wch_ch32v00x b/drivers/gpio/Kconfig.wch_ch32v00x index 9f7c447f17d4..c38d635cc753 100644 --- a/drivers/gpio/Kconfig.wch_ch32v00x +++ b/drivers/gpio/Kconfig.wch_ch32v00x @@ -5,3 +5,12 @@ config GPIO_WCH_GPIO bool "WCH CH32V00x GPIO driver" depends on DT_HAS_WCH_GPIO_ENABLED default y + +config GPIO_WCH_GPIO_INTERRUPTS + bool "Interrupt support" + depends on GPIO_WCH_GPIO + depends on DT_HAS_WCH_EXTI_ENABLED + default y + help + Support triggering an interrupt on pin change. Uses approximately + 700 bytes of flash and 60 bytes of RAM. diff --git a/drivers/gpio/wch_gpio_ch32v00x.c b/drivers/gpio/wch_gpio_ch32v00x.c index 1d901b3ebf31..76e660ec2bd7 100644 --- a/drivers/gpio/wch_gpio_ch32v00x.c +++ b/drivers/gpio/wch_gpio_ch32v00x.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,7 @@ struct gpio_ch32v00x_config { struct gpio_ch32v00x_data { struct gpio_driver_data common; + sys_slist_t callbacks; }; static int gpio_ch32v00x_configure(const struct device *dev, gpio_pin_t pin, gpio_flags_t flags) @@ -111,6 +113,131 @@ static int gpio_ch32v00x_port_toggle_bits(const struct device *dev, uint32_t pin return 0; } +#if defined(CONFIG_GPIO_WCH_GPIO_INTERRUPTS) + +static void gpio_ch32v00x_isr(uint8_t line, void *user) +{ + const struct device *dev = user; + struct gpio_ch32v00x_data *data = dev->data; + + gpio_fire_callbacks(&data->callbacks, dev, BIT(line)); +} + +static int gpio_ch32v00x_configure_exti(const struct device *dev, gpio_pin_t pin) +{ + const struct gpio_ch32v00x_config *config = dev->config; + AFIO_TypeDef *afio = (AFIO_TypeDef *)DT_REG_ADDR(DT_NODELABEL(pinctrl)); + uint8_t port_id; + uint8_t cr_id; + uint8_t bit0; + + /* Convert the device into a port ID by checking the address */ + switch ((uintptr_t)config->regs) { + case DT_REG_ADDR(DT_NODELABEL(gpioa)): + port_id = 0; + break; +#if DT_NODE_EXISTS(DT_NODELABEL(gpiob)) + case DT_REG_ADDR(DT_NODELABEL(gpiob)): + port_id = 1; + break; +#endif + case DT_REG_ADDR(DT_NODELABEL(gpioc)): + port_id = 2; + break; + case DT_REG_ADDR(DT_NODELABEL(gpiod)): + port_id = 3; + break; +#if DT_NODE_EXISTS(DT_NODELABEL(gpioe)) + case DT_REG_ADDR(DT_NODELABEL(gpioe)): + port_id = 4; + break; +#endif + default: + return -EINVAL; + } + +#if defined(AFIO_EXTICR_EXTI0) + /* CH32V003 style with one register with 2 bits per map. */ + BUILD_ASSERT(AFIO_EXTICR_EXTI0 == 0x03); + + (void)cr_id; + bit0 = pin << 1; + afio->EXTICR = (afio->EXTICR & ~(AFIO_EXTICR_EXTI0 << bit0)) | (port_id << bit0); +#elif defined(AFIO_EXTICR1_EXTI0) + /* + * CH32V20x style with multiple registers with 4 pins per register and 4 bits per + * map. + */ + BUILD_ASSERT(AFIO_EXTICR1_EXTI0 == 0x0F); + BUILD_ASSERT(ARRAY_SIZE(afio->EXTICR) == 4); + + cr_id = pin / 4; + bit0 = (pin % 4) * 4; + afio->EXTICR[cr_id] = + (afio->EXTICR[cr_id] & ~(AFIO_EXTICR1_EXTI0 << bit0)) | (port_id << bit0); +#else +#error Unrecognised EXTICR format +#endif + + return 0; +} + +static int gpio_ch32v00x_pin_interrupt_configure(const struct device *dev, gpio_pin_t pin, + enum gpio_int_mode mode, + enum gpio_int_trig trigger) +{ + int err; + + switch (mode) { + case GPIO_INT_MODE_DISABLED: + wch_exti_disable(pin); + err = wch_exti_configure(pin, NULL, NULL); + break; + case GPIO_INT_MODE_EDGE: + err = wch_exti_configure(pin, gpio_ch32v00x_isr, (void *)dev); + if (err != 0) { + break; + } + + err = gpio_ch32v00x_configure_exti(dev, pin); + if (err != 0) { + break; + } + + switch (trigger) { + case GPIO_INT_TRIG_LOW: + wch_exti_set_trigger(pin, WCH_EXTI_TRIGGER_FALLING_EDGE); + break; + case GPIO_INT_TRIG_HIGH: + wch_exti_set_trigger(pin, WCH_EXTI_TRIGGER_RISING_EDGE); + break; + case GPIO_INT_TRIG_BOTH: + wch_exti_set_trigger(pin, WCH_EXTI_TRIGGER_FALLING_EDGE | + WCH_EXTI_TRIGGER_RISING_EDGE); + break; + default: + return -ENOTSUP; + } + + wch_exti_enable(pin); + break; + default: + return -ENOTSUP; + } + + return err; +} + +static int gpio_ch32v00x_manage_callback(const struct device *dev, struct gpio_callback *callback, + bool set) +{ + struct gpio_ch32v00x_data *data = dev->data; + + return gpio_manage_callback(&data->callbacks, callback, set); +} + +#endif /* CONFIG_GPIO_WCH_GPIO_INTERRUPTS */ + static DEVICE_API(gpio, gpio_ch32v00x_driver_api) = { .pin_configure = gpio_ch32v00x_configure, .port_get_raw = gpio_ch32v00x_port_get_raw, @@ -118,6 +245,10 @@ static DEVICE_API(gpio, gpio_ch32v00x_driver_api) = { .port_set_bits_raw = gpio_ch32v00x_port_set_bits_raw, .port_clear_bits_raw = gpio_ch32v00x_port_clear_bits_raw, .port_toggle_bits = gpio_ch32v00x_port_toggle_bits, +#if defined(CONFIG_GPIO_WCH_GPIO_INTERRUPTS) + .pin_interrupt_configure = gpio_ch32v00x_pin_interrupt_configure, + .manage_callback = gpio_ch32v00x_manage_callback, +#endif }; static int gpio_ch32v00x_init(const struct device *dev) diff --git a/drivers/interrupt_controller/CMakeLists.txt b/drivers/interrupt_controller/CMakeLists.txt index 2bb2dbe16336..b652c748470e 100644 --- a/drivers/interrupt_controller/CMakeLists.txt +++ b/drivers/interrupt_controller/CMakeLists.txt @@ -45,6 +45,7 @@ zephyr_library_sources_ifdef(CONFIG_RENESAS_RZ_EXT_IRQ intc_renesas_rz_ext_ zephyr_library_sources_ifdef(CONFIG_NXP_IRQSTEER intc_nxp_irqsteer.c) zephyr_library_sources_ifdef(CONFIG_INTC_MTK_ADSP intc_mtk_adsp.c) zephyr_library_sources_ifdef(CONFIG_WCH_PFIC intc_wch_pfic.c) +zephyr_library_sources_ifdef(CONFIG_WCH_EXTI intc_wch_exti.c) if(CONFIG_INTEL_VTD_ICTL) zephyr_library_include_directories(${ZEPHYR_BASE}/arch/x86/include) diff --git a/drivers/interrupt_controller/Kconfig b/drivers/interrupt_controller/Kconfig index 3468cf4f3f23..a5715de368c7 100644 --- a/drivers/interrupt_controller/Kconfig +++ b/drivers/interrupt_controller/Kconfig @@ -110,4 +110,6 @@ source "drivers/interrupt_controller/Kconfig.mtk_adsp" source "drivers/interrupt_controller/Kconfig.wch_pfic" +source "drivers/interrupt_controller/Kconfig.wch_exti" + endmenu diff --git a/drivers/interrupt_controller/Kconfig.wch_exti b/drivers/interrupt_controller/Kconfig.wch_exti new file mode 100644 index 000000000000..d33739ef1865 --- /dev/null +++ b/drivers/interrupt_controller/Kconfig.wch_exti @@ -0,0 +1,10 @@ +# Copyright (c) 2025 Michael Hope +# SPDX-License-Identifier: Apache-2.0 + +config WCH_EXTI + bool "WCH CH32V00x/20x/30x External Interrupt and Event Controller (EXTI)" + default y + depends on DT_HAS_WCH_EXTI_ENABLED + help + Enable the WCH CH32V00x/20x/30x External Interrupt and Event Controller (EXTI) + driver. diff --git a/drivers/interrupt_controller/intc_wch_exti.c b/drivers/interrupt_controller/intc_wch_exti.c new file mode 100644 index 000000000000..846a27b0b62c --- /dev/null +++ b/drivers/interrupt_controller/intc_wch_exti.c @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2025 Michael Hope + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT wch_exti + +#include + +#include +#include +#include +#include + +#include + +#define WCH_EXTI_NUM_LINES DT_PROP(DT_NODELABEL(exti), num_lines) + +/* Per EXTI callback registration */ +struct wch_exti_registration { + wch_exti_callback_handler_t callback; + void *user; +}; + +struct wch_exti_data { + struct wch_exti_registration callbacks[WCH_EXTI_NUM_LINES]; +}; + +#define WCH_EXTI_INIT_RANGE(node_id, interrupts, idx) \ + DT_PROP_BY_IDX(node_id, line_ranges, UTIL_X2(idx)), + +/* + * List of [start, end) line ranges for each line group, where the range for group n is + * `[wch_exti_ranges[n-1]...wch_exti_ranges[n])`. This uses the fact that the ranges are contiguous, + * so the end of group n is the same as the start of group n+1. + */ +static const uint8_t wch_exti_ranges[] = { + DT_FOREACH_PROP_ELEM(DT_NODELABEL(exti), interrupt_names, WCH_EXTI_INIT_RANGE) + WCH_EXTI_NUM_LINES, +}; + +#define WCH_EXTI_INIT_INTERRUPT(node_id, interrupts, idx) DT_IRQ_BY_IDX(node_id, idx, irq), + +/* Interrupt number for each line group. Used when enabling the interrupt. */ +static const uint8_t wch_exti_interrupts[] = { + DT_FOREACH_PROP_ELEM(DT_NODELABEL(exti), interrupt_names, WCH_EXTI_INIT_INTERRUPT)}; + +BUILD_ASSERT(ARRAY_SIZE(wch_exti_interrupts) + 1 == ARRAY_SIZE(wch_exti_ranges)); + +static void wch_exti_isr(const void *user) +{ + const struct device *const dev = DEVICE_DT_INST_GET(0); + struct wch_exti_data *data = dev->data; + EXTI_TypeDef *regs = (EXTI_TypeDef *)DT_INST_REG_ADDR(0); + const uint8_t *range = user; + uint32_t intfr = regs->INTFR; + + for (uint8_t line = range[0]; line < range[1]; line++) { + if ((intfr & BIT(line)) != 0) { + const struct wch_exti_registration *callback = &data->callbacks[line]; + /* Clear the interrupt */ + regs->INTFR = BIT(line); + if (callback->callback != NULL) { + callback->callback(line, callback->user); + } + } + } +} + +void wch_exti_enable(uint8_t line) +{ + EXTI_TypeDef *regs = (EXTI_TypeDef *)DT_INST_REG_ADDR(0); + + regs->INTENR |= BIT(line); + /* Find the corresponding interrupt and enable it */ + for (uint8_t i = 1; i < ARRAY_SIZE(wch_exti_ranges); i++) { + if (line < wch_exti_ranges[i]) { + irq_enable(wch_exti_interrupts[i - 1]); + break; + } + } +} + +void wch_exti_disable(uint8_t line) +{ + EXTI_TypeDef *regs = (EXTI_TypeDef *)DT_INST_REG_ADDR(0); + + regs->INTENR &= ~BIT(line); +} + +int wch_exti_configure(uint8_t line, wch_exti_callback_handler_t callback, void *user) +{ + const struct device *const dev = DEVICE_DT_INST_GET(0); + struct wch_exti_data *data = dev->data; + struct wch_exti_registration *registration = &data->callbacks[line]; + + if (registration->callback == callback && registration->user == user) { + return 0; + } + + if (callback != NULL && registration->callback != NULL) { + return -EALREADY; + } + + registration->callback = callback; + registration->user = user; + + return 0; +} + +void wch_exti_set_trigger(uint8_t line, enum wch_exti_trigger trigger) +{ + EXTI_TypeDef *regs = (EXTI_TypeDef *)DT_INST_REG_ADDR(0); + + WRITE_BIT(regs->RTENR, line, (trigger & WCH_EXTI_TRIGGER_RISING_EDGE) != 0); + WRITE_BIT(regs->FTENR, line, (trigger & WCH_EXTI_TRIGGER_FALLING_EDGE) != 0); +} + +#define WCH_EXTI_CONNECT_IRQ(node_id, interrupts, idx) \ + IRQ_CONNECT(DT_IRQ_BY_IDX(node_id, idx, irq), DT_IRQ_BY_IDX(node_id, idx, priority), \ + wch_exti_isr, &wch_exti_ranges[idx], 0); + +static int wch_exti_init(const struct device *dev) +{ + /* Generate the registrations for each interrupt */ + DT_FOREACH_PROP_ELEM(DT_NODELABEL(exti), interrupt_names, WCH_EXTI_CONNECT_IRQ); + + return 0; +} + +static struct wch_exti_data wch_exti_data_0; + +DEVICE_DT_INST_DEFINE(0, wch_exti_init, NULL, &wch_exti_data_0, NULL, PRE_KERNEL_2, + CONFIG_INTC_INIT_PRIORITY, NULL); diff --git a/dts/bindings/interrupt-controller/wch,exti.yaml b/dts/bindings/interrupt-controller/wch,exti.yaml new file mode 100644 index 000000000000..3e3cba4d82b3 --- /dev/null +++ b/dts/bindings/interrupt-controller/wch,exti.yaml @@ -0,0 +1,36 @@ +# Copyright (c) 2025 Michael Hope +# SPDX-License-Identifier: Apache-2.0 + +description: WCH CH32V003/20x/30x External Interrupt and Event Controller (EXTI) + +compatible: "wch,exti" + +include: [base.yaml, interrupt-controller.yaml] + +properties: + reg: + required: true + + interrupts: + required: true + + num-lines: + type: int + required: true + description: Number of lines supported by the interrupt controller. + + line-ranges: + type: array + required: true + description: | + Describes how the input lines are grouped into ranges. Each range + consists of a (starting line, number of lines) pair and map to + a single interrupt. + + For example: + line-ranges = <0 1>, <1 1>, <2 1>, <3 1>, + <4 1>, <5 5>, <10 6>; + + defines seven ranges where the first five contain one line, the + sixth starts with line 5 and contains five elements (5 to 9), and + the last starts with line 10 and contains six elements (10 to 15). diff --git a/dts/riscv/wch/ch32v0/ch32v003.dtsi b/dts/riscv/wch/ch32v0/ch32v003.dtsi index d28667135dbe..f88fc2b8e9b3 100644 --- a/dts/riscv/wch/ch32v0/ch32v003.dtsi +++ b/dts/riscv/wch/ch32v0/ch32v003.dtsi @@ -72,6 +72,19 @@ status = "disabled"; }; + exti: interrupt-controller@40010400 { + compatible = "wch,exti"; + interrupt-controller; + #interrupt-cells = <1>; + reg = <0x40010400 16>; + interrupt-parent = <&pfic>; + num-lines = <8>; + interrupts = <20>; + line-ranges = <0 8>; + interrupt-names = "line0-7"; + status = "disabled"; + }; + pinctrl: pin-controller@40010000 { compatible = "wch,afio"; reg = <0x40010000 0x10>; diff --git a/dts/riscv/wch/ch32v0/ch32v006.dtsi b/dts/riscv/wch/ch32v0/ch32v006.dtsi index dd6cac5e0549..7aa311886536 100644 --- a/dts/riscv/wch/ch32v0/ch32v006.dtsi +++ b/dts/riscv/wch/ch32v0/ch32v006.dtsi @@ -72,6 +72,19 @@ status = "disabled"; }; + exti: interrupt-controller@40010400 { + compatible = "wch,exti"; + interrupt-controller; + #interrupt-cells = <1>; + reg = <0x40010400 16>; + interrupt-parent = <&pfic>; + num-lines = <8>; + interrupts = <20>; + line-ranges = <0 8>; + interrupt-names = "line0-7"; + status = "disabled"; + }; + pinctrl: pin-controller@40010000 { compatible = "wch,00x-afio"; reg = <0x40010000 0x10>; diff --git a/dts/riscv/wch/ch32v208/ch32v208.dtsi b/dts/riscv/wch/ch32v208/ch32v208.dtsi index 659fb1e62b5e..c049e1241bd6 100644 --- a/dts/riscv/wch/ch32v208/ch32v208.dtsi +++ b/dts/riscv/wch/ch32v208/ch32v208.dtsi @@ -71,6 +71,21 @@ status = "disabled"; }; + exti: interrupt-controller@40010400 { + compatible = "wch,exti"; + interrupt-controller; + #interrupt-cells = <1>; + reg = <0x40010400 16>; + interrupt-parent = <&pfic>; + num-lines = <16>; + line-ranges = <0 1>, <1 1>, <2 1>, <3 1>, <4 1>, + <5 5>, <10 6>; + interrupts = <22 23 24 25 26 39 56>; + interrupt-names = "line0", "line1", "line2", "line3", + "line4", "line5-9", "line10-15"; + status = "disabled"; + }; + pinctrl: pin-controller@40010000 { compatible = "wch,20x_30x-afio"; reg = <0x40010000 16>; diff --git a/include/zephyr/drivers/interrupt_controller/wch_exti.h b/include/zephyr/drivers/interrupt_controller/wch_exti.h new file mode 100644 index 000000000000..25baf0d5d66d --- /dev/null +++ b/include/zephyr/drivers/interrupt_controller/wch_exti.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 Michael Hope + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_DRIVERS_INTERRUPT_CONTROLLER_WCH_EXTI_H_ +#define ZEPHYR_INCLUDE_DRIVERS_INTERRUPT_CONTROLLER_WCH_EXTI_H_ + +#include + +#include + +/* Callback for EXTI interrupt. */ +typedef void (*wch_exti_callback_handler_t)(uint8_t line, void *user); + +enum wch_exti_trigger { + /* + * Note that this is a flag set and these values can be ORed to trigger on + * both edges. + */ + + /* Trigger on rising edge */ + WCH_EXTI_TRIGGER_RISING_EDGE = BIT(0), + /* Trigger on falling edge */ + WCH_EXTI_TRIGGER_FALLING_EDGE = BIT(1), +}; + +/* Enable the EXTI interrupt for `line` */ +void wch_exti_enable(uint8_t line); + +/* Disable the EXTI interrupt for `line` */ +void wch_exti_disable(uint8_t line); + +/* Set the trigger mode for `line` */ +void wch_exti_set_trigger(uint8_t line, enum wch_exti_trigger trigger); + +/* Register a callback for `line` */ +int wch_exti_configure(uint8_t line, wch_exti_callback_handler_t callback, void *user); + +#endif /* ZEPHYR_INCLUDE_DRIVERS_INTERRUPT_CONTROLLER_WCH_EXTI_H_ */