From dac729e4fd643dbee2d18355a6bdbe0cb0bd19d9 Mon Sep 17 00:00:00 2001 From: Matthew Dale Date: Sat, 27 Dec 2014 00:28:15 -0800 Subject: [PATCH 1/3] controller: add a lib for the HD44780 character display controller the hd44780 package supports HD44780 character display controllers connected by either a 4-bit GPIO bus or an I2C bus it also includes a high-level wrapper for easily printing messages --- controller/hd44780/character_display.go | 212 +++++++ controller/hd44780/character_display_test.go | 85 +++ controller/hd44780/hd44780.go | 572 +++++++++++++++++++ controller/hd44780/hd44780_test.go | 255 +++++++++ samples/hd44780.go | 43 ++ 5 files changed, 1167 insertions(+) create mode 100644 controller/hd44780/character_display.go create mode 100644 controller/hd44780/character_display_test.go create mode 100644 controller/hd44780/hd44780.go create mode 100644 controller/hd44780/hd44780_test.go create mode 100644 samples/hd44780.go diff --git a/controller/hd44780/character_display.go b/controller/hd44780/character_display.go new file mode 100644 index 0000000..6c23099 --- /dev/null +++ b/controller/hd44780/character_display.go @@ -0,0 +1,212 @@ +package hd44780 + +import ( + "github.com/golang/glog" + "github.com/kidoman/embd" +) + +// DefaultModes are the default initialization modes for a CharacterDisplay. +var DefaultModes []ModeSetter = []ModeSetter{ + FourBitMode, + OneLine, + Dots5x8, + EntryIncrement, + EntryShiftOff, + DisplayOn, + CursorOff, + BlinkOff, +} + +// CharacterDisplay represents an abstract character display and provides a +// convenience layer on top of the basic HD44780 library. +type CharacterDisplay struct { + *HD44780 + Cols int + Rows int + p *position +} + +type position struct { + col int + row int +} + +// NewGPIOCharacterDisplay creates a new CharacterDisplay connected by a 4-bit GPIO bus. +func NewGPIOCharacterDisplay( + rs, en, d4, d5, d6, d7, backlight interface{}, + blPolarity Polarity, + cols, rows int, + modes ...ModeSetter, +) (*CharacterDisplay, 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 + } + hd, err := NewGPIO( + pins[0], + pins[1], + pins[2], + pins[3], + pins[4], + pins[5], + pins[6], + blPolarity, + append(DefaultModes, modes...)..., + ) + if err != nil { + return nil, err + } + return NewCharacterDisplay(hd, cols, rows) +} + +// NewI2CCharacterDisplay creates a new CharacterDisplay connected by an I²C bus. +func NewI2CCharacterDisplay( + i2c embd.I2CBus, + addr byte, + pinMap I2CPinMap, + cols, rows int, + modes ...ModeSetter, +) (*CharacterDisplay, error) { + hd, err := NewI2C(i2c, addr, pinMap, append(DefaultModes, modes...)...) + if err != nil { + return nil, err + } + return NewCharacterDisplay(hd, cols, rows) +} + +// NewCharacterDisplay creates a new character display abstraction for an +// HD44780-compatible controller. +func NewCharacterDisplay(hd *HD44780, cols, rows int) (*CharacterDisplay, error) { + display := &CharacterDisplay{ + HD44780: hd, + Cols: cols, + Rows: rows, + p: &position{0, 0}, + } + err := display.BacklightOn() + if err != nil { + return nil, err + } + return display, nil +} + +// Home moves the cursor and all characters to the home position. +func (disp *CharacterDisplay) Home() error { + disp.currentPosition(0, 0) + return disp.HD44780.Home() +} + +// Clear clears the display, preserving the mode settings and setting the correct home. +func (disp *CharacterDisplay) Clear() error { + disp.currentPosition(0, 0) + err := disp.HD44780.Clear() + if err != nil { + return err + } + err = disp.SetMode() + if err != nil { + return err + } + if !disp.isLeftToRight() { + return disp.SetCursor(disp.Cols-1, 0) + } + return nil +} + +// Message prints the given string on the display. +func (disp *CharacterDisplay) 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 + } + if disp.isLeftToRight() { + disp.p.col++ + } else { + 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 *CharacterDisplay) Newline() error { + var col int + if disp.isLeftToRight() { + col = 0 + } else { + col = disp.Cols - 1 + } + return disp.SetCursor(col, disp.p.row+1) +} + +func (disp *CharacterDisplay) isLeftToRight() bool { + // EntryIncrement and EntryShiftOn is right-to-left + // EntryDecrement and EntryShiftOn is left-to-right + // EntryIncrement and EntryShiftOff is left-to-right + // EntryDecrement and EntryShiftOff is right-to-left + return disp.EntryIncrementEnabled() != disp.EntryShiftEnabled() +} + +// SetCursor sets the input cursor to the given position. +func (disp *CharacterDisplay) SetCursor(col, row int) error { + if row >= disp.Rows { + row = disp.Rows - 1 + } + disp.currentPosition(col, row) + return disp.HD44780.SetCursor(byte(col) + disp.lcdRowOffset(row)) +} + +func (disp *CharacterDisplay) lcdRowOffset(row int) byte { + // Offset for up to 4 rows + if row > 3 { + row = 3 + } + switch disp.Cols { + case 16: + // 16-char line mappings + return []byte{0x00, 0x40, 0x10, 0x50}[row] + default: + // default to the 20-char line mappings + return []byte{0x00, 0x40, 0x14, 0x54}[row] + } +} + +func (disp *CharacterDisplay) currentPosition(col, row int) { + disp.p.col = col + disp.p.row = row +} + +// Close closes the underlying HD44780 controller. +func (disp *CharacterDisplay) Close() error { + return disp.HD44780.Close() +} diff --git a/controller/hd44780/character_display_test.go b/controller/hd44780/character_display_test.go new file mode 100644 index 0000000..e2eca42 --- /dev/null +++ b/controller/hd44780/character_display_test.go @@ -0,0 +1,85 @@ +package hd44780 + +import ( + "testing" + + "github.com/kidoman/embd" +) + +const ( + rows = 20 + cols = 4 +) + +func TestNewGPIOCharacterDisplay_initPins(t *testing.T) { + var pins []*mockDigitalPin + for i := 0; i < 7; i++ { + pins = append(pins, newMockDigitalPin()) + } + NewGPIOCharacterDisplay( + pins[0], + pins[1], + pins[2], + pins[3], + pins[4], + pins[5], + pins[6], + Negative, + cols, + rows, + ) + 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) { + displayGPIO, _ := NewGPIOCharacterDisplay( + newMockDigitalPin(), + newMockDigitalPin(), + newMockDigitalPin(), + newMockDigitalPin(), + newMockDigitalPin(), + newMockDigitalPin(), + newMockDigitalPin(), + Negative, + cols, + rows, + ) + displayI2C, _ := NewI2CCharacterDisplay( + newMockI2CBus(), + testAddr, + MJKDZPinMap, + cols, + rows, + ) + + for idx, display := range []*CharacterDisplay{displayGPIO, displayI2C} { + if display.EightBitModeEnabled() { + t.Errorf("Display %d: Expected display to be initialized in 4-bit mode", idx) + } + if display.TwoLineEnabled() { + t.Errorf("Display %d: Expected display to be initialized in one-line mode", idx) + } + if display.Dots5x10Enabled() { + t.Errorf("Display %d: Expected display to be initialized in 5x8-dots mode", idx) + } + if !display.EntryIncrementEnabled() { + t.Errorf("Display %d: Expected display to be initialized in entry increment mode", idx) + } + if display.EntryShiftEnabled() { + t.Errorf("Display %d: Expected display to be initialized in entry shift off mode", idx) + } + if !display.DisplayEnabled() { + t.Errorf("Display %d: Expected display to be initialized in display on mode", idx) + } + if display.CursorEnabled() { + t.Errorf("Display %d: Expected display to be initialized in cursor off mode", idx) + } + if display.BlinkEnabled() { + t.Errorf("Display %d: Expected display to be initialized in blink off mode", idx) + } + } +} diff --git a/controller/hd44780/hd44780.go b/controller/hd44780/hd44780.go new file mode 100644 index 0000000..a977054 --- /dev/null +++ b/controller/hd44780/hd44780.go @@ -0,0 +1,572 @@ +/* +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. + +Character Display + +The CharacterDisplay type provides a convenience layer on top of the basic +HD44780 library. It includes functions for easier message printing and abstracts +some of the quirky behaviors of 4-row displays. + +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 Polarity bool +type entryMode byte +type displayMode byte +type functionMode byte + +const ( + Negative Polarity = false + Positive Polarity = 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 +} + +// NewGPIO creates a new HD44780 connected by a 4-bit GPIO bus. +func NewGPIO( + rs, en, d4, d5, d6, d7, backlight embd.DigitalPin, + blPolarity Polarity, + modes ...ModeSetter, +) (*HD44780, error) { + pins := []embd.DigitalPin{rs, en, d4, d5, d6, d7, backlight} + 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(rs, en, d4, d5, d6, d7, backlight, blPolarity), + modes..., + ) +} + +// NewI2C creates a new HD44780 connected by an I²C bus. +func NewI2C(i2c embd.I2CBus, addr byte, pinMap I2CPinMap, modes ...ModeSetter) (*HD44780, error) { + return New(NewI2CConnection(i2c, addr, pinMap), modes...) +} + +// New creates a new HD44780 connected by a Connection bus. +func New(bus Connection, modes ...ModeSetter) (*HD44780, error) { + controller := &HD44780{ + Connection: bus, + eMode: 0x00, + dMode: 0x00, + fMode: 0x00, + } + err := controller.lcdInit() + if err != nil { + return nil, err + } + err = controller.SetMode(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) +} + +// 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) + time.Sleep(clearDelay) + return err +} + +// SetCursor sets the input cursor to the given bye. +func (hd *HD44780) SetCursor(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 Polarity +} + +// NewGPIOConnection returns a new Connection based on a 4-bit GPIO bus. +func NewGPIOConnection( + rs, en, d4, d5, d6, d7, backlight embd.DigitalPin, + blPolarity Polarity, +) *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 Polarity +} + +var ( + // 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, + } + // 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..a061140 --- /dev/null +++ b/controller/hd44780/hd44780_test.go @@ -0,0 +1,255 @@ +package hd44780 + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/kidoman/embd" +) + +const testAddr byte = 0x20 + +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 (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 { + be := &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 <-be.en.values == embd.Low && + <-be.en.values == embd.High && + <-be.en.values == embd.Low { + rs = <-be.rs.values + b |= byte(<-be.d4.values) << 4 + b |= byte(<-be.d5.values) << 5 + b |= byte(<-be.d6.values) << 6 + b |= byte(<-be.d7.values) << 7 + } + // wait for EN low,high,low then read low nibble + if <-be.en.values == embd.Low && + <-be.en.values == embd.High && + <-be.en.values == embd.Low { + b |= byte(<-be.d4.values) + b |= byte(<-be.d5.values) << 1 + b |= byte(<-be.d6.values) << 2 + b |= byte(<-be.d7.values) << 3 + be.writes = append(be.writes, instruction{rs, b}) + } + } + }() + return be +} + +func (be *mockGPIOConnection) pins() []*mockDigitalPin { + return []*mockDigitalPin{be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.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) { + be := newMockGPIOConnection() + NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) + for idx, pin := range be.pins() { + if pin.direction != embd.Out { + t.Errorf("Pin %d not set to direction Out", idx) + } + } +} + +func TestInitialize4Bit_lcdInit(t *testing.T) { + be := newMockGPIOConnection() + NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) + instructions := []instruction{ + instruction{embd.Low, lcdInit}, + instruction{embd.Low, lcdInit4bit}, + instruction{embd.Low, byte(lcdSetEntryMode)}, + instruction{embd.Low, byte(lcdSetDisplayMode)}, + instruction{embd.Low, byte(lcdSetFunctionMode)}, + } + + if !reflect.DeepEqual(instructions, be.writes) { + t.Errorf( + "\nExpected\t%s\nActual\t\t%+v", + printInstructionsAsBinary(instructions), + printInstructionsAsBinary(be.writes)) + } +} + +func TestGPIOConnectionClose(t *testing.T) { + be := newMockGPIOConnection() + bus, _ := NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) + bus.Close() + for idx, pin := range be.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") + } +} diff --git a/samples/hd44780.go b/samples/hd44780.go new file mode 100644 index 0000000..3307a4c --- /dev/null +++ b/samples/hd44780.go @@ -0,0 +1,43 @@ +// +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) + + display, err := hd44780.NewI2CCharacterDisplay( + bus, + 0x20, + hd44780.PCF8574PinMap, + 20, + 4, + hd44780.TwoLine, + hd44780.BlinkOn, + ) + if err != nil { + panic(err) + } + defer display.Close() + + display.Clear() + display.Message("Hello, world!\n@embd") + time.Sleep(10 * time.Second) + display.BacklightOff() +} From 915b3b76a757075500bc585c6a008cefdf233745 Mon Sep 17 00:00:00 2001 From: Matthew Dale Date: Sat, 21 Feb 2015 22:50:56 -0800 Subject: [PATCH 2/3] interface: add an abstraction layer for character displays the characterdisplay package is an abstraction layer for controlling character displays also includes refactors to the hd44780 package to fit the characterdisplay Controller interface --- controller/hd44780/character_display.go | 212 ------------------ controller/hd44780/character_display_test.go | 85 ------- controller/hd44780/hd44780.go | 121 +++++++--- controller/hd44780/hd44780_test.go | 131 ++++++++--- .../characterdisplay/characterdisplay.go | 106 +++++++++ .../characterdisplay/characterdisplay_test.go | 129 +++++++++++ samples/characterdisplay.go | 45 ++++ samples/hd44780.go | 24 +- 8 files changed, 489 insertions(+), 364 deletions(-) delete mode 100644 controller/hd44780/character_display.go delete mode 100644 controller/hd44780/character_display_test.go create mode 100644 interface/display/characterdisplay/characterdisplay.go create mode 100644 interface/display/characterdisplay/characterdisplay_test.go create mode 100644 samples/characterdisplay.go diff --git a/controller/hd44780/character_display.go b/controller/hd44780/character_display.go deleted file mode 100644 index 6c23099..0000000 --- a/controller/hd44780/character_display.go +++ /dev/null @@ -1,212 +0,0 @@ -package hd44780 - -import ( - "github.com/golang/glog" - "github.com/kidoman/embd" -) - -// DefaultModes are the default initialization modes for a CharacterDisplay. -var DefaultModes []ModeSetter = []ModeSetter{ - FourBitMode, - OneLine, - Dots5x8, - EntryIncrement, - EntryShiftOff, - DisplayOn, - CursorOff, - BlinkOff, -} - -// CharacterDisplay represents an abstract character display and provides a -// convenience layer on top of the basic HD44780 library. -type CharacterDisplay struct { - *HD44780 - Cols int - Rows int - p *position -} - -type position struct { - col int - row int -} - -// NewGPIOCharacterDisplay creates a new CharacterDisplay connected by a 4-bit GPIO bus. -func NewGPIOCharacterDisplay( - rs, en, d4, d5, d6, d7, backlight interface{}, - blPolarity Polarity, - cols, rows int, - modes ...ModeSetter, -) (*CharacterDisplay, 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 - } - hd, err := NewGPIO( - pins[0], - pins[1], - pins[2], - pins[3], - pins[4], - pins[5], - pins[6], - blPolarity, - append(DefaultModes, modes...)..., - ) - if err != nil { - return nil, err - } - return NewCharacterDisplay(hd, cols, rows) -} - -// NewI2CCharacterDisplay creates a new CharacterDisplay connected by an I²C bus. -func NewI2CCharacterDisplay( - i2c embd.I2CBus, - addr byte, - pinMap I2CPinMap, - cols, rows int, - modes ...ModeSetter, -) (*CharacterDisplay, error) { - hd, err := NewI2C(i2c, addr, pinMap, append(DefaultModes, modes...)...) - if err != nil { - return nil, err - } - return NewCharacterDisplay(hd, cols, rows) -} - -// NewCharacterDisplay creates a new character display abstraction for an -// HD44780-compatible controller. -func NewCharacterDisplay(hd *HD44780, cols, rows int) (*CharacterDisplay, error) { - display := &CharacterDisplay{ - HD44780: hd, - Cols: cols, - Rows: rows, - p: &position{0, 0}, - } - err := display.BacklightOn() - if err != nil { - return nil, err - } - return display, nil -} - -// Home moves the cursor and all characters to the home position. -func (disp *CharacterDisplay) Home() error { - disp.currentPosition(0, 0) - return disp.HD44780.Home() -} - -// Clear clears the display, preserving the mode settings and setting the correct home. -func (disp *CharacterDisplay) Clear() error { - disp.currentPosition(0, 0) - err := disp.HD44780.Clear() - if err != nil { - return err - } - err = disp.SetMode() - if err != nil { - return err - } - if !disp.isLeftToRight() { - return disp.SetCursor(disp.Cols-1, 0) - } - return nil -} - -// Message prints the given string on the display. -func (disp *CharacterDisplay) 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 - } - if disp.isLeftToRight() { - disp.p.col++ - } else { - 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 *CharacterDisplay) Newline() error { - var col int - if disp.isLeftToRight() { - col = 0 - } else { - col = disp.Cols - 1 - } - return disp.SetCursor(col, disp.p.row+1) -} - -func (disp *CharacterDisplay) isLeftToRight() bool { - // EntryIncrement and EntryShiftOn is right-to-left - // EntryDecrement and EntryShiftOn is left-to-right - // EntryIncrement and EntryShiftOff is left-to-right - // EntryDecrement and EntryShiftOff is right-to-left - return disp.EntryIncrementEnabled() != disp.EntryShiftEnabled() -} - -// SetCursor sets the input cursor to the given position. -func (disp *CharacterDisplay) SetCursor(col, row int) error { - if row >= disp.Rows { - row = disp.Rows - 1 - } - disp.currentPosition(col, row) - return disp.HD44780.SetCursor(byte(col) + disp.lcdRowOffset(row)) -} - -func (disp *CharacterDisplay) lcdRowOffset(row int) byte { - // Offset for up to 4 rows - if row > 3 { - row = 3 - } - switch disp.Cols { - case 16: - // 16-char line mappings - return []byte{0x00, 0x40, 0x10, 0x50}[row] - default: - // default to the 20-char line mappings - return []byte{0x00, 0x40, 0x14, 0x54}[row] - } -} - -func (disp *CharacterDisplay) currentPosition(col, row int) { - disp.p.col = col - disp.p.row = row -} - -// Close closes the underlying HD44780 controller. -func (disp *CharacterDisplay) Close() error { - return disp.HD44780.Close() -} diff --git a/controller/hd44780/character_display_test.go b/controller/hd44780/character_display_test.go deleted file mode 100644 index e2eca42..0000000 --- a/controller/hd44780/character_display_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package hd44780 - -import ( - "testing" - - "github.com/kidoman/embd" -) - -const ( - rows = 20 - cols = 4 -) - -func TestNewGPIOCharacterDisplay_initPins(t *testing.T) { - var pins []*mockDigitalPin - for i := 0; i < 7; i++ { - pins = append(pins, newMockDigitalPin()) - } - NewGPIOCharacterDisplay( - pins[0], - pins[1], - pins[2], - pins[3], - pins[4], - pins[5], - pins[6], - Negative, - cols, - rows, - ) - 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) { - displayGPIO, _ := NewGPIOCharacterDisplay( - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - newMockDigitalPin(), - Negative, - cols, - rows, - ) - displayI2C, _ := NewI2CCharacterDisplay( - newMockI2CBus(), - testAddr, - MJKDZPinMap, - cols, - rows, - ) - - for idx, display := range []*CharacterDisplay{displayGPIO, displayI2C} { - if display.EightBitModeEnabled() { - t.Errorf("Display %d: Expected display to be initialized in 4-bit mode", idx) - } - if display.TwoLineEnabled() { - t.Errorf("Display %d: Expected display to be initialized in one-line mode", idx) - } - if display.Dots5x10Enabled() { - t.Errorf("Display %d: Expected display to be initialized in 5x8-dots mode", idx) - } - if !display.EntryIncrementEnabled() { - t.Errorf("Display %d: Expected display to be initialized in entry increment mode", idx) - } - if display.EntryShiftEnabled() { - t.Errorf("Display %d: Expected display to be initialized in entry shift off mode", idx) - } - if !display.DisplayEnabled() { - t.Errorf("Display %d: Expected display to be initialized in display on mode", idx) - } - if display.CursorEnabled() { - t.Errorf("Display %d: Expected display to be initialized in cursor off mode", idx) - } - if display.BlinkEnabled() { - t.Errorf("Display %d: Expected display to be initialized in blink off mode", idx) - } - } -} diff --git a/controller/hd44780/hd44780.go b/controller/hd44780/hd44780.go index a977054..3531230 100644 --- a/controller/hd44780/hd44780.go +++ b/controller/hd44780/hd44780.go @@ -3,12 +3,6 @@ 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. -Character Display - -The CharacterDisplay type provides a convenience layer on top of the basic -HD44780 library. It includes functions for easier message printing and abstracts -some of the quirky behaviors of 4-row displays. - Resources This library is based three other HD44780 libraries: @@ -25,14 +19,24 @@ import ( "github.com/kidoman/embd" ) -type Polarity bool type entryMode byte type displayMode byte type functionMode byte +// RowAddress defines the DDRAM address of the first column of each row, up to 4 rows. +type RowAddress [4]byte + +var ( + RowAddress16Col RowAddress = [4]byte{0x00, 0x40, 0x10, 0x50} // row addresses for 16-column displays + RowAddress20Col RowAddress = [4]byte{0x00, 0x40, 0x14, 0x54} // row addresses for 20-column displays +) + +// BacklightPolarity defines the polarity of the backlight switch, either positive or negative +type BacklightPolarity bool + const ( - Negative Polarity = false - Positive Polarity = true + Negative BacklightPolarity = false + Positive BacklightPolarity = true writeDelay = 37 * time.Microsecond pulseDelay = 1 * time.Microsecond @@ -84,18 +88,38 @@ const ( // HD44780 represents an HD44780-compatible character LCD controller. type HD44780 struct { Connection - eMode entryMode - dMode displayMode - fMode functionMode + 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 embd.DigitalPin, - blPolarity Polarity, + rs, en, d4, d5, d6, d7, backlight interface{}, + blPolarity BacklightPolarity, + rowAddr RowAddress, modes ...ModeSetter, ) (*HD44780, error) { - pins := []embd.DigitalPin{rs, en, d4, d5, d6, d7, backlight} + 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 @@ -107,29 +131,45 @@ func NewGPIO( } } return New( - NewGPIOConnection(rs, en, d4, d5, d6, d7, backlight, blPolarity), + 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, modes ...ModeSetter) (*HD44780, error) { - return New(NewI2CConnection(i2c, addr, pinMap), modes...) +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, modes ...ModeSetter) (*HD44780, error) { +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(modes...) + err = controller.SetMode(append(DefaultModes, modes...)...) if err != nil { return nil, err } @@ -146,6 +186,18 @@ func (controller *HD44780) lcdInit() error { return controller.WriteInstruction(lcdInit4bit) } +// DefaultModes are the default initialization modes for an HD44780. +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) @@ -312,12 +364,29 @@ func (hd *HD44780) Home() error { // 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) - return err + // have to set mode here because clear also clears some mode settings + return hd.SetMode() } -// SetCursor sets the input cursor to the given bye. -func (hd *HD44780) SetCursor(value byte) error { +// 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) } @@ -357,13 +426,13 @@ type GPIOConnection struct { RS, EN embd.DigitalPin D4, D5, D6, D7 embd.DigitalPin Backlight embd.DigitalPin - BLPolarity Polarity + 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 Polarity, + blPolarity BacklightPolarity, ) *GPIOConnection { return &GPIOConnection{ RS: rs, @@ -480,7 +549,7 @@ type I2CPinMap struct { RS, RW, EN byte D4, D5, D6, D7 byte Backlight byte - BLPolarity Polarity + BLPolarity BacklightPolarity } var ( diff --git a/controller/hd44780/hd44780_test.go b/controller/hd44780/hd44780_test.go index a061140..b05dddc 100644 --- a/controller/hd44780/hd44780_test.go +++ b/controller/hd44780/hd44780_test.go @@ -9,7 +9,13 @@ import ( "github.com/kidoman/embd" ) -const testAddr byte = 0x20 +const ( + testAddr byte = 0x20 + cols = 20 + rows = 4 +) + +var testRowAddr RowAddress = RowAddress20Col type mockDigitalPin struct { direction embd.Direction @@ -60,6 +66,11 @@ type instruction struct { 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)) } @@ -73,7 +84,7 @@ func printInstructionsAsBinary(ins []instruction) string { } func newMockGPIOConnection() *mockGPIOConnection { - be := &mockGPIOConnection{ + conn := &mockGPIOConnection{ rs: newMockDigitalPin(), en: newMockDigitalPin(), d4: newMockDigitalPin(), @@ -87,32 +98,32 @@ func newMockGPIOConnection() *mockGPIOConnection { var b byte = 0x00 var rs int = 0 // wait for EN low,high,low then read high nibble - if <-be.en.values == embd.Low && - <-be.en.values == embd.High && - <-be.en.values == embd.Low { - rs = <-be.rs.values - b |= byte(<-be.d4.values) << 4 - b |= byte(<-be.d5.values) << 5 - b |= byte(<-be.d6.values) << 6 - b |= byte(<-be.d7.values) << 7 + 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 <-be.en.values == embd.Low && - <-be.en.values == embd.High && - <-be.en.values == embd.Low { - b |= byte(<-be.d4.values) - b |= byte(<-be.d5.values) << 1 - b |= byte(<-be.d6.values) << 2 - b |= byte(<-be.d7.values) << 3 - be.writes = append(be.writes, instruction{rs, b}) + 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 be + return conn } -func (be *mockGPIOConnection) pins() []*mockDigitalPin { - return []*mockDigitalPin{be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight} +func (conn *mockGPIOConnection) pins() []*mockDigitalPin { + return []*mockDigitalPin{conn.rs, conn.en, conn.d4, conn.d5, conn.d6, conn.d7, conn.backlight} } type mockI2CBus struct { @@ -156,9 +167,9 @@ func printBytesAsBinary(bytes []byte) string { } func TestInitialize4Bit_directionOut(t *testing.T) { - be := newMockGPIOConnection() - NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) - for idx, pin := range be.pins() { + 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) } @@ -166,29 +177,29 @@ func TestInitialize4Bit_directionOut(t *testing.T) { } func TestInitialize4Bit_lcdInit(t *testing.T) { - be := newMockGPIOConnection() - NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) + 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(lcdSetEntryMode)}, - instruction{embd.Low, byte(lcdSetDisplayMode)}, - instruction{embd.Low, byte(lcdSetFunctionMode)}, + 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, be.writes) { + if !reflect.DeepEqual(instructions, mock.writes) { t.Errorf( "\nExpected\t%s\nActual\t\t%+v", printInstructionsAsBinary(instructions), - printInstructionsAsBinary(be.writes)) + printInstructionsAsBinary(mock.writes)) } } func TestGPIOConnectionClose(t *testing.T) { - be := newMockGPIOConnection() - bus, _ := NewGPIO(be.rs, be.en, be.d4, be.d5, be.d6, be.d7, be.backlight, Negative) + 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 be.pins() { + for idx, pin := range mock.pins() { if !pin.closed { t.Errorf("Pin %d was not closed", idx) } @@ -253,3 +264,55 @@ func TestI2CConnectionClose(t *testing.T) { 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 index 3307a4c..418fa7e 100644 --- a/samples/hd44780.go +++ b/samples/hd44780.go @@ -22,22 +22,32 @@ func main() { bus := embd.NewI2CBus(1) - display, err := hd44780.NewI2CCharacterDisplay( + hd, err := hd44780.NewI2C( bus, 0x20, hd44780.PCF8574PinMap, - 20, - 4, + hd44780.RowAddress20Col, hd44780.TwoLine, hd44780.BlinkOn, ) if err != nil { panic(err) } - defer display.Close() + defer hd.Close() - display.Clear() - display.Message("Hello, world!\n@embd") + 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) - display.BacklightOff() + hd.BacklightOff() } From 3254f15093cbe9566be45c3940c9f4522c9c0866 Mon Sep 17 00:00:00 2001 From: Matthew Dale Date: Thu, 9 Apr 2015 00:52:27 -0700 Subject: [PATCH 3/3] controller: add more descriptive docs to the hd44780 exported variables --- controller/hd44780/hd44780.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/controller/hd44780/hd44780.go b/controller/hd44780/hd44780.go index 3531230..88032d5 100644 --- a/controller/hd44780/hd44780.go +++ b/controller/hd44780/hd44780.go @@ -23,19 +23,25 @@ type entryMode byte type displayMode byte type functionMode byte -// RowAddress defines the DDRAM address of the first column of each row, up to 4 rows. +// 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 RowAddress = [4]byte{0x00, 0x40, 0x10, 0x50} // row addresses for 16-column displays - RowAddress20Col RowAddress = [4]byte{0x00, 0x40, 0x14, 0x54} // row addresses for 20-column displays + // 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 defines the polarity of the backlight switch, either positive or negative +// 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 @@ -187,6 +193,7 @@ func (controller *HD44780) lcdInit() error { } // 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, @@ -553,14 +560,14 @@ type I2CPinMap struct { } var ( - // Standard pin mapping for an MJKDZ-based I²C backpack. + // 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, } - // Standard pin mapping for a PCF8574-based I²C backpack. + // 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,