Heltec Wifi LoRa 32 V2 - I2C issues
While checking the board’s pinout diagram and its pin definitions (defined in the ESP32 Arduino Core) I noticed that the default SDA and SCL (I2C) pins are defined incorrectly which can cause problems.
This is explained below.
ESP32 by default uses GPIO21 and GPIO22 pins for I2C but also has the possibility to remap the I2C interface to different pins. Heltec Wifi Lora 32 V2 does not use the standard I2C pins for its OLED display and uses GPIO4 and GPIO15 instead. The values of SDA and SCL should be defined correspondingly but they are not. The values of SDA and SCL are incorrect.
SDA and SCL are defined as pin 21 (GPIO21) and pin 22 (GPIO22), which is common for ESP32.
But for connecting the display they used pins GPIO4 and GPIO15 instead.
Vext (which is actually ‘Vext control’) controls (enables/disables) Vext external power.
Like SDA, Vext is defined as GPIO21 which is weird because it conflicts with SDA.
The following is defined in the board’s pins_arduino.h
file (part of ESP32 Arduino Core):
static const uint8_t SDA = 21; /* CONFLICT! */
static const uint8_t SCL = 22;
static const uint8_t SDA_OLED = 4;
static const uint8_t SCL_OLED = 15;
static const uint8_t Vext = 21; /* CONFLICT! */
When using the Arduino Wire (I2C) library, the ‘Wire’ object (which controls the I2C hardware interface) is initialized with Wire.begin()
. Without parameters, this will initialize the I2C interface using the pins defined by SDA and SCL. To use different pins for I2C, the pins have to be explicitly specified: Wire.begin(sda_pin, scl_pin)
.
Vext and SDA are mutually exclusive. One GPIO cannot be used to control both Vext external power and SDA at the same time, but unfortunately that is how the pins are defined! The values of SDA and SCL cannot be changed in application code because they are defined in the ESP32 Arduino Core as const int
.
During I2C communication the SDA and SCL interface pins will switch between HIGH/LOW with a frequency of 100+ kHz. If GPIO21 is used for SDA then Vext power will also switch with 100+ kHz accordingly. This is not good and the Vext voltage regulator is not designed for that.
Therefore GPIO21 must not be used for SDA but unfortunately that is exactly what happens if Wire.begin() is called without parameters.
To use pins GPIO4 and GPIO15 for I2C instead (which is how the on-board display is wired) use:
Wire.begin(/*sda*/ 4, /*scl*/ 15);
Note:
The ESP32 Wire library will remember the sda_pin and scl_pin parameters that were specified in the first Wire.begin(…) call. If any 3rd-party library later calls Wire.begin() (without parameters) that luckily does not cause the pins used for I2C to be reset to the default SDA and SCL pins.
Wire.begin(sda_pin, scl_pin)
must be called before calling any third-party library initialization code (e.g. bme280.begin()
).
The ESP32 supports two separate I2C hardware interfaces. The second interface is accessible via the Wire1 object which can be initialized as follows:
Wire1.begin(alternate_sda_pin, alternate_scl_pin)
I2C is a shared bus that can be shared by many peripherals/sensors. In most cases a single I2C interface will be sufficient and the second I2C bus will not be needed.
About calling Wire.begin() from within third party library code
The I2C interface is a shared bus that can be used by many peripherals. Different types of peripherals will require different (third party) libraries. Initializing a shared bus is therefore the responsibility of the application and not the responsibility of third party libraries, which should therefore not try to initialize it (i.e. should not call Wire.begin()
).
In the early Arduino days only Atmel 8-bit MCU’s were supported. These only had a single I2C interface on fixed pins. But when Arduino matured, the framework was ported to other MCU architectures like ESP32 which supports 2 hardware I2C interfaces that can be mapped to different pins. If third party libraries then call Wire.begin() without parameters this can introduce problems.
Unfortunately, many libraries (including from Adafruit) call Wire.begin() (without parameters) from library code and do not provide a mechanism for specifying which pins to use for I2C so they will always use the pins defined by SDA and SCL - unless the application initializes the Wire object (I2C interface) first. If the real (mapped) I2C pins are different from what is defined by SDA and SCL the application has to call Wire.begin(sda_pin, scl_pin)
before any third party library initialization code is called.