├── .gitignore ├── LICENSE ├── README.md ├── boot.py ├── ili9341.py ├── medium_instructions.md └── xpt2046.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2023 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Micropython Examples for ESP32 "Cheap Yellow Display" 2 | This is display and touch screen boilerplate to use with Micropython on the ["CYD"](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display). 3 | 4 | # Full instructions 5 | 6 | Full instructions to get this working graciously provided by [kf107](https://github.com/kf107) from [thinklayer.com](https://thinklair.com) in a [Medium Article here](https://kf106.medium.com/how-to-use-micropython-on-a-cyd-cheap-yellow-display-e158d5e4a2e7). 7 | 8 | A mirror of the instructions in text format in case the article goes missing are located in the repo as medium_instructions.md. 9 | 10 | # Quick Instructions at a glance 11 | 12 | Flash the CYD with Micropython ESP32 Generic. 13 | I suggest using the most current stable release, not a development release. 14 | [https://micropython.org/download/ESP32_GENERIC/](https://micropython.org/download/ESP32_GENERIC/) 15 | 16 | Clone this repo and upload the files to your board using Thonny, rshell or whatever IDE extension that can talk to the board. 17 | 18 | Reboot and try it out. Your touch screen may not report the same values, so will need altering. 19 | 20 | Libraries from [@rdagger](https://github.com/rdagger): 21 | 22 | [https://github.com/rdagger/micropython-ili9341](https://github.com/rdagger/micropython-ili9341) 23 | -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | '''Micropython ILI9341 with xpt2046 touch screen demo for CYD 2 | libraries and boilerplate altered from @rdagger ili9341 repo 3 | https://raw.githubusercontent.com/rdagger/micropython-ili9341 4 | ''' 5 | 6 | from ili9341 import Display, color565 7 | from xpt2046 import Touch 8 | from machine import idle, Pin, SPI 9 | 10 | 11 | class Demo(object): 12 | '''Touchscreen simple demo.''' 13 | CYAN = color565(0, 255, 255) 14 | PURPLE = color565(255, 0, 255) 15 | WHITE = color565(255, 255, 255) 16 | 17 | def __init__(self, display, spi2): 18 | '''Initialize box. 19 | 20 | Args: 21 | display (ILI9341): display object 22 | spi2 (SPI): SPI bus 23 | ''' 24 | self.display = display 25 | self.touch = Touch(spi2, cs=Pin(33), int_pin=Pin(36), 26 | int_handler=self.touchscreen_press) 27 | # Display initial message 28 | self.display.draw_text8x8(self.display.width // 2 - 32, 29 | self.display.height - 9, 30 | "TOUCH ME", 31 | self.WHITE, 32 | background=self.PURPLE) 33 | 34 | # A small 5x5 sprite for the dot 35 | self.dot = bytearray(b'\x00\x00\x07\xE0\xF8\x00\x07\xE0\x00\x00\x07\xE0\xF8\x00\xF8\x00\xF8\x00\x07\xE0\xF8\x00\xF8\x00\xF8\x00\xF8\x00\xF8\x00\x07\xE0\xF8\x00\xF8\x00\xF8\x00\x07\xE0\x00\x00\x07\xE0\xF8\x00\x07\xE0\x00\x00') 36 | 37 | def touchscreen_press(self, x, y): 38 | '''Process touchscreen press events.''' 39 | print("Display touched.") 40 | 41 | # Y needs to be flipped 42 | y = (self.display.height - 1) - y 43 | # Display coordinates 44 | self.display.draw_text8x8(self.display.width // 2 - 32, 45 | self.display.height - 9, 46 | "{0:03d}, {1:03d}".format(x, y), 47 | self.CYAN) 48 | # Draw dot 49 | self.display.draw_sprite(self.dot, x - 2, y - 2, 5, 5) 50 | 51 | 52 | def test(): 53 | 54 | ''' 55 | Display Pins: 56 | IO2 TFT_RS AKA: TFT_DC 57 | IO12 TFT_SDO AKA: TFT_MISO 58 | IO13 TFT_SDI AKA: TFT_MOSI 59 | IO14 TFT_SCK 60 | IO15 TFT_CS 61 | IO21 TFT_BL 62 | 63 | Touch Screen Pins: 64 | IO25 XPT2046_CLK 65 | IO32 XPT2046_MOSI 66 | IO33 XPT2046_CS 67 | IO36 XPT2046_IRQ 68 | IO39 XPT2046_MISO 69 | ''' 70 | 71 | 72 | ''' Set up the display - ili9341 73 | Baud rate of 40000000 seems about the max ''' 74 | spi1 = SPI(1, baudrate=40000000, sck=Pin(14), mosi=Pin(13)) 75 | display = Display(spi1, dc=Pin(2), cs=Pin(15), rst=Pin(0)) 76 | 77 | 78 | bl_pin = Pin(21, Pin.OUT) 79 | bl_pin.on() 80 | 81 | # Set up the touch screen digitizer - xpt2046 82 | spi2 = SPI(2, baudrate=1000000, sck=Pin(25), mosi=Pin(32), miso=Pin(39)) 83 | 84 | Demo(display, spi2) 85 | 86 | try: 87 | while True: 88 | idle() 89 | 90 | except KeyboardInterrupt: 91 | print("\nCtrl-C pressed. Cleaning up and exiting...") 92 | finally: 93 | display.cleanup() 94 | 95 | 96 | test() -------------------------------------------------------------------------------- /ili9341.py: -------------------------------------------------------------------------------- 1 | """ILI9341 LCD/Touch module.""" 2 | from time import sleep 3 | from math import cos, sin, pi, radians 4 | from sys import implementation 5 | from framebuf import FrameBuffer, RGB565 # type: ignore 6 | 7 | 8 | def color565(r, g, b): 9 | """Return RGB565 color value. 10 | 11 | Args: 12 | r (int): Red value. 13 | g (int): Green value. 14 | b (int): Blue value. 15 | """ 16 | return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 17 | 18 | 19 | class Display(object): 20 | """Serial interface for 16-bit color (5-6-5 RGB) IL9341 display. 21 | 22 | Note: All coordinates are zero based. 23 | """ 24 | 25 | # Command constants from ILI9341 datasheet 26 | NOP = const(0x00) # No-op 27 | SWRESET = const(0x01) # Software reset 28 | RDDID = const(0x04) # Read display ID info 29 | RDDST = const(0x09) # Read display status 30 | SLPIN = const(0x10) # Enter sleep mode 31 | SLPOUT = const(0x11) # Exit sleep mode 32 | PTLON = const(0x12) # Partial mode on 33 | NORON = const(0x13) # Normal display mode on 34 | RDMODE = const(0x0A) # Read display power mode 35 | RDMADCTL = const(0x0B) # Read display MADCTL 36 | RDPIXFMT = const(0x0C) # Read display pixel format 37 | RDIMGFMT = const(0x0D) # Read display image format 38 | RDSELFDIAG = const(0x0F) # Read display self-diagnostic 39 | INVOFF = const(0x20) # Display inversion off 40 | INVON = const(0x21) # Display inversion on 41 | GAMMASET = const(0x26) # Gamma set 42 | DISPLAY_OFF = const(0x28) # Display off 43 | DISPLAY_ON = const(0x29) # Display on 44 | SET_COLUMN = const(0x2A) # Column address set 45 | SET_PAGE = const(0x2B) # Page address set 46 | WRITE_RAM = const(0x2C) # Memory write 47 | READ_RAM = const(0x2E) # Memory read 48 | PTLAR = const(0x30) # Partial area 49 | VSCRDEF = const(0x33) # Vertical scrolling definition 50 | MADCTL = const(0x36) # Memory access control 51 | VSCRSADD = const(0x37) # Vertical scrolling start address 52 | PIXFMT = const(0x3A) # COLMOD: Pixel format set 53 | WRITE_DISPLAY_BRIGHTNESS = const(0x51) # Brightness hardware dependent! 54 | READ_DISPLAY_BRIGHTNESS = const(0x52) 55 | WRITE_CTRL_DISPLAY = const(0x53) 56 | READ_CTRL_DISPLAY = const(0x54) 57 | WRITE_CABC = const(0x55) # Write Content Adaptive Brightness Control 58 | READ_CABC = const(0x56) # Read Content Adaptive Brightness Control 59 | WRITE_CABC_MINIMUM = const(0x5E) # Write CABC Minimum Brightness 60 | READ_CABC_MINIMUM = const(0x5F) # Read CABC Minimum Brightness 61 | FRMCTR1 = const(0xB1) # Frame rate control (In normal mode/full colors) 62 | FRMCTR2 = const(0xB2) # Frame rate control (In idle mode/8 colors) 63 | FRMCTR3 = const(0xB3) # Frame rate control (In partial mode/full colors) 64 | INVCTR = const(0xB4) # Display inversion control 65 | DFUNCTR = const(0xB6) # Display function control 66 | PWCTR1 = const(0xC0) # Power control 1 67 | PWCTR2 = const(0xC1) # Power control 2 68 | PWCTRA = const(0xCB) # Power control A 69 | PWCTRB = const(0xCF) # Power control B 70 | VMCTR1 = const(0xC5) # VCOM control 1 71 | VMCTR2 = const(0xC7) # VCOM control 2 72 | RDID1 = const(0xDA) # Read ID 1 73 | RDID2 = const(0xDB) # Read ID 2 74 | RDID3 = const(0xDC) # Read ID 3 75 | RDID4 = const(0xDD) # Read ID 4 76 | GMCTRP1 = const(0xE0) # Positive gamma correction 77 | GMCTRN1 = const(0xE1) # Negative gamma correction 78 | DTCA = const(0xE8) # Driver timing control A 79 | DTCB = const(0xEA) # Driver timing control B 80 | POSC = const(0xED) # Power on sequence control 81 | ENABLE3G = const(0xF2) # Enable 3 gamma control 82 | PUMPRC = const(0xF7) # Pump ratio control 83 | 84 | ROTATE = { 85 | 0: 0x88, 86 | 90: 0xE8, 87 | 180: 0x48, 88 | 270: 0x28 89 | } 90 | 91 | def __init__(self, spi, cs, dc, rst, 92 | width=240, height=320, rotation=0): 93 | """Initialize OLED. 94 | 95 | Args: 96 | spi (Class Spi): SPI interface for OLED 97 | cs (Class Pin): Chip select pin 98 | dc (Class Pin): Data/Command pin 99 | rst (Class Pin): Reset pin 100 | width (Optional int): Screen width (default 240) 101 | height (Optional int): Screen height (default 320) 102 | rotation (Optional int): Rotation must be 0 default, 90. 180 or 270 103 | """ 104 | self.spi = spi 105 | self.cs = cs 106 | self.dc = dc 107 | self.rst = rst 108 | self.width = width 109 | self.height = height 110 | if rotation not in self.ROTATE.keys(): 111 | raise RuntimeError('Rotation must be 0, 90, 180 or 270.') 112 | else: 113 | self.rotation = self.ROTATE[rotation] 114 | 115 | # Initialize GPIO pins and set implementation specific methods 116 | if implementation.name == 'circuitpython': 117 | self.cs.switch_to_output(value=True) 118 | self.dc.switch_to_output(value=False) 119 | self.rst.switch_to_output(value=True) 120 | self.reset = self.reset_cpy 121 | self.write_cmd = self.write_cmd_cpy 122 | self.write_data = self.write_data_cpy 123 | else: 124 | self.cs.init(self.cs.OUT, value=1) 125 | self.dc.init(self.dc.OUT, value=0) 126 | self.rst.init(self.rst.OUT, value=1) 127 | self.reset = self.reset_mpy 128 | self.write_cmd = self.write_cmd_mpy 129 | self.write_data = self.write_data_mpy 130 | self.reset() 131 | # Send initialization commands 132 | self.write_cmd(self.SWRESET) # Software reset 133 | sleep(.1) 134 | self.write_cmd(self.PWCTRB, 0x00, 0xC1, 0x30) # Pwr ctrl B 135 | self.write_cmd(self.POSC, 0x64, 0x03, 0x12, 0x81) # Pwr on seq. ctrl 136 | self.write_cmd(self.DTCA, 0x85, 0x00, 0x78) # Driver timing ctrl A 137 | self.write_cmd(self.PWCTRA, 0x39, 0x2C, 0x00, 0x34, 0x02) # Pwr ctrl A 138 | self.write_cmd(self.PUMPRC, 0x20) # Pump ratio control 139 | self.write_cmd(self.DTCB, 0x00, 0x00) # Driver timing ctrl B 140 | self.write_cmd(self.PWCTR1, 0x23) # Pwr ctrl 1 141 | self.write_cmd(self.PWCTR2, 0x10) # Pwr ctrl 2 142 | self.write_cmd(self.VMCTR1, 0x3E, 0x28) # VCOM ctrl 1 143 | self.write_cmd(self.VMCTR2, 0x86) # VCOM ctrl 2 144 | self.write_cmd(self.MADCTL, self.rotation) # Memory access ctrl 145 | self.write_cmd(self.VSCRSADD, 0x00) # Vertical scrolling start address 146 | self.write_cmd(self.PIXFMT, 0x55) # COLMOD: Pixel format 147 | self.write_cmd(self.FRMCTR1, 0x00, 0x18) # Frame rate ctrl 148 | self.write_cmd(self.DFUNCTR, 0x08, 0x82, 0x27) 149 | self.write_cmd(self.ENABLE3G, 0x00) # Enable 3 gamma ctrl 150 | self.write_cmd(self.GAMMASET, 0x01) # Gamma curve selected 151 | self.write_cmd(self.GMCTRP1, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 152 | 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00) 153 | self.write_cmd(self.GMCTRN1, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 154 | 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F) 155 | self.write_cmd(self.SLPOUT) # Exit sleep 156 | sleep(.1) 157 | self.write_cmd(self.DISPLAY_ON) # Display on 158 | sleep(.1) 159 | self.clear() 160 | 161 | def block(self, x0, y0, x1, y1, data): 162 | """Write a block of data to display. 163 | 164 | Args: 165 | x0 (int): Starting X position. 166 | y0 (int): Starting Y position. 167 | x1 (int): Ending X position. 168 | y1 (int): Ending Y position. 169 | data (bytes): Data buffer to write. 170 | """ 171 | self.write_cmd(self.SET_COLUMN, 172 | x0 >> 8, x0 & 0xff, x1 >> 8, x1 & 0xff) 173 | self.write_cmd(self.SET_PAGE, 174 | y0 >> 8, y0 & 0xff, y1 >> 8, y1 & 0xff) 175 | self.write_cmd(self.WRITE_RAM) 176 | self.write_data(data) 177 | 178 | def cleanup(self): 179 | """Clean up resources.""" 180 | self.clear() 181 | self.display_off() 182 | self.spi.deinit() 183 | print('display off') 184 | 185 | def clear(self, color=0, hlines=8): 186 | """Clear display. 187 | 188 | Args: 189 | color (Optional int): RGB565 color value (Default: 0 = Black). 190 | hlines (Optional int): # of horizontal lines per chunk (Default: 8) 191 | Note: 192 | hlines was introduced to deal with memory allocation on some 193 | boards. Smaller values allocate less memory but take longer 194 | to execute. hlines must be a factor of the display height. 195 | For example, for a 240 pixel height, valid values for hline 196 | would be 1, 2, 4, 5, 8, 10, 16, 20, 32, 40, 64, 80, 160. 197 | Higher values may result in memory allocation errors. 198 | """ 199 | w = self.width 200 | h = self.height 201 | assert hlines > 0 and h % hlines == 0, ( 202 | "hlines must be a non-zero factor of height.") 203 | # Clear display 204 | if color: 205 | line = color.to_bytes(2, 'big') * (w * hlines) 206 | else: 207 | line = bytearray(w * 2 * hlines) 208 | for y in range(0, h, hlines): 209 | self.block(0, y, w - 1, y + hlines - 1, line) 210 | 211 | def display_off(self): 212 | """Turn display off.""" 213 | self.write_cmd(self.DISPLAY_OFF) 214 | 215 | def display_on(self): 216 | """Turn display on.""" 217 | self.write_cmd(self.DISPLAY_ON) 218 | 219 | def draw_circle(self, x0, y0, r, color): 220 | """Draw a circle. 221 | 222 | Args: 223 | x0 (int): X coordinate of center point. 224 | y0 (int): Y coordinate of center point. 225 | r (int): Radius. 226 | color (int): RGB565 color value. 227 | """ 228 | f = 1 - r 229 | dx = 1 230 | dy = -r - r 231 | x = 0 232 | y = r 233 | self.draw_pixel(x0, y0 + r, color) 234 | self.draw_pixel(x0, y0 - r, color) 235 | self.draw_pixel(x0 + r, y0, color) 236 | self.draw_pixel(x0 - r, y0, color) 237 | while x < y: 238 | if f >= 0: 239 | y -= 1 240 | dy += 2 241 | f += dy 242 | x += 1 243 | dx += 2 244 | f += dx 245 | self.draw_pixel(x0 + x, y0 + y, color) 246 | self.draw_pixel(x0 - x, y0 + y, color) 247 | self.draw_pixel(x0 + x, y0 - y, color) 248 | self.draw_pixel(x0 - x, y0 - y, color) 249 | self.draw_pixel(x0 + y, y0 + x, color) 250 | self.draw_pixel(x0 - y, y0 + x, color) 251 | self.draw_pixel(x0 + y, y0 - x, color) 252 | self.draw_pixel(x0 - y, y0 - x, color) 253 | 254 | def draw_ellipse(self, x0, y0, a, b, color): 255 | """Draw an ellipse. 256 | 257 | Args: 258 | x0, y0 (int): Coordinates of center point. 259 | a (int): Semi axis horizontal. 260 | b (int): Semi axis vertical. 261 | color (int): RGB565 color value. 262 | Note: 263 | The center point is the center of the x0,y0 pixel. 264 | Since pixels are not divisible, the axes are integer rounded 265 | up to complete on a full pixel. Therefore the major and 266 | minor axes are increased by 1. 267 | """ 268 | a2 = a * a 269 | b2 = b * b 270 | twoa2 = a2 + a2 271 | twob2 = b2 + b2 272 | x = 0 273 | y = b 274 | px = 0 275 | py = twoa2 * y 276 | # Plot initial points 277 | self.draw_pixel(x0 + x, y0 + y, color) 278 | self.draw_pixel(x0 - x, y0 + y, color) 279 | self.draw_pixel(x0 + x, y0 - y, color) 280 | self.draw_pixel(x0 - x, y0 - y, color) 281 | # Region 1 282 | p = round(b2 - (a2 * b) + (0.25 * a2)) 283 | while px < py: 284 | x += 1 285 | px += twob2 286 | if p < 0: 287 | p += b2 + px 288 | else: 289 | y -= 1 290 | py -= twoa2 291 | p += b2 + px - py 292 | self.draw_pixel(x0 + x, y0 + y, color) 293 | self.draw_pixel(x0 - x, y0 + y, color) 294 | self.draw_pixel(x0 + x, y0 - y, color) 295 | self.draw_pixel(x0 - x, y0 - y, color) 296 | # Region 2 297 | p = round(b2 * (x + 0.5) * (x + 0.5) + 298 | a2 * (y - 1) * (y - 1) - a2 * b2) 299 | while y > 0: 300 | y -= 1 301 | py -= twoa2 302 | if p > 0: 303 | p += a2 - py 304 | else: 305 | x += 1 306 | px += twob2 307 | p += a2 - py + px 308 | self.draw_pixel(x0 + x, y0 + y, color) 309 | self.draw_pixel(x0 - x, y0 + y, color) 310 | self.draw_pixel(x0 + x, y0 - y, color) 311 | self.draw_pixel(x0 - x, y0 - y, color) 312 | 313 | def draw_hline(self, x, y, w, color): 314 | """Draw a horizontal line. 315 | 316 | Args: 317 | x (int): Starting X position. 318 | y (int): Starting Y position. 319 | w (int): Width of line. 320 | color (int): RGB565 color value. 321 | """ 322 | if self.is_off_grid(x, y, x + w - 1, y): 323 | return 324 | line = color.to_bytes(2, 'big') * w 325 | self.block(x, y, x + w - 1, y, line) 326 | 327 | def draw_image(self, path, x=0, y=0, w=320, h=240): 328 | """Draw image from flash. 329 | 330 | Args: 331 | path (string): Image file path. 332 | x (int): X coordinate of image left. Default is 0. 333 | y (int): Y coordinate of image top. Default is 0. 334 | w (int): Width of image. Default is 320. 335 | h (int): Height of image. Default is 240. 336 | """ 337 | x2 = x + w - 1 338 | y2 = y + h - 1 339 | if self.is_off_grid(x, y, x2, y2): 340 | return 341 | with open(path, "rb") as f: 342 | chunk_height = 1024 // w 343 | chunk_count, remainder = divmod(h, chunk_height) 344 | chunk_size = chunk_height * w * 2 345 | chunk_y = y 346 | if chunk_count: 347 | for c in range(0, chunk_count): 348 | buf = f.read(chunk_size) 349 | self.block(x, chunk_y, 350 | x2, chunk_y + chunk_height - 1, 351 | buf) 352 | chunk_y += chunk_height 353 | if remainder: 354 | buf = f.read(remainder * w * 2) 355 | self.block(x, chunk_y, 356 | x2, chunk_y + remainder - 1, 357 | buf) 358 | 359 | def draw_letter(self, x, y, letter, font, color, background=0, 360 | landscape=False, rotate_180=False): 361 | """Draw a letter. 362 | 363 | Args: 364 | x (int): Starting X position. 365 | y (int): Starting Y position. 366 | letter (string): Letter to draw. 367 | font (XglcdFont object): Font. 368 | color (int): RGB565 color value. 369 | background (int): RGB565 background color (default: black) 370 | landscape (bool): Orientation (default: False = portrait) 371 | rotate_180 (bool): Rotate text by 180 degrees 372 | """ 373 | buf, w, h = font.get_letter(letter, color, background, landscape) 374 | if rotate_180: 375 | # Manually rotate the buffer by 180 degrees 376 | # ensure bytes pairs for each pixel retain color565 377 | new_buf = bytearray(len(buf)) 378 | num_pixels = len(buf) // 2 379 | for i in range(num_pixels): 380 | # The index for the new buffer's byte pair 381 | new_idx = (num_pixels - 1 - i) * 2 382 | # The index for the original buffer's byte pair 383 | old_idx = i * 2 384 | # Swap the pixels 385 | new_buf[new_idx], new_buf[new_idx + 1] = buf[old_idx], buf[old_idx + 1] 386 | buf = new_buf 387 | 388 | # Check for errors (Font could be missing specified letter) 389 | if w == 0: 390 | return w, h 391 | 392 | if landscape: 393 | y -= w 394 | if self.is_off_grid(x, y, x + h - 1, y + w - 1): 395 | return 0, 0 396 | self.block(x, y, 397 | x + h - 1, y + w - 1, 398 | buf) 399 | else: 400 | if self.is_off_grid(x, y, x + w - 1, y + h - 1): 401 | return 0, 0 402 | self.block(x, y, 403 | x + w - 1, y + h - 1, 404 | buf) 405 | return w, h 406 | 407 | def draw_line(self, x1, y1, x2, y2, color): 408 | """Draw a line using Bresenham's algorithm. 409 | 410 | Args: 411 | x1, y1 (int): Starting coordinates of the line 412 | x2, y2 (int): Ending coordinates of the line 413 | color (int): RGB565 color value. 414 | """ 415 | # Check for horizontal line 416 | if y1 == y2: 417 | if x1 > x2: 418 | x1, x2 = x2, x1 419 | self.draw_hline(x1, y1, x2 - x1 + 1, color) 420 | return 421 | # Check for vertical line 422 | if x1 == x2: 423 | if y1 > y2: 424 | y1, y2 = y2, y1 425 | self.draw_vline(x1, y1, y2 - y1 + 1, color) 426 | return 427 | # Confirm coordinates in boundary 428 | if self.is_off_grid(min(x1, x2), min(y1, y2), 429 | max(x1, x2), max(y1, y2)): 430 | return 431 | # Changes in x, y 432 | dx = x2 - x1 433 | dy = y2 - y1 434 | # Determine how steep the line is 435 | is_steep = abs(dy) > abs(dx) 436 | # Rotate line 437 | if is_steep: 438 | x1, y1 = y1, x1 439 | x2, y2 = y2, x2 440 | # Swap start and end points if necessary 441 | if x1 > x2: 442 | x1, x2 = x2, x1 443 | y1, y2 = y2, y1 444 | # Recalculate differentials 445 | dx = x2 - x1 446 | dy = y2 - y1 447 | # Calculate error 448 | error = dx >> 1 449 | ystep = 1 if y1 < y2 else -1 450 | y = y1 451 | for x in range(x1, x2 + 1): 452 | # Had to reverse HW ???? 453 | if not is_steep: 454 | self.draw_pixel(x, y, color) 455 | else: 456 | self.draw_pixel(y, x, color) 457 | error -= abs(dy) 458 | if error < 0: 459 | y += ystep 460 | error += dx 461 | 462 | def draw_lines(self, coords, color): 463 | """Draw multiple lines. 464 | 465 | Args: 466 | coords ([[int, int],...]): Line coordinate X, Y pairs 467 | color (int): RGB565 color value. 468 | """ 469 | # Starting point 470 | x1, y1 = coords[0] 471 | # Iterate through coordinates 472 | for i in range(1, len(coords)): 473 | x2, y2 = coords[i] 474 | self.draw_line(x1, y1, x2, y2, color) 475 | x1, y1 = x2, y2 476 | 477 | def draw_pixel(self, x, y, color): 478 | """Draw a single pixel. 479 | 480 | Args: 481 | x (int): X position. 482 | y (int): Y position. 483 | color (int): RGB565 color value. 484 | """ 485 | if self.is_off_grid(x, y, x, y): 486 | return 487 | self.block(x, y, x, y, color.to_bytes(2, 'big')) 488 | 489 | def draw_polygon(self, sides, x0, y0, r, color, rotate=0): 490 | """Draw an n-sided regular polygon. 491 | 492 | Args: 493 | sides (int): Number of polygon sides. 494 | x0, y0 (int): Coordinates of center point. 495 | r (int): Radius. 496 | color (int): RGB565 color value. 497 | rotate (Optional float): Rotation in degrees relative to origin. 498 | Note: 499 | The center point is the center of the x0,y0 pixel. 500 | Since pixels are not divisible, the radius is integer rounded 501 | up to complete on a full pixel. Therefore diameter = 2 x r + 1. 502 | """ 503 | coords = [] 504 | theta = radians(rotate) 505 | n = sides + 1 506 | for s in range(n): 507 | t = 2.0 * pi * s / sides + theta 508 | coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)]) 509 | 510 | # Cast to python float first to fix rounding errors 511 | self.draw_lines(coords, color=color) 512 | 513 | def draw_rectangle(self, x, y, w, h, color): 514 | """Draw a rectangle. 515 | 516 | Args: 517 | x (int): Starting X position. 518 | y (int): Starting Y position. 519 | w (int): Width of rectangle. 520 | h (int): Height of rectangle. 521 | color (int): RGB565 color value. 522 | """ 523 | x2 = x + w - 1 524 | y2 = y + h - 1 525 | self.draw_hline(x, y, w, color) 526 | self.draw_hline(x, y2, w, color) 527 | self.draw_vline(x, y, h, color) 528 | self.draw_vline(x2, y, h, color) 529 | 530 | def draw_sprite(self, buf, x, y, w, h): 531 | """Draw a sprite (optimized for horizontal drawing). 532 | 533 | Args: 534 | buf (bytearray): Buffer to draw. 535 | x (int): Starting X position. 536 | y (int): Starting Y position. 537 | w (int): Width of drawing. 538 | h (int): Height of drawing. 539 | """ 540 | x2 = x + w - 1 541 | y2 = y + h - 1 542 | if self.is_off_grid(x, y, x2, y2): 543 | return 544 | self.block(x, y, x2, y2, buf) 545 | 546 | def draw_text(self, x, y, text, font, color, background=0, 547 | landscape=False, rotate_180=False, spacing=1): 548 | """Draw text. 549 | 550 | Args: 551 | x (int): Starting X position 552 | y (int): Starting Y position 553 | text (string): Text to draw 554 | font (XglcdFont object): Font 555 | color (int): RGB565 color value 556 | background (int): RGB565 background color (default: black) 557 | landscape (bool): Orientation (default: False = portrait) 558 | rotate_180 (bool): Rotate text by 180 degrees 559 | spacing (int): Pixels between letters (default: 1) 560 | """ 561 | iterable_text = reversed(text) if rotate_180 else text 562 | for letter in iterable_text: 563 | # Get letter array and letter dimensions 564 | w, h = self.draw_letter(x, y, letter, font, color, background, 565 | landscape, rotate_180) 566 | # Stop on error 567 | if w == 0 or h == 0: 568 | print('Invalid width {0} or height {1}'.format(w, h)) 569 | return 570 | 571 | if landscape: 572 | # Fill in spacing 573 | if spacing: 574 | self.fill_hrect(x, y - w - spacing, h, spacing, background) 575 | # Position y for next letter 576 | y -= (w + spacing) 577 | else: 578 | # Fill in spacing 579 | if spacing: 580 | self.fill_hrect(x + w, y, spacing, h, background) 581 | # Position x for next letter 582 | x += (w + spacing) 583 | 584 | # # Fill in spacing 585 | # if spacing: 586 | # self.fill_vrect(x + w, y, spacing, h, background) 587 | # # Position x for next letter 588 | # x += w + spacing 589 | 590 | def draw_text8x8(self, x, y, text, color, background=0, 591 | rotate=0): 592 | """Draw text using built-in MicroPython 8x8 bit font. 593 | 594 | Args: 595 | x (int): Starting X position. 596 | y (int): Starting Y position. 597 | text (string): Text to draw. 598 | color (int): RGB565 color value. 599 | background (int): RGB565 background color (default: black). 600 | rotate(int): 0, 90, 180, 270 601 | """ 602 | w = len(text) * 8 603 | h = 8 604 | # Confirm coordinates in boundary 605 | if self.is_off_grid(x, y, x + 7, y + 7): 606 | return 607 | # Rearrange color 608 | r = (color & 0xF800) >> 8 609 | g = (color & 0x07E0) >> 3 610 | b = (color & 0x1F) << 3 611 | buf = bytearray(w * 16) 612 | fbuf = FrameBuffer(buf, w, h, RGB565) 613 | if background != 0: 614 | bg_r = (background & 0xF800) >> 8 615 | bg_g = (background & 0x07E0) >> 3 616 | bg_b = (background & 0x1F) << 3 617 | fbuf.fill(color565(bg_b, bg_r, bg_g)) 618 | fbuf.text(text, 0, 0, color565(b, r, g)) 619 | if rotate == 0: 620 | self.block(x, y, x + w - 1, y + (h - 1), buf) 621 | elif rotate == 90: 622 | buf2 = bytearray(w * 16) 623 | fbuf2 = FrameBuffer(buf2, h, w, RGB565) 624 | for y1 in range(h): 625 | for x1 in range(w): 626 | fbuf2.pixel(y1, x1, 627 | fbuf.pixel(x1, (h - 1) - y1)) 628 | self.block(x, y, x + (h - 1), y + w - 1, buf2) 629 | elif rotate == 180: 630 | buf2 = bytearray(w * 16) 631 | fbuf2 = FrameBuffer(buf2, w, h, RGB565) 632 | for y1 in range(h): 633 | for x1 in range(w): 634 | fbuf2.pixel(x1, y1, 635 | fbuf.pixel((w - 1) - x1, (h - 1) - y1)) 636 | self.block(x, y, x + w - 1, y + (h - 1), buf2) 637 | elif rotate == 270: 638 | buf2 = bytearray(w * 16) 639 | fbuf2 = FrameBuffer(buf2, h, w, RGB565) 640 | for y1 in range(h): 641 | for x1 in range(w): 642 | fbuf2.pixel(y1, x1, 643 | fbuf.pixel((w - 1) - x1, y1)) 644 | self.block(x, y, x + (h - 1), y + w - 1, buf2) 645 | 646 | def draw_vline(self, x, y, h, color): 647 | """Draw a vertical line. 648 | 649 | Args: 650 | x (int): Starting X position. 651 | y (int): Starting Y position. 652 | h (int): Height of line. 653 | color (int): RGB565 color value. 654 | """ 655 | # Confirm coordinates in boundary 656 | if self.is_off_grid(x, y, x, y + h - 1): 657 | return 658 | line = color.to_bytes(2, 'big') * h 659 | self.block(x, y, x, y + h - 1, line) 660 | 661 | def fill_circle(self, x0, y0, r, color): 662 | """Draw a filled circle. 663 | 664 | Args: 665 | x0 (int): X coordinate of center point. 666 | y0 (int): Y coordinate of center point. 667 | r (int): Radius. 668 | color (int): RGB565 color value. 669 | """ 670 | f = 1 - r 671 | dx = 1 672 | dy = -r - r 673 | x = 0 674 | y = r 675 | self.draw_vline(x0, y0 - r, 2 * r + 1, color) 676 | while x < y: 677 | if f >= 0: 678 | y -= 1 679 | dy += 2 680 | f += dy 681 | x += 1 682 | dx += 2 683 | f += dx 684 | self.draw_vline(x0 + x, y0 - y, 2 * y + 1, color) 685 | self.draw_vline(x0 - x, y0 - y, 2 * y + 1, color) 686 | self.draw_vline(x0 - y, y0 - x, 2 * x + 1, color) 687 | self.draw_vline(x0 + y, y0 - x, 2 * x + 1, color) 688 | 689 | def fill_ellipse(self, x0, y0, a, b, color): 690 | """Draw a filled ellipse. 691 | 692 | Args: 693 | x0, y0 (int): Coordinates of center point. 694 | a (int): Semi axis horizontal. 695 | b (int): Semi axis vertical. 696 | color (int): RGB565 color value. 697 | Note: 698 | The center point is the center of the x0,y0 pixel. 699 | Since pixels are not divisible, the axes are integer rounded 700 | up to complete on a full pixel. Therefore the major and 701 | minor axes are increased by 1. 702 | """ 703 | a2 = a * a 704 | b2 = b * b 705 | twoa2 = a2 + a2 706 | twob2 = b2 + b2 707 | x = 0 708 | y = b 709 | px = 0 710 | py = twoa2 * y 711 | # Plot initial points 712 | self.draw_line(x0, y0 - y, x0, y0 + y, color) 713 | # Region 1 714 | p = round(b2 - (a2 * b) + (0.25 * a2)) 715 | while px < py: 716 | x += 1 717 | px += twob2 718 | if p < 0: 719 | p += b2 + px 720 | else: 721 | y -= 1 722 | py -= twoa2 723 | p += b2 + px - py 724 | self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color) 725 | self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color) 726 | # Region 2 727 | p = round(b2 * (x + 0.5) * (x + 0.5) + 728 | a2 * (y - 1) * (y - 1) - a2 * b2) 729 | while y > 0: 730 | y -= 1 731 | py -= twoa2 732 | if p > 0: 733 | p += a2 - py 734 | else: 735 | x += 1 736 | px += twob2 737 | p += a2 - py + px 738 | self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color) 739 | self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color) 740 | 741 | def fill_hrect(self, x, y, w, h, color): 742 | """Draw a filled rectangle (optimized for horizontal drawing). 743 | 744 | Args: 745 | x (int): Starting X position. 746 | y (int): Starting Y position. 747 | w (int): Width of rectangle. 748 | h (int): Height of rectangle. 749 | color (int): RGB565 color value. 750 | """ 751 | if self.is_off_grid(x, y, x + w - 1, y + h - 1): 752 | return 753 | chunk_height = 1024 // w 754 | chunk_count, remainder = divmod(h, chunk_height) 755 | chunk_size = chunk_height * w 756 | chunk_y = y 757 | if chunk_count: 758 | buf = color.to_bytes(2, 'big') * chunk_size 759 | for c in range(0, chunk_count): 760 | self.block(x, chunk_y, 761 | x + w - 1, chunk_y + chunk_height - 1, 762 | buf) 763 | chunk_y += chunk_height 764 | 765 | if remainder: 766 | buf = color.to_bytes(2, 'big') * remainder * w 767 | self.block(x, chunk_y, 768 | x + w - 1, chunk_y + remainder - 1, 769 | buf) 770 | 771 | def fill_rectangle(self, x, y, w, h, color): 772 | """Draw a filled rectangle. 773 | 774 | Args: 775 | x (int): Starting X position. 776 | y (int): Starting Y position. 777 | w (int): Width of rectangle. 778 | h (int): Height of rectangle. 779 | color (int): RGB565 color value. 780 | """ 781 | if self.is_off_grid(x, y, x + w - 1, y + h - 1): 782 | return 783 | if w > h: 784 | self.fill_hrect(x, y, w, h, color) 785 | else: 786 | self.fill_vrect(x, y, w, h, color) 787 | 788 | def fill_polygon(self, sides, x0, y0, r, color, rotate=0): 789 | """Draw a filled n-sided regular polygon. 790 | 791 | Args: 792 | sides (int): Number of polygon sides. 793 | x0, y0 (int): Coordinates of center point. 794 | r (int): Radius. 795 | color (int): RGB565 color value. 796 | rotate (Optional float): Rotation in degrees relative to origin. 797 | Note: 798 | The center point is the center of the x0,y0 pixel. 799 | Since pixels are not divisible, the radius is integer rounded 800 | up to complete on a full pixel. Therefore diameter = 2 x r + 1. 801 | """ 802 | # Determine side coordinates 803 | coords = [] 804 | theta = radians(rotate) 805 | n = sides + 1 806 | for s in range(n): 807 | t = 2.0 * pi * s / sides + theta 808 | coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)]) 809 | # Starting point 810 | x1, y1 = coords[0] 811 | # Minimum Maximum X dict 812 | xdict = {y1: [x1, x1]} 813 | # Iterate through coordinates 814 | for row in coords[1:]: 815 | x2, y2 = row 816 | xprev, yprev = x2, y2 817 | # Calculate perimeter 818 | # Check for horizontal side 819 | if y1 == y2: 820 | if x1 > x2: 821 | x1, x2 = x2, x1 822 | if y1 in xdict: 823 | xdict[y1] = [min(x1, xdict[y1][0]), max(x2, xdict[y1][1])] 824 | else: 825 | xdict[y1] = [x1, x2] 826 | x1, y1 = xprev, yprev 827 | continue 828 | # Non horizontal side 829 | # Changes in x, y 830 | dx = x2 - x1 831 | dy = y2 - y1 832 | # Determine how steep the line is 833 | is_steep = abs(dy) > abs(dx) 834 | # Rotate line 835 | if is_steep: 836 | x1, y1 = y1, x1 837 | x2, y2 = y2, x2 838 | # Swap start and end points if necessary 839 | if x1 > x2: 840 | x1, x2 = x2, x1 841 | y1, y2 = y2, y1 842 | # Recalculate differentials 843 | dx = x2 - x1 844 | dy = y2 - y1 845 | # Calculate error 846 | error = dx >> 1 847 | ystep = 1 if y1 < y2 else -1 848 | y = y1 849 | # Calcualte minimum and maximum x values 850 | for x in range(x1, x2 + 1): 851 | if is_steep: 852 | if x in xdict: 853 | xdict[x] = [min(y, xdict[x][0]), max(y, xdict[x][1])] 854 | else: 855 | xdict[x] = [y, y] 856 | else: 857 | if y in xdict: 858 | xdict[y] = [min(x, xdict[y][0]), max(x, xdict[y][1])] 859 | else: 860 | xdict[y] = [x, x] 861 | error -= abs(dy) 862 | if error < 0: 863 | y += ystep 864 | error += dx 865 | x1, y1 = xprev, yprev 866 | # Fill polygon 867 | for y, x in xdict.items(): 868 | self.draw_hline(x[0], y, x[1] - x[0] + 2, color) 869 | 870 | def fill_vrect(self, x, y, w, h, color): 871 | """Draw a filled rectangle (optimized for vertical drawing). 872 | 873 | Args: 874 | x (int): Starting X position. 875 | y (int): Starting Y position. 876 | w (int): Width of rectangle. 877 | h (int): Height of rectangle. 878 | color (int): RGB565 color value. 879 | """ 880 | if self.is_off_grid(x, y, x + w - 1, y + h - 1): 881 | return 882 | chunk_width = 1024 // h 883 | chunk_count, remainder = divmod(w, chunk_width) 884 | chunk_size = chunk_width * h 885 | chunk_x = x 886 | if chunk_count: 887 | buf = color.to_bytes(2, 'big') * chunk_size 888 | for c in range(0, chunk_count): 889 | self.block(chunk_x, y, 890 | chunk_x + chunk_width - 1, y + h - 1, 891 | buf) 892 | chunk_x += chunk_width 893 | 894 | if remainder: 895 | buf = color.to_bytes(2, 'big') * remainder * h 896 | self.block(chunk_x, y, 897 | chunk_x + remainder - 1, y + h - 1, 898 | buf) 899 | 900 | def is_off_grid(self, xmin, ymin, xmax, ymax): 901 | """Check if coordinates extend past display boundaries. 902 | 903 | Args: 904 | xmin (int): Minimum horizontal pixel. 905 | ymin (int): Minimum vertical pixel. 906 | xmax (int): Maximum horizontal pixel. 907 | ymax (int): Maximum vertical pixel. 908 | Returns: 909 | boolean: False = Coordinates OK, True = Error. 910 | """ 911 | if xmin < 0: 912 | print('x-coordinate: {0} below minimum of 0.'.format(xmin)) 913 | return True 914 | if ymin < 0: 915 | print('y-coordinate: {0} below minimum of 0.'.format(ymin)) 916 | return True 917 | if xmax >= self.width: 918 | print('x-coordinate: {0} above maximum of {1}.'.format( 919 | xmax, self.width - 1)) 920 | return True 921 | if ymax >= self.height: 922 | print('y-coordinate: {0} above maximum of {1}.'.format( 923 | ymax, self.height - 1)) 924 | return True 925 | return False 926 | 927 | def load_sprite(self, path, w, h): 928 | """Load sprite image. 929 | 930 | Args: 931 | path (string): Image file path. 932 | w (int): Width of image. 933 | h (int): Height of image. 934 | Notes: 935 | w x h cannot exceed 2048 936 | """ 937 | buf_size = w * h * 2 938 | with open(path, "rb") as f: 939 | return f.read(buf_size) 940 | 941 | def reset_cpy(self): 942 | """Perform reset: Low=initialization, High=normal operation. 943 | 944 | Notes: CircuitPython implemntation 945 | """ 946 | self.rst.value = False 947 | sleep(.05) 948 | self.rst.value = True 949 | sleep(.05) 950 | 951 | def reset_mpy(self): 952 | """Perform reset: Low=initialization, High=normal operation. 953 | 954 | Notes: MicroPython implemntation 955 | """ 956 | self.rst(0) 957 | sleep(.05) 958 | self.rst(1) 959 | sleep(.05) 960 | 961 | def scroll(self, y): 962 | """Scroll display vertically. 963 | 964 | Args: 965 | y (int): Number of pixels to scroll display. 966 | """ 967 | self.write_cmd(self.VSCRSADD, y >> 8, y & 0xFF) 968 | 969 | def set_scroll(self, top, bottom): 970 | """Set the height of the top and bottom scroll margins. 971 | 972 | Args: 973 | top (int): Height of top scroll margin 974 | bottom (int): Height of bottom scroll margin 975 | """ 976 | if top + bottom <= self.height: 977 | middle = self.height - (top + bottom) 978 | print(top, middle, bottom) 979 | self.write_cmd(self.VSCRDEF, 980 | top >> 8, 981 | top & 0xFF, 982 | middle >> 8, 983 | middle & 0xFF, 984 | bottom >> 8, 985 | bottom & 0xFF) 986 | 987 | def sleep(self, enable=True): 988 | """Enters or exits sleep mode. 989 | 990 | Args: 991 | enable (bool): True (default)=Enter sleep mode, False=Exit sleep 992 | """ 993 | if enable: 994 | self.write_cmd(self.SLPIN) 995 | else: 996 | self.write_cmd(self.SLPOUT) 997 | 998 | def write_cmd_mpy(self, command, *args): 999 | """Write command to OLED (MicroPython). 1000 | 1001 | Args: 1002 | command (byte): ILI9341 command code. 1003 | *args (optional bytes): Data to transmit. 1004 | """ 1005 | self.dc(0) 1006 | self.cs(0) 1007 | self.spi.write(bytearray([command])) 1008 | self.cs(1) 1009 | # Handle any passed data 1010 | if len(args) > 0: 1011 | self.write_data(bytearray(args)) 1012 | 1013 | def write_cmd_cpy(self, command, *args): 1014 | """Write command to OLED (CircuitPython). 1015 | 1016 | Args: 1017 | command (byte): ILI9341 command code. 1018 | *args (optional bytes): Data to transmit. 1019 | """ 1020 | self.dc.value = False 1021 | self.cs.value = False 1022 | # Confirm SPI locked before writing 1023 | while not self.spi.try_lock(): 1024 | pass 1025 | self.spi.write(bytearray([command])) 1026 | self.spi.unlock() 1027 | self.cs.value = True 1028 | # Handle any passed data 1029 | if len(args) > 0: 1030 | self.write_data(bytearray(args)) 1031 | 1032 | def write_data_mpy(self, data): 1033 | """Write data to OLED (MicroPython). 1034 | 1035 | Args: 1036 | data (bytes): Data to transmit. 1037 | """ 1038 | self.dc(1) 1039 | self.cs(0) 1040 | self.spi.write(data) 1041 | self.cs(1) 1042 | 1043 | def write_data_cpy(self, data): 1044 | """Write data to OLED (CircuitPython). 1045 | 1046 | Args: 1047 | data (bytes): Data to transmit. 1048 | """ 1049 | self.dc.value = True 1050 | self.cs.value = False 1051 | # Confirm SPI locked before writing 1052 | while not self.spi.try_lock(): 1053 | pass 1054 | self.spi.write(data) 1055 | self.spi.unlock() 1056 | self.cs.value = True 1057 | -------------------------------------------------------------------------------- /medium_instructions.md: -------------------------------------------------------------------------------- 1 | ### These instructions were scraped with links, and quickly formatted to markdown. It is only preserved in case the article goes unavailable. Please reference [the article located here](https://kf106.medium.com/how-to-use-micropython-on-a-cyd-cheap-yellow-display-e158d5e4a2e7). 2 | --------- 3 | # Instructions from kf107 from thinklair.com 4 | A while back I stumbled across Witnessmenow’s Cheap Yellow Display 5 | repository, and thought that it looked interesting (and cheap at $15 or so 6 | per device). 7 | 8 | Unfortunately it didn’t cover using Micropython on the device, and I hate 9 | using C++, so I ended up with two of them sitting on my desk doing 10 | nothing. 11 | 12 | Today I finally found a repository providing the code to get Micropython 13 | working on a CYD, but the instructions were rather brief, so I though I’d 14 | document how I got it to work for anyone else in the same position as me. 15 | 16 | This guide walks you through getting everything set up and working, 17 | assuming you are using Ubuntu 22.04. If you’re using MacOS or Windows, it 18 | should help, but you’ll have to dig out the platform specific steps by 19 | searching the web yourself. 20 | 21 | So, let’s get started. 22 | 23 | ### Set up your environment 24 | 25 | We want to use a virtual environment so we don’t mess up our Python 26 | installation on our machine. 27 | ``` 28 | mkdir CYD-micropython 29 | cd CYD-micropython 30 | mkdir venv 31 | python -m venv ./venv 32 | source venv/bin/activate 33 | ``` 34 | Then we install the esptool for flashing files to the CYD, and thonnyfor 35 | uploading files after we’ve flashed the CYD with Micropython. 36 | ``` 37 | pip install esptool 38 | sudo apt-get install thonny 39 | ``` 40 | ### Obtaining the Python driver files 41 | 42 | Clone JettisOnTheNet’s example and drivers for Micropython on the CYD: 43 | ``` 44 | git clone https://github.com/JettIsOnTheNet/Micropython-Examples-for-ESP32-Cheap-Yellow-Display 45 | ``` 46 | ### Gathering port information 47 | 48 | You will need to connect your CYD to your PC using a USB-micro cable (but 49 | don’t do it quite yet). Note that the USB cable has to be a data carrying 50 | cable — many of these cables are for power only. If the next steps don’t 51 | work, your first step should be to try a new cable. 52 | 53 | Get a list of current terminal ports: 54 | ``` 55 | ls /dev/tty 56 | ``` 57 | Then plug the board in, and run the above command again. A new port will 58 | appear in the list of ports shown. It is probably /dev/ttyUSB0. That’s 59 | your board port. 60 | 61 | You can now query the board over the USB port. For example: 62 | ``` 63 | esptool.py -p /dev/ttyUSB0 flash_id 64 | ``` 65 | Remember to replace ttyUSB0with your port number. This commad will return 66 | information about your board. 67 | 68 | ### Erasing the flash memory 69 | 70 | Now we need to clear the flash memory, and that involves getting the board 71 | into flash mode. 72 | 73 | Press the RESET (EN) button (top-left most button behind the board as you 74 | look at the screen) and the BOOT button (just to the right of the RESET 75 | button: they’re both labelled on the board) at the same time, then let go 76 | of the RESET button first and the BOOT button second. 77 | 78 | Hint: with a bit of practice you will learn how to do this by rolling your 79 | finger across the two buttons from left to right. 80 | 81 | The board won’t boot up into the demo program flashed onto it in the 82 | factory. Instead, the screen will stay black, but the red LED on the back 83 | of the board will remain lit to show power is connected. 84 | 85 | Because this is the first time we’re putting Micropython on the CYD, it is 86 | best to completely erase the flash: 87 | ``` 88 | esptool.py --chip esp32 --port /dev/ttyUSB0 erase_flash 89 | ``` 90 | This time, after the message Hard resetting via RTS pin the board won’t 91 | boot up into the demo program, because it’s not there. 92 | 93 | ### Flashing with Micropython 94 | 95 | Download the Micropython image from 96 | ``` 97 | https://micropython.org/download/ESP32_GENERIC/ 98 | ``` 99 | I used the first version I saw, namely v1.22.2 (2024–02–22) .bin, which 100 | worked. 101 | 102 | Flash it to the board by putting it into flash mode, and running the 103 | esptool with the following command: 104 | ``` 105 | esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 ESP32_GENERIC-20240222-v1.22.2.bin 106 | ``` 107 | ### Running Thonny and uploading the Python scripts 108 | 109 | Thonny is a Micropython IDE that you can use to upload files to your 110 | board, as an editor, and to test your scripts before trying to run them at 111 | the board. For now, we’re just going to use it to upload the files we need 112 | from JettisOnTheNet’s repository, so we don’t see a blank screen anymore. 113 | 114 | At the command line start Thonny: 115 | ``` 116 | thonny 117 | ``` 118 | The Thonny GUI should appear. The first time you’ll be asked to pick your 119 | language and settings. I just went with the defaults. Then this screen 120 | appears: 121 | 122 | First, we will connect the board to Thonny. Select Tools > Options, and 123 | then the Interpreter tab. 124 | 125 | From the “Which kind of interpreter should Thonny use for running your 126 | code?” dropdown, select MicroPython (ESP32). 127 | 128 | This changes the view in the Interpreter pane to show you the “Port or 129 | WebREPL” dropdown, which allows you to select the USB serial port you need 130 | to use — that’s the one you found out about above, for example USB Serial 131 | @ /dev/ttyUSB0 or whatever number you found out. 132 | 133 | Then click on the OK button. 134 | 135 | Now we are going to find the files we need to upload. Selected View > 136 | Files from the menu bar to show the file selection pane. Use the file pane 137 | to navigate to the Micropython-Examples-for-ESP32-Cheap-Yellow-Display 138 | folder you cloned from JettisOnTheNet’s github repository 139 | 140 | Double click on boot.py, ili9341.pyand xpt2046.py to open each in its own 141 | editor pane. 142 | 143 | You can get rid of tab by clicking on the x next to it. 144 | 145 | Then we save each of the three files to the CYD: 146 | 147 | Go to each file tab in turn, then select the menu option File > Save as…, 148 | and select MicroPython device. Enter the correct name for each file (as it 149 | doesn’t remember it). A second pane will appear in the Files tab, with the 150 | heading MicroPython device, showing each file as you save it. 151 | 152 | When all three files are copied across, you can hit the RESET button on 153 | your CYD, and the app will run. It puts a dot on the screen where you tap 154 | it, and shows the x and y co-ordinates at the bottom of the screen. 155 | 156 | Congratulations — you’ve installed Micropython, and loaded some scripts 157 | across that run a Python program on your CYD. 158 | 159 | ### Afterword 160 | 161 | At this point I hit a problem trying to upload new edited files for the 162 | CYD at this point. The script locks the device, and even re-flashing it 163 | doesn’t get it out of the locked state. 164 | 165 | It turns out you have to click in the Shell pane and then press Ctrl+C to 166 | exit the Python program running on the CYD, which puts you into a Python 167 | REPL and allows you to run Python commands on the device from the 168 | interpreter dynamically. 169 | 170 | For example, try the following to list the files you loaded on to the 171 | device: 172 | 173 | ``` 174 | >>> import os 175 | >>> os.listdir() 176 | ['boot.py', 'ili9341.py', 'xpt2046.py'] 177 | ``` -------------------------------------------------------------------------------- /xpt2046.py: -------------------------------------------------------------------------------- 1 | """XPT2046 Touch module.""" 2 | from time import sleep 3 | 4 | 5 | class Touch(object): 6 | """Serial interface for XPT2046 Touch Screen Controller.""" 7 | 8 | # Command constants from ILI9341 datasheet 9 | GET_X = const(0b11010000) # X position 10 | GET_Y = const(0b10010000) # Y position 11 | GET_Z1 = const(0b10110000) # Z1 position 12 | GET_Z2 = const(0b11000000) # Z2 position 13 | GET_TEMP0 = const(0b10000000) # Temperature 0 14 | GET_TEMP1 = const(0b11110000) # Temperature 1 15 | GET_BATTERY = const(0b10100000) # Battery monitor 16 | GET_AUX = const(0b11100000) # Auxiliary input to ADC 17 | 18 | def __init__(self, spi, cs, int_pin=None, int_handler=None, 19 | width=240, height=320, 20 | x_min=100, x_max=1962, y_min=100, y_max=1900): 21 | """Initialize touch screen controller. 22 | 23 | Args: 24 | spi (Class Spi): SPI interface for OLED 25 | cs (Class Pin): Chip select pin 26 | int_pin (Class Pin): Touch controller interrupt pin 27 | int_handler (function): Handler for screen interrupt 28 | width (int): Width of LCD screen 29 | height (int): Height of LCD screen 30 | x_min (int): Minimum x coordinate 31 | x_max (int): Maximum x coordinate 32 | y_min (int): Minimum Y coordinate 33 | y_max (int): Maximum Y coordinate 34 | """ 35 | self.spi = spi 36 | self.cs = cs 37 | self.cs.init(self.cs.OUT, value=1) 38 | self.rx_buf = bytearray(3) # Receive buffer 39 | self.tx_buf = bytearray(3) # Transmit buffer 40 | self.width = width 41 | self.height = height 42 | # Set calibration 43 | self.x_min = x_min 44 | self.x_max = x_max 45 | self.y_min = y_min 46 | self.y_max = y_max 47 | self.x_multiplier = width / (x_max - x_min) 48 | self.x_add = x_min * -self.x_multiplier 49 | self.y_multiplier = height / (y_max - y_min) 50 | self.y_add = y_min * -self.y_multiplier 51 | 52 | if int_pin is not None: 53 | self.int_pin = int_pin 54 | self.int_pin.init(int_pin.IN) 55 | self.int_handler = int_handler 56 | self.int_locked = False 57 | int_pin.irq(trigger=int_pin.IRQ_FALLING | int_pin.IRQ_RISING, 58 | handler=self.int_press) 59 | 60 | def get_touch(self): 61 | """Take multiple samples to get accurate touch reading.""" 62 | timeout = 2 # set timeout to 2 seconds 63 | confidence = 5 64 | buff = [[0, 0] for x in range(confidence)] 65 | buf_length = confidence # Require a confidence of 5 good samples 66 | buffptr = 0 # Track current buffer position 67 | nsamples = 0 # Count samples 68 | while timeout > 0: 69 | if nsamples == buf_length: 70 | meanx = sum([c[0] for c in buff]) // buf_length 71 | meany = sum([c[1] for c in buff]) // buf_length 72 | dev = sum([(c[0] - meanx)**2 + 73 | (c[1] - meany)**2 for c in buff]) / buf_length 74 | if dev <= 50: # Deviation should be under margin of 50 75 | return self.normalize(meanx, meany) 76 | # get a new value 77 | sample = self.raw_touch() # get a touch 78 | if sample is None: 79 | nsamples = 0 # Invalidate buff 80 | else: 81 | buff[buffptr] = sample # put in buff 82 | buffptr = (buffptr + 1) % buf_length # Incr, until rollover 83 | nsamples = min(nsamples + 1, buf_length) # Incr. until max 84 | 85 | sleep(.05) 86 | timeout -= .05 87 | return None 88 | 89 | def int_press(self, pin): 90 | """Send X,Y values to passed interrupt handler.""" 91 | if not pin.value() and not self.int_locked: 92 | self.int_locked = True # Lock Interrupt 93 | buff = self.raw_touch() 94 | 95 | if buff is not None: 96 | x, y = self.normalize(*buff) 97 | self.int_handler(x, y) 98 | sleep(.1) # Debounce falling edge 99 | elif pin.value() and self.int_locked: 100 | sleep(.1) # Debounce rising edge 101 | self.int_locked = False # Unlock interrupt 102 | 103 | def normalize(self, x, y): 104 | """Normalize mean X,Y values to match LCD screen.""" 105 | x = int(self.x_multiplier * x + self.x_add) 106 | y = int(self.y_multiplier * y + self.y_add) 107 | return x, y 108 | 109 | def raw_touch(self): 110 | """Read raw X,Y touch values. 111 | 112 | Returns: 113 | tuple(int, int): X, Y 114 | """ 115 | x = self.send_command(self.GET_X) 116 | y = self.send_command(self.GET_Y) 117 | if self.x_min <= x <= self.x_max and self.y_min <= y <= self.y_max: 118 | return (x, y) 119 | else: 120 | return None 121 | 122 | def send_command(self, command): 123 | """Write command to XT2046 (MicroPython). 124 | 125 | Args: 126 | command (byte): XT2046 command code. 127 | Returns: 128 | int: 12 bit response 129 | """ 130 | self.tx_buf[0] = command 131 | self.cs(0) 132 | self.spi.write_readinto(self.tx_buf, self.rx_buf) 133 | self.cs(1) 134 | 135 | return (self.rx_buf[1] << 4) | (self.rx_buf[2] >> 4) 136 | --------------------------------------------------------------------------------