diff --git a/examples/waveshare-epd/epd2in66b/main.go b/examples/waveshare-epd/epd2in66b/main.go index bc99323ce..07bc4e018 100644 --- a/examples/waveshare-epd/epd2in66b/main.go +++ b/examples/waveshare-epd/epd2in66b/main.go @@ -1,84 +1,74 @@ package main import ( - "machine" - "image/color" - + "machine" "time" - "tinygo.org/x/drivers/waveshare-epd/epd2in9" + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/waveshare-epd/epd2in66b" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freemono" ) -var display epd2in9.Device - -const ( - width = 128 - height = 296 +var ( + black = color.RGBA{0, 0, 0, 0xff} + white = color.RGBA{0xff, 0xff, 0xff, 0xff} + red = color.RGBA{0xff, 0, 0, 0xff} ) func main() { - machine.SPI0.Configure(machine.SPIConfig{ - Frequency: 8000000, - Mode: 0, - }) + machine.Serial.Configure(machine.UARTConfig{}) + time.Sleep(2 * time.Second) - display = epd2in9.New(machine.SPI0, machine.GPIO2, machine.GPIO3, machine.GPIO4, machine.GPIO5) - display.Configure(epd2in9.Config{ - Width: width, - LogicalWidth: width, - Height: height, + machine.SPI1.Configure(machine.SPIConfig{ + Frequency: epd2in66b.Baudrate, }) - black := color.RGBA{1, 1, 1, 255} - white := color.RGBA{0, 0, 0, 255} + println("started") - display.ClearBuffer() - println("Clear the display") - display.ClearDisplay() - display.WaitUntilIdle() - println("Waiting for 2 seconds") - time.Sleep(2 * time.Second) - - // Show a checkered board - for i := int16(0); i < width/8; i++ { - for j := int16(0); j < height/8; j++ { - if (i+j)%2 == 0 { - showRect(i*8, j*8, 8, 8, black) - } - } + display := epd2in66b.New(machine.SPI1) + err := display.Configure(epd2in66b.Config{}) + if err != nil { + panic(err) } - println("Show checkered board") - display.Display() - display.WaitUntilIdle() - println("Waiting for 2 seconds") - time.Sleep(2 * time.Second) - println("Set partial lut") - display.SetLUT(false) // partial updates (faster, but with some ghosting) - println("Show smaller striped area") - for i := int16(40); i < 88; i++ { - for j := int16(83); j < 166; j++ { - if (i+j)%4 == 0 || (i+j)%4 == 1 { - display.SetPixel(i, j, black) - } else { - display.SetPixel(i, j, white) - } - } + err = display.Reset() + if err != nil { + panic(err) } - display.Display() - display.WaitUntilIdle() + println("draw checkerboard") + drawCheckerBoard(&display) + + println("draw 'hello'") + tinyfont.WriteLineRotated(&display, &freemono.Bold24pt7b, 40, 10, "Hello!", white, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &freemono.Bold12pt7b, 10, 10, "tinygo rocks", white, tinyfont.ROTATION_90) + err = display.Display() + if err != nil { + panic(err) + } +} - display.DeepSleep() +func drawCheckerBoard(display drivers.Displayer) { + s := 8 + width, height := display.Size() + for x := 0; x <= int(width)-s; x += s { + for y := 0; y <= int(height)-s; y += s { + c := red + if (x/s)%2 == (y/s)%2 { + c = black + } - println("You could remove power now") + showRect(display, x, y, s, s, c) + } + } } -func showRect(x int16, y int16, w int16, h int16, c color.RGBA) { +func showRect(display drivers.Displayer, x int, y int, w int, h int, c color.RGBA) { for i := x; i < x+w; i++ { for j := y; j < y+h; j++ { - display.SetPixel(i, j, c) + display.SetPixel(int16(i), int16(j), c) } } } diff --git a/smoketest.sh b/smoketest.sh index b7530b224..39d37bcf8 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -80,6 +80,7 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl6 tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd4in2/main.go +tinygo build -size short -o ./build/test.hex -target=pico ./examples/waveshare-epd/epd4in66b/main.go tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812 tinygo build -size short -o ./build/test.bin -target=m5stamp-c3 ./examples/ws2812 tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go diff --git a/waveshare-epd/epd2in66b/2.66inch-e-paper-b-specification.pdf b/waveshare-epd/epd2in66b/2.66inch-e-paper-b-specification.pdf deleted file mode 100644 index 373cd900f..000000000 Binary files a/waveshare-epd/epd2in66b/2.66inch-e-paper-b-specification.pdf and /dev/null differ diff --git a/waveshare-epd/epd2in66b/dev.go b/waveshare-epd/epd2in66b/dev.go index 68b99b88c..78d9d3793 100644 --- a/waveshare-epd/epd2in66b/dev.go +++ b/waveshare-epd/epd2in66b/dev.go @@ -1,10 +1,12 @@ -// Package epd2in66b implements a driver for the Waveshare 2.66inch e-Paper (B) +// Package epd2in66b implements a driver for the Waveshare 2.66inch E-Paper E-Ink Display Module (B) +// for Raspberry Pi Pico, 296×152, Red / Black / White // Datasheet: https://files.waveshare.com/upload/e/ec/2.66inch-e-paper-b-specification.pdf package epd2in66b import ( "image/color" "time" + "tinygo.org/x/drivers" ) import "machine" @@ -13,12 +15,22 @@ const ( width = 152 height = 296 - rstPin = machine.GP12 - dcPin = machine.GP8 - csPin = machine.GP9 - busyPin = machine.GP13 + // using numerical values to enable generic tinygo compilation + rstPin = 12 + dcPin = 8 + csPin = 9 + busyPin = 13 ) +const Baudrate = 4 * machine.MHz + +type Config struct { + ResetPin machine.Pin + DataPin machine.Pin + ChipSelectPin machine.Pin + BusyPin machine.Pin +} + type Device struct { bus drivers.SPI cs machine.Pin @@ -33,20 +45,17 @@ type Device struct { redBuffer []byte } +// New allocates a new device. The SPI for the built-in header to be used is picos machine.SPI1 at 4 MHz baudrate. +// The bus is expected to be configured and ready for use. func New(bus drivers.SPI) Device { - - csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) - pixelCount := width * height - - bufLen := pixelCount / 8 if pixelCount%8 != 0 { - bufLen += 1 + // defend against copy & pasta foot-guns + panic("pixel count expected to be a multiple of 8") } + bufLen := pixelCount / 8 + return Device{ bus: bus, cs: csPin, @@ -61,6 +70,38 @@ func New(bus drivers.SPI) Device { } } +// Configure configures the device and its pins. The 'zero' config will fall back to the defaults. +// +// Default pins are: +// +// Data = GP8 +// ChipSelect = GP9 +// Reset = GP12 +// Busy = GP13 +func (d *Device) Configure(c Config) error { + if c.ChipSelectPin > 0 { + d.cs = c.ChipSelectPin + } + if c.DataPin > 0 { + d.dc = c.DataPin + } + + if c.ResetPin > 0 { + d.rst = c.ResetPin + } + + if c.BusyPin > 0 { + d.busy = c.BusyPin + } + + d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.busy.Configure(machine.PinConfig{Mode: machine.PinInput}) + + return nil +} + func (d *Device) Size() (x, y int16) { return d.width, d.height } @@ -68,41 +109,20 @@ func (d *Device) Size() (x, y int16) { // SetPixel modifies the internal buffer in a single pixel. // The display has 3 colors: red, black and white // -// - white = RGBA(0,0,0, *) -// - red = RGBA(1-255,0,0,*) +// - white = RGBA(255,255,255, 1-255) +// - red = RGBA(1-255,0,0,1-255) // - Anything else as black func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { if x < 0 || x >= d.width || y < 0 || y >= d.height { return } - /* - high = self.height - if( self.width % 8 == 0) : - wide = self.width // 8 - else : - wide = self.width // 8 + 1 - - self.send_command(0x24) - for j in range(0, high): - for i in range(0, wide): - self.send_data(~self.buffer_black[i + j * wide]) - - self.send_command(0x26) - for j in range(0, high): - for i in range(0, wide): - self.send_data(~self.buffer_red[i + j * wide]) - - - */ - bytePos, bitPos := pos(x, y, d.width) - // https://www.waveshare.com/wiki/Pico-ePaper-2.66-B#Working_protocoal - if c.R == 0xff && c.G == 0xff && c.B == 0xff { // white + if c.R == 0xff && c.G == 0xff && c.B == 0xff && c.A > 0 { // white set(d.blackBuffer, bytePos, bitPos, true) set(d.redBuffer, bytePos, bitPos, false) - } else if c.R != 0 && c.G == 0 && c.B == 0 { // red-ish + } else if c.R != 0 && c.G == 0 && c.B == 0 && c.A > 0 { // red-ish set(d.blackBuffer, bytePos, bitPos, true) set(d.redBuffer, bytePos, bitPos, true) } else { // black or other @@ -120,22 +140,16 @@ func set(buf []byte, bytePos, bitPos int, v bool) { } func pos(x, y, stride int16) (bytePos int, bitPos int) { - - /* - - for y in range(0, high): - for x in range(0, wide): - self.send_data(~self.buffer_red[x + y * wide]) - */ - p := int(x) + int(y)*int(stride) bytePos = p / 8 - bitPos = p % 8 + + // reverse bit position as it is reversed on the device's buffer + bitPos = 7 - p%8 + return bytePos, bitPos } func (d *Device) Display() error { - // Write RAM (Black White) / RAM 0x24 // 1 == white, 0 == black if err := d.sendCommandByte(0x24); err != nil { @@ -156,7 +170,9 @@ func (d *Device) Display() error { return err } - return d.turnOnDisplay() + err := d.turnOnDisplay() + + return err } func (d *Device) ClearBuffer() { @@ -165,17 +181,15 @@ func (d *Device) ClearBuffer() { } func (d *Device) turnOnDisplay() error { - - // Master Activation - + // also documented as 'Master Activation' if err := d.sendCommandByte(0x20); err != nil { return err } d.WaitUntilIdle() return nil } -func (d *Device) Reset() error { +func (d *Device) Reset() error { d.hwReset() d.WaitUntilIdle() @@ -185,12 +199,12 @@ func (d *Device) Reset() error { } d.WaitUntilIdle() - //data entry mode setting + // data entry mode setting if err := d.sendCommandSequence([]byte{0x11, 0x03}); err != nil { return err } - if err := d.setWindow(0, 0, d.width-1, d.height-1); err != nil { + if err := d.setWindow(0, d.width-1, 0, d.height-1); err != nil { return err } @@ -208,7 +222,6 @@ func (d *Device) Reset() error { } func (d *Device) setCursor(x, y uint16) error { - // Set RAM X address counter if err := d.sendCommandSequence([]byte{0x4e, byte(x & 0x1f)}); err != nil { return err @@ -234,15 +247,14 @@ func (d *Device) hwReset() { } func (d *Device) setWindow(xstart, xend, ystart, yend int16) error { - - // Set RAM X- address Start / End position + // set RAM X-address start / end position d1 := byte((xstart >> 3) & 0x1f) d2 := byte((xend >> 3) & 0x1f) if err := d.sendCommandSequence([]byte{0x44, d1, d2}); err != nil { return err } - // Set Ram Y- address Start / End position + // set RAM Y-address start / end position ystartLo := byte(ystart) ystartHi := byte(ystart>>8) & 0x1 @@ -256,6 +268,7 @@ func (d *Device) setWindow(xstart, xend, ystart, yend int16) error { } func (d *Device) WaitUntilIdle() { + // give it some time to get busy time.Sleep(50 * time.Millisecond) for d.busy.Get() { // high = busy @@ -265,8 +278,9 @@ func (d *Device) WaitUntilIdle() { // give it some extra time time.Sleep(50 * time.Millisecond) } -func (d *Device) sendCommandSequence(seq []byte) error { +// sendCommandSequence sends the first byte in the buffer as a 'command' and all following bytes as data +func (d *Device) sendCommandSequence(seq []byte) error { err := d.sendCommandByte(seq[0]) if err != nil { return err @@ -281,6 +295,7 @@ func (d *Device) sendCommandSequence(seq []byte) error { return nil } + func (d *Device) sendCommandByte(b byte) error { d.dc.Low() d.cs.Low() @@ -298,7 +313,6 @@ func (d *Device) sendDataByte(b byte) error { } func (d *Device) sendData(b []byte) error { - d.dc.High() d.cs.Low() err := d.bus.Tx(b, nil) @@ -308,7 +322,6 @@ func (d *Device) sendData(b []byte) error { // fill quickly fills a slice with a given value func fill(s []byte, b byte) { - s[0] = b for j := 1; j < len(s); j *= 2 { copy(s[j:], s[:j]) diff --git a/waveshare-epd/epd2in66b/dev_test.go b/waveshare-epd/epd2in66b/dev_test.go new file mode 100644 index 000000000..7dd52b73f --- /dev/null +++ b/waveshare-epd/epd2in66b/dev_test.go @@ -0,0 +1,92 @@ +package epd2in66b + +import ( + _ "embed" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "os" + "testing" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freemono" +) + +type mockBus struct{} + +func (m *mockBus) Tx(w, r []byte) error { + return nil +} + +func (m *mockBus) Transfer(b byte) (byte, error) { + return 0, nil +} + +func TestBufferDrawing(t *testing.T) { + dev := New(&mockBus{}) + + tinyfont.WriteLine(&dev, &freemono.Bold9pt7b, 10, 40, "Hello World!", color.RGBA{0xff, 0xff, 0xff, 0xff}) + + red := color.RGBA{0xff, 0, 0, 0xff} + black := color.RGBA{0xff, 0xff, 0xff, 0xff} + showRect(&dev, 10, 10, 10, 10, black) + showRect(&dev, 10, 20, 10, 10, red) + + img := toImage(&dev) + writeImage(img) +} + +func toImage(dev *Device) *image.RGBA { + red := color.RGBA{0xff, 0, 0, 0xff} + + xMax, yMax := dev.Size() + r := image.Rect(0, 0, int(xMax), int(yMax)) + container := image.NewRGBA(r) + draw.Draw(container, container.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Over) + + for x := 0; x < int(xMax); x++ { + for y := 0; y < int(yMax); y++ { + + bytePos, bitPos := pos(int16(x), int16(y), dev.width) + + if isSet(dev.redBuffer, bytePos, bitPos) { + container.Set(x, y, red) + } else if isSet(dev.blackBuffer, bytePos, bitPos) { + container.Set(x, y, color.Black) + } + } + } + + return container +} + +func isSet(buf []byte, bytePos, bitPos int) bool { + return (buf[bytePos])&(0x1<>3) & 0x1f) - self.send_data((x_end>>3) & 0x1f) - - self.send_command(0x45) - self.send_data(y_start&0xff) - self.send_data((y_start&0x100)>>8) - self.send_data((y_end&0xff)) - self.send_data((y_end&0x100)>>8) - - def SetCursor(self, x_start, y_start): - self.send_command(0x4E) - self.send_data(x_start & 0x1f) - - self.send_command(0x4f) - self.send_data(y_start&0xff) - self.send_data((y_start&0x100)>>8) - - def ReadBusy(self): - print('e-Paper busy') - utime.sleep_ms(50) - while(self.busy_pin.value() == 1): # 0: idle, 1: busy - utime.sleep_ms(10) - print('e-Paper busy release') - utime.sleep_ms(50) - - def TurnOnDisplay(self): - self.send_command(0x20) - self.ReadBusy() - - def init(self): - print('init') - self.reset() - self.ReadBusy() - self.send_command(0x12) - self.ReadBusy()#waiting for the electronic paper IC to release the idle signal - - self.send_command(0x11) - self.send_data(0x03) - - self.SetWindow(0, 0, self.width-1, self.height-1) - - self.send_command(0x21) #resolution setting - self.send_data (0x00) - self.send_data (0x80) - - - self.SetCursor(0,0) - self.ReadBusy() - - - def display(self): - high = self.height - if( self.width % 8 == 0) : - wide = self.width // 8 - else : - wide = self.width // 8 + 1 - - self.send_command(0x24) - for j in range(0, high): - for i in range(0, wide): - self.send_data(~self.buffer_black[i + j * wide]) - - self.send_command(0x26) - for j in range(0, high): - for i in range(0, wide): - self.send_data(~self.buffer_red[i + j * wide]) - - self.TurnOnDisplay() - - - def Clear(self, colorblack, colorred): - high = self.height - if( self.width % 8 == 0) : - wide = self.width // 8 - else : - wide = self.width // 8 + 1 - - self.send_command(0x24) - self.send_data1([colorblack] * high * wide) - - self.send_command(0x26) - self.send_data1([~colorred] * high * wide) - - self.TurnOnDisplay() - - def sleep(self): - self.send_command(0X10) # deep sleep - self.send_data(0x01) - -if __name__=='__main__': - epd = EPD_2in9_B() - epd.Clear(0xff, 0xff) - - epd.imageblack.fill(0xff) - epd.imagered.fill(0xff) - epd.imageblack.text("Waveshare", 0, 10, 0x00) - epd.imagered.text("ePaper-2.66-B", 0, 25, 0x00) - epd.imageblack.text("RPi Pico", 0, 40, 0x00) - epd.imagered.text("Hello World", 0, 55, 0x00) - epd.display() - epd.delay_ms(2000) - - epd.imagered.vline(10, 90, 40, 0x00) - epd.imagered.vline(90, 90, 40, 0x00) - epd.imageblack.hline(10, 90, 80, 0x00) - epd.imageblack.hline(10, 130, 80, 0x00) - epd.imagered.line(10, 90, 90, 130, 0x00) - epd.imageblack.line(90, 90, 10, 130, 0x00) - epd.display() - epd.delay_ms(2000) - - epd.imageblack.rect(10, 150, 40, 40, 0x00) - epd.imagered.fill_rect(60, 150, 40, 40, 0x00) - epd.display() - epd.delay_ms(2000) - - - epd.Clear(0xff, 0xff) - epd.delay_ms(2000) - print("sleep") - epd.sleep() \ No newline at end of file diff --git a/waveshare-epd/epd2in66b/framebuf.go b/waveshare-epd/epd2in66b/framebuf.go deleted file mode 100644 index 7874acbd2..000000000 --- a/waveshare-epd/epd2in66b/framebuf.go +++ /dev/null @@ -1 +0,0 @@ -package epd2in66b