ST7735 128x128 LCDs

Posted on
  • I try to port another driver library for this inexpensive 1.44" display
    It uses the ST7735 driver - which is used by most of these cheap displays.
    Currently I try to use first the new ILI9163 library from Gordon (Forum post)

    Here is what I get as initial state:

    Try this as inline to play with commands...
    This is what comes out of the box with the ILI9163 driver:

    As you can see, the coordinates are not right now. Everything seems to be shifted...

    var exports = {};
    
    /* Copyright (c) 2015 Gordon Williams, Pur3 Ltd. See the file LICENSE for copying permission. 
    Module for the ILI9163 LCD controller
    ILI9163 128x128
    
    | VCC | B15 | 3.3v
    | GND | B14 | GND
    | CS/CE  | B13 |
    | RST | B10 | 3.3v
    | A0/DC  | B1  |
    | SDA | A7  |
    | SCL | A6  |
    | LED | A5  |
    */
    
    var LCD_WIDTH = 128;
    var LCD_HEIGHT = 128;
    
    function init(spi, dc, ce, rst, callback) {
      function cmd(c,d) {
        dc.reset();
        spi.write(c, ce);
        if (d!==undefined) {
          dc.set();
          spi.write(d, ce);
        }
      }
      
      if (rst) {
        digitalPulse(rst,0,10);
      } else {
        cmd(0x01); //Software reset
      }
      setTimeout(function() {
        cmd(0x11); //Exit Sleep
        setTimeout(function() {
          cmd(0x26, 0x04); //Set Default Gamma
          cmd(0xB1, [0x0e,0x10]); //Set Frame Rate
          cmd(0xC0, [0x08,0]); //Set VRH1[4:0] & VC[2:0] for VCI1 & GVDD
          cmd(0xC1, 0x05); //Set BT[2:0] for AVDD & VCL & VGH & VGL
          cmd(0xC5, [0x38,0x40]); //Set VMH[6:0] & VML[6:0] for VOMH & VCOML
    
          cmd(0x3a, 5); //Set Color Format, 5=16 bit,3=12 bit
          cmd(0x36, 0xc8); //RGB
    
          cmd(0x2A,[0,0,0,LCD_WIDTH]); //Set Column Address
          cmd(0x2B,[0,0,0,LCD_HEIGHT]); //Set Page Address
    
          cmd(0xB4, 0); // display inversion
    
          cmd(0xf2, 1); //Enable Gamma bit
          cmd(0xE0,[0x3f,0x22,0x20,0x30,0x29,0x0c,­0x4e,0xb7,0x3c,0x19,0x22,0x1e,0x02,0x01,­0x00]); 
          cmd(0xE1,[0x00,0x1b,0x1f,0x0f,0x16,0x13,­0x31,0x84,0x43,0x06,0x1d,0x21,0x3d,0x3e,­0x3f]);
    
          cmd(0x29); // Display On
          cmd(0x2C); // reset frame ptr      
    
          if (callback) callback();
        },20);
      } ,100);
    }
    
    exports.connect = function(spi, dc, ce, rst, callback) {
      var g = Graphics.createCallback(LCD_WIDTH, LCD_HEIGHT, 16, {
        setPixel:function(x,y,c){
          ce.reset();
          spi.write(0x2A,dc);
          spi.write(0,x,0,x+1);
          spi.write(0x2B,dc);
          spi.write(0,y,0,y+1);
          spi.write(0x2C,dc);
          spi.write(c>>8,c);
          ce.set();
        },
        fillRect:function(x1,y1,x2,y2,c){
          ce.reset();
          spi.write(0x2A,dc);
          spi.write(0,x1,0,x2);
          spi.write(0x2B,dc);
          spi.write(0,y1,0,y2);
          spi.write(0x2C,dc);
          spi.write({data:String.fromCharCode(c>>8­,c), count:(x2-x1+1)*(y2-y1+1)});
          ce.set();
        }
      });
      init(spi, dc, ce, rst, callback);
      return g;
    };
    
    exports.connectPaletted = function(palette, spi, dc, ce, rst, callback) {
      var bits;
      if (palette.length>16) bits=8;
      else if (palette.length>4) bits=4;
      else if (palette.length>2) bits=2;
      else bits=1;
      var g = Graphics.createArrayBuffer(LCD_WIDTH, LCD_HEIGHT, bits, { msb:true });
      g.flip = function() {
        ce.reset();
        spi.write(0x2A,dc);
        spi.write(0,0,0,LCD_WIDTH);
        spi.write(0x2B,dc);
        spi.write(0,0,0,LCD_HEIGHT);
        spi.write(0x2C,dc);
        var lines = 16; // size of buffer to use for un-paletting
        var a = new Uint16Array(LCD_WIDTH*lines);
        for (var y=0;y<LCD_HEIGHT;y+=lines) {
          E.mapInPlace(new Uint8Array(g.buffer, y*LCD_WIDTH*bits/8, a.length), a, palette, bits);
          spi.write(a.buffer);
        }
        ce.set();
      };
      init(spi, dc, ce, rst, callback);
      return g;
    };
    
    /* using library */
    B15.set();// VCC
    B14.reset(); // GND
    A5.set(); // Backlight On
    
    var colorPalette = new Uint16Array([0, 0xF80F, 0x001F, 0xFFFF]);
    var spi = new SPI();
    spi.setup({mosi:A7 /* sda */, sck:A6 /* scl */});
    var g = exports.connectPaletted(colorPalette, spi, B1 /* DC */, B13 /* CE */, B10 /* RST */, function() {
      g.clear();
      //g.setRotation(2);
      g.setColor(3);
      g.drawString("Hello",0,0);
      g.setColor(1);
      g.setFontVector(20);
      g.drawString("Espruino",0,10);
      g.flip();
    });
    

    I will start working on this tomorrow - my son is getting his third tooth, so everything is grumpy today :/
    Thanks to @Gordon, I'll try add these commands tomorrow

  • Wow, that's pretty close! Looks like the width is right, but the start address and 'view window' is off.

    I'd look at copying these two:

        ST7735_FRMCTR1, 3+DELAY,  //  4: Frame rate control, 3 args + delay:
          0x00,                   //     fastest refresh
          0x06,                   //     6 lines front porch
          0x03,                   //     3 lines back porch
          10,                     //     10 ms delay
    
        ST7735_CASET  , 4      ,  // 15: Column addr set, 4 args, no delay:
          0x00, 0x02,             //     XSTART = 2
          0x00, 0x81,             //     XEND = 129
        ST7735_RASET  , 4      ,  // 16: Row addr set, 4 args, no delay:
          0x00, 0x02,             //     XSTART = 1
          0x00, 0x81,             //     XEND = 160
    

    The first one is already there, but just uses different values, and for the second one you'll need to change the code in g.flip as well (although come to think of it, g.flip may not need that code at all).

    There's also this code - it looks like the Adafruit lib supports a few different types of screen by sending different initialisation code. It wouldn't surprise me if their 'default' setup doesn't work for you either.

  • I just figured out on my train drive, that I need
    this code + this code + this code
    I started to create the code, based on this defines as cmd block

    But good idea to add this later on to one LCD-display driver library.
    Perhaps it is useful to see also this implementation as reference.

  • Interestingly, Espruino already has a similar 'do anything' implementation for parallel-interface LCDs, that's used on the 'HY' boards: https://github.com/espruino/Espruino/blo­b/master/libs/graphics/lcd_fsmc.c#L403

    With the two-way comms on those it can actually read out the controller ID and then execute the correct code.

    It's a hard balance with modules though - you don't want loads of modules from the point of view of maintenance, but at the same time you don't want to be using up precious RAM with code that you're never using.

    I guess one option is to actually generate multiple files using some kind of preprocessor. In fact with minification and the closure compiler it'd be possible to do it just with IF statements...

  • Also, pretty sure you won't need the Magical Unicorn Dust. I'm pretty sure that's for setting the Gamma ramp, but the colour doesn't seem to be a problem on your screen at the moment.

  • Now it is much better, but still not as expected...
    I have to do some extra effort to make this working.
    I'm wondering that I have to do the range to width and height only up to 127 instead of 128.
    But with 127 it works "better"

    var exports = {};
    
    /* Copyright (c) 2015 Gordon Williams, Pur3 Ltd. See the file LICENSE for copying permission. 
    
    Module for the ILI9163 LCD controller
    
    ,------------------.
    | ILI9163 128x128  |
    +-----------------´
    | VCC | B15 | 3.3v
    | GND | B14 | GND
    | CS/CE  | B13 |
    | RST | B10 | 3.3v
    | A0/DC  | B1  |
    | SDA | A7  |
    | SCL | A6  |
    | LED | A5  |
    
    
    */
    
    var LCD_WIDTH = 128;
    var LCD_HEIGHT = 128;
    
    function init(spi, dc, ce, rst, callback) {
      function cmd(c,d) {
        dc.reset();
        spi.write(c, ce);
        if (d!==undefined) {
          dc.set();
          spi.write(d, ce);
        }
      }
      
      if (rst) {
        digitalPulse(rst,0,10);
      } else {
        cmd(0x01); //ST7735_SWRESET: Software reset, 0 args, w/delay: 150 ms delay
      }
      setTimeout(function() {
        cmd(0x11); //ST7735_SLPOUT: Out of sleep mode, 0 args, w/delay: 500 ms delay
        setTimeout(function() {
          cmd(0xB1,[0x01,0x2C,0x2D]); //ST7735_FRMCTR1: Set Frame rate ctrl - normal mode, 3 args: (Rate = fosc/(1x2+40) * (LINE+2C+2D))
          cmd(0xB2,[0x01,0x2C,0x2D]); //ST7735_FRMCTR2: Set Frame rate control - idle mode, 3 args: Rate = fosc/(1x2+40) * (LINE+2C+2D)
          cmd(0xB3,[0x01,0x2C,0x2D,0x01,0x2C,0x2D]­); //ST7735_FRMCTR3: Set Frame rate ctrl - partial mode, 6 args: Dot inversion mode + Line inversion mode
          cmd(0xB4,0x07); // ST7735_INVCTR: Set Display inversion ctrl, 1 arg, no delay: No inversion
          cmd(0xC0,[0xA2,0x02,0x84]); //ST7735_PWCTR1: Set Power control, 3 args, no delay: init + -4.6V + AUTO mode 
          cmd(0xC1,0xC5); //ST7735_PWCTR2: Set Power control, 1 arg, no delay: VGH25 = 2.4C VGSEL = -10 VGH = 3 * AVDD
          cmd(0xC2,[0x0A,0x00]); //ST7735_PWCTR3: Set Power control, 2 args, no delay: Opamp current small + Boost frequency
          cmd(0xC3,[0x8A,0x2A]); //ST7735_PWCTR4: Set Power control, 2 args, no delay: BCLK/2, Opamp current small & Medium low
          cmd(0xC4,[0x8A,0xEE]); //ST7735_PWCTR5: Set Power control, 2 args, no delay:
          cmd(0xC5,0x0E); //ST7735_VMCTR1: Set Power control, 1 arg, no delay:
          cmd(0x20,0x00); //ST7735_INVOFF: Don't invert display, no args, no delay
          cmd(0x36,0xC8); //ST7735_MADCTL: Set Memory access control (directions), 1 arg: row addr/col addr, bottom to top refresh
          cmd(0x3A,0x05); //ST7735_COLMOD: Set color mode, 1 arg, no delay: 16-bit color
          cmd(0x2A,[0x00,0x00,0x00,0x7F]); //ST7735_CASET: Set Column addr set, 4 args, no delay: XSTART = 0 + XEND = 127
          cmd(0x2B,[0x00,0x00,0x00,0x7F]); //ST7735_RASET: Set Row addr set, 4 args, no delay: XSTART = 0 + XEND = 127
          cmd(0xE0,[0x02,0x1c,0x07,0x12,0x37,0x32,­0x29,0x2d,0x29,0x25,0x2B,0x39,0x00,0x01,­0x03,0x10]); //ST7735_GMCTRP1: color and gamma correction
          cmd(0xE1,[0x03,0x1d,0x07,0x06,0x2E,0x2C,­0x29,0x2D,0x2E,0x2E,0x37,0x3F,0x00,0x00,­0x02,0x10]); //ST7735_GMCTRN1: color and gamma correction
          cmd(0x13); //ST7735_NORON: Set Normal display on, no args, w/delay: 10 ms delay
          cmd(0x29); //ST7735_DISPON: Set Main screen turn on, no args w/delay: 100 ms delay
    
          if (callback) callback();
        },10);
      } ,100);
    
    }
    
    
    exports.connect = function(spi, dc, ce, rst, callback) {
      var g = Graphics.createCallback(LCD_WIDTH, LCD_HEIGHT, 16, {
        setPixel:function(x,y,c){
          ce.reset();
          spi.write(0x2A,dc);
          spi.write(0,x,0,x+1);
          spi.write(0x2B,dc);
          spi.write(0,y,0,y+1);
          spi.write(0x2C,dc);
          spi.write(c>>8,c);
          ce.set();
        },
        fillRect:function(x1,y1,x2,y2,c){
          ce.reset();
          spi.write(0x2A,dc);
          spi.write(0,x1,0,x2);
          spi.write(0x2B,dc);
          spi.write(0,y1,0,y2);
          spi.write(0x2C,dc);
          spi.write({data:String.fromCharCode(c>>8­,c), count:(x2-x1+1)*(y2-y1+1)});
          ce.set();
        }
      });
      init(spi, dc, ce, rst, callback);
      return g;
    };
    
    exports.connectPaletted = function(palette, spi, dc, ce, rst, callback) {
      var bits;
      if (palette.length>16) bits=8;
      else if (palette.length>4) bits=4;
      else if (palette.length>2) bits=2;
      else bits=1;
      var g = Graphics.createArrayBuffer(LCD_WIDTH, LCD_HEIGHT, bits, { msb:true });
      g.flip = function() {
        ce.reset();
        spi.write(0x2A,dc);
        spi.write(0,0,0,LCD_WIDTH-1);
        spi.write(0x2B,dc);
        spi.write(0,0,0,LCD_HEIGHT-1);
        spi.write(0x2C,dc);
        var lines = 16; // size of buffer to use for un-paletting
        var a = new Uint16Array(LCD_WIDTH*lines);
        for (var y=0;y<LCD_HEIGHT;y+=lines) {
          E.mapInPlace(new Uint8Array(g.buffer, y*LCD_WIDTH*bits/8, a.length), a, palette, bits);
          spi.write(a.buffer);
        }
        ce.set();
      };
      init(spi, dc, ce, rst, callback);
      return g;
    };
    
    
    
    
    
    B15.set();// VCC
    B14.reset(); // GND
    A5.set(); // Backlight On
    
    var colorPalette = new Uint16Array([0, 0xF80F, 0x001F, 0xFFFF]);
    var spi = new SPI();
    spi.setup({mosi:A7 /* sda */, sck:A6 /* scl */});
    var g = exports.connectPaletted(colorPalette, spi, B1 /* DC */, B13 /* CE */, B10 /* RST */, function() {
      g.clear();
      //g.setRotation(2);
      g.setColor(3);
      g.drawString("Hello",0,0);
      g.setColor(1);
      g.setFontVector(20);
      g.drawString("Espruino",0,10);
      g.flip();
    });
    
  • What's it look like now?

  • It seems that it is still a kind of offset in here...

  • Ok, don't know why, but when I modify the flip by add 2px to the X and 3px to the Y it works...

    Line 11 + 13

    exports.connectPaletted = function(palette, spi, dc, ce, rst, callback) {
      var bits;
      if (palette.length>16) bits=8;
      else if (palette.length>4) bits=4;
      else if (palette.length>2) bits=2;
      else bits=1;
      var g = Graphics.createArrayBuffer(LCD_WIDTH, LCD_HEIGHT, bits, { msb:true });
      g.flip = function() {
        ce.reset();
        spi.write(0x2A,dc);
        spi.write(2,2,2,LCD_WIDTH+1);
        spi.write(0x2B,dc);
        spi.write(3,3,3,LCD_HEIGHT+2);
        spi.write(0x2C,dc);
        var lines = 16; // size of buffer to use for un-paletting
        var a = new Uint16Array(LCD_WIDTH*lines);
        for (var y=0;y<LCD_HEIGHT;y+=lines) {
          E.mapInPlace(new Uint8Array(g.buffer, y*LCD_WIDTH*bits/8, a.length), a, palette, bits);
          spi.write(a.buffer);
        }
        ce.set();
      };
      init(spi, dc, ce, rst, callback);
      return g;
    };
    

  • here is my file to play with. I just add it to my local webserver and load it with require.
    @Gordon Thx for your help, bringing this screen to life ;)


    1 Attachment

  • Thanks! I'll update the docs with it - it looks like you've changed enough that it's better as a separate module.

    Mind if I use your photo of it working on the page?

  • @Gordon I Just try to use your original source and only change the part of rewrite flip

    g.flip = function() {
        ce.reset();
        spi.write(0x2A,dc);
        spi.write(0,2,0,LCD_WIDTH+1);
        spi.write(0x2B,dc);
        spi.write(0,3,0,LCD_HEIGHT+2);
        spi.write(0x2C,dc);
        var lines = 16; // size of buffer to use for un-paletting
        var a = new Uint16Array(LCD_WIDTH*lines);
        for (var y=0;y<LCD_HEIGHT;y+=lines) {
          E.mapInPlace(new Uint8Array(g.buffer, y*LCD_WIDTH*bits/8, a.length), a, palette, bits);
          spi.write(a.buffer);
        }
        ce.set();
      };
    

    And voilà it works also :D
    So the display only needs this magix offset for X+2 / Y+3.
    Should I take a better picture, or is the pic above ok? Of course you can use.

  • Actually it'd be great if you could do just the display on a white background, but don't worry if not.

    Ok, great! interesting about the offset - I'll try and fit that into the existing module.

  • Hi there !

    I stumbled upon this thread ( 3 years old -> cute ^^) while looking for fixes to drive the following screen correctly:
    https://www.ebay.fr/itm/1-44-inch-Full-C­olor-128x160-SPI-TFT-LCD-Screen-Panel-re­place-OLED-for-Arduino/322569266920?ssPa­geName=STRK%3AMEBIDX%3AIT&_trksid=p20578­72.m2749.l2649

    The breakout uses an ST7735 driver, as @Jorgen's & as within the Espruino tutorial

    The issues:

    • not using the 'paletted' mode, I had "pixel artifacts" ( x & y offsets/borders with visuals glitches)
    • using the 'paletted' mode, I had text in italic ( ! ) & not aligned with the screen dimensions ( the part of "italic" text cut out on the left appeared on the right side of the screen)

    Many tries, but I couldn't get the above fixed without further modding the ILI9163 library ( fiddling with g.setRotation didn't fix the offset no the italic text :/ .. )
    After some time trying out stuff, I successfully fixed both issues ( yay ! ^^), and I think the fixes may as well be ported to other Espruino screen libraries ( to be as "generic" as possible while not increasing the libs drastically ;) )

    // Platform: EspruinoWiFi
    
    // pins used
    var screen_mosi_sda = A7;
    var screen_sck_scl = A6;
    var screen_dc = B1;
    var screen_cs = B13;
    var screen_rst = B10;
    
    var spi = new SPI();
    spi.setup({mosi: screen_mosi_sda, sck:screen_sck_scl});
    
    require("Font8x12").add(Graphics);
    
    // -- modded module --
    var exports = {};
    // tweaks that fixed the offset wile not in 'palettes' mode, whatever the rotation ;)
    var colStart = 2;
    var rowStart = 3;
    
    // Nb: I added some commented out stuff ( from http://www.dipmicro.com/?datasheet=DE444­7-ILI9163AN_V0_2.pdf )
    // but I don't have yet enough infos on what these do ( same as for some of @Jorgen's spa init stuff )
    function init(spi, dc, ce, rst, callback) {
      function cmd(c,d) {
        dc.reset();
        spi.write(c, ce);
        if (d!==undefined) {
          dc.set();
          spi.write(d, ce);
        }
      }
      
      if (rst) {
        digitalPulse(rst,0,10);
      } else {
        cmd(0x01); //Software reset
      }
      setTimeout(function() {
        cmd(0x11); //Exit Sleep
        setTimeout(function() {
          cmd(0x26, 0x04); //Set Default Gamma
          //cmd(0xF2, 0x00) //E0h & E1h Enable/Disable ( from ILI9163AN datasheet - Laibo1.8" / CMO1.8" )
          cmd(0xB1, [0x0e,0x10]); //Set Frame Rate -> default
          //cmd(0xB1, [0x0C,0x14]); //Set Frame Rate ( from ILI9163AN datasheet - Laibo1.8" / CMO1.8" )
          cmd(0xC0, [0x08,0]); //Set VRH1[4:0] & VC[2:0] for VCI1 & GVDD
          //cmd(0xC0, [0x0C,0x05]); // Set VRH1[4:0] & VC[2:0] for VCI1 & GVDD ( from ILI9163AN datasheet - Laibo1.8" / CMO1.8" )
          cmd(0xC1, 0x05); //Set BT[2:0] for AVDD & VCL & VGH & VGL
          //cmd(0xC1, 0x02); //Set BT[2:0] for AVDD & VCL & VGH & VGL ( from ILI9163AN datasheet )
          cmd(0xC5, [0x38,0x40]); //Set VMH[6:0] & VML[6:0] for VOMH & VCOML
          //cmd(0xC5, [0x32,0x3B]); //Set VMH[6:0] & VML[6:0] for VOMH & VCOML ( from ILI9163AN datasheet - Laibo1.8" )
          ////cmd(0xC5, [0x29,0x43]); //Set VMH[6:0] & VML[6:0] for VOMH & VCOML ( from ILI9163AN datasheet - CMO1.8" )
          //cmd(0xC7, 0x40); // ??? ( from ILI9163AN datasheet )
          
          cmd(0x3a, 5); //Set Color Format, 5=16 bit,3=12 bit
          cmd(0x36, 0xc8); //RGB
    
          cmd(0x2A,[0,0,0,LCD_WIDTH]); //Set Column Address R: digg if we can set the offset only here instead of within 3 fcns
          //cmd(0x2A,[0,0,0,0x7F]); //Set Column Address 0x7F = 127 ( from ILI9163AN datasheet )
          cmd(0x2B,[0,0,0,LCD_HEIGHT]); //Set Page Address R: digg if we can set the offset only here instead of within 3 fcns
          //cmd(0x2B,[0,0,0,0x9F]); //Set Page Address 0x9F = 135 ( from ILI9163AN datasheet )
          
          //cmd(0x36, 0xC0); // Set Scanning Direction ( from ILI9163AN datasheet - Laibo1.8" / CMO1.8" )
          
          cmd(0xB4, 0); // display inversion
          
          //cmd(0xB7, 0x00); // Set Source Output Direction ( from ILI9163AN datasheet - CMO1.8" )
    
          cmd(0xf2, 1); //Enable Gamma bit
          cmd(0xE0,[0x3f,0x22,0x20,0x30,0x29,0x0c,­0x4e,0xb7,0x3c,0x19,0x22,0x1e,0x02,0x01,­0x00]); 
          cmd(0xE1,[0x00,0x1b,0x1f,0x0f,0x16,0x13,­0x31,0x84,0x43,0x06,0x1d,0x21,0x3d,0x3e,­0x3f]);
    
          cmd(0x29); // Display On
          cmd(0x2C); // reset frame ptr      
    
          if (callback) callback();
        },20);
      } ,100);
    }
    
    // modded 'connect()' to fix the offset & pixel artifacts
    exports.connect = function(spi, dc, ce, rst, callback) {
      var g = Graphics.createCallback(LCD_WIDTH, LCD_HEIGHT, 16, {
        setPixel:function(x,y,c){
          ce.reset();
          spi.write(0x2A,dc);
          //spi.write(0,x,0,x+1); // default
          spi.write(0,x+colStart,0,x+colStart+1);
          spi.write(0x2B,dc);
          //spi.write(0,y,0,y+1); // default
          spi.write(0,y+rowStart,0,y+rowStart+1);
          spi.write(0x2C,dc);
          spi.write(c>>8,c);
          ce.set();
        },
        fillRect:function(x1,y1,x2,y2,c){
          ce.reset();
          spi.write(0x2A,dc);
          //spi.write(0,x1,0,x2); // default
          spi.write(0,x1+colStart,0,x2+colStart);
          spi.write(0x2B,dc);
          //spi.write(0,y1,0,y2); // default
          spi.write(0,y1+rowStart,0,y2+rowStart);
          spi.write(0x2C,dc);
          //spi.write({data:String.fromCharCode(c>­>8,c), count:(x2-x1+1)*(y2-y1+1)}); // default
          spi.write({data:String.fromCharCode(c>>8­,c), count:((x2+colStart)-(x1+colStart)+1)*((­y2+rowStart)-(y1+rowStart)+1)});
          ce.set();
        }
      });
      // quick & hacky fix
      g.flip = function() { /*prevents error during my quick tests*/ };
      init(spi, dc, ce, rst, callback);
      return g;
    };
    
    // modded 'connectPaletted()' to fix 'italic" text
    exports.connectPaletted = function(palette, spi, dc, ce, rst, callback) {
      var bits;
      if (palette.length>16) bits=8;
      else if (palette.length>4) bits=4;
      else if (palette.length>2) bits=2;
      else bits=1;
      var g = Graphics.createArrayBuffer(LCD_WIDTH, LCD_HEIGHT, bits, { msb:true, vertical_byte: false });
      g.flip = function() {
        ce.reset();
        spi.write(0x2A,dc);
        //spi.write(0,0,0,LCD_WIDTH); // default - otherwise, we had spi.write(0,x+colStart,0,x+colStart+1);
        //spi.write(0,2,0,LCD_WIDTH); // thx to @Jorgen who inspired this - italic but other orientation
        spi.write(0,1,0,LCD_WIDTH); // WORKS: made the trick for non-italic text drawn at once ! :D
        spi.write(0x2B,dc);
        spi.write(0,0,0,LCD_HEIGHT); // otherwise, we had spi.write(0,y+rowStart,0,y+rowStart+1);
        //spi.write(0,3,0,LCD_HEIGHT); // thx to @Jorgen who inspired this - italic but other orientation
        spi.write(0x2C,dc);
        var lines = 16; // size of buffer to use for un-paletting
        var a = new Uint16Array(LCD_WIDTH*lines); // default
        for (var y=0;y<LCD_HEIGHT;y+=lines) { // default
          E.mapInPlace(new Uint8Array(g.buffer, y*LCD_WIDTH*bits/8, a.length), a, palette, bits); // default
          spi.write(a.buffer);
        }
        ce.set();
      };
      init(spi, dc, ce, rst, callback);
      return g;
    };
    
    // test code
    var colorPalette = new Uint16Array([BLACK, WHITE]); // instant refresh
    //var g = exports.connect(spi, screen_dc, screen_cs, screen_rst, function() { // works fine
    var g = exports.connectPaletted(colorPalette, spi, screen_dc, screen_cs, screen_rst, function() {
      g.clear();
      g.setFont8x12(); // perfect size for 128x128 :)
      g.setColor(1);
      g.drawString("Troubles are fixed !", 2, 2);
      g.flip(); // needed when using 'paletted' mode
    });
    

    From the above fixes, I don't know if a 'offset: { x: , y:}' could be added to an option object that could be passed while connecting to the screen ?

    This being said, I have few questions on the graphics module ( & I'd be glad to format & add the answers fetched to the graphics page ;) ):

    Q1: on screen colors & update speed for a "little bigger" chip, I was wondering If I could expect a nearly instant refresh for paletted ( & non paletted as well ? ) modes on this screen & the STM32F407VGT6 ( 1024kB flash, 192kB ram). 128*128*16bpp = 32768 bytes = 33kB -> ~159kB are ram left ? ( I bet this assumption is pretty naïve, since the amount of ram available to Espruino may not match the total amount of ram available or something of the same kind I'm not aware of ? .. )

    Q2: would a mode with 'non palettes zones' & other stuff 'palettes' be of any interest ?

    Q3: would a mod of the 'clear()' & 'flip()' methods accepting a rectangle x1,y1,x2,y2 be of any help ( to either clear or update only part of the screen ) ? ( then we could also have 'canvases' & do stuff like g.cnvs[0].clear(), .draw.. & maybe have x&y coords shifted by the top-left of such canvases - that may be rotated as well ? ;) )

    Q4: could 'dynamic italic text' be come a feature ? ( I'll add visuals from my tests to better illustrate this 'glitch-not-yet-a-feature' ;p )

    Q5: could an alternative behavior for the 'palettes' mode be handy ? ( if color is outa bounds, fallback to either black or white depending on a param passed or settings )

    Q6: best way to get 'instant drawing' for a (colored or not but likely ) logo ? I tried using setPixel() & timing the time taken to draw the entire screen with sort of a gradient ( see hacks in code below ), then just using fillPoly() & while the latter seems way more promising than loading setting every pixel, getting it into the right colors quickly drawn from an array is I think possible, yet I'm not sure yet how to achieve that ( by using E.mapInPlace() & Cie ? - I also wonder how loading an image is done - and I failed but 'll retry to getting the Espruino logo example loading as an image ;) )

    I currently have two possible scenarios of how to get that colored logo draw quickly ( 'll presumably happen on device boot or reboot - and may be used as a "background" while a loading bar is updated at the bottom - not sure how of much time a 'clearToBackground()' fcn would take ;p .. )

    A:

    • draw it, which 'll take some time ( setPixel(), fillPoly(), .. )
    • save buffer to a screen-size specific format ( uint16 array ? )
    • redraw it 'as fast as can do' by using E.mapInPlace() or any related helper

    B:

    • backlight off
    • draw logo ( not palleted mode - so we see it while it's being drawn, which may be cool depending on the logo ;p )
    • backlight on ( show logo )
    • transition to menu ( go back to using palettes mode as quick as can do, if possible not redrawing the screen while doing so - but we might as well backlight off ->connect() & then backlight on once menu is drawn )

    drawing to screen directly being slower than to memory then in-one-row to screen, I'm wondering how drawing from obj ( from an already painted in ArrayBuffer saved in flash ? ) to screen would preform ( I guess it'll take more time for more colors ) & how to minimize the ram used to do so ( if I get correctly the current paletted code, it send rows by rows what I'd like to load & send row-by-row ( to lessen ram used ), but I'm not sure how 'fillPoly()' is implemented & whether or not I may be able to just draw its points & then use 'ce.set' once all are drawn
    Also, I'm quite curious if saving then loading an array buffer to only the part of screen needed would be fast enough ..

    I'll try some stuff out & then post an update ;)

    here some hacks for the adventurous readers ;p

    // draw color tests before trying to fix the 'paletted' mode
    function drawColorGradient(){
      for(var x=0; x <= 127; x+=1){
      //var x=0;
        for(var y=0; y <= 127; y++){
          //g.setPixel(x, y, [0, x, y]);
          //g.setPixel(x, y, [0, 0.7, 0.7]); // hangs for more than 10 sec on espruino wifi & display nothing :/
          //g.setPixel(x, y, 0x53d); // R: WON'T WORK IF NOT PASSED AN INTEGER !
          // slowest hack ever ..
          //g.setColor(0, x/255, x/255); // don't know yt why it fails ..
          console.log('current color: ' + x/255 + ', ' + (y/255).toFixed(2));
          //g.setColor(0, x/255, y/255); // works fine
          //g.setColor(0, x*2/255, y*2/255); // works fine
          //g.setColor((x+y)/255, x*2/255, y*2/255); // works fine
          //g.setColor(1-(x+y)/255, x*2/255, y*2/255); // works fine
          //g.setColor(1-(x+y)/255, 1-(x*2/255), 1-(y*2/255)); // works fine
          //g.setColor(1-(x-y)/255, 1-(y*2/255), 1-(x*2/255)); // works fine
          g.setColor(1-(y-x)/255, y*2/255, 1-(x*2/255)); // works fine - & quite color full actually ;)
          //g.setColor(0, x/255, (y/255).toFixed(2)); // also works
          //g.setColor(0, 0.65, 0.92); // this works
          var color = g.getColor();
          g.setPixel(x, y, color);
        }
      }
      console.log('screen filed with colors !');
    }
    
    // same as above but for paletted color
    function drawPaletteColorGradient(){
      var y = 20;
      for(var x=0; x <= colorPalette.length; x++){
        // slowest hack ever ..
        console.log('current color: ' + x/255 + ', ' + (y/255).toFixed(2));
        //g.setColor(1-(y-x)/255, y*2/255, 1-(x*2/255)); // works fine - & quite color full actually ;)
        //var color = g.getColor();
        //g.setPixel(x, y, color);
        //g.setPixel(x, y, colorPalette[x]);
        //g.drawLine(x, y, x, y+5, colorPalette[x]);
        g.setColor(x);
        var color = g.getColor();
        //g.drawLine(x, y, x, y+5, color);
        //g.setPixel(x, y, color);
        //g.setPixel(x, y, 0xF81F);
        g.setPixel(x, y, color);
        g.drawLine(x, y, x, y+5, color);
        g.drawLine(x, y+15, x, y+15, color);
      }
      console.log('screen filed with colors !');
    }
    
    // drawing the simplified logo
    / this is the kid of stuff I'd like drawn quickly in a row ;)
    function logo(xof, yof, sizeAdj){
      // Graphics.fillPoly(poly)
      // Graphics.drawPoly(poly, closed)
      //g.drawPoly([13.06,29.59 , 22.99,19.66 , 22.99,46.15 , 13.06,46.15], true);
      //g.setColor(COLOR1);
      //g.setColor(2);
      g.fillPoly([(xof+13.06)*sizeAdj,(yof+29.­59)*sizeAdj ,
                  (xof+22.99)*sizeAdj,(yof+19.66)*sizeAdj ,
                  (xof+22.99)*sizeAdj,(yof+46.15)*sizeAdj ,
                  (xof+13.06)*sizeAdj,(yof+46.15)*sizeAdj]­,
                 true);
      g.flip();
    }
    
  • a quick update on the subject: I tried for a while getting the following to work but yet without success ( it seems I get the beginning right, but then my code happily produces 'instant pixel artifacts garbage' instead of sending part of the buffer for spi refresh )

    The idea behind the 'flipP' ( fliPartial() ) & 'clearP()' ( clearPartial() ) is to be able to later build upon those in order to provide saving & restoring an ArrayBuffer ( g.buffer ) smaller than the entire screen as well as providing a way to quickly update or clear only part of the screen ( in normal & paletted mode )

    If anyone's skilled & brave enough to defeat the troubles that lies below, I guess I'm not the only one who'd benefit for such kindness ;p

    the non-working-quite-right versions ( rest of the code is as above )

    exports.connectPaletted = function(palette, spi, dc, ce, rst, callback) {
    // ( .. )
    // flipP(): execute a partial refresh of the buffer to the screen - seems working for whatever fy & fheight if fx==0 & fwidth==LCD_WIDTH
      g.flipP = function(fx, fy, fwidth ,fheight) {
        ce.reset();
        spi.write(0x2A,dc);
        //spi.write(0,fx+1,0,fwidth+1);
        //spi.write(0,fx+2,0,fwidth);
        //spi.write(0,fx+1,0,fwidth); // the +1 is what actually does the trick
        spi.write(0,fx+1,0,fwidth);
        //spi.write(0,fx+1, 0,fx+fwidth); // COMBO THAT SEEMS TO PASS ( seemed to trick this out for fwidth == lcd_width ) ?!
        spi.write(0x2B,dc);
        //spi.write(0,fy,0,fy+fheight); // responsible for the glitches ? ( stuff 'rolling over' itself on the vertical axis )
        //spi.write(0,fy+1,fheight+1);
        //spi.write(0,fy,fheight);         // COMBO THAT SEEMS TO PASS for fwidth == lcd_width, but was missing an arg ?!
        //spi.write(0,fy,1,fheight);
        spi.write(1,fy,0,fheight);
        spi.write(0x2C,dc);
        var lines = 16; // size of buffer to use for un-paletting
        var a = new Uint16Array(fwidth*lines); // default
        //for (var y=0;y<fheight;y+=lines) { // default - doesn't work for stuff not at top of screen
        for (var y=fy;y<fy+fheight;y+=lines) { // A-way - seemed to work for fxwidth == lcd_width & stuff not at top of screen
          //E.mapInPlace(new Uint8Array(g.buffer, y*fwidth*bits/8, a.length), a, palette, bits); // default - seems to work for whatever fheight & fy
          // idea: (new Uint8Array(g.buffer, (y+(fx*y))*fwidth*bits/8, a.length), a, palette, bits);
          E.mapInPlace(new Uint8Array(g.buffer, (y+(fx*y))*fwidth*bits/8, a.length), a, palette, bits); // seemed to work for whatever fheight & fy
          spi.write(a.buffer);
        }
        ce.set();
      };
    
      // clearP(): execute a partial clear of the buffer to the screen - currently discovered as an error in the above 'flipP' code .. :|
      g.clearP = function(fx, fy, fwidth ,fheight) {
        ce.reset();
        spi.write(0x2A,dc);
        //spi.write(0,fx+1,0,fwidth+1);
        //spi.write(0,fx+2,0,fwidth);
        //spi.write(0,fx,0,fwidth);
        spi.write(0,fx+1,0,fwidth); // the +1 is what actually does the trick
        //spi.write(0,fx+1, 0,fx+fwidth); // COMBO THAT SEEMS TO PASS ( seemed to trick this out for fwidth == lcd_width ) ?!
        spi.write(0x2B,dc);
        //spi.write(0,fy,0,fy+fheight); // responsible for the glitches ? ( stuff 'rolling over' itself on the vertical axis )
        //spi.write(0,fy+1,fheight+1);
        //spi.write(0,fy,fheight);         // COMBO THAT SEEMS TO PASS for fwidth == lcd_width, but was missing an arg ?!
        spi.write(0,fy,0,fheight);
        //spi.write(0,fy,1,fheight);
        //spi.write(0,fy+1,0,fheight);
        spi.write(0x2C,dc);
        var lines = 16; // size of buffer to use for un-paletting
        var a = new Uint16Array(fwidth*lines); // default
        //for (var y=0;y<fheight;y+=lines) { // default - doesn't work for stuff not at top of screen
        for (var y=fy;y<fy+fheight;y+=lines) { // default - seemed to work for fxwidth == lcd_width & stuff not at top of screen
          E.mapInPlace(new Uint8Array(g.buffer, y*fwidth*bits/8, a.length), a, palette, bits); // default
          // idea: (new Uint8Array(g.buffer, (y+(fx*y))*fwidth*bits/8, a.length), a, palette, bits);
          //E.mapInPlace(new Uint8Array(g.buffer, (y+(fx*y))*fwidth*bits/8, a.length), a, palette, bits); // give vertical-space colored glitches
          //E.mapInPlace(new Uint8Array(g.buffer, y*(fx+y)*fwidth*bits/8, a.length), a, palette, bits); // give vertical-space colored glitches
          spi.write(a.buffer);
        }
        ce.set();
      };
    

    test code ( better tried with something already drawn on screen, like some polygons or a logo ..

    var g = exports.connectPaletted(colorPalette, spi, screen_dc, screen_cs, screen_rst, function() {
      //g.clear(); // NOPE, we DON'T ;)
      g.setFont8x12();
      // ( .. )
      g.setColor(3);
        g.drawString("Hello, world!", 2, 2);
        g.setColor(2);
        g.drawString("Hello, world 2!", 2, 16);
    
        //g.flip(); // NOPE, we DON'T use this one either ..    
        
        // full changing both but nothing else
        g.flipP(0, 0, LCD_WIDTH, 12);  // seems to work fine
        g.flipP(0, 16, LCD_WIDTH, 12);  // seems to work fine
    
        // trying to partially change both
        setTimeout(function(){
          g.setColor(8);
          g.drawString("Karaki", 2, 2);
          g.setColor(9);
          g.drawString("Karaki2", 2, 16);
          //g.flipP(0, 2, LCD_WIDTH, 4); // cut its top ?! :L
          g.flipP(0, 0, LCD_WIDTH, 4); // what about its top ?
          g.flipP(0, 16+2, LCD_WIDTH, 4); // doesn't work at all :L
          console.log('color change 2 ( partial horizontal ) occured ?');
          setTimeout(function(){
            g.setColor(10);
            g.drawString("Karaki", 2, 2);
            g.setColor(11);
            g.drawString("Karaki2", 2, 16);
            g.flipP(0, 0, 50, 4); // so how is its beginning ?
            g.flipP(0, 16+2, 50, 4); // & this one's ?
            console.log('color change 3 ( partial vertical ) occured ?'); // -> getting garbage for both ..
          }, 6000);
        }, 2000);
    

    I achieved 'horizontal color stripes' on the 1st item with 3 redrawns before messing with the code & not being able to fix my mess back to that moment I wished I saved it :/ ( gotta upload picture of this & other cases as well .. )

    this being said, I'm onto a little break ..

  • Thanks for posting this up -I'll have to diff and see what init values actually changed. Looks like we need some kind of options object to let this stuff be specified - although the module itself is quite small so I wonder whether we shouldn't just have another module for this slightly different type of display?

    The row/col offsets could hopefully be added during initialisation as well - calculating offsets for each pixel write would really slow things down.

    Q1: STM32F407VGT6

    I think you're still going to be limited by SPI transfer speed. Until Espruino supports DMA you're not going to be able to push the data that fast.

    Q2: would a mode with 'non palettes zones' & other stuff 'palettes' be of any interest ?

    I'm not sure I understand... But if the other mode is unusable on all official Espruino boards because of the memory usage then I doubt it'd be that useful to people?

    Q3: would a mod of the 'clear()' & 'flip()' methods accepting a rectangle x1,y1,x2,y2 be of any help ( to either clear or update only part of the screen ) ?

    There is actually Graphics.getModified http://www.espruino.com/Reference#l_Grap­hics_getModified as Graphics keeps track of what has changed.

    If you could make flip() use that information and only upload the area of the screen that had changed then I think that would be very useful! Although you may find that the extra code needed to crop line-by line slows it down - so it may be worth ignoring the X coordinates and sending only entire lines.

    Q4: could 'dynamic italic text' be come a feature ?

    I know what you mean, but I'm not convinced! You could write a function a bit like there is for doubling font size though? http://forum.espruino.com/conversations/­327333

    Q5: could an alternative behavior for the 'palettes' mode be handy ? ( if color is outa bounds, fallback to either black or white depending on a param passed or settings )

    Wouldn't that make everything really slow? And I'm not sure how it could be out of bounds - if you have a 4 bit image with 16 colours then they can't be out of bounds by definition??

    Q6: best way to get 'instant drawing' for a (colored or not but likely ) logo ?

    Just copy the contents of g.buffer using the Uint8Array.set method - This example 'Mario Game' does it to speed things up: http://www.espruino.com/Platform+Game

    Or if you want to save memory you could use heatshrink compress/decompress on it: http://www.espruino.com/Reference#l_heat­shrink_compress

  • Hi there !
    -> it's always a pleasure sharing findings ;)

    A whole module seems a little overkill for stuff that could be set via options ( or it seems to me, at least ? ), also I have a little 64x48 old screen for which I can't fix the display ( I didn't mod the SSD1306 module right during my tests, reason for the horizontal white lines I'm getting :/ .. )

    I totally agree on putting the rows/cols offsets in the init cmds & I tired to do so, but I still don't have enough hints on the params of column & page address ( I could successfully fiddle with those to fix the "italic" text, but couldn't get the same fix working with tweaks on init .. ) -> if you have any docs on those, I'll be very glad to read about it :)

    cmd(0x2A,[0,0,0,LCD_WIDTH]); 
    cmd(0x2B,[0,0,0,LCD_HEIGHT]);
    

    Understood for the DMA stuff ( I 'll have to continue my beginner tries on how to use DMA before jumping to Espruino code that would relate to the subject .. but not skilled enough to help you there :/ )

    For the "other mode", I was thinking of something that would actually work on the official Espruino boards as well & use some "partial clear & flip()" to minimize the ram & memory needed :)

    Thanks for the link on ".getModified()", it'd be pretty nice to clear the last stuff drawn :)

    For your proposal on 'flip()' you'd wish something that clears only the last area drawn or every areas drawn ? (in other words, if I do the following, do I get a "cumulative" modified area ? )

    g.setPixel(10,20); // 1st thing draw
    g.getModified(); //> {x1:10, y1:20, x2:10, y2:20}
    g.setPixel(20,30); // 2nd thing draw
    g.getModified(); //> {x1:10, y1:20, x2:20, y2:30} --> I get the src correct ? 
    

    If the above is actually what's returned, I'll be happy modding the flip() fcn in order to only send the modified rows ( shall we include a bool to "force" re-sending unmodified stuff as well ? ) :)

    For the "italic" text, it was actually a glitch from setting the 1st or 3rd params in the 0x2A/0x2B calls, but thanks for the hint on the font doubling ;)
    -> it reminds me I "extracted" the font that was used by Autodesk for their 123DCircuit tool before it went offline, and being pretty nice for pcbs it'd be pretty cool to also have it on displays ;)

    For the "alternative behavior" of the "paletted mode", I was getting weird colors mainly because of having a not big enough palette I guess ( it seemed to went away when having a palette of 16 items, clearer after reading the source again .. ;) )

    For the "instant logo drawing", I studied your platform game code ( & tested it -> it seemed I had some fixes to do be done with the player "flying" and going down otherwise, but this may have come from the way I set my btns ;) )
    -> I'll have to try the flappy example & the neat clock one :)

    The goal being to save memory & get a colored logo drawn at once in whatever paletted/normal mode, I guess a possible solution would be to 1st save it to .boot0 & cie & then have the code that draws it on power up & reboot ?

    -> I'll try the said stuff in few mins & give an update that as well ( .. )

    Last but not least, I digged within jswrap_graphics.h/.c & graphics.h.c to better understand stuff .. and there's a lot to grasp :) !
    I think aside from the 'flip()' update, a version accepting args 'd be still relevant, as well as 'clear(area(s))' ( & maybe 'clip(area(s))' to only draw stuff that's within the said area(s) - for interesting & retro text effects ;p ? )

    For the "partial clear" part, what about the following ?

    // jswrap_graphics.h
    void jswrap_graphics_clearP(JsVar *parent, int x1, int y1, int x2, int y2);
    
    // jswrap_graphics.c
    /*JSON{
      "type" : "method",
      "class" : "Graphics",
      "name" : "clearP",
      "generate" : "jswrap_graphics_clearP"
    }
    Clear the LCD partially with the Background Color
    */
    void jswrap_graphics_clearP(JsVar *parent, int x1, int y1, int x2, int y2) {
      JsGraphics gfx; if (!graphicsGetFromVar(&gfx, parent)) return;
      graphicsClear(&gfx);
      graphicsSetVar(&gfx); // gfx data changed because modified area
    }
    
    // graphics.h
    void graphicsClearP(JsGraphics *gfx, short x1, short y1, short x2, short y2);
    
    // graphics.c
    void graphicsClearP(JsGraphics *gfx, int x1, int y1, int x2, int y2) {
    unsigned int c = gfx->data.fgColor;
      gfx->data.fgColor = gfx->data.bgColor;
    graphicsFillRectDevice(gfx,x1,y1,x2,y2);­
      gfx->data.fgColor = c;
    }
    

    For the 'flip()' update, maybe something like the following to be able to either try a flip or a partial flip if something was modified, and "force" it for whatever reason if needed - but I guess the "force" bool wouldn't be used .. ?

    // R: untested yet ! ;)
    /*
    options = {
      force: false, // to force a refresh even if g.buffer is unmodified ( some debug mess happened ? )
      y1: 0, // for specific rows handle ( partial )
      y2: LCD_HEIGHT, // for specific rows handle ( partial )
      x1: 0,  // 'll surely be too slow for usage ..
      x2: LCD_WIDTH // 'll surely be too slow for usage ..
    }
    */
    g.flip = function(options) {
       // check if modified
      var tmp = g.getModified();
      if(typeof tmp === "undefined"){
        // nothing to update: force ?
      } else {
        var y1 = options.y1 || 0;
        var y2 = options.y2 || LCD_HEIGHT;
        var y1 = options.x1 || 0;
        var y1 = options.x2 || LCD_WIDTH;
        ce.reset();
        spi.write(0x2A,dc);
        //spi.write(0,1,0,LCD_WIDTH); // WORKS: made the trick for non-italic text drawn at once ! :D
        spi.write(0,1+ x1,0,x2);
        spi.write(0x2B,dc);
        //spi.write(0,0,0,LCD_HEIGHT); // otherwise, we had spi.write(0,y+rowStart,0,y+rowStart+1);
        spi.write(0,y1,0,y2);
        spi.write(0x2C,dc);
        var lines = 16; // size of buffer to use for un-paletting
        //var a = new Uint16Array(LCD_WIDTH*lines); // default
        var a = new Uint16Array((x2-x1)*lines); // default
        //for (var y=0;y<LCD_HEIGHT;y+=lines) { // default
        for (var y=0;y<y2-y1;y+=lines) { // default
          //E.mapInPlace(new Uint8Array(g.buffer, y*LCD_WIDTH*bits/8, a.length), a, palette, bits);
          E.mapInPlace(new Uint8Array(g.buffer, y*(x2-x1)*bits/8, a.length), a, palette, bits);
          spi.write(a.buffer);
        }
        ce.set();
      }
      };
    

    This being said, I'm onto some fun with g.buffer ;)

    I hope the above 'll work as is ( I still have to make my 1st own Espruino build before trying further modding the src ;)

  • Surely a partial clear is just a fillRect with the background colour? I'm not sure I see the need for a whole extra function there?

    And yes, pass true to getModified - after you've sent the changed area to the screen you want everything reset to 'unmodified' so you're ready for the next time...

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

ST7735 128x128 LCDs

Posted by Avatar for Jorgen @Jorgen

Actions