Initial Add of Waveshare 1602 Support

pull/100/head
George Sexton 1 year ago
parent f007d15374
commit a66a398bcb

@ -0,0 +1,372 @@
// 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{}

@ -0,0 +1,105 @@
// 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 aip31068_test
import (
"errors"
"testing"
"time"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/display/displaytest"
"periph.io/x/conn/v3/i2c/i2ctest"
"periph.io/x/devices/v3/aip31068"
"periph.io/x/devices/v3/waveshare1602"
)
var pause time.Duration = 0
var liveDevice bool
func getDev(recordingName string) (*aip31068.Dev, error) {
bus := &i2ctest.Playback{Ops: recordingData[recordingName], DontPanic: true}
dev, err := waveshare1602.New(bus, waveshare1602.LCD1602RGBBacklight, 2, 16)
return dev, err
}
func TestBasic(t *testing.T) {
dev, err := getDev("TestBasic")
if err != nil {
t.Fatal(err)
}
s := dev.String()
if len(s) == 0 {
t.Error("error on String()")
}
t.Log(s)
t.Cleanup(func() {
_ = dev.Halt()
})
err = dev.Clear()
if err != nil {
t.Error(err)
}
err = dev.Backlight(0xff)
if err != nil {
t.Error(err)
}
n, err := dev.WriteString("aip31068")
if err != nil {
t.Error(err)
}
if n != 8 {
t.Error("expected 8 bytes written")
}
time.Sleep(5 * pause)
}
func TestComplete(t *testing.T) {
dev, err := getDev("TestComplete")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = dev.Halt()
})
testErrs := displaytest.TestTextDisplay(dev, liveDevice)
for _, err := range testErrs {
if !errors.Is(err, display.ErrNotImplemented) {
t.Error(err)
}
}
}
func TestBacklights(t *testing.T) {
dev, err := getDev("TestBacklights")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = dev.Halt()
})
for ix := range 3 {
leds := make([]display.Intensity, 3)
leds[ix] = 0xff
err = dev.RGBBacklight(leds[0], leds[1], leds[2])
if err != nil {
t.Error(err)
}
time.Sleep(pause)
}
err = dev.Backlight(0)
if err != nil {
t.Error(err)
}
time.Sleep(pause)
err = dev.Backlight(1)
if err != nil {
t.Error(err)
}
time.Sleep(pause)
}

