espruino / Espruino

The Espruino JavaScript interpreter - Official Repo
http://www.espruino.com/
Other
2.79k stars 747 forks source link

What about extend SPI with DMA to use TFC LCD's in unbuffered mode for all boards #1884

Closed MaBecker closed 4 years ago

MaBecker commented 4 years ago

In times of 1bit or 2bit display standard SPI is expectable fast, but looking at those TFT LCD's like in Bangle.JS you like to use those kind of displays with other boards too.

Yes, there are already some modules available to use them with limitation in color because of using buffered mode. Yes, Bangle.JS is using eight GPIOS for data transfer and most of the other display do not offer this feature so they will never be as fast as Bangle.JS. But using DMA can make them much much faster.

How could this be done ?

In my mind this will allow writing fast spi display modules base on the fast spi class

Would be great to have a module based implementation using on FastSPI.

Let me know how you think about this.

gfwilliams commented 4 years ago

SPI should now already use DMA as far as I'm aware - maybe not on ESPxx but on nRF52 (and I think STM32) as well.

Honestly, the issue here is the JS execution speed. You're never going to have a fast SPI LCD if you have to call into JS for every pixel, so I think the TFTLCD_Module approach is needed.

There is already an SPI LCD driver for buffered mode that can be compiled in here: https://github.com/espruino/Espruino/blob/master/libs/graphics/lcd_spilcd.c

But I believe @fanoush has an SPI LCD driver for unbuffered mode, and maybe we could pull that in? I think he even had a version using Inline C?

However I'm not sure about having this built into every firmware - flash space is so tight on many platforms now I think this is something that'd need compiling in on request.

MaBecker commented 4 years ago

Thanks - Yep, it is all about must have and nice to have functions and modules:)

I guess you mean this nice piece of code:

https://gist.github.com/fanoush/3dede6a16cef85fbf55f9d925521e4a0

I will soon have a TFT LCD standalone display in my hands to run some tests with all types of Espruino boards (Pico, Pixel, Wifi and MDBQ42T) and come back with some results.

MaBecker commented 4 years ago

So this is what is getting more and more concret:

var initData = [
  .... lcd_tft specifc data
];

SPI1.setup({sck:SCK, mosi:MOSI, baud: 30000000});

var g = tft_lcd_unbuf.connect(SPI1,
                      {dc:DC, reset:RST cs:CS,
                       height: 320, width:240,
                       colstart:0,rowstart:20},
                       initData);

/* do your drawings */

Any comments about this approch?

Edit: tft_lcd_unbuf is c code based

gfwilliams commented 4 years ago

Well, IMO the init code could just be done in JavaScript. Speed is no issue and it's far more flexible re. reset (what if the reset wire is connected via an IO expander?). Literally all we care about is the code that sends the coordinates followed by the pixel color (and maybe checks to see if it can send other pixels without re-sending coordinates).

MaBecker commented 4 years ago

Sample code how to use a module working with a generic unbuffered lcd spi driver with 16bit color depth

M5 = {
    LCD_MODEL: "ST7735",
    LCD_WIDTH: 80, LCD_HEIGHT: 160, LCD_MOSI: D15, LCD_SCK: D13, LCD_CS: D5,
    LCD_DC: D23, LCD_RST: D18 };

draw = function(){
    var times = 0;
    g.clear();
    g.setRotation(1);
    x = g.getWidth();  y = g.getHeight();
    setInterval(function() {
        g.setColor(Math.random(), Math.random(), Math.random());
        g.fillRect(Math.random() * x, Math.random() * y,
                   Math.random() * x, Math.random() * y);
        g.setColor(0xffff).setFont("6x8", 4).drawString(times, 20, 10, true);
        times++;
    }, 500);
};

SPI1.setup({ sck: M5.LCD_SCK, mosi: M5.LCD_MOSI, baud: 30000000 });

var g = require("ST7735-UB").connect(SPI1,
          { 
            // chip select pin
            cs: M5.LCD_CS, 
            // 
            dc: M5.LCD_DC,
            // x
            width: M5.LCD_WIDTH, 
            // y
            height: M5.LCD_HEIGHT,
            // offset for x
            colstart:0, 
            // offset for y
            rowstart:0, 
            // inverse  true|false
            inverse: 0
          },draw);

Module ST7735-UB.js is based on ST7735.js

/*
documentation is to be added
*/
var LCD_WIDTH = 240,
    LCD_HEIGHT = 320,
    INVERSE = 0;

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 {
        //ST7735_SWRESET
        cmd(0x01);
    }
    setTimeout(function() {
        //ST7735_SLPOUT
        cmd(0x11);
        setTimeout(function() {
            //ST7735_FRMCTR1: Set Frame rate ctrl - normal mode, 3 args: (Rate = fosc/(1x2+40) * (LINE+2C+2D))
            cmd(0xB1, [0x01, 0x2C, 0x2D]);
            //ST7735_FRMCTR2: Set Frame rate control - idle mode, 3 args: Rate = fosc/(1x2+40) * (LINE+2C+2D) 
            cmd(0xB2, [0x01, 0x2C, 0x2D]);
            //ST7735_FRMCTR3: Set Frame rate ctrl - partial mode, 6 args: Dot inversion mode + Line inversion mode
            cmd(0xB3, [0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D]);
            // ST7735_INVCTR: Set Display inversion ctrl, 1 arg, no delay: No inversion
            cmd(0xB4, 0x07);
            //ST7735_PWCTR1: Set Power control, 3 args, no delay: init + -4.6V + AUTO mode
            cmd(0xC0, [0xA2, 0x02, 0x84]);
            //ST7735_PWCTR2: Set Power control, 1 arg, no delay: VGH25 = 2.4C VGSEL = -10 VGH = 3 * AVDD
            cmd(0xC1, 0xC5);
            //ST7735_PWCTR3: Set Power control, 2 args, no delay: Opamp current small + Boost frequency
            cmd(0xC2, [0x0A, 0x00]);
            //ST7735_PWCTR4: Set Power control, 2 args, no delay: BCLK/2, Opamp current small & Medium low 
            cmd(0xC3, [0x8A, 0x2A]);
            //ST7735_PWCTR5: Set Power control, 2 args, no delay:
            cmd(0xC4, [0x8A, 0xEE]);
            //ST7735_VMCTR1: Set Power control, 1 arg, no delay:
            cmd(0xC5, 0x0E);
            if (INVERSE) {
                //ST7735_INVONN: Invert display, no args, no delay
                cmd(0x21, 0x00);
                //ST7735_INVOFF: Don't invert display, no args, no delay
            } else {
                //ST7735_INVOFF: Don't invert display, no args, no delay
                cmd(0x20, 0x00);
            }
            //ST7735_MADCTL: Set Memory access control (directions), 1 arg: row addr/col addr, bottom to top refresh
            cmd(0x36, 0xC8);
            //ST7735_COLMOD: Set color mode, 1 arg, no delay: 16-bit color 
            cmd(0x3A, 0x05);
            //ST7735_CASET: Set Column addr set, 4 args, no delay: XSTART = 0 + XEND = 127
            cmd(0x2A, [0x00, 0x00, 0x00, LCD_WIDTH - 1]);
            //ST7735_RASET: Set Row addr set, 4 args, no delay: XSTART = 0 + XEND = 127
            cmd(0x2B, [0x00, 0x00, 0x00, LCD_HEIGHT - 1]);
            //ST7735_GMCTRP1: color and gamma correction
            cmd(0xE0, [0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29, 0x25, 0x2B, 0x39, 0x00, 0x01, 0x03, 0x10]);
            //ST7735_GMCTRN1: color and gamma correction
            cmd(0xE1, [0x03, 0x1d, 0x07, 0x06, 0x2E, 0x2C, 0x29, 0x2D, 0x2E, 0x2E, 0x37, 0x3F, 0x00, 0x00, 0x02, 0x10]);
            //ST7735_NORON: Set Normal display on, no args, w/delay: 10 ms delay
            cmd(0x13);
            //ST7735_DISPON: Set Main screen turn on, no args w/delay: 100 ms delay
            cmd(0x29);
            if (callback) callback();
        }, 50);
    }, 200);
}

exports.connect = function(spi, opt, callback) {
    try {
        console.log(opt);
        var g = lcd_spi_unbuf.connect(SPI1, {
            dc: opt.dc,
            cs: opt.cs,
            height: opt.height,
            width: opt.width,
            colstart: opt.colstart,
            rowstart: opt.rowstart
        });
        LCD_HEIGHT = opt.height;
        LCD_WIDTH = opt.width;
        INVERSE = opt.inv;
        init(spi, opt.dc, opt.cs, opt.rst, callback);
        g.lcd_on = function() {
            opt.dc.reset();
            spi.write(0x29, opt.cs);
        };
        g.lcd_off = function() {
            opt.dc.reset();
            spi.write(0x28, opt.cs);
        };
        return g;
    } catch (e) {
        console.log("catch:", e);
        return null;
    }
};

The c-code returns object of Graphics and handle the whole data transfer.

/*JSON{
  "type" : "class",
  "class" : "lcd_spi_unbuf"
}*/

..........

// using jshSPISendMany() to send data

..........

/*JSON{
  "type" : "staticmethod",
  "class" : "lcd_spi_unbuf",
  "name" : "connect",
  "generate" : "jswrap_lcd_spi_unbuf_connect",
  "params" : [
    ["device","JsVar","The used SPI device"],
    ["options","JsVar","An Object containing extra information"]
  ],
  "return" : ["JsVar","The new Graphics object"],
  "return_object" : "Graphics"
}*/
JsVar *jswrap_lcd_spi_unbuf_connect(JsVar *device, JsVar *options) { 
  JsVar *parent = jspNewObject(0, "Graphics");
  if (!parent) {
    return 0;  
  }

  JshLCD_SPI_UNBUFInfo inf;

  if (!jsspiPopulateOptionsInfo(&inf, options)) {
    jsiConsolePrint("ERROR pins not supplied\r\n");
    return;
  }
  _pin_cs = inf.pinCS;
  _pin_dc = inf.pinDC;
  _colstart = inf.colstart;
  _rowstart = inf.rowstart;
  _device = jsiGetDeviceFromClass(device);

  if (!DEVICE_IS_SPI(_device)) { // TODO support software spi
    jsiConsolePrint("No Software spi supported for now\r\n");
    return;
  }  

  JsGraphics gfx;
  graphicsStructInit(&gfx,inf.width,inf.height,16);
  gfx.data.type = JSGRAPHICSTYPE_LCD_SPI_UNBUF;
  gfx.graphicsVar = parent;

  jshPinOutput(_pin_dc, 1);
  jshPinSetValue(_pin_dc, 1);

  jshPinOutput(_pin_cs, 1);
  jshPinSetValue(_pin_cs, 1);

  lcd_spi_unbuf_setCallbacks(&gfx);
  graphicsSetVar(&gfx); 
  return parent;
}

..........

Please advise if further things should be changed or added.

MaBecker commented 4 years ago
var g = require("ST7735S-UB").connect(SPI1,
          {
            cs: D5,
            dc: D23,
            width: 80
            height: 160,
            colstart:26,
            rowstart:1,
            inverse: 0
          },draw);

ST7735S_via_Espruino

MaBecker commented 4 years ago
var g = require("ILI9341-UB").connect(SPI1,
          { cs: D14,
            dc: D27,
            width: 240,
            height: 320,
            colstart:0,
            rowstart:0,
            inverse : 1 
          }, draw);

ILI9341_via_Espruino

gfwilliams commented 4 years ago

That sounds good - I guess my worry is that some LCDs may not use the same commands for setting row/column and you might have to specify them?

It's the case for parallel displays: https://github.com/espruino/Espruino/blob/master/libs/graphics/lcd_fsmc.c#L1285 but maybe by now all the SPI ones have settled on the same format?

gfwilliams commented 4 years ago

Also not sure if you're considering this but for the ST7789 I store the last X/Y coordinates so that if we're blitting pixels in order (eg for an image) you can skip the 'setwindow' before the write: https://github.com/espruino/Espruino/blob/master/libs/graphics/lcd_st7789_8bit.c#L421

MaBecker commented 4 years ago

That sounds good - I guess my worry is that some LCDs may not use the same commands for setting row/column and you might have to specify them?

It's the case for parallel displays: https://github.com/espruino/Espruino/blob/master/libs/graphics/lcd_fsmc.c#L1285 but maybe by now all the SPI ones have settled on the same format?

Well, you will never be sure about this.....

MaBecker commented 4 years ago

Also not sure if you're considering this but for the ST7789 I store the last X/Y coordinates so that if we're blitting pixels in order (eg for an image) you can skip the 'setwindow' before the write: https://github.com/espruino/Espruino/blob/master/libs/graphics/lcd_st7789_8bit.c#L421

The ST7789 works with the lcd_spi_unbuf code as well even if there are boards without a CS pin and therefor require spi mode 3.

ST7789_via_Espruino

MaBecker commented 4 years ago

Next step:

Try Pico and MDBQ42T board with this class.

fanoush commented 4 years ago

I guess you mean this nice piece of code:

https://gist.github.com/fanoush/3dede6a16cef85fbf55f9d925521e4a0

sorry for late answer, was on vacation last week

as per http://forum.espruino.com/conversations/347237/ that piece was just quick hack inspired by https://github.com/espruino/Espruino/blob/master/libs/graphics/lcd_spilcd.c because that one has framebuffer hardcoded as static byte array and also has ST7735 hardcoded in https://github.com/espruino/Espruino/blob/master/libs/graphics/lcd_spilcd_info.h so it was good excuse to try inline C. And it is in buffered mode - the native/inline C code does palette conversion of next block while waiting for DMA to finish. Inline C is good for not having the code part of firmware but bad because you cannot easily use nordic sdk, link existing espruino code (SPI object) and hook into interrupts (btw hooking into unused interrupt vectors would be nice feature for inline C, will probably create issue/pull request for that later).

Anyway, decoupling display type/init string and linking into generic Graphics array buffer object instead of static byte array is good direction. If there was generic asynchronous SPI with js callback when transfer is done the rest could be done on JS side. So it is not (only) about DMA but about asynchronous writing so you could push some pixels over SPI and do something else in JS in parallel.

And BTW, not sure if it is a norm but with ST7789 in P8 watch I managed to read(!) from the display over same MOSI wire, it is not pure SPI but some bidirectional 3 wire variant described on page 58 here https://www.rhydolabz.com/documents/33/ST7789.pdf#page=58 This can be used to read pixels back or sync to refresh rate to avoid tearing. Worked with software SPI for me, had to reconfigure MOSI for MISO is SPI object on the fly after writing the command. If you have LCD module with both MISO and MOSI this is not interesting but watches often have only have MOSI wire connected. This is mostly unrelated to current topic, just that this would be another reason to prefer sticking smaller pieces together on JS side instead of having one monolithic display driver in Espruino codebase.

gfwilliams commented 4 years ago

Yeah, an async JS SPI API would be nice, but I think right now the JS execution speed is too low to really make it that sensible :(

with ST7789 in P8 watch I managed to read

That's very interesting. I don't suppose you had any luck with that on the 8 bit version in Bangle.js? That would be amazingly exciting