diff --git a/.travis.yml b/.travis.yml index 795fd2c..14c3702 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,9 @@ branches: - go-rpi go: - - 1.2 - - 1.2.1 + - 1.2.2 + - 1.3.3 + - 1.4 - tip script: diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9c5ffef..f44d830 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -5,3 +5,4 @@ Marco P. Monteiro Nikesh Vora Steve Beaulac Al S-M +Ben Delarre diff --git a/README.md b/README.md index ed8ade3..f37b5b5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ It allows you to start your hardware hack on easily available hobby boards (like the Raspberry Pi, BeagleBone Black, etc.) by giving you staight forward access to the board's capabilities as well as a plethora of **sensors** (like accelerometers, gyroscopes, thermometers, etc.) and **controllers** (PWM generators, digital-to-analog convertors) for which we have written drivers. And when things get serious, you dont have to throw away the code. You carry forward the effort onto more custom designed boards where the HAL abstraction of EMBD will save you precious time. -Development supported and sponsored by [**ThoughtWorks**](http://www.thoughtworks.com/) +Development supported and sponsored by [**SoStronk**](https://www.sostronk.com) and [**ThoughtWorks**](http://www.thoughtworks.com/) Also, you might be interested in: [Why Golang?](https://github.com/kidoman/embd/wiki/Why-Go) @@ -66,9 +66,10 @@ Join the [mailing list](https://groups.google.com/forum/#!forum/go-embd) ## Platforms Supported -* [RaspberryPi](http://www.raspberrypi.org/) +* [RaspberryPi](http://www.raspberrypi.org/) (including [A+](http://www.raspberrypi.org/products/model-a-plus/) and [B+](http://www.raspberrypi.org/products/model-b-plus/)) +* [RaspberryPi 2](http://www.raspberrypi.org/) * [BeagleBone Black](http://beagleboard.org/Products/BeagleBone%20Black) -* [Intel Galileo](http://www.intel.com/content/www/us/en/do-it-yourself/galileo-maker-quark-board.html) **coming soon** +* [Intel Edison](http://www.intel.com/content/www/us/en/do-it-yourself/galileo-maker-quark-board.html) **coming soon** * [Radxa](http://radxa.com/) **coming soon** * [Cubietruck](http://www.cubietruck.com/) **coming soon** * Bring Your Own **coming soon** @@ -224,7 +225,7 @@ platforms. * **US020** Ultrasonic proximity sensor [Documentation](http://godoc.org/github.com/kidoman/embd/sensor/us020), [Product Page](http://www.digibay.in/sensor/object-detection-and-proximity?product_id=239) -* **BH1750FVI** Luminosity sensor [Documentation](http://godoc.org/github.com/kidoman/embd/sensor/us020), [Datasheet](http://www.elechouse.com/elechouse/images/product/Digital%20light%20Sensor/bh1750fvi-e.pdf) +* **BH1750FVI** Luminosity sensor [Documentation](http://godoc.org/github.com/kidoman/embd/sensor/bh1750fvi), [Datasheet](http://www.elechouse.com/elechouse/images/product/Digital%20light%20Sensor/bh1750fvi-e.pdf) ## Interfaces 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/detect.go b/detect.go index 07cf61e..a92ff4e 100644 --- a/detect.go +++ b/detect.go @@ -42,10 +42,6 @@ func execOutput(name string, arg ...string) (output string, err error) { return } -func nodeName() (string, error) { - return execOutput("uname", "-n") -} - func parseVersion(str string) (major, minor, patch int, err error) { versionNumber := strings.Split(str, "-") parts := strings.Split(versionNumber[0], ".") @@ -77,24 +73,35 @@ func kernelVersion() (major, minor, patch int, err error) { return parseVersion(output) } -func getPiRevision() (int, error) { - //default return code of a rev2 board - cpuinfo, err := ioutil.ReadFile("/proc/cpuinfo") +func cpuInfo() (model, hardware string, revision int, err error) { + output, err := ioutil.ReadFile("/proc/cpuinfo") if err != nil { - return 4, err + return "", "", 0, err } - for _, line := range strings.Split(string(cpuinfo), "\n") { - fields := strings.Fields(line) - if len(fields) > 0 && fields[0] == "Revision" { - rev, err := strconv.ParseInt(fields[2], 16, 8) - return int(rev), err + for _, line := range strings.Split(string(output), "\n") { + fields := strings.Split(line, ":") + if len(fields) < 1 { + continue + } + switch { + case strings.HasPrefix(fields[0], "Revision"): + revs := strings.TrimSpace(fields[1]) + rev, err := strconv.ParseInt(revs, 16, 32) + if err != nil { + continue + } + revision = int(rev) + case strings.HasPrefix(fields[0], "Hardware"): + hardware = fields[1] + case strings.HasPrefix(fields[0], "model name"): + model = fields[1] } } - return 4, nil + return model, hardware, revision, nil } // DetectHost returns the detected host and its revision number. -func DetectHost() (Host, int, error) { +func DetectHost() (host Host, rev int, err error) { major, minor, patch, err := kernelVersion() if err != nil { return HostNull, 0, err @@ -104,23 +111,17 @@ func DetectHost() (Host, int, error) { return HostNull, 0, fmt.Errorf("embd: linux kernel versions lower than 3.8 are not supported. you have %v.%v.%v", major, minor, patch) } - node, err := nodeName() + model, hardware, rev, err := cpuInfo() if err != nil { return HostNull, 0, err } - var host Host - var rev int - - switch node { - case "raspberrypi": - host = HostRPi - rev, _ = getPiRevision() - case "beaglebone": - host = HostBBB + switch { + case strings.Contains(model, "ARMv7") && (strings.Contains(hardware, "AM33XX") || strings.Contains(hardware, "AM335X")): + return HostBBB, rev, nil + case strings.Contains(hardware, "BCM2708") || strings.Contains(hardware, "BCM2709"): + return HostRPi, rev, nil default: - return HostNull, 0, fmt.Errorf("embd: your host %q is not supported at this moment. please request support at https://github.com/kidoman/embd/issues", node) + return HostNull, 0, fmt.Errorf(`embd: your host "%v:%v" is not supported at this moment. request support at https://github.com/kidoman/embd/issues`, host, model) } - - return host, rev, nil } diff --git a/embd/detect.go b/embd/detect.go index 8d268e6..6659980 100644 --- a/embd/detect.go +++ b/embd/detect.go @@ -14,7 +14,7 @@ func detect(c *cli.Context) { fmt.Println(err) os.Exit(1) } - fmt.Printf("detected host %v (rev %v)\n", host, rev) + fmt.Printf("detected host %v (rev %#x)\n", host, rev) } var detectCmd = cli.Command{ diff --git a/gpio.go b/gpio.go index 17badd2..26bd866 100644 --- a/gpio.go +++ b/gpio.go @@ -125,6 +125,9 @@ type PWMPin interface { // GPIODriver implements a generic GPIO driver. type GPIODriver interface { + // PinMap returns the pinmap for this driver. + PinMap() PinMap + // Unregister unregisters the pin from the driver. Should be called when the pin is closed. Unregister(string) error diff --git a/gpiodriver.go b/gpiodriver.go index 07fceff..be2f8d8 100644 --- a/gpiodriver.go +++ b/gpiodriver.go @@ -107,6 +107,10 @@ func (io *gpioDriver) PWMPin(key interface{}) (PWMPin, error) { return p, nil } +func (io *gpioDriver) PinMap() PinMap { + return io.pinMap +} + func (io *gpioDriver) Close() error { for _, p := range io.initializedPins { if err := p.Close(); err != nil { diff --git a/host/generic/interrupt.go b/host/generic/interrupt.go index e2ebfa8..4052283 100644 --- a/host/generic/interrupt.go +++ b/host/generic/interrupt.go @@ -31,33 +31,33 @@ func (i *interrupt) Signal() { i.handler(i.pin) } -type ePollListener struct { - epollFd int +type epollListener struct { + mu sync.Mutex // Guards the following. + fd int interruptablePins map[int]*interrupt - mu sync.Mutex } -var ePollListenerInstance *ePollListener +var epollListenerInstance *epollListener -func getEPollListenerInstance() *ePollListener { - if ePollListenerInstance == nil { - ePollListenerInstance = initEPollListener() +func getEpollListenerInstance() *epollListener { + if epollListenerInstance == nil { + epollListenerInstance = initEpollListener() } - return ePollListenerInstance + return epollListenerInstance } -func initEPollListener() *ePollListener { - epollFd, err := syscall.EpollCreate1(0) +func initEpollListener() *epollListener { + fd, err := syscall.EpollCreate1(0) if err != nil { panic(fmt.Sprintf("Unable to create epoll: %v", err)) } - listener := &ePollListener{epollFd: epollFd, interruptablePins: make(map[int]*interrupt)} + listener := &epollListener{fd: fd, interruptablePins: make(map[int]*interrupt)} go func() { var epollEvents [MaxGPIOInterrupt]syscall.EpollEvent for { - n, err := syscall.EpollWait(listener.epollFd, epollEvents[:], -1) + n, err := syscall.EpollWait(listener.fd, epollEvents[:], -1) if err != nil { panic(fmt.Sprintf("EpollWait error: %v", err)) } @@ -72,7 +72,7 @@ func initEPollListener() *ePollListener { } func registerInterrupt(pin *digitalPin, handler func(embd.DigitalPin)) error { - l := getEPollListenerInstance() + l := getEpollListenerInstance() pinFd := int(pin.val.Fd()) @@ -92,7 +92,7 @@ func registerInterrupt(pin *digitalPin, handler func(embd.DigitalPin)) error { event.Fd = int32(pinFd) - if err := syscall.EpollCtl(l.epollFd, syscall.EPOLL_CTL_ADD, pinFd, &event); err != nil { + if err := syscall.EpollCtl(l.fd, syscall.EPOLL_CTL_ADD, pinFd, &event); err != nil { return err } @@ -102,7 +102,7 @@ func registerInterrupt(pin *digitalPin, handler func(embd.DigitalPin)) error { } func unregisterInterrupt(pin *digitalPin) error { - l := getEPollListenerInstance() + l := getEpollListenerInstance() pinFd := int(pin.val.Fd()) @@ -113,7 +113,7 @@ func unregisterInterrupt(pin *digitalPin) error { return nil } - if err := syscall.EpollCtl(l.epollFd, syscall.EPOLL_CTL_DEL, pinFd, nil); err != nil { + if err := syscall.EpollCtl(l.fd, syscall.EPOLL_CTL_DEL, pinFd, nil); err != nil { return err } diff --git a/host/generic/spibus.go b/host/generic/spibus.go index e00150c..f682daa 100644 --- a/host/generic/spibus.go +++ b/host/generic/spibus.go @@ -233,6 +233,13 @@ func (b *spiBus) ReceiveByte() (byte, error) { return byte(d[0]), nil } +func (b *spiBus) Write(data []byte) (n int, err error) { + if err := b.init(); err != nil { + return 0, err + } + return b.file.Write(data) +} + func (b *spiBus) Close() error { b.mu.Lock() defer b.mu.Unlock() diff --git a/host/rpi/rpi.go b/host/rpi/rpi.go index 372835b..0a82a4b 100644 --- a/host/rpi/rpi.go +++ b/host/rpi/rpi.go @@ -1,5 +1,5 @@ /* - Package rpi provides Raspberry Pi support. + Package rpi provides Raspberry Pi (including A+/B+) support. The following features are supported on Linux kernel 3.8+ GPIO (digital (rw)) @@ -55,13 +55,31 @@ var rev2Pins = embd.PinMap{ &embd.PinDesc{ID: "P1_26", Aliases: []string{"7", "GPIO_7", "CE1", "SPI0_CE1_N"}, Caps: embd.CapDigital | embd.CapSPI, DigitalLogical: 7}, } +// This is the same as the Rev 2 for the first 26 pins. +var rev3Pins = append(append(embd.PinMap(nil), rev2Pins...), embd.PinMap{ + &embd.PinDesc{ID: "P1_29", Aliases: []string{"5", "GPIO_5"}, Caps: embd.CapDigital, DigitalLogical: 5}, + &embd.PinDesc{ID: "P1_31", Aliases: []string{"6", "GPIO_6"}, Caps: embd.CapDigital, DigitalLogical: 6}, + &embd.PinDesc{ID: "P1_32", Aliases: []string{"12", "GPIO_12"}, Caps: embd.CapDigital, DigitalLogical: 12}, + &embd.PinDesc{ID: "P1_33", Aliases: []string{"13", "GPIO_13"}, Caps: embd.CapDigital, DigitalLogical: 13}, + &embd.PinDesc{ID: "P1_35", Aliases: []string{"19", "GPIO_19"}, Caps: embd.CapDigital, DigitalLogical: 19}, + &embd.PinDesc{ID: "P1_36", Aliases: []string{"16", "GPIO_16"}, Caps: embd.CapDigital, DigitalLogical: 16}, + &embd.PinDesc{ID: "P1_37", Aliases: []string{"26", "GPIO_26"}, Caps: embd.CapDigital, DigitalLogical: 26}, + &embd.PinDesc{ID: "P1_38", Aliases: []string{"20", "GPIO_20"}, Caps: embd.CapDigital, DigitalLogical: 20}, + &embd.PinDesc{ID: "P1_40", Aliases: []string{"21", "GPIO_21"}, Caps: embd.CapDigital, DigitalLogical: 21}, +}...) + var ledMap = embd.LEDMap{ "led0": []string{"0", "led0", "LED0"}, } func init() { embd.Register(embd.HostRPi, func(rev int) *embd.Descriptor { - var pins = rev2Pins + // Refer to http://elinux.org/RPi_HardwareHistory#Board_Revision_History + // for details. + pins := rev3Pins + if rev < 16 { + pins = rev2Pins + } if rev < 4 { pins = rev1Pins } 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/28bjy-48.go b/samples/28bjy-48.go new file mode 100644 index 0000000..456b7db --- /dev/null +++ b/samples/28bjy-48.go @@ -0,0 +1,109 @@ +// +build ignore + +package main + +// Control a stepper motor (28BJY-48) +// +// Datasheet: +// http://www.raspberrypi-spy.co.uk/wp-content/uploads/2012/07/Stepper-Motor-28BJY-48-Datasheet.pdf +// +// this is a port of Matt Hawkins' example impl from +// http://www.raspberrypi-spy.co.uk/2012/07/stepper-motor-control-in-python/ +// (link privides additional instructions for wiring your pi) + +import ( + "flag" + "fmt" + "os" + "os/signal" + "time" + + "github.com/kidoman/embd" + _ "github.com/kidoman/embd/host/rpi" +) + +func main() { + stepDelay := flag.Int("step-delay", 10, "milliseconds between steps") + flag.Parse() + + if err := embd.InitGPIO(); err != nil { + panic(err) + } + defer embd.CloseGPIO() + + // Physical pins 11,15,16,18 on rasp pi + // GPIO17,GPIO22,GPIO23,GPIO24 + stepPinNums := []int{17, 22, 23, 24} + + stepPins := make([]embd.DigitalPin, 4) + + for i, pinNum := range stepPinNums { + pin, err := embd.NewDigitalPin(pinNum) + if err != nil { + panic(err) + } + defer pin.Close() + if err := pin.SetDirection(embd.Out); err != nil { + panic(err) + } + if err := pin.Write(embd.Low); err != nil { + panic(err) + } + defer pin.SetDirection(embd.In) + + stepPins[i] = pin + } + + // Define sequence described in manufacturer's datasheet + seq := [][]int{ + []int{1, 0, 0, 0}, + []int{1, 1, 0, 0}, + []int{0, 1, 0, 0}, + []int{0, 1, 1, 0}, + []int{0, 0, 1, 0}, + []int{0, 0, 1, 1}, + []int{0, 0, 0, 1}, + []int{1, 0, 0, 1}, + } + stepCount := len(seq) - 1 + stepDir := 2 // Set to 1 or 2 for clockwise, -1 or -2 for counter-clockwise + + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt, os.Kill) + defer signal.Stop(quit) + + // Start main loop + ticker := time.NewTicker(time.Duration(*stepDelay) * time.Millisecond) + defer ticker.Stop() + + var stepCounter int + for { + select { + case <-ticker.C: + // set pins to appropriate values for given position in the sequence + for i, pin := range stepPins { + if seq[stepCounter][i] != 0 { + fmt.Printf("Enable pin %d, step %d\n", i, stepCounter) + if err := pin.Write(embd.High); err != nil { + panic(err) + } + } else { + if err := pin.Write(embd.Low); err != nil { + panic(err) + } + } + } + stepCounter += stepDir + + // If we reach the end of the sequence start again + if stepCounter >= stepCount { + stepCounter = 0 + } else if stepCounter < 0 { + stepCounter = stepCount + } + + case <-quit: + return + } + } +} 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() +} diff --git a/spi.go b/spi.go index e6aecb0..e7b9671 100644 --- a/spi.go +++ b/spi.go @@ -2,6 +2,10 @@ package embd +import ( + "io" +) + const ( spiCpha = 0x01 spiCpol = 0x02 @@ -21,6 +25,8 @@ const ( // SPIBus interface allows interaction with the SPI bus. type SPIBus interface { + io.Writer + // TransferAndRecieveData transmits data in a buffer(slice) and receives into it. TransferAndRecieveData(dataBuffer []uint8) error