diff --git a/aip31068/dev.go b/aip31068/dev.go new file mode 100644 index 0000000..8d7f2bd --- /dev/null +++ b/aip31068/dev.go @@ -0,0 +1,374 @@ +// 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 + blMono display.DisplayBacklight + blRGB display.DisplayRGBBacklight +} + +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 any, + rows, + cols int) (*Dev, error) { + + dev := &Dev{ + d: &i2c.Dev{Bus: bus, Addr: address}, + rows: rows, + cols: cols, + } + switch bl := backlight.(type) { + case display.DisplayBacklight: + dev.blMono = bl + case display.DisplayRGBBacklight: + dev.blRGB = bl + } + + 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 { + 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 { + if dev.blMono != nil { + return dev.blMono.Backlight(intensity) + } else if dev.blRGB != nil { + return dev.blRGB.RGBBacklight(intensity, intensity, intensity) + } + 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 { + if dev.blRGB != nil { + return dev.blRGB.RGBBacklight(red, green, blue) + } else if dev.blMono != nil { + return dev.blMono.Backlight(red | green | blue) + } + return ErrNotImplemented +} + +var _ conn.Resource = &Dev{} +var _ display.TextDisplay = &Dev{} +var _ display.DisplayBacklight = &Dev{} +var _ display.DisplayRGBBacklight = &Dev{} diff --git a/aip31068/dev_test.go b/aip31068/dev_test.go new file mode 100644 index 0000000..44485f6 --- /dev/null +++ b/aip31068/dev_test.go @@ -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) +} diff --git a/aip31068/devbustestrecording_test.go b/aip31068/devbustestrecording_test.go new file mode 100644 index 0000000..418d1b3 --- /dev/null +++ b/aip31068/devbustestrecording_test.go @@ -0,0 +1,163 @@ +// 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 ( + "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}}}, +} diff --git a/pca9633/dev.go b/pca9633/dev.go new file mode 100644 index 0000000..471f5a1 --- /dev/null +++ b/pca9633/dev.go @@ -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) +} diff --git a/pca9633/dev_test.go b/pca9633/dev_test.go new file mode 100644 index 0000000..fedd967 --- /dev/null +++ b/pca9633/dev_test.go @@ -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) + } +} diff --git a/waveshare1602/1602.go b/waveshare1602/1602.go new file mode 100644 index 0000000..8a6843a --- /dev/null +++ b/waveshare1602/1602.go @@ -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{} diff --git a/waveshare1602/example_test.go b/waveshare1602/example_test.go new file mode 100644 index 0000000..5ffbcae --- /dev/null +++ b/waveshare1602/example_test.go @@ -0,0 +1,45 @@ +// 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 ( + "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() { + // 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() +}