Introducing the LiquidCrystal NKC Library!

Hello all! My name is Dominic. I'm an engineering scientist of nearly 20 years with a special interest and proficiency in Arduino and AVR-similar microcontrollers. I mostly work on the hardware side of development, but venture over to the software side when needed, especially for personal projects. And today, I'd like to share one with you all; the LiquidCrystal NKC library for Arduino IDE.

It all started a few weeks ago when I hastily ordered a "serial LCD module" off eBay thinking it was just another HD44780 LCD with an I2C backpack. The price was great and I really loved the amber on black, so I ordered it.

To my surprise, it was an HD44780 LCD, but further research lead me to discover that it was a proprietary LCD module by NKC Electronics with built-in support for I2C, RS232, and SPI interfaces. The only problem was the only Arduino library available for it looked half-finished and would've required a major rewrite of my project code to utilize just the basic functionality. But I didn't want to change my project code too much as I wanted to be able to swap back and forth between the NKC LCD and the PCF8574 LCD, and neither library was command compatible with the other.

So... I rewrote and redesigned the entire library!

I'd like to present the LiquidCrystal NKC library for the Arduino IDE, available for immediate download and installation via the official GitHub repository, or the Arduino Library Manager.

The LiquidCrystal NKC library was born from the fusion of the SerialLCD and the LiquidCrystal I2C Arduino libraries, and is the latest addition to the LiquidCrystal family, enabling simple, yet extensive, universally compatible plug-n-play control of the advanced LCD display modules by Longtech & NKC Electronics.

My biggest goal when creating LiquidCrystal NKC was universal compatibility, especially as these NKC LCD displays aren't as mainstream as PCF8574 and MCP23017 LCD interfaces. So, to me, it seemed it would probably be a good idea to make it so people can switch back and forth between LiquidCrystal NKC and LiquidCrystal (I2C), and their respective displays, with little to no extra coding required. So LiquidCrystal NKC should be a perfect replacement library both ways when using the LiquidCrystal (I2C) compatible commands, which is most of them.

On that note, I have also added a few extra commands to the LiquidCrystal NKC library, as well as expanded a few, extending functionality with less overhead. These expanded commands are not LiquidCrystal (I2C) compatible, but hopefully they will provide the user an easier coding experience while offering greater control of the LCD display. I hope to add more features in the future, and am open to suggestions if there are features you all and the community would like to see. Let me know here or on the GitHub repo's Issues page.

For further information, here are some links of interest:

Links to NKC Electronics Serial LCD Display Modules For Sale:

This is my first library of, hopefully, many, and one of my proudest works. So I'd like to thank you all for your time, use, and support. It means the world to me. I truly hope this library is of help to someone out there! So let's see what we can create!

2 Likes

That a full project with all the bells and whistles :smiley:
I looked at it, and there are a few things that can be improved.

☛ Indents and tabs:
Could you use indents of 2 spaces. There are now tabs in the source code and the indents are all over the place.

☛ Wire.setClock is buggy:
The minimum SCL clock frequency is 50kHz. The real minimum is a little lower, but I keep 50kHz as a minimum. 10kHz does not work.

#define I2C_SLOW  10000

☛ Delay between Wire.write() has no influence on the bus:
This makes no sense:

Wire.beginTransmission(g_i2c_address);
Wire.write(0xFE);
delayMicroseconds(I2CDELAY);
Wire.write(command);
delayMicroseconds(I2CDELAY);
Wire.endTransmission();
delayMicroseconds(I2CDELAY);

All those delays will do nothing for the I2C bus signals. You can remove them.
If the display needs some time after sending a command, then add a delay after the Wire.endTransmission().

☛ Delay between Serial.write() is very tricky:
There is a 64 byte serial buffer. If you put something in that buffer and wait before putting in the next byte in that buffer, then it depends on the baudrate if that has any influence. Since your delay is small, it has no influence.

Serial.write(0xFE);
delayMicroseconds(RS232DELAY);
Serial.write(command);
delayMicroseconds(RS232DELAY);
Serial.write(value);
delayMicroseconds(RS232DELAY);

A serial device rarely requires a extra long stop-bit or a gap between the data bytes. You can remove those delays.

1 Like

Thanks for checking it out. Like I mentioned, I work mostly on the hardware side of things rather than the software side, so I anticipated bugs and little things with the formatting and such. One of the reasons I opened it up for others to play with. I was starting to get tunnel visioned! :stuck_out_tongue_winking_eye:

Indents & Tabs
I'm not sure what you mean on the indents and tabs, though. I don't see anything on my end when opening the source code in any of my editors, even Arduino 1.8 and 2, and when viewing on GitHub. It all lines up evenly, and my tabs are all set for 2 spaces. The only time I'll use more than one tab is when lining up comments at the end of a line, and all my editors are set so a tab is "2 spaces". So I must be missing something somewhere. But that's an easy fix to redo all the indents.

