nRF52832: how to toggle two synced pulsing GPIO pins between input (high impedance) and output

Description:

I have two GPIO pins whose polarity is toggled via PPI signal. These pins are pulsing in sync: both are simultaneously high, and simultaneously low. I'd like to toggle between these two pins so that only one is in use at a time (output mode) while the other is inactive (input / high impedance). There's always a falling edge following a rising edge and vice versa, no matter which pin is active. The input pin is not supposed to read anything, the main purpose is to be disconnected.

GPIOTE initialization:

static uint8_t gpiote_channel1;
static nrfx_gpiote_pin_t           gpiote_pin1 = DT_GPIO_PIN(DT_NODELABEL(m_gpio1), gpios);
static nrfx_gpiote_output_config_t pin1_out_config;
static nrfx_gpiote_task_config_t   pin1_task_config;

static uint8_t gpiote_channel2;
static nrfx_gpiote_pin_t           gpiote_pin2 = DT_GPIO_PIN(DT_NODELABEL(m_gpio2), gpios);
static nrfx_gpiote_output_config_t pin2_out_config;
static nrfx_gpiote_task_config_t   pin2_task_config;

...
...
...

void gpiote_init() {
    /* pin 1 init */
    
    nrfx_gpiote_channel_alloc(&gpiote_channel1);

    pin1_out_config.drive = NRF_GPIO_PIN_S0S1;
    pin1_out_config.input_connect = NRF_GPIO_PIN_INPUT_DISCONNECT;
    pin1_out_config.pull = NRF_GPIO_PIN_NOPULL;

    pin1_task_config.task_ch = gpiote_channel1;
    pin1_task_config.polarity = NRF_GPIOTE_POLARITY_TOGGLE;
    pin1_task_config.init_val = NRF_GPIOTE_INITIAL_VALUE_LOW;

    nrfx_gpiote_output_configure(gpiote_pin1, &pin1_out_config, &pin1_task_config);
    nrfx_gpiote_out_task_enable(gpiote_pin1);
    
    /* pin 2 init */
    
    nrfx_gpiote_channel_alloc(&gpiote_channel2);

    pin2_out_config.drive = NRF_GPIO_PIN_S0S1;
    pin2_out_config.input_connect = NRF_GPIO_PIN_INPUT_DISCONNECT;
    pin2_out_config.pull = NRF_GPIO_PIN_NOPULL;

    pin2_task_config.task_ch = gpiote_channel2;
    pin2_task_config.polarity = NRF_GPIOTE_POLARITY_TOGGLE;
    pin2_task_config.init_val = NRF_GPIOTE_INITIAL_VALUE_LOW;

    nrfx_gpiote_output_configure(gpiote_pin2, &pin2_out_config, &pin2_task_config);
    nrfx_gpiote_out_task_enable(gpiote_pin2);
}
    

PPI setup:

void mm_ppi_set() 
{
    nrfx_ppi_channel_alloc(&mm_ppi_ch1);

    nrfx_ppi_channel_assign(mm_ppi_ch1, mm_rtc_event(), mm_gpiote_toggle1());
    rfx_ppi_channel_fork_assign(mm_ppi_ch1, mm_gpiote_toggle2());

    err = nrfx_ppi_channel_enable(mm_ppi_ch1);
}

1st approach:

By using nrfx_gpiote.h-library I tried to affect the pin state by using functions nrfx_gpiote_output_configure() and nrfx_gpiote_input_configure(). I never managed to get the expected result, the closest I got was to toggle between the pins, but on the oscilloscope I'd sometimes see two rising edges following each other from different pins, or two falling edges. This would mean that they are no longer in sync. The aforementioned functions had some rules to apply to ensure the correct behaviour, but either I missed something or this is not the way to go.

Input configuration:

