1
0
mirror of https://github.com/kidoman/embd synced 2024-12-22 12:50:19 +01:00

Merge pull request #19 from matthewdale/master

controller: add a lib for the HD44780 character display controller
This commit is contained in:
Karan Misra 2015-04-09 14:50:57 +05:30
commit 91dc0f5744
6 changed files with 1299 additions and 0 deletions

View File

@ -0,0 +1,648 @@
/*
Package hd44780 allows controlling an HD44780-compatible character LCD
controller. Currently the library is write-only and does not support
reading from the display controller.
Resources
This library is based three other HD44780 libraries:
Adafruit https://github.com/adafruit/Adafruit-Raspberry-Pi-Python-Code/blob/master/Adafruit_CharLCD/Adafruit_CharLCD.py
hwio https://github.com/mrmorphic/hwio/blob/master/devices/hd44780/hd44780_i2c.go
LiquidCrystal https://github.com/arduino/Arduino/blob/master/libraries/LiquidCrystal/LiquidCrystal.cpp
*/
package hd44780
import (
"time"
"github.com/golang/glog"
"github.com/kidoman/embd"
)
type entryMode byte
type displayMode byte
type functionMode byte
// RowAddress defines the cursor (DDRAM) address of the first column of each row, up to 4 rows.
// You must use the RowAddress value that matches the number of columns on your character display
// for the SetCursor function to work correctly.
type RowAddress [4]byte
var (
// RowAddress16Col are row addresses for a 16-column display
RowAddress16Col RowAddress = [4]byte{0x00, 0x40, 0x10, 0x50}
// RowAddress20Col are row addresses for a 20-column display
RowAddress20Col RowAddress = [4]byte{0x00, 0x40, 0x14, 0x54}
)
// BacklightPolarity is used to set the polarity of the backlight switch, either positive or negative.
type BacklightPolarity bool
const (
// Negative indicates that the backlight is active-low and must have a logical low value to enable.
Negative BacklightPolarity = false
// Positive indicates that the backlight is active-high and must have a logical high value to enable.
Positive BacklightPolarity = true
writeDelay = 37 * time.Microsecond
pulseDelay = 1 * time.Microsecond
clearDelay = 1520 * time.Microsecond
// Initialize display
lcdInit byte = 0x33 // 00110011
lcdInit4bit byte = 0x32 // 00110010
// Commands
lcdClearDisplay byte = 0x01 // 00000001
lcdReturnHome byte = 0x02 // 00000010
lcdCursorShift byte = 0x10 // 00010000
lcdSetCGRamAddr byte = 0x40 // 01000000
lcdSetDDRamAddr byte = 0x80 // 10000000
// Cursor and display move flags
lcdCursorMove byte = 0x00 // 00000000
lcdDisplayMove byte = 0x08 // 00001000
lcdMoveLeft byte = 0x00 // 00000000
lcdMoveRight byte = 0x04 // 00000100
// Entry mode flags
lcdSetEntryMode entryMode = 0x04 // 00000100
lcdEntryDecrement entryMode = 0x00 // 00000000
lcdEntryIncrement entryMode = 0x02 // 00000010
lcdEntryShiftOff entryMode = 0x00 // 00000000
lcdEntryShiftOn entryMode = 0x01 // 00000001
// Display mode flags
lcdSetDisplayMode displayMode = 0x08 // 00001000
lcdDisplayOff displayMode = 0x00 // 00000000
lcdDisplayOn displayMode = 0x04 // 00000100
lcdCursorOff displayMode = 0x00 // 00000000
lcdCursorOn displayMode = 0x02 // 00000010
lcdBlinkOff displayMode = 0x00 // 00000000
lcdBlinkOn displayMode = 0x01 // 00000001
// Function mode flags
lcdSetFunctionMode functionMode = 0x20 // 00100000
lcd4BitMode functionMode = 0x00 // 00000000
lcd8BitMode functionMode = 0x10 // 00010000
lcd1Line functionMode = 0x00 // 00000000
lcd2Line functionMode = 0x08 // 00001000
lcd5x8Dots functionMode = 0x00 // 00000000
lcd5x10Dots functionMode = 0x04 // 00000100
)
// HD44780 represents an HD44780-compatible character LCD controller.
type HD44780 struct {
Connection
eMode entryMode
dMode displayMode
fMode functionMode
rowAddr RowAddress
}
// NewGPIO creates a new HD44780 connected by a 4-bit GPIO bus.
func NewGPIO(
rs, en, d4, d5, d6, d7, backlight interface{},
blPolarity BacklightPolarity,
rowAddr RowAddress,
modes ...ModeSetter,
) (*HD44780, error) {
pinKeys := []interface{}{rs, en, d4, d5, d6, d7, backlight}
pins := [7]embd.DigitalPin{}
for idx, key := range pinKeys {
if key == nil {
continue
}
var digitalPin embd.DigitalPin
if pin, ok := key.(embd.DigitalPin); ok {
digitalPin = pin
} else {
var err error
digitalPin, err = embd.NewDigitalPin(key)
if err != nil {
glog.V(1).Infof("hd44780: error creating digital pin %+v: %s", key, err)
return nil, err
}
}
pins[idx] = digitalPin
}
for _, pin := range pins {
if pin == nil {
continue
}
err := pin.SetDirection(embd.Out)
if err != nil {
glog.Errorf("hd44780: error setting pin %+v to out direction: %s", pin, err)
return nil, err
}
}
return New(
NewGPIOConnection(
pins[0],
pins[1],
pins[2],
pins[3],
pins[4],
pins[5],
pins[6],
blPolarity),
rowAddr,
modes...,
)
}
// NewI2C creates a new HD44780 connected by an I²C bus.
func NewI2C(
i2c embd.I2CBus,
addr byte,
pinMap I2CPinMap,
rowAddr RowAddress,
modes ...ModeSetter,
) (*HD44780, error) {
return New(NewI2CConnection(i2c, addr, pinMap), rowAddr, modes...)
}
// New creates a new HD44780 connected by a Connection bus.
func New(bus Connection, rowAddr RowAddress, modes ...ModeSetter) (*HD44780, error) {
controller := &HD44780{
Connection: bus,
eMode: 0x00,
dMode: 0x00,
fMode: 0x00,
rowAddr: rowAddr,
}
err := controller.lcdInit()
if err != nil {
return nil, err
}
err = controller.SetMode(append(DefaultModes, modes...)...)
if err != nil {
return nil, err
}
return controller, nil
}
func (controller *HD44780) lcdInit() error {
glog.V(2).Info("hd44780: initializing display")
err := controller.WriteInstruction(lcdInit)
if err != nil {
return err
}
glog.V(2).Info("hd44780: initializing display in 4-bit mode")
return controller.WriteInstruction(lcdInit4bit)
}
// DefaultModes are the default initialization modes for an HD44780.
// ModeSetters passed in to a constructor will override these default values.
var DefaultModes []ModeSetter = []ModeSetter{
FourBitMode,
OneLine,
Dots5x8,
EntryIncrement,
EntryShiftOff,
DisplayOn,
CursorOff,
BlinkOff,
}
// ModeSetter defines a function used for setting modes on an HD44780.
// ModeSetters must be used with the SetMode function or in a constructor.
type ModeSetter func(*HD44780)
// EntryDecrement is a ModeSetter that sets the HD44780 to entry decrement mode.
func EntryDecrement(hd *HD44780) { hd.eMode &= ^lcdEntryIncrement }
// EntryIncrement is a ModeSetter that sets the HD44780 to entry increment mode.
func EntryIncrement(hd *HD44780) { hd.eMode |= lcdEntryIncrement }
// EntryShiftOff is a ModeSetter that sets the HD44780 to entry shift off mode.
func EntryShiftOff(hd *HD44780) { hd.eMode &= ^lcdEntryShiftOn }
// EntryShiftOn is a ModeSetter that sets the HD44780 to entry shift on mode.
func EntryShiftOn(hd *HD44780) { hd.eMode |= lcdEntryShiftOn }
// DisplayOff is a ModeSetter that sets the HD44780 to display off mode.
func DisplayOff(hd *HD44780) { hd.dMode &= ^lcdDisplayOn }
// DisplayOn is a ModeSetter that sets the HD44780 to display on mode.
func DisplayOn(hd *HD44780) { hd.dMode |= lcdDisplayOn }
// CursorOff is a ModeSetter that sets the HD44780 to cursor off mode.
func CursorOff(hd *HD44780) { hd.dMode &= ^lcdCursorOn }
// CursorOn is a ModeSetter that sets the HD44780 to cursor on mode.
func CursorOn(hd *HD44780) { hd.dMode |= lcdCursorOn }
// BlinkOff is a ModeSetter that sets the HD44780 to cursor blink off mode.
func BlinkOff(hd *HD44780) { hd.dMode &= ^lcdBlinkOn }
// BlinkOn is a ModeSetter that sets the HD44780 to cursor blink on mode.
func BlinkOn(hd *HD44780) { hd.dMode |= lcdBlinkOn }
// FourBitMode is a ModeSetter that sets the HD44780 to 4-bit bus mode.
func FourBitMode(hd *HD44780) { hd.fMode &= ^lcd8BitMode }
// EightBitMode is a ModeSetter that sets the HD44780 to 8-bit bus mode.
func EightBitMode(hd *HD44780) { hd.fMode |= lcd8BitMode }
// OneLine is a ModeSetter that sets the HD44780 to 1-line display mode.
func OneLine(hd *HD44780) { hd.fMode &= ^lcd2Line }
// TwoLine is a ModeSetter that sets the HD44780 to 2-line display mode.
func TwoLine(hd *HD44780) { hd.fMode |= lcd2Line }
// Dots5x8 is a ModeSetter that sets the HD44780 to 5x8-pixel character mode.
func Dots5x8(hd *HD44780) { hd.fMode &= ^lcd5x10Dots }
// Dots5x10 is a ModeSetter that sets the HD44780 to 5x10-pixel character mode.
func Dots5x10(hd *HD44780) { hd.fMode |= lcd5x10Dots }
// EntryIncrementEnabled returns true if entry increment mode is enabled.
func (hd *HD44780) EntryIncrementEnabled() bool { return hd.eMode&lcdEntryIncrement > 0 }
// EntryShiftEnabled returns true if entry shift mode is enabled.
func (hd *HD44780) EntryShiftEnabled() bool { return hd.eMode&lcdEntryShiftOn > 0 }
// DisplayEnabled returns true if the display is on.
func (hd *HD44780) DisplayEnabled() bool { return hd.dMode&lcdDisplayOn > 0 }
// CursorEnabled returns true if the cursor is on.
func (hd *HD44780) CursorEnabled() bool { return hd.dMode&lcdCursorOn > 0 }
// BlinkEnabled returns true if the cursor blink mode is enabled.
func (hd *HD44780) BlinkEnabled() bool { return hd.dMode&lcdBlinkOn > 0 }
// EightBitModeEnabled returns true if 8-bit bus mode is enabled and false if 4-bit
// bus mode is enabled.
func (hd *HD44780) EightBitModeEnabled() bool { return hd.fMode&lcd8BitMode > 0 }
// TwoLineEnabled returns true if 2-line display mode is enabled and false if 1-line
// display mode is enabled.
func (hd *HD44780) TwoLineEnabled() bool { return hd.fMode&lcd2Line > 0 }
// Dots5x10Enabled returns true if 5x10-pixel characters are enabled.
func (hd *HD44780) Dots5x10Enabled() bool { return hd.fMode&lcd5x8Dots > 0 }
// SetModes modifies the entry mode, display mode, and function mode with the
// given mode setter functions.
func (hd *HD44780) SetMode(modes ...ModeSetter) error {
for _, m := range modes {
m(hd)
}
functions := []func() error{
func() error { return hd.setEntryMode() },
func() error { return hd.setDisplayMode() },
func() error { return hd.setFunctionMode() },
}
for _, f := range functions {
err := f()
if err != nil {
return err
}
}
return nil
}
func (hd *HD44780) setEntryMode() error {
return hd.WriteInstruction(byte(lcdSetEntryMode | hd.eMode))
}
func (hd *HD44780) setDisplayMode() error {
return hd.WriteInstruction(byte(lcdSetDisplayMode | hd.dMode))
}
func (hd *HD44780) setFunctionMode() error {
return hd.WriteInstruction(byte(lcdSetFunctionMode | hd.fMode))
}
// DisplayOff sets the display mode to off.
func (hd *HD44780) DisplayOff() error {
DisplayOff(hd)
return hd.setDisplayMode()
}
// DisplayOn sets the display mode to on.
func (hd *HD44780) DisplayOn() error {
DisplayOn(hd)
return hd.setDisplayMode()
}
// CursorOff turns the cursor off.
func (hd *HD44780) CursorOff() error {
CursorOff(hd)
return hd.setDisplayMode()
}
// CursorOn turns the cursor on.
func (hd *HD44780) CursorOn() error {
CursorOn(hd)
return hd.setDisplayMode()
}
// BlinkOff sets cursor blink mode off.
func (hd *HD44780) BlinkOff() error {
BlinkOff(hd)
return hd.setDisplayMode()
}
// BlinkOn sets cursor blink mode on.
func (hd *HD44780) BlinkOn() error {
BlinkOn(hd)
return hd.setDisplayMode()
}
// ShiftLeft shifts the cursor and all characters to the left.
func (hd *HD44780) ShiftLeft() error {
return hd.WriteInstruction(lcdCursorShift | lcdDisplayMove | lcdMoveLeft)
}
// ShiftRight shifts the cursor and all characters to the right.
func (hd *HD44780) ShiftRight() error {
return hd.WriteInstruction(lcdCursorShift | lcdDisplayMove | lcdMoveRight)
}
// Home moves the cursor and all characters to the home position.
func (hd *HD44780) Home() error {
err := hd.WriteInstruction(lcdReturnHome)
time.Sleep(clearDelay)
return err
}
// Clear clears the display and mode settings sets the cursor to the home position.
func (hd *HD44780) Clear() error {
err := hd.WriteInstruction(lcdClearDisplay)
if err != nil {
return err
}
time.Sleep(clearDelay)
// have to set mode here because clear also clears some mode settings
return hd.SetMode()
}
// SetCursor sets the input cursor to the given position.
func (hd *HD44780) SetCursor(col, row int) error {
return hd.SetDDRamAddr(byte(col) + hd.lcdRowOffset(row))
}
func (hd *HD44780) lcdRowOffset(row int) byte {
// Offset for up to 4 rows
if row > 3 {
row = 3
}
return hd.rowAddr[row]
}
// SetDDRamAddr sets the input cursor to the given address.
func (hd *HD44780) SetDDRamAddr(value byte) error {
return hd.WriteInstruction(lcdSetDDRamAddr | value)
}
// WriteInstruction writes a byte to the bus with register select in data mode.
func (hd *HD44780) WriteChar(value byte) error {
return hd.Write(true, value)
}
// WriteInstruction writes a byte to the bus with register select in command mode.
func (hd *HD44780) WriteInstruction(value byte) error {
return hd.Write(false, value)
}
// Close closes the underlying Connection.
func (hd *HD44780) Close() error {
return hd.Connection.Close()
}
// Connection abstracts the different methods of communicating with an HD44780.
type Connection interface {
// Write writes a byte to the HD44780 controller with the register select
// flag either on or off.
Write(rs bool, data byte) error
// BacklightOff turns the optional backlight off.
BacklightOff() error
// BacklightOn turns the optional backlight on.
BacklightOn() error
// Close closes all open resources.
Close() error
}
// GPIOConnection implements Connection using a 4-bit GPIO bus.
type GPIOConnection struct {
RS, EN embd.DigitalPin
D4, D5, D6, D7 embd.DigitalPin
Backlight embd.DigitalPin
BLPolarity BacklightPolarity
}
// NewGPIOConnection returns a new Connection based on a 4-bit GPIO bus.
func NewGPIOConnection(
rs, en, d4, d5, d6, d7, backlight embd.DigitalPin,
blPolarity BacklightPolarity,
) *GPIOConnection {
return &GPIOConnection{
RS: rs,
EN: en,
D4: d4,
D5: d5,
D6: d6,
D7: d7,
Backlight: backlight,
BLPolarity: blPolarity,
}
}
// BacklightOff turns the optional backlight off.
func (conn *GPIOConnection) BacklightOff() error {
if conn.Backlight != nil {
return conn.Backlight.Write(conn.backlightSignal(false))
}
return nil
}
// BacklightOn turns the optional backlight on.
func (conn *GPIOConnection) BacklightOn() error {
if conn.Backlight != nil {
return conn.Backlight.Write(conn.backlightSignal(true))
}
return nil
}
func (conn *GPIOConnection) backlightSignal(state bool) int {
if state == bool(conn.BLPolarity) {
return embd.High
} else {
return embd.Low
}
}
// Write writes a register select flag and byte to the 4-bit GPIO connection.
func (conn *GPIOConnection) Write(rs bool, data byte) error {
glog.V(3).Infof("hd44780: writing to GPIO RS: %t, data: %#x", rs, data)
rsInt := embd.Low
if rs {
rsInt = embd.High
}
functions := []func() error{
func() error { return conn.RS.Write(rsInt) },
func() error { return conn.D4.Write(int((data >> 4) & 0x01)) },
func() error { return conn.D5.Write(int((data >> 5) & 0x01)) },
func() error { return conn.D6.Write(int((data >> 6) & 0x01)) },
func() error { return conn.D7.Write(int((data >> 7) & 0x01)) },
func() error { return conn.pulseEnable() },
func() error { return conn.D4.Write(int(data & 0x01)) },
func() error { return conn.D5.Write(int((data >> 1) & 0x01)) },
func() error { return conn.D6.Write(int((data >> 2) & 0x01)) },
func() error { return conn.D7.Write(int((data >> 3) & 0x01)) },
func() error { return conn.pulseEnable() },
}
for _, f := range functions {
err := f()
if err != nil {
return err
}
}
time.Sleep(writeDelay)
return nil
}
func (conn *GPIOConnection) pulseEnable() error {
values := []int{embd.Low, embd.High, embd.Low}
for _, v := range values {
time.Sleep(pulseDelay)
err := conn.EN.Write(v)
if err != nil {
return err
}
}
return nil
}
// Close closes all open DigitalPins.
func (conn *GPIOConnection) Close() error {
glog.V(2).Info("hd44780: closing all GPIO pins")
pins := []embd.DigitalPin{
conn.RS,
conn.EN,
conn.D4,
conn.D5,
conn.D6,
conn.D7,
conn.Backlight,
}
for _, pin := range pins {
err := pin.Close()
if err != nil {
glog.Errorf("hd44780: error closing pin %+v: %s", pin, err)
return err
}
}
return nil
}
// I2CConnection implements Connection using an I²C bus.
type I2CConnection struct {
I2C embd.I2CBus
Addr byte
PinMap I2CPinMap
Backlight bool
}
// I2CPinMap represents a mapping between the pins on an I²C port expander and
// the pins on the HD44780 controller.
type I2CPinMap struct {
RS, RW, EN byte
D4, D5, D6, D7 byte
Backlight byte
BLPolarity BacklightPolarity
}
var (
// MJKDZPinMap is the standard pin mapping for an MJKDZ-based I²C backpack.
MJKDZPinMap I2CPinMap = I2CPinMap{
RS: 6, RW: 5, EN: 4,
D4: 0, D5: 1, D6: 2, D7: 3,
Backlight: 7,
BLPolarity: Negative,
}
// PCF8574PinMap is the standard pin mapping for a PCF8574-based I²C backpack.
PCF8574PinMap I2CPinMap = I2CPinMap{
RS: 0, RW: 1, EN: 2,
D4: 4, D5: 5, D6: 6, D7: 7,
Backlight: 3,
BLPolarity: Positive,
}
)
// NewI2CConnection returns a new Connection based on an I²C bus.
func NewI2CConnection(i2c embd.I2CBus, addr byte, pinMap I2CPinMap) *I2CConnection {
return &I2CConnection{
I2C: i2c,
Addr: addr,
PinMap: pinMap,
}
}
// BacklightOff turns the optional backlight off.
func (conn *I2CConnection) BacklightOff() error {
conn.Backlight = false
return conn.Write(false, 0x00)
}
// BacklightOn turns the optional backlight on.
func (conn *I2CConnection) BacklightOn() error {
conn.Backlight = true
return conn.Write(false, 0x00)
}
// Write writes a register select flag and byte to the I²C connection.
func (conn *I2CConnection) Write(rs bool, data byte) error {
var instructionHigh byte = 0x00
instructionHigh |= ((data >> 4) & 0x01) << conn.PinMap.D4
instructionHigh |= ((data >> 5) & 0x01) << conn.PinMap.D5
instructionHigh |= ((data >> 6) & 0x01) << conn.PinMap.D6
instructionHigh |= ((data >> 7) & 0x01) << conn.PinMap.D7
var instructionLow byte = 0x00
instructionLow |= (data & 0x01) << conn.PinMap.D4
instructionLow |= ((data >> 1) & 0x01) << conn.PinMap.D5
instructionLow |= ((data >> 2) & 0x01) << conn.PinMap.D6
instructionLow |= ((data >> 3) & 0x01) << conn.PinMap.D7
instructions := []byte{instructionHigh, instructionLow}
for _, ins := range instructions {
if rs {
ins |= 0x01 << conn.PinMap.RS
}
if conn.Backlight == bool(conn.PinMap.BLPolarity) {
ins |= 0x01 << conn.PinMap.Backlight
}
glog.V(3).Infof("hd44780: writing to I2C: %#x", ins)
err := conn.pulseEnable(ins)
if err != nil {
return err
}
}
time.Sleep(writeDelay)
return nil
}
func (conn *I2CConnection) pulseEnable(data byte) error {
bytes := []byte{data, data | (0x01 << conn.PinMap.EN), data}
for _, b := range bytes {
time.Sleep(pulseDelay)
err := conn.I2C.WriteByte(conn.Addr, b)
if err != nil {
return err
}
}
return nil
}
// Close closes the I²C connection.
func (conn *I2CConnection) Close() error {
glog.V(2).Info("hd44780: closing I2C bus")
return conn.I2C.Close()
}

View File

@ -0,0 +1,318 @@
package hd44780
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/kidoman/embd"
)
const (
testAddr byte = 0x20
cols = 20
rows = 4
)
var testRowAddr RowAddress = RowAddress20Col
type mockDigitalPin struct {
direction embd.Direction
values chan int
closed bool
}
func newMockDigitalPin() *mockDigitalPin {
return &mockDigitalPin{
values: make(chan int, 256),
closed: false,
}
}
func (pin *mockDigitalPin) Watch(edge embd.Edge, handler func(embd.DigitalPin)) error { return nil }
func (pin *mockDigitalPin) StopWatching() error { return nil }
func (pin *mockDigitalPin) N() int { return 0 }
func (pin *mockDigitalPin) Read() (int, error) { return 0, nil }
func (pin *mockDigitalPin) TimePulse(state int) (time.Duration, error) { return time.Duration(0), nil }
func (pin *mockDigitalPin) ActiveLow(b bool) error { return nil }
func (pin *mockDigitalPin) PullUp() error { return nil }
func (pin *mockDigitalPin) PullDown() error { return nil }
func (pin *mockDigitalPin) Write(val int) error {
pin.values <- val
return nil
}
func (pin *mockDigitalPin) SetDirection(dir embd.Direction) error {
pin.direction = dir
return nil
}
func (pin *mockDigitalPin) Close() error {
pin.closed = true
return nil
}
type mockGPIOConnection struct {
rs, en *mockDigitalPin
d4, d5, d6, d7 *mockDigitalPin
backlight *mockDigitalPin
writes []instruction
}
type instruction struct {
rs int
data byte
}
func (conn *mockGPIOConnection) Write(rs bool, data byte) error { return nil }
func (conn *mockGPIOConnection) BacklightOff() error { return nil }
func (conn *mockGPIOConnection) BacklightOn() error { return nil }
func (conn *mockGPIOConnection) Close() error { return nil }
func (ins *instruction) printAsBinary() string {
return fmt.Sprintf("RS:%d|Byte:%s", ins.rs, printByteAsBinary(ins.data))
}
func printInstructionsAsBinary(ins []instruction) string {
var binary []string
for _, i := range ins {
binary = append(binary, i.printAsBinary())
}
return fmt.Sprintf("%+v", binary)
}
func newMockGPIOConnection() *mockGPIOConnection {
conn := &mockGPIOConnection{
rs: newMockDigitalPin(),
en: newMockDigitalPin(),
d4: newMockDigitalPin(),
d5: newMockDigitalPin(),
d6: newMockDigitalPin(),
d7: newMockDigitalPin(),
backlight: newMockDigitalPin(),
}
go func() {
for {
var b byte = 0x00
var rs int = 0
// wait for EN low,high,low then read high nibble
if <-conn.en.values == embd.Low &&
<-conn.en.values == embd.High &&
<-conn.en.values == embd.Low {
rs = <-conn.rs.values
b |= byte(<-conn.d4.values) << 4
b |= byte(<-conn.d5.values) << 5
b |= byte(<-conn.d6.values) << 6
b |= byte(<-conn.d7.values) << 7
}
// wait for EN low,high,low then read low nibble
if <-conn.en.values == embd.Low &&
<-conn.en.values == embd.High &&
<-conn.en.values == embd.Low {
b |= byte(<-conn.d4.values)
b |= byte(<-conn.d5.values) << 1
b |= byte(<-conn.d6.values) << 2
b |= byte(<-conn.d7.values) << 3
conn.writes = append(conn.writes, instruction{rs, b})
}
}
}()
return conn
}
func (conn *mockGPIOConnection) pins() []*mockDigitalPin {
return []*mockDigitalPin{conn.rs, conn.en, conn.d4, conn.d5, conn.d6, conn.d7, conn.backlight}
}
type mockI2CBus struct {
writes []byte
closed bool
}
func (bus *mockI2CBus) ReadByte(addr byte) (byte, error) { return 0x00, nil }
func (bus *mockI2CBus) WriteBytes(addr byte, value []byte) error { return nil }
func (bus *mockI2CBus) ReadFromReg(addr, reg byte, value []byte) error { return nil }
func (bus *mockI2CBus) ReadByteFromReg(addr, reg byte) (byte, error) { return 0x00, nil }
func (bus *mockI2CBus) ReadWordFromReg(addr, reg byte) (uint16, error) { return 0, nil }
func (bus *mockI2CBus) WriteToReg(addr, reg byte, value []byte) error { return nil }
func (bus *mockI2CBus) WriteByteToReg(addr, reg, value byte) error { return nil }
func (bus *mockI2CBus) WriteWordToReg(addr, reg byte, value uint16) error { return nil }
func (bus *mockI2CBus) WriteByte(addr, value byte) error {
bus.writes = append(bus.writes, value)
return nil
}
func (bus *mockI2CBus) Close() error {
bus.closed = true
return nil
}
func newMockI2CBus() *mockI2CBus {
return &mockI2CBus{closed: false}
}
func printByteAsBinary(b byte) string {
return fmt.Sprintf("%08b(%#x)", b, b)
}
func printBytesAsBinary(bytes []byte) string {
var binary []string
for _, w := range bytes {
binary = append(binary, printByteAsBinary(w))
}
return fmt.Sprintf("%+v", binary)
}
func TestInitialize4Bit_directionOut(t *testing.T) {
mock := newMockGPIOConnection()
NewGPIO(mock.rs, mock.en, mock.d4, mock.d5, mock.d6, mock.d7, mock.backlight, Negative, testRowAddr)
for idx, pin := range mock.pins() {
if pin.direction != embd.Out {
t.Errorf("Pin %d not set to direction Out", idx)
}
}
}
func TestInitialize4Bit_lcdInit(t *testing.T) {
mock := newMockGPIOConnection()
gpio, _ := NewGPIO(mock.rs, mock.en, mock.d4, mock.d5, mock.d6, mock.d7, mock.backlight, Negative, testRowAddr)
instructions := []instruction{
instruction{embd.Low, lcdInit},
instruction{embd.Low, lcdInit4bit},
instruction{embd.Low, byte(gpio.eMode | lcdSetEntryMode)},
instruction{embd.Low, byte(gpio.dMode | lcdSetDisplayMode)},
instruction{embd.Low, byte(gpio.fMode | lcdSetFunctionMode)},
}
if !reflect.DeepEqual(instructions, mock.writes) {
t.Errorf(
"\nExpected\t%s\nActual\t\t%+v",
printInstructionsAsBinary(instructions),
printInstructionsAsBinary(mock.writes))
}
}
func TestGPIOConnectionClose(t *testing.T) {
mock := newMockGPIOConnection()
bus, _ := NewGPIO(mock.rs, mock.en, mock.d4, mock.d5, mock.d6, mock.d7, mock.backlight, Negative, testRowAddr)
bus.Close()
for idx, pin := range mock.pins() {
if !pin.closed {
t.Errorf("Pin %d was not closed", idx)
}
}
}
func TestI2CConnectionPinMap(t *testing.T) {
cases := []map[string]interface{}{
map[string]interface{}{
"instruction": lcdDisplayMove | lcdMoveRight,
"pinMap": MJKDZPinMap,
"expected": []byte{
0x0, // 00000000 high nibble
0x10, // 00010000
0x0, // 00000000
0xc, // 00001100 low nibble
0x1c, // 00011100
0xc, // 00001100
},
},
map[string]interface{}{
"instruction": lcdDisplayMove | lcdMoveRight,
"pinMap": PCF8574PinMap,
"expected": []byte{
0x8, // 00001000 high nibble
0xc, // 00001100
0x8, // 00001000
0xc8, // 11001000 low nibble
0xcc, // 11001100
0xc8, // 11001000
},
},
}
for idx, c := range cases {
instruction := c["instruction"].(byte)
pinMap := c["pinMap"].(I2CPinMap)
expected := c["expected"].([]byte)
i2c := newMockI2CBus()
conn := NewI2CConnection(i2c, testAddr, pinMap)
rawInstruction := instruction
// instructions (RS = false) with backlight on
conn.Backlight = true
conn.Write(false, rawInstruction)
if !reflect.DeepEqual(expected, i2c.writes) {
t.Errorf(
"Case %d:\nExpected\t%s\nActual\t\t%s",
idx+1,
printBytesAsBinary(expected),
printBytesAsBinary(i2c.writes))
}
}
}
func TestI2CConnectionClose(t *testing.T) {
i2c := newMockI2CBus()
conn := NewI2CConnection(i2c, testAddr, MJKDZPinMap)
conn.Close()
if !i2c.closed {
t.Error("I2C bus was not closed")
}
}
func TestNewGPIO_initPins(t *testing.T) {
var pins []*mockDigitalPin
for i := 0; i < 7; i++ {
pins = append(pins, newMockDigitalPin())
}
NewGPIO(
pins[0],
pins[1],
pins[2],
pins[3],
pins[4],
pins[5],
pins[6],
Negative,
testRowAddr,
)
for idx, pin := range pins {
if pin.direction != embd.Out {
t.Errorf("Pin %d not set to direction Out(%d), set to %d", idx, embd.Out, pin.direction)
}
}
}
func TestDefaultModes(t *testing.T) {
display, _ := New(newMockGPIOConnection(), testRowAddr)
if display.EightBitModeEnabled() {
t.Error("Expected display to be initialized in 4-bit mode")
}
if display.TwoLineEnabled() {
t.Error("Expected display to be initialized in one-line mode")
}
if display.Dots5x10Enabled() {
t.Error("Expected display to be initialized in 5x8-dots mode")
}
if !display.EntryIncrementEnabled() {
t.Error("Expected display to be initialized in entry increment mode")
}
if display.EntryShiftEnabled() {
t.Error("Expected display to be initialized in entry shift off mode")
}
if !display.DisplayEnabled() {
t.Error("Expected display to be initialized in display on mode")
}
if display.CursorEnabled() {
t.Error("Expected display to be initialized in cursor off mode")
}
if display.BlinkEnabled() {
t.Error("Expected display to be initialized in blink off mode")
}
}