Wire.setClock
The Wire.setClock values came from the display's suggestions of accepted values, but I'll make that fix to make I2C_SLOW = 50000 (50kHz). The one time I tested it at 10kHz it seemed to work, but I think that's because...

Wire.write() Delays
...the delay between Wire.write() was longer. I know it seems silly to do all that with the delays, trust me. But they were suggested by the manufacturer's examples I dug up, and initially I tossed them aside. However, every time I removed them or lowered the delay period, the display started to bug out and show junk, sort of like the display wasn't able to keep up. So I wrote it like it is now with the tiniest possible delay, just to keep the display working. But I already planned on fixing it, and it just didn't seem like it would be an issue if I left it as is for the time being while I toyed around with a solution. Though I didn't try it your way yet, so thank you for that. It's greatly appreciated help that seems to be working.

Serial.write() Delays
And I'll remove the RS232 delays as well. Truthfully, I added them because of the aforementioned issues I was having. I figured they were too small to cause issues, but just big enough to prevent any possible similar ones. But I'd rather not have them then deal with baud rate issues because of them.

Thank you very much for your time and help. I know I'm nowhere close to being a code guru god, but my heart is in the right place. I'm just trying to make something that'll help people :slightly_smiling_face:

Check back on the LiquidCrystal NKC GitHub repo, or Arduino Library Manager, next Friday for weekly updates. I plan on doing weekly updates on Fridays if/when there are things that need to be fixed or modified.

In the meantime, if there are any other comments, suggestions, fixes, and so on before then, either here or on the LC NKC GitHub repo's Issues page, I'll do my absolute best to get to them before Friday! :grinning_face_with_smiling_eyes:

Indents
All the examples have indents of 2 spaces.
The LiquidCrystal_NKC.cpp and LiquidCrystal_NKC.h have real tabs, in my browser I see this:

Wire library clock speed
The 10kHz works for an Arduino Uno, but it is not 10kHz. It could be a very high or very low value.

Wire library writing data
A delay between Wire.write() is nonsense code. All the delays will be added together for a delay before the I2C transaction and the delay after the Wire.endTransmission() is after the I2C transaction.

The Wire.beginTransmission() initializes variables and clears a buffer.
The Wire.write() puts data in a buffer.
The Wire.endTransmission() does the complete I2C sessions (using the data in the buffer).

I'm not saying that the display can work without the delays (probably not), but put them before Wire.beginTransmission() or after Wire.endTransmission().
What you do is waiting to put data in a memory location, similar to this:

byte buffer[3];

delay(1);
buffer[0] = 0xAA;
delay(1);                // wait until buffer[0] is really 0xAA
buffer[1] = 0xBB;
delay(1);
buffer[2] = 0xCC;
delay(1);
someCommunicationFunction( buffer);
delay(1);

What you really need is a delay after the command is given, or maybe a delay before starting with the new command (depending on how your code is organized).

byte buffer[3];

// delay(3);     // some extra time, so the device is no longer busy
buffer[0] = 0xAA;
buffer[1] = 0xBB;
buffer[2] = 0xCC;
someCommunicationFunction( buffer);
delay(10);       // give the device some time to execute the command

The problem with nonsense code is that others will copy it, and that I have to spend years trying to tell that it is wrong.
Try this for example: https://www.google.com/search?q=site%3Agithub.com+issues+wire.requestfrom+koepel :scream:

Serial/UART gap between bytes
If you really want a gap between sending Serial/UART data, then you have to use the flush function. I don't thing it is needed, but perhaps for testing:

Serial.write( 0xAA);    // put 0xAA in the buffer to transmit it
Serial.flush();         // wait until it is transmitted
delay(1);               // a gap of 1ms between the data bytes
Serial.write( 0xBB);
Serial.flush();
delay(1);

Did you know that a gap between the data bytes is the same as a extra long stop-bit ?

Indents
Oh my God, wow... that's really weird. I'll have to double check all my editors. One of them must be deceiving me with how big it says a tab is. No biggie. I'll fix that ASAP.

Wire Speed
I typically use a Nano or a Nano Every for testing. And I can change I2C_SLOW to 50000, making the lowest speed 50kHz. But I'm not sure what you mean by "It could be a very high or very low value". As it's written, the value is a constant, or at least should be.

Writing Wire Data Delays
Okay, yeah, I see what you're saying. That makes extra sense when you put it like that. And it seems to be working with a short delay after Wire.endTransmission() only. So I've resolved that bit, and hopefully I haven't created too much of an issue for you yet. :joy:

RS232 Delay
I'm all for trimming useless code and streamlining functionality, so I'm just going to eliminate those delays in the Serial functions. But I'll keep that in mind for the future in case there's some bug that emerges there at some point. But I doubt that it will.

