ESP32 pin misbehaving - is it me or Wokwi?

I am writing a program to manipulate an 8-bit bus using 8 GPIO pins on an ESP32-S3 model. Because I need to switch multiple pins in parrallel, I am using direct register manipulation. I have ran into a peculiar issue.

So, I want to set the bus in read mode and then read a byte.

The 8 GPIO pins are first set into READ mode by writing to the GPIO_ENABLE_W1TC_REG register using a mask. I then set the pull-ups using the gpio_pullup_en() function using a loop to set each pin in turn. (I couldn't find a parallel way to do this yet). I have also set up an 8-way switch grounded on one side to apply a ground to manually and selectively be able to apply a ground to any pin in the bus. By default all switches are turned off so the pins should be pulled up high.

I am expecting that that when I read the input register GPIO_IN_REG, that all pins would be high - but pin 8 consistently stays low.

I did try using alternative pins, e.g. 45 or 46. Reading pins above 32 requires a second 32-bit register to be read , which I initially had problems with due to a mistake I made in the bit shifting statement. using 1 << i instead of 1ULL << i which meant that the shifting was truncated. Once I recognized and corrected that error, those two pins worked as expected. This would seem to confirm that the algorithm is correct.

Which brings me back to why pin 8 is mis-behaving?

The emulation is at:

https://wokwi.com/projects/446609949895379969

I do have the project downloaded just in case so I don't mind someone making notes or additions.

Does pin 8 have a special function? I am aware that not all pins on the ESP32 can be used as GPIOs for various reasons including connected PSRAM and other peripherals typically found on dev boards. I have found some notes on specifically for the S3 here:

https://github.com/atomic14/esp32-s3-pinouts

However, pin 8 does not appear to get a special mention. So is the problem with the Wokwi emulation perhaps?

Or do I first need to set the pin in GPIO mode in some way (not sure if this is possible or how I would do this but e.g. on the Pico pins need to be initialised before you can use them) because its set to a different function by default? But if so, why is that inconsistent with other adjacent pins?

My suspicion is that thisis a bug in the Wokwi emulator, but that's what I initially thought about regarding pins 45 & 46 until I found my mistake. I guess I am therefore looking for a sanity check.

The acid test, I suppose, will be trying it on an actual ESP32-S3 which I don't have yet, although I do have other variants to play with, e.g. WROOM and WROVER and neither of these actually have a pin 8....

You are making the situation more complicated by using Wokwi. You could have a bug in it's software. I would use a real esp32 at least once to see which way the problem goes. Simple divide and conquer.

I sometimes prototype and quick and dirty test out ideas on Wokwi prior to testing them on actual hardware as it is more convenient sometimes than multiple cycles of compile, upload, test, fail, repeat over on actual hardware. Once the idea is working in principle I then start testing and if necessary, tweak on the real thing.

Wokwi does seem to have some strange behaviors and I have bugs without really looking, so I appreciate it has shortcomings.

I am pretty much at that point with this particular group of functions which almost work as they should except for that one bug. My next step is indeed to test with a pin configuration adapted to one of the ESP32's that I have knocking around.

Can you use a sketch with only one line to turn the pin on or off, and gradually add to the sketch to do more until the failure occurs? I've had to insert delay()s to SEE and READ a separation between bits being OFF and ON.

That is a reasonable idea and I did do something like that to test the output code that light the LEDs, which do light up or not according to the mask I set. I also used the tactic of printing out the state of the mask and registers to observe what was happening with each bit between iteration of a loop to find the problem with pins > 32 and discovered the problem with pins > 32 which is now sorted.

In this case, since its pin 8, its another issue. The mask is being generated with the appropriate bits set and the register is being written to. On reading back the register I see that all pins get set except this particular one and I can see no reason why that should be. An incorrect mask might explain it, but that's not the case. Changing that pin assignment to another GPIO works as it should do.My guess, therefore, is that the Wokwi model mis-behaving.

So I have now set it up on real hardware - an ESP32-WROOM on a Devkit v1. It doesn't have a pin 8, so I just used a pin that was available. Well I now find that a number of pins do not change state. This is using the exact same pin manipulation code that seems to work on the Wokwi emulator, so it seems that emulator experience does not quite match real hardware. If I use alternate functions with standard Arduino pinMode() and digitalWrite(), everything works fine. The trace looks a little misaligned, but it works.

As Sonofcy suggests, I now need to figure it out on the real ESP32 and forget the emulator. BTW, I did try delays in placed to allow pins to settle, but it didn't really make much of a difference. Of course, I might not have put a delay in the right place yet....

Anyway, will persevere with it on the real ESP32 tomorrow and see how goes. Must be something silly that I am missing somewhere.

@BitSeeker If you're at the point where you've identified a testable, demonstrable 'feature' of Wokwi, it would be beneficial to ALL if you were to go to the Wokwi Discord group, become a member, and post your findings. It is very likely that those who developed Wokwi will then have the opportunity to discuss your findings, seek any clarifications, and potentially FIX WHAT YOU'VE FOUND. Because otherwise, how will it ever get fixed?

That is of course a fair point and although I don't particularly like going on Discord and so usually avoid it, I have now been on there. I have been given some links to some Espressif reference documentation which previously didn't turn up using Google. While having this function and parameters information might actually be helpful, applying it in practice is another matter which I am still trying to figure out.

It also turns out that I have used one of the "strapping pins". I thought I had managed to avoid all of those pins that are presented but can't actually be used. I don't really get the point of that, but it does seem to be a "feature" of ESP32 boards and is confusing, but at least I can now correct that oversight.

Ok, so I have created a simplified test sketch which I am running on a Devkit 1. Here, the 8 pins assigned to a "bus" are initially set to input_pullup. The bus is next set to output high using two different methods - the standard Arduino pinMode() function and then using ESP functions, but the result is different. When using pinMode() both the input and output register are set to '1', but when using the equivalent ESP function, only the output register is set, which confuses digitalRead(). I have verified with a multi-meter that the pin is actually going high. Since one can't write to the input register - it is read-only, what is pinMode() doing that gpio_config_t isn't?

On an AVR, digitalRead() will return '1' in both the case when the pin is set to output and output is set high, or, when it is set to input and the input is high or pulled up. The ESP is behaving differently here if I use the ESP functions.

Code:

/*
 * ESP32 GPIO pin test
 */

// Pin assignments
#define DIO1  32
#define DIO2  33
#define DIO3  25
#define DIO4  26
#define DIO5  27
#define DIO6  14
#define DIO7   4
#define DIO8  13


// Struct used to hold GPIO register values
struct gpioregister_t {
  uint32_t reg0 = 0;
  uint32_t reg1 = 0;
};


// Array holding pin map
const uint8_t databus[8] = { DIO1, DIO2, DIO3, DIO4, DIO5, DIO6, DIO7, DIO8 };


// 64-bit control and data bus mask
uint64_t gpioDbMask = 0;


// ESP GPIO configuratiom object
gpio_config_t gpioCfg;
const gpio_config_t * gpioCfgPtr = &gpioCfg;


/***** Covert 64-bit mask to 2 x 32-bit regster values *****/
void mask64ToReg(gpioregister_t& gpioreg, uint64_t mask) {
  gpioreg.reg0 = (mask & 0xFFFFFFFF);
  gpioreg.reg1 = (mask >> 32);
}


/***** Convert 2 x 32-bit register values to 64-bit mask *****/
uint64_t regToMask64(gpioregister_t& gpioreg) {
  uint64_t gpiomask = 0;
  gpiomask = gpioreg.reg0;
  gpiomask |= ((uint64_t)gpioreg.reg1 << 32);
  return gpiomask;
}


/***** Generate GPIO mask from assigned pin map *****/
uint64_t genGpioMask(const uint8_t buspins[], uint8_t bitmask) {
  uint64_t gpioreg = 0;
  for (uint8_t i=0; i<8; i++) {
    if (bitmask & (1 << i)) {
      gpioreg |= ( 1ULL << buspins[i] );
    }
  }
  return gpioreg;
}


/***** Set the direction of GPIO pins using pinMode() and mask *****/
void setGpioDirMasked1(const uint8_t bus[], uint8_t mask, uint8_t state = INPUT_PULLUP) {

  // OUTPUT mode
  if (state == OUTPUT) {
    for (uint8_t i=0; i<8; i++) {
      if ( mask & (1U << i) ) pinMode(bus[i], OUTPUT);
    }
    return;
  }

  // INPUT_PULLUP mode
  for (uint8_t i=0; i<8; i++) {
    if ( mask & (1U << i) ) pinMode(bus[i], INPUT_PULLUP);
  }

}


/***** Set the direction of GPIO pins using ESP functions and mask *****/
void setGpioDirMasked2(const uint8_t bus[], uint8_t mask, uint8_t state = INPUT_PULLUP) {

  uint64_t gpiomask = 0;

  for (uint8_t i=0; i<8; i++){
    if ( mask & (1U << i) ) gpiomask |= (1ULL<<bus[i]);
  }

  if (state == OUTPUT) {
  // OUTPUT mode
    gpioCfg.pin_bit_mask = gpiomask;
    gpioCfg.mode = GPIO_MODE_OUTPUT;
    gpioCfg.pull_up_en = GPIO_PULLUP_DISABLE;
  }else{
    // INPUT_PULLUP mode
    gpioCfg.pin_bit_mask = gpiomask;
    gpioCfg.mode = GPIO_MODE_INPUT;
    gpioCfg.pull_up_en = GPIO_PULLUP_ENABLE;
  }

  gpio_config(gpioCfgPtr);

}


/***** Set the bus using registers *****/
void setDbusHigh() {

  gpioregister_t gpiodb;

  mask64ToReg(gpiodb, gpioDbMask);

  // Set all high
  REG_WRITE(GPIO_OUT_W1TS_REG, gpiodb.reg0);
  REG_WRITE(GPIO_OUT1_W1TS_REG, gpiodb.reg1);

}


/***** Show the bus using standard Arduino function *****/
void showBus(){
  for (uint8_t i=0; i<8; i++){
    Serial.print("DIO");
    Serial.print(i);
    Serial.print(": ");
    Serial.println(digitalRead(databus[i]));
  }
}


/***** Show the bus using registers *****/
void showBusReg(){
  uint64_t gpioall = 0;
  gpioregister_t gpioreg;
  
  // Read the input register
  gpioreg.reg0 = REG_READ(GPIO_IN_REG);
  gpioreg.reg1 = REG_READ(GPIO_IN1_REG);
  gpioall = regToMask64(gpioreg);
  Serial.print("IN: ");
  Serial.println(gpioall, BIN);


  // Read the output register
  gpioreg.reg0 = REG_READ(GPIO_OUT_REG);
  gpioreg.reg1 = REG_READ(GPIO_OUT1_REG);
  gpioall = regToMask64(gpioreg);
  Serial.print("OUT:");
  Serial.println(gpioall, BIN);

}


void setup(){

  uint8_t i=0;

  Serial.begin(115200);
  delay(2000);

  // Generate masks
  gpioDbMask = genGpioMask(databus, 0xFF);

  // Configure all GPIOs to input pullup (default?)
  gpioCfg.pin_bit_mask = gpioDbMask;
  gpioCfg.mode = GPIO_MODE_INPUT;
  gpioCfg.pull_up_en = GPIO_PULLUP_ENABLE;
  gpioCfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
  gpioCfg.intr_type =  GPIO_INTR_DISABLE;
  gpio_config(gpioCfgPtr);

  // Default INPUT_PULLUP
  Serial.println("Default INPUT_PULLUP:");
  showBus();

  // Test3: OUTPUT pinmode()
  Serial.println("\nTest1: OUTPUT using pinMode():");
  setGpioDirMasked1(databus, 0xFF, OUTPUT);
  for (i=0; i<8; i++){
    digitalWrite(databus[i], HIGH);
  }
  showBus();
  showBusReg();

  // Test4: OUTPUT using registers
  Serial.println("\nTest1: OUTPUT using registers:");
  setGpioDirMasked2(databus, 0xFF, OUTPUT);
  setDbusHigh();
  showBus();
  showBusReg();

  Serial.flush();

}



void loop(){

}

Result:

rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:1
load:0x3fff0030,len:4980
load:0x40078000,len:16612
load:0x40080400,len:3480
entry 0x400805b4
Default INPUT_PULLUP:
DIO1: 1
DIO2: 1
DIO3: 1
DIO4: 1
DIO5: 1
DIO6: 1
DIO7: 1
DIO8: 1

Test1: OUTPUT using pinMode():
DIO1: 1
DIO2: 1
DIO3: 1
DIO4: 1
DIO5: 1
DIO6: 1
DIO7: 1
DIO8: 1
IN: 1100001110000000001110101010111001
OUT:1100001110000000000110000000010000

Test2: OUTPUT using registers:
DIO1: 0
DIO2: 0
DIO3: 0
DIO4: 0
DIO5: 0
DIO6: 0
DIO7: 0
DIO8: 0
IN: 1000101010101001
OUT:1100001110000000000110000000010000

So what have I missed? What is pinMode() doing that the equivalent ESP functions are not?

I think I have found a partial answer. The ESP version of digitalRead() calls `gpio_get_level()'. In the API reference, for that function it says this:

    Warning

    If the pad is not configured for input (or input and output) the returned value is always 0.

So it can only guarantee to read a pin correctly if it has been configured for input, bit, that still does not entirely explain the above result because in both cases the pins are configured as output. However, it does show that under some circumstances the reading may be inconsistent, which is also backed up by this in the comments of the __digitalRead(uint8_t pin) function in esp32-hal-periman.c:

// This work when the pin is set as GPIO and in INPUT mode. For all other pin functions, it may return inconsistent response

So I guess the way to determine the state of a pin configured for output is to read the output register. But that means I now need to check whether the pin is configured for input or output first to determine which approach to use. This perhaps means first reading the state of the pin from the ENABLE register.