hd44780: Restructure to implement conn/display/TextDisplay. Refine to use GPIO.Group (#91)

* Add support for Adafruit I2C/SPI Backpack
* Add support for PCF857x Backpacks
* Add support for conn/gpio/Group
* Implement conn/display/TextDisplay
* Add examples
* Cleanups
* Change backlight implementation
pull/109/head
gsexton 1 year ago committed by GitHub
parent bc9678aa18
commit 9a7f7ac5da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -12,7 +12,7 @@ require (
github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-colorable v0.1.13
golang.org/x/image v0.23.0 golang.org/x/image v0.23.0
periph.io/x/conn/v3 v3.7.2 periph.io/x/conn/v3 v3.7.2
periph.io/x/host/v3 v3.8.3 periph.io/x/host/v3 v3.8.4
) )
require ( require (

@ -17,5 +17,5 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
periph.io/x/conn/v3 v3.7.2 h1:qt9dE6XGP5ljbFnCKRJ9OOCoiOyBGlw7JZgoi72zZ1s= periph.io/x/conn/v3 v3.7.2 h1:qt9dE6XGP5ljbFnCKRJ9OOCoiOyBGlw7JZgoi72zZ1s=
periph.io/x/conn/v3 v3.7.2/go.mod h1:Ao0b4sFRo4QOx6c1tROJU1fLJN1hUIYggjOrkIVnpGg= periph.io/x/conn/v3 v3.7.2/go.mod h1:Ao0b4sFRo4QOx6c1tROJU1fLJN1hUIYggjOrkIVnpGg=
periph.io/x/host/v3 v3.8.3 h1:v90ozCFDWgEyfNElZ+JnOvq0jAdW0vmgjCUy8dYXDds= periph.io/x/host/v3 v3.8.4 h1:QNleTythDd0k6Chu0n+ISrJFlf3LFig9oNbtOIkxoCc=
periph.io/x/host/v3 v3.8.3/go.mod h1:uKrIpfXjELwHkwGBNe6aos//XiQ/3uxDa1P2BmLV6Ok= periph.io/x/host/v3 v3.8.4/go.mod h1:hPq8dISZIc+UNfWoRj+bPH3XEBQqJPdFdx218W92mdc=

@ -0,0 +1,60 @@
# Hitachi HD44780 Package
## Overview
The Hitachi HD44780 is an LCD driver chip. It's used in a variety of text LCD
displays. The datasheet is available here:
https://www.sparkfun.com/datasheets/LCD/HD44780.pdf
Generally, there are three kinds of displays that use this chip.
1. A raw display. This is a board with a row of pin connectors at the top. It's
made for interfacing directly to GPIO pins. This is the most complicated method
of using the LCD because you have to wire a minimum of 6 GPIO pins (4 data, 1
reset, 1 enable) and 2 power pins to make it work.
There are also complex initialization routines that have to be performed. This
is further complicated by having specific initialization calls depending upon
whether the device is connected to 4 data lines, or 8 data lines.
2. A backpack display. With this type of LCD display, an I2C, serial, or SPI
interface is provided. This type of display is easier because there are fewer
pins, but the complex intialization remains. Examples of backpack interfaces
include The Adafruit I2C/SPI backpack (MCP23008/74HC595D), the generic
LCDXXXX/PCF8547T backpack, etc.
3. The third and final variety is the "Intelligent" display. These displays have
a micro-controller that is connected to the LCD chip. Typically these intelligent
displays support multiple I/O methods and the micro-controller handles the
LCD initialization/communication.
Examples of this kind of display would be the SparkFun SerLCD display, the
MatrixOrbital LK2047T, the AdaFruit USB+Serial Backpack etc.
## Interfacing Notes
The driver package is designed to use the gpio.Group interface. This allows
the LCD driver to be agnostic about the physical connection between the display
and the host device. Any host/expander that supports the
periph.io/x/conn/v3/gpio.Group interface can be used to easily drive the LCD
display.
## Hardware Notes
DO NOT attempt to source VCC for the unit backlight, or sink VCC to ground.
The backlight draws ~250ma of current which exceeds the current capability
of GPIO pins. It will permanently damage your device. If you would like to
control the backlight, connect a GPIO pin through a 1K Ohm resistor to a
transistor (2N2222 or equivalent).
## Troubleshooting
If nothing displays at all, check the contrast. Adjust the contrast control
until the 5x7 dot grid on the display is visible.
If the first row contains blocks of dots, and the other rows are blank, then
the initialization of the device failed. Check IO pins are connected properly.
If the text is garbled, verify the gpio.Group is configured and the IO Pins
are connected to the right pins of the LCD display.

@ -0,0 +1,62 @@
// Copyright 2025 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package hd44780
import (
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/spi"
"periph.io/x/devices/v3/mcp23xxx"
"periph.io/x/devices/v3/nxp74hc595"
)
const (
// Name is the LCD pin name, and the integer value is the GPIO
// number (not physical) of the MCP23008 I2C GPIO Expander.
d4 = 3
d5 = 4
d6 = 5
d7 = 6
rsPin = 1
enablePin = 2
backlightPin = 7
)
// This function returns a display configured to use the Adafruit I2C/SPI LCD Backpack.
//
// # Product Information
//
// https://www.adafruit.com/product/292
//
// The I2C side of this backpack uses an MCP23008 I/O expander. This function
// creates an MCP23008 device with the required pin configuration. To use this,
// get an I2C bus, and call this function with the bus, i2c address, number of
// rows, and columns.
func NewAdafruitI2CBackpack(bus i2c.Bus, address uint16, rows, cols int) (*HD44780, error) {
mcp, err := mcp23xxx.NewI2C(bus, mcp23xxx.MCP23008, address)
if err != nil {
return nil, err
}
gr := *mcp.Group(0, []int{d4, d5, d6, d7, rsPin, enablePin, backlightPin})
reset, _ := gr.ByOffset(4).(gpio.PinOut)
enable, _ := gr.ByOffset(5).(gpio.PinOut)
bl := gr.ByOffset(6).(gpio.PinOut)
return NewHD44780(gr, reset, enable, NewBacklight(bl), rows, cols)
}
// This function returns a display configured to use the SPI side of the Adafruit
// I2c/SPI backpack. The SPI side uses a 74HC595 Serial->Parallel shift register.
func NewAdafruitSPIBackpack(conn spi.Conn, rows, cols int) (*HD44780, error) {
chip, err := nxp74hc595.New(conn)
if err != nil {
return nil, err
}
// The SPI side has the same pins but in reverse order from the I2C side.
gr, _ := chip.Group(d7, d6, d5, d4)
rs := chip.Pins[rsPin]
e := chip.Pins[enablePin]
bl := chip.Pins[backlightPin]
return NewHD44780(gr, rs, e, NewBacklight(bl), rows, cols)
}

@ -0,0 +1,40 @@
// Copyright 2025 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package hd44780
import (
"fmt"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/gpio"
)
// A monochrome backlight Implements display.Backlight It uses a single
// GPIO Pin to turn the backlight on or off.
type GPIOMonoBacklight struct {
blPin gpio.PinOut
}
// Given a GPIO pin that turns the backlight on/off, construct a monobacklight
// to use with HD44780.
func NewBacklight(blPin gpio.PinOut) *GPIOMonoBacklight {
return &GPIOMonoBacklight{blPin: blPin}
}
// Turn the display backlight on or off.
func (bl *GPIOMonoBacklight) Backlight(intensity display.Intensity) (err error) {
if intensity == 0 {
err = bl.blPin.Out(gpio.Low)
} else {
err = bl.blPin.Out(gpio.High)
}
return err
}
func (bl *GPIOMonoBacklight) String() string {
return fmt.Sprintf("%#v", bl)
}
var _ display.DisplayBacklight = &GPIOMonoBacklight{}

@ -0,0 +1,225 @@
// Copyright 2018 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package hd44780
import (
"fmt"
"time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/gpio"
)
// lineTwo offset for the second line in the LCD buffer.
const lineTwo = 0x40
// Deprecated: Use HD44780
type Dev struct {
// data pins
dataPins []gpio.PinOut
// register select pin
rsPin gpio.PinOut
// enable pin
enablePin gpio.PinOut
}
// Deprecated: Use NewHD44780()
// New creates and initializes the LCD device
//
// data - references to data pins
// rs - rs pin
// e - strobe pin
func New(data []gpio.PinOut, rs, e gpio.PinOut) (*Dev, error) {
if len(data) != 4 {
return nil, fmt.Errorf("expected 4 data pins, passed %d", len(data))
}
dev := &Dev{
dataPins: data,
enablePin: e,
rsPin: rs,
}
if err := dev.Reset(); err != nil {
return nil, err
}
return dev, nil
}
// Reset resets the HC-44780 chipset, clears the screen buffer and moves cursor to the
// home of screen (line 0, column 0).
func (r *Dev) Reset() error {
if err := r.clearBits(); err != nil {
return err
}
delayMs(15)
if err := r.rsPin.Out(gpio.Low); err != nil {
return err
}
if err := r.enablePin.Out(gpio.Low); err != nil {
return err
}
if err := r.bulkSendData(resetSequence, r.write4Bits); err != nil {
return err
}
return r.bulkSendData(initSequence, r.writeInstruction)
}
func (r *Dev) String() string {
return "HD44870, 4 bit mode"
}
// Halt clears the LCD screen
func (r *Dev) Halt() error {
if err := r.writeInstruction(0x01); err != nil {
return err
}
delayMs(2)
return nil
}
// SetCursor positions the cursor
//
// line - screen line, 0-based
// column - column, 0-based
func (r *Dev) SetCursor(line uint8, column uint8) error {
return r.writeInstruction(0x80 | (line*lineTwo + column))
}
// Print the data string
//
// data string to display
func (r *Dev) Print(data string) error {
for _, v := range []byte(data) {
if err := r.WriteChar(v); err != nil {
return err
}
}
return nil
}
// WriteChar writes a single byte (character) at the cursor position.
//
// data - character code
func (r *Dev) WriteChar(data uint8) error {
if err := r.sendData(); err != nil {
return err
}
if err := r.write4Bits(data >> 4); err != nil {
return err
}
if err := r.write4Bits(data); err != nil {
return err
}
delayUs(10)
return nil
}
// service methods
var resetSequence = [][]uint{
{0x03, 50}, // init 1-st cycle
{0x03, 10}, // init 2-nd cycle
{0x03, 10}, // init 3-rd cycle
{0x02, 10}, // init finish
}
var initSequence = [][]uint{
{0x14, 0}, // 4-bit mode, 2 lines, 5x7 chars high
{0x10, 0}, // disable display
{0x01, 2000}, // clear screen
{0x06, 0}, // cursor shift right, no display move
{0x0c, 0}, // enable display no cursor
{0x01, 2000}, // clear screen
{0x02, 2000}, // cursor home
}
func (r *Dev) bulkSendData(seq [][]uint, f func(_data uint8) error) error {
for _, v := range seq {
if err := f(uint8(v[0])); err != nil {
return err
}
if v[1] > 0 {
delayUs(v[1])
}
}
return nil
}
func (r *Dev) clearBits() error {
for _, v := range r.dataPins {
if err := v.Out(gpio.Low); err != nil {
return err
}
}
return nil
}
func (r *Dev) write4Bits(data uint8) error {
for i, v := range r.dataPins {
if data&(1<<uint(i)) > 0 {
if err := v.Out(gpio.High); err != nil {
return err
}
} else {
if err := v.Out(gpio.Low); err != nil {
return err
}
}
}
return r.strobe()
}
func (r *Dev) sendInstruction() error {
if err := r.rsPin.Out(gpio.Low); err != nil {
return err
}
return r.enablePin.Out(gpio.Low)
}
func (r *Dev) sendData() error {
if err := r.rsPin.Out(gpio.High); err != nil {
return err
}
return r.enablePin.Out(gpio.Low)
}
func (r *Dev) writeInstruction(data uint8) error {
if err := r.sendInstruction(); err != nil {
return err
}
// write high 4 bits
if err := r.write4Bits(data >> 4); err != nil {
return err
}
// write low bits
if err := r.write4Bits(data); err != nil {
return err
}
delayUs(50)
return nil
}
func (r *Dev) strobe() error {
if err := r.enablePin.Out(gpio.High); err != nil {
return err
}
delayUs(2)
return r.enablePin.Out(gpio.Low)
}
func delayUs(ms uint) {
time.Sleep(time.Duration(ms) * time.Microsecond)
}
func delayMs(ms int) {
time.Sleep(time.Duration(ms) * time.Millisecond)
}
var _ conn.Resource = &Dev{}

@ -0,0 +1,160 @@
// Copyright 2025 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package hd44780_test
import (
"errors"
"fmt"
"log"
"time"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/display/displaytest"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/i2c/i2creg"
"periph.io/x/conn/v3/physic"
"periph.io/x/conn/v3/spi"
"periph.io/x/conn/v3/spi/spireg"
"periph.io/x/devices/v3/hd44780"
"periph.io/x/devices/v3/pcf857x"
"periph.io/x/host/v3"
"periph.io/x/host/v3/gpioioctl"
)
// This example shows using a gpio.Group with an HD44780 display. For this
// example its using the periph.io/x/host/gpioioctl package to obtain
// the gpio.Group and pins. You can use an I/O device that implements
// gpio.Group and gpio.PinOut to drive a display.
func Example() {
var err error
if _, err = host.Init(); err != nil {
log.Fatal(err)
}
chip := gpioioctl.Chips[0]
var ls gpio.Group
// Using a group to obtain the pins. The first 4 pins in the group are the
// data pins, and the remaining ones are reset, enable, and backlight.
// For 8 bit mode, specify additional data pins.
ls, err = chip.LineSet(gpioioctl.LineOutput, gpio.NoEdge, gpio.PullNoChange,
"GPIO27", "GPIO22", "GPIO23", "GPIO24", "GPIO17", "GPIO18", "GPIO25")
if err != nil {
log.Fatal(err)
}
pins := ls.Pins()
reset := pins[4].(gpio.PinOut)
enable := pins[5].(gpio.PinOut)
bl := hd44780.NewBacklight(pins[6].(gpio.PinOut))
lcd, err := hd44780.NewHD44780(ls, reset, enable, bl, 2, 16)
if err != nil {
log.Fatal(err)
}
n, err := lcd.WriteString("Hello")
time.Sleep(5 * time.Second)
fmt.Printf("n=%d, err=%s\n", n, err)
fmt.Println("lcd=", lcd.String())
_ = lcd.Home()
_ = lcd.MoveTo(1, 1)
_, _ = lcd.WriteString("Line 1")
_ = lcd.MoveTo(2, 2)
_, _ = lcd.WriteString("Line 2")
time.Sleep(5 * time.Second)
_ = lcd.Clear()
fmt.Println("calling TestTextDisplay")
errs := displaytest.TestTextDisplay(lcd, true)
fmt.Println("back from TestTextDisplay")
for _, e := range errs {
if !errors.Is(e, display.ErrNotImplemented) {
log.Println(e)
}
}
}
// Create a new HD44780 that uses the Adafruit I2C/SPI Backpack.
func ExampleNewAdafruitI2CBackpack() {
// Make sure periph is initialized.
if _, err := host.Init(); err != nil {
log.Fatal(err)
}
// Open default I²C bus.
bus, err := i2creg.Open("")
if err != nil {
log.Fatalf("failed to open I²C: %v", err)
}
defer bus.Close()
dev, err := hd44780.NewAdafruitI2CBackpack(bus, 0x20, 4, 20)
fmt.Println(dev.String())
if err != nil {
log.Fatal(err)
}
_ = dev.Clear()
_, _ = dev.WriteString("Hello")
fmt.Println("wrote hello")
time.Sleep(5 * time.Second)
fmt.Println("calling test text display")
_ = displaytest.TestTextDisplay(dev, true)
}
func ExampleNewAdafruitSPIBackpack() {
if _, err := host.Init(); err != nil {
log.Fatal(err)
}
pc, err := spireg.Open("")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
conn, err := pc.Connect(physic.MegaHertz, spi.Mode1, 8)
if err != nil {
log.Fatal(err)
}
display, err := hd44780.NewAdafruitSPIBackpack(conn, 2, 26)
if err != nil {
log.Fatal(err)
}
_ = display.Clear()
_, _ = display.WriteString("Hello")
time.Sleep(5 * time.Second)
_ = displaytest.TestTextDisplay(display, true)
}
func ExampleNewPCF857xBackpack() {
// Make sure periph is initialized.
if _, err := host.Init(); err != nil {
log.Fatal(err)
}
// Open default I²C bus.
bus, err := i2creg.Open("")
if err != nil {
log.Fatalf("failed to open I²C: %v", err)
}
defer bus.Close()
dev, err := hd44780.NewPCF857xBackpack(bus, pcf857x.DefaultAddress, 4, 20)
fmt.Println(dev.String())
if err != nil {
log.Fatal(err)
}
for range 5 {
fmt.Println("toggling backlight")
_ = dev.Backlight(0)
time.Sleep(500 * time.Millisecond)
_ = dev.Backlight(255)
time.Sleep(500 * time.Millisecond)
}
_ = dev.Clear()
_, _ = dev.WriteString("Hello")
fmt.Println("wrote hello")
time.Sleep(5 * time.Second)
fmt.Println("calling test text display")
_ = displaytest.TestTextDisplay(dev, true)
}

@ -1,4 +1,4 @@
// Copyright 2018 The Periph Authors. All rights reserved. // Copyright 2025 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0 // Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file. // that can be found in the LICENSE file.
@ -14,216 +14,427 @@ import (
"time" "time"
"periph.io/x/conn/v3" "periph.io/x/conn/v3"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/gpio" "periph.io/x/conn/v3/gpio"
) )
// lineTwo offset for the second line in the LCD buffer. type writeMode bool
const lineTwo = 0x40
// Dev is the 4-bit addressing device for HD-44780 type ifMode byte
type Dev struct {
// data pins
dataPins []gpio.PinOut
// register select pin const (
rsPin gpio.PinOut modeCommand writeMode = false
modeData writeMode = true
// enable pin cmdByte byte = 0xfe
mode4Bit ifMode = 0x04
mode8Bit ifMode = 0x08
)
// HD44780 is an implementation that supports writing to LCD displays using a
// gpio.Group for the data pins, and discrete pins for the reset, enable, and
// and backlight pins.
//
// Implements periph.io/conn/x/display/TextDisplay and display.DisplayBacklight
type HD44780 struct {
dataPins gpio.Group
resetPin gpio.PinOut
enablePin gpio.PinOut enablePin gpio.PinOut
blMono display.DisplayBacklight
blRGB display.DisplayRGBBacklight
mode ifMode
rows int
cols int
on bool
cursor bool
blink bool
lastWrite int64
} }
// New creates and initializes the LCD device const (
delayCommand time.Duration = 2000
delayCharacter time.Duration = 200
)
var rowConstants = [][]byte{{0, 0, 64}, {0, 0, 64, 20, 84}}
var clearScreen = []byte{cmdByte, 0x01}
var goHome = []byte{cmdByte, 0x02}
var setCursorPosition = []byte{cmdByte, 0x80}
// Return the row offset value
func getRowConstant(row, maxcols int) byte {
var offset int
if maxcols != 16 {
offset = 1
}
return rowConstants[offset][row]
}
// NewHD44780 takes a GPIO group, and gpio.PinOut for reset and enable. It
// returns an HD44780 device in an initialized state and ready for use.
//
// The first 4 or 8 pins of the data group must be connected to the data lines
// To use 4 bit mode, you would connect lines D4-D7 on the display, and for 8
// bit mode, D0-D7. If dataPinGroup is 8 or more pins, then it's assumed the
// display is connected using all 8 pins.
// //
// data - references to data pins // backlight should implement either display.DisplayBacklight or
// rs - rs pin // display.DisplayRGBBacklight. See GPIOMonoBacklight.
// e - strobe pin func NewHD44780(
func New(data []gpio.PinOut, rs, e gpio.PinOut) (*Dev, error) { dataPinGroup gpio.Group,
if len(data) != 4 { resetPin, enablePin gpio.PinOut,
return nil, fmt.Errorf("expected 4 data pins, passed %d", len(data)) backlight any,
rows, cols int) (*HD44780, error) {
mode := mode4Bit
if len(dataPinGroup.Pins()) >= 8 {
mode = mode8Bit
} }
dev := &Dev{
dataPins: data, lcd := &HD44780{
enablePin: e, dataPins: dataPinGroup,
rsPin: rs, resetPin: resetPin,
enablePin: enablePin,
mode: mode,
rows: rows,
cols: cols,
on: true,
} }
if err := dev.Reset(); err != nil { switch bl := backlight.(type) {
return nil, err case display.DisplayBacklight:
lcd.blMono = bl
case display.DisplayRGBBacklight:
lcd.blRGB = bl
} }
return dev, nil return lcd, lcd.init()
}
// Not supported by this device. Returns display.ErrNotImplemented
func (lcd *HD44780) AutoScroll(enabled bool) error {
// TODO: Wrap
return display.ErrNotImplemented
} }
// Reset resets the HC-44780 chipset, clears the screen buffer and moves cursor to the // Clears the screen and moves the cursor to the first position.
// home of screen (line 0, column 0). func (lcd *HD44780) Clear() error {
func (r *Dev) Reset() error { _, err := lcd.Write(clearScreen)
if err := r.clearBits(); err != nil {
return err return err
} }
delayMs(15) // Return the number of columns the display supports
func (lcd *HD44780) Cols() int {
return lcd.cols
}
if err := r.rsPin.Out(gpio.Low); err != nil { // Set the cursor mode. You can pass multiple arguments.
return err // Cursor(CursorOff, CursorUnderline)
func (lcd *HD44780) Cursor(modes ...display.CursorMode) (err error) {
var val = byte(0x08)
if lcd.on {
val |= 0x04
} }
if err := r.enablePin.Out(gpio.Low); err != nil { for _, mode := range modes {
return err switch mode {
case display.CursorOff:
// lcd.Write(underlineCursorOff)
lcd.blink = false
lcd.cursor = false
case display.CursorBlink:
lcd.blink = true
lcd.cursor = true
val |= 0x01
case display.CursorUnderline:
lcd.cursor = true
lcd.blink = true
// lcd.Write(underlineCursorOn)
val |= 0x02
case display.CursorBlock:
lcd.cursor = true
lcd.blink = true
val |= 0x01
default:
err = fmt.Errorf("HD44780 - unexpected cursor: %d", mode)
return
} }
if err := r.bulkSendData(resetSequence, r.write4Bits); err != nil {
return err
} }
_, err = lcd.Write([]byte{cmdByte, val & 0x0f})
return r.bulkSendData(initSequence, r.writeInstruction) return err
} }
func (r *Dev) String() string { // Move the cursor home (MinRow(),MinCol())
return "HD44870, 4 bit mode" func (lcd *HD44780) Home() (err error) {
_, err = lcd.Write(goHome)
return err
} }
// Halt clears the LCD screen // Return the min column position.
func (r *Dev) Halt() error { func (lcd *HD44780) MinCol() int {
if err := r.writeInstruction(0x01); err != nil { return 1
return err
}
delayMs(2)
return nil
} }
// SetCursor positions the cursor // Return the min row position.
// func (lcd *HD44780) MinRow() int {
// line - screen line, 0-based return 1
// column - column, 0-based
func (r *Dev) SetCursor(line uint8, column uint8) error {
return r.writeInstruction(0x80 | (line*lineTwo + column))
} }
// Print the data string // Move the cursor forward or backward.
// func (lcd *HD44780) Move(dir display.CursorDirection) (err error) {
// data string to display var val byte = 0x10
func (r *Dev) Print(data string) error { switch dir {
for _, v := range []byte(data) { case display.Backward:
if err := r.WriteChar(v); err != nil { case display.Forward:
return err val |= 0x04
case display.Down, display.Up:
fallthrough
default:
err = fmt.Errorf("hd44780: %w", display.ErrNotImplemented)
return
} }
} _, err = lcd.Write([]byte{cmdByte, val})
return nil return
} }
// WriteChar writes a single byte (character) at the cursor position. // Move the cursor to arbitrary position.
// func (lcd *HD44780) MoveTo(row, col int) (err error) {
// data - character code if row < lcd.MinRow() || row > lcd.rows || col < lcd.MinCol() || col > lcd.cols {
func (r *Dev) WriteChar(data uint8) error { err = fmt.Errorf("HD44780.MoveTo(%d,%d) value out of range", row, col)
if err := r.sendData(); err != nil { return
return err
}
if err := r.write4Bits(data >> 4); err != nil {
return err
}
if err := r.write4Bits(data); err != nil {
return err
} }
delayUs(10) var cmd = []byte{cmdByte, setCursorPosition[1]}
return nil cmd[1] |= getRowConstant(row, lcd.cols) + byte(col-1)
_, err = lcd.Write(cmd)
return
} }
// service methods // Return the number of rows the display supports.
func (lcd *HD44780) Rows() int {
var resetSequence = [][]uint{ return lcd.rows
{0x03, 50}, // init 1-st cycle
{0x03, 10}, // init 2-nd cycle
{0x03, 10}, // init 3-rd cycle
{0x02, 10}, // init finish
} }
var initSequence = [][]uint{ // Return info about the dsiplay.
{0x14, 0}, // 4-bit mode, 2 lines, 5x7 chars high func (lcd *HD44780) String() string {
{0x10, 0}, // disable display return fmt.Sprintf("HD44780 - Rows: %d, Cols: %d", lcd.rows, lcd.cols)
{0x01, 2000}, // clear screen
{0x06, 0}, // cursor shift right, no display move
{0x0c, 0}, // enable display no cursor
{0x01, 2000}, // clear screen
{0x02, 2000}, // cursor home
} }
func (r *Dev) bulkSendData(seq [][]uint, f func(_data uint8) error) error { // Turn the display on / off
for _, v := range seq { func (lcd *HD44780) Display(on bool) error {
if err := f(uint8(v[0])); err != nil { lcd.on = on
return err val := byte(0x08)
if on {
val |= 0x04
} }
if v[1] > 0 { if lcd.blink {
delayUs(v[1]) val |= 0x01
} }
if lcd.cursor {
val |= 0x02
} }
return nil _, err := lcd.Write([]byte{cmdByte, val})
return err
} }
func (r *Dev) clearBits() error { // Write a set of bytes to the display.
for _, v := range r.dataPins { func (lcd *HD44780) Write(p []byte) (n int, err error) {
if err := v.Out(gpio.Low); err != nil {
return err if len(p) == 0 {
return
} }
if p[0] == cmdByte {
n = len(p) - 1
err = lcd.sendCommand(p[1:])
return
}
lcd.delayWrite(delayCommand)
err = lcd.resetPin.Out(gpio.Level(modeData))
if err != nil {
return
} }
return nil
}
func (r *Dev) write4Bits(data uint8) error { for _, byteVal := range p {
for i, v := range r.dataPins { lcd.lastWrite = time.Now().UnixMicro()
if data&(1<<uint(i)) > 0 { if lcd.mode == mode4Bit {
if err := v.Out(gpio.High); err != nil { err = lcd.write4Bits(byteVal >> 4)
return err if err == nil {
err = lcd.write4Bits(byteVal & 0x0f)
} }
} else { } else {
if err := v.Out(gpio.Low); err != nil { err = lcd.write8Bits(byteVal)
return err
} }
if err != nil {
return
} }
n += 1
time.Sleep(delayCharacter * time.Microsecond)
} }
return r.strobe() lcd.lastWrite = time.Now().UnixMicro()
return
} }
func (r *Dev) sendInstruction() error { // Write a string output to the display.
if err := r.rsPin.Out(gpio.Low); err != nil { func (lcd *HD44780) WriteString(text string) (int, error) {
return err return lcd.Write([]byte(text))
}
// Halt clears the display, turns the backlight off, and turns the display off.
// Halt() is called for the data pins gpio.Group.
func (lcd *HD44780) Halt() error {
_ = lcd.Clear()
_ = lcd.Backlight(0)
_ = lcd.Display(false)
return lcd.dataPins.Halt()
}
// Set the backlight intensity.
func (lcd *HD44780) Backlight(intensity display.Intensity) error {
if lcd.blMono != nil {
return lcd.blMono.Backlight(intensity)
} else if lcd.blRGB != nil {
return lcd.blRGB.RGBBacklight(intensity, intensity, intensity)
} }
return r.enablePin.Out(gpio.Low) return display.ErrNotImplemented
} }
func (r *Dev) sendData() error { // For units that have an RGB Backlight, set the backlight color/intensity.
if err := r.rsPin.Out(gpio.High); err != nil { // The range of the values is 0-255.
return err func (lcd *HD44780) RGBBacklight(red, green, blue display.Intensity) error {
if lcd.blRGB != nil {
return lcd.blRGB.RGBBacklight(red, green, blue)
} else if lcd.blMono != nil {
return lcd.blMono.Backlight(red | green | blue)
}
return display.ErrNotImplemented
}
// delayWrite looks at the time of the last LCD write and if the specified
// microseconds period has not elapsed, it invokes time.Sleep() with the
// difference.
//
// Some I/O methods, like direct GPIO on a Pi are very fast, while other methods
// like i2c take longer. Without delays, on very fast I/O paths, the LCD will
// display garbage. The correct way to handle this would be to read the Busy
// flag on the LCD display. However, some backpacks don't have the capability to
// check the Busy flag because the R/W pin isn't connected. So, we can't
// correctly handle io delays. This handles the very fast interfaces, while not
// penalizing the slower ones with unnecessary delays.
//
// The value of lcd.lastWrite is updated to the current time by the call.
func (lcd *HD44780) delayWrite(microseconds time.Duration) {
diff := microseconds - time.Duration(time.Now().UnixMicro()-lcd.lastWrite)
if diff > 0 {
time.Sleep(time.Duration(diff) * time.Microsecond)
} }
return r.enablePin.Out(gpio.Low) lcd.lastWrite = time.Now().UnixMicro()
} }
func (r *Dev) writeInstruction(data uint8) error { // Init the display. The HD44780 has a fairly complex initialization cycle
if err := r.sendInstruction(); err != nil { // with variations for 4 and 8 pin mode.
func (lcd *HD44780) init() error {
/*
This is the startup sequence for the Hitachi HD44780U chip as
documented in the Datasheet.
*/
lcd.lastWrite = time.Now().UnixMicro()
if lcd.mode == mode4Bit {
var lineMode byte = 0x20
if lcd.rows > 1 {
lineMode |= 0x08
}
err := lcd.resetPin.Out(gpio.Level(modeCommand))
if err != nil {
return err return err
} }
// write high 4 bits err = lcd.enablePin.Out(gpio.Low)
if err := r.write4Bits(data >> 4); err != nil { if err != nil {
return err return err
} }
// write low bits err = lcd.write4Bits(0x03)
if err := r.write4Bits(data); err != nil { if err != nil {
return err return err
} }
delayUs(50) time.Sleep(4100 * time.Microsecond)
_ = lcd.write4Bits(0x03)
_ = lcd.write4Bits(0x03)
_ = lcd.write4Bits(0x02)
_ = lcd.sendCommand([]byte{lineMode})
} else {
// Init the display for 8 pin operation.
lineMode := byte(0x30) // Set the line mode and interface to 8 bits
if lcd.rows > 1 {
lineMode |= 0x08
}
err := lcd.resetPin.Out(gpio.Level(modeCommand))
if err != nil {
return err
}
err = lcd.enablePin.Out(gpio.Low)
if err != nil {
return err
}
_ = lcd.write8Bits(0x03 << 4) // Get it's attention
time.Sleep(4100 * time.Microsecond)
_ = lcd.write8Bits(0x03 << 4)
_ = lcd.write8Bits(0x03 << 4)
_ = lcd.write8Bits(lineMode)
_ = lcd.write8Bits(0x4) // set entry mode
}
_ = lcd.Cursor(display.CursorOff)
_ = lcd.Display(true)
_ = lcd.Clear()
_ = lcd.Home()
// If there's not a backlight, ignore the error.
_ = lcd.Backlight(0xff)
return nil return nil
} }
func (r *Dev) strobe() error { func (lcd *HD44780) sendCommand(commands []byte) error {
if err := r.enablePin.Out(gpio.High); err != nil { lcd.delayWrite(delayCommand)
err := lcd.resetPin.Out(gpio.Level(modeCommand))
if err != nil {
return err return err
} }
delayUs(2) for _, command := range commands {
return r.enablePin.Out(gpio.Low) if lcd.mode == mode4Bit {
err = lcd.write4Bits(byte(command >> 4))
if err == nil {
err = lcd.write4Bits(byte(command))
}
} else {
err = lcd.write8Bits(command)
}
if err != nil {
break
}
}
lcd.lastWrite = time.Now().UnixMicro()
return err
}
func (lcd *HD44780) write4Bits(value byte) error {
return lcd.writeBits(gpio.GPIOValue(value), 0x0f)
} }
func delayUs(ms uint) { func (lcd *HD44780) write8Bits(value byte) error {
time.Sleep(time.Duration(ms) * time.Microsecond) return lcd.writeBits(gpio.GPIOValue(value), 0xff)
} }
func delayMs(ms int) { func (lcd *HD44780) writeBits(value, mask gpio.GPIOValue) error {
time.Sleep(time.Duration(ms) * time.Millisecond) err := lcd.dataPins.Out(value, mask)
if err != nil {
return err
}
err = lcd.enablePin.Out(gpio.High)
if err == nil {
time.Sleep(2 * time.Microsecond)
err = lcd.enablePin.Out(gpio.Low)
}
return err
} }
var _ conn.Resource = &Dev{} var _ display.TextDisplay = &HD44780{}
var _ display.DisplayBacklight = &HD44780{}
var _ display.DisplayRGBBacklight = &HD44780{}
var _ conn.Resource = &HD44780{}

