2

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

7
  • 1
    Are you sure you are decoding the stack trace with the right binary?
    – Sim Son
    Jun 2, 2022 at 18:15
  • 1
    build, upload, run, crash, copy stack dump, decode in one pass so the decoder can use the elf file corresponding to the bin which was uploaded
    – Juraj
    Jun 2, 2022 at 19:05
  • @SimSon yes, I believe I am. I am building everything with platformio which outputs the final binary in .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/EspStackTraceDecoder
    – Luca
    Jun 2, 2022 at 21:06
  • 1
    You didn't post your code on how you create the instance of GpioExpander. But judging that your GpioExpander construct is conducting a Wire transaction, this will depend on where you create an instance of GpioExpander, if it is created before the setup() function, the Wire.begin() has not be established yet. It is better to create a GpioExpander::begin() method and move most of the code in the construct to the begin() method.
    – hcheung
    Jun 4, 2022 at 12:51
  • @hcheung sadly, that changes nothing. As I mentioned, the gpio expander works just fine, I can arbitrarily turn on and off pins. The issue only presents itself when using it with the epaper display.
    – Luca
    Jun 4, 2022 at 15:59

1 Answer 1

2

In the end the answer to the problem is the update frequency of the pins that drive the e-paper display:

The original waveshare library (as linked in the post) does the following to send data to the EPD:

  • enable DC (to tell the EPD that it's receiving DATA)
  • enable CS (to tell the EPD to listen to the incoming SPI data)
  • send SPI data
  • disable CS (to tell the EPD that anything received after that is not its competence)

Normally with arduino's digitalWrite this happens in a matter of ~10µs, and even if we repeat this process 15k times (that's how many bytes a 4.2 inch display supports) we spend 0.15 seconds to update half of the display's content (because the EPD has 2 memory banks which must be written).

When using the GPIO expander, instead, the mean time to send data via I2C is something around 50µs, which means that to enable / disable the pins three times we increase tenfold the time that the normal digital write takes (~150µs) which translates to 1.5 seconds to update half of the display and finally roughly 3 seconds for a full update.

During this loop, the watchdog thinks that the program is stalling and resets the chip. This is why using yield the update runs its course correctly.

To fix this issue, I worked on the EPD library and reduced the use of the digital writes, by creating a "session" of sorts: whenever I start writing data in a loop, I enable the CS pin, and disable it only at the end of the loop, so that the 100µs are only expended around the data transfer loop.

This is actually what happened in the Micropython driver for the e-paper display: the SPI connection sent data in chunks, enabling and disabling the chip select pin only at the beginning and end of the "transaction".

I'm planning to publish the updated library at some point, once everything is smoothed out, if this can interest anyone leave a message :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.