-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdisplay.py
More file actions
265 lines (239 loc) · 15.2 KB
/
display.py
File metadata and controls
265 lines (239 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
''' `FrameBuffer` based asynchronous display driver implementation for ILI9225 on RP2040/RP2350 based boards (using DMA)
Copyright (c) 2025 Harm Lammers
This display driver doesn’t include an option to change the screen orientation, because it is optimized to be used by Cybo-Drummer
(https://github.com/HLammers/cybo-drummer), which uses the display in a landscape orientation.
MIT licence:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
'''
from machine import Pin, SPI
import time
import rp2
import micropython
import asyncio
import framebuf
from singleton import singleton
DISPLAY_W = const(220) # display width
DISPLAY_H = const(176) # display height
_BUF_SIZE = const(77440) # size of the display buffer (_DISPLAY_W * _DISPLAY_H * 2)
# ILI9225 LCD Registers
_DRIVER_OUTPUT_CTRL = const(0x01) # Driver Output Control
_LCD_AC_DRIVING_CTRL = const(0x02) # LCD AC Driving Control
_ENTRY_MODE = const(0x03) # Entry Mode
_DISP_CTRL1 = const(0x07) # Display Control 1
_BLANK_PERIOD_CTRL1 = const(0x08) # Blank Period Control
_FRAME_CYCLE_CTRL = const(0x0B) # Frame Cycle Control
_INTERFACE_CTRL = const(0x0C) # Interface Control
_OSC_CTRL = const(0x0F) # OSC Control
_POWER_CTRL1 = const(0x10) # Power Control 1
_POWER_CTRL2 = const(0x11) # Power Control 2
_POWER_CTRL3 = const(0x12) # Power Control 3
_POWER_CTRL4 = const(0x13) # Power Control 4
_POWER_CTRL5 = const(0x14) # Power Control 5
_VCI_RECYCLING = const(0x15) # VCI Recycling
_RAM_ADDR_SET1 = const(0x20) # Horizontal GRAM Address Set
_RAM_ADDR_SET2 = const(0x21) # Vertical GRAM Address Set
_GRAM_DATA_REG = const(0x22) # GRAM Data Register
_GATE_SCAN_CTRL = const(0x30) # Gate Scan Control Register
_VER_SCROLL_CTRL1 = const(0x31) # Vertical Scroll Control 1 Register
_VER_SCROLL_CTRL2 = const(0x32) # Vertical Scroll Control 2 Register
_VER_SCROLL_CTRL3 = const(0x33) # Vertical Scroll Control 3 Register
_PART_DRIVING_POS1 = const(0x34) # Partial Driving Position 1 Register
_PART_DRIVING_POS2 = const(0x35) # Partial Driving Position 2 Register
_HOR_WINDOW_ADDR1 = const(0x36) # Horizontal Address Start Position
_HOR_WINDOW_ADDR2 = const(0x37) # Horizontal Address End Position
_VER_WINDOW_ADDR1 = const(0x38) # Vertical Address Start Position
_VER_WINDOW_ADDR2 = const(0x39) # Vertical Address End Position
_GAMMA_CTRL1 = const(0x50) # Gamma Control 1
_GAMMA_CTRL2 = const(0x51) # Gamma Control 2
_GAMMA_CTRL3 = const(0x52) # Gamma Control 3
_GAMMA_CTRL4 = const(0x53) # Gamma Control 4
_GAMMA_CTRL5 = const(0x54) # Gamma Control 5
_GAMMA_CTRL6 = const(0x55) # Gamma Control 6
_GAMMA_CTRL7 = const(0x56) # Gamma Control 7
_GAMMA_CTRL8 = const(0x57) # Gamma Control 8
_GAMMA_CTRL9 = const(0x58) # Gamma Control 9
_GAMMA_CTRL10 = const(0x59) # Gamma Control 10
_SET_UP_SEQUENCE = ( # (register, data_0, data_1, sleep_ms)
bytes((_POWER_CTRL1, 0x00, 0x00, 0)), bytes((_POWER_CTRL2, 0x00, 0x00, 0)), bytes((_POWER_CTRL3, 0x00, 0x00, 0)),
bytes((_POWER_CTRL4, 0x00, 0x00, 0)), bytes((_POWER_CTRL5, 0x00, 0x00, 0)), bytes((_POWER_CTRL2, 0x00, 0x18, 0)),
bytes((_POWER_CTRL3, 0x11, 0x21, 0)), bytes((_POWER_CTRL4, 0x00, 0x66, 0)), bytes((_POWER_CTRL5, 0x5F, 0x60, 0)),
bytes((_POWER_CTRL1, 0x0A, 0x00, 0)), bytes((_POWER_CTRL2, 0x10, 0x38, 50)), bytes((_DRIVER_OUTPUT_CTRL, 0x03, 0x1C, 10)),
bytes((_LCD_AC_DRIVING_CTRL, 0x01, 0x00, 10)), bytes((_ENTRY_MODE, 0x10, 0x30, 10)), bytes((_DISP_CTRL1, 0x00, 0x00, 10)),
bytes((_BLANK_PERIOD_CTRL1, 0x08, 0x08, 10)), bytes((_FRAME_CYCLE_CTRL, 0x11, 0x00, 10)), bytes((_INTERFACE_CTRL, 0x00, 0x00, 10)),
bytes((_OSC_CTRL, 0x0D, 0x01, 10)), bytes((_VCI_RECYCLING, 0x00, 0x20, 10)), bytes((_RAM_ADDR_SET1, 0x00, 0x00, 0)),
bytes((_RAM_ADDR_SET2, 0x00, 0x00, 0)), bytes((_GATE_SCAN_CTRL, 0x00, 0x00, 0)), bytes((_VER_SCROLL_CTRL1, 0x00, 0xDB, 0)),
bytes((_VER_SCROLL_CTRL2, 0x00, 0x00, 0)), bytes((_VER_SCROLL_CTRL3, 0x00, 0x00, 0)), bytes((_PART_DRIVING_POS1, 0x00, 0xDB, 0)),
bytes((_PART_DRIVING_POS2, 0x00, 0x00, 0)), bytes((_HOR_WINDOW_ADDR1, 0x00, 0xAF, 0)), bytes((_HOR_WINDOW_ADDR2, 0x00, 0x00, 0)),
bytes((_VER_WINDOW_ADDR1, 0x00, 0xDB, 0)), bytes((_VER_WINDOW_ADDR2, 0x00, 0x20, 0)), bytes((_GAMMA_CTRL1, 0x00, 0x00, 0)),
bytes((_GAMMA_CTRL2, 0x08, 0x08, 0)), bytes((_GAMMA_CTRL3, 0x08, 0x0A, 0)), bytes((_GAMMA_CTRL4, 0x00, 0x0A, 0)),
bytes((_GAMMA_CTRL5, 0x0A, 0x08, 0)), bytes((_GAMMA_CTRL6, 0x08, 0x08, 0)), bytes((_GAMMA_CTRL7, 0x00, 0x00, 0)),
bytes((_GAMMA_CTRL8, 0x0A, 0x00, 0)), bytes((_GAMMA_CTRL9, 0x07, 0x10, 0)), bytes((_GAMMA_CTRL10, 0x07, 0x10, 0)),
bytes((_DISP_CTRL1, 0x00, 0x12, 50)), bytes((_DISP_CTRL1, 0x10, 0x17, 0)))
# Global variables (faster than class variables)
_g_spi = None
_g_rs = None
_g_led = None
_g_dma = None
@singleton
class Display(framebuf.FrameBuffer):
''' `FrameBuffer` based asynchronous display driver implementation for ILI9225 on RP2040/RP2350 based boards (using DMA)
This class inherits all `framebuf.FrameBuffer` methods
Args:
spi_id (int): ID of the SPI controller to be used (0: SPI0, 1: SPI1)
baudrate (int): SCK clock rage (Hz)
rs_pin (machine.Pin): Pin object for the RS (register selection) pin
rst_pin (machine.Pin): Pin object for the RST (reset) pin
led_pin (machine.Pin): Pin object for the LED (backlight) pin
sck (machine.Pin | None, optional): Pin object for the CLK (serial clock) pin; defaults to `None` (18 for SPI0, 10 for SPI1)
mosi (machine.Pin | None, optional): Pin object for the MOSI (master output slave input) / SDI pin; defaults to `None` (19 for SPI0,
11 for SPI1)
miso (machine.Pin | None, optional): Pin object for the MISO (master input slave output) pin; defaults to `None` (16 for SPI0,
8 for SPI1)
rp2350 (bool): `True` to set up for RP2350 based board, `False` for RP2040 based board; defaults to `True` (RP2350)
Important note:
* This display driver assumes that the CS (chip select) pin is hard-wired to +3.3V
* SPI0 can only be mapped to GPIO 2/3/0, 6/7/4 or 18/19/16; SPI1 can only be mapped to GPIO 10/11/8 or 14/15/12
'''
def __init__(self, spi_id: int, baudrate: int, rs_pin: Pin, rst_pin: Pin, led_pin: Pin, rp2350: bool = True) -> None:
global _g_spi, _g_rs, _g_led, _g_dma
_g_spi = SPI(spi_id, baudrate=baudrate)
_g_rs = rs_pin
self._rst = rst_pin
_g_led = led_pin
led_pin.off()
_g_dma = (_dma := rp2.DMA())
_dma.irq(self._cb_dma, True)
if spi_id == 0:
self.treq_sel = 24 if rp2350 else 16 # TX DREQ number
self.tx_fifo_addr = 0x40080008 if rp2350 else 0x4003C008
else:
self.treq_sel = 26 if rp2350 else 18 # TX DREQ number
self.tx_fifo_addr = 0x40088008 if rp2350 else 0x40040008
self._dma_flag = (_dma_flag := asyncio.ThreadSafeFlag())
_dma_flag.set()
self.dma_done = True
rst_pin.init(rst_pin.OUT, value=0)
rs_pin.init(rs_pin.OUT, value=0)
self._byte_buf = (byte_buf := memoryview(bytearray(_BUF_SIZE)))
super().__init__(byte_buf, DISPLAY_W, DISPLAY_H, (RGB565 := framebuf.RGB565))
# Reset screen
_sleep_ms = time.sleep_ms
rst_pin.high(); _sleep_ms(1); rst_pin.low(); _sleep_ms(10); rst_pin.high(); _sleep_ms(50)
self._setup()
self._palette = framebuf.FrameBuffer(bytearray(4), 2, 1, RGB565)
@micropython.viper
def on(self):
''' Turn display on (including backlight) '''
reg_buf = bytearray(2); reg_buf_ptr = ptr8(reg_buf) # pyright: ignore[reportUndefinedVariable]
data_buf = bytearray(2); data_buf_ptr = ptr8(reg_buf) # pyright: ignore[reportUndefinedVariable]
_spi = _g_spi; _write = _spi.write # pyright: ignore[reportOptionalMemberAccess]
_dc = _g_rs; _dc_low = _dc.low; _dc_high = _dc.high # pyright: ignore[reportOptionalMemberAccess]
_sleep_ms = time.sleep_ms
reg_buf_ptr[1] = 0xFF
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
reg_buf_ptr[1] = _POWER_CTRL1
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
_sleep_ms(50)
reg_buf_ptr[1] = _DISP_CTRL1; data_buf_ptr[0] = 0x10; data_buf_ptr[1] = 0x17
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
_sleep_ms(200)
_backlight = _g_led; _backlight.on() # pyright: ignore[reportOptionalMemberAccess]
@micropython.viper
def off(self):
''' Turn display off (including backlight) '''
reg_buf = bytearray(2); reg_buf_ptr = ptr8(reg_buf) # pyright: ignore[reportUndefinedVariable]
data_buf = bytearray(2); data_buf_ptr = ptr8(reg_buf) # pyright: ignore[reportUndefinedVariable]
_spi = _g_spi; _write = _spi.write # pyright: ignore[reportOptionalMemberAccess]
_dc = _g_rs; _dc_low = _dc.low; _dc_high = _dc.high # pyright: ignore[reportOptionalMemberAccess]
_sleep_ms = time.sleep_ms
reg_buf_ptr[1] = 0xFF
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
_backlight = _g_led; _backlight.off() # pyright: ignore[reportOptionalMemberAccess]
reg_buf_ptr[1] = _POWER_CTRL1
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
_sleep_ms(50)
reg_buf_ptr[1] = _DISP_CTRL1; data_buf_ptr[0] = 0x00; data_buf_ptr[1] = 0x03
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
_sleep_ms(200)
async def async_show(self) -> None:
'''`asyncio` awaitable to flush display buffer to screen asynchronously '''
_dma = _g_dma; ctrl = _dma.pack_ctrl(size=0, inc_read=True, inc_write=False, treq_sel=self.treq_sel, irq_quiet=False) # pyright: ignore[reportOptionalMemberAccess]
self._set_window(0, 0, DISPLAY_W - 1, DISPLAY_H - 1)
_dc = _g_rs; _dc.high() # pyright: ignore[reportOptionalMemberAccess]
tx_fifo_addr = self.tx_fifo_addr
_dma.config(read=self._byte_buf, write=tx_fifo_addr, count=DISPLAY_W * DISPLAY_H * 2, ctrl=ctrl, trigger=True) # pyright: ignore[reportOptionalMemberAccess]
await self._dma_flag.wait()
_backlight = _g_led; _backlight.on() # pyright: ignore[reportOptionalMemberAccess]
def show(self) -> None:
''' Flush display buffer to screen (blocks until completed) '''
_dma = _g_dma; ctrl = _dma.pack_ctrl(size=0, inc_read=True, inc_write=False, treq_sel=self.treq_sel, irq_quiet=False) # pyright: ignore[reportOptionalMemberAccess]
self._set_window(0, 0, DISPLAY_W - 1, DISPLAY_H - 1)
_dc = _g_rs; _dc.high() # pyright: ignore[reportOptionalMemberAccess]
tx_fifo_addr = self.tx_fifo_addr
_dma.config(read=self._byte_buf, write=tx_fifo_addr, count=DISPLAY_W * DISPLAY_H * 2, ctrl=ctrl, trigger=True) # pyright: ignore[reportOptionalMemberAccess]
while not self.dma_done:
pass
_backlight = _g_led; _backlight.on() # pyright: ignore[reportOptionalMemberAccess]
def deinit(self) -> None:
''' Turn screen off, close DMA and deinitialize SPI controller '''
self.off()
if (_dma := _g_dma) is not None: _dma.close()
if (_spi := _g_spi) is not None: _spi.deinit()
@micropython.viper
def _setup(self):
''' screen set up routine '''
_write = _g_spi.write # pyright: ignore[reportOptionalMemberAccess]
_dc_low = _g_rs.low # pyright: ignore[reportOptionalMemberAccess]
_dc_high = _g_rs.high # pyright: ignore[reportOptionalMemberAccess]
_sleep_ms = time.sleep_ms
reg_buf = bytearray(2)
reg_buf_ptr = ptr8(reg_buf) # pyright: ignore[reportUndefinedVariable]
data_buf = bytearray(2)
data_buf_ptr = ptr8(data_buf) # pyright: ignore[reportUndefinedVariable]
for step in _SET_UP_SEQUENCE:
step_ptr = ptr8(step) # pyright: ignore[reportUndefinedVariable]
reg_buf_ptr[1] = step_ptr[0]; data_buf_ptr[0] = step_ptr[1]; data_buf_ptr[1] = step_ptr[2]
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
if (sleep := step_ptr[3]) > 0: _sleep_ms(sleep)
@micropython.viper
def _set_window(self, x0: int, y0: int, x1: int, y1: int):
''' Set draw window
Args:
x0 (int): Top-left x position
y0 (int): Top-left y position
x1 (int): Bottom-right x position
y1 (int): Bottom-right y position
'''
_spi = _g_spi; _write = _spi.write # pyright: ignore[reportOptionalMemberAccess]
_dc = _g_rs; _dc_low = _dc.low; _dc_high = _dc.high # pyright: ignore[reportOptionalMemberAccess]
reg_buf = bytearray(2); reg_buf_ptr = ptr8(reg_buf) # pyright: ignore[reportUndefinedVariable]
data_buf = bytearray(2); data_buf_ptr = ptr8(data_buf) # pyright: ignore[reportUndefinedVariable]
reg_buf_ptr[1] = _ENTRY_MODE; data_buf_ptr[0] = 0x10; data_buf_ptr[1] = 0x38
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
reg_buf_ptr[1] = _HOR_WINDOW_ADDR1; data_buf_ptr[0] = 0; data_buf_ptr[1] = y1
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
reg_buf_ptr[1] = _HOR_WINDOW_ADDR2; data_buf_ptr[1] = y0
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
reg_buf_ptr[1] = _VER_WINDOW_ADDR1; data_buf_ptr[1] = x1
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
reg_buf_ptr[1] = _VER_WINDOW_ADDR2; data_buf_ptr[1] = x0
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
reg_buf_ptr[1] = _RAM_ADDR_SET1; data_buf_ptr[1] = y0
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
reg_buf_ptr[1] = _RAM_ADDR_SET2; data_buf_ptr[1] = x0
_dc_low(); _write(reg_buf); _dc_high(); _write(data_buf)
reg_buf_ptr[1] = _GRAM_DATA_REG; _dc_low(); _write(reg_buf)
@micropython.viper
def _cb_dma(self, _):
''' Callback to handle DMA interrupt: set write complete flag '''
_dma_flag = self._dma_flag
_dma_flag.set()
self.dma_done = True