@ -0,0 +1,118 @@
// Copyright 2025 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package hd44780
import (
"errors"
"testing"
"time"
periphDisplay "periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/display/displaytest"
"periph.io/x/conn/v3/i2c/i2ctest"
)
func getLCD(t *testing.T, recordingName string) (*HD44780, error) {
bus := &i2ctest.Playback{Ops: recordingData[recordingName], DontPanic: true}
dev, err := NewAdafruitI2CBackpack(bus, 0x20, 2, 16)
if err != nil {
t.Fatal(err)
}
return dev, err
}
const (
testRows = 2
testCols = 16
)
var liveDevice = false
func TestBasic(t *testing.T) {
display, err := getLCD(t, "TestBasic")
if err != nil {
t.Fatal(err)
}
s := display.String()
t.Log(s)
if len(s) == 0 {
t.Error("display.String()")
}
_, err = display.WriteString("1234567890")
if err != nil {
t.Error(err)
}
err = display.MoveTo(2, 2)
if err != nil {
t.Error(err)
}
_, err = display.WriteString("2345678901")
if err != nil {
t.Error(err)
}
rows := display.Rows()
if rows != testRows {
t.Errorf("display.Rows() expected %d, received %d", testRows, rows)
}
cols := display.Cols()
if cols != testCols {
t.Errorf("display.Cols() expected %d, received %d", testCols, cols)
}
if liveDevice {
time.Sleep(5 * time.Second)
}
err = display.Halt()
if err != nil {
t.Error(err)
}
}
func TestInterface(t *testing.T) {
display, err := getLCD(t, "TestInterface")
if err != nil {
t.Fatal(err)
}
defer func() { _ = display.Halt() }()
errs := displaytest.TestTextDisplay(display, liveDevice)
for _, err := range errs {
if !errors.Is(err, periphDisplay.ErrNotImplemented) {
t.Error(err)
}
}
if liveDevice {
time.Sleep(5 * time.Second)
}
}
func TestBacklights(t *testing.T) {
display, err := getLCD(t, "TestBacklights")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = display.Halt()
})
err = display.Backlight(0)
if err != nil {
t.Error(err)
}
err = display.Backlight(0xff)
if err != nil {
t.Error(err)
}
for ix := range 3 {
colors := make([]periphDisplay.Intensity, 3)
colors[ix] = 0xff
err = display.RGBBacklight(colors[0], colors[1], colors[2])
if err != nil {
t.Error(err)
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,50 @@
// Copyright 2025 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package hd44780
import (
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/i2c"
"periph.io/x/devices/v3/pcf857x"
)
const (
// Name is the LCD pin name, and the integer value is the GPIO
// number (not physical) of the PCF8574 I2C GPIO Expander.
pcf_d4 = 4
pcf_d5 = 5
pcf_d6 = 6
pcf_d7 = 7
pcf_rsPin = 0
pcf_enablePin = 2
pcf_backlightPin = 3
pcf_rwPin = 1
)
// This function returns a display configured to use the pcf8574 i2c backpacks.
//
// # Product Information
//
// https://www.handsontec.com/dataspecs/I2C_2004_LCD.pdf
//
// This function creates a PCF8574 backpack device with the required pin
// configuration. To use this, get an I2C bus, and call this function with the
// bus, i2c address, number of rows, and columns.
func NewPCF857xBackpack(bus i2c.Bus, address uint16, rows, cols int) (*HD44780, error) {
pcf, err := pcf857x.New(bus, address, pcf857x.PCF8574)
if err != nil {
return nil, err
}
// R/W is connected on this backpack. Set it to low.
_ = pcf.Pins[pcf_rwPin].Out(gpio.Low)
// Create our gpio.Group
gr, _ := pcf.Group(pcf_d4, pcf_d5, pcf_d6, pcf_d7, pcf_rsPin, pcf_enablePin, pcf_backlightPin)
grPins := gr.Pins()
reset := grPins[4].(gpio.PinOut)
enable := grPins[5].(gpio.PinOut)
bl := grPins[6].(gpio.PinOut)
return NewHD44780(gr, reset, enable, NewBacklight(bl), rows, cols)
}
Loading…
Cancel
Save