The project I had in mind for this - a standalone, ESP8266-powered data display, didn't have enough pins to support both the display and the other components I wanted to hang off it. To work around this, I picked up a couple of I2C LCD backpack adaptors, which can be found for well under $1 each at your favourite Chinese electronics marketplace.
These modules employ the PCF8574 chip, which is a "remote 8-bit I/O expander for I2C bus", which is basically a fancy name for a serial to parallel convertor; that is - it takes one byte of serial data in (via the I2C, or two-wire bus), and expands that out into 8 individual digital pins as bits.
Modules, How Do They Work?
From this, I was able to determine the following byte-to-pin
mappings:
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Pin | D7 | D6 | D5 | D4 | K | EN | RW | RS |
So all up, we've got 8 pins of output from only 2 GPIOs... but how do we use this with the ubiquitous LiquidCrystal library?
First of all - just a very small bit of background as to how the HD44780 works. These have a total of 16 pins - two for power, two for backlighting, one for contrast, 8 for data and 3 for control. The 8 data pins accept 8 individual data bits in parallel, however we also have the option of operating in 4-bit mode, meaning we only need four of the data pins (but everything takes twice as long to send, as we have to send each byte in two halves.)
To differentiate between a display data and command data, we use the RS, or "register select" pin. Commands include functions such as display initialisation, showing or hiding the cursor, or scrolling the display.
If you want a bit more of a deep-dive on how this all works, the 8-Bit Guy does a great video on the subject which you can find here.
I’m aware that several libraries exist already – however I found most implementations to be a little slow; the VFD doesn’t suffer from the same poor response rate as the LCD, meaning animated effects look a lot nicer – but this calls for a faster-performing library. In addition, I couldn’t find any libraries that implemented the VFD’s brightness control functions, and I wanted something that was as close to a drop-in replacement for the existing LiquidCrystal library as possible.
Most of the magic happens in in the write4bits() function. In the original library, this
simply splits the input into four bits, and writes this to their corresponding
pins using digitalWrite(), then sends a quick pulse to the "enable" or "E" pin through the pulseEnable() function:
void LiquidCrystal::write4bits(uint8_t value) { for (int i = 0; i < 4; i++) { digitalWrite(_data_pins[i], (value >> i) & 0x01); } pulseEnable(); }
This is where we need to do 99% of the work.
Instead of writing to four GPIOs, we need to redirect these writes to I2C. We do this by taking the 4 bytes of data, shifting these left four places (you’ll remember earlier that that bits 4 - 7 are our data bits), and appending our other pin settings (RS, E and backlight) for bits 0, 2 and 3. As we never read data from the LCD, "RW" is always held low.
In order to speed things up, we can take a shortcut
around the pulseEnable() function. It turns out, we don't need to pulse the "E" pin; it's only once the "E" pin is returned to low that the data is read by the LCD. Instead
of setting the data pins and then sending a quick pulse to "E", we can set
the "E" pin at the same time as our data pins, and then set "E" to low, saving us a a third of our I2C bandwidth:
void LiquidCrystal::write4bits(uint8_t value, uint8_t mode) { uint8_t bits = ((value & 0x0F) << 4); // mask 4 data bits and shift left writeToI2C(bits | _backlight | (mode & RS_PIN)); } void LiquidCrystal::writeToI2C(uint8_t bits) { Wire.beginTransmission(_addr); Wire.write(bits | EN_PIN); // write data bits + enable bit delayMicroseconds(1); // enable pulse must be >450ns Wire.write(bits); delayMicroseconds(50); // commands need > 37us to settle Wire.endTransmission(); }
All that's left is to incorporate the LCD backlight control, which is handled by the backlight() and noBacklight() commands:
void LiquidCrystal::noBacklight() { _backlight = LCD_BACKLIGHTOFF; } void LiquidCrystal::backlight() { _backlight = LCD_BACKLIGHTON; }
And finally, incorporate VFD backlight control (after finding the relevant command from the datasheet) with the setBrightness() command:
void LiquidCrystal::setBrightness(uint8_t value) { _displayfunction &= ~0x03; _displayfunction |= (value & 0x03); command(LCD_FUNCTIONSET | _displayfunction); }
Here's a short video of it in action with the VFD:
I wrote a quick bit of test code to see just how quickly the library performed versus other comparable implementations, and found it to be 68% faster (largely due to the pulseEnable() shortcut, as best as I can tell.) Take that, other implementations!
You can download the library from my GitHub, which includes full detail on how to use this or adapt existing implementations.
No comments:
Post a Comment