View File

@ -0,0 +1,106 @@
/*
Package characterdisplay provides an ease-of-use layer on top of a character
display controller.
*/
package characterdisplay
// Controller is an interface that describes the basic functionality of a character
// display controller.
type Controller interface {
DisplayOff() error // turns the display off
DisplayOn() error // turns the display on
CursorOff() error // sets the cursor visibility to off
CursorOn() error // sets the cursor visibility to on
BlinkOff() error // sets the cursor blink off
BlinkOn() error // sets the cursor blink on
ShiftLeft() error // moves the cursor and text one column to the left
ShiftRight() error // moves the cursor and text one column to the right
BacklightOff() error // turns the display backlight off
BacklightOn() error // turns the display backlight on
Home() error // moves the cursor to the home position
Clear() error // clears the display and moves the cursor to the home position
WriteChar(byte) error // writes a character to the display
SetCursor(col, row int) error // sets the cursor position
Close() error // closes the controller resources
}
// Display represents an abstract character display and provides a
// ease-of-use layer on top of a character display controller.
type Display struct {
Controller
cols, rows int
p *position
}
type position struct {
col int
row int
}
// New creates a new Display
func New(controller Controller, cols, rows int) *Display {
return &Display{
Controller: controller,
cols: cols,
rows: rows,
p: &position{0, 0},
}
}
// Home moves the cursor and all characters to the home position.
func (disp *Display) Home() error {
disp.setCurrentPosition(0, 0)
return disp.Controller.Home()
}
// Clear clears the display, preserving the mode settings and setting the correct home.
func (disp *Display) Clear() error {
disp.setCurrentPosition(0, 0)
return disp.Controller.Clear()
}
// Message prints the given string on the display, including interpreting newline
// characters and wrapping at the end of lines.
func (disp *Display) Message(message string) error {
bytes := []byte(message)
for _, b := range bytes {
if b == byte('\n') {
err := disp.Newline()
if err != nil {
return err
}
continue
}
err := disp.WriteChar(b)
if err != nil {
return err
}
disp.p.col++
if disp.p.col >= disp.cols || disp.p.col < 0 {
err := disp.Newline()
if err != nil {
return err
}
}
}
return nil
}
// Newline moves the input cursor to the beginning of the next line.
func (disp *Display) Newline() error {
return disp.SetCursor(0, disp.p.row+1)
}
// SetCursor sets the input cursor to the given position.
func (disp *Display) SetCursor(col, row int) error {
if row >= disp.rows {
row = disp.rows - 1
}
disp.setCurrentPosition(col, row)
return disp.Controller.SetCursor(col, row)
}
func (disp *Display) setCurrentPosition(col, row int) {
disp.p.col = col
disp.p.row = row
}

