diff --git a/st7789/st7789.go b/st7789/st7789.go index 7d828cb..0ee23ba 100644 --- a/st7789/st7789.go +++ b/st7789/st7789.go @@ -46,6 +46,7 @@ type Device struct { batchLength int32 isBGR bool vSyncLines int16 + cmdBuf [1]byte buf [6]byte } @@ -124,24 +125,25 @@ func (d *Device) Configure(cfg Config) { time.Sleep(50 * time.Millisecond) // Common initialization - d.Command(SWRESET) // Soft reset + d.startWrite() + d.sendCommand(SWRESET, nil) // Soft reset + d.endWrite() time.Sleep(150 * time.Millisecond) // + d.startWrite() - d.Sleep(false) // Exit sleep mode + d.sendCommand(SLPOUT, nil) // Exit sleep mode // Memory initialization - d.Command(COLMOD) // Set color mode - d.Data(0x55) // 16-bit color - time.Sleep(10 * time.Millisecond) // + d.sendCommand(COLMOD, []byte{0x55}) // Set color mode to 16-bit color + time.Sleep(10 * time.Millisecond) - d.SetRotation(d.rotation) // Memory orientation + d.setRotation(d.rotation) // Memory orientation d.setWindow(0, 0, d.width, d.height) // Full draw window - d.FillScreen(color.RGBA{0, 0, 0, 255}) // Clear screen + d.fillScreen(color.RGBA{0, 0, 0, 255}) // Clear screen // Framerate - d.Command(FRCTRL2) // Frame rate for normal mode - d.Data(uint8(d.frameRate)) // Default is 60Hz + d.sendCommand(FRCTRL2, []byte{byte(d.frameRate)}) // Frame rate for normal mode (default 60Hz) // Frame vertical sync and "porch" // @@ -151,36 +153,66 @@ func (d *Device) Configure(cfg Config) { fp := uint8(d.vSyncLines / 2) // Split the desired pause half and half bp := uint8(d.vSyncLines - int16(fp)) // between front and back porch. - d.Command(PORCTRL) - d.Data(bp) // Back porch 5bit (0x7F max 0x08 default) - d.Data(fp) // Front porch 5bit (0x7F max 0x08 default) - d.Data(0x00) // Seprarate porch (TODO: what is this?) - d.Data(0x22) // Idle mode porch (4bit-back 4bit-front 0x22 default) - d.Data(0x22) // Partial mode porch (4bit-back 4bit-front 0x22 default) + d.sendCommand(PORCTRL, []byte{ + bp, // Back porch 5bit (0x7F max 0x08 default) + fp, // Front porch 5bit (0x7F max 0x08 default) + 0x00, // Seprarate porch (TODO: what is this?) + 0x22, // Idle mode porch (4bit-back 4bit-front 0x22 default) + 0x22, // Partial mode porch (4bit-back 4bit-front 0x22 default) + }) // Ready to display - d.Command(INVON) // Inversion ON + d.sendCommand(INVON, nil) // Inversion ON time.Sleep(10 * time.Millisecond) // // Set gamma tables, if configured. if len(cfg.PVGAMCTRL) == 14 { - d.Command(GMCTRP1) // PVGAMCTRL: Positive Voltage Gamma Control - d.Tx(cfg.PVGAMCTRL, false) + d.sendCommand(GMCTRP1, cfg.PVGAMCTRL) // PVGAMCTRL: Positive Voltage Gamma Control } if len(cfg.NVGAMCTRL) == 14 { - d.Command(GMCTRN1) // NVGAMCTRL: Negative Voltage Gamma Control - d.Tx(cfg.NVGAMCTRL, false) + d.sendCommand(GMCTRN1, cfg.NVGAMCTRL) // NVGAMCTRL: Negative Voltage Gamma Control } - d.Command(NORON) // Normal mode ON + d.sendCommand(NORON, nil) // Normal mode ON time.Sleep(10 * time.Millisecond) // - d.Command(DISPON) // Screen ON + d.sendCommand(DISPON, nil) // Screen ON time.Sleep(10 * time.Millisecond) // + d.endWrite() d.blPin.High() // Backlight ON } +// Send a command with data to the display. It does not change the chip select +// pin (it must be low when calling). The DC pin is left high after return, +// meaning that data can be sent right away. +func (d *Device) sendCommand(command uint8, data []byte) error { + d.cmdBuf[0] = command + d.dcPin.Low() + err := d.bus.Tx(d.cmdBuf[:1], nil) + d.dcPin.High() + if len(data) != 0 { + err = d.bus.Tx(data, nil) + } + return err +} + +// startWrite must be called at the beginning of all exported methods to set the +// chip select pin low. +func (d *Device) startWrite() { + if d.csPin != machine.NoPin { + d.csPin.Low() + } +} + +// endWrite must be called at the end of all exported methods to set the chip +// select pin high. +func (d *Device) endWrite() { + if d.csPin != machine.NoPin { + d.csPin.High() + } +} + // Sync waits for the display to hit the next VSYNC pause func (d *Device) Sync() { d.SyncToScanLine(0) @@ -228,9 +260,17 @@ func (d *Device) SyncToScanLine(scanline uint16) { // GetScanLine reads the current scanline value from the display func (d *Device) GetScanLine() uint16 { + d.startWrite() data := []uint8{0x00, 0x00} - d.Rx(GSCAN, data) - return uint16(data[0])<<8 + uint16(data[1]) + d.dcPin.Low() + d.bus.Transfer(GSCAN) + d.dcPin.High() + for i := range data { + data[i], _ = d.bus.Transfer(0xFF) + } + scanline := uint16(data[0])<<8 + uint16(data[1]) + d.endWrite() + return scanline } // GetHighestScanLine calculates the last scanline id in the frame before VSYNC pause @@ -264,17 +304,22 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { func (d *Device) setWindow(x, y, w, h int16) { x += d.columnOffset y += d.rowOffset - d.Command(CASET) copy(d.buf[:4], []uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)}) - d.Tx(d.buf[:4], false) - d.Command(RASET) + d.sendCommand(CASET, d.buf[:4]) copy(d.buf[:4], []uint8{uint8(y >> 8), uint8(y), uint8((y + h - 1) >> 8), uint8(y + h - 1)}) - d.Tx(d.buf[:4], false) - d.Command(RAMWR) + d.sendCommand(RASET, d.buf[:4]) + d.sendCommand(RAMWR, nil) } // FillRectangle fills a rectangle at a given coordinates with a color func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { + d.startWrite() + err := d.fillRectangle(x, y, width, height, c) + d.endWrite() + return err +} + +func (d *Device) fillRectangle(x, y, width, height int16, c color.RGBA) error { k, i := d.Size() if x < 0 || y < 0 || width <= 0 || height <= 0 || x >= k || (x+width) > k || y >= i || (y+height) > i { @@ -292,10 +337,12 @@ func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { } j := int32(width) * int32(height) for j > 0 { + // The DC pin is already set to data in the setWindow call, so we can + // just write bytes on the SPI bus. if j >= d.batchLength { - d.Tx(data, false) + d.bus.Tx(data, nil) } else { - d.Tx(data[:j*2], false) + d.bus.Tx(data[:j*2], nil) } j -= d.batchLength } @@ -309,8 +356,10 @@ func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { x >= k || (x+w) > k || y >= i || (y+h) > i { return errOutOfBounds } + d.startWrite() d.setWindow(x, y, w, h) - d.Tx(data, false) + d.bus.Tx(data, nil) + d.endWrite() return nil } @@ -324,6 +373,7 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col if int32(width)*int32(height) != int32(len(buffer)) { return errors.New("buffer length does not match with rectangle size") } + d.startWrite() d.setWindow(x, y, width, height) k := int32(width) * int32(height) @@ -339,14 +389,17 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col data[i*2+1] = c2 } } + // The DC pin is already set to data in the setWindow call, so we don't + // have to set it here. if k >= d.batchLength { - d.Tx(data, false) + d.bus.Tx(data, nil) } else { - d.Tx(data[:k*2], false) + d.bus.Tx(data[:k*2], nil) } k -= d.batchLength offset += d.batchLength } + d.endWrite() return nil } @@ -368,10 +421,16 @@ func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { // FillScreen fills the screen with a given color func (d *Device) FillScreen(c color.RGBA) { + d.startWrite() + d.fillScreen(c) + d.endWrite() +} + +func (d *Device) fillScreen(c color.RGBA) { if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 { - d.FillRectangle(0, 0, d.width, d.height, c) + d.fillRectangle(0, 0, d.width, d.height, c) } else { - d.FillRectangle(0, 0, d.height, d.width, c) + d.fillRectangle(0, 0, d.height, d.width, c) } } @@ -382,6 +441,13 @@ func (d *Device) Rotation() drivers.Rotation { // SetRotation changes the rotation of the device (clock-wise) func (d *Device) SetRotation(rotation Rotation) error { + d.startWrite() + err := d.setRotation(rotation) + d.endWrite() + return err +} + +func (d *Device) setRotation(rotation Rotation) error { madctl := uint8(0) switch rotation % 4 { case drivers.Rotation0: @@ -403,45 +469,7 @@ func (d *Device) SetRotation(rotation Rotation) error { if d.isBGR { madctl |= MADCTL_BGR } - d.Command(MADCTL) - d.Data(madctl) - return nil -} - -// Command sends a command to the display. -func (d *Device) Command(command uint8) { - d.buf[0] = command - d.Tx(d.buf[:1], true) -} - -// Data sends data to the display. -func (d *Device) Data(data uint8) { - d.buf[0] = data - d.Tx(d.buf[:1], false) -} - -// Tx sends data to the display -func (d *Device) Tx(data []byte, isCommand bool) { - if isCommand { - d.dcPin.Low() - } else { - d.dcPin.High() - } - d.csPin.Low() - d.bus.Tx(data, nil) - d.csPin.High() -} - -// Rx reads data from the display -func (d *Device) Rx(command uint8, data []byte) { - d.dcPin.Low() - d.csPin.Low() - d.bus.Transfer(command) - d.dcPin.High() - for i := range data { - data[i], _ = d.bus.Transfer(0xFF) - } - d.csPin.High() + return d.sendCommand(MADCTL, []byte{madctl}) } // Size returns the current size of the display. @@ -466,11 +494,15 @@ func (d *Device) EnableBacklight(enable bool) { // will be kept. func (d *Device) Sleep(sleepEnabled bool) error { if sleepEnabled { - d.Command(SLPIN) + d.startWrite() + d.sendCommand(SLPIN, nil) + d.endWrite() time.Sleep(5 * time.Millisecond) // 5ms required by the datasheet } else { // Turn the LCD panel back on. - d.Command(SLPOUT) + d.startWrite() + d.sendCommand(SLPOUT, nil) + d.endWrite() // Note: the st7789 documentation says that it is needed to wait at // least 120ms before going to sleep again. Sleeping here would not be // practical (delays turning on the screen too much), so just hope the @@ -483,11 +515,13 @@ func (d *Device) Sleep(sleepEnabled bool) error { // InvertColors inverts the colors of the screen func (d *Device) InvertColors(invert bool) { + d.startWrite() if invert { - d.Command(INVON) + d.sendCommand(INVON, nil) } else { - d.Command(INVOFF) + d.sendCommand(INVOFF, nil) } + d.endWrite() } // IsBGR changes the color mode (RGB/BGR) @@ -497,25 +531,29 @@ func (d *Device) IsBGR(bgr bool) { // SetScrollArea sets an area to scroll with fixed top and bottom parts of the display. func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) { - d.Command(VSCRDEF) copy(d.buf[:6], []uint8{ uint8(topFixedArea >> 8), uint8(topFixedArea), uint8(d.height - topFixedArea - bottomFixedArea>>8), uint8(d.height - topFixedArea - bottomFixedArea), uint8(bottomFixedArea >> 8), uint8(bottomFixedArea)}) - d.Tx(d.buf[:6], false) + d.startWrite() + d.sendCommand(VSCRDEF, d.buf[:6]) + d.endWrite() } // SetScroll sets the vertical scroll address of the display. func (d *Device) SetScroll(line int16) { - d.Command(VSCRSADD) d.buf[0] = uint8(line >> 8) d.buf[1] = uint8(line) - d.Tx(d.buf[:2], false) + d.startWrite() + d.sendCommand(VSCRSADD, d.buf[:2]) + d.endWrite() } // StopScroll returns the display to its normal state. func (d *Device) StopScroll() { - d.Command(NORON) + d.startWrite() + d.sendCommand(NORON, nil) + d.endWrite() } // RGBATo565 converts a color.RGBA to uint16 used in the display