Skip to main content

4.1 LCD Message Display

Every example so far has reported back through the console, which means the board only has something to say while it is tethered to a computer. This one changes that: the 16x2 LCD in the kit gives the board a screen of its own, and a message printed there stays on it whether the computer is watching or not.

The wiring is the easiest in the whole kit. The LCD is a Qwiic module, so one cable does everything.

In this documentation you will learn:

  • How to connect the 16x2 LCD with a single Qwiic cable.
  • What the purple I2C LCD adapter on the back of the display is for.
  • How to initialize a display and turn on its backlight, and why the order matters.
  • How to put text where you want it with setCursor(), and why counting from zero matters.
  • Why only 16 characters fit on a row, and what happens to the seventeenth.
  • What to do when the screen glows but stays empty, the one fault that catches everybody.

Hardware required:

  • 1x Soldered NULA MINI board
  • 1x Breadboard
  • 1x 16x2 LCD display with I2C adapter
  • 1x Qwiic cable
  • 1x USB-C cable
ℹ️
No resistors and no jumper wires. Like example 3.1, this is a Qwiic module, so a single cable carries power and data together and it only fits one way round. The breadboard is here purely to hold the board steady.

Putting the components together

1. Insert the NULA MINI board on the breadboard

Push the board into one end of the breadboard so that its two rows of pins sit on either side of the centre channel, with the chip facing down.

NULA MINI board seated on the breadboard
Step 1: the board seated on the breadboard

2. Plug the Qwiic cable into the board

The NULA MINI has one Qwiic connector, on the edge of the board between the USER and RST buttons. It is the white connector marked qwiic on the silkscreen. Push the cable in until it clicks.

Qwiic cable plugged into the NULA MINI board, with the other end still loose
Step 2: the cable in the Qwiic connector of the board, other end still free
⚠️
Do not confuse it with the smaller two-contact connector on the opposite edge of the board, marked - and +. That one is the battery connector, and a Qwiic cable will not fit it.

If you would like to see exactly which connector this is on a bare board, it is highlighted here:

The NULA MINI board with its Qwiic connector highlighted
The Qwiic connector on the NULA MINI, highlighted in blue

3. Plug the other end into the display

Turn the display over and you will find a purple board screwed to its back, silkscreened I2C LCD ADAPTER. It carries two Qwiic connectors, one at each end. Plug the free end of the cable into either one. They are wired together, and the spare exists so you can chain another module onwards.

The display turned over to show its purple adapter board, with the Qwiic cable running from it to the NULA MINI on the breadboard
Step 3: the display turned over, cable in one of the two Qwiic connectors of the adapter
ℹ️
Along the edge of the adapter you can also see holes marked SCL, SDA, 5V and GND. That is the same connection brought out to solder pins, for boards that have no Qwiic socket. You do not need it here. The cable does the same job.

4. Plug in the USB-C cable

Connect the board to your computer. The purple PWR light comes on and the backlight of the display lights up blue, but the screen itself stays empty, because nothing has told it what to show yet.

The finished build with USB-C connected: the display backlight is lit blue but the screen shows no characters
Step 4: powered up, before running the script. Backlight on, screen still blank
ℹ️
This is worth looking at properly, because it is exactly what a fault looks like too. A lit backlight only means the display has power. It says nothing about whether the board is talking to it. Right now the screen is blank for the ordinary reason that no script has run. If it still looks like this after you press Run, see If the backlight is on but the screen is empty below.

What the adapter board is for

Look closely at the green display board and you will count 16 pins along its top edge, labelled VSS, VDD, V0, RS, RW, E, D0 through D7, A and K. That is the native interface of the HD44780, the controller chip that has driven displays like this since the 1980s. Wiring it directly means fourteen or so connections, and you would spend this whole example pushing jumper wires into a breadboard.

The purple adapter board does that job for you. It sits on those 16 pins permanently and talks to your board over just two wires instead.

The purple I2C LCD adapter board mounted on the back of the display, showing its silkscreen labels
The I2C LCD adapter: 16 display pins on one side, two Qwiic connectors and an I2C address on the other

