In my project I'm trying to use a PCF8574 to drive some peripherals, since the ESP8266 doesn't offer all the GPIO pins I need, however I seem to be having problems at the most basic level of driving the pins of an e-paper display.
The e-paper display is connected to MOSI, MISO and SCL to the "native" pins GPIO12, 13 and 14, and I wanted to rely on the PCF to drive the other four pins (CS, DC, RST, BUSY).
I modified the original library so that it could accept an arbitrary Pin
abstraction that looks like the following:
class Pin {
public:
Pin(void){};
~Pin(void){};
virtual uint8_t read() = 0;
virtual void write(uint8_t value) = 0;
};
and at first implemented it as ArduinoPin
, which all it does is setting up the pin mode and calling digitalRead
or digitalWrite
when needed.
While connecting the EPD with the ArduinoPin
implementation, using D0, D1, D2, D4 as pins, the e-paper display works correctly, so I proceeded to make another implementation of Pin with the GPIO Expander.
Following the GPIO Expander and relative Pin implementations
gpio-expander.h
class GpioExpander {
public:
GpioExpander(uint16_t address);
GpioExpander();
uint8_t read(uint8_t pin);
void write(uint8_t pin, uint8_t value);
private:
uint8_t buffer[1] = {0xFF};
uint16_t address;
};
class GpioExpanderPin : public Pin {
public:
GpioExpanderPin(GpioExpander *expander, uint8_t pinNumber);
~GpioExpanderPin(void);
virtual uint8_t read();
virtual void write(uint8_t value);
private:
uint8_t myPin;
GpioExpander *expander;
};
gpio-expander.cpp
GpioExpander::GpioExpander(uint16_t address) {
this->address = address;
buffer[0] = 0xFF;
Wire.beginTransmission(address);
Wire.write(buffer[0]);
Wire.endTransmission();
}
GpioExpander::GpioExpander() : GpioExpander(0x20) {}
uint8_t GpioExpander::read(uint8_t pin) {
write(pin, 1);
Wire.requestFrom(address, 1);
buffer[0] = Wire.read();
return (buffer[0] >> pin) & 1;
}
void GpioExpander::write(uint8_t pin, uint8_t value) {
if (value == 1)
buffer[0] |= (1 << pin);
else
buffer[0] &= ~(1 << pin);
Wire.beginTransmission(address);
Wire.write(buffer[0]);
Wire.endTransmission();
}
gpio-expander-pin.cpp
GpioExpanderPin::GpioExpanderPin(GpioExpander *expander, uint8_t pinNumber) {
this->expander = expander;
myPin = pinNumber;
}
GpioExpanderPin::~GpioExpanderPin(void) {}
uint8_t GpioExpanderPin::read() { return this->expander->read(myPin); }
void GpioExpanderPin::write(uint8_t value) {
this->expander->write(myPin, value);
}
These implementations work fine, I tested everything with a multimeter and I can arbitrarily write 1s and 0s on all 8 pins of the PCF, but when I try to drive the four pins of the e-paper, the ESP8266 resets and I have no clue on why. I tried with the exception decoder and the following stacktrace appears:
Exception Cause: Not found
0x40202984: Twi::read_bit() at ??:?
0x402029db: Twi::write_byte(unsigned char) at ??:?
0x40202b94: Twi::writeTo(unsigned char, unsigned char*, unsigned int, unsigned char) at ??:?
0x40202bb9: Twi::writeTo(unsigned char, unsigned char*, unsigned int, unsigned char) at ??:?
0x40202cc0: twi_writeTo at ??:?
0x40201720: TwoWire::endTransmission(unsigned char) at ??:?
0x40105cfe: os_printf_plus at ??:?
0x40201748: TwoWire::endTransmission() at ??:?
0x402017f5: GpioExpander::write(unsigned char, unsigned char) at ??:?
0x40201794: GpioExpanderPin::write(unsigned char) at ??:?
0x402011f9: Epd::spiTransfer(unsigned char) at ??:?
0x40201238: Epd::sendData(unsigned char) at ??:?
0x40201385: Epd::clear() at ??:?
0x402010b6: setup at ??:?
0x4020583c: std::_Function_handler<bool (), settimeofday::{lambda()#1}>::_M_manager(std::_Any_data&, std::_Function_handler<bool (), settimeofday::{lambda()#1}> const&, std::_Manager_operation) at time.cpp:?
0x4020583c: std::_Function_handler<bool (), settimeofday::{lambda()#1}>::_M_manager(std::_Any_data&, std::_Function_handler<bool (), settimeofday::{lambda()#1}> const&, std::_Manager_operation) at time.cpp:?
0x4020583c: std::_Function_handler<bool (), settimeofday::{lambda()#1}>::_M_manager(std::_Any_data&, std::_Function_handler<bool (), settimeofday::{lambda()#1}> const&, std::_Manager_operation) at time.cpp:?
0x4020583c: std::_Function_handler<bool (), settimeofday::{lambda()#1}>::_M_manager(std::_Any_data&, std::_Function_handler<bool (), settimeofday::{lambda()#1}> const&, std::_Manager_operation) at time.cpp:?
0x40202158: loop_wrapper() at core_esp8266_main.cpp:?
0x40100e85: cont_wrapper at ??:?
(Not sure why I can't get the source lines to appear, any suggestion in that is most welcome.)
Anyway apparently the problem appears when ending the I2C transmission, at the lowest level possible, at Twi::read_bit, which I include for the sake of all of you trying to help me out
core_esp8266_si2c.cpp
bool Twi::read_bit(void)
{
SCL_LOW(twi_scl);
SDA_HIGH(twi_sda);
busywait(twi_dcount + 2);
SCL_HIGH(twi_scl);
WAIT_CLOCK_STRETCH();
bool bit = SDA_READ(twi_sda);
busywait(twi_dcount);
return bit;
}
The most upsetting thing is that this same setup, when programmed in Micropython works just fine (I can't use Micropython though, because I need all the RAM I can get from the ESP to draw pictures, and I'm much interested in the overall performance I can get with C++).
Anyone had this same problem? Any advice on how to fix this behavior?
EDIT
With some experimentation, I managed to replicate the problem in a smaller scale. The following code resets the ESP8266 as well:
void setup() {
Wire.begin();
GpioExpander expander;
GpioExpanderPin pin(&expander, 0);
while(true) {
pin.write(0);
pin.write(1);
}
}
It looks like fast cycles of on-off crash the system (which is what the EPD library does when sending data, turning on and off the pins relatively fast to switch between modes etc.). Not sure how to address this though, but I believe it might be something related to the Twi libraries at this point.
EDIT 2
Adding a yield()
to the write
function of the gpio expander class makes the issue go away, but the time to execute a simple display clear grows to unacceptable levels (~3 seconds to perform a full display refresh).
Any suggestion is appreciated at this point.
EDIT 3
I did some more experimenting and benchmarking, and reached the following conclusions:
- the time that it takes for the Wire library to send data to the GPIO expander (the
write
function) is ~50µs - when using the gpio expander, the time to write data to the SPI device (DC write + enable CS + SPI transfer + disable CS) takes a total of ~170µs (which checks out, 3 times calling the gpio expander makes for ~150µs and the rest is SPI transaction)
- when using the arduino pins, the time to write data to the SPI device takes ~10µs (an order of magnitude less than the GPIO expander).
To send a total of 150,000 bytes to the e-paper display, then, with the GPIO expander takes roughly 3 seconds, while with the normal pins it would take 10 times less (0.3 seconds).
The reset issue is surely due to the time spent in the send loop: the watchdog thinks the program is stuck in an infinite loop and thus resets the board. This is why the calls to yield
remove the reset issues.
I believe I must either find a faster implementation of wire (?) or try to minimize usage of CS / DC pins
.pio/build/nodemcuv2/firmware.elf
, and that is the target I'm supplying to the exception decoder, which, by the way, is the jar one found here: github.com/littleyoda/EspStackTraceDecoderGpioExpander
. But judging that yourGpioExpander
construct is conducting a Wire transaction, this will depend on where you create an instance ofGpioExpander
, if it is created before thesetup()
function, theWire.begin()
has not be established yet. It is better to create aGpioExpander::begin()
method and move most of the code in the construct to thebegin()
method.