├── README.md ├── ST7735.py ├── examples └── simple_text.py ├── logo.png ├── main.py └── sd.py /README.md: -------------------------------------------------------------------------------- 1 | # Spryg 2 | ![Spryg Logo](logo.png) 3 | 4 | A library for the [Sprig](https://sprig.hackclub.com/) game console by HackClub to write games using MicroPython. 5 | 6 | ## Installation 7 | It's not too difficult to get set up. Being that I'm not particularly great at writing instructions, it may be good to have experience with Picos and MicroPython first. 8 | ### Setting up MicroPython 9 | - [Download Latest MicroPython](https://micropython.org/download/rp2-pico/rp2-pico-latest.uf2) 10 | - Hold down BOOTSEL on your Pico (the big white button) and plug it in to your computer 11 | - Move the downloaded MicroPython file into the Pico when it's mounted as a flash device 12 | ### Setting up Spryg 13 | - Go to the latest release on GitHub and download the source code `ST7735.py` (the library for the screen by GuyCarver), and `main.py` (the actual library itself). Extract that zip while you're at it. 14 | - Open your Pico in a program where you can access the MicroPython filesystem. I recommend [Thonny](https://projects.raspberrypi.org/en/projects/getting-started-with-the-pico/2) as it gets the job done. 15 | - Move `main.py`, `ST7735.py`, and optionally `sd.py` to the Pico 16 | 17 | You now have Spryg installed on your Pico. When you turn on your Sprig, it will tell you there is no game loaded. You must install one. The way to do this is to put a file called `game.py` on your Pico. The library will call the function `run` in your game, and it must accept one argument which is the Spryg library class. 18 | You can copy a game from the examples folder if you wish, and move it over to the Pico. Make sure its name is `game.py`, otherwise Spryg won't find it. 19 | 20 | ## Docs 21 | 22 | The documentation is in the Wiki tab on Github. 23 | 24 | ## Troubleshooting 25 | 26 | #### The speaker starts screaming 27 | 28 | This will happen when the speaker is not properly uninitialized. It can be caused by an unrecoverable crash or other bug. Simply restart your console. 29 | 30 | #### Another issue 31 | 32 | If you have an issue that was *somehow* not listed in this one-problem troubleshooting document, feel free to go to the Issues tab in Github and report a bug. 33 | 34 | ## It doesn't have `INSERT FEATURE` 35 | 36 | Ask for it to be added in the Issues tab on Github, or do it yourself with a pull request. I'm likely too lazy to do it myself. 37 | -------------------------------------------------------------------------------- /ST7735.py: -------------------------------------------------------------------------------- 1 | #driver for Sainsmart 1.8" TFT display ST7735 2 | #Translated by Guy Carver from the ST7735 sample code. 3 | #Modified for micropython-esp32 by boochow 4 | 5 | import machine 6 | import time 7 | from math import sqrt 8 | 9 | #TFTRotations and TFTRGB are bits to set 10 | # on MADCTL to control display rotation/color layout 11 | #Looking at display with pins on top. 12 | #00 = upper left printing right 13 | #10 = does nothing (MADCTL_ML) 14 | #20 = upper left printing down (backwards) (Vertical flip) 15 | #40 = upper right printing left (backwards) (X Flip) 16 | #80 = lower left printing right (backwards) (Y Flip) 17 | #04 = (MADCTL_MH) 18 | 19 | #60 = 90 right rotation 20 | #C0 = 180 right rotation 21 | #A0 = 270 right rotation 22 | TFTRotations = [0x00, 0x60, 0xC0, 0xA0] 23 | TFTBGR = 0x08 #When set color is bgr else rgb. 24 | TFTRGB = 0x00 25 | 26 | #@micropython.native 27 | def clamp( aValue, aMin, aMax ) : 28 | return max(aMin, min(aMax, aValue)) 29 | 30 | #@micropython.native 31 | def TFTColor( aR, aG, aB ) : 32 | '''Create a 16 bit rgb value from the given R,G,B from 0-255. 33 | This assumes rgb 565 layout and will be incorrect for bgr.''' 34 | return ((aR & 0xF8) << 8) | ((aG & 0xFC) << 3) | (aB >> 3) 35 | 36 | ScreenSize = (128, 160) 37 | 38 | class TFT(object) : 39 | """Sainsmart TFT 7735 display driver.""" 40 | 41 | NOP = 0x0 42 | SWRESET = 0x01 43 | RDDID = 0x04 44 | RDDST = 0x09 45 | 46 | SLPIN = 0x10 47 | SLPOUT = 0x11 48 | PTLON = 0x12 49 | NORON = 0x13 50 | 51 | INVOFF = 0x20 52 | INVON = 0x21 53 | DISPOFF = 0x28 54 | DISPON = 0x29 55 | CASET = 0x2A 56 | RASET = 0x2B 57 | RAMWR = 0x2C 58 | RAMRD = 0x2E 59 | 60 | VSCRDEF = 0x33 61 | VSCSAD = 0x37 62 | 63 | COLMOD = 0x3A 64 | MADCTL = 0x36 65 | 66 | FRMCTR1 = 0xB1 67 | FRMCTR2 = 0xB2 68 | FRMCTR3 = 0xB3 69 | INVCTR = 0xB4 70 | DISSET5 = 0xB6 71 | 72 | PWCTR1 = 0xC0 73 | PWCTR2 = 0xC1 74 | PWCTR3 = 0xC2 75 | PWCTR4 = 0xC3 76 | PWCTR5 = 0xC4 77 | VMCTR1 = 0xC5 78 | 79 | RDID1 = 0xDA 80 | RDID2 = 0xDB 81 | RDID3 = 0xDC 82 | RDID4 = 0xDD 83 | 84 | PWCTR6 = 0xFC 85 | 86 | GMCTRP1 = 0xE0 87 | GMCTRN1 = 0xE1 88 | 89 | BLACK = 0 90 | 91 | @staticmethod 92 | def color( aR, aG, aB ) : 93 | '''Create a 565 rgb TFTColor value''' 94 | return TFTColor(aR, aG, aB) 95 | 96 | def __init__( self, spi, aDC, aReset, aCS) : 97 | """aLoc SPI pin location is either 1 for 'X' or 2 for 'Y'. 98 | aDC is the DC pin and aReset is the reset pin.""" 99 | self._size = ScreenSize 100 | self._offset = bytearray([0,0]) 101 | self.rotate = 0 #Vertical with top toward pins. 102 | self._rgb = True #color order of rgb. 103 | self.tfa = 0 #top fixed area 104 | self.bfa = 0 #bottom fixed area 105 | self.dc = machine.Pin(aDC, machine.Pin.OUT, machine.Pin.PULL_DOWN) 106 | self.reset = machine.Pin(aReset, machine.Pin.OUT, machine.Pin.PULL_DOWN) 107 | self.cs = machine.Pin(aCS, machine.Pin.OUT, machine.Pin.PULL_DOWN) 108 | self.cs(1) 109 | self.spi = spi 110 | self.colorData = bytearray(2) 111 | self.windowLocData = bytearray(4) 112 | 113 | def size( self ) : 114 | return self._size 115 | 116 | # @micropython.native 117 | def on( self, aTF = True ) : 118 | '''Turn display on or off.''' 119 | self._writecommand(TFT.DISPON if aTF else TFT.DISPOFF) 120 | 121 | # @micropython.native 122 | def invertcolor( self, aBool ) : 123 | '''Invert the color data IE: Black = White.''' 124 | self._writecommand(TFT.INVON if aBool else TFT.INVOFF) 125 | 126 | # @micropython.native 127 | def rgb( self, aTF = True ) : 128 | '''True = rgb else bgr''' 129 | self._rgb = aTF 130 | self._setMADCTL() 131 | 132 | # @micropython.native 133 | def rotation( self, aRot ) : 134 | '''0 - 3. Starts vertical with top toward pins and rotates 90 deg 135 | clockwise each step.''' 136 | if (0 <= aRot < 4): 137 | rotchange = self.rotate ^ aRot 138 | self.rotate = aRot 139 | #If switching from vertical to horizontal swap x,y 140 | # (indicated by bit 0 changing). 141 | if (rotchange & 1): 142 | self._size =(self._size[1], self._size[0]) 143 | self._setMADCTL() 144 | 145 | 146 | # @micropython.native 147 | def pixel( self, aPos, aColor ) : 148 | '''Draw a pixel at the given position''' 149 | if 0 <= aPos[0] < self._size[0] and 0 <= aPos[1] < self._size[1]: 150 | self._setwindowpoint(aPos) 151 | self._pushcolor(aColor) 152 | 153 | # @micropython.native 154 | def text( self, aPos, aString, aColor, aFont, aSize = 1, nowrap = False ) : 155 | '''Draw a text at the given position. If the string reaches the end of the 156 | display it is wrapped to aPos[0] on the next line. aSize may be an integer 157 | which will size the font uniformly on w,h or a or any type that may be 158 | indexed with [0] or [1].''' 159 | 160 | if aFont == None: 161 | return 162 | 163 | #Make a size either from single value or 2 elements. 164 | if (type(aSize) == int) or (type(aSize) == float): 165 | wh = (aSize, aSize) 166 | else: 167 | wh = aSize 168 | 169 | px, py = aPos 170 | width = wh[0] * aFont["Width"] + 1 171 | for c in aString: 172 | self.char((px, py), c, aColor, aFont, wh) 173 | px += width 174 | #We check > rather than >= to let the right (blank) edge of the 175 | # character print off the right of the screen. 176 | if px + width > self._size[0]: 177 | if nowrap: 178 | break 179 | else: 180 | py += aFont["Height"] * wh[1] + 1 181 | px = aPos[0] 182 | 183 | # @micropython.native 184 | def char( self, aPos, aChar, aColor, aFont, aSizes ) : 185 | '''Draw a character at the given position using the given font and color. 186 | aSizes is a tuple with x, y as integer scales indicating the 187 | # of pixels to draw for each pixel in the character.''' 188 | 189 | if aFont == None: 190 | return 191 | 192 | startchar = aFont['Start'] 193 | endchar = aFont['End'] 194 | 195 | ci = ord(aChar) 196 | if (startchar <= ci <= endchar): 197 | fontw = aFont['Width'] 198 | fonth = aFont['Height'] 199 | ci = (ci - startchar) * fontw 200 | 201 | charA = aFont["Data"][ci:ci + fontw] 202 | px = aPos[0] 203 | if aSizes[0] <= 1 and aSizes[1] <= 1 : 204 | buf = bytearray(2 * fonth * fontw) 205 | for q in range(fontw) : 206 | c = charA[q] 207 | for r in range(fonth) : 208 | if c & 0x01 : 209 | pos = 2 * (r * fontw + q) 210 | buf[pos] = aColor >> 8 211 | buf[pos + 1] = aColor & 0xff 212 | c >>= 1 213 | self.image(aPos[0], aPos[1], aPos[0] + fontw - 1, aPos[1] + fonth - 1, buf) 214 | else: 215 | for c in charA : 216 | py = aPos[1] 217 | for r in range(fonth) : 218 | if c & 0x01 : 219 | self.fillrect((px, py), aSizes, aColor) 220 | py += aSizes[1] 221 | c >>= 1 222 | px += aSizes[0] 223 | 224 | # @micropython.native 225 | def line( self, aStart, aEnd, aColor ) : 226 | '''Draws a line from aStart to aEnd in the given color. Vertical or horizontal 227 | lines are forwarded to vline and hline.''' 228 | if aStart[0] == aEnd[0]: 229 | #Make sure we use the smallest y. 230 | pnt = aEnd if (aEnd[1] < aStart[1]) else aStart 231 | self.vline(pnt, abs(aEnd[1] - aStart[1]) + 1, aColor) 232 | elif aStart[1] == aEnd[1]: 233 | #Make sure we use the smallest x. 234 | pnt = aEnd if aEnd[0] < aStart[0] else aStart 235 | self.hline(pnt, abs(aEnd[0] - aStart[0]) + 1, aColor) 236 | else: 237 | px, py = aStart 238 | ex, ey = aEnd 239 | dx = ex - px 240 | dy = ey - py 241 | inx = 1 if dx > 0 else -1 242 | iny = 1 if dy > 0 else -1 243 | 244 | dx = abs(dx) 245 | dy = abs(dy) 246 | if (dx >= dy): 247 | dy <<= 1 248 | e = dy - dx 249 | dx <<= 1 250 | while (px != ex): 251 | self.pixel((px, py), aColor) 252 | if (e >= 0): 253 | py += iny 254 | e -= dx 255 | e += dy 256 | px += inx 257 | else: 258 | dx <<= 1 259 | e = dx - dy 260 | dy <<= 1 261 | while (py != ey): 262 | self.pixel((px, py), aColor) 263 | if (e >= 0): 264 | px += inx 265 | e -= dy 266 | e += dx 267 | py += iny 268 | 269 | # @micropython.native 270 | def vline( self, aStart, aLen, aColor ) : 271 | '''Draw a vertical line from aStart for aLen. aLen may be negative.''' 272 | start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1])) 273 | stop = (start[0], clamp(start[1] + aLen, 0, self._size[1])) 274 | #Make sure smallest y 1st. 275 | if (stop[1] < start[1]): 276 | start, stop = stop, start 277 | self._setwindowloc(start, stop) 278 | self._setColor(aColor) 279 | self._draw(aLen) 280 | 281 | # @micropython.native 282 | def hline( self, aStart, aLen, aColor ) : 283 | '''Draw a horizontal line from aStart for aLen. aLen may be negative.''' 284 | start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1])) 285 | stop = (clamp(start[0] + aLen, 0, self._size[0]), start[1]) 286 | #Make sure smallest x 1st. 287 | if (stop[0] < start[0]): 288 | start, stop = stop, start 289 | self._setwindowloc(start, stop) 290 | self._setColor(aColor) 291 | self._draw(aLen) 292 | 293 | # @micropython.native 294 | def rect( self, aStart, aSize, aColor ) : 295 | '''Draw a hollow rectangle. aStart is the smallest coordinate corner 296 | and aSize is a tuple indicating width, height.''' 297 | self.hline(aStart, aSize[0], aColor) 298 | self.hline((aStart[0], aStart[1] + aSize[1] - 1), aSize[0], aColor) 299 | self.vline(aStart, aSize[1], aColor) 300 | self.vline((aStart[0] + aSize[0] - 1, aStart[1]), aSize[1], aColor) 301 | 302 | # @micropython.native 303 | def fillrect( self, aStart, aSize, aColor ) : 304 | '''Draw a filled rectangle. aStart is the smallest coordinate corner 305 | and aSize is a tuple indicating width, height.''' 306 | start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1])) 307 | end = (clamp(start[0] + aSize[0] - 1, 0, self._size[0]), clamp(start[1] + aSize[1] - 1, 0, self._size[1])) 308 | 309 | if (end[0] < start[0]): 310 | tmp = end[0] 311 | end = (start[0], end[1]) 312 | start = (tmp, start[1]) 313 | if (end[1] < start[1]): 314 | tmp = end[1] 315 | end = (end[0], start[1]) 316 | start = (start[0], tmp) 317 | 318 | self._setwindowloc(start, end) 319 | numPixels = (end[0] - start[0] + 1) * (end[1] - start[1] + 1) 320 | self._setColor(aColor) 321 | self._draw(numPixels) 322 | 323 | # @micropython.native 324 | def circle( self, aPos, aRadius, aColor ) : 325 | '''Draw a hollow circle with the given radius and color with aPos as center.''' 326 | self.colorData[0] = aColor >> 8 327 | self.colorData[1] = aColor 328 | xend = int(0.7071 * aRadius) + 1 329 | rsq = aRadius * aRadius 330 | for x in range(xend) : 331 | y = int(sqrt(rsq - x * x)) 332 | xp = aPos[0] + x 333 | yp = aPos[1] + y 334 | xn = aPos[0] - x 335 | yn = aPos[1] - y 336 | xyp = aPos[0] + y 337 | yxp = aPos[1] + x 338 | xyn = aPos[0] - y 339 | yxn = aPos[1] - x 340 | 341 | self._setwindowpoint((xp, yp)) 342 | self._writedata(self.colorData) 343 | self._setwindowpoint((xp, yn)) 344 | self._writedata(self.colorData) 345 | self._setwindowpoint((xn, yp)) 346 | self._writedata(self.colorData) 347 | self._setwindowpoint((xn, yn)) 348 | self._writedata(self.colorData) 349 | self._setwindowpoint((xyp, yxp)) 350 | self._writedata(self.colorData) 351 | self._setwindowpoint((xyp, yxn)) 352 | self._writedata(self.colorData) 353 | self._setwindowpoint((xyn, yxp)) 354 | self._writedata(self.colorData) 355 | self._setwindowpoint((xyn, yxn)) 356 | self._writedata(self.colorData) 357 | 358 | # @micropython.native 359 | def fillcircle( self, aPos, aRadius, aColor ) : 360 | '''Draw a filled circle with given radius and color with aPos as center''' 361 | rsq = aRadius * aRadius 362 | for x in range(aRadius) : 363 | y = int(sqrt(rsq - x * x)) 364 | y0 = aPos[1] - y 365 | ey = y0 + y * 2 366 | y0 = clamp(y0, 0, self._size[1]) 367 | ln = abs(ey - y0) + 1; 368 | 369 | self.vline((aPos[0] + x, y0), ln, aColor) 370 | self.vline((aPos[0] - x, y0), ln, aColor) 371 | 372 | def fill( self, aColor = BLACK ) : 373 | '''Fill screen with the given color.''' 374 | self.fillrect((0, 0), self._size, aColor) 375 | 376 | def image( self, x0, y0, x1, y1, data ) : 377 | self._setwindowloc((x0, y0), (x1, y1)) 378 | self._writedata(data) 379 | 380 | def setvscroll(self, tfa, bfa) : 381 | ''' set vertical scroll area ''' 382 | self._writecommand(TFT.VSCRDEF) 383 | data2 = bytearray([0, tfa]) 384 | self._writedata(data2) 385 | data2[1] = 162 - tfa - bfa 386 | self._writedata(data2) 387 | data2[1] = bfa 388 | self._writedata(data2) 389 | self.tfa = tfa 390 | self.bfa = bfa 391 | 392 | def vscroll(self, value) : 393 | a = value + self.tfa 394 | if (a + self.bfa > 162) : 395 | a = 162 - self.bfa 396 | self._vscrolladdr(a) 397 | 398 | def _vscrolladdr(self, addr) : 399 | self._writecommand(TFT.VSCSAD) 400 | data2 = bytearray([addr >> 8, addr & 0xff]) 401 | self._writedata(data2) 402 | 403 | # @micropython.native 404 | def _setColor( self, aColor ) : 405 | self.colorData[0] = aColor >> 8 406 | self.colorData[1] = aColor 407 | self.buf = bytes(self.colorData) * 32 408 | 409 | # @micropython.native 410 | def _draw( self, aPixels ) : 411 | '''Send given color to the device aPixels times.''' 412 | 413 | 414 | self.dc(1) 415 | self.cs(0) 416 | for i in range(aPixels//32): 417 | self.spi.write(self.buf) 418 | rest = (int(aPixels) % 32) 419 | if rest > 0: 420 | buf2 = bytes(self.colorData) * rest 421 | self.spi.write(buf2) 422 | self.cs(1) 423 | 424 | # @micropython.native 425 | def _setwindowpoint( self, aPos ) : 426 | '''Set a single point for drawing a color to.''' 427 | x = self._offset[0] + int(aPos[0]) 428 | y = self._offset[1] + int(aPos[1]) 429 | self._writecommand(TFT.CASET) #Column address set. 430 | self.windowLocData[0] = self._offset[0] 431 | self.windowLocData[1] = x 432 | self.windowLocData[2] = self._offset[0] 433 | self.windowLocData[3] = x 434 | self._writedata(self.windowLocData) 435 | 436 | self._writecommand(TFT.RASET) #Row address set. 437 | self.windowLocData[0] = self._offset[1] 438 | self.windowLocData[1] = y 439 | self.windowLocData[2] = self._offset[1] 440 | self.windowLocData[3] = y 441 | self._writedata(self.windowLocData) 442 | self._writecommand(TFT.RAMWR) #Write to RAM. 443 | 444 | # @micropython.native 445 | def _setwindowloc( self, aPos0, aPos1 ) : 446 | '''Set a rectangular area for drawing a color to.''' 447 | self._writecommand(TFT.CASET) #Column address set. 448 | self.windowLocData[0] = self._offset[0] 449 | self.windowLocData[1] = self._offset[0] + int(aPos0[0]) 450 | self.windowLocData[2] = self._offset[0] 451 | self.windowLocData[3] = self._offset[0] + int(aPos1[0]) 452 | self._writedata(self.windowLocData) 453 | 454 | self._writecommand(TFT.RASET) #Row address set. 455 | self.windowLocData[0] = self._offset[1] 456 | self.windowLocData[1] = self._offset[1] + int(aPos0[1]) 457 | self.windowLocData[2] = self._offset[1] 458 | self.windowLocData[3] = self._offset[1] + int(aPos1[1]) 459 | self._writedata(self.windowLocData) 460 | 461 | self._writecommand(TFT.RAMWR) #Write to RAM. 462 | 463 | #@micropython.native 464 | def _writecommand( self, aCommand ) : 465 | '''Write given command to the device.''' 466 | 467 | self.dc(0) 468 | self.cs(0) 469 | self.spi.write(bytearray([aCommand])) 470 | self.cs(1) 471 | 472 | #@micropython.native 473 | def _writedata( self, aData ) : 474 | '''Write given data to the device. This may be 475 | either a single int or a bytearray of values.''' 476 | 477 | 478 | self.dc(1) 479 | self.cs(0) 480 | self.spi.write(aData) 481 | self.cs(1) 482 | 483 | #@micropython.native 484 | def _pushcolor( self, aColor ) : 485 | '''Push given color to the device.''' 486 | 487 | self.colorData[0] = aColor >> 8 488 | self.colorData[1] = aColor 489 | self._writedata(self.colorData) 490 | 491 | #@micropython.native 492 | def _setMADCTL( self ) : 493 | '''Set screen rotation and RGB/BGR format.''' 494 | self._writecommand(TFT.MADCTL) 495 | rgb = TFTRGB if self._rgb else TFTBGR 496 | self._writedata(bytearray([TFTRotations[self.rotate] | rgb])) 497 | 498 | #@micropython.native 499 | def _reset( self ) : 500 | '''Reset the device.''' 501 | self.dc(0) 502 | self.reset(1) 503 | time.sleep_us(500) 504 | self.reset(0) 505 | time.sleep_us(500) 506 | self.reset(1) 507 | time.sleep_us(500) 508 | 509 | def initb( self ) : 510 | '''Initialize blue tab version.''' 511 | self._size = (ScreenSize[0] + 2, ScreenSize[1] + 1) 512 | self._reset() 513 | self._writecommand(TFT.SWRESET) #Software reset. 514 | time.sleep_us(50) 515 | self._writecommand(TFT.SLPOUT) #out of sleep mode. 516 | time.sleep_us(500) 517 | 518 | data1 = bytearray(1) 519 | self._writecommand(TFT.COLMOD) #Set color mode. 520 | data1[0] = 0x05 #16 bit color. 521 | self._writedata(data1) 522 | time.sleep_us(10) 523 | 524 | data3 = bytearray([0x00, 0x06, 0x03]) #fastest refresh, 6 lines front, 3 lines back. 525 | self._writecommand(TFT.FRMCTR1) #Frame rate control. 526 | self._writedata(data3) 527 | time.sleep_us(10) 528 | 529 | self._writecommand(TFT.MADCTL) 530 | data1[0] = 0x08 #row address/col address, bottom to top refresh 531 | self._writedata(data1) 532 | 533 | data2 = bytearray(2) 534 | self._writecommand(TFT.DISSET5) #Display settings 535 | data2[0] = 0x15 #1 clock cycle nonoverlap, 2 cycle gate rise, 3 cycle oscil, equalize 536 | data2[1] = 0x02 #fix on VTL 537 | self._writedata(data2) 538 | 539 | self._writecommand(TFT.INVCTR) #Display inversion control 540 | data1[0] = 0x00 #Line inversion. 541 | self._writedata(data1) 542 | 543 | self._writecommand(TFT.PWCTR1) #Power control 544 | data2[0] = 0x02 #GVDD = 4.7V 545 | data2[1] = 0x70 #1.0uA 546 | self._writedata(data2) 547 | time.sleep_us(10) 548 | 549 | self._writecommand(TFT.PWCTR2) #Power control 550 | data1[0] = 0x05 #VGH = 14.7V, VGL = -7.35V 551 | self._writedata(data1) 552 | 553 | self._writecommand(TFT.PWCTR3) #Power control 554 | data2[0] = 0x01 #Opamp current small 555 | data2[1] = 0x02 #Boost frequency 556 | self._writedata(data2) 557 | 558 | self._writecommand(TFT.VMCTR1) #Power control 559 | data2[0] = 0x3C #VCOMH = 4V 560 | data2[1] = 0x38 #VCOML = -1.1V 561 | self._writedata(data2) 562 | time.sleep_us(10) 563 | 564 | self._writecommand(TFT.PWCTR6) #Power control 565 | data2[0] = 0x11 566 | data2[1] = 0x15 567 | self._writedata(data2) 568 | 569 | #These different values don't seem to make a difference. 570 | # dataGMCTRP = bytearray([0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f, 571 | # 0x1b, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10]) 572 | dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29, 573 | 0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10]) 574 | self._writecommand(TFT.GMCTRP1) 575 | self._writedata(dataGMCTRP) 576 | 577 | # dataGMCTRN = bytearray([0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30, 578 | # 0x30, 0x39, 0x3f, 0x00, 0x07, 0x03, 0x10]) 579 | dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d, 0x2e, 580 | 0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10]) 581 | self._writecommand(TFT.GMCTRN1) 582 | self._writedata(dataGMCTRN) 583 | time.sleep_us(10) 584 | 585 | self._writecommand(TFT.CASET) #Column address set. 586 | self.windowLocData[0] = 0x00 587 | self.windowLocData[1] = 2 #Start at column 2 588 | self.windowLocData[2] = 0x00 589 | self.windowLocData[3] = self._size[0] - 1 590 | self._writedata(self.windowLocData) 591 | 592 | self._writecommand(TFT.RASET) #Row address set. 593 | self.windowLocData[1] = 1 #Start at row 2. 594 | self.windowLocData[3] = self._size[1] - 1 595 | self._writedata(self.windowLocData) 596 | 597 | self._writecommand(TFT.NORON) #Normal display on. 598 | time.sleep_us(10) 599 | 600 | self._writecommand(TFT.RAMWR) 601 | time.sleep_us(500) 602 | 603 | self._writecommand(TFT.DISPON) 604 | self.cs(1) 605 | time.sleep_us(500) 606 | 607 | def initr( self ) : 608 | '''Initialize a red tab version.''' 609 | self._reset() 610 | 611 | self._writecommand(TFT.SWRESET) #Software reset. 612 | time.sleep_us(150) 613 | self._writecommand(TFT.SLPOUT) #out of sleep mode. 614 | time.sleep_us(500) 615 | 616 | data3 = bytearray([0x01, 0x2C, 0x2D]) #fastest refresh, 6 lines front, 3 lines back. 617 | self._writecommand(TFT.FRMCTR1) #Frame rate control. 618 | self._writedata(data3) 619 | 620 | self._writecommand(TFT.FRMCTR2) #Frame rate control. 621 | self._writedata(data3) 622 | 623 | data6 = bytearray([0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d]) 624 | self._writecommand(TFT.FRMCTR3) #Frame rate control. 625 | self._writedata(data6) 626 | time.sleep_us(10) 627 | 628 | data1 = bytearray(1) 629 | self._writecommand(TFT.INVCTR) #Display inversion control 630 | data1[0] = 0x07 #Line inversion. 631 | self._writedata(data1) 632 | 633 | self._writecommand(TFT.PWCTR1) #Power control 634 | data3[0] = 0xA2 635 | data3[1] = 0x02 636 | data3[2] = 0x84 637 | self._writedata(data3) 638 | 639 | self._writecommand(TFT.PWCTR2) #Power control 640 | data1[0] = 0xC5 #VGH = 14.7V, VGL = -7.35V 641 | self._writedata(data1) 642 | 643 | data2 = bytearray(2) 644 | self._writecommand(TFT.PWCTR3) #Power control 645 | data2[0] = 0x0A #Opamp current small 646 | data2[1] = 0x00 #Boost frequency 647 | self._writedata(data2) 648 | 649 | self._writecommand(TFT.PWCTR4) #Power control 650 | data2[0] = 0x8A #Opamp current small 651 | data2[1] = 0x2A #Boost frequency 652 | self._writedata(data2) 653 | 654 | self._writecommand(TFT.PWCTR5) #Power control 655 | data2[0] = 0x8A #Opamp current small 656 | data2[1] = 0xEE #Boost frequency 657 | self._writedata(data2) 658 | 659 | self._writecommand(TFT.VMCTR1) #Power control 660 | data1[0] = 0x0E 661 | self._writedata(data1) 662 | 663 | self._writecommand(TFT.INVOFF) 664 | 665 | self._writecommand(TFT.MADCTL) #Power control 666 | data1[0] = 0xC8 667 | self._writedata(data1) 668 | 669 | self._writecommand(TFT.COLMOD) 670 | data1[0] = 0x05 671 | self._writedata(data1) 672 | 673 | self._writecommand(TFT.CASET) #Column address set. 674 | self.windowLocData[0] = 0x00 675 | self.windowLocData[1] = 0x00 676 | self.windowLocData[2] = 0x00 677 | self.windowLocData[3] = self._size[0] - 1 678 | self._writedata(self.windowLocData) 679 | 680 | self._writecommand(TFT.RASET) #Row address set. 681 | self.windowLocData[3] = self._size[1] - 1 682 | self._writedata(self.windowLocData) 683 | 684 | dataGMCTRP = bytearray([0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f, 685 | 0x1b, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10]) 686 | self._writecommand(TFT.GMCTRP1) 687 | self._writedata(dataGMCTRP) 688 | 689 | dataGMCTRN = bytearray([0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30, 690 | 0x30, 0x39, 0x3f, 0x00, 0x07, 0x03, 0x10]) 691 | self._writecommand(TFT.GMCTRN1) 692 | self._writedata(dataGMCTRN) 693 | time.sleep_us(10) 694 | 695 | self._writecommand(TFT.DISPON) 696 | time.sleep_us(100) 697 | 698 | self._writecommand(TFT.NORON) #Normal display on. 699 | time.sleep_us(10) 700 | 701 | self.cs(1) 702 | 703 | def initb2( self ) : 704 | '''Initialize another blue tab version.''' 705 | self._size = (ScreenSize[0] + 2, ScreenSize[1] + 1) 706 | self._offset[0] = 2 707 | self._offset[1] = 1 708 | self._reset() 709 | self._writecommand(TFT.SWRESET) #Software reset. 710 | time.sleep_us(50) 711 | self._writecommand(TFT.SLPOUT) #out of sleep mode. 712 | time.sleep_us(500) 713 | 714 | data3 = bytearray([0x01, 0x2C, 0x2D]) # 715 | self._writecommand(TFT.FRMCTR1) #Frame rate control. 716 | self._writedata(data3) 717 | time.sleep_us(10) 718 | 719 | self._writecommand(TFT.FRMCTR2) #Frame rate control. 720 | self._writedata(data3) 721 | time.sleep_us(10) 722 | 723 | self._writecommand(TFT.FRMCTR3) #Frame rate control. 724 | self._writedata(data3) 725 | time.sleep_us(10) 726 | 727 | self._writecommand(TFT.INVCTR) #Display inversion control 728 | data1 = bytearray(1) # 729 | data1[0] = 0x07 730 | self._writedata(data1) 731 | 732 | self._writecommand(TFT.PWCTR1) #Power control 733 | data3[0] = 0xA2 # 734 | data3[1] = 0x02 # 735 | data3[2] = 0x84 # 736 | self._writedata(data3) 737 | time.sleep_us(10) 738 | 739 | self._writecommand(TFT.PWCTR2) #Power control 740 | data1[0] = 0xC5 # 741 | self._writedata(data1) 742 | 743 | self._writecommand(TFT.PWCTR3) #Power control 744 | data2 = bytearray(2) 745 | data2[0] = 0x0A # 746 | data2[1] = 0x00 # 747 | self._writedata(data2) 748 | 749 | self._writecommand(TFT.PWCTR4) #Power control 750 | data2[0] = 0x8A # 751 | data2[1] = 0x2A # 752 | self._writedata(data2) 753 | 754 | self._writecommand(TFT.PWCTR5) #Power control 755 | data2[0] = 0x8A # 756 | data2[1] = 0xEE # 757 | self._writedata(data2) 758 | 759 | self._writecommand(TFT.VMCTR1) #Power control 760 | data1[0] = 0x0E # 761 | self._writedata(data1) 762 | time.sleep_us(10) 763 | 764 | self._writecommand(TFT.MADCTL) 765 | data1[0] = 0xC8 #row address/col address, bottom to top refresh 766 | self._writedata(data1) 767 | 768 | #These different values don't seem to make a difference. 769 | # dataGMCTRP = bytearray([0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f, 770 | # 0x1b, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10]) 771 | dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29, 772 | 0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10]) 773 | self._writecommand(TFT.GMCTRP1) 774 | self._writedata(dataGMCTRP) 775 | 776 | # dataGMCTRN = bytearray([0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30, 777 | # 0x30, 0x39, 0x3f, 0x00, 0x07, 0x03, 0x10]) 778 | dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d, 0x2e, 779 | 0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10]) 780 | self._writecommand(TFT.GMCTRN1) 781 | self._writedata(dataGMCTRN) 782 | time.sleep_us(10) 783 | 784 | self._writecommand(TFT.CASET) #Column address set. 785 | self.windowLocData[0] = 0x00 786 | self.windowLocData[1] = 0x02 #Start at column 2 787 | self.windowLocData[2] = 0x00 788 | self.windowLocData[3] = self._size[0] - 1 789 | self._writedata(self.windowLocData) 790 | 791 | self._writecommand(TFT.RASET) #Row address set. 792 | self.windowLocData[1] = 0x01 #Start at row 2. 793 | self.windowLocData[3] = self._size[1] - 1 794 | self._writedata(self.windowLocData) 795 | 796 | data1 = bytearray(1) 797 | self._writecommand(TFT.COLMOD) #Set color mode. 798 | data1[0] = 0x05 #16 bit color. 799 | self._writedata(data1) 800 | time.sleep_us(10) 801 | 802 | self._writecommand(TFT.NORON) #Normal display on. 803 | time.sleep_us(10) 804 | 805 | self._writecommand(TFT.RAMWR) 806 | time.sleep_us(500) 807 | 808 | self._writecommand(TFT.DISPON) 809 | self.cs(1) 810 | time.sleep_us(500) 811 | 812 | #@micropython.native 813 | def initg( self ) : 814 | '''Initialize a green tab version.''' 815 | self._reset() 816 | 817 | self._writecommand(TFT.SWRESET) #Software reset. 818 | time.sleep_us(150) 819 | self._writecommand(TFT.SLPOUT) #out of sleep mode. 820 | time.sleep_us(255) 821 | 822 | data3 = bytearray([0x01, 0x2C, 0x2D]) #fastest refresh, 6 lines front, 3 lines back. 823 | self._writecommand(TFT.FRMCTR1) #Frame rate control. 824 | self._writedata(data3) 825 | 826 | self._writecommand(TFT.FRMCTR2) #Frame rate control. 827 | self._writedata(data3) 828 | 829 | data6 = bytearray([0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d]) 830 | self._writecommand(TFT.FRMCTR3) #Frame rate control. 831 | self._writedata(data6) 832 | time.sleep_us(10) 833 | 834 | self._writecommand(TFT.INVCTR) #Display inversion control 835 | self._writedata(bytearray([0x07])) 836 | self._writecommand(TFT.PWCTR1) #Power control 837 | data3[0] = 0xA2 838 | data3[1] = 0x02 839 | data3[2] = 0x84 840 | self._writedata(data3) 841 | 842 | self._writecommand(TFT.PWCTR2) #Power control 843 | self._writedata(bytearray([0xC5])) 844 | 845 | data2 = bytearray(2) 846 | self._writecommand(TFT.PWCTR3) #Power control 847 | data2[0] = 0x0A #Opamp current small 848 | data2[1] = 0x00 #Boost frequency 849 | self._writedata(data2) 850 | 851 | self._writecommand(TFT.PWCTR4) #Power control 852 | data2[0] = 0x8A #Opamp current small 853 | data2[1] = 0x2A #Boost frequency 854 | self._writedata(data2) 855 | 856 | self._writecommand(TFT.PWCTR5) #Power control 857 | data2[0] = 0x8A #Opamp current small 858 | data2[1] = 0xEE #Boost frequency 859 | self._writedata(data2) 860 | 861 | self._writecommand(TFT.VMCTR1) #Power control 862 | self._writedata(bytearray([0x0E])) 863 | 864 | self._writecommand(TFT.INVOFF) 865 | 866 | self._setMADCTL() 867 | 868 | self._writecommand(TFT.COLMOD) 869 | self._writedata(bytearray([0x05])) 870 | 871 | self._writecommand(TFT.CASET) #Column address set. 872 | self.windowLocData[0] = 0x00 873 | self.windowLocData[1] = 0x01 #Start at row/column 1. 874 | self.windowLocData[2] = 0x00 875 | self.windowLocData[3] = self._size[0] - 1 876 | self._writedata(self.windowLocData) 877 | 878 | self._writecommand(TFT.RASET) #Row address set. 879 | self.windowLocData[3] = self._size[1] - 1 880 | self._writedata(self.windowLocData) 881 | 882 | dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29, 883 | 0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10]) 884 | self._writecommand(TFT.GMCTRP1) 885 | self._writedata(dataGMCTRP) 886 | 887 | dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d, 0x2e, 888 | 0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10]) 889 | self._writecommand(TFT.GMCTRN1) 890 | self._writedata(dataGMCTRN) 891 | 892 | self._writecommand(TFT.NORON) #Normal display on. 893 | time.sleep_us(10) 894 | 895 | self._writecommand(TFT.DISPON) 896 | time.sleep_us(100) 897 | 898 | self.cs(1) 899 | 900 | 901 | def maker( ) : 902 | t = TFT(1, "X1", "X2") 903 | print("Initializing") 904 | t.initr() 905 | t.fill(0) 906 | return t 907 | 908 | def makeb( ) : 909 | t = TFT(1, "X1", "X2") 910 | print("Initializing") 911 | t.initb() 912 | t.fill(0) 913 | return t 914 | 915 | def makeg( ) : 916 | t = TFT(1, "X1", "X2") 917 | print("Initializing") 918 | t.initg() 919 | t.fill(0) 920 | return t 921 | -------------------------------------------------------------------------------- /examples/simple_text.py: -------------------------------------------------------------------------------- 1 | def run(spryg): 2 | spryg.screen.text("Lorem Ipsum", 0, 0, 0xFFFF) 3 | spryg.flip() 4 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DedFishy/Spryg/ca2d54b6c862db61a146cbccf74547ced46a4b7d/logo.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from machine import SPI,Pin,I2S 2 | import os 3 | import math 4 | import struct 5 | import time 6 | from ST7735 import TFT 7 | from framebuf import FrameBuffer, RGB565 8 | 9 | try: 10 | import sd as sdcard 11 | try_sd = True 12 | except ImportError as e: 13 | print("[?] SD card module not found. SD Card will not be initialized.") 14 | try_sd = False 15 | 16 | def bytes_to_gb(b): 17 | return b/1e+9 18 | 19 | 20 | class Spryg: 21 | BUTTONS = { 22 | "W": Pin(5, Pin.IN, Pin.PULL_UP), 23 | "A": Pin(6, Pin.IN, Pin.PULL_UP), 24 | "S": Pin(7, Pin.IN, Pin.PULL_UP), 25 | "D": Pin(8, Pin.IN, Pin.PULL_UP), 26 | 27 | "I": Pin(12, Pin.IN, Pin.PULL_UP), 28 | "J": Pin(13, Pin.IN, Pin.PULL_UP), 29 | "K": Pin(14, Pin.IN, Pin.PULL_UP), 30 | "L": Pin(15, Pin.IN, Pin.PULL_UP), 31 | } 32 | 33 | LED_L = Pin(28, Pin.OUT) 34 | LED_R = Pin(4, Pin.OUT) 35 | 36 | 37 | SCK_PIN = 10 #BCLK 38 | WS_PIN = 11 #LCK 39 | SD_PIN = 9 #DIN 40 | I2S_ID = 0 41 | BUFFER_LENGTH_IN_BYTES = 2000 42 | SAMPLE_RATE_IN_HZ = 22_050 43 | SAMPLE_SIZE_IN_BITS = 16 44 | 45 | def __init__(self): 46 | 47 | # Screen setup 48 | 49 | # The backlight, by default on because the screen practically needs it 50 | self.backlight = Pin(17, Pin.OUT) 51 | self.backlight.on() 52 | 53 | # Set up the SPI protocol 54 | spi = SPI(0, baudrate=20000000, polarity=0, phase=0, sck=Pin(18), mosi=Pin(19), miso=Pin(16)) 55 | # Set up the screen class 56 | self.tft=TFT(spi, 22, 26, 20) 57 | self.tft.initr() 58 | self.tft.rgb(True) 59 | # Rotate it to the correct orientation 60 | self.tft.rotation(1) 61 | # By default we want it black 62 | self.tft.fill(TFT.BLACK) 63 | 64 | # Set up a buffer so screen writing is smooth 65 | self.buf = bytearray(128*160*2) 66 | self.screen = FrameBuffer(self.buf, 160, 128, RGB565) 67 | 68 | # Final bit of screen set up 69 | self.tft._setwindowloc((0,0),(159,127)) 70 | 71 | # Set up audio 72 | self.audio_out = I2S( 73 | self.I2S_ID, 74 | sck=Pin(self.SCK_PIN), 75 | ws=Pin(self.WS_PIN), 76 | sd=Pin(self.SD_PIN), 77 | mode=I2S.TX, 78 | bits=self.SAMPLE_SIZE_IN_BITS, 79 | format=I2S.MONO, 80 | rate=self.SAMPLE_RATE_IN_HZ, 81 | ibuf=self.BUFFER_LENGTH_IN_BYTES, 82 | ) 83 | 84 | if try_sd: 85 | try: 86 | # Set up SD Card 87 | self.sd_spi = SPI(0,baudrate=40000000,sck=Pin(18),mosi=Pin(19),miso=Pin(16)) 88 | # Initialize SD card 89 | self.sd = sdcard.SDCard(self.sd_spi, Pin(21)) 90 | os.mount(self.sd, "/sd") 91 | 92 | print("Files: " + str(os.listdir("/sd"))) 93 | 94 | self.loaded_sd = True 95 | except Exception as e: 96 | print("[!] " + str(e)) 97 | self.loaded_sd = False 98 | 99 | ## Buttons 100 | def get_button(self, bid): 101 | return not bool(self.BUTTONS[bid].value()) 102 | 103 | ## LEDs 104 | def set_led(self, name, state): 105 | if name == "L": 106 | if state: 107 | self.LED_L.on() 108 | else: 109 | self.LED_L.off() 110 | if name == "R": 111 | if state: 112 | self.LED_R.on() 113 | else: 114 | self.LED_R.off() 115 | 116 | ## Audio 117 | def make_tone(self, frequency): 118 | # create a buffer containing the pure tone samples 119 | samples_per_cycle = self.SAMPLE_RATE_IN_HZ // frequency 120 | sample_size_in_bytes = self.SAMPLE_SIZE_IN_BITS // 8 121 | samples = bytearray(samples_per_cycle * sample_size_in_bytes) 122 | volume_reduction_factor = 32 123 | range = pow(2, self.SAMPLE_SIZE_IN_BITS) // 2 // volume_reduction_factor 124 | 125 | if self.SAMPLE_SIZE_IN_BITS == 16: 126 | format = "> 6 97 | c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7 98 | read_bl_len = csd[5] & 0b1111 99 | capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len) 100 | self.sectors = capacity // 512 101 | else: 102 | raise OSError("SD card CSD format not supported") 103 | # print('sectors', self.sectors) 104 | 105 | # CMD16: set block length to 512 bytes 106 | if self.cmd(16, 512, 0) != 0: 107 | raise OSError("can't set 512 block size") 108 | 109 | # set to high data rate now that it's initialised 110 | self.init_spi(baudrate) 111 | 112 | def init_card_v1(self): 113 | for i in range(_CMD_TIMEOUT): 114 | time.sleep_ms(50) 115 | self.cmd(55, 0, 0) 116 | if self.cmd(41, 0, 0) == 0: 117 | # SDSC card, uses byte addressing in read/write/erase commands 118 | self.cdv = 512 119 | # print("[SDCard] v1 card") 120 | return 121 | raise OSError("timeout waiting for v1 card") 122 | 123 | def init_card_v2(self): 124 | for i in range(_CMD_TIMEOUT): 125 | time.sleep_ms(50) 126 | self.cmd(58, 0, 0, 4) 127 | self.cmd(55, 0, 0) 128 | if self.cmd(41, 0x40000000, 0) == 0: 129 | self.cmd(58, 0, 0, -4) # 4-byte response, negative means keep the first byte 130 | ocr = self.tokenbuf[0] # get first byte of response, which is OCR 131 | if not ocr & 0x40: 132 | # SDSC card, uses byte addressing in read/write/erase commands 133 | self.cdv = 512 134 | else: 135 | # SDHC/SDXC card, uses block addressing in read/write/erase commands 136 | self.cdv = 1 137 | # print("[SDCard] v2 card") 138 | return 139 | raise OSError("timeout waiting for v2 card") 140 | 141 | def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False): 142 | self.cs(0) 143 | 144 | # create and send the command 145 | buf = self.cmdbuf 146 | buf[0] = 0x40 | cmd 147 | buf[1] = arg >> 24 148 | buf[2] = arg >> 16 149 | buf[3] = arg >> 8 150 | buf[4] = arg 151 | buf[5] = crc 152 | self.spi.write(buf) 153 | 154 | if skip1: 155 | self.spi.readinto(self.tokenbuf, 0xFF) 156 | 157 | # wait for the response (response[7] == 0) 158 | for i in range(_CMD_TIMEOUT): 159 | self.spi.readinto(self.tokenbuf, 0xFF) 160 | response = self.tokenbuf[0] 161 | if not (response & 0x80): 162 | # this could be a big-endian integer that we are getting here 163 | # if final<0 then store the first byte to tokenbuf and discard the rest 164 | if final < 0: 165 | self.spi.readinto(self.tokenbuf, 0xFF) 166 | final = -1 - final 167 | for j in range(final): 168 | self.spi.write(b"\xff") 169 | if release: 170 | self.cs(1) 171 | self.spi.write(b"\xff") 172 | return response 173 | 174 | # timeout 175 | self.cs(1) 176 | self.spi.write(b"\xff") 177 | return -1 178 | 179 | def readinto(self, buf): 180 | self.cs(0) 181 | 182 | # read until start byte (0xff) 183 | for i in range(_CMD_TIMEOUT): 184 | self.spi.readinto(self.tokenbuf, 0xFF) 185 | if self.tokenbuf[0] == _TOKEN_DATA: 186 | break 187 | time.sleep_ms(1) 188 | else: 189 | self.cs(1) 190 | raise OSError("timeout waiting for response") 191 | 192 | # read data 193 | mv = self.dummybuf_memoryview 194 | if len(buf) != len(mv): 195 | mv = mv[: len(buf)] 196 | self.spi.write_readinto(mv, buf) 197 | 198 | # read checksum 199 | self.spi.write(b"\xff") 200 | self.spi.write(b"\xff") 201 | 202 | self.cs(1) 203 | self.spi.write(b"\xff") 204 | 205 | def write(self, token, buf): 206 | self.cs(0) 207 | 208 | # send: start of block, data, checksum 209 | self.spi.read(1, token) 210 | self.spi.write(buf) 211 | self.spi.write(b"\xff") 212 | self.spi.write(b"\xff") 213 | 214 | # check the response 215 | if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05: 216 | self.cs(1) 217 | self.spi.write(b"\xff") 218 | return 219 | 220 | # wait for write to finish 221 | while self.spi.read(1, 0xFF)[0] == 0: 222 | pass 223 | 224 | self.cs(1) 225 | self.spi.write(b"\xff") 226 | 227 | def write_token(self, token): 228 | self.cs(0) 229 | self.spi.read(1, token) 230 | self.spi.write(b"\xff") 231 | # wait for write to finish 232 | while self.spi.read(1, 0xFF)[0] == 0x00: 233 | pass 234 | 235 | self.cs(1) 236 | self.spi.write(b"\xff") 237 | 238 | def readblocks(self, block_num, buf): 239 | # workaround for shared bus, required for (at least) some Kingston 240 | # devices, ensure MOSI is high before starting transaction 241 | self.spi.write(b"\xff") 242 | 243 | nblocks = len(buf) // 512 244 | assert nblocks and not len(buf) % 512, "Buffer length is invalid" 245 | if nblocks == 1: 246 | # CMD17: set read address for single block 247 | if self.cmd(17, block_num * self.cdv, 0, release=False) != 0: 248 | # release the card 249 | self.cs(1) 250 | raise OSError(5) # EIO 251 | # receive the data and release card 252 | self.readinto(buf) 253 | else: 254 | # CMD18: set read address for multiple blocks 255 | if self.cmd(18, block_num * self.cdv, 0, release=False) != 0: 256 | # release the card 257 | self.cs(1) 258 | raise OSError(5) # EIO 259 | offset = 0 260 | mv = memoryview(buf) 261 | while nblocks: 262 | # receive the data and release card 263 | self.readinto(mv[offset : offset + 512]) 264 | offset += 512 265 | nblocks -= 1 266 | if self.cmd(12, 0, 0xFF, skip1=True): 267 | raise OSError(5) # EIO 268 | 269 | def writeblocks(self, block_num, buf): 270 | # workaround for shared bus, required for (at least) some Kingston 271 | # devices, ensure MOSI is high before starting transaction 272 | self.spi.write(b"\xff") 273 | 274 | nblocks, err = divmod(len(buf), 512) 275 | assert nblocks and not err, "Buffer length is invalid" 276 | if nblocks == 1: 277 | # CMD24: set write address for single block 278 | if self.cmd(24, block_num * self.cdv, 0) != 0: 279 | raise OSError(5) # EIO 280 | 281 | # send the data 282 | self.write(_TOKEN_DATA, buf) 283 | else: 284 | # CMD25: set write address for first block 285 | if self.cmd(25, block_num * self.cdv, 0) != 0: 286 | raise OSError(5) # EIO 287 | # send the data 288 | offset = 0 289 | mv = memoryview(buf) 290 | while nblocks: 291 | self.write(_TOKEN_CMD25, mv[offset : offset + 512]) 292 | offset += 512 293 | nblocks -= 1 294 | self.write_token(_TOKEN_STOP_TRAN) 295 | 296 | def ioctl(self, op, arg): 297 | if op == 4: # get number of blocks 298 | return self.sectors 299 | if op == 5: # get block size in bytes 300 | return 512 --------------------------------------------------------------------------------