TL;DR: how to use BigNums with I2C LCDs
We recently had a comment on our original BigNums2x2 blog post about using the library, which is designed for parallel interface Hitachi HD44780-based 16x2 LCDs, with an I2C-equipped LCD. We immediately set to work adding I2C compatibility to our library, with the goal of enabling 16x2 LCDs of all kinds to have big numbers. But then things got complicated – complicated enough that we ultimately decided not to incorporate I2C support into our library. Here’s what we learned – hopefully it will help you implement BigNums with whatever I2C LCD you are using!
Rewinding for a moment, let’s talk about the different interfaces that you may encounter with 16x2 LCDs. The most common Hitachi HD44780-based displays use a dozen or more pins – greater still if it’s RGB – which can lead to a fairly messy breadboard, before you’ve even fleshed out other parts of your project. By using I2C, you can bring that pin count down to four: the same power and ground as before, plus SDA (data) and SCL (clock). And because the I2C bus allows up to 128 devices (or more with multiplexing), you’re actually kind of not using any pins, since a second, third, or … 128th device can in theory also be added with no additional I/O overhead. So, using an LCD that uses I2C, or obtaining an adapter for your existing display, can really simplify things. But what happens if you want to use BigNums?
Our BigNums2x2 library in turn uses Arduino’s LiquidCrystal library, which only supports 4 or 8-bit parallel Hitachi-compatible displays. But there are a number of I2C LCDs designed for use with Arduino, so, how do they work? It turns out each one of them tends to have its own library – usually not well maintained, and largely ripped off from one another in the ones that we looked at!
The first display that we tried, simply because it was on hand, was Seeed’s Grove 16X2 LCD RGB Backlight. This LCD presented its own special challenge in that its I2C implementation appears to be flawed. When used alone, the example code for this device completely failed to work. Further investigation, including diagnostics via i2c_scanner, revealed that the device was not found on address 0x3e as expected. But curiously, Seeed’s Grove Digital Light Sensor v1.1 seemed to work fine with all things equivalent hardware-wise – and curiouser still, the LCD worked too when the Digital Light Sensor was also present on the bus! Our best guess is that the LCD lacks the correct termination resistance and thus adding the Digital Light Sensor helps terminate the bus line.
After resolving these hardware issues, we started prototyping an updated BigNums2x2 library, using the Grove_LCD_RGB_Backlight library. The easiest way to do this was actually to duplicate our library files BigNums2x2.h and BigNums2x2.cpp locally alongside our sketch in place of the “real” library. By substituting the Grove library, which (mostly) supports the same interface, for the stock Arduino library in our code, we had BigNums2x2 up and running on the Grove LCD fairly painlessly. But, what about other I2C boards – it seemed imperative that we get a larger sample of representative devices in order to ensure that other devices could be added similarly.
Next up was the DFRobot Gravity: I2C LCD1602 Arduino LCD Display. As with the Seeed device, a custom library provided by the manufacturer is used via include as a substitute for the original Arduino library. After a quick confirmation that the library examples worked, it was time to see if it could be adapted as easily as the Seeed library. Unfortunately, the DFRobot library appeared to be missing a critical function, createChar(), which BigNums2x2 uses to create the custom characters that we use to make the Big Nums themselves. A closer look at the datasheet revealed mention of the requisite CGRAM functionality, so how come it wasn’t in the library? Analyzing that source revealed two very interesting facts: first, that the CGRAM function existed, but was called customSymbol(), and … that vast portions of the header file appeared identical to Seeed’s! Which wouldn’t inherently be an issue, since it’s open source – but DFRobot failed to preserve Seeed’s 2013 copyright, claiming authorship and replacing with their own 2016 copyright.
So…surely we couldn’t be the first library to encounter the issue of wanting to support multiple I2C LCDs? After a few false starts with other libraries, we noticed a lot of references online to fmalpartida’s new-liquidcrystal library – which seemed to no longer exist at the BitBucket repo which all the links pointed to. Further searching revealed a clone of the BitBucket library on GitHub, with a handful of commits over the past half decade or so. And! The documentation was retrievable via the Internet Archive’s Wayback Machine! Before adopting it as the basis for our LCD support though, we thought we should put it through its paces with the displays on hand.
We installed the library as usual, but received the error Multiple libraries were found for "LiquidCrystal.h" – weird that this new library would collide with Arduino’s default; but a closer look at those old docs clarified how the library was intended to be used:
The library has been developed to replace the current Arduino library, therefore you will need to remove/backup the LiquidCrystal folder from the Arduino library folder the original LiquidCrystal library and replace it for this one. You will also potentially need to remove other LCD libraries like LiquidCrystal_I2C as that will also conflict with this library.
Instead of just including the library, we had to use it to physically replace the stock library files in C:\Program Files (x86)\Arduino\libraries\LiquidCrystal – highly unusual! And a hassle we wouldn’t want to force on our users … but perhaps we could just rename it to prevent the conflict and include in the normal fashion if it turned out to be good – so let’s see how it goes…
With the standard HD44780 display, everything worked great once the pins were updated to match our breadboarded example. The DFRobot I2C module, however, presented further challenges. Unlike using the device’s own library, we had to specify the I2C address ourselves; actually, let’s take a moment to unpack what that means: remember the earlier discussion about i2cscanning? Different I2C devices have different addresses – typically a good thing, since it means you can combine them on a single bus. But what this also means is that LCDs from different manufacturers often have different addresses, so you can’t just have a hard-coded value like the individual libraries that were provided by each OEM (and even those typically require hacking if you cut the trace or bridge the pads that allow you to use their alternate address in order to eliminate a conflict with another device) and expect it to work for all. So either the library needs a way to “know” the address of each commonly-used device, in order to provide a seamless experience for users, or, as is the case with new-liquidcrystal, users are required to specify the address during initialization. But, manufacturers don’t always do a good job of making the address clear – in fact, as mentioned, they typically abstract it in the library so that users don’t have to “worry” about it. And in the case of the DFRobot display there was nothing in their wiki or examples, so we had to go digging. Diving into their library source code (again) suggested it was 0x7c, so we updated the example sketch to use this, and:
HelloWorld_i2c_DFR:8:27: error: no matching function for call to 'LiquidCrystal_I2C::LiquidCrystal_I2C(int)'
Weird, why would changing the address cause that? The answer was that it wouldn’t: the example didn’t compile even with the default value. There were multiple signatures for the lcd() function, and one finally built without error when we #DEFINE’d the missing POSITIVE macro, but … nothing happened with the LCD. Double-checking the device using the i2c_scanner revealed a device on 0x3e (same as the Grove!) – but … didn’t the code say 0x7c? Closer inspection revealed that the value is right shifted (0x7c>>1 = 0x3e) – because I2C addresses are 7-bit plus one more bit for read/write – but even the corrected value failed to produce any result.
Running out of development boards to test with while trying to avoid continually rewiring between testing, we tried the Grove LCD on Adafruit’s Metro M0, a SAMD21-based board. The original lcd() signature worked on this different architecture, but provided no output, and the other, even after manual #DEFINE of POSITIVE resulted in the error:
HelloWorld_i2c_Grove:11:18: error: invalid conversion from 'int' to 't_backlightPol' [-fpermissive]
11 | #define POSITIVE 1
At this point it felt like time to accept defeat.
What did we learn in our failed attempt to add I2C support to our library? First of all, be skeptical of your hardware: it might not be you – it might be a flawed I2C implementation! Second, pay close attention to library source code and its providence. And finally: sometimes it’s best to just do the thing, instead of creating a universal factory to do all the things. If we would have just demonstrated how to hack the library by making a local copy and adjusting it to the specific board/library that the commenter was working with, we would have been done much sooner. But at the same time, we wouldn’t have this lovely blog post full of lessons, so … maybe it was worth it in the end!
Leave a Reply.