You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/hd44780/hd44780.go

441 lines
11 KiB
Go

// 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 controls the Hitachi LCD display chipset HD-44780
//
// # Datasheet
//
// https://www.sparkfun.com/datasheets/LCD/HD44780.pdf
package hd44780
import (
"fmt"
"time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/gpio"
)
type writeMode bool
type ifMode byte
const (
modeCommand writeMode = false
modeData writeMode = true
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
blMono display.DisplayBacklight
blRGB display.DisplayRGBBacklight
mode ifMode
rows int
cols int
on bool
cursor bool
blink bool
lastWrite int64
}
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.
//
// backlight should implement either display.DisplayBacklight or
// display.DisplayRGBBacklight. See GPIOMonoBacklight.
func NewHD44780(
dataPinGroup gpio.Group,
resetPin, enablePin gpio.PinOut,
backlight any,
rows, cols int) (*HD44780, error) {
mode := mode4Bit
if len(dataPinGroup.Pins()) >= 8 {
mode = mode8Bit
}
lcd := &HD44780{
dataPins: dataPinGroup,
resetPin: resetPin,
enablePin: enablePin,
mode: mode,
rows: rows,
cols: cols,
on: true,
}
switch bl := backlight.(type) {
case display.DisplayBacklight:
lcd.blMono = bl
case display.DisplayRGBBacklight:
lcd.blRGB = bl
}
return lcd, lcd.init()
}
// Not supported by this device. Returns display.ErrNotImplemented
func (lcd *HD44780) AutoScroll(enabled bool) error {
// TODO: Wrap
return display.ErrNotImplemented
}
// Clears the screen and moves the cursor to the first position.
func (lcd *HD44780) Clear() error {
_, err := lcd.Write(clearScreen)
return err
}
// Return the number of columns the display supports
func (lcd *HD44780) Cols() int {
return lcd.cols
}
// Set the cursor mode. You can pass multiple arguments.
// Cursor(CursorOff, CursorUnderline)
func (lcd *HD44780) Cursor(modes ...display.CursorMode) (err error) {
var val = byte(0x08)
if lcd.on {
val |= 0x04
}
for _, mode := range modes {
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
}
}
_, err = lcd.Write([]byte{cmdByte, val & 0x0f})
return err
}
// Move the cursor home (MinRow(),MinCol())
func (lcd *HD44780) Home() (err error) {
_, err = lcd.Write(goHome)
return err
}
// Return the min column position.
func (lcd *HD44780) MinCol() int {
return 1
}
// Return the min row position.
func (lcd *HD44780) MinRow() int {
return 1
}
// Move the cursor forward or backward.
func (lcd *HD44780) Move(dir display.CursorDirection) (err error) {
var val byte = 0x10
switch dir {
case display.Backward:
case display.Forward:
val |= 0x04
case display.Down, display.Up:
fallthrough
default:
err = fmt.Errorf("hd44780: %w", display.ErrNotImplemented)
return
}
_, err = lcd.Write([]byte{cmdByte, val})
return
}
// Move the cursor to arbitrary position.
func (lcd *HD44780) MoveTo(row, col int) (err error) {
if row < lcd.MinRow() || row > lcd.rows || col < lcd.MinCol() || col > lcd.cols {
err = fmt.Errorf("HD44780.MoveTo(%d,%d) value out of range", row, col)
return
}
var cmd = []byte{cmdByte, setCursorPosition[1]}
cmd[1] |= getRowConstant(row, lcd.cols) + byte(col-1)
_, err = lcd.Write(cmd)
return
}
// Return the number of rows the display supports.
func (lcd *HD44780) Rows() int {
return lcd.rows
}
// Return info about the dsiplay.
func (lcd *HD44780) String() string {
return fmt.Sprintf("HD44780 - Rows: %d, Cols: %d", lcd.rows, lcd.cols)
}
// Turn the display on / off
func (lcd *HD44780) Display(on bool) error {
lcd.on = on
val := byte(0x08)
if on {
val |= 0x04
}
if lcd.blink {
val |= 0x01
}
if lcd.cursor {
val |= 0x02
}
_, err := lcd.Write([]byte{cmdByte, val})
return err
}
// Write a set of bytes to the display.
func (lcd *HD44780) Write(p []byte) (n int, err error) {
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
}
for _, byteVal := range p {
lcd.lastWrite = time.Now().UnixMicro()
if lcd.mode == mode4Bit {
err = lcd.write4Bits(byteVal >> 4)
if err == nil {
err = lcd.write4Bits(byteVal & 0x0f)
}
} else {
err = lcd.write8Bits(byteVal)
}
if err != nil {
return
}
n += 1
time.Sleep(delayCharacter * time.Microsecond)
}
lcd.lastWrite = time.Now().UnixMicro()
return
}
// Write a string output to the display.
func (lcd *HD44780) WriteString(text string) (int, error) {
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 display.ErrNotImplemented
}
// For units that have an RGB Backlight, set the backlight color/intensity.
// The range of the values is 0-255.
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)
}
lcd.lastWrite = time.Now().UnixMicro()
}
// Init the display. The HD44780 has a fairly complex initialization cycle
// 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
}
err = lcd.enablePin.Out(gpio.Low)
if err != nil {
return err
}
err = lcd.write4Bits(0x03)
if err != nil {
return err
}
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
}
func (lcd *HD44780) sendCommand(commands []byte) error {
lcd.delayWrite(delayCommand)
err := lcd.resetPin.Out(gpio.Level(modeCommand))
if err != nil {
return err
}
for _, command := range commands {
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 (lcd *HD44780) write8Bits(value byte) error {
return lcd.writeBits(gpio.GPIOValue(value), 0xff)
}
func (lcd *HD44780) writeBits(value, mask gpio.GPIOValue) error {
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 _ display.TextDisplay = &HD44780{}
var _ display.DisplayBacklight = &HD44780{}
var _ display.DisplayRGBBacklight = &HD44780{}
var _ conn.Resource = &HD44780{}