Those two wires are the I2C bus, the same one the ultrasonic sensor used in example 3.1: SDA carries the data and SCL provides the clock that keeps both ends in step. Several modules can share one pair of wires, each answering to its own address, which is why this board has I2C ADDR 0X20 printed on it. 0x20 is the number this display answers to.

That is why the display is created from the same i2c object you already know, with no address and no pin numbers of its own:

i2c = I2C(0, scl=Pin(7), sda=Pin(6))
lcd = LCD_I2C(i2c)
ℹ️
LCD_I2C already knows that 0x20 is where to find the display, and it sets the controller up for a two-row screen on its own, unlike the Arduino library, which is created as LCD lcd(16, 2). There is nothing to pass but the connection.
⚠️
The scl=Pin(7), sda=Pin(6) part is not optional, exactly as in 3.1. Swap the two numbers or leave them out and the display never answers, which looks like a dead screen but is a mistyped pin.

You may also spot three small pads marked A0, A1 and A2 near the SOLDERED logo. Bridging them with solder changes the address, up to 0x27. You would only ever do that to put two of these displays on one cable. Leave them alone.


Getting the driver onto the board

This example needs LCD.py, which lives in the lib folder of the examples repository. As with the ultrasonic sensor, the driver has to be on the board itself, in its /lib folder, which is where MicroPython looks when it sees from LCD import LCD_I2C.

If you followed Setting up MicroPython all four drivers are already there. If not:

mpremote mip install github:SolderedElectronics/Soldered-NULA-Beginner-kit-MicroPython-project-examples/lib
⚠️
ImportError: no module named 'LCD' means the driver is not on the board, or it landed somewhere other than /lib. There is nothing wrong with the wiring or the display.

begin() before backlight()

Two calls start the display, and their order is not interchangeable:

lcd.begin()
lcd.backlight()

begin() opens the conversation and prepares the display for use. It also resets the display as part of that, which switches the backlight off. So backlight() has to come second. Put it first and the light goes out again a moment later, leaving you with a dark screen and no obvious reason why.

ℹ️
clear() then wipes anything left over from whatever ran before. Without it you can end up reading a mixture of an old message and a new one, because the display keeps showing whatever it was last told until something changes it.

Rows, columns, and counting from zero

The display is a grid: 16 characters across, 2 rows down. lcd.setCursor(column, row) chooses where the next thing you print will land, and both numbers start at zero:

  • lcd.setCursor(0, 0) is the very first character of the top row.
  • lcd.setCursor(0, 1) is the very first character of the bottom row.

So the rows are numbered 0 and 1, not 1 and 2, and the columns run 0 to 15. Getting that wrong does not raise an error, which is what makes it hard to spot: the driver treats every row other than 0 as the bottom row, so setCursor(0, 2) and even setCursor(0, 5) all quietly write to row 1. If a line of text keeps landing on the wrong row, this is why.

The 16-character limit is worth taking seriously, because the display does not wrap onto the next row. Print a nineteen-character string and you see the first sixteen characters and nothing more. The last three are not lost, though. Each row is really a 40-character buffer inside the controller chip, and the glass is only a 16-character window onto it, so the extra characters sit just off the edge of the screen. Example 4.2 puts that to work by sliding the window along. For now the practical rule is simply that anything past the sixteenth character will not be visible. Both messages in this example are twelve characters long, so both fit comfortably:

column: 0123456789012345
row 0: Hello, NULA!
row 1: Let's start!
ℹ️
Count your own messages before printing them. If a string is too long, the usual fix is to shorten the wording rather than to split it across the two rows, since the second row is normally carrying something else.

Everything happens once

One last thing to notice before the code: this script has no while True loop at all. It runs from the first line to the last and then ends.

That is deliberate. An LCD holds whatever it was last told to show, without being reminded, so printing the message once is enough. It stays on the screen after the script has finished, and only disappears when you cut the power or run something else.

