One possible application of ESP32 WROVER’s PSRAM memory is using it as a (double) display buffer. I thought this could be useful for fractal-based animations on a TFT display, as these sketches often require a considerable amount of calculations for each pixel, followed by an update of that single pixel on the display, This way, a full refresh of the display can take a lot of time, but by writing calculated color values to a PSRAM buffer instead, the actual refresh can be done much faster by pushing the entire buffer to the display after all pixel colors have been calculated in a background process (even simultaneously on a different core, using a double buffering technique).
As a proof of concept, I converted one of my Julia Fractal sketches to a version that continuously draws a specific Julia Set Jc with an incrementing zoom level. The first result is satisfying enough, even though my pixel-by-pixel approach is probably not the most efficient way to interact with PSRAM. I may post a video in due course, but for now you can find the prototype sketch for a 320×240 display at the end of this post. It uses both fast cores and a double buffering technique: while one buffer is being filled by a core 1 task, the other one can be read by a core 0 task for filling the display.
Perhaps this display buffer concept can finally make aircraft position updates in my ‘What’s Up’ Flight Radar sketch completely flicker free. Instead of always having to save clean map tiles from the diplay (with readPixel) before drawing updated aircraft sprites on them, it will allow me to restore the appropriate tiles directly from the full map image stored in PSRAM. That will circumvent my long time problem with the only fast library for the HX8357D display (Bodmer’s TFT_eSPI library): it’s readPixel() function doesn’t work on that display, so until now I couldn’t use it for my Flight Radar sketches. No longer having to use readPixel allows me drive this nice 480×320 display in that library’s parallel mode, which is very fast! With the maximum of 12 aircraft on display, updating all positions will take < 80 milliseconds. So my next post may be ‘What’s Up – final version‘.
Here’s a quick and dirty dual core ‘Julia Fractal Zoomer’ sketch. For some reason, omitting the vTaskDelay command in loop() makes it run slower! Leaving out these commands from the task functions will crash the ESP32. Make sure to enable PSRAM before compiling the sketch on a WROVER based ESP32 (option will appear under ‘Tools’ in the Arduino IDE or can be set with make menuconfig if you use Espressif’s ESP-IDF).
[UPDATE] the prototype sketch below ran much faster after dividing calculations for the new display buffer over both cores and, as expected, by replacing the individual drawPixel commands by pushing the entire display buffer in one single SPI transaction.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
// pF 16-06-2019 - Prototype of 'Julia fractal zoomer' with double display buffer in PSRAM. // Dual core sketch for ESP32 WROVER board and 320x240 display. #include <SPI.h> #include <TFT_eSPI.h> #include <math.h> // display dimensions: #define pixHor 320 #define pixVer 240 // Julia stuff: #define maxIter 128 float sx = 0, sy = 0; uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0; int moveX = 0.2; int moveY = 0.7; float zoom, z_step; int kleur[maxIter + 1]; // PSRAM stuff: uint16_t* psram_buffer = NULL; unsigned long buf_size = 153600; // 2 x buffer space for 320x240 16-bits RGB565 color values int rbufIndex, wbufIndex; unsigned long bufOffset[2] = {0, 76800}; unsigned long readpointer; TFT_eSPI tft = TFT_eSPI(); // Dual Core Business: TaskHandle_t fillBuffer; TaskHandle_t readBuffer; void setup() { zoom = 1.2; z_step = 0.1; rbufIndex = 0; wbufIndex = 1; psram_buffer = (uint16_t*)ps_malloc(buf_size); tft.init(); tft.setRotation(3); tft.fillScreen(TFT_BLACK); for (int j = 0; j < maxIter; j++) { double i = (j * 255 / 256); int red = round(sin(0.024 * i + 0) * 127 + 128); int green = round(sin(0.024 * i + 2) * 127 + 128); int blue = round(sin(0.024 * i + 4) * 127 + 128); kleur[j] = (red << 11) + (green << 5) + blue; } kleur[maxIter] = 0; xTaskCreatePinnedToCore(fillBuffer_task, "fillBuffer", 10000, NULL, 1, &fillBuffer, 1); xTaskCreatePinnedToCore( readBuffer_task, "readBuffer", 10000, NULL, 1, &readBuffer, 0); } void fillBuffer_task( void * parameter) { for (;;) { if (rbufIndex != wbufIndex) { drawJulia(zoom, bufOffset[wbufIndex]); wbufIndex ^= 1; zoom = zoom + z_step; } vTaskDelay(2 / portTICK_PERIOD_MS); } } void readBuffer_task( void * parameter) { for (;;) { if (rbufIndex != wbufIndex) { readpointer = bufOffset[rbufIndex]; for (int px = 0; px < pixHor; px++) { for (int py = 0; py < pixVer; py++) { tft.drawPixel(px, py, psram_buffer[readpointer]); readpointer++; } } rbufIndex ^= 1; } vTaskDelay(2 / portTICK_PERIOD_MS); } } void drawJulia(float zoomlevel, unsigned long buf_pointer) { unsigned long pspointer = buf_pointer; for (int px = 0; px < pixHor; px++) { for (int py = 0; py < pixVer; py++) { float x0 = -0.79; float yy0 = -0.15; float xx = 1.5 * (px - pixHor / 2) / (0.5 * zoomlevel * pixHor) + moveX; float yy = (py - pixVer / 2) / (0.5 * zoomlevel * pixVer) + moveY; int iteration = 0; int max_iteration = 128; while ( ((xx * xx + yy * yy) < 4) && (iteration < max_iteration) ) { float xtemp = xx * xx - yy * yy + x0; yy = 2 * xx * yy + yy0; xx = xtemp; iteration++; } int color = kleur[iteration]; //tft.drawPixel(px, py, color); psram_buffer[pspointer] = color; pspointer++; } } } void loop() { vTaskDelay(2 / portTICK_PERIOD_MS); } |