Monday, October 10, 2022

Micropython - Driving an LCM4004A LCD with dual HD44780 (clone) driver chips


This is the display I'm using:

There are a few vendors for these 40 column x 4 row displays, all using clones of the Hitachi HD44780 controller:

These have a character pallet built in and have memory for 8 custom characters when needed.  And they have memory/wiring to support 80 characters on a display, so this 4004 display with 160 characters uses two drivers.  They share the same Data, RS, RW pins, but each have a separate enable pin to instruct them separately when writing to different lines of the display.

I didn't find any libraries specifically for this display, most of them are for 2004 or 4002 displays with a but this one from Adafruit gets pretty close - it's for the correct driver chip, but needed to be adapted to support two driver chips.  I ended up completely gutting this to get stuff working without the extra code, but adapted the setup sequence and write8 functions from here:

I'm using an Arduino Nano Connect RP2040 here, so the pins will be a little different for a PI Pico or other chip. 

Generally, the way this works is we:
  • Define our pins
  • Initialize some globals with the values for specific instructions that the contorller understands
  • Call the initialize function to turn the display on
  • Subsequently use the "write8" function to write characters to the display.

What's left to do?
  • Figure out how to control the cursor and write to specific locations on the display.
  • Figure out how to turn on the backlight.
  • Get contrast control working.  I tried to do this with a 10k pot, but the display isn't responding.
  • Adapt this module to work with the fbconsole library for a fun terminal.

import time
from machine import Pin

D4 = Pin(26, Pin.OUT)
D5 = Pin(27, Pin.OUT)
D6 = Pin(28, Pin.OUT)
D7 = Pin(29, Pin.OUT)
E1 = Pin(12, Pin.OUT)
E2 = Pin(13, Pin.OUT)
RS = Pin(5, Pin.OUT)

# Commands

# Entry flags

# Control flags

# Move flags

# Function set flags
LCD_2LINE = 0x08
LCD_1LINE = 0x00
LCD_5x10DOTS = 0x04
LCD_5x8DOTS = 0x00

def delay_microseconds(microseconds):
# Busy wait in loop because delays are generally very short (few microseconds).
end = time.time() + (microseconds/1000000.0)
while time.time() < end:

def write8(value, enable_pin, char_mode=False):
"""Write 8-bit value in character or data mode. Value should be an int
value from 0-255, and char_mode is True if character data or False if
non-character data (default).
print('write8', f'{value:#010b}', enable_pin, char_mode)
# One millisecond delay to prevent writing too quickly.

# Set the character/data bit
RS.value(1 if char_mode else 0)

# Write the Upper 4 bits
D4.value(((value >> 4) & 1) > 0)
D5.value(((value >> 5) & 1) > 0)
D6.value(((value >> 6) & 1) > 0)
D7.value(((value >> 7) & 1) > 0)

# Write the Lower 4 bits
D4.value((value & 1) > 0)
D5.value(((value >> 1) & 1) > 0)
D6.value(((value >> 2) & 1) > 0)
D7.value(((value >> 3) & 1) > 0)

def clear():
write8(LCD_CLEARDISPLAY, E1) # command to clear display
delay_microseconds(3000) # 3000 microsecond sleep, clearing the display takes a long time

def initialize():
# Initialize display control, function, and mode registers.
displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_2LINE | LCD_5x8DOTS

for enable_pin in E1, E2:
write8(0x33, enable_pin)
write8(0x32, enable_pin)
# Write registers.
write8(LCD_DISPLAYCONTROL | displaycontrol, enable_pin)
write8(LCD_FUNCTIONSET | displayfunction, enable_pin)
write8(LCD_ENTRYMODESET | displaymode, enable_pin) # set the entry mode

if __name__ == '__main__':
# Initialize the display.
for char in 'Hello World':
write8(ord(char), E1, True)
for char in 'Foo Bar! +~!123':
write8(ord(char), E2, True)

Saturday, August 13, 2022

Micropython - Modifying to support keystroke injection with os.dupterm

 This is something I needed to make an interactive micropython terminal without using the serial connection to interact with it.  E.g.  a separate library handles keyborad scanning using the pi pico pins and when a keypress is registered, a callback function is used to pass the dupterm instance.  Since I want to use fbconsole to display the console on a screen, it made sense to add the needed bits to the fbconsole library to support passing in the keystrokes:


In our display module:

display = ssd1306.SSD1306_I2C(128, 64, i2c_list[0], 0x3c)

scr = FBConsole(display)

And for a callback function, we can use something like this:

STREAM = oled_fb.scr
keeb.activate(STREAM.inject) # setup the callback

To pass in single or multiple characters, you can use the inject method.  You'll see them appear in the terminal when the function is called:

STREAM.inject('1 + 1')
STREAM.inject(b'\r') Modifications:

Within, we add a couple imports, constants, and define self._data in __init__:

import framebuf
import uio
import os

_MP_STREAM_POLL = const(3)
_MP_STREAM_POLL_RD = const(0x0001)
class FBConsole(uio.IOBase):
def __init__(self, fb, bgcolor=0, fgcolor=-1, width=-1, height=-1, readobj=None):
self._data = bytearray()
self.readobj = readobj

And methods in the class:

def inject(self, data):
self._data += data

if hasattr(os, 'dupterm_notify'):

def read(self, sz=None):
d = self._data
self._data[:] = b''
return d

def ioctl(self, op, arg):
if op == _MP_STREAM_POLL:
if self._data:

And finally modify the readinto method. 

# def readinto(self, buf, nbytes=0):
# if self.readobj != None:
# return self.readobj.readinto(buf, nbytes)
# else:
# return None

def readinto(self, buf):
if not self._data:
return None
b = min(len(buf), len(self._data))
buf[:b] = self._data[:b]
self._data = self._data[b:]
return b

Finally, in addition to regular numbers, letters, and symbols, you'll need a few ascii codes to pass in enter, backspace, etc as well as ctrl+key cobmos:

'_bksp': b'\b',
'_entr': b'\r',
'_tab': b'\t',

'a': b'\x01', # ctrl + a
'b': b'\x02',
'c': b'\x03',
'd': b'\x04',

I'll do a followup article with more info on ctrl and other special characters.  We should be able to pass in arrows to move the cursor, etc. 

Finally, this article will be updated with a link to the complete code soon. 


Tuesday, July 26, 2022

RP2040 Micropython PIO - Part 4 - Using IRQ to signal when pins changes have been pushed!

I'm delighted that this worked exactly as it seemed it ought to.  Just like we did in Part 3, we track the pins states and only push them when there's been a change.  But now, when we push the changed pins, we toggle the irq. 

When we initialize the state machine, we bind a function, "printFromGet" to the state machine irq. When the irq is toggled, this function is called, and a pointer to the state machine is passed to the function.  Then we can call sm.get() to read the new pins values.   This is fantastic because python doesn't need to spin checking the pins using the gpio library wasting cpu cycles.  When we press a key, python is detoured momentarily to our needed function to handle the pin press, and once it returns, python can resume whatever else it was working on!

Here's a more complete code from the last couple examples. 

import safety_pin
import pio_junk
from machine import Pin
import rp2
import time


def initPinsAsIn(direction=Pin.PULL_UP):
for n in range(32):
Pin(n, Pin.IN, direction)
print("Couldn't initialize pin:", n)

def printFromGet(sm):
global COUNT
out = sm.get()
print(f'{COUNT} - {out:>032b}')
COUNT += 1

@rp2.asm_pio( set_init=[PIO.IN_HIGH]*32 )
def irq_pins_changes():
mov(y, pins)
#in_(y, 32)
mov(isr, y)

label("read loop")
mov(x, pins)

jmp(x_not_y, "exit read loop")
jmp("read loop")
label("exit read loop")
mov(isr, x)
mov(y, x)


if __name__ == '__main__':
print('In Main Now')

sm = rp2.StateMachine(0, irq_pins_changes,
freq=2000, in_base=Pin(0))
sm.irq(printFromGet, 0)
for n in range(10):
out = sm.get()
print(f'{n}, {out:>032b}')

The last thing I might want to do is sort of look up which pin changed and push an integer value corresponding with the addr of the changed pin so each time I do a get() in python, I get the number of the changed pin.  

Monday, July 25, 2022

RP2040 Micropython PIO - Part 3 - Pushing pins only on changes

 In order to check for changes to the pins, each time we read the pins values, we can compare them to previous values stored in the x or y scratch registers. 

So the statemachine records the pin state in y when it starts, pushes y so python has the initial state as well, and then the state machine keeps checking the pins until the pin values cached in x aren't equal to y.  Then it pushes x, copies x to y, and starts checking again. 

for n in range(32):
Pin(n, Pin.IN, Pin.PULL_UP)
print("Couldn't initialize pin:", n)

@rp2.asm_pio( set_init=[PIO.IN_HIGH]*32 )
def echo_pins_changes():
mov(y, pins)
#in_(y, 32)
mov(isr, y)

label("read loop")
mov(x, pins)

jmp(x_not_y, "exit read loop")
jmp("read loop")
label("exit read loop")
mov(isr, x)
mov(y, x)


sm = rp2.StateMachine(0, pio_junk.echo_pins_changes,
freq=2000, in_base=Pin(0))
for n in range(10):
out = sm.get()
print(f'{n}, {out:>032b}')

Next step is setting an irq to tell python it's time to pull a value from the fifo.  

And maybe later some time, we can implement matrix scanning!  I'm not too sure how that would work since we'd need to potentially track the state of more than 32 keys...  maybe we'd use separate state machines for each row of the keyboard!

Sunday, July 24, 2022

RP2040 Micropython PIO - Part 2 - Reading all the pins!

 The next thing I wanted to get working with PIO was reading the pin values.   I struggled with this until I realized that I needed to initialize the pins and set them to pull_up before starting the state machine.  Anyway, here's an example reading all available pins and returning their states on the fifo:

@rp2.asm_pio( set_init=[PIO.IN_HIGH]*32 )
def echo_pins():
in_(pins, 32)

set(x, 31) # call nop 32 times to slow things down
nop() [31]
jmp(x_dec, "aloop")


for n in range(32): # Initialize all the pins with PULL_UP
Pin(n, Pin.IN, Pin.PULL_UP)
print("Couldn't initialize pin:", n)

sm = rp2.StateMachine(0, pio_junk.echo_pins,
for n in range(10): # pull pin states from the fifo ten times!
out = sm.get()
print(f'{n}, {out:>032b}')

And the output looks like the following.  Some of the bits change as I press keys on the keyboard:

MPY: soft reboot
Checking Safety Pin 25...
In Main Now
Couldn't initialize pin: 30
Couldn't initialize pin: 31
0, 00111110111111111111101111111011
1, 00111110111111111111101111111011
2, 00111110111111111111101111111011
3, 00111110111111111111101111111011
4, 00111110110101011111101101111011
5, 00111110110101011111101101111011
6, 00111110110101011111101101111011
7, 00111110111111111111101111111011
8, 00111110111111111111101111111011
9, 00111110110101011111101101111011
MicroPython v1.19.1 on 2022-06-18; Arduino Nano RP2040 Connect with RP2040
Type "help()" for more information.

In Part 3, we'll track the pin states and  only output a value when there is a change.  

And in Part 4, we'll use an irq to trigger a python function only when there is a key press event so we don't need to keep a cpu core busy watching for button presses. 

Saturday, July 23, 2022

RP2040 Micropython PIO - Part 1 - Blinking nop, jmp, and .side experiments!

 I'm working on incrementally adding complexity to PIO programs to learn how to use them.  Here are a few examples.  Note that I'm using Pin 6 for the LED on the Arduino Connect Nano RP2040 board.  I think the pin is different for the Pi Pico.

Most Simple Blinking the LED:

The most simple classic blinking example - toggle a pin on and off with the slowest freqency (2000) and a bunch of nop waits to slow the blinking down enough that we can see it!

def blink():
set(pins, 1) [31]
nop() [31]
nop() [31]
nop() [31]
nop() [31]
set(pins, 0) [31]
nop() [31]
nop() [31]
nop() [31]
nop() [31]

# sm = rp2.StateMachine(0, pio_junk.blink, freq=2000, set_base=Pin(6))

Blinking the LED Using sideset and jmp for more nop:

In this example, we use sideset to toggle the LED pin on and off and jmp to loop for more nop.

Normally, in each PIO assembled instruction, 5 bits are available to specify length of wait, so 0-31 wait cycles.  But when we enable sideset, 2 (or more??) bits are used to specify the sideset value.  In this case, we use Pin 6 (the LED) as the sideset pin, and we have a max wait of 7 (three bits). 

This speeds up the program execution without a bunch more nop calls.  So we can count on the x scratch register to repeatedly call nop with a jmp and slow the blinking enough that we can see it, like before using sideset and the longer [31] wait.

def side_blink():
set(x, 31).side(0x0)
nop() [7]
jmp(x_dec, "aloop")

set(x, 31).side(0x1)
nop() [7]
jmp(x_dec, "bloop")

# sm = rp2.StateMachine(0, pio_junk.side_blink, freq=2000, sideset_base=Pin(6))

Up next...

I'm trying to read the value of all of the pins and put the values into the fifo to be used by the Micropython program...

Micropython - Safety pin to avoid soft-bricking

 I ran into an issue on an Arduino Connect Nano RP2040 board this last week where I got some code in that caused micropython to hang without functional serial or shared drive, so I had no way to change the code and fix it. 

It was a little difficult to fix this.  A few things I tried:

  • Re-flash the micropython uf2 - this doesn't wipe the and other code, so the problem remains.
  • Flash circuitpython uf2 - circuitpython stores code at a different addr and works perfectly when flashed, but when you put micropython back, the old broken remains.  I even tried to dd a large random data file to the circuitpython shared folder, but somehow it didn't overwrite the micropython content!
  • Flash the various nuke uf2 files - It seems that these are all set up for the pi pico, which has 4MB of storage.  The arduino board has 8MB of storage and the area micropython uses is unaffected. 
What did finally work!
  • Flash the OpenMV customized version of Micropython.  It didn't load the and exposed the same memory/file share that micropython uses so I was able to rename the broken and flash standard Micropython back onto the board to resume working on it!
And how to avoid this in the first place - It's surprisingly easy to hang micropython when experimenting with PIO and other stuff with blocking functions. The solution I'm using now is to source a file that checks if a pin is grounded.  If it is, it calls sys.exit() so the program quits instead of continuing and triggering another hang.

import saftey_pin

from machine import Pin
import time
import sys

PIN_NUM = 25

print(f"Checking Safety Pin {PIN_NUM}...")
if not SAFETY_PIN.value():
    print("PIN Shorted to Ground!")
    print("Aborting loading and quitting.")

 To use it, I put a key between the two pins shown here and plug the board into the usb port:

Friday, July 08, 2022

Micropython - Keyboard library with chording, layers, etc.

 I'm working on a project to build an interacive terminal using micropython, and a component of that is checking for button presses and translating them to keys to be used to control the terminal or however that works.  

I've made a couple of attempts at keyboard firmwares in the past and changed strategies this time from key-focused to event-focused - events start when a key is pressed and terminate after either a hold timer or key that is part of the event is released.  

Based on the numbers of keys in the event, their functions, and hold duration, we can figure out the correct keyboard key to be returned.  

Examples to consider:

  • Chords can map combinations of keys to letters, numbers, or symbols,
  • A single key, if held for some duration, can trigger a layer shift and then subsequent keys or combinations of keys can result in letters, numbers, etc from that layer. 
  • Short taps result in a number, letter, etc. 
Anyway, this might be a helpful template for anyone trying to do something similar: