Skip to content

Commit 749b2f6

Browse files
kenbelldeadprogram
authored andcommitted
epd: add waveshare 2.9in (v1)
1 parent 71c77d4 commit 749b2f6

File tree

4 files changed

+408
-1
lines changed

4 files changed

+408
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func main() {
5252

5353
## Currently supported devices
5454

55-
The following 81 devices are supported.
55+
The following 82 devices are supported.
5656

5757
| Device Name | Interface Type |
5858
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
@@ -137,6 +137,7 @@ The following 81 devices are supported.
137137
| [VL53L1X time-of-flight distance sensor](https://www.st.com/resource/en/datasheet/vl53l1x.pdf) | I2C |
138138
| [Waveshare 2.13" (B & C) e-paper display](https://www.waveshare.com/w/upload/d/d3/2.13inch-e-paper-b-Specification.pdf) | SPI |
139139
| [Waveshare 2.13" e-paper display](https://www.waveshare.com/w/upload/e/e6/2.13inch_e-Paper_Datasheet.pdf) | SPI |
140+
| [Waveshare 2.9" e-paper display (V1)](https://www.waveshare.com/w/upload/e/e6/2.9inch_e-Paper_Datasheet.pdf) | SPI |
140141
| [Waveshare 4.2" e-paper B/W display](https://www.waveshare.com/w/upload/6/6a/4.2inch-e-paper-specification.pdf) | SPI |
141142
| [WS2812 RGB LED](https://cdn-shop.adafruit.com/datasheets/WS2812.pdf) | GPIO |
142143
| [XPT2046 touch controller](http://grobotronics.com/images/datasheets/xpt2046-datasheet.pdf) | GPIO |
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package main
2+
3+
import (
4+
"machine"
5+
6+
"image/color"
7+
8+
"time"
9+
10+
"tinygo.org/x/drivers/waveshare-epd/epd2in9"
11+
)
12+
13+
var display epd2in9.Device
14+
15+
const (
16+
width = 128
17+
height = 296
18+
)
19+
20+
func main() {
21+
machine.SPI0.Configure(machine.SPIConfig{
22+
Frequency: 8000000,
23+
Mode: 0,
24+
})
25+
26+
display = epd2in9.New(machine.SPI0, machine.GPIO2, machine.GPIO3, machine.GPIO4, machine.GPIO5)
27+
display.Configure(epd2in9.Config{
28+
Width: width,
29+
LogicalWidth: width,
30+
Height: height,
31+
})
32+
33+
black := color.RGBA{1, 1, 1, 255}
34+
white := color.RGBA{0, 0, 0, 255}
35+
36+
display.ClearBuffer()
37+
println("Clear the display")
38+
display.ClearDisplay()
39+
display.WaitUntilIdle()
40+
println("Waiting for 2 seconds")
41+
time.Sleep(2 * time.Second)
42+
43+
// Show a checkered board
44+
for i := int16(0); i < width/8; i++ {
45+
for j := int16(0); j < height/8; j++ {
46+
if (i+j)%2 == 0 {
47+
showRect(i*8, j*8, 8, 8, black)
48+
}
49+
}
50+
}
51+
println("Show checkered board")
52+
display.Display()
53+
display.WaitUntilIdle()
54+
println("Waiting for 2 seconds")
55+
time.Sleep(2 * time.Second)
56+
57+
println("Set partial lut")
58+
display.SetLUT(false) // partial updates (faster, but with some ghosting)
59+
println("Show smaller striped area")
60+
for i := int16(40); i < 88; i++ {
61+
for j := int16(83); j < 166; j++ {
62+
if (i+j)%4 == 0 || (i+j)%4 == 1 {
63+
display.SetPixel(i, j, black)
64+
} else {
65+
display.SetPixel(i, j, white)
66+
}
67+
}
68+
}
69+
70+
display.Display()
71+
display.WaitUntilIdle()
72+
73+
display.DeepSleep()
74+
75+
println("You could remove power now")
76+
}
77+
78+
func showRect(x int16, y int16, w int16, h int16, c color.RGBA) {
79+
for i := x; i < x+w; i++ {
80+
for j := y; j < y+h; j++ {
81+
display.SetPixel(i, j, c)
82+
}
83+
}
84+
}

waveshare-epd/epd2in9/epd2in9.go

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
// Package epd2in9 implements a driver for Waveshare 2.9in black and white e-paper device.
2+
//
3+
// Note: this is for the V1 device (using IL3820), the V2 device uses a different chipset.
4+
//
5+
// Datasheets:
6+
// https://www.waveshare.com/w/upload/e/e6/2.9inch_e-Paper_Datasheet.pdf
7+
// https://www.smart-prototyping.com/image/data/9_Modules/EinkDisplay/GDE029A1/IL3820.pdf
8+
//
9+
// This implementation is essentially a copy of the 2in13 driver with the correct LUTs and
10+
// default size for the 2.9in device.
11+
//
12+
package epd2in9 // import "tinygo.org/x/drivers/waveshare-epd/epd2in9"
13+
14+
import (
15+
"image/color"
16+
"machine"
17+
"time"
18+
19+
"tinygo.org/x/drivers"
20+
)
21+
22+
type Config struct {
23+
Width int16 // Width is the display resolution
24+
Height int16
25+
LogicalWidth int16 // LogicalWidth must be a multiple of 8 and same size or bigger than Width
26+
Rotation Rotation // Rotation is clock-wise
27+
}
28+
29+
type Device struct {
30+
bus drivers.SPI
31+
cs machine.Pin
32+
dc machine.Pin
33+
rst machine.Pin
34+
busy machine.Pin
35+
logicalWidth int16
36+
width int16
37+
height int16
38+
buffer []uint8
39+
bufferLength uint32
40+
rotation Rotation
41+
}
42+
43+
type Rotation uint8
44+
45+
// Look up table for full updates
46+
var lutFullUpdate = [30]uint8{
47+
0x50, 0xAA, 0x55, 0xAA, 0x11, 0x00,
48+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
49+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
50+
0x00, 0x00, 0xFF, 0xFF, 0x1F, 0x00,
51+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
52+
}
53+
54+
// Look up table for partial updates, faster but there will be some ghosting
55+
var lutPartialUpdate = [30]uint8{
56+
0x10, 0x18, 0x18, 0x08, 0x18, 0x18,
57+
0x08, 0x00, 0x00, 0x00, 0x00, 0x00,
58+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
59+
0x00, 0x00, 0x13, 0x14, 0x44, 0x12,
60+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
61+
}
62+
63+
// New returns a new epd2in9 driver. Pass in a fully configured SPI bus.
64+
func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device {
65+
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
66+
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
67+
rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
68+
busyPin.Configure(machine.PinConfig{Mode: machine.PinInput})
69+
return Device{
70+
bus: bus,
71+
cs: csPin,
72+
dc: dcPin,
73+
rst: rstPin,
74+
busy: busyPin,
75+
}
76+
}
77+
78+
// Configure sets up the device.
79+
func (d *Device) Configure(cfg Config) {
80+
if cfg.LogicalWidth != 0 {
81+
d.logicalWidth = cfg.LogicalWidth
82+
} else {
83+
d.logicalWidth = 128
84+
}
85+
if cfg.Width != 0 {
86+
d.width = cfg.Width
87+
} else {
88+
d.width = 128
89+
}
90+
if cfg.Height != 0 {
91+
d.height = cfg.Height
92+
} else {
93+
d.height = 296
94+
}
95+
d.rotation = cfg.Rotation
96+
d.bufferLength = (uint32(d.logicalWidth) * uint32(d.height)) / 8
97+
d.buffer = make([]uint8, d.bufferLength)
98+
for i := uint32(0); i < d.bufferLength; i++ {
99+
d.buffer[i] = 0xFF
100+
}
101+
102+
d.cs.Low()
103+
d.dc.Low()
104+
d.rst.Low()
105+
106+
d.Reset()
107+
108+
d.SendCommand(DRIVER_OUTPUT_CONTROL)
109+
d.SendData(uint8((d.height - 1) & 0xFF))
110+
d.SendData(uint8(((d.height - 1) >> 8) & 0xFF))
111+
d.SendData(0x00) // GD = 0; SM = 0; TB = 0;
112+
d.SendCommand(BOOSTER_SOFT_START_CONTROL)
113+
d.SendData(0xD7)
114+
d.SendData(0xD6)
115+
d.SendData(0x9D)
116+
d.SendCommand(WRITE_VCOM_REGISTER)
117+
d.SendData(0xA8) // VCOM 7C
118+
d.SendCommand(SET_DUMMY_LINE_PERIOD)
119+
d.SendData(0x1A) // 4 dummy lines per gate
120+
d.SendCommand(SET_GATE_TIME)
121+
d.SendData(0x08) // 2us per line
122+
d.SendCommand(DATA_ENTRY_MODE_SETTING)
123+
d.SendData(0x03) // X increment; Y increment
124+
125+
d.SetLUT(true)
126+
}
127+
128+
// Reset resets the device
129+
func (d *Device) Reset() {
130+
d.rst.Low()
131+
time.Sleep(200 * time.Millisecond)
132+
d.rst.High()
133+
time.Sleep(200 * time.Millisecond)
134+
}
135+
136+
// DeepSleep puts the display into deepsleep
137+
func (d *Device) DeepSleep() {
138+
d.SendCommand(DEEP_SLEEP_MODE)
139+
d.WaitUntilIdle()
140+
}
141+
142+
// SendCommand sends a command to the display
143+
func (d *Device) SendCommand(command uint8) {
144+
d.sendDataCommand(true, command)
145+
}
146+
147+
// SendData sends a data byte to the display
148+
func (d *Device) SendData(data uint8) {
149+
d.sendDataCommand(false, data)
150+
}
151+
152+
// sendDataCommand sends image data or a command to the screen
153+
func (d *Device) sendDataCommand(isCommand bool, data uint8) {
154+
if isCommand {
155+
d.dc.Low()
156+
} else {
157+
d.dc.High()
158+
}
159+
d.cs.Low()
160+
d.bus.Transfer(data)
161+
d.cs.High()
162+
}
163+
164+
// SetLUT sets the look up tables for full or partial updates
165+
func (d *Device) SetLUT(fullUpdate bool) {
166+
d.SendCommand(WRITE_LUT_REGISTER)
167+
if fullUpdate {
168+
for i := 0; i < 30; i++ {
169+
d.SendData(lutFullUpdate[i])
170+
}
171+
} else {
172+
for i := 0; i < 30; i++ {
173+
d.SendData(lutPartialUpdate[i])
174+
}
175+
}
176+
}
177+
178+
// SetPixel modifies the internal buffer in a single pixel.
179+
// The display have 2 colors: black and white
180+
// We use RGBA(0,0,0, 255) as white (transparent)
181+
// Anything else as black
182+
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
183+
x, y = d.xy(x, y)
184+
if x < 0 || x >= d.logicalWidth || y < 0 || y >= d.height {
185+
return
186+
}
187+
byteIndex := (int32(x) + int32(y)*int32(d.logicalWidth)) / 8
188+
if c.R == 0 && c.G == 0 && c.B == 0 { // TRANSPARENT / WHITE
189+
d.buffer[byteIndex] |= 0x80 >> uint8(x%8)
190+
} else { // WHITE / EMPTY
191+
d.buffer[byteIndex] &^= 0x80 >> uint8(x%8)
192+
}
193+
}
194+
195+
// Display sends the buffer to the screen.
196+
func (d *Device) Display() error {
197+
d.setMemoryArea(0, 0, d.logicalWidth-1, d.height-1)
198+
for j := int16(0); j < d.height; j++ {
199+
d.setMemoryPointer(0, j)
200+
d.SendCommand(WRITE_RAM)
201+
for i := int16(0); i < d.logicalWidth/8; i++ {
202+
d.SendData(d.buffer[i+j*(d.logicalWidth/8)])
203+
}
204+
}
205+
206+
d.SendCommand(DISPLAY_UPDATE_CONTROL_2)
207+
d.SendData(0xC4)
208+
d.SendCommand(MASTER_ACTIVATION)
209+
d.SendCommand(TERMINATE_FRAME_READ_WRITE)
210+
return nil
211+
}
212+
213+
// ClearDisplay erases the device SRAM
214+
func (d *Device) ClearDisplay() {
215+
d.setMemoryArea(0, 0, d.logicalWidth-1, d.height-1)
216+
d.setMemoryPointer(0, 0)
217+
d.SendCommand(WRITE_RAM)
218+
for i := uint32(0); i < d.bufferLength; i++ {
219+
d.SendData(0xFF)
220+
}
221+
d.Display()
222+
}
223+
224+
// setMemoryArea sets the area of the display that will be updated
225+
func (d *Device) setMemoryArea(x0 int16, y0 int16, x1 int16, y1 int16) {
226+
d.SendCommand(SET_RAM_X_ADDRESS_START_END_POSITION)
227+
d.SendData(uint8((x0 >> 3) & 0xFF))
228+
d.SendData(uint8((x1 >> 3) & 0xFF))
229+
d.SendCommand(SET_RAM_Y_ADDRESS_START_END_POSITION)
230+
d.SendData(uint8(y0 & 0xFF))
231+
d.SendData(uint8((y0 >> 8) & 0xFF))
232+
d.SendData(uint8(y1 & 0xFF))
233+
d.SendData(uint8((y1 >> 8) & 0xFF))
234+
}
235+
236+
// setMemoryPointer moves the internal pointer to the speficied coordinates
237+
func (d *Device) setMemoryPointer(x int16, y int16) {
238+
d.SendCommand(SET_RAM_X_ADDRESS_COUNTER)
239+
d.SendData(uint8((x >> 3) & 0xFF))
240+
d.SendCommand(SET_RAM_Y_ADDRESS_COUNTER)
241+
d.SendData(uint8(y & 0xFF))
242+
d.SendData(uint8((y >> 8) & 0xFF))
243+
d.WaitUntilIdle()
244+
}
245+
246+
// WaitUntilIdle waits until the display is ready
247+
func (d *Device) WaitUntilIdle() {
248+
for d.busy.Get() {
249+
time.Sleep(100 * time.Millisecond)
250+
}
251+
}
252+
253+
// IsBusy returns the busy status of the display
254+
func (d *Device) IsBusy() bool {
255+
return d.busy.Get()
256+
}
257+
258+
// ClearBuffer sets the buffer to 0xFF (white)
259+
func (d *Device) ClearBuffer() {
260+
for i := uint32(0); i < d.bufferLength; i++ {
261+
d.buffer[i] = 0xFF
262+
}
263+
}
264+
265+
// Size returns the current size of the display.
266+
func (d *Device) Size() (w, h int16) {
267+
if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 {
268+
return d.height, d.logicalWidth
269+
}
270+
return d.logicalWidth, d.height
271+
}
272+
273+
// SetRotation changes the rotation (clock-wise) of the device
274+
func (d *Device) SetRotation(rotation Rotation) {
275+
d.rotation = rotation
276+
}
277+
278+
// xy chages the coordinates according to the rotation
279+
func (d *Device) xy(x, y int16) (int16, int16) {
280+
switch d.rotation {
281+
case NO_ROTATION:
282+
return x, y
283+
case ROTATION_90:
284+
return d.width - y - 1, x
285+
case ROTATION_180:
286+
return d.width - x - 1, d.height - y - 1
287+
case ROTATION_270:
288+
return y, d.height - x - 1
289+
}
290+
return x, y
291+
}

0 commit comments

Comments
 (0)