void callback(void)
{
...
...
    nrfx_gpiote_input_config_t p_input_config = {
        .pull = NRF_GPIO_PIN_NOPULL,
    };
    nrfx_gpiote_trigger_config_t p_trigger_config = {
        .trigger = NRFX_GPIOTE_TRIGGER_NONE,
        .p_in_channel = NULL,                 // <--- if NULL, the sensing mechanism is used.
    };
    nrfx_gpiote_pin_uninit(nonactive);  // <--- without this the input configuration returns 'NRFX_ERROR_INVALID_PARAM'
    nrf_gpiote_task_configure(NRF_GPIOTE, nonactive_task->task_ch, nonactive,
                              NRF_GPIOTE_POLARITY_NONE, NRF_GPIOTE_INITIAL_VALUE_LOW);  // another way to set POLARITY_NONE?
    nrfx_gpiote_input_configure(nonactive, &p_input_config, &p_trigger_config, NULL);
...
...
}

I understand that the p_input_config cannot be NULL, if I want to configure the pin from output to input. For this I need to disable the task by configuring NRF_GPIOTE_POLARITY_NONE. There is a mention of this in the comments of the code snippet. Also configuring trigger shouldn't be done, but instead use sensing. This is done by setting .p_in_channel to NULL, correct?

Output configuration:

void callback(void)
{
...
...
    //nrfx_gpiote_trigger_disable(active);  <--- is this needed if the trigger was configured as NONE?
    nrfx_gpiote_output_configure(active, &pin1_out_config, active_task);  <--- active task points to the init task conf of the active pin
    nrfx_gpiote_out_task_enable(active);
...
...
}

With the pin reconfiguration to output I'm a bit lost: how should I go about it?

1st conclusion:

The pins remain on low state (or high-z, I wasn't able to measure the exact state) and doesn't produce any rising edges, so this approach is either missing something or is definitely wrong. It is still the theory on what I should do.

2nd approach:

I write directly on the pin configuration drive registers. I understand that this way I can hold on to the output state, which doesn't cause any accidental misbehaviour. It would also explain why NRF_GPIOTE_POLARITY_TOGGLE in the configuration now works flawlessly. S0S1 means "standard'0', standard '1'" and S0D1 is "standard '0', disconnect '1'".

void callback(void)
{
...
...
    NRF_GPIO->PIN_CNF[nonactive] = (GPIO_PIN_CNF_DRIVE_S0D1 << GPIO_PIN_CNF_DRIVE_Pos);
    NRF_GPIO->PIN_CNF[active]    = (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos);
...
...
}

2nd conclusion:

The oscilloscope shows that the pins are switching between each other, and are in sync. This is exactly the behaviour I was after. However the deactivated pin is disconnected only half the time. It's either grounded (0) or disconnected (1), but instead I'd like to have it disconnected altogether. Since there is no D0D1-option, I'm not all the way there with my goal.

So now I'd like to hear your opinions and suggestions on how to improve the codes and ideas I share here, or maybe you have an alternative solution to this issue.

Thank you for your help, and let me know if I can provide you with some information I overlooked so far.

  • Hi,

    Have you tried to set the PIN_CNF->DIR to "Input" and PIN_CNF->INPUT to "Disconnect" for the nonactive pin?

    Best regards,
    Jørgen

  • Hi,

    thank you for the suggestion.

    I tried as such:

    NRF_GPIO->PIN_CNF[nonactive] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos);
    NRF_GPIO->PIN_CNF[nonactive] = (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos);
    //NRF_GPIO->PIN_CNF[active] = (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);

    I tried both with and without the output configuration, hoping to see any kind of effect like a flatline. However these configurations seem to do nothing as is. Might this be the reason (from the documents):

    ...

    As long as a SET[n], CLR[n] and OUT[n] task or an IN[n] event is configured to control a pin n, the pin's output value will only be updated by the GPIOTE module. The pin's output value as specified in the GPIO will therefore be ignored as long as the pin is controlled by GPIOTE. Attempting to write a pin as a normal GPIO pin will have no effect. When the GPIOTE is disconnected from a pin, see MODE field in CONFIG[n] register, the associated pin will get the output and configuration values specified in the GPIO module.

    ...

    Or maybe I have other registers to toggle that I have overlooked? I haven't worked like this with registers too much yet.

    There is a mention of disconnecting GPIOTE from the GPIO before writing. Would this be a solution? I don't know how to do this other than using something like nrfx_gpiote_pin_uninit(). This seems to lead a reconfiguration cycle, since I would need to configure the pin as output everytime a switch is done. This seems a bit messy way to do it.

    Do you have any further thoughts or suggestions?

  • Hi,

    The PIN_CFG will not hold the previous value is you write a new config. If you want to set multiple fields, you either need to write the second value with "|=", or bitwise or the values before assigning:

    NRF_GPIO->PIN_CNF[nonactive] =  (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos);
    NRF_GPIO->PIN_CNF[nonactive] |= (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos);
    
    // OR:
    NRF_GPIO->PIN_CNF[nonactive] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) |
                                   (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos);
    In this particular case, it should not matter though, as DIR_Input is the reset value (0) of the field in this register.

    ISH said:
    hoping to see any kind of effect like a flatline.

    What do you mean by this? If the pin is configured as high impedance, the pin would be floating, not tied to zero.

    ISH said:

    Might this be the reason (from the documents):

    ...

    As long as a SET[n], CLR[n] and OUT[n] task or an IN[n] event is configured to control a pin n, the pin's output value will only be updated by the GPIOTE module. The pin's output value as specified in the GPIO will therefore be ignored as long as the pin is controlled by GPIOTE. Attempting to write a pin as a normal GPIO pin will have no effect. When the GPIOTE is disconnected from a pin, see MODE field in CONFIG[n] register, the associated pin will get the output and configuration values specified in the GPIO module.

    As far as I know, the GPIO configuration (drive strength, pull resistors, etc) should still be applied to the pin, even though it is controlled by the GPIOTE, you will just not be able to control the value of the pin. GPIO configuration functions are used by the GPIOTE driver to set configs like initial output value, and pull resistor configs.

    ISH said:
    There is a mention of disconnecting GPIOTE from the GPIO before writing. Would this be a solution? I don't know how to do this other than using something like nrfx_gpiote_pin_uninit(). This seems to lead a reconfiguration cycle, since I would need to configure the pin as output everytime a switch is done. This seems a bit messy way to do it.

    I think the most suitable API that the driver provides would be nrfx_gpiote_out_uninit(). It would also technically be possible to clear the PSEL field of the GPIOTE->CONFIG[n] register, but then you might cause issues for the driver if you do not restore it properly.

    Best regards,
    Jørgen

  • Hi,

    sorry for the late response. I've been trying everything I could come up with based on your suggestions. Unfortunately I still haven't been able to solve this issue. Reading back to the original post I feel like I haven't been clear enough with the program structure, so please let me try to elaborate.

    In the PPI-module the RTC event that triggers the GPIOTE TOGGLE task also triggers an SAADC task that reads a value. This triggers a callback function, which evaluates the result and calls the GPIOTE function that is responsible for the switching between the direction of these two pins. So this switching happens in the CPU.

    The goal is that the POLARITY.TOGGLE register would change the pin's polarity even though the pin itself is an input. And the effect would take place once the pin is switched back to output direction.

    To make the demonstrations easier to understand, I'll try to toggle only one pin instead of two like in the OP. This is what I've tried in the callback function since your last post:

    Direct register writing to GPIO pin configuration:

    static nrfx_gpiote_pin_t pin = DT_GPIO_PIN(DT_NODELABEL(mm_gpio0), gpios);
    
    void callback(bool set_in)
    {
        static NRF_GPIO_Type *reg = nrf_gpio_pin_port_decode(&pin);
        
        if (set_in) {
            reg->PIN_CFG[pin] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) |
                                (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos) |
                                (GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos) |
                                (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos) |
                                (GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos);
        } else {
            reg->PIN_CFG[pin] = (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos) |
                                (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos) |
                                (GPIO_PIN_CNF_PULL_Disabled << GPIO_PIN_CNF_PULL_Pos) |
                                (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos) |
                                (GPIO_PIN_CNF_SENSE_Disabled << GPIO_PIN_CNF_SENSE_Pos);
        }
    }

    or more dynamic way:

    static nrfx_gpiote_pin_t pin = DT_GPIO_PIN(DT_NODELABEL(mm_gpio0), gpios);
    
    void callback(bool set_in)
    {
        NRF_GPIO_Type *reg = nrf_gpio_pin_port_decode(&pin);
        uint32_t cur_cfg = reg->PIN_CNF[pin];           // get pin's configuration
        cur_cfg &= ~(GPIO_PIN_CNF_DIR_Msk);             // clear direction register
        
        if (set_in) {
            cur_cfg |= (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos);
        } else {
            cur_cfg |= (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos);
        }
        
        reg->PIN_CNF[pin] = cur_cfg;
    }

    I also tried to affect the GPIOTE module registers, specifically CONFIG[n].MODE. The documentation says that if the mode is switched to Event, it switches the GPIO's PIN_CNF[n].DIR to Input, MODE.Task switches the DIR to Output and MODE.Disabled enables the CNF[n] of the GPIO pin, disconnecting the GPIOTE module from the pin.

    static uint8_t ch;    // this has been allocated by using nrfx_gpiote_channel_alloc(&ch)
    
    void callback(bool set_in)
    {
        uin32_t cur_cfg;
        cur_cfg = NRF_GPIOTE->CONFIG[ch];
        cur_cfg &= ~(GPIOTE_CONFIG_MODE_Msk);
        
        if (set_in) {
            cur_cfg |= (GPIOTE_CONFIG_MODE_Disabled << GPIOTE_CONFIG_MODE_Pos);
            
            // Also tried this
            //cur_cfg |= (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos);
        } else {
            cur_cfg |= (GPIOTE_CONFIG_MODE_Task << GPIOTE_CONFIG_MODE_Pos);
        }
    
        NRF_GPIOTE->CONFIG[ch] = cur_cfg;
    }

    nrfx_gpiote.h library functions:

    void callback(bool set_in)
    {
        if (set_in) {
            nrfx_gpiote_out_task_disable(pin);
        } else {
            nrfx_gpiote_out_task_enable(pin);
        }
    }

    None of these methods worked. The result was a flatline, the pin is not set. I hope I managed to explain the situation better, but if not, please let me know if I could provide some more information. Any suggestions on how to try to solve the issue?

    Thank you for your help.

    EDIT: I read the GPIO register after changing the GPIOTE MODE, and the register content didn't change. I don't know if I should see the changes in DIR? But also at the same time writing to GPIO's DIR does change the register values, but doesn't show on the oscilloscope as pulsing. The pin is still flatlining.

  • Hi,

    good news! I reached my goal and got the pins' directions to change. It's a bit embarrassing to admit, but the cause of the problem was the way I triggered the callback. Basically, the way I got to test the high-z was to apply a 2-divisible cycle to ensure the pulse had both rising and falling edge. Although this is not the situation I want to end up with, it's an issue I have to solve later. The original problem with the pins' direction is solved. Here's the callback function that did the trick:

    void callback(bool ok)
    {
    ...
    ...
        nrf_gpiote_task_disable(NRF_GPIOTE, ch_inactive);     // this releases the GPIOTE from the GPIO
        nrf_gpio_cfg_input(p_inactive, NRF_GPIO_PIN_NOPULL);  // now we can adjust the GPIO's PIN_CNF
        nrf_gpio_cfg_output(p_active);
        nrf_gpiote_task_enable(NRF_GPIOTE, ch_active);
    ...
    ...
    }

Related