diff --git a/controller/hd44780/hd44780.go b/controller/hd44780/hd44780.go new file mode 100644 index 0000000..88032d5 --- /dev/null +++ b/controller/hd44780/hd44780.go @@ -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() +} diff --git a/controller/hd44780/hd44780_test.go b/controller/hd44780/hd44780_test.go new file mode 100644 index 0000000..b05dddc --- /dev/null +++ b/controller/hd44780/hd44780_test.go @@ -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") + } +} diff --git a/interface/display/characterdisplay/characterdisplay.go b/interface/display/characterdisplay/characterdisplay.go new file mode 100644 index 0000000..ca866d9 --- /dev/null +++ b/interface/display/characterdisplay/characterdisplay.go @@ -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 +} diff --git a/interface/display/characterdisplay/characterdisplay_test.go b/interface/display/characterdisplay/characterdisplay_test.go new file mode 100644 index 0000000..4bd6c39 --- /dev/null +++ b/interface/display/characterdisplay/characterdisplay_test.go @@ -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) +} diff --git a/samples/characterdisplay.go b/samples/characterdisplay.go new file mode 100644 index 0000000..7a054aa --- /dev/null +++ b/samples/characterdisplay.go @@ -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() +} diff --git a/samples/hd44780.go b/samples/hd44780.go new file mode 100644 index 0000000..418fa7e --- /dev/null +++ b/samples/hd44780.go @@ -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() +}