diff --git a/examples/ili9325/itdb02/bus_atsamd21.go b/examples/ili9325/itdb02/bus_atsamd21.go new file mode 100644 index 000000000..d928412cf --- /dev/null +++ b/examples/ili9325/itdb02/bus_atsamd21.go @@ -0,0 +1,13 @@ +//go:build atsamd21 + +package main + +import "tinygo.org/x/drivers/ili9325" + +func newBus() ili9325.Bus { + bus, err := ili9325.NewPortBus(data, wr) + if err != nil { + panic(err) + } + return bus +} diff --git a/examples/ili9325/itdb02/bus_other.go b/examples/ili9325/itdb02/bus_other.go new file mode 100644 index 000000000..9ed947fdc --- /dev/null +++ b/examples/ili9325/itdb02/bus_other.go @@ -0,0 +1,9 @@ +//go:build !atsamd21 + +package main + +import "tinygo.org/x/drivers/ili9325" + +func newBus() ili9325.Bus { + return ili9325.NewPinBus(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], wr) +} diff --git a/examples/ili9325/itdb02/main.go b/examples/ili9325/itdb02/main.go new file mode 100644 index 000000000..17a2f85cd --- /dev/null +++ b/examples/ili9325/itdb02/main.go @@ -0,0 +1,63 @@ +// Draws a test pattern on the ITead ITDB02-2.8 shield in landscape, with the +// red square in the top left corner. +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/ili9325" +) + +var ( + data = [8]machine.Pin{machine.D0, machine.D1, machine.D2, machine.D3, machine.D4, machine.D5, machine.D6, machine.D7} + rs = machine.ADC5 + wr = machine.ADC4 + cs = machine.ADC3 + rst = machine.ADC2 +) + +var ( + black = color.RGBA{0, 0, 0, 255} + white = color.RGBA{255, 255, 255, 255} + red = color.RGBA{255, 0, 0, 255} + green = color.RGBA{0, 255, 0, 255} + blue = color.RGBA{0, 0, 255, 255} +) + +func main() { + out := machine.PinConfig{Mode: machine.PinOutput} + for _, p := range append(data[:], rs, wr, cs, rst) { + p.Configure(out) + } + + display := ili9325.New(newBus(), rs, cs, rst) + display.Configure(ili9325.Config{Rotation: drivers.Rotation90}) + + w, h := display.Size() + display.FillScreen(black) + display.FillRectangle(0, 0, 40, 40, red) + display.FillRectangle(w-40, 0, 40, 40, green) + display.FillRectangle(0, h-40, 40, 40, blue) + display.FillRectangle(w-40, h-40, 40, 40, white) + + // Only redraw the strips that change, so the square doesn't flicker. + x, y, dx := int16(40), h/2-20, int16(4) + display.FillRectangle(x, y, 40, 40, green) + for { + if x+dx < 40 || x+dx > w-80 { + dx = -dx + } + if dx > 0 { + display.FillRectangle(x, y, dx, 40, black) + display.FillRectangle(x+40, y, dx, 40, green) + } else { + display.FillRectangle(x+40+dx, y, -dx, 40, black) + display.FillRectangle(x+dx, y, -dx, 40, green) + } + x += dx + time.Sleep(10 * time.Millisecond) + } +} diff --git a/examples/ili9325/lorenz/bus_atsamd21.go b/examples/ili9325/lorenz/bus_atsamd21.go new file mode 100644 index 000000000..d928412cf --- /dev/null +++ b/examples/ili9325/lorenz/bus_atsamd21.go @@ -0,0 +1,13 @@ +//go:build atsamd21 + +package main + +import "tinygo.org/x/drivers/ili9325" + +func newBus() ili9325.Bus { + bus, err := ili9325.NewPortBus(data, wr) + if err != nil { + panic(err) + } + return bus +} diff --git a/examples/ili9325/lorenz/bus_other.go b/examples/ili9325/lorenz/bus_other.go new file mode 100644 index 000000000..9ed947fdc --- /dev/null +++ b/examples/ili9325/lorenz/bus_other.go @@ -0,0 +1,9 @@ +//go:build !atsamd21 + +package main + +import "tinygo.org/x/drivers/ili9325" + +func newBus() ili9325.Bus { + return ili9325.NewPinBus(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], wr) +} diff --git a/examples/ili9325/lorenz/main.go b/examples/ili9325/lorenz/main.go new file mode 100644 index 000000000..0880cab13 --- /dev/null +++ b/examples/ili9325/lorenz/main.go @@ -0,0 +1,106 @@ +// Draws the Lorenz attractor on the ITead ITDB02-2.8 shield. A 3000 point +// trail traces the attractor while the view slowly rotates around the z axis. +package main + +import ( + "image/color" + "machine" + "math" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/ili9325" +) + +var ( + data = [8]machine.Pin{machine.D0, machine.D1, machine.D2, machine.D3, machine.D4, machine.D5, machine.D6, machine.D7} + rs = machine.ADC5 + wr = machine.ADC4 + cs = machine.ADC3 + rst = machine.ADC2 +) + +const ( + trail = 3000 + sigma = 10 + rho = 28 + beta = 8.0 / 3 + dt = 0.006 + scale = 5.2 +) + +var ( + black = color.RGBA{0, 0, 0, 255} + palette [64]color.RGBA + px, py [trail]int16 +) + +func main() { + out := machine.PinConfig{Mode: machine.PinOutput} + for _, p := range append(data[:], rs, wr, cs, rst) { + p.Configure(out) + } + + display := ili9325.New(newBus(), rs, cs, rst) + display.Configure(ili9325.Config{Rotation: drivers.Rotation90}) + display.FillScreen(black) + + for i := range palette { + palette[i] = hue(float32(i) / float32(len(palette))) + } + for i := range px { + px[i] = -1 + } + + w, h := display.Size() + cx, cy := float32(w/2), float32(h-6) + x, y, z := float32(0.1), float32(0), float32(0) + theta := float64(0) + cos, sin := float32(1), float32(0) + for i := 0; ; i++ { + dx := sigma * (y - x) + dy := x*(rho-z) - y + dz := x*y - beta*z + x += dx * dt + y += dy * dt + z += dz * dt + + if i%64 == 0 { + theta += 0.004 + cos, sin = float32(math.Cos(theta)), float32(math.Sin(theta)) + } + sx := int16(cx + (x*cos-y*sin)*scale) + sy := int16(cy - z*4.4) + + j := i % trail + if px[j] >= 0 { + display.SetPixel(px[j], py[j], black) + } + zi := int(z * 1.3) + if zi < 0 { + zi = 0 + } + display.SetPixel(sx, sy, palette[zi%len(palette)]) + px[j], py[j] = sx, sy + } +} + +// hue returns a fully saturated colour for h in [0, 1). +func hue(h float32) color.RGBA { + h6 := h * 6 + f := h6 - float32(int(h6)) + up, down := uint8(255*f), uint8(255*(1-f)) + switch int(h6) % 6 { + case 0: + return color.RGBA{255, up, 0, 255} + case 1: + return color.RGBA{down, 255, 0, 255} + case 2: + return color.RGBA{0, 255, up, 255} + case 3: + return color.RGBA{0, down, 255, 255} + case 4: + return color.RGBA{up, 0, 255, 255} + default: + return color.RGBA{255, 0, down, 255} + } +} diff --git a/ili9325/ili9325.go b/ili9325/ili9325.go new file mode 100644 index 000000000..c2ad39a70 --- /dev/null +++ b/ili9325/ili9325.go @@ -0,0 +1,269 @@ +// Package ili9325 implements a driver for the ILI9325 TFT controller on an +// 8-bit parallel bus, as used on the ITead ITDB02-2.8 shield. +// +// Datasheet: https://cdn-shop.adafruit.com/datasheets/ILI9325.pdf +package ili9325 // import "tinygo.org/x/drivers/ili9325" + +import ( + "errors" + "image/color" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/pin" + "tinygo.org/x/drivers/pixel" +) + +// Bus is an 8-bit parallel data bus including its write strobe. +type Bus interface { + // WriteBytes writes each byte in turn, pulsing WR after each one. + WriteBytes(buf []byte) + + // WriteRepeat writes v high byte first, n times. + WriteRepeat(v uint16, n int) +} + +// Image buffer type used by the ili9325. +type Image = pixel.Image[pixel.RGB565BE] + +const ( + width = 240 + height = 320 +) + +var ( + errOutOfBounds = errors.New("ili9325: rectangle coordinates outside display area") + errShortData = errors.New("ili9325: pixel data shorter than rectangle") +) + +// Config is the configuration for the display. +type Config struct { + Rotation drivers.Rotation +} + +// Device is an ILI9325 display on an 8-bit parallel bus. +type Device struct { + bus Bus + rs pin.OutputFunc + cs pin.OutputFunc + rst pin.OutputFunc + rotation drivers.Rotation + buf [2]byte +} + +// New returns a new ili9325 driver. The rs, cs and rst pins must already be +// configured as outputs. +func New(bus Bus, rs, cs, rst pin.Output) *Device { + return &Device{ + bus: bus, + rs: rs.Set, + cs: cs.Set, + rst: rst.Set, + } +} + +type reg struct { + addr uint8 + value uint16 + delay time.Duration +} + +// Power on sequence and gamma from the UTFT library ILI9325D driver, see +// https://github.com/gnulabis/UTFT-ESP/blob/master/UTFT/tft_drivers/ili9325d/default/initlcd.h +var initSequence = [...]reg{ + {0xE5, 0x78F0, 0}, {DRIVER_OUTPUT_CONTROL, 0x0100, 0}, {LCD_DRIVING_CONTROL, 0x0200, 0}, + {ENTRY_MODE, entryModeBGR | entryModeIDX | entryModeIDY, 0}, + {RESIZE_CONTROL, 0x0000, 0}, {DISPLAY_CONTROL2, 0x0207, 0}, {DISPLAY_CONTROL3, 0x0000, 0}, + {DISPLAY_CONTROL4, 0x0000, 0}, {RGB_INTERFACE_CONTROL1, 0x0000, 0}, + {FRAME_MARKER_POSITION, 0x0000, 0}, {RGB_INTERFACE_CONTROL2, 0x0000, 0}, + {POWER_CONTROL1, 0x0000, 0}, {POWER_CONTROL2, 0x0007, 0}, {POWER_CONTROL3, 0x0000, 0}, + {POWER_CONTROL4, 0x0000, 0}, {DISPLAY_CONTROL1, 0x0001, 200 * time.Millisecond}, + {POWER_CONTROL1, 0x1690, 0}, {POWER_CONTROL2, 0x0227, 50 * time.Millisecond}, + {POWER_CONTROL3, 0x000D, 50 * time.Millisecond}, + {POWER_CONTROL4, 0x1200, 0}, {POWER_CONTROL7, 0x000A, 0}, + {FRAME_RATE_CONTROL, 0x000D, 50 * time.Millisecond}, + {GRAM_HORIZONTAL_ADDRESS, 0x0000, 0}, {GRAM_VERTICAL_ADDRESS, 0x0000, 0}, + {0x30, 0x0000, 0}, {0x31, 0x0404, 0}, {0x32, 0x0003, 0}, {0x35, 0x0405, 0}, + {0x36, 0x0808, 0}, {0x37, 0x0407, 0}, {0x38, 0x0303, 0}, {0x39, 0x0707, 0}, + {0x3C, 0x0504, 0}, {0x3D, 0x0808, 0}, + {HORIZONTAL_START, 0x0000, 0}, {HORIZONTAL_END, width - 1, 0}, + {VERTICAL_START, 0x0000, 0}, {VERTICAL_END, height - 1, 0}, + {GATE_SCAN_CONTROL1, 0xA700, 0}, {GATE_SCAN_CONTROL2, 0x0001, 0}, {GATE_SCAN_CONTROL3, 0x0000, 0}, + {0x80, 0x0000, 0}, {0x81, 0x0000, 0}, {0x82, 0x0000, 0}, {0x83, 0x0000, 0}, + {0x84, 0x0000, 0}, {0x85, 0x0000, 0}, + {PANEL_INTERFACE_CONTROL1, 0x0010, 0}, {PANEL_INTERFACE_CONTROL2, 0x0000, 0}, + {DISPLAY_CONTROL1, 0x0133, 0}, +} + +// Configure resets and initialises the display. +func (d *Device) Configure(config Config) error { + d.cs.High() + d.rst.High() + time.Sleep(5 * time.Millisecond) + d.rst.Low() + time.Sleep(15 * time.Millisecond) + d.rst.High() + time.Sleep(15 * time.Millisecond) + + d.cs.Low() + for _, r := range initSequence { + d.writeReg(r.addr, r.value) + if r.delay > 0 { + time.Sleep(r.delay) + } + } + d.cs.High() + return d.SetRotation(config.Rotation) +} + +// Size returns the current size of the display. +func (d *Device) Size() (x, y int16) { + if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 { + return height, width + } + return width, height +} + +// Rotation returns the current rotation of the display. +func (d *Device) Rotation() drivers.Rotation { + return d.rotation +} + +// SetRotation changes the rotation of the display (clockwise). Mirrored +// rotations are not supported. +func (d *Device) SetRotation(rotation drivers.Rotation) error { + switch rotation { + case drivers.Rotation0, drivers.Rotation90, drivers.Rotation180, drivers.Rotation270: + default: + return errors.New("ili9325: unsupported rotation") + } + d.rotation = rotation + d.cs.Low() + d.writeReg(ENTRY_MODE, entryModeBGR|d.entryMode()) + d.cs.High() + return nil +} + +// SetPixel draws a single pixel. +func (d *Device) SetPixel(x, y int16, c color.RGBA) { + w, h := d.Size() + if x < 0 || y < 0 || x >= w || y >= h { + return + } + d.cs.Low() + d.setWindow(x, y, 1, 1) + d.bus.WriteRepeat(RGBATo565(c), 1) + d.cs.High() +} + +// Display does nothing, there is no buffer. +func (d *Device) Display() error { + return nil +} + +// FillRectangle fills a rectangle at the given coordinates with a colour. +func (d *Device) FillRectangle(x, y, w, h int16, c color.RGBA) error { + if !d.inBounds(x, y, w, h) { + return errOutOfBounds + } + d.cs.Low() + d.setWindow(x, y, w, h) + d.bus.WriteRepeat(RGBATo565(c), int(w)*int(h)) + d.cs.High() + return nil +} + +// FillScreen fills the whole screen with a colour. +func (d *Device) FillScreen(c color.RGBA) { + w, h := d.Size() + d.FillRectangle(0, 0, w, h, c) +} + +// DrawRGBBitmap8 draws big endian RGB565 pixel data at the given coordinates. +func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { + if !d.inBounds(x, y, w, h) { + return errOutOfBounds + } + if len(data) < int(w)*int(h)*2 { + return errShortData + } + d.cs.Low() + d.setWindow(x, y, w, h) + d.bus.WriteBytes(data[:int(w)*int(h)*2]) + d.cs.High() + return nil +} + +// DrawBitmap draws the bitmap at the given coordinates. +func (d *Device) DrawBitmap(x, y int16, bitmap Image) error { + w, h := bitmap.Size() + return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(w), int16(h)) +} + +func (d *Device) inBounds(x, y, w, h int16) bool { + dw, dh := d.Size() + return x >= 0 && y >= 0 && w > 0 && h > 0 && x+w <= dw && y+h <= dh +} + +// entryMode returns the address counter direction that writes pixels in row +// order for the current rotation. +func (d *Device) entryMode() uint16 { + switch d.rotation { + case drivers.Rotation90: + return entryModeIDY | entryModeAM + case drivers.Rotation180: + return 0 + case drivers.Rotation270: + return entryModeIDX | entryModeAM + default: + return entryModeIDX | entryModeIDY + } +} + +// setWindow maps a window in rotated coordinates to the panel. +func (d *Device) setWindow(x, y, w, h int16) { + var x0, x1, y0, y1, cx, cy int16 + switch d.rotation { + case drivers.Rotation90: + x0, x1 = width-y-h, width-1-y + y0, y1 = x, x+w-1 + cx, cy = x1, y0 + case drivers.Rotation180: + x0, x1 = width-x-w, width-1-x + y0, y1 = height-y-h, height-1-y + cx, cy = x1, y1 + case drivers.Rotation270: + x0, x1 = y, y+h-1 + y0, y1 = height-x-w, height-1-x + cx, cy = x0, y1 + default: + x0, x1 = x, x+w-1 + y0, y1 = y, y+h-1 + cx, cy = x0, y0 + } + d.writeReg(HORIZONTAL_START, uint16(x0)) + d.writeReg(HORIZONTAL_END, uint16(x1)) + d.writeReg(VERTICAL_START, uint16(y0)) + d.writeReg(VERTICAL_END, uint16(y1)) + d.writeReg(GRAM_HORIZONTAL_ADDRESS, uint16(cx)) + d.writeReg(GRAM_VERTICAL_ADDRESS, uint16(cy)) + d.writeIndex(WRITE_DATA_TO_GRAM) + d.rs.High() +} + +func (d *Device) writeIndex(addr uint8) { + d.rs.Low() + d.buf = [2]byte{0, addr} + d.bus.WriteBytes(d.buf[:]) +} + +func (d *Device) writeReg(addr uint8, value uint16) { + d.writeIndex(addr) + d.rs.High() + d.bus.WriteRepeat(value, 1) +} + +// RGBATo565 converts a color.RGBA to the RGB565 value used by the display. +func RGBATo565(c color.RGBA) uint16 { + return uint16(c.R&0xF8)<<8 | uint16(c.G&0xFC)<<3 | uint16(c.B)>>3 +} diff --git a/ili9325/pinbus.go b/ili9325/pinbus.go new file mode 100644 index 000000000..11cb382d4 --- /dev/null +++ b/ili9325/pinbus.go @@ -0,0 +1,41 @@ +package ili9325 + +import "tinygo.org/x/drivers/internal/pin" + +// PinBus is a portable Bus that sets each data pin in turn. The pins must +// already be configured as outputs. +type PinBus struct { + data [8]pin.OutputFunc + wr pin.OutputFunc +} + +// NewPinBus returns a Bus using data pins d0 to d7 and the WR strobe. +func NewPinBus(d0, d1, d2, d3, d4, d5, d6, d7, wr pin.Output) *PinBus { + return &PinBus{ + data: [8]pin.OutputFunc{d0.Set, d1.Set, d2.Set, d3.Set, d4.Set, d5.Set, d6.Set, d7.Set}, + wr: wr.Set, + } +} + +func (b *PinBus) write(v byte) { + for i, set := range b.data { + set(v&(1< 0; n-- { + b.write(byte(v >> 8)) + b.write(byte(v)) + } +} diff --git a/ili9325/portbus_atsamd21.go b/ili9325/portbus_atsamd21.go new file mode 100644 index 000000000..f04f2e5bc --- /dev/null +++ b/ili9325/portbus_atsamd21.go @@ -0,0 +1,82 @@ +//go:build atsamd21 + +package ili9325 + +import ( + "device/arm" + "device/sam" + "errors" + "machine" + "runtime/volatile" +) + +// PortBus is a Bus for the SAMD21 that writes each byte with single cycle +// IOBUS stores. The data pins and WR must be on the same port and already be +// configured as outputs. WR is held low and high for at least 3 cycles, 62 ns +// at 48 MHz, to meet the ILI9325 datasheet V0.43 13.4.1 PWLW and PWHW of 50 ns. +type PortBus struct { + outset *volatile.Register32 + outclr *volatile.Register32 + dataMask uint32 + wrMask uint32 + setMask [256]uint32 +} + +// NewPortBus returns a Bus using data pins d0 to d7 and the WR strobe. +func NewPortBus(data [8]machine.Pin, wr machine.Pin) (*PortBus, error) { + group := uint8(wr) / 32 + b := &PortBus{wrMask: 1 << (uint8(wr) % 32)} + for i, p := range data { + if uint8(p)/32 != group { + return nil, errors.New("ili9325: data and WR pins must be on the same port") + } + bit := uint32(1) << (uint8(p) % 32) + b.dataMask |= bit + for v := range b.setMask { + if v&(1<>8], b.setMask[v&0xff] + hiClr, loClr := b.dataMask&^hiSet|wr, b.dataMask&^loSet|wr + for ; n > 0; n-- { + outclr.Set(hiClr) + outset.Set(hiSet) + arm.Asm("nop") + outset.Set(wr) + arm.Asm("nop\nnop") + outclr.Set(loClr) + outset.Set(loSet) + arm.Asm("nop") + outset.Set(wr) + arm.Asm("nop\nnop") + } +} diff --git a/ili9325/registers.go b/ili9325/registers.go new file mode 100644 index 000000000..b8cae7bb5 --- /dev/null +++ b/ili9325/registers.go @@ -0,0 +1,42 @@ +package ili9325 + +// Registers from the ILI9325 datasheet register list. +const ( + DRIVER_OUTPUT_CONTROL = 0x01 + LCD_DRIVING_CONTROL = 0x02 + ENTRY_MODE = 0x03 + RESIZE_CONTROL = 0x04 + DISPLAY_CONTROL1 = 0x07 + DISPLAY_CONTROL2 = 0x08 + DISPLAY_CONTROL3 = 0x09 + DISPLAY_CONTROL4 = 0x0A + RGB_INTERFACE_CONTROL1 = 0x0C + FRAME_MARKER_POSITION = 0x0D + RGB_INTERFACE_CONTROL2 = 0x0F + POWER_CONTROL1 = 0x10 + POWER_CONTROL2 = 0x11 + POWER_CONTROL3 = 0x12 + POWER_CONTROL4 = 0x13 + GRAM_HORIZONTAL_ADDRESS = 0x20 + GRAM_VERTICAL_ADDRESS = 0x21 + WRITE_DATA_TO_GRAM = 0x22 + POWER_CONTROL7 = 0x29 + FRAME_RATE_CONTROL = 0x2B + HORIZONTAL_START = 0x50 + HORIZONTAL_END = 0x51 + VERTICAL_START = 0x52 + VERTICAL_END = 0x53 + GATE_SCAN_CONTROL1 = 0x60 + GATE_SCAN_CONTROL2 = 0x61 + GATE_SCAN_CONTROL3 = 0x6A + PANEL_INTERFACE_CONTROL1 = 0x90 + PANEL_INTERFACE_CONTROL2 = 0x92 +) + +// Entry mode (R03h) bits. +const ( + entryModeBGR = 1 << 12 + entryModeIDY = 1 << 5 + entryModeIDX = 1 << 4 + entryModeAM = 1 << 3 +) diff --git a/smoketest.sh b/smoketest.sh index b9f2c4995..866de14bf 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -51,6 +51,9 @@ tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341 tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341/scroll tinygo build -size short -o ./build/test.hex -target=xiao ./examples/ili9341/scroll tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341/slideshow +tinygo build -size short -o ./build/test.hex -target=arduino-zero ./examples/ili9325/itdb02 +tinygo build -size short -o ./build/test.hex -target=arduino ./examples/ili9325/itdb02 +tinygo build -size short -o ./build/test.hex -target=arduino-zero ./examples/ili9325/lorenz tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/lis3dh/main.go tinygo build -size short -o ./build/test.hex -target=nano-33-ble ./examples/lps22hb/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/lsm303agr/main.go