@ -0,0 +1,159 @@
package aip31068_test
import (
"periph.io/x/conn/v3/i2c/i2ctest"
)
// Auto-Generated by i2ctest.BusTest
var recordingData = map[string][]i2ctest.IO{
"TestBacklights": {
{Addr: 0x60, W: []uint8{0x0, 0x81}},
{Addr: 0x60, W: []uint8{0x1, 0x5}},
{Addr: 0x3e, W: []uint8{0x0, 0x28}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x2}},
{Addr: 0x3e, W: []uint8{0x0, 0x6}},
{Addr: 0x60, W: []uint8{0x8, 0x15}},
{Addr: 0x60, W: []uint8{0x4, 0xff}},
{Addr: 0x60, W: []uint8{0x8, 0x20}},
{Addr: 0x60, W: []uint8{0x8, 0x4}},
{Addr: 0x60, W: []uint8{0x8, 0x1}},
{Addr: 0x60, W: []uint8{0x8, 0x0}},
{Addr: 0x60, W: []uint8{0x2, 0x1}},
{Addr: 0x60, W: []uint8{0x3, 0x1}},
{Addr: 0x60, W: []uint8{0x4, 0x1}},
{Addr: 0x60, W: []uint8{0x8, 0x2a}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x8}},
{Addr: 0x60, W: []uint8{0x8, 0x0}}},
"TestBasic": {
{Addr: 0x60, W: []uint8{0x0, 0x81}},
{Addr: 0x60, W: []uint8{0x1, 0x5}},
{Addr: 0x3e, W: []uint8{0x0, 0x28}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x2}},
{Addr: 0x3e, W: []uint8{0x0, 0x6}},
{Addr: 0x60, W: []uint8{0x8, 0x15}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x60, W: []uint8{0x2, 0xff}},
{Addr: 0x60, W: []uint8{0x3, 0xff}},
{Addr: 0x60, W: []uint8{0x4, 0xff}},
{Addr: 0x60, W: []uint8{0x8, 0x2a}},
{Addr: 0x3e, W: []uint8{0x40, 0x61, 0x69, 0x70, 0x33, 0x31, 0x30, 0x36, 0x38}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x8}},
{Addr: 0x60, W: []uint8{0x8, 0x0}}},
"TestComplete": {
{Addr: 0x60, W: []uint8{0x0, 0x81}},
{Addr: 0x60, W: []uint8{0x1, 0x5}},
{Addr: 0x3e, W: []uint8{0x0, 0x28}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x2}},
{Addr: 0x3e, W: []uint8{0x0, 0x6}},
{Addr: 0x60, W: []uint8{0x8, 0x15}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x40, 0x61, 0x69, 0x70, 0x33, 0x31, 0x30, 0x36, 0x38, 0x20, 0x52, 0x6f, 0x77, 0x73, 0x3a, 0x20, 0x32, 0x20, 0x43, 0x6f, 0x6c, 0x73, 0x3a, 0x20, 0x31, 0x36}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x40, 0x41, 0x75, 0x74, 0x6f, 0x20, 0x53, 0x63, 0x72, 0x6f, 0x6c, 0x6c, 0x20, 0x54, 0x65, 0x73, 0x74}},
{Addr: 0x3e, W: []uint8{0x0, 0x80}},
{Addr: 0x3e, W: []uint8{0x40, 0x41}},
{Addr: 0x3e, W: []uint8{0x40, 0x42}},
{Addr: 0x3e, W: []uint8{0x40, 0x43}},
{Addr: 0x3e, W: []uint8{0x40, 0x44}},
{Addr: 0x3e, W: []uint8{0x40, 0x45}},
{Addr: 0x3e, W: []uint8{0x40, 0x20}},
{Addr: 0x3e, W: []uint8{0x40, 0x47}},
{Addr: 0x3e, W: []uint8{0x40, 0x48}},
{Addr: 0x3e, W: []uint8{0x40, 0x49}},
{Addr: 0x3e, W: []uint8{0x40, 0x4a}},
{Addr: 0x3e, W: []uint8{0x40, 0x20}},
{Addr: 0x3e, W: []uint8{0x40, 0x4c}},
{Addr: 0x3e, W: []uint8{0x40, 0x4d}},
{Addr: 0x3e, W: []uint8{0x40, 0x4e}},
{Addr: 0x3e, W: []uint8{0x40, 0x4f}},
{Addr: 0x3e, W: []uint8{0x40, 0x20}},
{Addr: 0x3e, W: []uint8{0x0, 0xc0}},
{Addr: 0x3e, W: []uint8{0x40, 0x41}},
{Addr: 0x3e, W: []uint8{0x40, 0x42}},
{Addr: 0x3e, W: []uint8{0x40, 0x43}},
{Addr: 0x3e, W: []uint8{0x40, 0x44}},
{Addr: 0x3e, W: []uint8{0x40, 0x45}},
{Addr: 0x3e, W: []uint8{0x40, 0x20}},
{Addr: 0x3e, W: []uint8{0x40, 0x47}},
{Addr: 0x3e, W: []uint8{0x40, 0x48}},
{Addr: 0x3e, W: []uint8{0x40, 0x49}},
{Addr: 0x3e, W: []uint8{0x40, 0x4a}},
{Addr: 0x3e, W: []uint8{0x40, 0x20}},
{Addr: 0x3e, W: []uint8{0x40, 0x4c}},
{Addr: 0x3e, W: []uint8{0x40, 0x4d}},
{Addr: 0x3e, W: []uint8{0x40, 0x4e}},
{Addr: 0x3e, W: []uint8{0x40, 0x4f}},
{Addr: 0x3e, W: []uint8{0x40, 0x20}},
{Addr: 0x3e, W: []uint8{0x40, 0x61, 0x75, 0x74, 0x6f, 0x20, 0x73, 0x63, 0x72, 0x6f, 0x6c, 0x6c, 0x20, 0x68, 0x61, 0x70, 0x70, 0x65, 0x6e}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x40, 0x41, 0x62, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x65, 0x20, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x80}},
{Addr: 0x3e, W: []uint8{0x40, 0x28, 0x31, 0x2c, 0x31, 0x29}},
{Addr: 0x3e, W: []uint8{0x0, 0xc1}},
{Addr: 0x3e, W: []uint8{0x40, 0x28, 0x32, 0x2c, 0x32, 0x29}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x80}},
{Addr: 0x3e, W: []uint8{0x40, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x4f, 0x66, 0x66}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x80}},
{Addr: 0x3e, W: []uint8{0x40, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x55, 0x6e, 0x64, 0x65, 0x72, 0x6c, 0x69, 0x6e, 0x65}},
{Addr: 0x3e, W: []uint8{0x0, 0xe}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x80}},
{Addr: 0x3e, W: []uint8{0x40, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x42, 0x6c, 0x6f, 0x63, 0x6b}},
{Addr: 0x3e, W: []uint8{0x0, 0xd}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x80}},
{Addr: 0x3e, W: []uint8{0x40, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x3a, 0x20, 0x42, 0x6c, 0x69, 0x6e, 0x6b}},
{Addr: 0x3e, W: []uint8{0x0, 0xd}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x40, 0x54, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x3e}},
{Addr: 0x3e, W: []uint8{0x0, 0x14}},
{Addr: 0x3e, W: []uint8{0x0, 0x14}},
{Addr: 0x3e, W: []uint8{0x40, 0x30}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x40, 0x31}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x40, 0x32}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x40, 0x33}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x40, 0x34}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x40, 0x35}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x40, 0x36}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x40, 0x37}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x40, 0x38}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x40, 0x39}},
{Addr: 0x3e, W: []uint8{0x0, 0x10}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x40, 0x53, 0x65, 0x74, 0x20, 0x64, 0x65, 0x76, 0x20, 0x6f, 0x66, 0x66}},
{Addr: 0x3e, W: []uint8{0x0, 0x8}},
{Addr: 0x3e, W: []uint8{0x0, 0xc}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x40, 0x53, 0x65, 0x74, 0x20, 0x64, 0x65, 0x76, 0x20, 0x6f, 0x6e}},
{Addr: 0x3e, W: []uint8{0x0, 0x1}},
{Addr: 0x3e, W: []uint8{0x0, 0x8}},
{Addr: 0x60, W: []uint8{0x8, 0x0}}},
}

