From 2a9886cb09c7c5810e5f692b44d07bf7cd473d38 Mon Sep 17 00:00:00 2001 From: Dave Cheney Date: Tue, 20 Oct 2026 19:09:21 +1100 Subject: [PATCH 1/3] ili9325: add ILI9325 driver for 8-bit parallel displays This adds a driver for the ILI9325 TFT controller on an 8-bit parallel bus, with an example for the ITead ITDB02-2.8 Arduino shield. --- examples/ili9325/itdb02/bus_atsamd21.go | 13 ++ examples/ili9325/itdb02/bus_other.go | 9 + examples/ili9325/itdb02/main.go | 63 ++++++ ili9325/ili9325.go | 247 ++++++++++++++++++++++++ ili9325/pinbus.go | 39 ++++ ili9325/portbus_atsamd21.go | 72 +++++++ ili9325/registers.go | 42 ++++ smoketest.sh | 2 + 8 files changed, 487 insertions(+) create mode 100644 examples/ili9325/itdb02/bus_atsamd21.go create mode 100644 examples/ili9325/itdb02/bus_other.go create mode 100644 examples/ili9325/itdb02/main.go create mode 100644 ili9325/ili9325.go create mode 100644 ili9325/pinbus.go create mode 100644 ili9325/portbus_atsamd21.go create mode 100644 ili9325/registers.go 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/ili9325/ili9325.go b/ili9325/ili9325.go new file mode 100644 index 000000000..1008170bf --- /dev/null +++ b/ili9325/ili9325.go @@ -0,0 +1,247 @@ +// 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 +) + +type Config struct { + Rotation drivers.Rotation +} + +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 + 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 errors.New("rectangle coordinates outside display area") + } + 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) || len(data) < int(w)*int(h)*2 { + return errors.New("rectangle coordinates outside display area") + } + 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 +} + +// setWindow maps a window in rotated coordinates to the panel and sets the +// address counter direction so pixels can be written in row order. +func (d *Device) setWindow(x, y, w, h int16) { + var x0, x1, y0, y1, cx, cy int16 + var mode uint16 + switch d.rotation { + case drivers.Rotation90: + x0, x1 = width-y-h, width-1-y + y0, y1 = x, x+w-1 + cx, cy = x1, y0 + mode = entryModeIDY | entryModeAM + 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 + mode = entryModeIDX | entryModeAM + default: + x0, x1 = x, x+w-1 + y0, y1 = y, y+h-1 + cx, cy = x0, y0 + mode = entryModeIDX | entryModeIDY + } + d.writeReg(ENTRY_MODE, entryModeBGR|mode) + 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..31df855fb --- /dev/null +++ b/ili9325/pinbus.go @@ -0,0 +1,39 @@ +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..b29480e24 --- /dev/null +++ b/ili9325/portbus_atsamd21.go @@ -0,0 +1,72 @@ +//go:build atsamd21 + +package ili9325 + +import ( + "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. +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|b.wrMask, b.dataMask&^loSet|b.wrMask + for ; n > 0; n-- { + outclr.Set(hiClr) + outset.Set(hiSet) + outset.Set(b.wrMask) + outclr.Set(loClr) + outset.Set(loSet) + outset.Set(b.wrMask) + } +} 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..d0b16b377 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -51,6 +51,8 @@ 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=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 From 14c3110c8f4f0cdd49ece5a67ce201710841ed4a Mon Sep 17 00:00:00 2001 From: Dave Cheney Date: Tue, 20 Oct 2026 19:16:09 +1100 Subject: [PATCH 2/3] examples/ili9325: add Lorenz attractor example This adds an example that draws the Lorenz attractor on the ITead ITDB02-2.8 shield. --- examples/ili9325/lorenz/bus_atsamd21.go | 13 +++ examples/ili9325/lorenz/bus_other.go | 9 ++ examples/ili9325/lorenz/main.go | 106 ++++++++++++++++++++++++ smoketest.sh | 1 + 4 files changed, 129 insertions(+) create mode 100644 examples/ili9325/lorenz/bus_atsamd21.go create mode 100644 examples/ili9325/lorenz/bus_other.go create mode 100644 examples/ili9325/lorenz/main.go 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/smoketest.sh b/smoketest.sh index d0b16b377..866de14bf 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -53,6 +53,7 @@ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/ili9341/scr 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 From 74c77c14535086cde9e7d10f10855ecb002c0660 Mon Sep 17 00:00:00 2001 From: Dave Cheney Date: Tue, 27 Oct 2026 14:36:20 +1100 Subject: [PATCH 3/3] ili9325: keep WR pulses within the datasheet limits Review of #926 asked for ENTRY_MODE to be written once per rotation, for clearer errors and doc comments, and for a check of the WR pulse width against the ILI9325 AC characteristics. The datasheet V0.43 13.4.1 gives PWLW and PWHW 50 ns min. A DSLogic capture at 400 MS/s on an Arduino M0 at -opt=2 showed WR high of 25 ns between the two bytes of a pixel. WR low was in spec. At the default -opt=z each register store is a call, so the pulses were long only by accident. Inline asm padding gives the same minimum at every opt level. After the change the minimums are 58 ns low and 65 ns high. Traces: https://raw.githubusercontent.com/davecheney/tinygo/pr-assets/drivers926-wr-timing.png --- ili9325/ili9325.go | 42 ++++++++++++++++++++++++++++--------- ili9325/pinbus.go | 2 ++ ili9325/portbus_atsamd21.go | 26 ++++++++++++++++------- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/ili9325/ili9325.go b/ili9325/ili9325.go index 1008170bf..c2ad39a70 100644 --- a/ili9325/ili9325.go +++ b/ili9325/ili9325.go @@ -31,10 +31,17 @@ const ( 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 @@ -131,6 +138,9 @@ func (d *Device) SetRotation(rotation drivers.Rotation) error { return errors.New("ili9325: unsupported rotation") } d.rotation = rotation + d.cs.Low() + d.writeReg(ENTRY_MODE, entryModeBGR|d.entryMode()) + d.cs.High() return nil } @@ -154,7 +164,7 @@ func (d *Device) Display() error { // 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 errors.New("rectangle coordinates outside display area") + return errOutOfBounds } d.cs.Low() d.setWindow(x, y, w, h) @@ -171,8 +181,11 @@ func (d *Device) FillScreen(c color.RGBA) { // 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) || len(data) < int(w)*int(h)*2 { - return errors.New("rectangle coordinates outside display area") + 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) @@ -192,17 +205,29 @@ func (d *Device) inBounds(x, y, w, h int16) bool { return x >= 0 && y >= 0 && w > 0 && h > 0 && x+w <= dw && y+h <= dh } -// setWindow maps a window in rotated coordinates to the panel and sets the -// address counter direction so pixels can be written in row order. +// 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 - var mode uint16 switch d.rotation { case drivers.Rotation90: x0, x1 = width-y-h, width-1-y y0, y1 = x, x+w-1 cx, cy = x1, y0 - mode = entryModeIDY | entryModeAM case drivers.Rotation180: x0, x1 = width-x-w, width-1-x y0, y1 = height-y-h, height-1-y @@ -211,14 +236,11 @@ func (d *Device) setWindow(x, y, w, h int16) { x0, x1 = y, y+h-1 y0, y1 = height-x-w, height-1-x cx, cy = x0, y1 - mode = entryModeIDX | entryModeAM default: x0, x1 = x, x+w-1 y0, y1 = y, y+h-1 cx, cy = x0, y0 - mode = entryModeIDX | entryModeIDY } - d.writeReg(ENTRY_MODE, entryModeBGR|mode) d.writeReg(HORIZONTAL_START, uint16(x0)) d.writeReg(HORIZONTAL_END, uint16(x1)) d.writeReg(VERTICAL_START, uint16(y0)) diff --git a/ili9325/pinbus.go b/ili9325/pinbus.go index 31df855fb..11cb382d4 100644 --- a/ili9325/pinbus.go +++ b/ili9325/pinbus.go @@ -25,12 +25,14 @@ func (b *PinBus) write(v byte) { b.wr.High() } +// WriteBytes writes each byte in turn, pulsing WR after each one. func (b *PinBus) WriteBytes(buf []byte) { for _, v := range buf { b.write(v) } } +// WriteRepeat writes v high byte first, n times. func (b *PinBus) WriteRepeat(v uint16, n int) { for ; n > 0; n-- { b.write(byte(v >> 8)) diff --git a/ili9325/portbus_atsamd21.go b/ili9325/portbus_atsamd21.go index b29480e24..f04f2e5bc 100644 --- a/ili9325/portbus_atsamd21.go +++ b/ili9325/portbus_atsamd21.go @@ -3,6 +3,7 @@ package ili9325 import ( + "device/arm" "device/sam" "errors" "machine" @@ -11,7 +12,8 @@ import ( // 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. +// 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 @@ -47,26 +49,34 @@ func NewPortBus(data [8]machine.Pin, wr machine.Pin) (*PortBus, error) { return b, nil } +// WriteBytes writes each byte in turn, pulsing WR after each one. func (b *PortBus) WriteBytes(buf []byte) { - outset, outclr := b.outset, b.outclr + outset, outclr, wr := b.outset, b.outclr, b.wrMask for _, v := range buf { set := b.setMask[v] - outclr.Set(b.dataMask&^set | b.wrMask) + outclr.Set(b.dataMask&^set | wr) outset.Set(set) - outset.Set(b.wrMask) + arm.Asm("nop") + outset.Set(wr) + arm.Asm("nop\nnop") } } +// WriteRepeat writes v high byte first, n times. func (b *PortBus) WriteRepeat(v uint16, n int) { - outset, outclr := b.outset, b.outclr + outset, outclr, wr := b.outset, b.outclr, b.wrMask hiSet, loSet := b.setMask[v>>8], b.setMask[v&0xff] - hiClr, loClr := b.dataMask&^hiSet|b.wrMask, b.dataMask&^loSet|b.wrMask + hiClr, loClr := b.dataMask&^hiSet|wr, b.dataMask&^loSet|wr for ; n > 0; n-- { outclr.Set(hiClr) outset.Set(hiSet) - outset.Set(b.wrMask) + arm.Asm("nop") + outset.Set(wr) + arm.Asm("nop\nnop") outclr.Set(loClr) outset.Set(loSet) - outset.Set(b.wrMask) + arm.Asm("nop") + outset.Set(wr) + arm.Asm("nop\nnop") } }