View File

@ -0,0 +1,129 @@
package characterdisplay
import (
"reflect"
"testing"
"time"
)
const (
rows = 4
cols = 20
)
type mockController struct {
calls chan call
}
type call struct {
name string
arguments []interface{}
}
func noArgCall(name string) call {
return call{name, []interface{}{}}
}
func (mock *mockController) DisplayOff() error { mock.calls <- noArgCall("DisplayOff"); return nil }
func (mock *mockController) DisplayOn() error { mock.calls <- noArgCall("DisplayOn"); return nil }
func (mock *mockController) CursorOff() error { mock.calls <- noArgCall("CursorOff"); return nil }
func (mock *mockController) CursorOn() error { mock.calls <- noArgCall("CursorOn"); return nil }
func (mock *mockController) BlinkOff() error { mock.calls <- noArgCall("BlinkOff"); return nil }
func (mock *mockController) BlinkOn() error { mock.calls <- noArgCall("BlinkOn"); return nil }
func (mock *mockController) ShiftLeft() error { mock.calls <- noArgCall("ShiftLeft"); return nil }
func (mock *mockController) ShiftRight() error { mock.calls <- noArgCall("ShiftRight"); return nil }
func (mock *mockController) BacklightOff() error { mock.calls <- noArgCall("BacklightOff"); return nil }
func (mock *mockController) BacklightOn() error { mock.calls <- noArgCall("BacklightOn"); return nil }
func (mock *mockController) Home() error { mock.calls <- noArgCall("Home"); return nil }
func (mock *mockController) Clear() error { mock.calls <- noArgCall("Clear"); return nil }
func (mock *mockController) Close() error { mock.calls <- noArgCall("Close"); return nil }
func (mock *mockController) WriteChar(b byte) error {
mock.calls <- call{"WriteChar", []interface{}{b}}
return nil
}
func (mock *mockController) SetCursor(col, row int) error {
mock.calls <- call{"SetCursor", []interface{}{col, row}}
return nil
}
func (mock *mockController) testExpectedCalls(expectedCalls []call, t *testing.T) {
for _, expectedCall := range expectedCalls {
select {
case actualCall := <-mock.calls:
if !reflect.DeepEqual(expectedCall, actualCall) {
t.Errorf("Expected call %+v, actual call %+v", expectedCall, actualCall)
}
case <-time.After(time.Millisecond * 1):
t.Errorf("Timeout reading next call. Expected call %+v", expectedCall)
}
}
ExtraCallsCheck:
for {
select {
case extraCall := <-mock.calls:
t.Errorf("Unexpected call %+v", extraCall)
case <-time.After(time.Millisecond * 1):
break ExtraCallsCheck
}
}
}
func newMockController() *mockController {
return &mockController{make(chan call, 256)}
}
func TestNewline(t *testing.T) {
mock := newMockController()
disp := New(mock, cols, rows)
disp.Newline()
expectedCalls := []call{
call{"SetCursor", []interface{}{0, 1}},
}
mock.testExpectedCalls(expectedCalls, t)
}
func TestMessage(t *testing.T) {
mock := newMockController()
disp := New(mock, cols, rows)
disp.Message("ab")
expectedCalls := []call{
call{"WriteChar", []interface{}{byte('a')}},
call{"WriteChar", []interface{}{byte('b')}},
}
mock.testExpectedCalls(expectedCalls, t)
}
func TestMessage_newLine(t *testing.T) {
mock := newMockController()
disp := New(mock, cols, rows)
disp.Message("a\nb")
expectedCalls := []call{
call{"WriteChar", []interface{}{byte('a')}},
call{"SetCursor", []interface{}{0, 1}},
call{"WriteChar", []interface{}{byte('b')}},
}
mock.testExpectedCalls(expectedCalls, t)
}
func TestMessage_wrap(t *testing.T) {
mock := newMockController()
disp := New(mock, cols, rows)
disp.SetCursor(cols-1, 0)
disp.Message("ab")
expectedCalls := []call{
call{"SetCursor", []interface{}{cols - 1, 0}},
call{"WriteChar", []interface{}{byte('a')}},
call{"SetCursor", []interface{}{0, 1}},
call{"WriteChar", []interface{}{byte('b')}},
}
mock.testExpectedCalls(expectedCalls, t)
}