@ -0,0 +1,212 @@
// 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 PCA9633 is a four-channel LED PWM controller. Additionally, it provides
// features for dimming and blink.
//
// # Datasheet
//
// https://www.nxp.com/docs/en/data-sheet/PCA9633.pdf
package pca9633
import (
"fmt"
"time"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/i2c"
)
type LEDStructure byte
const (
// LEDs are connected in OpenDrain format
STRUCT_OPENDRAIN LEDStructure = iota
// LEDs are connected in TotemPole format.
STRUCT_TOTEMPOLE
)
type LEDMode byte
const (
MODE_FULL_OFF LEDMode = iota
MODE_FULL_ON
// The brightness of the LED is controlled by the PWM setting.
MODE_PWM
// The brightness of the LED is controlled by the PWM setting AND the group
// PWM/blinking options.
MODE_PWM_PLUS_GROUP
)
const (
// Register offsets from the datasheet
_DEV_MODE1 byte = iota
_DEV_MODE2
_PWM0
_PWM1
_PWM2
_PWM3
_GRPPWM
_GRPFREQ
_LED_MODE
)
const (
_DEV_MODE_BLINK byte = 0x20
_DEV_MODE_TOTEM byte = 0x08
_DEV_MODE_INVERT byte = 0x10
_DEV_MODE2_DEFAULT byte = 0x05
_DEV_MODE1_DEFAULT byte = 0x81
)
// Dev represents a PCA9633 LED PWM Controller.
type Dev struct {
d *i2c.Dev
modes []LEDMode
// bit settings for device mode register 2
devMode2 byte
}
// New returns an initialized PCA9633 device ready for use.
func New(bus i2c.Bus, address uint16, ledStructure LEDStructure) (*Dev, error) {
dev := &Dev{d: &i2c.Dev{Bus: bus, Addr: address},
modes: make([]LEDMode, 4),
devMode2: _DEV_MODE2_DEFAULT}
if ledStructure == STRUCT_TOTEMPOLE {
dev.devMode2 |= _DEV_MODE_TOTEM
}
return dev, dev.init()
}
func (dev *Dev) init() error {
// We have to write 0 to bit 5 to turn on the PWM oscillator...
err := dev.d.Tx([]byte{_DEV_MODE1, _DEV_MODE1_DEFAULT}, nil)
if err == nil {
err = dev.d.Tx([]byte{_DEV_MODE2, dev.devMode2}, nil)
if err == nil {
err = dev.SetModes(MODE_FULL_OFF, MODE_FULL_OFF, MODE_FULL_OFF, MODE_FULL_OFF)
}
}
return wrap(err)
}
func wrap(err error) error {
if err == nil {
return nil
}
return fmt.Errorf("pca9633: %w", err)
}
// Halt stops all LED display by setting them all to MODE_FULL_OFF. Implements
// conn.Resource
func (dev *Dev) Halt() error {
return dev.SetModes(MODE_FULL_OFF, MODE_FULL_OFF, MODE_FULL_OFF, MODE_FULL_OFF)
}
// Set the output intensity for LEDs. If intensity is 0, the LED is set to full
// off. If intensity==255, the LED is set to full on, otherwise the LED is PWMd
// to the desired intensity.
func (dev *Dev) Out(intensities ...display.Intensity) error {
newModes := make([]LEDMode, len(dev.modes))
copy(newModes, dev.modes)
for ix := range len(intensities) {
if intensities[ix] == 0 {
newModes[ix] = MODE_FULL_OFF
} else if intensities[ix] >= 0xff && dev.modes[ix] == MODE_FULL_OFF {
newModes[ix] = MODE_FULL_ON
} else {
if dev.modes[ix] != MODE_PWM && dev.modes[ix] != MODE_PWM_PLUS_GROUP {
newModes[ix] = MODE_PWM
}
err := dev.d.Tx([]byte{_PWM0 + byte(ix), byte(intensities[ix])}, nil)
if err != nil {
return wrap(err)
}
}
}
return dev.SetModes(newModes...)
}
// SetGroupPWMBlink sets the group level PWM value, and optionally, a blink
// duration. Blink duration can range from 41,666 uS to 10.625 S. If 0, blink
// is disabled.
//
// Refer to the datasheet on this functionality. If the mode is not blink,
// then it's group PWM, but group PWM is only applied if the individual led
// mode is MODE_PWM_PLUS_GROUP
func (dev *Dev) SetGroupPWMBlink(intensity display.Intensity, blinkDuration time.Duration) error {
periodIncrement := 41_666 * time.Microsecond
newDevMode := dev.devMode2
if blinkDuration >= periodIncrement {
// calculate the duration value.
var blinkSetting int
cnt := int(blinkDuration / periodIncrement)
if cnt < 0 {
blinkSetting = 0
} else if cnt > 0xff {
blinkSetting = 0xff
} else {
blinkSetting = cnt
}
if blinkSetting == 0 {
newDevMode ^= _DEV_MODE_BLINK
} else {
err := dev.d.Tx([]byte{_GRPFREQ, byte(blinkSetting)}, nil)
if err != nil {
return wrap(err)
}
if dev.devMode2&_DEV_MODE_BLINK != _DEV_MODE_BLINK {
newDevMode |= _DEV_MODE_BLINK
}
}
} else {
if dev.devMode2&_DEV_MODE_BLINK == _DEV_MODE_BLINK {
newDevMode ^= _DEV_MODE_BLINK
}
}
if newDevMode != dev.devMode2 {
err := dev.d.Tx([]byte{_DEV_MODE2, newDevMode}, nil)
if err != nil {
return wrap(err)
}
dev.devMode2 = newDevMode
}
err := dev.d.Tx([]byte{_GRPPWM, byte(intensity)}, nil)
return wrap(err)
}
// SetInvert allows you to easily invert the meaning of the PWM values. This
// is useful if you're driving LEDs with a transistor or other device that
// inverts the output.
func (dev *Dev) SetInvert(invert bool) error {
if invert {
dev.devMode2 |= _DEV_MODE_INVERT
} else {
dev.devMode2 ^= _DEV_MODE_INVERT
}
err := dev.d.Tx([]byte{_DEV_MODE2, dev.devMode2}, nil)
return wrap(err)
}
// SetModes sets the output mode of LEDs. The value for modes should be
// one of the LEDMode constants.
func (dev *Dev) SetModes(modes ...LEDMode) error {
var mode byte
var changed bool
for i := range len(modes) {
changed = changed || (modes[i] != dev.modes[i])
mode |= (byte(modes[i]) << (i * 2))
}
if !changed {
return nil
}
copy(dev.modes, modes)
err := dev.d.Tx([]byte{_LED_MODE, mode}, nil)
return wrap(err)
}
func (dev *Dev) String() string {
return fmt.Sprintf("PCA9633::%#v", dev.d)
}

