ESP32 - does setting pin mode to output clear pull-ups?

On the ESP32, when the pins are set to input_pullup and then switched to output, does this disable the pull-ups? I am struggling to find a definitive answer and I have seen comments saying that is doesn't but others saying that it does.

I was also told that there is no need to turn pull-ups on or off and that these can be set at the start and just left. But then how does the pin go LOW when a pull-up is enabled?

I am trying to understand the behaviour of pins on the ESP as they seem to be somewhat different to the AVR platform, especially when using ESP native functions.

When the pin is set LOW, I imagine this would easily defeat the effect of the internal pull-up.

Have you tested to find out? Just use a multimeter and a 1K resistor for example.

In this case, it would have to defeat the pull-up at the other end of the wire as well as the signal is high by default and pulled low to assert the signal. If it can do that, then I guess this answers the question. Thank you.

The other thing, if anyone knows where to find it, is the code for pinMode() for the ESP. I have dug around in the ESP32 core for Arduino on Github, but although I found two declarations, one of which was extern, I couldn't find any code for the actual function to examine how it works.

Possible way to check experimentally:

Enable pullups on all outputs that support them, set pins to OUTPUT and LOW. Measure total current.

Repeat with pullups disabled.

If current differs, pullups remain enabled.

An active LOW trumps a passive ~45K resistor to Vcc.

I think this is the critical section in the core
arduino-esp32/cores/esp32 /esp32-hal-gpio.c

You can see where all the settings are disabled before being set

