/* * This file is part of Espruino, a JavaScript interpreter for Microcontrollers * * Copyright (C) 2019 Gordon Williams * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * ---------------------------------------------------------------------------- * Graphics Backend for drawing to LPM013M126 displays * ---------------------------------------------------------------------------- */ #include "platform_config.h" #include "jsutils.h" #include "jshardware.h" #include "jsinteractive.h" #include "lcd_memlcd.h" #include "jswrap_graphics.h" #include "jswrap_espruino.h" // for reversebyte // ====================================================================== #define LCD_SPI EV_SPI1 #define LCD_ROWHEADER 2 #define LCD_STRIDE (LCD_ROWHEADER+((LCD_WIDTH*LCD_BPP+7)>>3)) // data in required BPP, plus 2 bytes LCD command /** Buffer for our LCD data. - We add 2 extra lines (LCD_HEIGHT+2) as a scratch area for doing the overlay (if enabled) - 2 bytes at end of transfer (needed by LCD) included in that extra line - 4 bytes at end (needed to allow fast scrolling) also handled by the extra line */ unsigned char lcdBuffer[LCD_STRIDE*(LCD_HEIGHT+2)]; bool isBacklightOn; ///< is LCD backlight on? If so we need to toggle EXTCOMIN faster JsVar *lcdOverlayImage; ///< if set, an Image to use for overlays short lcdOverlayX,lcdOverlayY; ///< coordinates of the graphics instance volatile bool lcdIsBusy; ///< We're now allowing SPI send in the background - if we're sending, block execution until it finishes #ifdef EMULATED bool EMSCRIPTEN_GFX_CHANGED; unsigned char fakeLCDBuffer[LCD_STRIDE*LCD_HEIGHT]; bool jsGfxChanged() { bool b = EMSCRIPTEN_GFX_CHANGED; EMSCRIPTEN_GFX_CHANGED = false; return b; } char *jsGfxGetPtr(int line) { if (line<0 || line>=LCD_HEIGHT) return 0; return &fakeLCDBuffer[LCD_ROWHEADER + (line*LCD_STRIDE)]; } #endif // bayer dithering pattern #define BAYER_RGBSHIFT(b) (b<<13) | (b<<8) | (b<<2) const unsigned short BAYER2[2][2] = { { BAYER_RGBSHIFT(1), BAYER_RGBSHIFT(5) }, { BAYER_RGBSHIFT(7), BAYER_RGBSHIFT(3) } }; static ALWAYS_INLINE unsigned int lcdMemLCD_convert16toLCD(unsigned int c, int x, int y) { c = (c&0b1110011100011100) + BAYER2[y&1][x&1]; return ((c&0x10000)?1:0) | ((c&0x00800)?2:0) | ((c&0x00020)?4:0); } /** 'Flip' now ends while data is still sending to the LCD. This * function allows us to wait until the flip has finished (or a timeout * has occurred) which we'll need to do before we next modify what * is on the LCD. */ static ALWAYS_INLINE void lcdMemLCD_waitForSendComplete() { int timeout = 1000000; while (lcdIsBusy && --timeout) {}; if (lcdIsBusy) { // LCD timeout! Do we want to log this? lcdIsBusy = false; } } // ====================================================================== unsigned int lcdMemLCD_getPixel(JsGraphics *gfx, int x, int y) { #if LCD_BPP==3 int bitaddr = LCD_ROWHEADER*8 + (x*3) + (y*LCD_STRIDE*8); int bit = bitaddr&7; uint16_t b = *(uint16_t*)&lcdBuffer[bitaddr>>3]; // get in MSB format unsigned int c = (b>>bit) & 0x7; #endif #if LCD_BPP==4 int addr = LCD_ROWHEADER + (x>>1) + (y*LCD_STRIDE); unsigned char b = lcdBuffer[addr]; unsigned int c = (x&1) ? ((b>>1)&7) : (b>>5); #endif return ((((c)&1)?0xF800:0)|(((c)&2)?0x07E0:0)|(((c)&4)?0x001F:0)); } /* 0123456701234567 RGB bpp=0, RGB bpp=13 */ void lcdMemLCD_setPixel(JsGraphics *gfx, int x, int y, unsigned int col) { col = lcdMemLCD_convert16toLCD(col,x,y); lcdMemLCD_waitForSendComplete(); #if LCD_BPP==3 int bitaddr = LCD_ROWHEADER*8 + (x*3) + (y*LCD_STRIDE*8); int bit = bitaddr&7; uint16_t b = *(uint16_t*)&lcdBuffer[bitaddr>>3]; *(uint16_t*)&lcdBuffer[bitaddr>>3] = (b & ~(7<>1) + (y*LCD_STRIDE); if (x&1) lcdBuffer[addr] = (lcdBuffer[addr] & 0x0F) | (col << 4); else lcdBuffer[addr] = (lcdBuffer[addr] & 0xF0) | col; #endif } void lcdMemLCD_fillRect(struct JsGraphics *gfx, int x1, int y1, int x2, int y2, unsigned int col) { lcdMemLCD_waitForSendComplete(); // Super-fast fill if whole width if (x1==0 && x2==LCD_WIDTH-1 && (col==0 || col==0xFFFF)) { int addr = LCD_ROWHEADER + y1*LCD_STRIDE; for (int y=y1;y<=y2;y++) { memset(&lcdBuffer[addr], (col==0)?0:0xFF, LCD_STRIDE-LCD_ROWHEADER); addr += LCD_STRIDE; } return; } /* Otherwise go line-by line. 2x2 dithering means we're not writing the * same color to every pixel, so we usually work out what a 2x2 block * or 2x1 row is beforehand to save time */ #if LCD_BPP==3 /* On 3bpp if we're filling a small width of pixels, just set them individually - it's possible to do with a mask too, but it hurts my head and it's not quite as big a performance improvement. */ #ifdef EMSCRIPTEN if (true) { // on emscripten, unaligned 32 bit access doesn't work - we just set pixels individually (speed doesn't matter as it's emulated) #else if (x2-x1 < 8) { #endif // For 3 bit, precalculate what the 2 pixels go to unsigned int cols[2][2] = { { lcdMemLCD_convert16toLCD(col,0,0), lcdMemLCD_convert16toLCD(col,1,0) }, // even row { lcdMemLCD_convert16toLCD(col,0,1), lcdMemLCD_convert16toLCD(col,1,1) } // odd row }; // write pixels individually for (int y=y1;y<=y2;y++) { unsigned int *c = cols[y&1]; int bitaddr = LCD_ROWHEADER*8 + (x1*3) + (y*LCD_STRIDE*8); for (int x=x1;x<=x2;x++) { int bit = bitaddr&7; uint16_t b = *(uint16_t*)&lcdBuffer[bitaddr>>3]; *(uint16_t*)&lcdBuffer[bitaddr>>3] = (b & ~(7<>3)*3; int bit = x1&7; uint32_t *pixels = (uint32_t*)&row[byteAddr]; uint32_t mask = (0xFFFFFF<<(bit*3)) & 0xFFFFFF; *pixels = (*pixels&~mask) | (c&mask); } for (int x=(x1+7)>>3;x<(x2+1)>>3;x++) { // do the middle blocks of 24 bits (8 pixels) uint32_t *pixels = (uint32_t*)&row[x*3]; uint32_t mask = 0xFFFFFF; *pixels = (*pixels&~mask) | (c&mask); } if ((x2+1)&7) { // do the final block after we're aligned (up to 8 pixels) int byteAddr = ((x2+1)>>3)*3; int bit = (x2+1)&7; uint32_t *pixels = (uint32_t*)&row[byteAddr]; uint32_t mask = (0xFFFFFF<<(bit*3)) | 0xFF000000; *pixels = (*pixels&mask) | (c&~mask); } #endif #if LCD_BPP==4 // For 4 bit, we can pack the 2 pixels into a byte unsigned char ditheredCol = lcdMemLCD_convert16toLCD(col,0,y) | (lcdMemLCD_convert16toLCD(col,1,y)<<4); int x=x1; int addr = LCD_ROWHEADER + (x>>1) + (y*LCD_STRIDE); if (x&1) { // first pixel on odd coordinate, unaligned lcdBuffer[addr] = (lcdBuffer[addr] & 0x0F) | (ditheredCol&0xF0); addr++;x++; } for (;x>5; shiftBits &= 31; int wordLen = (LCD_WIDTH*LCD_BPP - shiftBits)>>5; for (int x=0;x<=wordLen;x++) dw[x] = (sw[x+shiftWords]>>shiftBits) | (sw[x+shiftWords+1]<<(32-shiftBits)); } else { // >0 int shiftBits = xdir * LCD_BPP; int shiftWords = shiftBits>>5; shiftBits &= 31; int wordLen = (LCD_WIDTH*LCD_BPP + 15 - shiftBits)>>5; for (int x=0;x<=wordLen;x++) dw[x] = (sw[x-shiftWords]<>(32-shiftBits)); } } void lcdMemLCD_scroll(struct JsGraphics *gfx, int xdir, int ydir, int x1, int y1, int x2, int y2) { lcdMemLCD_waitForSendComplete(); // if we can't shift entire line in one go, go with the slow method as this case would be a nightmare in 3 bits if (x1!=0 || x2!=LCD_WIDTH-1) return graphicsFallbackScroll(gfx, xdir, ydir, x1,y1,x2,y2); // otherwise... unsigned char lineBuffer[LCD_STRIDE+4]; // allow out of bounds write if (ydir<=0) { for (int y=y1;y<=y2+ydir;y++) { int yx = y-ydir; lcdMemLCD_scrollX(gfx, lineBuffer, &lcdBuffer[yx*LCD_STRIDE], xdir); memcpy(&lcdBuffer[y*LCD_STRIDE + LCD_ROWHEADER],&lineBuffer[LCD_ROWHEADER],LCD_STRIDE-LCD_ROWHEADER); } } else if (ydir>0) { for (int y=y2-ydir;y>=y1;y--) { int yx = y+ydir; lcdMemLCD_scrollX(gfx, lineBuffer, &lcdBuffer[y*LCD_STRIDE], xdir); memcpy(&lcdBuffer[yx*LCD_STRIDE + LCD_ROWHEADER],&lineBuffer[LCD_ROWHEADER],LCD_STRIDE-LCD_ROWHEADER); } } } // ----------------------------------------------------------------------------- // used to allow SPI send to work async WHEN DOING OVERLAYS (we don't care when it finishes) void lcdMemLCD_flip_spi_ovr_callback() {} // used to allow SPI send to work async for normal sends. void lcdMemLCD_flip_spi_callback() { jshPinSetValue(LCD_SPI_CS, 0); lcdIsBusy = false; } // send the data to the screen void lcdMemLCD_flip(JsGraphics *gfx) { if (gfx->data.modMinY > gfx->data.modMaxY) return; // nothing to do! #ifdef EMULATED EMSCRIPTEN_GFX_CHANGED = true; #endif lcdMemLCD_waitForSendComplete(); int y1 = gfx->data.modMinY; int y2 = gfx->data.modMaxY; int l = 1+y2-y1; bool hasOverlay = false; GfxDrawImageInfo overlayImg; if (lcdOverlayImage) hasOverlay = _jswrap_graphics_parseImage(gfx, lcdOverlayImage, 0, &overlayImg); jshPinSetValue(LCD_SPI_CS, 1); if (hasOverlay) { /* If lcdOverlayImage is defined, we want to overlay this image * on top of what we have in our LCD buffer. Do this line by * line. It's slower but it won't use a bunch of memory. * * We use an extra line added to the end of lcdBuffer for this, which * allows us to use lcdMemLCD_setPixel to do color conversion and dither * without loads of duplicate code. * * Optimisation: we could just send any non-overlaid stuff above or below * the overlay... */ // Take account of rotation - only check for a full 180 rotation - doing 90 is too hard bool isRotated180 = (graphicsInternal.data.flags & (JSGRAPHICSFLAGS_SWAP_XY | JSGRAPHICSFLAGS_INVERT_X | JSGRAPHICSFLAGS_INVERT_Y)) == (JSGRAPHICSFLAGS_INVERT_X | JSGRAPHICSFLAGS_INVERT_Y); int ovY = isRotated180 ? (LCD_HEIGHT-(lcdOverlayY+overlayImg.height)) : lcdOverlayY; // Set colors to current theme unsigned int oldFgColor = gfx->data.fgColor; unsigned int oldBgColor = gfx->data.bgColor; gfx->data.fgColor = graphicsTheme.fg; gfx->data.bgColor = graphicsTheme.bg; // initialise image layer GfxDrawImageLayer l; l.x1 = 0; l.y1 = ovY; l.img = overlayImg; l.rotate = isRotated180 ? 3.141592 : 0; l.scale = 1; l.center = false; l.repeat = false; jsvStringIteratorNew(&l.it, l.img.buffer, (size_t)l.img.bitmapOffset); _jswrap_drawImageLayerInit(&l); _jswrap_drawImageLayerSetStart(&l, 0, y1); for (int y=y1;y<=y2;y++) { int bufferLine = LCD_HEIGHT + (y&1); // alternate lines so we still get dither AND we can send while calculating next line unsigned char *buf = &lcdBuffer[LCD_STRIDE*bufferLine]; // point to line right on the end of gfx // copy original line in memcpy(buf, &lcdBuffer[LCD_STRIDE*y], LCD_STRIDE); // overwrite areas with overlay image if (y>=ovY && y= 0)) lcdMemLCD_setPixel(NULL, ox, bufferLine, c); _jswrap_drawImageLayerNextX(&l); } } _jswrap_drawImageLayerNextY(&l); // send the line #ifdef EMULATED memcpy(&fakeLCDBuffer[LCD_STRIDE*y], buf, LCD_STRIDE); #else jshSPISendMany(LCD_SPI, buf, NULL, LCD_STRIDE, lcdMemLCD_flip_spi_ovr_callback); #endif } jsvStringIteratorFree(&l.it); _jswrap_graphics_freeImageInfo(&overlayImg); // and 2 final bytes to finish the transfer #ifndef EMULATED jshSPISendMany(LCD_SPI, lcdBuffer, NULL, 2, NULL); lcdMemLCD_flip_spi_callback(); #endif // Restore colors to previous state gfx->data.fgColor = oldFgColor; gfx->data.bgColor = oldBgColor; } else { // standard, non-overlay #ifdef EMULATED memcpy(fakeLCDBuffer, lcdBuffer, LCD_HEIGHT*LCD_STRIDE); #else lcdIsBusy = true; if (!jshSPISendMany(LCD_SPI, &lcdBuffer[LCD_STRIDE*y1], NULL, (l*LCD_STRIDE)+2, lcdMemLCD_flip_spi_callback)) lcdMemLCD_flip_spi_callback(); // lcdMemLCD_flip_spi_callback will call jshPinSetValue(LCD_SPI_CS, 0); when done and set lcdIsBusy=false #endif } // Reset modified-ness gfx->data.modMaxX = -32768; gfx->data.modMaxY = -32768; gfx->data.modMinX = 32767; gfx->data.modMinY = 32767; } void lcdMemLCD_init(JsGraphics *gfx) { gfx->data.width = LCD_WIDTH; gfx->data.height = LCD_HEIGHT; gfx->data.bpp = 16; // take color as 16 bit even though we only use 3 memset(lcdBuffer,0,sizeof(lcdBuffer)); for (int y=0;ysetPixel = lcdMemLCD_setPixel; gfx->fillRect = lcdMemLCD_fillRect; gfx->getPixel = lcdMemLCD_getPixel; gfx->scroll = lcdMemLCD_scroll; }