From b068fc0202caa7f8317fdc91cd7dff738c8b467b Mon Sep 17 00:00:00 2001 From: Martino Facchin Date: Mon, 17 Nov 2025 17:19:29 +0100 Subject: [PATCH 1/8] core: pinmux: implement rough solution While we prepare a cleaner solution, this patch allows back and forth between pinmuxes. Tested on UNO Q with next patch in the series and this sketch void loop() { Serial.begin(115200); Serial.println("helloooooo"); delay(20); Serial.end(); delay(20); pinMode(1, OUTPUT); digitalWrite(1, HIGH); delay(10); digitalWrite(1, LOW); delay(100); analogWrite(1, 33); } Pin 1 correctly prints the string, muxes as GPIo and then produces the required PWM --- cores/arduino/zephyrCommon.cpp | 20 ++++++++++++++++++++ cores/arduino/zephyrInternal.h | 1 + cores/arduino/zephyrSerial.cpp | 2 ++ cores/arduino/zephyrSerial.h | 3 +++ libraries/SPI/SPI.cpp | 4 ++++ libraries/Wire/Wire.cpp | 4 ++++ loader/prj.conf | 2 ++ 7 files changed, 36 insertions(+) diff --git a/cores/arduino/zephyrCommon.cpp b/cores/arduino/zephyrCommon.cpp index 64a0f712a..cb3358157 100644 --- a/cores/arduino/zephyrCommon.cpp +++ b/cores/arduino/zephyrCommon.cpp @@ -7,6 +7,19 @@ #include #include "zephyrInternal.h" +// create an array of arduino_pins with functions to reinitialize pins if needed +static const struct device *pinmux_array[DT_PROP_LEN(DT_PATH(zephyr_user), digital_pin_gpios)] = { + nullptr}; + +void _reinit_peripheral_if_needed(pin_size_t pin, const struct device *dev) { + if (pinmux_array[pin] != dev) { + pinmux_array[pin] = dev; + if (dev != NULL) { + dev->ops.init(dev); + } + } +} + static const struct gpio_dt_spec arduino_pins[] = { DT_FOREACH_PROP_ELEM_SEP( DT_PATH(zephyr_user), digital_pin_gpios, GPIO_DT_SPEC_GET_BY_IDX, (, ))}; @@ -209,6 +222,7 @@ void yield(void) { * A high physical level will be interpreted as value 1 */ void pinMode(pin_size_t pinNumber, PinMode pinMode) { + _reinit_peripheral_if_needed(pinNumber, NULL); if (pinMode == INPUT) { // input mode gpio_pin_configure_dt(&arduino_pins[pinNumber], GPIO_INPUT | GPIO_ACTIVE_HIGH); } else if (pinMode == INPUT_PULLUP) { // input with internal pull-up @@ -320,6 +334,7 @@ void analogWrite(pin_size_t pinNumber, int value) { return; } + _reinit_peripheral_if_needed(pinNumber, arduino_pwm[idx].dev); value = map(value, 0, 1 << _analog_write_resolution, 0, arduino_pwm[idx].period); if (((uint32_t)value) > arduino_pwm[idx].period) { @@ -343,6 +358,9 @@ void analogWrite(enum dacPins dacName, int value) { return; } + // TODO: add reverse map to find pin name from DAC* define + // In the meantime, consider A0 == DAC0 + _reinit_peripheral_if_needed((pin_size_t)(dacName + A0), dac_dev); dac_channel_setup(dac_dev, &dac_ch_cfg[dacName]); const int max_dac_value = 1U << dac_ch_cfg[dacName].resolution; @@ -393,6 +411,8 @@ int analogRead(pin_size_t pinNumber) { return -ENOTSUP; } + _reinit_peripheral_if_needed(pinNumber, arduino_adc[idx].dev); + err = adc_channel_setup(arduino_adc[idx].dev, &arduino_adc[idx].channel_cfg); if (err < 0) { return err; diff --git a/cores/arduino/zephyrInternal.h b/cores/arduino/zephyrInternal.h index b07e1bd9c..e3938e603 100644 --- a/cores/arduino/zephyrInternal.h +++ b/cores/arduino/zephyrInternal.h @@ -14,6 +14,7 @@ extern "C" { void enableInterrupt(pin_size_t); void disableInterrupt(pin_size_t); +void _reinit_peripheral_if_needed(pin_size_t pin, const struct device *dev); #ifdef __cplusplus } // extern "C" diff --git a/cores/arduino/zephyrSerial.cpp b/cores/arduino/zephyrSerial.cpp index 2ebece7b9..a9e0b2951 100644 --- a/cores/arduino/zephyrSerial.cpp +++ b/cores/arduino/zephyrSerial.cpp @@ -59,6 +59,8 @@ void arduino::ZephyrSerial::begin(unsigned long baud, uint16_t conf) { .flow_ctrl = UART_CFG_FLOW_CTRL_NONE, }; + uart->ops.init(uart); + uart_configure(uart, &config); uart_irq_callback_user_data_set(uart, arduino::ZephyrSerial::IrqDispatch, this); k_sem_take(&rx.sem, K_FOREVER); diff --git a/cores/arduino/zephyrSerial.h b/cores/arduino/zephyrSerial.h index 5e1d15fe7..46ee1b1d7 100644 --- a/cores/arduino/zephyrSerial.h +++ b/cores/arduino/zephyrSerial.h @@ -75,6 +75,9 @@ class ZephyrSerial : public HardwareSerial { void flush(); void end() { + if (uart->ops.deinit) { + uart->ops.deinit(uart); + } } size_t write(const uint8_t *buffer, size_t size); diff --git a/libraries/SPI/SPI.cpp b/libraries/SPI/SPI.cpp index 1a0b39115..77caa2e7a 100644 --- a/libraries/SPI/SPI.cpp +++ b/libraries/SPI/SPI.cpp @@ -115,9 +115,13 @@ void arduino::ZephyrSPI::detachInterrupt() { } void arduino::ZephyrSPI::begin() { + spi_dev->ops.init(spi_dev); } void arduino::ZephyrSPI::end() { + if (spi_dev->ops.deinit) { + spi_dev->ops.deinit(spi_dev); + } } #if DT_NODE_HAS_PROP(DT_PATH(zephyr_user), spis) diff --git a/libraries/Wire/Wire.cpp b/libraries/Wire/Wire.cpp index 18ec6140c..d8cc06e5d 100644 --- a/libraries/Wire/Wire.cpp +++ b/libraries/Wire/Wire.cpp @@ -54,6 +54,7 @@ arduino::ZephyrI2C::ZephyrI2C(const struct device *i2c) : i2c_dev(i2c), i2c_cfg( } void arduino::ZephyrI2C::begin() { + i2c_dev->ops.init(i2c_dev); } void arduino::ZephyrI2C::begin(uint8_t slaveAddr) { @@ -70,6 +71,9 @@ void arduino::ZephyrI2C::end() { i2c_target_unregister(i2c_dev, &i2c_cfg); memset(&i2c_cfg, 0, sizeof(i2c_cfg)); } + if (i2c_dev->ops.deinit) { + i2c_dev->ops.deinit(i2c_dev); + } } void arduino::ZephyrI2C::setClock(uint32_t freq) { diff --git a/loader/prj.conf b/loader/prj.conf index 99c2c7172..399ceccc7 100644 --- a/loader/prj.conf +++ b/loader/prj.conf @@ -30,6 +30,8 @@ CONFIG_LLEXT_EDK_FORMAT_TAR_ZSTD=y CONFIG_GPIO=y CONFIG_PINCTRL=y +CONFIG_PINCTRL_DYNAMIC=y +CONFIG_DEVICE_DEINIT_SUPPORT=y CONFIG_I2C=y CONFIG_SPI=y From 505c6f6c2168fa0e4cbc2427ed7c290a5789ca15 Mon Sep 17 00:00:00 2001 From: Martino Facchin Date: Mon, 17 Nov 2025 17:22:41 +0100 Subject: [PATCH 2/8] unoq: restore conflicting PWMs and DACs --- .../arduino_uno_q_stm32u585xx.overlay | 76 +++++++++---------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay b/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay index b37c40282..65639c739 100644 --- a/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay +++ b/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay @@ -75,15 +75,13 @@ status = "okay"; /* tim1_etr_pa12 is not available for PWM */ /* Currently only the pins marked with ~ on the pin headers are enabled */ - /* pinctrl-0 = <&tim1_ch4_pa11 &tim1_ch3n_pb15 &tim1_ch1n_pb13 &tim1_ch2n_pb14>; */ - pinctrl-0 = <&tim1_ch4_pa11>; + pinctrl-0 = <&tim1_ch4_pa11 &tim1_ch3n_pb15 &tim1_ch1n_pb13 &tim1_ch2n_pb14>; pinctrl-names = "default"; }; }; /* Currently only the pins marked with ~ on the pin headers are enabled */ /* PB10 and PB11 conflict with I2C configuation */ -/* &timers2 { status = "okay"; st,prescaler = <4>; @@ -94,7 +92,6 @@ pinctrl-names = "default"; }; }; -*/ &timers3 { status = "okay"; @@ -103,8 +100,7 @@ pwm3: pwm { status = "okay"; /* Currently only the pins marked with ~ on the pin headers are enabled */ - /* pinctrl-0 = <&tim3_ch3_pb0 &tim3_ch4_pb1 &tim3_ch1_pb4>; */ - pinctrl-0 = <&tim3_ch3_pb0 &tim3_ch4_pb1>; + pinctrl-0 = <&tim3_ch3_pb0 &tim3_ch4_pb1 &tim3_ch1_pb4>; pinctrl-names = "default"; }; }; @@ -117,8 +113,7 @@ status = "okay"; /* PB6 PB7 not usable for PWM until dynamic pin muxing works */ /* Currently only the pins marked with ~ on the pin headers are enabled */ - /* pinctrl-0 = <&tim4_ch3_pb8 &tim4_ch4_pb9 &tim4_ch1_pb6 &tim4_ch2_pb7>; */ - pinctrl-0 = <&tim4_ch3_pb8 &tim4_ch4_pb9>; + pinctrl-0 = <&tim4_ch3_pb8 &tim4_ch4_pb9 &tim4_ch1_pb6 &tim4_ch2_pb7>; pinctrl-names = "default"; }; }; @@ -135,7 +130,6 @@ }; /* Currently only the pins marked with ~ on the pin headers are enabled */ -/* &timers8 { status = "okay"; st,prescaler = <4>; @@ -146,7 +140,6 @@ pinctrl-names = "default"; }; }; -*/ &timers16 { status = "okay"; @@ -258,25 +251,25 @@ /* PWM pin mapping - Digital pins with their timer channels */ /* Currently only the pins marked with ~ on the pin headers are enabled */ - pwm-pin-gpios = - /* <&gpiob 6 0>, */ /* D1/PB6 - TIM4_CH1 */ - /* <&gpiob 3 0>, */ /* D2/PB3 - TIM2_CH2 */ - /* <&gpiob 3 0>, */ /* D2/PB3 - TIM2_CH2 */ - <&gpiob 0 0>, /* D3/PB0 - TIM3_CH3 */ - <&gpioa 11 0>, /* D5/PA11 - TIM1_CH4 */ - <&gpiob 1 0>, /* D6/PB1 - TIM3_CH4 */ - /* <&gpiob 2 0>, */ /* D7/PB2 - TIM8_CH4N */ - /* <&gpiob 4 0>, */ /* D8/PB4 - TIM3_CH1 */ - <&gpiob 8 0>, /* D9/PB8 - TIM4_CH3 */ - <&gpiob 9 0>, /* D10/PB9 - TIM4_CH4 */ - /* <&gpiob 15 0>, */ /* D11/PB15 - TIM1_CH3N */ - /* <&gpiob 14 0>, */ /* D12/PB14 - TIM1_CH2N */ - /* <&gpiob 13 0>, */ /* D13/PB13 - TIM1_CH1N */ - /* <&gpiob 11 0>, */ /* D20/PB11 - TIM2_CH4 */ - /* <&gpiob 10 0>; */ /* D21/PB10 - TIM2_CH3 */ - <&gpioh 10 0>, /* LED3_R - TIM5_CH1 */ - <&gpioh 11 0>, /* LED3_G - TIM5_CH2 */ - <&gpioh 12 0>; /* LED3_B - TIM5_CH3 */ + pwm-pin-gpios = + <&gpiob 7 0>, /* D0/PB6 - TIM4_CH2 */ + <&gpiob 6 0>, /* D1/PB6 - TIM4_CH1 */ + <&gpiob 3 0>, /* D2/PB3 - TIM2_CH2 */ + <&gpiob 0 0>, /* D3/PB0 - TIM3_CH3 */ + <&gpioa 11 0>, /* D5/PA11 - TIM1_CH4 */ + <&gpiob 1 0>, /* D6/PB1 - TIM3_CH4 */ + <&gpiob 2 0>, /* D7/PB2 - TIM8_CH4N */ + <&gpiob 4 0>, /* D8/PB4 - TIM3_CH1 */ + <&gpiob 8 0>, /* D9/PB8 - TIM4_CH3 */ + <&gpiob 9 0>, /* D10/PB9 - TIM4_CH4 */ + <&gpiob 15 0>, /* D11/PB15 - TIM1_CH3N */ + <&gpiob 14 0>, /* D12/PB14 - TIM1_CH2N */ + <&gpiob 13 0>, /* D13/PB13 - TIM1_CH1N */ + <&gpiob 11 0>, /* D20/PB11 - TIM2_CH4 */ + <&gpiob 10 0>, /* D21/PB10 - TIM2_CH3 */ + <&gpioh 10 0>, /* LED3_R - TIM5_CH1 */ + <&gpioh 11 0>, /* LED3_G - TIM5_CH2 */ + <&gpioh 12 0>; /* LED3_B - TIM5_CH3 */ adc-pin-gpios = <&gpioa 4 0>, <&gpioa 5 0>, @@ -290,22 +283,22 @@ spis = <&spi2>, <&spi3>; /* PWM mapping for the digital pins */ /* Currently only the pins marked with ~ on the pin headers are enabled */ - pwms = - /* <&pwm4 2 PWM_HZ(500) PWM_POLARITY_NORMAL>, */ /* D0/PB7 → TIM4_CH2 */ - /* <&pwm4 1 PWM_HZ(500) PWM_POLARITY_NORMAL>, */ /* D1/PB6 → TIM4_CH1 */ - /* <&pwm2 2 PWM_HZ(500) PWM_POLARITY_NORMAL>, */ /* D2/PB3 → TIM2_CH2 */ + pwms = + <&pwm4 2 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D0/PB7 → TIM4_CH2 */ + <&pwm4 1 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D1/PB6 → TIM4_CH1 */ + <&pwm2 2 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D2/PB3 → TIM2_CH2 */ <&pwm3 3 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D3/PB0 → TIM3_CH3 */ <&pwm1 4 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D5/PA11 → TIM1_CH4 */ <&pwm3 4 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D6/PB1 → TIM3_CH4 */ - /* <&pwm8 4 PWM_HZ(500) PWM_POLARITY_INVERTED>, */ /* D7/PB2 → TIM8_CH4N */ - /* <&pwm3 1 PWM_HZ(500) PWM_POLARITY_NORMAL>, */ /* D8/PB4 → TIM3_CH1 */ + <&pwm8 4 PWM_HZ(500) PWM_POLARITY_INVERTED>, /* D7/PB2 → TIM8_CH4N */ + <&pwm3 1 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D8/PB4 → TIM3_CH1 */ <&pwm4 3 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D9/PB8 → TIM4_CH3 */ <&pwm4 4 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D10/PB9 → TIM4_CH4 */ - /* <&pwm1 3 PWM_HZ(500) PWM_POLARITY_INVERTED>, */ /* D11/PB15 → TIM1_CH3N */ - /* <&pwm1 2 PWM_HZ(500) PWM_POLARITY_INVERTED>, */ /* D12/PB14 → TIM1_CH2N */ - /* <&pwm1 1 PWM_HZ(500) PWM_POLARITY_INVERTED>, */ /* D13/PB13 → TIM1_CH1N */ - /* <&pwm2 4 PWM_HZ(500) PWM_POLARITY_NORMAL>, */ /* D20/PB11 → TIM2_CH4 */ - /* <&pwm2 3 PWM_HZ(500) PWM_POLARITY_NORMAL>; */ /* D21/PB10 → TIM2_CH3 */ + <&pwm1 3 PWM_HZ(500) PWM_POLARITY_INVERTED>, /* D11/PB15 → TIM1_CH3N */ + <&pwm1 2 PWM_HZ(500) PWM_POLARITY_INVERTED>, /* D12/PB14 → TIM1_CH2N */ + <&pwm1 1 PWM_HZ(500) PWM_POLARITY_INVERTED>, /* D13/PB13 → TIM1_CH1N */ + <&pwm2 4 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D20/PB11 → TIM2_CH4 */ + <&pwm2 3 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D21/PB10 → TIM2_CH3 */ <&pwm5 1 PWM_HZ(500) PWM_POLARITY_INVERTED>, /* LED3_R/PH10 → TIM5_CH1 */ <&pwm5 2 PWM_HZ(500) PWM_POLARITY_INVERTED>, /* LED3_G/PH11 → TIM5_CH2 */ <&pwm5 3 PWM_HZ(500) PWM_POLARITY_INVERTED>; /* LED3_B/PH12 → TIM5_CH3 */ @@ -320,6 +313,9 @@ <&adc1 2>, /* A4 - PC1 */ <&adc1 1>; /* A5 - PC0 */ + dac-pin-gpios = <&gpioa 4 0>, + <&gpioa 5 0>; + dac = <&dac1>; dac-channels = <1>, <2>; dac-resolution = <12>; From 8ffc81cc17c62a7cae9dcd9370aab4f864d2df1e Mon Sep 17 00:00:00 2001 From: Kurt Eckhardt Date: Mon, 23 Feb 2026 06:37:42 -0800 Subject: [PATCH 3/8] UnoQ: Add Wire2 to A4/A5 pins As these pins are shown as Wire pins on the official documents such as the pinout and multiple people have reported that it does not work. --- .../arduino_uno_q_stm32u585xx.overlay | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay b/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay index 98d9552b9..86d0b2634 100644 --- a/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay +++ b/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay @@ -22,6 +22,13 @@ */ }; +&i2c3 { + status = "okay"; + pinctrl-0 = <&i2c3_scl_pc0 &i2c3_sda_pc1>; + pinctrl-names = "default"; + clock-frequency = ; +}; + /* clock from HSI48 */ &mco1 { status = "okay"; @@ -301,7 +308,7 @@ <&gpioc 0 0>; serials = <&usart1>, <&lpuart1>; - i2cs = <&i2c2>, <&i2c4>; + i2cs = <&i2c2>, <&i2c4>, <&i2c3>; spis = <&spi2>, <&spi3>; /* PWM mapping for the digital pins */ /* Currently only the pins marked with ~ on the pin headers are enabled */ From 34430f06d614002a245065ab6093fdc7f8b01c12 Mon Sep 17 00:00:00 2001 From: pennam Date: Mon, 2 Mar 2026 15:40:13 +0100 Subject: [PATCH 4/8] unoq: keep zephyr serial output on D0/D1 --- .../arduino_uno_q_stm32u585xx.overlay | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay b/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay index 65639c739..bdc339a3b 100644 --- a/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay +++ b/variants/arduino_uno_q_stm32u585xx/arduino_uno_q_stm32u585xx.overlay @@ -113,7 +113,7 @@ status = "okay"; /* PB6 PB7 not usable for PWM until dynamic pin muxing works */ /* Currently only the pins marked with ~ on the pin headers are enabled */ - pinctrl-0 = <&tim4_ch3_pb8 &tim4_ch4_pb9 &tim4_ch1_pb6 &tim4_ch2_pb7>; + pinctrl-0 = <&tim4_ch3_pb8 &tim4_ch4_pb9 /*&tim4_ch1_pb6 &tim4_ch2_pb7*/>; pinctrl-names = "default"; }; }; @@ -252,8 +252,8 @@ /* PWM pin mapping - Digital pins with their timer channels */ /* Currently only the pins marked with ~ on the pin headers are enabled */ pwm-pin-gpios = - <&gpiob 7 0>, /* D0/PB6 - TIM4_CH2 */ - <&gpiob 6 0>, /* D1/PB6 - TIM4_CH1 */ + /*<&gpiob 7 0>,*/ /* D0/PB6 - TIM4_CH2 */ + /*<&gpiob 6 0>,*/ /* D1/PB6 - TIM4_CH1 */ <&gpiob 3 0>, /* D2/PB3 - TIM2_CH2 */ <&gpiob 0 0>, /* D3/PB0 - TIM3_CH3 */ <&gpioa 11 0>, /* D5/PA11 - TIM1_CH4 */ @@ -284,8 +284,8 @@ /* PWM mapping for the digital pins */ /* Currently only the pins marked with ~ on the pin headers are enabled */ pwms = - <&pwm4 2 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D0/PB7 → TIM4_CH2 */ - <&pwm4 1 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D1/PB6 → TIM4_CH1 */ + /*<&pwm4 2 PWM_HZ(500) PWM_POLARITY_NORMAL>,*/ /* D0/PB7 → TIM4_CH2 */ + /*<&pwm4 1 PWM_HZ(500) PWM_POLARITY_NORMAL>,*/ /* D1/PB6 → TIM4_CH1 */ <&pwm2 2 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D2/PB3 → TIM2_CH2 */ <&pwm3 3 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D3/PB0 → TIM3_CH3 */ <&pwm1 4 PWM_HZ(500) PWM_POLARITY_NORMAL>, /* D5/PA11 → TIM1_CH4 */ From d9bcd9e525db4b01a26dcc3f9501402fe9a848c7 Mon Sep 17 00:00:00 2001 From: pennam Date: Tue, 3 Mar 2026 14:34:26 +0100 Subject: [PATCH 5/8] core: pinmux: wrap peripheral init and deinit operations with CONFIG ifdefs --- cores/arduino/zephyrCommon.cpp | 11 ++++++++++- cores/arduino/zephyrInternal.h | 2 ++ cores/arduino/zephyrSerial.cpp | 2 ++ cores/arduino/zephyrSerial.h | 2 ++ libraries/SPI/SPI.cpp | 4 ++++ libraries/Wire/Wire.cpp | 4 ++++ 6 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cores/arduino/zephyrCommon.cpp b/cores/arduino/zephyrCommon.cpp index cb3358157..b3562fc95 100644 --- a/cores/arduino/zephyrCommon.cpp +++ b/cores/arduino/zephyrCommon.cpp @@ -7,6 +7,7 @@ #include #include "zephyrInternal.h" +#ifdef CONFIG_PINCTRL_DYNAMIC // create an array of arduino_pins with functions to reinitialize pins if needed static const struct device *pinmux_array[DT_PROP_LEN(DT_PATH(zephyr_user), digital_pin_gpios)] = { nullptr}; @@ -19,6 +20,7 @@ void _reinit_peripheral_if_needed(pin_size_t pin, const struct device *dev) { } } } +#endif static const struct gpio_dt_spec arduino_pins[] = { DT_FOREACH_PROP_ELEM_SEP( @@ -222,7 +224,9 @@ void yield(void) { * A high physical level will be interpreted as value 1 */ void pinMode(pin_size_t pinNumber, PinMode pinMode) { +#ifdef CONFIG_PINCTRL_DYNAMIC _reinit_peripheral_if_needed(pinNumber, NULL); +#endif if (pinMode == INPUT) { // input mode gpio_pin_configure_dt(&arduino_pins[pinNumber], GPIO_INPUT | GPIO_ACTIVE_HIGH); } else if (pinMode == INPUT_PULLUP) { // input with internal pull-up @@ -334,7 +338,9 @@ void analogWrite(pin_size_t pinNumber, int value) { return; } +#ifdef CONFIG_PINCTRL_DYNAMIC _reinit_peripheral_if_needed(pinNumber, arduino_pwm[idx].dev); +#endif value = map(value, 0, 1 << _analog_write_resolution, 0, arduino_pwm[idx].period); if (((uint32_t)value) > arduino_pwm[idx].period) { @@ -360,7 +366,9 @@ void analogWrite(enum dacPins dacName, int value) { // TODO: add reverse map to find pin name from DAC* define // In the meantime, consider A0 == DAC0 +#ifdef CONFIG_PINCTRL_DYNAMIC _reinit_peripheral_if_needed((pin_size_t)(dacName + A0), dac_dev); +#endif dac_channel_setup(dac_dev, &dac_ch_cfg[dacName]); const int max_dac_value = 1U << dac_ch_cfg[dacName].resolution; @@ -411,8 +419,9 @@ int analogRead(pin_size_t pinNumber) { return -ENOTSUP; } +#ifdef CONFIG_PINCTRL_DYNAMIC _reinit_peripheral_if_needed(pinNumber, arduino_adc[idx].dev); - +#endif err = adc_channel_setup(arduino_adc[idx].dev, &arduino_adc[idx].channel_cfg); if (err < 0) { return err; diff --git a/cores/arduino/zephyrInternal.h b/cores/arduino/zephyrInternal.h index e3938e603..30ae255fd 100644 --- a/cores/arduino/zephyrInternal.h +++ b/cores/arduino/zephyrInternal.h @@ -14,7 +14,9 @@ extern "C" { void enableInterrupt(pin_size_t); void disableInterrupt(pin_size_t); +#ifdef CONFIG_PINCTRL_DYNAMIC void _reinit_peripheral_if_needed(pin_size_t pin, const struct device *dev); +#endif #ifdef __cplusplus } // extern "C" diff --git a/cores/arduino/zephyrSerial.cpp b/cores/arduino/zephyrSerial.cpp index a9e0b2951..629149724 100644 --- a/cores/arduino/zephyrSerial.cpp +++ b/cores/arduino/zephyrSerial.cpp @@ -59,7 +59,9 @@ void arduino::ZephyrSerial::begin(unsigned long baud, uint16_t conf) { .flow_ctrl = UART_CFG_FLOW_CTRL_NONE, }; +#ifdef CONFIG_PINCTRL_DYNAMIC uart->ops.init(uart); +#endif uart_configure(uart, &config); uart_irq_callback_user_data_set(uart, arduino::ZephyrSerial::IrqDispatch, this); diff --git a/cores/arduino/zephyrSerial.h b/cores/arduino/zephyrSerial.h index 46ee1b1d7..0f8ba1cd4 100644 --- a/cores/arduino/zephyrSerial.h +++ b/cores/arduino/zephyrSerial.h @@ -75,9 +75,11 @@ class ZephyrSerial : public HardwareSerial { void flush(); void end() { +#ifdef CONFIG_DEVICE_DEINIT_SUPPORT if (uart->ops.deinit) { uart->ops.deinit(uart); } +#endif } size_t write(const uint8_t *buffer, size_t size); diff --git a/libraries/SPI/SPI.cpp b/libraries/SPI/SPI.cpp index 77caa2e7a..66bd363de 100644 --- a/libraries/SPI/SPI.cpp +++ b/libraries/SPI/SPI.cpp @@ -115,13 +115,17 @@ void arduino::ZephyrSPI::detachInterrupt() { } void arduino::ZephyrSPI::begin() { +#ifdef CONFIG_PINCTRL_DYNAMIC spi_dev->ops.init(spi_dev); +#endif } void arduino::ZephyrSPI::end() { +#ifdef CONFIG_DEVICE_DEINIT_SUPPORT if (spi_dev->ops.deinit) { spi_dev->ops.deinit(spi_dev); } +#endif } #if DT_NODE_HAS_PROP(DT_PATH(zephyr_user), spis) diff --git a/libraries/Wire/Wire.cpp b/libraries/Wire/Wire.cpp index d8cc06e5d..c5aceaad7 100644 --- a/libraries/Wire/Wire.cpp +++ b/libraries/Wire/Wire.cpp @@ -54,7 +54,9 @@ arduino::ZephyrI2C::ZephyrI2C(const struct device *i2c) : i2c_dev(i2c), i2c_cfg( } void arduino::ZephyrI2C::begin() { +#ifdef CONFIG_PINCTRL_DYNAMIC i2c_dev->ops.init(i2c_dev); +#endif } void arduino::ZephyrI2C::begin(uint8_t slaveAddr) { @@ -71,9 +73,11 @@ void arduino::ZephyrI2C::end() { i2c_target_unregister(i2c_dev, &i2c_cfg); memset(&i2c_cfg, 0, sizeof(i2c_cfg)); } +#ifdef CONFIG_DEVICE_DEINIT_SUPPORT if (i2c_dev->ops.deinit) { i2c_dev->ops.deinit(i2c_dev); } +#endif } void arduino::ZephyrI2C::setClock(uint32_t freq) { From 5bda4df94924d4af1f4c25efa4e6ae91781fb1a3 Mon Sep 17 00:00:00 2001 From: Luca Burelli Date: Tue, 10 Mar 2026 10:32:37 +0100 Subject: [PATCH 6/8] ci: restore BLE tests on Arduino Uno Q Target is supported by the library so any PR should check this functionality in the CI runs, like on zephyr_main boards. Signed-off-by: Luca Burelli --- extra/artifacts/zephyr_unoq.test_setup.sh | 7 ++++++- variants/arduino_uno_q_stm32u585xx/skip_these_examples.txt | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/extra/artifacts/zephyr_unoq.test_setup.sh b/extra/artifacts/zephyr_unoq.test_setup.sh index 7527e2051..064b14ad1 100644 --- a/extra/artifacts/zephyr_unoq.test_setup.sh +++ b/extra/artifacts/zephyr_unoq.test_setup.sh @@ -13,4 +13,9 @@ # copy artifacts under the provided paths. # ArduinoBLE -get_branch_tip libraries arduino-libraries/ArduinoBLE master +get_branch_tip libraries arduino-libraries/ArduinoBLE master \ + examples/Central/LedControl \ + examples/Central/Scan \ + examples/Peripheral/Advertising/EnhancedAdvertising \ + examples/Peripheral/ButtonLED \ + diff --git a/variants/arduino_uno_q_stm32u585xx/skip_these_examples.txt b/variants/arduino_uno_q_stm32u585xx/skip_these_examples.txt index 22e511274..1ccad6243 100644 --- a/variants/arduino_uno_q_stm32u585xx/skip_these_examples.txt +++ b/variants/arduino_uno_q_stm32u585xx/skip_these_examples.txt @@ -8,5 +8,4 @@ # Each line in this file should contain the path to an example to exclude, # relative to the root of the repository. -libraries/ArduinoBLE libraries/Arduino_RPClite/extras/integration_test From d056449741467e23b70788b698a587eb0e9ece8b Mon Sep 17 00:00:00 2001 From: Luca Burelli Date: Tue, 10 Mar 2026 17:22:31 +0100 Subject: [PATCH 7/8] libraries/SocketWrapper: squash unused argument warning This argument is not used and a proper refactor is still pending, so just mark it as unused for now to avoid warnings. Signed-off-by: Luca Burelli --- libraries/SocketWrapper/SocketHelpers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/SocketWrapper/SocketHelpers.cpp b/libraries/SocketWrapper/SocketHelpers.cpp index 30a8f7b75..d6e1f0c84 100644 --- a/libraries/SocketWrapper/SocketHelpers.cpp +++ b/libraries/SocketWrapper/SocketHelpers.cpp @@ -154,9 +154,12 @@ void NetworkInterface::setMACAddress(const uint8_t *mac) { int NetworkInterface::begin(bool blocking, uint64_t additional_event_mask) { dhcp(); + // FIXME: additional_event_mask cannot be ORed here due to how Zephyr // events are handled internally. Must be reworked to wait on a sem // and register multiple event masks with event_handler instead. + ARG_UNUSED(additional_event_mask); + int ret = net_mgmt_event_wait_on_iface(netif, NET_EVENT_IPV4_ADDR_ADD, NULL, NULL, NULL, blocking ? K_FOREVER : K_SECONDS(1)); return (ret == 0) ? 1 : 0; From b01a1d3276fb09462fdcc05fd27c19576fc260dd Mon Sep 17 00:00:00 2001 From: Luca Burelli Date: Fri, 6 Mar 2026 09:16:32 +0100 Subject: [PATCH 8/8] package_core: add size delta calculation in CI Use the new ci_calc_size_reports.py script to calculate size deltas for PRs, using the report archived at the time base commit of the PR was created as reference. The resulting delta information is added to the JSON report files, and a summary table is printed in the logs. The delta information is also used by the report-size-deltas workflow to post a comment in the PR with the size delta details. Signed-off-by: Luca Burelli --- .github/workflows/package_core.yml | 19 +- .github/workflows/report_size_deltas.yml | 20 ++ extra/ci_calc_size_reports.py | 270 +++++++++++++++++++++++ 3 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/report_size_deltas.yml create mode 100755 extra/ci_calc_size_reports.py diff --git a/.github/workflows/package_core.yml b/.github/workflows/package_core.yml index 0dc5cd6aa..97deb7e2d 100644 --- a/.github/workflows/package_core.yml +++ b/.github/workflows/package_core.yml @@ -407,11 +407,13 @@ jobs: pattern: "*-report-*" merge-multiple: true - - run: | + - name: Generate workflow summary + run: | # gather the array of job metadata (especially name and ID) for the current workflow run export WORKFLOW_JOBS=$(gh run view ${{ github.run_id }} --attempt ${{ github.run_attempt }} --json jobs --jq '.jobs') # Run the log inspection script extra/ci_inspect_logs.py result summary full_log + cat result # Display the summary and full log in the step summary cat summary >> $GITHUB_STEP_SUMMARY @@ -431,6 +433,12 @@ jobs: tar jchf size-reports-${{ needs.build-env.outputs.CORE_HASH }}.tar.bz2 arduino-*.json fi + - name: Compute code size changes + if: ${{ github.event_name == 'pull_request' }} + run: | + export GITHUB_BASE_SHA=$(git describe --always origin/${GITHUB_BASE_REF}) + extra/ci_calc_size_reports.py ${GITHUB_BASE_SHA} sketches-reports/ + # upload comment request artifact (will be retrieved by leave_pr_comment.yml) - name: Archive comment information uses: actions/upload-artifact@v4 @@ -440,6 +448,15 @@ jobs: path: comment-request/ retention-days: 1 + # upload size delta report artifact (will be retrieved by report_size_deltas.yml) + - name: Archive size deltas report information + uses: actions/upload-artifact@v4 + if: ${{ github.event_name == 'pull_request' }} + with: + name: sketches-reports + path: sketches-reports/ + retention-days: 1 + # upload new official test size artifact (for AWS storage) - name: Archive sketch report information uses: actions/upload-artifact@v4 diff --git a/.github/workflows/report_size_deltas.yml b/.github/workflows/report_size_deltas.yml new file mode 100644 index 000000000..adc4abf13 --- /dev/null +++ b/.github/workflows/report_size_deltas.yml @@ -0,0 +1,20 @@ +# Copyright (c) Arduino s.r.l. and/or its affiliated companies +# SPDX-License-Identifier: Apache-2.0 + +name: Report size deltas + +on: + workflow_run: + workflows: ["Package, test and upload core"] + types: + - completed + +permissions: + contents: read + pull-requests: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: arduino/report-size-deltas@v1.1.0 diff --git a/extra/ci_calc_size_reports.py b/extra/ci_calc_size_reports.py new file mode 100755 index 000000000..8bb71e8cc --- /dev/null +++ b/extra/ci_calc_size_reports.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 + +# Copyright (c) Arduino s.r.l. and/or its affiliated companies +# SPDX-License-Identifier: Apache-2.0 + +# Script to analyze CI test logs, download previously generated size reports, +# and calculate size deltas in the official JSON report format. +# +# This scripts expects the following arguments: +# - : SHA of the base report to get, used for delta calculation +# - : directory to populate with the delta reports +# +# The script performs the following actions: +# - downloads the previous report archive from AWS +# - extracts it in the output folder +# - for each JSON report file in the output folder: +# - calculates the delta with the corresponding report in the +# current directory (if it exists and both reports are valid) +# - updates the output report file with the delta information. + +from collections import defaultdict +import copy +import json +import os +from pathlib import Path +import requests +import re +import sys +import tarfile + +def extract_url(url, file_pattern, sha, local_path): + """ + Download and extract a tar.bz2 archive from a URL, trying with truncated + versions of the SHA until a match is found. The archive is expected to + contain size reports in JSON format. The extracted files will be placed in + the specified local path. If no archive is found for the given SHA, a + FileNotFoundError is raised. + """ + + session = requests.Session() + + # try with the input SHA, then with truncated versions + # until we find a match or run out of characters + while len(sha) > 6: + file = file_pattern.format(sha) + archive_url = f"{url}/{file}" + archive_path = Path(local_path) / file + try: + os.makedirs(local_path, exist_ok=True) + response = session.get(archive_url) + if response.status_code == 404: + sha = sha[:-1] + continue + # raise other errors + response.raise_for_status() + with open(archive_path, 'wb') as f: + f.write(response.content) + print(f"Downloaded {archive_url}") + break + except Exception as e: + print(f"Error downloading {url}: {e}") + raise + + if not archive_path.exists(): + raise FileNotFoundError(f"Could not find size archive for SHA {sha}") + + try: + with tarfile.open(archive_path, 'r:bz2') as tar: + tar.extractall(path=local_path) + print(f"Extracted {archive_path} to {local_path}") + except Exception as e: + print(f"Error extracting {archive_path}: {e}") + raise + + os.remove(archive_path) + +def update_final_deltas(final, abs_max, delta): + """ + Update the final delta values for a given entry (e.g. "flash" or "RAM for + global variables") based on a new delta calculation from a sketch. + """ + + if not "maximum" in final: + final["maximum"] = abs_max + final["delta"] = {} + for key in delta: # rel/abs + val = delta[key] + final["delta"][key] = { + "minimum": val, + "maximum": val + } + else: + final["maximum"] = max(final["maximum"], abs_max) + for key in delta: # rel/abs + val = delta[key] + final["delta"][key]["minimum"] = min(final["delta"][key]["minimum"], val) + final["delta"][key]["maximum"] = max(final["delta"][key]["maximum"], val) + +def range_str(delta): + """ + Format a delta value (with "absolute" and "relative" keys) as a string for + display in the summary table. + """ + + if not delta: + return "N/A" + + dmin = delta['absolute']['minimum'] + dmax = delta['absolute']['maximum'] + if not dmin and not dmax: + return "===" + + dmin_str = f"{int(dmin)}" if dmin <= 0 else f"+{int(dmin)}" + dmax_str = f"{int(dmax)}" if dmax <= 0 else f"+{int(dmax)}" + if dmin == dmax: + return dmin_str + else: + return f"{dmin_str}..{dmax_str}" + +if not len(sys.argv) == 3: + print("Usage: ci_size_reports.py ") + sys.exit(1) + +prev_sha = sys.argv[1] +output_folder = sys.argv[2] + +headers = [ "Package", "Board", "Options", "Flash", " RAM ", "Tests", f"vs {prev_sha}" ] +formats = [ "{{:<{}}}", "{{:<{}}}", "{{:<{}}}", "{{:^{}}}", "{{:^{}}}", "{{:>{}}}", "{{:<{}}}" ] +data_lines = [] +col_widths = [ len(header) for header in headers ] + +name_regexp = re.compile(r'(libraries|examples)/([^/]+)/(examples/|extras/)?(.*)') + +# get the previous data to the output folder +extract_url("https://downloads.arduino.cc/cores/zephyr/size-reports", + "size-reports-{}.tar.bz2", prev_sha, output_folder) + +# find every JSON file in the output folder +old_jsons = [] +max_package_len = 0 +max_board_len = 0 +max_mode_len = 0 +for dirent in os.scandir(output_folder): + if dirent.is_file() and dirent.name.endswith(".json"): + package, board, opts = dirent.name[:-5].split('-')[1:4] + old_jsons.append(( package, board, opts, dirent )) + +# calculate deltas and update output files +for package, board, opts, dirent in sorted(old_jsons): + with open(dirent.path, 'r') as f: + old_report_data = json.load(f) + # test if the current file exists + if not os.path.exists(dirent.name): + # no: remove the old report to prevent confusion + os.remove(dirent.path) + continue + with open(dirent.name, 'r') as f: + new_report_data = json.load(f) + + # the output file will be a copy of the new data, plus some fields + output_report_data = copy.deepcopy(new_report_data) + sketch_now_missing = 0 + sketch_was_missing = 0 + updated_sketches = 0 + finals = defaultdict(dict) + + output_sketches = output_report_data['boards'][0]['sketches'] + + group_lines = [] + + for sketch in sorted(output_sketches, key=lambda s: s['name']): + match = name_regexp.search(sketch['name']) + display_name = f"{match.group(2)} {match.group(4)}" if match else sketch['name'] + + sketch_ok = sketch['compilation_success'] + old_sketch = next((s for s in old_report_data['boards'][0]['sketches'] if s['name'] == sketch['name']), None) + old_sketch_ok = old_sketch and old_sketch['compilation_success'] + + if not sketch_ok: + # compile time issue in the new data, skip + if old_sketch_ok: + group_lines.append(( display_name, "-- compile failed" )) + sketch_now_missing += 1 + continue + if not old_sketch_ok: + # compile time issue or missing old data, skip + group_lines.append(( display_name, "-- old data missing" )) + sketch_was_missing += 1 + continue + deltas = {} + for entry in sketch['sizes']: # list, name is "flash" or "RAM for global variables" + key = entry['name'] + old_match = next((e for e in old_sketch['sizes'] if e['name'] == entry['name']), None) + if not old_match: + # major group missing in old data, skip + group_lines.append(( display_name, f"-- old group mismatch" )) + continue + # add "previous" data from the old report + entry['previous'] = old_match['current'] + curr_abs = entry['current']['absolute'] + prev_abs = entry['previous']['absolute'] + if isinstance(curr_abs, str) or isinstance(prev_abs, str): + # "N/A", ignore delta for this entry + continue + deltas[key] = entry['delta'] = { + 'absolute': curr_abs - prev_abs, + 'relative': ((curr_abs - prev_abs) * 100 // prev_abs) / 100 if prev_abs > 0 else "N/A" + } + update_final_deltas(finals[key], entry['maximum'], entry['delta']) + + # update display name in output file + sketch['name'] = display_name + + if not deltas: + group_lines.append(( display_name, "-- no matches" )) + else: + updated_sketches += 1 + ram_delta = deltas["RAM for global variables"]['absolute'] if "RAM for global variables" in deltas else "N/A" + flash_delta = deltas["flash"]['absolute'] if "flash" in deltas else "N/A" + group_lines.append(( display_name, f"{flash_delta:6} {ram_delta:6}" )) + + if group_lines: + print(f"::group::{package} {board} {opts}") + max_name_len = max(len(display_name) for display_name, _ in group_lines) + for display_name, text in group_lines: + print(f"{package} {board} {opts} {display_name:{max_name_len}} {text}") + print(f"::endgroup::") + + # update the board-specific 'sizes' list with the new deltas + output_report_data['boards'][0]['sizes'] = [] + for entry_name, final_values in finals.items(): + output_report_data['boards'][0]['sizes'].append({ + "name": entry_name, + **final_values + }) + + # overwite old file with new data + valid deltas + with open(dirent.path, 'w') as f: + json.dump(output_report_data, f, indent=2) + + # generate a line for the summary table + changes = f"{sketch_now_missing} lost" if sketch_now_missing else "" + changes += ", " if sketch_now_missing and sketch_was_missing else "" + changes += f"{sketch_was_missing} new" if sketch_was_missing else "" + data_line = [ package, board, opts, + range_str(finals["flash"].get('delta')), + range_str(finals["RAM for global variables"].get('delta')), + updated_sketches or '-', + changes ] + + col_widths = [ max(len(str(data_line[i])), col_widths[i]) for i in range(len(data_line)) ] + data_lines.append(data_line) + +# format and output table +format_string = "| " + " | ".join([ formats[i].format(col_widths[i]) for i in range(len(col_widths)) ]) + " |" +spacer_string = "+-" + "-+-".join([ "-"*col_widths[i] for i in range(len(col_widths)) ]) + "-+" + +print(spacer_string) +print(format_string.format(*headers)) + +last_package = None +for data_line in data_lines: + if data_line[0] != last_package: + last_package = data_line[0] + print(spacer_string) + else: + data_line[0] = "" + print(format_string.format(*data_line)) + +print(spacer_string)