mirror of https://github.com/periph/devices
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.
373 lines
8.3 KiB
Go
373 lines
8.3 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.
|
|
|
|
// The aip31068 is an HD44780 compatible I²C driver chip. It provides an I²C
|
|
// interface to an LCD. This is not a _backpack_ chip in the sense that it
|
|
// provides GPIO pins via an I²C interface. The I²C write commands go directly
|
|
// to the LCD display driver.
|
|
//
|
|
// Implements periph.io/x/conn/display/TextDisplay
|
|
//
|
|
// # Datasheet
|
|
//
|
|
// https://support.newhavendisplay.com/hc/en-us/article_attachments/4414498095511
|
|
package aip31068
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"periph.io/x/conn/v3"
|
|
"periph.io/x/conn/v3/display"
|
|
"periph.io/x/conn/v3/i2c"
|
|
)
|
|
|
|
const (
|
|
busyFlag byte = 0x80
|
|
cmdByte byte = 0xfe
|
|
dataByte byte = 0x40
|
|
moreControls byte = 0x80
|
|
packageName = "aip31068"
|
|
)
|
|
|
|
var (
|
|
ErrNotImplemented = fmt.Errorf("%s: %w", packageName, display.ErrNotImplemented)
|
|
|
|
rowConstants = [][]byte{{0, 0, 64}, {0, 0, 64, 20, 84}}
|
|
clearScreen = []byte{cmdByte, 0x01}
|
|
goHome = []byte{cmdByte, 0x02}
|
|
setCursorPosition = []byte{cmdByte, 0x80}
|
|
displayMode = []byte{cmdByte, 0x20}
|
|
defaultEntryMode = []byte{cmdByte, 0x06}
|
|
)
|
|
|
|
type Dev struct {
|
|
rows int
|
|
cols int
|
|
|
|
mu sync.Mutex
|
|
d *i2c.Dev
|
|
blink bool
|
|
on bool
|
|
cursor bool
|
|
backlight interface{}
|
|
}
|
|
|
|
func wrap(err error) error {
|
|
if err == nil || strings.HasPrefix(err.Error(), packageName) {
|
|
return err
|
|
}
|
|
return fmt.Errorf("%s: %w", packageName, err)
|
|
}
|
|
|
|
// New creates an aip31068 based LCD.
|
|
//
|
|
// backlight is a controller that manipulates the display backlight. If the
|
|
// display backlight is hard-wired on, then this can be nil. Otherwise, it
|
|
// should implement either display.DisplayBacklight or
|
|
// display.DisplayRGBBacklight.
|
|
func New(bus i2c.Bus,
|
|
address uint16,
|
|
backlight interface{},
|
|
rows,
|
|
cols int) (*Dev, error) {
|
|
|
|
dev := &Dev{
|
|
d: &i2c.Dev{Bus: bus, Addr: address},
|
|
backlight: backlight,
|
|
rows: rows,
|
|
cols: cols,
|
|
}
|
|
err := dev.init()
|
|
if err != nil {
|
|
dev = nil
|
|
}
|
|
return dev, wrap(err)
|
|
}
|
|
|
|
// Perform the display initialization routine,
|
|
func (dev *Dev) init() error {
|
|
// Set the lines display value
|
|
var modeToSet = []byte{cmdByte, displayMode[1]}
|
|
if dev.rows > 1 {
|
|
modeToSet[1] = modeToSet[1] | 0x08
|
|
}
|
|
_, err := dev.Write(modeToSet)
|
|
if err == nil {
|
|
err = dev.Display(true)
|
|
time.Sleep(40 * time.Microsecond)
|
|
}
|
|
if err == nil {
|
|
err = dev.Clear()
|
|
time.Sleep(2000 * time.Microsecond)
|
|
}
|
|
|
|
if err == nil {
|
|
err = dev.Home()
|
|
time.Sleep(40 * time.Microsecond)
|
|
}
|
|
|
|
if err == nil {
|
|
// Set the entry mode
|
|
_, err = dev.Write(defaultEntryMode)
|
|
}
|
|
if err == nil {
|
|
_ = dev.Backlight(0xff)
|
|
}
|
|
if err != nil {
|
|
fmt.Println("dev.init() returns", err)
|
|
err = wrap(err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Return the row offset value
|
|
func getRowConstant(row, maxcols int) byte {
|
|
var offset int
|
|
if maxcols != 16 {
|
|
offset = 1
|
|
}
|
|
return rowConstants[offset][row]
|
|
}
|
|
|
|
// Enable/Disable auto scroll
|
|
func (dev *Dev) AutoScroll(enabled bool) error {
|
|
return ErrNotImplemented
|
|
}
|
|
|
|
// Return the number of columns the display supports
|
|
func (dev *Dev) Cols() int {
|
|
return dev.cols
|
|
}
|
|
|
|
// Clear the display and move the cursor home.
|
|
func (dev *Dev) Clear() error {
|
|
_, err := dev.Write(clearScreen)
|
|
if err != nil {
|
|
err = wrap(err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Set the cursor mode. You can pass multiple arguments.
|
|
// Cursor(CursorOff, CursorUnderline)
|
|
func (dev *Dev) Cursor(modes ...display.CursorMode) (err error) {
|
|
var val = byte(0x08)
|
|
if dev.on {
|
|
val |= 0x04
|
|
}
|
|
for _, mode := range modes {
|
|
switch mode {
|
|
case display.CursorOff:
|
|
// dev.Write(underlineCursorOff)
|
|
dev.blink = false
|
|
dev.cursor = false
|
|
case display.CursorBlink:
|
|
dev.blink = true
|
|
dev.cursor = true
|
|
val |= 0x01
|
|
case display.CursorUnderline:
|
|
dev.cursor = true
|
|
dev.blink = true
|
|
// dev.Write(underlineCursorOn)
|
|
val |= 0x02
|
|
case display.CursorBlock:
|
|
dev.cursor = true
|
|
dev.blink = true
|
|
val |= 0x01
|
|
default:
|
|
err = fmt.Errorf("Waveshare1602 - unexpected cursor: %d", mode)
|
|
return
|
|
}
|
|
}
|
|
_, err = dev.Write([]byte{cmdByte, val & 0x0f})
|
|
return wrap(err)
|
|
|
|
}
|
|
|
|
// Turn the display on / off
|
|
func (dev *Dev) Display(on bool) error {
|
|
dev.on = on
|
|
val := byte(0x08)
|
|
if on {
|
|
val |= 0x04
|
|
}
|
|
if dev.blink {
|
|
val |= 0x01
|
|
}
|
|
if dev.cursor {
|
|
val |= 0x02
|
|
}
|
|
_, err := dev.Write([]byte{cmdByte, val})
|
|
return err
|
|
|
|
}
|
|
|
|
// Halt clears the display, turns the backlight off, and turns the display off.
|
|
// Halt() is called for the data pins gpio.Group.
|
|
func (dev *Dev) Halt() error {
|
|
_ = dev.Clear()
|
|
_ = dev.Display(false)
|
|
_ = dev.Backlight(0)
|
|
return nil
|
|
}
|
|
|
|
// Move the cursor home (MinRow(),MinCol())
|
|
func (dev *Dev) Home() error {
|
|
_, err := dev.Write(goHome)
|
|
return err
|
|
}
|
|
|
|
// Return the min column position.
|
|
func (dev *Dev) MinCol() int {
|
|
return 1
|
|
}
|
|
|
|
// Return the min row position.
|
|
func (dev *Dev) MinRow() int {
|
|
return 1
|
|
}
|
|
|
|
// Move the cursor forward or backward.
|
|
func (dev *Dev) 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 = ErrNotImplemented
|
|
return
|
|
}
|
|
_, err = dev.Write([]byte{cmdByte, val})
|
|
err = wrap(err)
|
|
return
|
|
}
|
|
|
|
// Move the cursor to arbitrary position.
|
|
func (dev *Dev) MoveTo(row, col int) (err error) {
|
|
if row < dev.MinRow() || row > dev.rows || col < dev.MinCol() || col > dev.cols {
|
|
err = fmt.Errorf("%s.MoveTo(%d,%d) value out of range", packageName, row, col)
|
|
return
|
|
}
|
|
var cmd = []byte{cmdByte, setCursorPosition[1]}
|
|
cmd[1] |= getRowConstant(row, dev.cols) + byte(col-1)
|
|
_, err = dev.Write(cmd)
|
|
err = wrap(err)
|
|
return err
|
|
}
|
|
|
|
// Return the number of rows the display supports.
|
|
func (dev *Dev) Rows() int {
|
|
return dev.rows
|
|
}
|
|
|
|
func (dev *Dev) String() string {
|
|
return fmt.Sprintf("%s Rows: %d Cols: %d", packageName, dev.rows, dev.cols)
|
|
}
|
|
|
|
// Read the busy flag to make sure it's clear to write. It's a little wonky
|
|
// initially but then smooths out, so it makes a best effort and ignores errors.
|
|
func (dev *Dev) waitForFree() {
|
|
tLimit := time.Now().Add(3 * time.Millisecond)
|
|
w := make([]byte, 2)
|
|
r := make([]byte, 1)
|
|
for time.Now().Before(tLimit) {
|
|
err := dev.d.Tx(w, r)
|
|
if err == nil && (r[0]&busyFlag) == 0 {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Microsecond)
|
|
}
|
|
}
|
|
|
|
// Write a set of bytes to the display. This routine handles control
|
|
// and data characters transparently.
|
|
func (dev *Dev) Write(p []byte) (n int, err error) {
|
|
dev.mu.Lock()
|
|
defer dev.mu.Unlock()
|
|
dev.waitForFree()
|
|
|
|
lastControl := -1
|
|
for i := range len(p) {
|
|
if p[i] == cmdByte {
|
|
lastControl = i
|
|
}
|
|
}
|
|
|
|
w := make([]byte, 0, len(p))
|
|
|
|
for pos := 0; pos < len(p); {
|
|
|
|
// So, when we're writing, we need to send a control byte first
|
|
// that says type data, or cmd. We then send the bytes. If the
|
|
// type changes, then we need to send a new control byte.
|
|
//
|
|
// If there are more control bytes, then the control byte has bit 7
|
|
// set, and we send a control byte for each character sent.
|
|
var controlByte byte = 0x00
|
|
if p[pos] == cmdByte {
|
|
pos += 1
|
|
} else {
|
|
controlByte |= dataByte
|
|
}
|
|
if pos < lastControl {
|
|
controlByte |= moreControls
|
|
}
|
|
|
|
if (pos - 1) <= lastControl {
|
|
w = append(w, controlByte)
|
|
}
|
|
|
|
w = append(w, p[pos])
|
|
pos += 1
|
|
}
|
|
err = dev.d.Tx(w, nil)
|
|
if err == nil {
|
|
n = len(p)
|
|
}
|
|
err = wrap(err)
|
|
return n, err
|
|
}
|
|
|
|
// Write a string output to the display.
|
|
func (dev *Dev) WriteString(text string) (n int, err error) {
|
|
return dev.Write([]byte(text))
|
|
}
|
|
|
|
// Set the backlight intensity.
|
|
func (dev *Dev) Backlight(intensity display.Intensity) error {
|
|
switch bl := dev.backlight.(type) {
|
|
case display.DisplayBacklight:
|
|
return bl.Backlight(intensity)
|
|
case display.DisplayRGBBacklight:
|
|
return bl.RGBBacklight(intensity, intensity, intensity)
|
|
default:
|
|
return ErrNotImplemented
|
|
}
|
|
}
|
|
|
|
// For units that have an RGB Backlight, set the backlight color/intensity.
|
|
// The range of the values is 0-255.
|
|
func (dev *Dev) RGBBacklight(red, green, blue display.Intensity) error {
|
|
switch bl := dev.backlight.(type) {
|
|
case display.DisplayRGBBacklight:
|
|
return bl.RGBBacklight(red, green, blue)
|
|
case display.DisplayBacklight:
|
|
return bl.Backlight(red | green | blue)
|
|
default:
|
|
return ErrNotImplemented
|
|
}
|
|
}
|
|
|
|
var _ conn.Resource = &Dev{}
|
|
var _ display.TextDisplay = &Dev{}
|
|
var _ display.DisplayBacklight = &Dev{}
|
|
var _ display.DisplayRGBBacklight = &Dev{}
|