gpio_config_t conf = {
    .pin_bit_mask = (1ULL << pin),         /*!< GPIO pin: set with bit mask, each bit maps to a GPIO */
    .mode = GPIO_MODE_DISABLE,             /*!< GPIO mode: set input/output mode                     */
    .pull_up_en = GPIO_PULLUP_DISABLE,     /*!< GPIO pull-up                                         */
    .pull_down_en = GPIO_PULLDOWN_DISABLE, /*!< GPIO pull-down                                       */
extern void ARDUINO_ISR_ATTR __pinMode(uint8_t pin, uint8_t mode) 
{
#ifdef RGB_BUILTIN
  if (pin == RGB_BUILTIN) {
    __pinMode(RGB_BUILTIN - SOC_GPIO_PIN_COUNT, mode);
    return;
  }
#endif

  if (pin >= SOC_GPIO_PIN_COUNT) {
    log_e("Invalid IO %i selected", pin);
    return;
  }

  if (perimanGetPinBus(pin, ESP32_BUS_TYPE_GPIO) == NULL) {
    perimanSetBusDeinit(ESP32_BUS_TYPE_GPIO, gpioDetachBus);
    if (!perimanClearPinBus(pin)) {
      log_e("Deinit of previous bus from IO %i failed", pin);
      return;
    }
  }

  gpio_hal_context_t gpiohal;
  gpiohal.dev = GPIO_LL_GET_HW(GPIO_PORT_0);

  gpio_config_t conf = {
    .pin_bit_mask = (1ULL << pin),         /*!< GPIO pin: set with bit mask, each bit maps to a GPIO */
    .mode = GPIO_MODE_DISABLE,             /*!< GPIO mode: set input/output mode                     */
    .pull_up_en = GPIO_PULLUP_DISABLE,     /*!< GPIO pull-up                                         */
    .pull_down_en = GPIO_PULLDOWN_DISABLE, /*!< GPIO pull-down                                       */
#ifndef CONFIG_IDF_TARGET_ESP32C61
    .intr_type = gpiohal.dev->pin[pin].int_type /*!< GPIO interrupt type - previously set                 */
#else
    .intr_type = gpiohal.dev->pinn[pin].pinn_int_type /*!< GPIO interrupt type - previously set                 */
#endif
  };
  if (mode < 0x20) {  //io
    conf.mode = mode & (INPUT | OUTPUT);
    if (mode & OPEN_DRAIN) {
      conf.mode |= GPIO_MODE_DEF_OD;
    }
    if (mode & PULLUP) {
      conf.pull_up_en = GPIO_PULLUP_ENABLE;
    }
    if (mode & PULLDOWN) {
      conf.pull_down_en = GPIO_PULLDOWN_ENABLE;
    }
  }
  if (gpio_config(&conf) != ESP_OK) {
    log_e("IO %i config failed", pin);
    return;
  }
  if (perimanGetPinBus(pin, ESP32_BUS_TYPE_GPIO) == NULL) {
    if (!perimanSetPinBus(pin, ESP32_BUS_TYPE_GPIO, (void *)(pin + 1), -1, -1)) {
      //gpioDetachBus((void *)(pin+1));
      return;
    }
  }
}

I will take a SWAG and say that does not really matter. ESP32 GPIO pins can both source and sink current. They can source up to 40mA and sink up to 28mA which will more then swamp any pull up/down resistor. When you put the GPIO in output mode you enable the drivers and the output will then follow the input High/Low from the ESP32 core.

Thank you. Some interesting stuff going on here. Seemingly not a lot more than what I am already doing but I don't yet understand the periman bit.

Do you still have the full link? The above does not work and I couldn't find that path on the arduino-esp32 github?

I rather suspect that as well. For most part what I am trying to emulate needs to be driven - and it is anyway on an AVR - so I am not using the _OUTPUT_OD mode, just normal OUTPUT.

https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/esp32-hal-gpio.c

Great. Thank you!

I think I follow that because if the pull-ups are enabled they will draw some current so there will be a difference. Will give it a try.

That depends on what the pin is connected to. If it is at Vcc or open it will draw some leakage probably in the pico amp range at most. If the pin is at some other voltage it will draw current depending on the voltage difference and the resistance value.

you can check the pin configuration with code like below
(yes, output floats the pin)

#include <soc/gpio_struct.h>
#include <hal/gpio_ll.h>

void pinConfig(gpio_num_t pinNo) {
  gpio_io_config_t gpioConfig;

  gpio_get_io_config(pinNo, &gpioConfig);
  Serial.println("--------------");
  Serial.print("pin");
  Serial.print(pinNo);
  Serial.println(":");
  Serial.print("  input ");
  Serial.println(gpioConfig.ie);
  Serial.print("  output ");
  Serial.println(gpioConfig.oe);
  Serial.print("  output open drain ");
  Serial.println(gpioConfig.od);
  Serial.print("  pullup ");
  Serial.println(gpioConfig.pu);
  Serial.print("  pulldown ");
  Serial.println(gpioConfig.pd);
  Serial.println("--------------");
}

void setup() {
  Serial.begin(9600);
  delay(2000);
  Serial.println(F("\n Start"));

  pinMode(GPIO_NUM_5, INPUT);
  pinConfig(GPIO_NUM_5);

  pinMode(GPIO_NUM_5, INPUT_PULLUP);
  pinConfig(GPIO_NUM_5);

  pinMode(GPIO_NUM_5, OUTPUT);
  pinConfig(GPIO_NUM_5);

  pinMode(GPIO_NUM_5, OUTPUT_OPEN_DRAIN);
  pinConfig(GPIO_NUM_5);

  // just checking :)
  pinMode(GPIO_NUM_5, INPUT);
  pinConfig(GPIO_NUM_5);

  Serial.println(F("\n End"));
}

void loop() {
}

serial terminal:
 Start
--------------
pin5:
  input 1
  output 0
  output open drain 0
  pullup 0
  pulldown 0
--------------
--------------
pin5:
  input 1
  output 0
  output open drain 0
  pullup 1
  pulldown 0
--------------
--------------
pin5:
  input 1
  output 1
  output open drain 0
  pullup 0
  pulldown 0
--------------
--------------
pin5:
  input 1
  output 1
  output open drain 1
  pullup 0
  pulldown 0
--------------
--------------
pin5:
  input 1
  output 0
  output open drain 0
  pullup 0
  pulldown 0
--------------

 End

Thank you for that. There is the gpio_dump_io_configuration() but unnfortunately is is designed to stream to stdio which is a concept hat does not exist on the Arduino so I couldn't use that. I hadn't considered using gpio_io_config_t. I added your function to my test sketch.

So at the start when pins are set to input_pullup it returns:

--------------
pin 32:
  input 1
  output 0
  output open drain 0
  pullup 1
  pulldown 0
--------------

So far that looks as might be expected. The pin is in input mode with pull-up enabled. Next, when the pins are switched to output using pinMode() I get:

--------------
pin 32:
  input 1
  output 1
  output open drain 0
  pullup 0
  pulldown 0
--------------

That is starting to look a little strange. The pin is in BOTH input and output mode at the same time.

For the final test switching using registers the result is:

--------------
pin 32:
  input 0
  output 1
  output open drain 0
  pullup 0
  pulldown 0
--------------

Writing to the register switches on output as expected but this time input is disabled. Also to note is that regardless of whether pinMode() or writing to registers is used for switching to output mode, the pull-ups evidently do get disabled.

I am showing the result for just one pin here, but all 8 switched pins behaved exactly the same.

So does this mean that pinMode() is actually switching to input_output mode? I reversed the order of the two output tests and got the same result, so its not just a case of pinMode() not clearing the input setting. It is actively setting both input and output modes on.

I am unsure at this point but it doesn't seem possible to turn on input_output mode by writing to GPIO_ENABLE_W1TS_REG or GPIO_ENABLE_W1TC_REG because either a bit is set or its cleared. However, it can be done using the gpio_io_config_t object.

I added another couple of tests doing just that the result can then be correctly read with digitalRead(). It now shows the output pin bits turned on rather than all zeroes.

I was surprised myself as well but apparently that's the case
I'm not sure what the consequences are.
I too am using registers if I want pure "output".

on second thought, it makes sense in a way since pinmode() doesn't have an INPUT_OUTPUT mode.


https://docs.arduino.cc/language-reference/en/functions/digital-io/pinMode/

It depends on whether or not you are using the esp32 Arduino core which does not support that mode, or the esp32 idf which does.

Within the gpio_config_t structure of the esp32 idf you find this

GPIO_MODE_DISABLE, GPIO_MODE_INPUT, GPIO_MODE_OUTPUT, GPIO_MODE_OUTPUT_OD, GPIO_MODE_INPUT_OUTPUT_OD, GPIO_MODE_INPUT_OUTPUT

The INPUT_OUTPUT mode can be use for reading a pulsed output with ledc on the same pins with PCNT.

https://esp32.com/viewtopic.php?t=18115

The Arduino Reference pages are written with the ATmega328 in mind. Other products are largely ignored.
Example: in the Nano Every the statement digitalWrite(pin, CHANGE); is legal, it toggles the output. I haven't found this in the Reference.
I can't but assume this goes for OP's board as well, probably worse.

indeed we're on the Arduino forum
the Esp32 idf is clearly more finely grained but also more difficult to get started with

that seems to be the case
for ESP32 there's a mode not listed in the docs that actually works

  pinMode(GPIO_NUM_5, OUTPUT_OPEN_DRAIN);

Indeed. The Arduino core does not provide a simultaneous INPUT_OUTPUT mode. Its exclusively either INPUT or OUTPUT. Might that be down to the inner workings of the older ATMega AVR architecture?

I went back to my original project and tried setting pins to INPUT_OUTPUT (no _OD) to emulate pinMode() where output mode was required and it finally worked. I had previously tried INPUT_OUTPUT_OD mode but that didn't work. As it turns out, this is no surprise since although it might have been possible to get away with open drain mode for the data bus, the control bus pins needed to be driven. At that point I had dismissed INPUT_OUTPUT mode altogether and concentrated on pure OUTPUT but couldn't get that to work correctly either.

Exactly. I was seeing a different behaviour between when using functions from the Arduino core and when using native ESP functions like gpio_config(). I was looking to understand the reason for this difference in order to figure out the problem with my project. Arduino uses the ESP functions to mimic what it does on an AVR, but it is now evident that pinMode(pin, OUTPUT) behaves differently to the native GPIO_MODE_OUTPUT or REG_WRITE(GPIO_ENABLE_W1TS_REG) function in the ESP core and as a result digitalRead() also behaves differently under certain circumstances.

I have not come across either of those before, but I suspect that they are based on features that are actually available within the design of the platform architecture. Could it be that the 4809 provides a means to toggle a pin state in its architecture but the 328P requires an explicit state to be set? The ESP32 does support open drain in its architecture whereas the 328P does not. I wonder whether that OUTPUT_OPEN_DRAIN state parameter would also work on the UNO/Nano R4 since the Renesas RA4M1 supports open drain output on its GPIO pins?

Working at bare metal level certainly does have its challenges. In line with their beginner friendly approach, Arduino conceals most of this low level stuff going on under the hood. However, with an improved understanding of what is actually going on there thanks to these comments, I now have my project working as intended. All pulses of the my "data bus" are now nicely aligned on the LA trace rather than having the previous staggered "staircase" appearance when they were being switched using a loop with pinMode(). There is only a slight, not very significant speed improvement but I can live with that for now.

It's even worse: the 328 chip allows toggling the output by writing to the Input Register, but the Arduino Language doesn’t support it. The 4809 has a register function specifically for this, what was apparently recognized by the writers of the compiler, but ignored by the writers of the documentation.