I did not know that about the gap between data bytes being an extra long stop bit, no. But now I do. This is all very helpful information

I recommend this excellent browser add-on:

It is a matter of preference whether to use spaces or tabs for indentation. The important thing is to do it consistently.

However, when it comes to alignment, such as with comments, spaces are objectively better. The reason is that alignment using tabs is dependent on the specific tab size setting, and everyone has a different size setting, This is why the alignment is all screwy on GitHub, which uses 8 space tab width.

Back to indentation, as I said, use whatever style you like, but pick one and use it everywhere (though I think it's not so bad if you want to use a different style in the examples vs. library code). But you have a mixture of indentation styles, which is easy to see on GitHub when using that browser add-on I mentioned. Here is an example:

image

Here you have used both tabs and four space indents. In LiquidCrystal_NKC.h, the indentation is consistent, but it's consistently 2 spaces.

It seems kind of uptight, but it really does make it more difficult for people to contribute to the code when there isn't a clear prevailing code style for them to follow.

I can totally see that now. And that wasn't intentional. I'm actually very big on consistently formatting and styling my code to the point of OCD. I'm that way with all my work. It's why I make all the components I use in EAGLE CAD from complete scratch so they all look the same style-wise.

The thing is that I edit the library files in a different code editor than the Arduino IDE so I can go back and forth between projects and the library quickly. But I was in a hurry the other night before I uploaded, and I opened the source code files in the wrong editor, one I don't typically use. I didn't think it'd matter, so I made my edits using that editor before uploading, and they looked fine at the time. But I'm looking at the settings for those editors now, and it looks like their tab sizings were actually different. So I deeply apologize for the formatting being FUBAR. That was wholly accidental. I'll definitely fix that by next release. Thanks for making me aware of that. And thank you all for the help.

No need for apologies. I am also afflicted with OCD in this respect (even though I'm an absolute slob IRL), but I am usually fairly successful in keeping my code style OCD to myself, since I have found it to often be a counterproductive rabbit hole to dive down with those who aren't so picky as myself. But since the subject was already broached, I couldn't resist.

It distracted my from my original intent, which was to thank you for your contribution to the Arduino community. Thanks!

Back on the formatting subject. Even though I really like consistent formatting, I don't like to think about it. For this reason, I really like to follow the continuous integration practice of setting up an automated formatting check that runs every time I push a commit to GitHub. This way, I can let the robots do what they do best and leave me to focus my attention on the creative aspects humans are better at. GitHub has a really nice automation service called "GitHub Actions" that is very well suited for this sort of thing. Your submission of the library to the Arduino Library Manager registry was handled by GitHub Actions.

After years of service from the Artistic Style formatter used by the classic Arduino IDE, I'm now favoring the ClangFormat formatter tool for Arduino code due to it being used in Arduino IDE 2.x. There is a GitHub Actions workflow for checking Arduino and C++ code formatting with ClangFormat here. Using it is only a matter of adding two files to your repository.

Library looks really nice.

IMO, a good goal of creating a LCD library like this should be to make it as compatible as possible with other existing APIs, to try to make it as easy as possible to migrate existing sketch code to this library and hardware.
In the best case scenario, the user would only have to modify the header file includes and his lcd object constructor declaration/definition with no modifications necessary to the actual sketch code.
i.e. the goal should be to make things as easy and as simple as possible for user migrate existing sketch code and not have to do any device specific API functions to get the existing sketch code up and running.

With that in mind, here are a couple comments on the library API functions as I would recommend a few changes to get the functions to conform to the LCD 1.0 API as well as the LiquidCrystal API.
https://playground.arduino.cc/Code/LCDAPI/
Note: It is not possible to fully compatible with all the LCD API 1.0 functions and the LIquidCrystal functions as there are some incompatibilities like the parameter order in setCursor()

You can take a look at what I did for the hd44780 library here:

While there are many libraries out there that are non conformant particularly with how they implemented the setBacklight() function, this is how some of the the better and more popular libraries have implemented a few of these functions.

off()
Should turn everything off if possible, backlight and display pixels.

on()
Should turn everything back on, backlight and display pixels.

displayOff()
If possible turns off pixels but does not alter backlight.
Implemented this way conforms with the Arduino LiquidCrystal API since that API does not support backlight control.

displayOn()
If possible turns on pixels but does not alter backlght.
Implemented this way conforms with the Arduino LiquidCrystal API since that API does not support backlight control.

setBacklight(dimlevel)
This is often incorrectly implemented, including by entities such as AdaFruit,
PLEASE, PLEASE do not promote the use of setBacklight(HIGH) as that is an improper use of the setBacklight(dimlevel) API function and for libraries that implement this function properly and have dimmable backlights, it will result in a very dim backlight.
While you may see some implementations that check for zero/non-zero of the dimlevel parameter, they are doing that for h/w that does not support dimming and for h/w that does not support dimming, the backlight should be on for any non-zero dim level, but when dimming is supported, the parameter is a dim level.

As noted in the LCD API 1.0 spec, this function is supposed to set a dim level, if possible, not turn the backlight on/off based on non-zero or zero value.
If dimming is not supported, then 0 turns the backlight off, and any non-zero value turns the backlight on.

init() vs begin()
While the LCD API 1.0 used init() (and so did the very early LiquidCrystal API), the LiquidCrystal API now uses begin() rather than init() and that is what pretty much all the LCD libraries now use.
I would highly recommend adding a begin() function.
What the LiquidCrystal_I2C library did was to leave in the init() function but add a begin function. This works well as it makes it easy for users to migrate regardless of whether they are using older LiquidCrystal_I2C sketch code that used init() or newer code or LiquidCrystal code that uses begin()
In the hd44780 code i have a hidden/undocumented init() function for the hd44780_I2Cexp i/o class to allow easy migration of old code that used the LiquidCrystal_I2C library that used init() rather than begin()

backlight state after initialization.
This is a grey area, in that should it be on or off when initialization is complete?
It can be argued either way.
Some libraries have decided to leave it off, some have chosen to turn the backlight on during initialization so that the backlight is always on when the begin() function returns.
My reasoning for always turning on the backlight in begin() is so that existing sketch code written for the LiquidCrystal API which has no backlight control API functions will continue to work with no sketch changes.
If the backlight is off by default when begin() return, then sketch code written for LiquidCrystal will never turn on the backlight light unless modified since the LiquidCrystal API had no backlight support functions and the existing sketch would obviously never have called a backlight API function to turn it on.
IMO, turning it on makes it easier for less technical users to tell that something is working particularly when using inverted displays that show white text on dark backgrounds since when the backlight is off, it is just a dark/black display.

Here are what a few of the more popular libraries have done:
note: i wrote the backlight code for both newLiquidCrystal and hd44780

  • LiquidCrystal - there is no backlight support for this library.
  • fm's newLiquidCrystal - on after begin()
    when using newer backlight constructor.
  • hd44780 library - on after begin()
  • LiquidCrystal_I2C - explicitly turns it off in begin() / init()

--- bill

1 Like

Thank you for your support and kind words, @bperrybap, and @in0, you and the rest of the Arduino community are more than welcome! I'm very happy to contribute anything, anywhere I can. But honestly, this library was kind of a by-product of another, bigger project I'm working on. But I figured I'd go the extra mile and make it a nice, complete library with all the trimmings and bells and whistles I could in hopes that it'd help someone somewhere at some point. Plus it gives me something routine to do, which is something I sorely need these days. Without going into too much detail, I was on a chemo medication for my regular medical issues that prevented a stomach ulcer I had from healing, eventually causing it to eat a giant hole right through my stomach. I was in a coma for 10 days after barely surviving emergency surgery, and the only part of the experience that scarred me worse than the actual scar was the horrific nightmares and hallucinations I had non-stop in that coma. I legitimately thought I had died and gone to hell. Long story short, the library has really helped me keep my head straight, especially when the PTSD decides to wake me up at 1AM. So making this library has been very therapeutic and helpful to me as well. I just hope something good comes out of it for someone out there :wink:.

Anyways, @in0, thank you very much for those links. I've got a really busy week this week, but I'm really interested in setting that up in my repository and will definitely make an effort in the time I have to work tomorrow and Thursday to wrap up these edits and add that GitHub Action for ClangFormat to the repo before Friday. That way I can post an update and take care of all these issues.

And @bperrybap, I wholeheartedly agree with you. To be honest, I didn't know there was a standardized set of commands like the LCD 1.0 API, but I'm extremely happy that you brought it up! If I don't get to updating the command set this week, I will absolutely get to it next week. I don't think it will be too difficult to add support for those commands and update the ones that I already included so that they comply with the LCD API standards. Side note: I actually added support for several of the commands you listed like begin() despite using init() in the example sketches. But that's easy to switch! Thank you for your help and input. I really appreciate the guidance and agree that your suggested changes would bolster the library and improve the universal "plug-n-play" compatibility I'm hoping to most achieve. Oh, and I'll get rid of the setBacklight(HIGH) and revise that feature to comply with the API. No problem :+1:

I think for a plugin replacement you shoul also consider the different formats of print, to be covered in your library. As you don't inherit from print, you should bring them to your lib also. numbers, HEX, DEC, BIN, F-Makro /PROGMEM support etc.
And by the way, why is String print for SPI missing?

    case SPI:
      /*
			for (uint8_t i = 0; i < len; i++)	{
				spitransfer(inputStr.charAt(i));
			}
		  */
      break;