View File

@ -0,0 +1,45 @@
// +build ignore
package main
import (
"flag"
"time"
"github.com/kidoman/embd"
"github.com/kidoman/embd/controller/hd44780"
"github.com/kidoman/embd/interface/display/characterdisplay"
_ "github.com/kidoman/embd/host/all"
)
func main() {
flag.Parse()
if err := embd.InitI2C(); err != nil {
panic(err)
}
defer embd.CloseI2C()
bus := embd.NewI2CBus(1)
controller, err := hd44780.NewI2C(
bus,
0x20,
hd44780.PCF8574PinMap,
hd44780.RowAddress20Col,
hd44780.TwoLine,
hd44780.BlinkOn,
)
if err != nil {
panic(err)
}
display := characterdisplay.New(controller, 20, 4)
defer display.Close()
display.Clear()
display.Message("Hello, world!\n@embd | characterdisplay")
time.Sleep(10 * time.Second)
display.BacklightOff()
}

53
samples/hd44780.go Normal file
View File

@ -0,0 +1,53 @@
// +build ignore
package main
import (
"flag"
"time"
"github.com/kidoman/embd"
"github.com/kidoman/embd/controller/hd44780"
_ "github.com/kidoman/embd/host/all"
)
func main() {
flag.Parse()
if err := embd.InitI2C(); err != nil {
panic(err)
}
defer embd.CloseI2C()
bus := embd.NewI2CBus(1)
hd, err := hd44780.NewI2C(
bus,
0x20,
hd44780.PCF8574PinMap,
hd44780.RowAddress20Col,
hd44780.TwoLine,
hd44780.BlinkOn,
)
if err != nil {
panic(err)
}
defer hd.Close()
hd.Clear()
message := "Hello, world!"
bytes := []byte(message)
for _, b := range bytes {
hd.WriteChar(b)
}
hd.SetCursor(0, 1)
message = "@embd | hd44780"
bytes = []byte(message)
for _, b := range bytes {
hd.WriteChar(b)
}
time.Sleep(10 * time.Second)
hd.BacklightOff()
}