From 45922f6524d7ba5c905d8741ee34f3ca18934688 Mon Sep 17 00:00:00 2001 From: Daniel Esteban Date: Sat, 3 Aug 2019 11:13:15 +0200 Subject: [PATCH] Driver ST7735 for TFT color displays (#72) * Driver ST7735 for TFT color displays --- Makefile | 1 + README.md | 1 + examples/st7735/main.go | 33 +++ st7735/README.md | 9 + st7735/registers.go | 57 ++++++ st7735/st7735.go | 440 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 541 insertions(+) create mode 100644 examples/st7735/main.go create mode 100644 st7735/README.md create mode 100644 st7735/registers.go create mode 100644 st7735/st7735.go diff --git a/Makefile b/Makefile index cce8b83..f68aab2 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ smoke-test: tinygo build -size short -o ./build/test.elf -target=microbit ./examples/sht3x/main.go tinygo build -size short -o ./build/test.elf -target=microbit ./examples/ssd1306/i2c_128x32/main.go tinygo build -size short -o ./build/test.elf -target=microbit ./examples/ssd1306/spi_128x64/main.go + tinygo build -size short -o ./build/test.elf -target=microbit ./examples/st7735/main.go tinygo build -size short -o ./build/test.elf -target=circuitplay-express ./examples/thermistor/main.go tinygo build -size short -o ./build/test.elf -target=itsybitsy-m0 ./examples/vl53l1x/main.go tinygo build -size short -o ./build/test.elf -target=microbit ./examples/waveshare-epd/epd2in13/main.go diff --git a/README.md b/README.md index c259edc..51e1ae7 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ func main() { | [PCD8544 display](http://eia.udg.edu/~forest/PCD8544_1.pdf) | SPI | | [SHT3x Digital Humidity Sensor](https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/0_Datasheets/Humidity/Sensirion_Humidity_Sensors_SHT3x_Datasheet_digital.pdf) | I2C | | [SSD1306 OLED display](https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf) | I2C / SPI | +| [ST7735 TFT color display](https://www.crystalfontz.com/controllers/Sitronix/ST7735R/319/) | SPI | | [Thermistor](https://www.farnell.com/datasheets/33552.pdf) | ADC | | [VL53L1X time-of-flight distance sensor](https://www.st.com/resource/en/datasheet/vl53l1x.pdf) | I2C | | [Waveshare 2.13" e-paper display](https://www.waveshare.com/w/upload/e/e6/2.13inch_e-Paper_Datasheet.pdf) | SPI | diff --git a/examples/st7735/main.go b/examples/st7735/main.go new file mode 100644 index 0000000..141eea5 --- /dev/null +++ b/examples/st7735/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "machine" + + "image/color" + + "tinygo.org/x/drivers/st7735" +) + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 8000000, + }) + display := st7735.New(machine.SPI0, machine.P6, machine.P7, machine.P8, machine.P9) + display.Configure(st7735.Config{}) + + width, height := display.Size() + + white := color.RGBA{255, 255, 255, 255} + red := color.RGBA{255, 0, 0, 255} + blue := color.RGBA{0, 0, 255, 255} + green := color.RGBA{0, 255, 0, 255} + black := color.RGBA{0, 0, 0, 255} + + display.FillScreen(black) + + display.FillRectangle(0, 0, width/2, height/2, white) + display.FillRectangle(width/2, 0, width/2, height/2, red) + display.FillRectangle(0, height/2, width/2, height/2, green) + display.FillRectangle(width/2, height/2, width/2, height/2, blue) + display.FillRectangle(width/4, height/4, width/2, height/2, black) +} diff --git a/st7735/README.md b/st7735/README.md new file mode 100644 index 0000000..4305e98 --- /dev/null +++ b/st7735/README.md @@ -0,0 +1,9 @@ +# ST7735 driver + +There are multiple devices using the ST7735 chip, and there are multiple versions ST7735B, ST7735R & ST7735S. Two apparently identical displays might have different configurations. The most common issues are: + +* Colors are inverted (black is white and viceversa), invert the colors with display.InvertColors(true) +* Colors are not right (red is blue and viceversa, but green is ok), some displays uses BRG instead of RGB for defining colors, change the mode with display.IsBGR(true) +* There is noise/snow/confetti in the screen, probably rows and columns offsets are wrong, configure them with st7735.Config{RowOffset:XX, ColumnOffset:YY} + +If nothing of the above works, your device may need a different boot-up process. \ No newline at end of file diff --git a/st7735/registers.go b/st7735/registers.go new file mode 100644 index 0000000..0fb01c7 --- /dev/null +++ b/st7735/registers.go @@ -0,0 +1,57 @@ +package st7735 + +// Registers +const ( + NOP = 0x00 + SWRESET = 0x01 + RDDID = 0x04 + RDDST = 0x09 + SLPIN = 0x10 + SLPOUT = 0x11 + PTLON = 0x12 + NORON = 0x13 + INVOFF = 0x20 + INVON = 0x21 + DISPOFF = 0x28 + DISPON = 0x29 + CASET = 0x2A + RASET = 0x2B + RAMWR = 0x2C + RAMRD = 0x2E + PTLAR = 0x30 + COLMOD = 0x3A + MADCTL = 0x36 + MADCTL_MY = 0x80 + MADCTL_MX = 0x40 + MADCTL_MV = 0x20 + MADCTL_ML = 0x10 + MADCTL_RGB = 0x00 + MADCTL_BGR = 0x08 + MADCTL_MH = 0x04 + RDID1 = 0xDA + RDID2 = 0xDB + RDID3 = 0xDC + RDID4 = 0xDD + FRMCTR1 = 0xB1 + FRMCTR2 = 0xB2 + FRMCTR3 = 0xB3 + INVCTR = 0xB4 + DISSET5 = 0xB6 + PWCTR1 = 0xC0 + PWCTR2 = 0xC1 + PWCTR3 = 0xC2 + PWCTR4 = 0xC3 + PWCTR5 = 0xC4 + VMCTR1 = 0xC5 + PWCTR6 = 0xFC + GMCTRP1 = 0xE0 + GMCTRN1 = 0xE1 + + GREENTAB Model = 0 + MINI80x160 Model = 1 + + NO_ROTATION Rotation = 0 + ROTATION_90 Rotation = 1 // 90 degrees clock-wise rotation + ROTATION_180 Rotation = 2 + ROTATION_270 Rotation = 3 +) diff --git a/st7735/st7735.go b/st7735/st7735.go new file mode 100644 index 0000000..37e3c3d --- /dev/null +++ b/st7735/st7735.go @@ -0,0 +1,440 @@ +// Package st7735 implements a driver for the ST7735 TFT displays, it comes in various screen sizes. +// +// Datasheet: https://www.crystalfontz.com/controllers/Sitronix/ST7735R/319/ +// +package st7735 // import "tinygo.org/x/drivers/st7735" + +import ( + "image/color" + "machine" + "time" + + "errors" +) + +type Model uint8 +type Rotation uint8 + +// Device wraps an SPI connection. +type Device struct { + bus machine.SPI + dcPin machine.Pin + resetPin machine.Pin + csPin machine.Pin + blPin machine.Pin + width int16 + height int16 + columnOffset int16 + rowOffset int16 + rotation Rotation + batchLength int16 + model Model + isBGR bool + batchData []uint8 +} + +// Config is the configuration for the display +type Config struct { + Width int16 + Height int16 + Rotation Rotation + Model Model + RowOffset int16 + ColumnOffset int16 +} + +// New creates a new ST7735 connection. The SPI wire must already be configured. +func New(bus machine.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { + dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + return Device{ + bus: bus, + dcPin: dcPin, + resetPin: resetPin, + csPin: csPin, + blPin: blPin, + } +} + +// Configure initializes the display with default configuration +func (d *Device) Configure(cfg Config) { + d.model = cfg.Model + if cfg.Width != 0 { + d.width = cfg.Width + } else { + if d.model == MINI80x160 { + d.width = 80 + } else { + d.width = 128 + } + } + if cfg.Height != 0 { + d.height = cfg.Height + } else { + d.height = 160 + } + d.rotation = cfg.Rotation + + if cfg.RowOffset != 0 { + d.rowOffset = cfg.RowOffset + } else { + d.rowOffset = 1 + } + if cfg.ColumnOffset != 0 { + d.columnOffset = cfg.ColumnOffset + } else { + if d.model == MINI80x160 { + d.columnOffset = 26 + } else { + d.columnOffset = 2 + } + } + + d.batchLength = d.width + if d.height > d.width { + d.batchLength = d.height + } + d.batchLength += d.batchLength & 1 + d.batchData = make([]uint8, d.batchLength*2) + + // reset the device + d.resetPin.High() + time.Sleep(5 * time.Millisecond) + d.resetPin.Low() + time.Sleep(20 * time.Millisecond) + d.resetPin.High() + time.Sleep(150 * time.Millisecond) + + // Common initialization + d.Command(SWRESET) + time.Sleep(150 * time.Millisecond) + d.Command(SLPOUT) + time.Sleep(500 * time.Millisecond) + d.Command(FRMCTR1) + d.Data(0x01) + d.Data(0x2C) + d.Data(0x2D) + d.Command(FRMCTR2) + d.Data(0x01) + d.Data(0x2C) + d.Data(0x2D) + d.Command(FRMCTR3) + d.Data(0x01) + d.Data(0x2C) + d.Data(0x2D) + d.Data(0x01) + d.Data(0x2C) + d.Data(0x2D) + d.Command(INVCTR) + d.Data(0x07) + d.Command(PWCTR1) + d.Data(0xA2) + d.Data(0x02) + d.Data(0x84) + d.Command(PWCTR2) + d.Data(0xC5) + d.Command(PWCTR3) + d.Data(0x0A) + d.Data(0x00) + d.Command(PWCTR4) + d.Data(0x8A) + d.Data(0x2A) + d.Command(PWCTR5) + d.Data(0x8A) + d.Data(0xEE) + d.Command(VMCTR1) + d.Data(0x0E) + d.Command(COLMOD) + d.Data(0x05) + + if d.model == GREENTAB { + d.InvertColors(false) + d.Command(CASET) + d.Data(0x00) + d.Data(0x02) + d.Data(0x00) + d.Data(0x7F + 0x02) + d.Command(RASET) + d.Data(0x00) + d.Data(0x01) + d.Data(0x00) + d.Data(0x9F + 0x01) + } else if d.model == MINI80x160 { + d.isBGR = true + d.InvertColors(true) + d.Command(CASET) + d.Data(0x00) + d.Data(0x00) + d.Data(0x00) + d.Data(0x7F) + d.Command(RASET) + d.Data(0x00) + d.Data(0x00) + d.Data(0x00) + d.Data(0x9F) + } + + // common color adjustment + d.Command(GMCTRP1) + d.Data(0x02) + d.Data(0x1C) + d.Data(0x07) + d.Data(0x12) + d.Data(0x37) + d.Data(0x32) + d.Data(0x29) + d.Data(0x2D) + d.Data(0x29) + d.Data(0x25) + d.Data(0x2B) + d.Data(0x39) + d.Data(0x00) + d.Data(0x01) + d.Data(0x03) + d.Data(0x10) + d.Command(GMCTRN1) + d.Data(0x03) + d.Data(0x1D) + d.Data(0x07) + d.Data(0x06) + d.Data(0x2E) + d.Data(0x2C) + d.Data(0x29) + d.Data(0x2D) + d.Data(0x2E) + d.Data(0x2E) + d.Data(0x37) + d.Data(0x3F) + d.Data(0x00) + d.Data(0x00) + d.Data(0x02) + d.Data(0x10) + + d.Command(NORON) + time.Sleep(10 * time.Millisecond) + d.Command(DISPON) + time.Sleep(500 * time.Millisecond) + + if cfg.Model == MINI80x160 { + d.Command(MADCTL) + d.Data(0xC0) + } + + d.SetRotation(d.rotation) + + d.blPin.High() +} + +// Display does nothing, there's no buffer as it might be too big for some boards +func (d *Device) Display() error { + return nil +} + +// SetPixel sets a pixel in the screen +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + w, h := d.Size() + if x < 0 || y < 0 || x >= w || y >= h { + return + } + d.FillRectangle(x, y, 1, 1, c) +} + +// setWindow prepares the screen to be modified at a given rectangle +func (d *Device) setWindow(x, y, w, h int16) { + if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 { + x += d.columnOffset + y += d.rowOffset + } else { + x += d.rowOffset + y += d.columnOffset + } + d.Tx([]uint8{CASET}, true) + d.Tx([]uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)}, false) + d.Tx([]uint8{RASET}, true) + d.Tx([]uint8{uint8(y >> 8), uint8(y), uint8((y + h - 1) >> 8), uint8(y + h - 1)}, false) + d.Command(RAMWR) +} + +// FillRectangle fills a rectangle at a given coordinates with a color +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 { + return errors.New("rectangle coordinates outside display area") + } + d.setWindow(x, y, width, height) + c565 := RGBATo565(c) + c1 := uint8(c565 >> 8) + c2 := uint8(c565) + + for i = 0; i < d.batchLength; i++ { + d.batchData[i*2] = c1 + d.batchData[i*2+1] = c2 + } + i = width * height + for i > 0 { + if i >= d.batchLength { + d.Tx(d.batchData, false) + } else { + d.Tx(d.batchData[:i*2], false) + } + i -= d.batchLength + } + return nil +} + +// FillRectangle fills a rectangle at a given coordinates with a buffer +func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { + k, l := d.Size() + if x < 0 || y < 0 || width <= 0 || height <= 0 || + x >= k || (x+width) > k || y >= l || (y+height) > l { + return errors.New("rectangle coordinates outside display area") + } + k = width * height + l = int16(len(buffer)) + if k != l { + return errors.New("buffer length does not match with rectangle size") + } + + d.setWindow(x, y, width, height) + + offset := int16(0) + for k > 0 { + for i := int16(0); i < d.batchLength; i++ { + if offset+i < l { + c565 := RGBATo565(buffer[offset+i]) + c1 := uint8(c565 >> 8) + c2 := uint8(c565) + d.batchData[i*2] = c1 + d.batchData[i*2+1] = c2 + } + } + if k >= d.batchLength { + d.Tx(d.batchData, false) + } else { + d.Tx(d.batchData[:k*2], false) + } + k -= d.batchLength + offset += d.batchLength + } + return nil +} + +// DrawFastVLine draws a vertical line faster than using SetPixel +func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { + if y0 > y1 { + y0, y1 = y1, y0 + } + d.FillRectangle(x, y0, 1, y1-y0+1, c) +} + +// DrawFastHLine draws a horizontal line faster than using SetPixel +func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { + if x0 > x1 { + x0, x1 = x1, x0 + } + d.FillRectangle(x0, y, x1-x0+1, y, c) +} + +// FillScreen fills the screen with a given color +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) + } else { + d.FillRectangle(0, 0, d.height, d.width, c) + } +} + +// SetRotation changes the rotation of the device (clock-wise) +func (d *Device) SetRotation(rotation Rotation) { + madctl := uint8(0) + switch rotation % 4 { + case 0: + madctl = MADCTL_MX | MADCTL_MY + break + case 1: + madctl = MADCTL_MY | MADCTL_MV + break + case 2: + break + case 3: + madctl = MADCTL_MX | MADCTL_MV + break + } + if d.isBGR { + madctl |= MADCTL_BGR + } + d.Command(MADCTL) + d.Data(madctl) + +} + +// Command sends a command to the display +func (d *Device) Command(command uint8) { + d.Tx([]byte{command}, true) +} + +// Command sends a data to the display +func (d *Device) Data(data uint8) { + d.Tx([]byte{data}, false) +} + +// Tx sends data to the display +func (d *Device) Tx(data []byte, isCommand bool) { + if isCommand { + d.csPin.High() + d.dcPin.Low() + d.csPin.Low() + + d.bus.Tx(data, nil) + d.csPin.High() + } else { + d.csPin.High() + d.dcPin.High() + d.csPin.Low() + + d.bus.Tx(data, nil) + d.csPin.High() + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 { + return d.width, d.height + } + return d.height, d.width +} + +// EnableBacklight enables or disables the backlight +func (d *Device) EnableBacklight(enable bool) { + if enable { + d.blPin.High() + } else { + d.blPin.Low() + } +} + +// InverColors inverts the colors of the screen +func (d *Device) InvertColors(invert bool) { + if invert { + d.Command(INVON) + } else { + d.Command(INVOFF) + } +} + +// IsBGR changes the color mode (RGB/BGR) +func (d *Device) IsBGR(bgr bool) { + d.isBGR = bgr +} + +// RGBATo565 converts a color.RGBA to uint16 used in the display +func RGBATo565(c color.RGBA) uint16 { + r, g, b, _ := c.RGBA() + return uint16((r & 0xF800) + + ((g & 0xFC00) >> 5) + + ((b & 0xF800) >> 11)) +}