@ -0,0 +1,89 @@
// 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 pca9633
import (
"testing"
"time"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/i2c/i2ctest"
)
var recordingData = map[string][]i2ctest.IO{
"TestBasic": {
{Addr: 0x60, W: []uint8{0x0, 0x81}},
{Addr: 0x60, W: []uint8{0x1, 0x5}},
{Addr: 0x60, W: []uint8{0x8, 0x1}},
{Addr: 0x60, W: []uint8{0x8, 0x4}},
{Addr: 0x60, W: []uint8{0x8, 0x10}},
{Addr: 0x60, W: []uint8{0x8, 0x40}},
{Addr: 0x60, W: []uint8{0x1, 0x15}},
{Addr: 0x60, W: []uint8{0x1, 0x5}},
{Addr: 0x60, W: []uint8{0x6, 0x80}},
{Addr: 0x60, W: []uint8{0x8, 0x3f}},
{Addr: 0x60, W: []uint8{0x2, 0xff}},
{Addr: 0x60, W: []uint8{0x3, 0xff}},
{Addr: 0x60, W: []uint8{0x4, 0xff}},
{Addr: 0x60, W: []uint8{0x7, 0x30}},
{Addr: 0x60, W: []uint8{0x1, 0x25}},
{Addr: 0x60, W: []uint8{0x6, 0x80}},
{Addr: 0x60, W: []uint8{0x8, 0x0}}},
}
func TestBasic(t *testing.T) {
bus := &i2ctest.Playback{Ops: recordingData["TestBasic"]}
dev, err := New(bus, 0x60, STRUCT_OPENDRAIN)
if err != nil {
t.Fatal(err)
}
for i := range 4 {
values := make([]display.Intensity, 4)
values[i] = 0xff
err = dev.Out(values...)
if err != nil {
t.Error(err)
}
}
err = dev.SetInvert(true)
if err != nil {
t.Error(err)
}
err = dev.SetInvert(false)
if err != nil {
t.Error(err)
}
err = dev.SetGroupPWMBlink(0x80, 0)
if err != nil {
t.Error(err)
}
err = dev.SetModes(MODE_PWM_PLUS_GROUP, MODE_PWM_PLUS_GROUP, MODE_PWM_PLUS_GROUP, MODE_FULL_OFF)
if err != nil {
t.Error(err)
}
err = dev.Out(0xff, 0xff, 0xff)
if err != nil {
t.Error(err)
}
err = dev.SetGroupPWMBlink(0x80, 2*time.Second)
if err != nil {
t.Error(err)
}
s := dev.String()
if len(s) == 0 {
t.Error("empty string")
}
err = dev.Halt()
if err != nil {
t.Error(err)
}
}

