Closed MaBecker closed 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.
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.
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
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).
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.
var g = require("ST7735S-UB").connect(SPI1,
{
cs: D5,
dc: D23,
width: 80
height: 160,
colstart:26,
rowstart:1,
inverse: 0
},draw);
var g = require("ILI9341-UB").connect(SPI1,
{ cs: D14,
dc: D27,
width: 240,
height: 320,
colstart:0,
rowstart:0,
inverse : 1
}, draw);
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?
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
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.....
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.
Next step:
Try Pico and MDBQ42T board with this class.
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.
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
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 ?
eg. add DMA feature to the existing SPI jswrapper and to the targets
eg: SPI.setup({..... fast: true});
or eg. come up with a new lib for this, similar to neopixel and name it FastSPI
or .......
In my mind this will allow writing fast spi display modules base on the fast spi class
keep the init sequence out of the firmware to handle any tft lcd
add the init commands to an array in a special way to easily decode and send them
use require ('TFTLCD_Module').connect({, [ array with init sequence]})
Would be great to have a module based implementation using on FastSPI.
Let me know how you think about this.