ℹ️
The Arduino version of this example puts everything in setup() and leaves loop() empty for the same reason. MicroPython does not need the empty loop at all. A script that reaches its last line has simply finished.

Code

# I2C is a way for several devices to talk to the board over just two wires, and it is what the Qwiic connector
# carries. We need it here because the display is a Qwiic module.
from machine import I2C, Pin

# The Soldered driver for the LCD display. It lives in the lib folder of the examples repository, so copy the whole
# lib folder onto your board, otherwise MicroPython will not be able to find it.
from LCD import LCD_I2C

# Here we set up the I2C connection. The two pins are fixed by the board: on the NULA MINI, I2C uses IO6 for the
# data line (SDA) and IO7 for the clock line (SCL), which are exactly the pins the Qwiic connector is wired to.
i2c = I2C(0, scl=Pin(7), sda=Pin(6))

# Here we create our display object, which we named "lcd", and hand it the I2C connection.
# The display in this kit is 16 characters wide and 2 rows tall, which is where the name 16x2 comes from.
lcd = LCD_I2C(i2c)

# begin() starts the communication and prepares the display for use. It has to come first, before anything else we
# ask the display to do.
lcd.begin()

# backlight() turns on the light behind the screen, without which the text is very hard to read. Note that this has
# to come after begin(), because begin() resets the display and would switch the light back off.
lcd.backlight()

# clear() wipes anything that was left on the screen from before, so we start from a clean display.
lcd.clear()

# setCursor() chooses where the next text will appear. The first number is the column and the second is the row, and
# both start counting at zero, so (0, 0) is the top left corner.
# print() then writes our text starting at that position.
lcd.setCursor(0, 0)
lcd.print("Hello, NULA!")

# Move to the second line and print another message. (0, 1) means column zero of row one.
# Keep in mind that this display fits exactly 16 characters per row, so anything longer is cut off at the edge.
lcd.setCursor(0, 1)
lcd.print("Let's start!")

# And that is all. Unlike the earlier examples there is no while True loop here, because nothing needs to happen
# over and over: the message stays on the screen until the board is reset or powered off.

What you should see

Press Run. Within a second both lines appear, the same build as the photo above, now with something on the screen:

The finished build with the 16x2 LCD showing Hello, NULA! on the top row and Let's start! on the bottom row
`Hello, NULA!` on row 0 and `Let's start!` on row 1, each starting at column 0

Nothing is printed to the Shell this time. The script never calls print() on its own, because the display is the output. Note that lcd.print() and the built-in print() are two different things: one writes to the screen, the other to the console.

If the backlight is on but the screen is empty

This is the single most common problem with these displays, and it is not a fault in your code.

The characters are being sent correctly, but the contrast voltage of the display is set so low that they are invisible against the backlight. The fix is a small trimmer potentiometer on the adapter board, marked CONTRAST:

The I2C LCD adapter board with the CONTRAST trimmer potentiometer highlighted
The CONTRAST trimmer on the adapter board, highlighted in blue

With the board powered and the script run, turn that trimmer slowly through its full range with a small flat screwdriver. At one end you get a blank blue screen; at the other, a row of solid dark blocks. Your text is somewhere in between. Stop where the characters look crisp. You only ever need to do this once.

ℹ️
Nothing in the driver touches contrast. It is set purely by that trimmer, which is why no amount of changing the code will bring the text back. If your screen is blank, reach for the screwdriver, not the keyboard.
⚠️
If the trimmer does nothing across its whole range, the problem is communication rather than contrast. Check the scl=Pin(7), sda=Pin(6) in the I2C() line, then reseat both ends of the Qwiic cable until each one clicks. A script that stops with OSError: [Errno 19] ENODEV never reached the display at all.

Try it yourself: change one of the messages to something longer than sixteen characters and run it again. You will see it clipped at exactly the sixteenth character, which is a quicker way to learn the limit than counting.


Full example

Check out the full example code on the link below:

4.1_Print_Message.py

Example that initializes the Qwiic 16x2 LCD and prints a static two-line message using the Soldered LCD driver.