Skip to content

Commit 38cb339

Browse files
authored
Devices: Initial Add of Waveshare 1602 Support (#100)
* Initial Add of Waveshare 1602 Support
1 parent ea55a05 commit 38cb339

File tree

7 files changed

+1071
-0
lines changed

7 files changed

+1071
-0
lines changed

aip31068/dev.go

+374
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
// Copyright 2025 The Periph Authors. All rights reserved.
2+
// Use of this source code is governed under the Apache License, Version 2.0
3+
// that can be found in the LICENSE file.
4+
5+
// The aip31068 is an HD44780 compatible I²C driver chip. It provides an I²C
6+
// interface to an LCD. This is not a _backpack_ chip in the sense that it
7+
// provides GPIO pins via an I²C interface. The I²C write commands go directly
8+
// to the LCD display driver.
9+
//
10+
// Implements periph.io/x/conn/display/TextDisplay
11+
//
12+
// # Datasheet
13+
//
14+
// https://support.newhavendisplay.com/hc/en-us/article_attachments/4414498095511
15+
package aip31068
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
"sync"
21+
"time"
22+
23+
"periph.io/x/conn/v3"
24+
"periph.io/x/conn/v3/display"
25+
"periph.io/x/conn/v3/i2c"
26+
)
27+
28+
const (
29+
busyFlag byte = 0x80
30+
cmdByte byte = 0xfe
31+
dataByte byte = 0x40
32+
moreControls byte = 0x80
33+
packageName = "aip31068"
34+
)
35+
36+
var (
37+
ErrNotImplemented = fmt.Errorf("%s: %w", packageName, display.ErrNotImplemented)
38+
39+
rowConstants = [][]byte{{0, 0, 64}, {0, 0, 64, 20, 84}}
40+
clearScreen = []byte{cmdByte, 0x01}
41+
goHome = []byte{cmdByte, 0x02}
42+
setCursorPosition = []byte{cmdByte, 0x80}
43+
displayMode = []byte{cmdByte, 0x20}
44+
defaultEntryMode = []byte{cmdByte, 0x06}
45+
)
46+
47+
type Dev struct {
48+
rows int
49+
cols int
50+
51+
mu sync.Mutex
52+
d *i2c.Dev
53+
blink bool
54+
on bool
55+
cursor bool
56+
blMono display.DisplayBacklight
57+
blRGB display.DisplayRGBBacklight
58+
}
59+
60+
func wrap(err error) error {
61+
if err == nil || strings.HasPrefix(err.Error(), packageName) {
62+
return err
63+
}
64+
return fmt.Errorf("%s: %w", packageName, err)
65+
}
66+
67+
// New creates an aip31068 based LCD.
68+
//
69+
// backlight is a controller that manipulates the display backlight. If the
70+
// display backlight is hard-wired on, then this can be nil. Otherwise, it
71+
// should implement either display.DisplayBacklight or
72+
// display.DisplayRGBBacklight.
73+
func New(bus i2c.Bus,
74+
address uint16,
75+
backlight any,
76+
rows,
77+
cols int) (*Dev, error) {
78+
79+
dev := &Dev{
80+
d: &i2c.Dev{Bus: bus, Addr: address},
81+
rows: rows,
82+
cols: cols,
83+
}
84+
switch bl := backlight.(type) {
85+
case display.DisplayBacklight:
86+
dev.blMono = bl
87+
case display.DisplayRGBBacklight:
88+
dev.blRGB = bl
89+
}
90+
91+
err := dev.init()
92+
if err != nil {
93+
dev = nil
94+
}
95+
return dev, wrap(err)
96+
}
97+
98+
// Perform the display initialization routine,
99+
func (dev *Dev) init() error {
100+
// Set the lines display value
101+
var modeToSet = []byte{cmdByte, displayMode[1]}
102+
if dev.rows > 1 {
103+
modeToSet[1] = modeToSet[1] | 0x08
104+
}
105+
_, err := dev.Write(modeToSet)
106+
if err == nil {
107+
err = dev.Display(true)
108+
time.Sleep(40 * time.Microsecond)
109+
}
110+
if err == nil {
111+
err = dev.Clear()
112+
time.Sleep(2000 * time.Microsecond)
113+
}
114+
115+
if err == nil {
116+
err = dev.Home()
117+
time.Sleep(40 * time.Microsecond)
118+
}
119+
120+
if err == nil {
121+
// Set the entry mode
122+
_, err = dev.Write(defaultEntryMode)
123+
}
124+
if err == nil {
125+
_ = dev.Backlight(0xff)
126+
}
127+
if err != nil {
128+
err = wrap(err)
129+
}
130+
return err
131+
}
132+
133+
// Return the row offset value
134+
func getRowConstant(row, maxcols int) byte {
135+
var offset int
136+
if maxcols != 16 {
137+
offset = 1
138+
}
139+
return rowConstants[offset][row]
140+
}
141+
142+
// Enable/Disable auto scroll
143+
func (dev *Dev) AutoScroll(enabled bool) error {
144+
return ErrNotImplemented
145+
}
146+
147+
// Return the number of columns the display supports
148+
func (dev *Dev) Cols() int {
149+
return dev.cols
150+
}
151+
152+
// Clear the display and move the cursor home.
153+
func (dev *Dev) Clear() error {
154+
_, err := dev.Write(clearScreen)
155+
if err != nil {
156+
err = wrap(err)
157+
}
158+
return err
159+
}
160+
161+
// Set the cursor mode. You can pass multiple arguments.
162+
// Cursor(CursorOff, CursorUnderline)
163+
func (dev *Dev) Cursor(modes ...display.CursorMode) (err error) {
164+
var val = byte(0x08)
165+
if dev.on {
166+
val |= 0x04
167+
}
168+
for _, mode := range modes {
169+
switch mode {
170+
case display.CursorOff:
171+
// dev.Write(underlineCursorOff)
172+
dev.blink = false
173+
dev.cursor = false
174+
case display.CursorBlink:
175+
dev.blink = true
176+
dev.cursor = true
177+
val |= 0x01
178+
case display.CursorUnderline:
179+
dev.cursor = true
180+
dev.blink = true
181+
// dev.Write(underlineCursorOn)
182+
val |= 0x02
183+
case display.CursorBlock:
184+
dev.cursor = true
185+
dev.blink = true
186+
val |= 0x01
187+
default:
188+
err = fmt.Errorf("Waveshare1602 - unexpected cursor: %d", mode)
189+
return
190+
}
191+
}
192+
_, err = dev.Write([]byte{cmdByte, val & 0x0f})
193+
return wrap(err)
194+
195+
}
196+
197+
// Turn the display on / off
198+
func (dev *Dev) Display(on bool) error {
199+
dev.on = on
200+
val := byte(0x08)
201+
if on {
202+
val |= 0x04
203+
}
204+
if dev.blink {
205+
val |= 0x01
206+
}
207+
if dev.cursor {
208+
val |= 0x02
209+
}
210+
_, err := dev.Write([]byte{cmdByte, val})
211+
return err
212+
213+
}
214+
215+
// Halt clears the display, turns the backlight off, and turns the display off.
216+
// Halt() is called for the data pins gpio.Group.
217+
func (dev *Dev) Halt() error {
218+
_ = dev.Clear()
219+
_ = dev.Display(false)
220+
_ = dev.Backlight(0)
221+
return nil
222+
}
223+
224+
// Move the cursor home (MinRow(),MinCol())
225+
func (dev *Dev) Home() error {
226+
_, err := dev.Write(goHome)
227+
return err
228+
}
229+
230+
// Return the min column position.
231+
func (dev *Dev) MinCol() int {
232+
return 1
233+
}
234+
235+
// Return the min row position.
236+
func (dev *Dev) MinRow() int {
237+
return 1
238+
}
239+
240+
// Move the cursor forward or backward.
241+
func (dev *Dev) Move(dir display.CursorDirection) (err error) {
242+
var val byte = 0x10
243+
switch dir {
244+
case display.Backward:
245+
246+
case display.Forward:
247+
val |= 0x04
248+
case display.Down, display.Up:
249+
fallthrough
250+
default:
251+
err = ErrNotImplemented
252+
return
253+
}
254+
_, err = dev.Write([]byte{cmdByte, val})
255+
err = wrap(err)
256+
return
257+
}
258+
259+
// Move the cursor to arbitrary position.
260+
func (dev *Dev) MoveTo(row, col int) (err error) {
261+
if row < dev.MinRow() || row > dev.rows || col < dev.MinCol() || col > dev.cols {
262+
err = fmt.Errorf("%s.MoveTo(%d,%d) value out of range", packageName, row, col)
263+
return
264+
}
265+
var cmd = []byte{cmdByte, setCursorPosition[1]}
266+
cmd[1] |= getRowConstant(row, dev.cols) + byte(col-1)
267+
_, err = dev.Write(cmd)
268+
err = wrap(err)
269+
return err
270+
}
271+
272+
// Return the number of rows the display supports.
273+
func (dev *Dev) Rows() int {
274+
return dev.rows
275+
}
276+
277+
func (dev *Dev) String() string {
278+
return fmt.Sprintf("%s Rows: %d Cols: %d", packageName, dev.rows, dev.cols)
279+
}
280+
281+
// Read the busy flag to make sure it's clear to write. It's a little wonky
282+
// initially but then smooths out, so it makes a best effort and ignores errors.
283+
func (dev *Dev) waitForFree() {
284+
tLimit := time.Now().Add(3 * time.Millisecond)
285+
w := make([]byte, 2)
286+
r := make([]byte, 1)
287+
for time.Now().Before(tLimit) {
288+
err := dev.d.Tx(w, r)
289+
if err == nil && (r[0]&busyFlag) == 0 {
290+
break
291+
}
292+
time.Sleep(100 * time.Microsecond)
293+
}
294+
}
295+
296+
// Write a set of bytes to the display. This routine handles control
297+
// and data characters transparently.
298+
func (dev *Dev) Write(p []byte) (n int, err error) {
299+
dev.mu.Lock()
300+
defer dev.mu.Unlock()
301+
dev.waitForFree()
302+
303+
lastControl := -1
304+
for i := range len(p) {
305+
if p[i] == cmdByte {
306+
lastControl = i
307+
}
308+
}
309+
310+
w := make([]byte, 0, len(p))
311+
312+
for pos := 0; pos < len(p); {
313+
314+
// So, when we're writing, we need to send a control byte first
315+
// that says type data, or cmd. We then send the bytes. If the
316+
// type changes, then we need to send a new control byte.
317+
//
318+
// If there are more control bytes, then the control byte has bit 7
319+
// set, and we send a control byte for each character sent.
320+
var controlByte byte = 0x00
321+
if p[pos] == cmdByte {
322+
pos += 1
323+
} else {
324+
controlByte |= dataByte
325+
}
326+
if pos < lastControl {
327+
controlByte |= moreControls
328+
}
329+
330+
if (pos - 1) <= lastControl {
331+
w = append(w, controlByte)
332+
}
333+
334+
w = append(w, p[pos])
335+
pos += 1
336+
}
337+
err = dev.d.Tx(w, nil)
338+
if err == nil {
339+
n = len(p)
340+
}
341+
err = wrap(err)
342+
return n, err
343+
}
344+
345+
// Write a string output to the display.
346+
func (dev *Dev) WriteString(text string) (n int, err error) {
347+
return dev.Write([]byte(text))
348+
}
349+
350+
// Set the backlight intensity.
351+
func (dev *Dev) Backlight(intensity display.Intensity) error {
352+
if dev.blMono != nil {
353+
return dev.blMono.Backlight(intensity)
354+
} else if dev.blRGB != nil {
355+
return dev.blRGB.RGBBacklight(intensity, intensity, intensity)
356+
}
357+
return ErrNotImplemented
358+
}
359+
360+
// For units that have an RGB Backlight, set the backlight color/intensity.
361+
// The range of the values is 0-255.
362+
func (dev *Dev) RGBBacklight(red, green, blue display.Intensity) error {
363+
if dev.blRGB != nil {
364+
return dev.blRGB.RGBBacklight(red, green, blue)
365+
} else if dev.blMono != nil {
366+
return dev.blMono.Backlight(red | green | blue)
367+
}
368+
return ErrNotImplemented
369+
}
370+
371+
var _ conn.Resource = &Dev{}
372+
var _ display.TextDisplay = &Dev{}
373+
var _ display.DisplayBacklight = &Dev{}
374+
var _ display.DisplayRGBBacklight = &Dev{}

0 commit comments

Comments
 (0)