// Package hd44780 provides a driver for the HD44780 LCD controller. // // Datasheet: https://www.sparkfun.com/datasheets/LCD/HD44780.pdf package hd44780 // import "tinygo.org/x/drivers/hd44780" import ( "errors" "io" "machine" "time" ) const ( // These are the default execution times for the Clear and // Home commands and everything else. // // These are used if RW is passed as machine.NoPin and ignored // otherwise. // // They are set conservatively here and can be tweaked in the // Config structure. DefaultClearHomeTime = 80 * time.Millisecond DefaultInstrExecTime = 80 * time.Microsecond ) type Buser interface { io.ReadWriter SetCommandMode(set bool) WriteOnly() bool } type Device struct { bus Buser width uint8 height uint8 buffer []uint8 bufferLength uint8 rowOffset []uint8 // Row offsets in DDRAM datalength uint8 cursor cursor busyStatus []byte clearHomeTime time.Duration // time clear/home instructions might take instrExecTime time.Duration // time all other instructions might take } type cursor struct { x, y uint8 } type Config struct { Width int16 Height int16 CursorBlink bool CursorOnOff bool Font uint8 ClearHomeTime time.Duration // time clear/home instructions might take - use 0 for the default InstrExecTime time.Duration // time all other instructions might take - use 0 for the default } // NewGPIO4Bit returns 4bit data length HD44780 driver. Datapins are LCD DB pins starting from DB4 to DB7 // // If your device has RW set permanently to ground then pass in rw as machine.NoPin func NewGPIO4Bit(dataPins []machine.Pin, e, rs, rw machine.Pin) (Device, error) { const fourBitMode = 4 if len(dataPins) != fourBitMode { return Device{}, errors.New("4 pins are required in data slice (D4-D7) when HD44780 is used in 4 bit mode") } return newGPIO(dataPins, e, rs, rw, DATA_LENGTH_4BIT), nil } // NewGPIO8Bit returns 8bit data length HD44780 driver. Datapins are LCD DB pins starting from DB0 to DB7 // // If your device has RW set permanently to ground then pass in rw as machine.NoPin func NewGPIO8Bit(dataPins []machine.Pin, e, rs, rw machine.Pin) (Device, error) { const eightBitMode = 8 if len(dataPins) != eightBitMode { return Device{}, errors.New("8 pins are required in data slice (D0-D7) when HD44780 is used in 8 bit mode") } return newGPIO(dataPins, e, rs, rw, DATA_LENGTH_8BIT), nil } // Configure initializes device func (d *Device) Configure(cfg Config) error { d.busyStatus = make([]byte, 1) d.width = uint8(cfg.Width) d.height = uint8(cfg.Height) if d.width == 0 || d.height == 0 { return errors.New("width and height must be set") } d.clearHomeTime = cfg.ClearHomeTime d.instrExecTime = cfg.InstrExecTime memoryMap := uint8(ONE_LINE) if d.height > 1 { memoryMap = TWO_LINE } d.setRowOffsets() d.ClearBuffer() cursor := CURSOR_OFF if cfg.CursorOnOff { cursor = CURSOR_ON } cursorBlink := CURSOR_BLINK_OFF if cfg.CursorBlink { cursorBlink = CURSOR_BLINK_ON } if !(cfg.Font == FONT_5X8 || cfg.Font == FONT_5X10) { cfg.Font = FONT_5X8 } //Wait 15ms after Vcc rises to 4.5V time.Sleep(15 * time.Millisecond) d.bus.SetCommandMode(true) d.bus.Write([]byte{DATA_LENGTH_8BIT}) time.Sleep(5 * time.Millisecond) for i := 0; i < 2; i++ { d.bus.Write([]byte{DATA_LENGTH_8BIT}) time.Sleep(150 * time.Microsecond) } if d.datalength == DATA_LENGTH_4BIT { d.bus.Write([]byte{DATA_LENGTH_4BIT}) } // Busy flag is now accessible d.SendCommand(memoryMap | cfg.Font | d.datalength) d.SendCommand(DISPLAY_OFF) d.SendCommand(DISPLAY_CLEAR) d.SendCommand(ENTRY_MODE | CURSOR_INCREASE | DISPLAY_NO_SHIFT) d.SendCommand(DISPLAY_ON | uint8(cursor) | uint8(cursorBlink)) return nil } // Write writes data to internal buffer func (d *Device) Write(data []byte) (n int, err error) { size := len(data) if size > len(d.buffer) { size = len(d.buffer) } d.bufferLength = uint8(size) for i := uint8(0); i < d.bufferLength; i++ { d.buffer[i] = data[i] } return size, nil } // Display sends the whole buffer to the screen at cursor position func (d *Device) Display() error { // Buffer may contain less characters than its capacity. // We must be sure that we will not send unassigned characters // That would result in sending zero values of buffer slice and // potentialy displaying some character. var totalDisplayedChars uint8 var bufferPos uint8 for ; d.cursor.y < d.height; d.cursor.y++ { d.SetCursor(d.cursor.x, d.cursor.y) for ; d.cursor.x < d.width && totalDisplayedChars < d.bufferLength; d.cursor.x++ { d.sendData(d.buffer[bufferPos]) bufferPos++ totalDisplayedChars++ } if d.cursor.x >= d.width { d.cursor.x = 0 } if totalDisplayedChars >= d.bufferLength { break } } return nil } // SetCursor moves cursor to position x,y, where (0,0) is top left corner and (width-1, height-1) bottom right func (d *Device) SetCursor(x, y uint8) { d.cursor.x = x d.cursor.y = y d.SendCommand(DDRAM_SET | (x + (d.rowOffset[y] * y))) } // SetRowOffsets sets initial memory addresses coresponding to the display rows // Each row on display has different starting address in DDRAM. Rows are not mapped in order. // These addresses tend to differ between the types of the displays (16x2, 16x4, 20x4 etc ..), // https://web.archive.org/web/20111122175541/http://web.alfredstate.edu/weimandn/lcd/lcd_addressing/lcd_addressing_index.html func (d *Device) setRowOffsets() { switch d.height { case 1: d.rowOffset = []uint8{} case 2: d.rowOffset = []uint8{0x0, 0x40, 0x0, 0x40} case 4: d.rowOffset = []uint8{0x0, 0x40, d.width, 0x40 + d.width} default: d.rowOffset = []uint8{0x0, 0x40, d.width, 0x40 + d.width} } } // SendCommand sends commands to driver func (d *Device) SendCommand(command byte) { d.bus.SetCommandMode(true) d.bus.Write([]byte{command}) for d.isBusy(command == DISPLAY_CLEAR || command == CURSOR_HOME) { } } // sendData sends byte data directly to display. func (d *Device) sendData(data byte) { d.bus.SetCommandMode(false) d.bus.Write([]byte{data}) for d.isBusy(false) { } } // CreateCharacter crates characters using data and stores it under cgram Addr in CGRAM func (d *Device) CreateCharacter(cgramAddr uint8, data []byte) { d.SendCommand(CGRAM_SET | cgramAddr) for _, dd := range data { d.sendData(dd) } } // isBusy returns true when hd447890 is isBusy // or after the timeout specified func (d *Device) isBusy(longDelay bool) bool { if d.bus.WriteOnly() { // Can't read busy flag if write only, so sleep a bit then return if longDelay { // Note that we sleep like this so the default // time.Sleep is time.Sleep(constant) as // time.Sleep(variable) doesn't seem to work on AVR yet if d.clearHomeTime != 0 { time.Sleep(d.clearHomeTime) } else { time.Sleep(DefaultClearHomeTime) } } else { if d.instrExecTime != 0 { time.Sleep(d.instrExecTime) } else { time.Sleep(DefaultInstrExecTime) } } return false } d.bus.SetCommandMode(true) d.bus.Read(d.busyStatus) return (d.busyStatus[0] & BUSY) > 0 } // Busy returns true when hd447890 is busy func (d *Device) Busy() bool { return d.isBusy(false) } // Size returns the current size of the display. func (d *Device) Size() (w, h int16) { return int16(d.width), int16(d.height) } // ClearDisplay clears displayed content and buffer func (d *Device) ClearDisplay() { d.SendCommand(DISPLAY_CLEAR) d.ClearBuffer() } // ClearBuffer clears internal buffer func (d *Device) ClearBuffer() { d.buffer = make([]uint8, d.width*d.height) }