UART PINS REMAPPING AT RUN TIME

Hello,

I have multiple UART devices connected to various pins on my nRF5340, but due to hardware limitations, I can only use two UARTs concurrently alongside interfaces like SPI and I2C. To address this constraint, I need the ability to dynamically switch or manipulate the UART RX/TX pins during runtime. I'm open to temporarily disabling a UART, reconfiguring its pins for a different device, and then re-enabling it. For example, I'd like to switch UART2 pins to accommodate another device when needed. This capability is crucial for my project, as the default Zephyr framework doesn't support it, necessitating the implementation of a custom solution, potentially involving creative workarounds.

Parents
  • I know this is an old thread but it might be easiest to just change the  PSEL.TXD, and  PSEL.RXD registers for the specific UART in the HW versus the other suggested methods. You can have your thread change this and wait a certain amount of time for any current TX transactions to finish (assuming you don't need very tightly timed switch over). Or see this about using pin control groups and dynamically selecting at runtime Pin Control — Zephyr Project Documentation

  • Hi, 

    I encountered the same use case for my application where everything must be reconfigure at runtime and we try not to lose the functionnalities offered by Zephyr Services built on top of the driver api. I found a solution based on the use of Device Runtime Power Management and the pin switching is done via pincontroller API.

    My configuration : ncs 2.7.0

    This solution works only for UART driver, with the <zephyr/drivers/serial/uart_nrfx_uarte.c> at the end, as far as I know.

    What we need to do, in a nutshell :

    • disable/deinitialise the uart driver ie. uarte peripheral and interruptions basically
    • retreive the pinctrl config of interest
    • apply the new pinctrl config
    • enable/reinitialise the uart driver

    From the application perspective, there is no way to deinitialise the uart driver directly. But looking at the <zephyr/drivers/serial/uart_nrfx_uarte.c> PM action callback implementation, I found something really interesting :

    You can configure the driver not to handle the automatic pin initialisation when switching power state using Kconfig option CONFIG_UART_n_GPIO_MANAGEMENT (n is the uart instance id). Given that, we are able to deinitialise/reinitialise the uart driver properly using Device Runtime Power Management.

    prj.conf (fragment):

    CONFIG_PM=y
    CONFIG_PM_DEVICE=y
    CONFIG_PM_DEVICE_RUNTIME=y
    
    CONFIG_UART_0_GPIO_MANAGEMENT=n # UARTE0 pins must be controlled independantly because the peripheral is used for multiple devices.
    
    CONFIG_PINCTRL_DYNAMIC=y

    Now let's configure the pins !

    I use the empty pinctrl-N slots to add my other pin configurations in the devicetree.

    &uart0 {
        status = "okay";
    
        pinctrl-0 = <&uart0_default>;
        pinctrl-1 = <&uart0_sleep>;
        pinctrl-2 = <&uart0_gsm>;
        pinctrl-3 = <&uart0_gnss>;
        pinctrl-names = "default","sleep","gsm","gnss";
    };

    Although "default" state refers PINCTRL_STATE_DEFAULT and "sleep" state refers PINCTRL_STATE_SLEEP as expected, "gsm" and "gnss" states are custom states whose corresponding macro names
    PINCTRL_STATE_GSM and PINCTRL_STATE_GNSS will be generated at build time.
    These names are unknown from the uart driver's perspective : at this point we hit serious compilations problem in case we try to build the project.
    The first option I thought about was modifying the uart_nrfx_uarte.c source code, adding my new pinctrl states definitions on the top of the file like we can see in other drivers. Easy but not sexy.
    So I looked for another option more "project specific", end I ended up providing these macro definitions to the compiler via my project's CMakeLists.txt, using Zephyr's cmake extention 
    zephyr_compile_definitions().
    # SPDX-License-Identifier: Apache-2.0
    
    cmake_minimum_required(VERSION 3.20.0)
    find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
    project(custom_dts_binding)
    
    # This is a convenience hack : we need it to inform the uart driver that these states exists, without
    # having to modify the uart driver itself.
    zephyr_compile_definitions(PINCTRL_STATE_GSM=2 PINCTRL_STATE_GNSS=3)
    
    FILE(GLOB app_sources src/*.c)
    target_sources(app PRIVATE ${app_sources})
      
    Now gcc shouldn't complain anymore (at least about unknown pinctlr states).
    In the application code, the dynamic switch is handled like this:
    /**
     * @brief Initialize runtime power management for uart0
     *
     * @return int
     */
    static int switch_init__PM_METHOD(void)
    {
    	int err;
    	err = pm_device_runtime_enable(p_uarte0_dev);
    	if (err != 0)
    	{
    		printf("The PM device runtime is not ready.\n");
    		return err;
    	}
    
    	err = pm_device_runtime_get(p_uarte0_dev);
    	if (err != 0)
    	{
    		printf("The PM device still suspended.\n");
    		return err;
    	}
    	return 0;
    }
    
    /**
     * @brief Switch uart0 pins dynamically to gsm pins
     *
     * @return int
     */
    static int switch_uarte0_to_gsm__PM_METHOD(void)
    {
    	int ret;
    
    	// The device (UARTE0) must be configured with CONFIG_UART_0_GPIO_MANAGEMENT disabled,
    	// otherwise, this call ant further pm_device_runtime_get() will set pinctrl depending on SLEEP and DEFAULT states.
    	ret = pm_device_runtime_put(p_uarte0_dev);
    	if (ret < 0)
    	{
    		return ret;
    	}
    
    	// Now we fetch the pin config of interest.
    	const struct pinctrl_dev_config *p_config = PINCTRL_DT_DEV_CONFIG_GET(UARTE_NODE);
    	ret = pinctrl_apply_state(p_config, PINCTRL_STATE_GSM);
    	if (ret < 0)
    	{
    		return ret;
    	}
    
    	// And we can resume our device through PM, which won't
    	// touch pinctrl.
    	ret = pm_device_runtime_get(p_uarte0_dev);
    	if (ret < 0)
    	{
    		return ret;
    	}
    
    	printf("Switched UARTE0 to GSM\n");
    	return 0;
    }

    All the best,
    Hugo
Reply
  • Hi, 

    I encountered the same use case for my application where everything must be reconfigure at runtime and we try not to lose the functionnalities offered by Zephyr Services built on top of the driver api. I found a solution based on the use of Device Runtime Power Management and the pin switching is done via pincontroller API.

    My configuration : ncs 2.7.0

    This solution works only for UART driver, with the <zephyr/drivers/serial/uart_nrfx_uarte.c> at the end, as far as I know.

    What we need to do, in a nutshell :

    • disable/deinitialise the uart driver ie. uarte peripheral and interruptions basically
    • retreive the pinctrl config of interest
    • apply the new pinctrl config
    • enable/reinitialise the uart driver

    From the application perspective, there is no way to deinitialise the uart driver directly. But looking at the <zephyr/drivers/serial/uart_nrfx_uarte.c> PM action callback implementation, I found something really interesting :

    You can configure the driver not to handle the automatic pin initialisation when switching power state using Kconfig option CONFIG_UART_n_GPIO_MANAGEMENT (n is the uart instance id). Given that, we are able to deinitialise/reinitialise the uart driver properly using Device Runtime Power Management.

    prj.conf (fragment):

    CONFIG_PM=y
    CONFIG_PM_DEVICE=y
    CONFIG_PM_DEVICE_RUNTIME=y
    
    CONFIG_UART_0_GPIO_MANAGEMENT=n # UARTE0 pins must be controlled independantly because the peripheral is used for multiple devices.
    
    CONFIG_PINCTRL_DYNAMIC=y

    Now let's configure the pins !

    I use the empty pinctrl-N slots to add my other pin configurations in the devicetree.

    &uart0 {
        status = "okay";
    
        pinctrl-0 = <&uart0_default>;
        pinctrl-1 = <&uart0_sleep>;
        pinctrl-2 = <&uart0_gsm>;
        pinctrl-3 = <&uart0_gnss>;
        pinctrl-names = "default","sleep","gsm","gnss";
    };

    Although "default" state refers PINCTRL_STATE_DEFAULT and "sleep" state refers PINCTRL_STATE_SLEEP as expected, "gsm" and "gnss" states are custom states whose corresponding macro names
    PINCTRL_STATE_GSM and PINCTRL_STATE_GNSS will be generated at build time.
    These names are unknown from the uart driver's perspective : at this point we hit serious compilations problem in case we try to build the project.
    The first option I thought about was modifying the uart_nrfx_uarte.c source code, adding my new pinctrl states definitions on the top of the file like we can see in other drivers. Easy but not sexy.
    So I looked for another option more "project specific", end I ended up providing these macro definitions to the compiler via my project's CMakeLists.txt, using Zephyr's cmake extention 
    zephyr_compile_definitions().
    # SPDX-License-Identifier: Apache-2.0
    
    cmake_minimum_required(VERSION 3.20.0)
    find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
    project(custom_dts_binding)
    
    # This is a convenience hack : we need it to inform the uart driver that these states exists, without
    # having to modify the uart driver itself.
    zephyr_compile_definitions(PINCTRL_STATE_GSM=2 PINCTRL_STATE_GNSS=3)
    
    FILE(GLOB app_sources src/*.c)
    target_sources(app PRIVATE ${app_sources})
      
    Now gcc shouldn't complain anymore (at least about unknown pinctlr states).
    In the application code, the dynamic switch is handled like this:
    /**
     * @brief Initialize runtime power management for uart0
     *
     * @return int
     */
    static int switch_init__PM_METHOD(void)
    {
    	int err;
    	err = pm_device_runtime_enable(p_uarte0_dev);
    	if (err != 0)
    	{
    		printf("The PM device runtime is not ready.\n");
    		return err;
    	}
    
    	err = pm_device_runtime_get(p_uarte0_dev);
    	if (err != 0)
    	{
    		printf("The PM device still suspended.\n");
    		return err;
    	}
    	return 0;
    }
    
    /**
     * @brief Switch uart0 pins dynamically to gsm pins
     *
     * @return int
     */
    static int switch_uarte0_to_gsm__PM_METHOD(void)
    {
    	int ret;
    
    	// The device (UARTE0) must be configured with CONFIG_UART_0_GPIO_MANAGEMENT disabled,
    	// otherwise, this call ant further pm_device_runtime_get() will set pinctrl depending on SLEEP and DEFAULT states.
    	ret = pm_device_runtime_put(p_uarte0_dev);
    	if (ret < 0)
    	{
    		return ret;
    	}
    
    	// Now we fetch the pin config of interest.
    	const struct pinctrl_dev_config *p_config = PINCTRL_DT_DEV_CONFIG_GET(UARTE_NODE);
    	ret = pinctrl_apply_state(p_config, PINCTRL_STATE_GSM);
    	if (ret < 0)
    	{
    		return ret;
    	}
    
    	// And we can resume our device through PM, which won't
    	// touch pinctrl.
    	ret = pm_device_runtime_get(p_uarte0_dev);
    	if (ret < 0)
    	{
    		return ret;
    	}
    
    	printf("Switched UARTE0 to GSM\n");
    	return 0;
    }

    All the best,
    Hugo
Children
No Data
Related