@ -0,0 +1,83 @@
// 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 Waveshare 1602 LCD is a 2 line by 16 column LCD display. It's available
// in multiple variants:
//
// - LCD1602 5V Blue Backlight
// - LCD1602 3.3V Yellow Backlight
// - LCD1602 3.3V Blue Backlight
//
// These are bare LCD displays with no backpack. They have an hd44780 compatible
// driver chip. Use the driver located in the [hd44780] package.
//
// - LCD1602 I²C Module, White color w/ Blue Background, 16x2 characters, 3.3V/5V
// - LCD1602 I²C Module, Options for 3 Colors 3.3v/5v Backlight Adjustable
//
// These displays use the [aip31068] I²C LCD Driver chip. The command set is
// compatible with the HD44780. The tri-color version has purchase options to
// select a backlight color and uses an SN3193 to dim the backlight.
//
// - LCD1602 RGB Module, 16x2 Characters LCD, RGB Backlight, 3.3V/5V, I²C Bus
//
// This display uses the AiP31068 I²C LCD Driver w/ a PCA9633 RGB LED PWM
// controller.
package waveshare1602
import (
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/i2c"
"periph.io/x/devices/v3/aip31068"
"periph.io/x/devices/v3/pca9633"
)
type Variant string
const (
// SKU 19537 - RGB Backlight
LCD1602RGBBacklight Variant = "LCD1602RGBBacklight"
// SKU 23991 - I²C w/ Monochrome Backlight
LCD1602MonoBacklight Variant = "LCD1602MonoBacklight"
// Not Implemented. SKU 30494, 30495, and 30496. Uses an SN3193 for
// controlling the backlight.
LCD1602DimmableMonoBacklight Variant = "LCD1602DimmableMonoBacklight"
_LCD_ADDRESS uint16 = 0x3e
_RGB_ADDRESS uint16 = 0x60
)
type RGBBLController struct {
controller *pca9633.Dev
variant Variant
}
// Create new LCD display.
func New(bus i2c.Bus, variant Variant, rows, cols int) (*aip31068.Dev, error) {
var bl any
if variant == LCD1602RGBBacklight {
blcontroller, err := pca9633.New(bus, _RGB_ADDRESS, pca9633.STRUCT_OPENDRAIN)
if err != nil {
return nil, err
}
bl = &RGBBLController{variant: variant, controller: blcontroller}
} else if variant == LCD1602DimmableMonoBacklight {
return nil, display.ErrNotImplemented
}
return aip31068.New(bus, _LCD_ADDRESS, bl, rows, cols)
}
func (bl *RGBBLController) String() string {
return string(bl.variant)
}
// For units that have an RGB Backlight, set the backlight color/intensity.
// This unit does not persist settings in EEPROM, so you can call it as often
// as desired. The range of the values is 0-255.
func (bl *RGBBLController) RGBBacklight(red, green, blue display.Intensity) error {
// The device is really connected to the LEDs in this channel order...
return bl.controller.Out(blue, green, red)
}
var _ display.DisplayRGBBacklight = &RGBBLController{}

@ -0,0 +1,47 @@
// 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 waveshare1602_test
import (
"fmt"
"log"
"time"
"periph.io/x/conn/v3/display"
"periph.io/x/conn/v3/display/displaytest"
"periph.io/x/conn/v3/i2c/i2creg"
"periph.io/x/devices/v3/waveshare1602"
"periph.io/x/host/v3"
)
func Example() {
fmt.Println("Starting")
// 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 := waveshare1602.New(bus, waveshare1602.LCD1602RGBBacklight, 2, 16)
if err != nil {
log.Fatal(err)
}
_ = dev.Backlight(display.Intensity(0xff))
_ = dev.Clear()
time.Sleep(time.Second)
_, _ = dev.WriteString("Hello")
_ = dev.MoveTo(2, 2)
time.Sleep(5 * time.Second)
_, _ = dev.WriteString("1234567890")
time.Sleep(10 * time.Second)
displaytest.TestTextDisplay(dev, true)
_ = dev.Halt()
}
Loading…
Cancel
Save