395 lines
11 KiB
C++
395 lines
11 KiB
C++
// TFT_eSPI library demo, principally for STM32F processors with DMA:
|
|
// https://en.wikipedia.org/wiki/Direct_memory_access
|
|
|
|
// Tested with ESP32, Nucleo 64 STM32F446RE and Nucleo 144 STM32F767ZI
|
|
// TFT's with SPI can use DMA, the sketch also works with 8 bit
|
|
// parallel TFT's (tested with ILI9341 and ILI9481)
|
|
|
|
// The sketch will run on processors without DMA and also parallel
|
|
// interface TFT's. Comment out line 29 for no DMA.
|
|
|
|
// Library here:
|
|
// https://github.com/Bodmer/TFT_eSPI
|
|
|
|
// Adapted by Bodmer 18/12/19 from "RotatingCube" by Daniel Eichhorn.
|
|
// See MIT License at end of sketch.
|
|
|
|
// The rotating cube is drawn into a 128 x 128 Sprite and then this is
|
|
// rendered to screen. The Sprite need 32Kbytes of RAM and DMA buffer the same
|
|
// so processors with at least >64Kbytes RAM free will be required.
|
|
|
|
// Define to use DMA for Sprite transfer to SPI TFT, comment out to use no DMA
|
|
// (Tested with Nucleo 64 STM32F446RE and Nucleo 144 STM32F767ZI)
|
|
// STM32F767 27MHz SPI 50% processor load: Non DMA 52 fps, with DMA 101 fps
|
|
// STM32F767 27MHz SPI 0% processor load: Non DMA 97 fps, with DMA 102 fps
|
|
|
|
// ESP32 27MHz SPI 0% processor load: Non DMA 90 fps, with DMA 101 fps
|
|
// ESP32 40MHz SPI 0% processor load: Non DMA 127 fps, with DMA 145 fps
|
|
// NOTE: FOR SPI DISPLAYS ONLY
|
|
#define USE_DMA_TO_TFT
|
|
|
|
// Show a processing load does not impact rendering performance
|
|
// Processing load is simple algorithm to calculate prime numbers
|
|
//#define PRIME_NUMBER_PROCESSOR_LOAD 491 // 241 = 50% CPU load for 128 * 128 and STM32F466 Nucleo 64
|
|
// 491 = 50% CPU load for 128 * 128 and STM32F767 Nucleo 144
|
|
|
|
// Color depth has to be 16 bits if DMA is used to render image
|
|
#define COLOR_DEPTH 16
|
|
|
|
// 128x128 for a 16 bit colour Sprite (32Kbytes RAM)
|
|
// Maximum is 181x181 (64Kbytes) for DMA - restricted by processor design
|
|
#define IWIDTH 128
|
|
#define IHEIGHT 128
|
|
|
|
// Size of cube image
|
|
// 358 is max for 128x128 sprite, too big and pixel trails are drawn...
|
|
#define CUBE_SIZE 358
|
|
|
|
#include <TFT_eSPI.h>
|
|
|
|
// Library instance
|
|
TFT_eSPI tft = TFT_eSPI(); // Declare object "tft"
|
|
|
|
// Create two sprites for a DMA toggle buffer
|
|
TFT_eSprite spr[2] = {TFT_eSprite(&tft), TFT_eSprite(&tft) };
|
|
|
|
// Toggle buffer selection
|
|
bool sprSel = 0;
|
|
|
|
// Pointers to start of Sprites in RAM
|
|
uint16_t* sprPtr[2];
|
|
|
|
// Define the cube face colors
|
|
uint16_t palette[] = {TFT_WHITE, // 1
|
|
TFT_GREENYELLOW, // 2
|
|
TFT_YELLOW, // 3
|
|
TFT_PINK, // 4
|
|
TFT_MAGENTA, // 5
|
|
TFT_CYAN // 6
|
|
};
|
|
|
|
// Used for fps measuring
|
|
uint16_t counter = 0;
|
|
long startMillis = millis();
|
|
uint16_t interval = 100;
|
|
|
|
// size / 2 of cube edge
|
|
float d = 15;
|
|
float px[] = {
|
|
-d, d, d, -d, -d, d, d, -d
|
|
};
|
|
float py[] = {
|
|
-d, -d, d, d, -d, -d, d, d
|
|
};
|
|
float pz[] = {
|
|
-d, -d, -d, -d, d, d, d, d
|
|
};
|
|
|
|
// Define the triangles
|
|
// The order of the vertices MUST be CCW or the
|
|
// shoelace method won't work to detect visible edges
|
|
int faces[12][3] = {
|
|
{0, 1, 4},
|
|
{1, 5, 4},
|
|
{1, 2, 5},
|
|
{2, 6, 5},
|
|
{5, 7, 4},
|
|
{6, 7, 5},
|
|
{3, 4, 7},
|
|
{4, 3, 0},
|
|
{0, 3, 1},
|
|
{1, 3, 2},
|
|
{2, 3, 6},
|
|
{6, 3, 7}
|
|
};
|
|
|
|
// mapped coordinates on screen
|
|
float p2x[] = {
|
|
0, 0, 0, 0, 0, 0, 0, 0
|
|
};
|
|
float p2y[] = {
|
|
0, 0, 0, 0, 0, 0, 0, 0
|
|
};
|
|
|
|
// rotation angle in radians
|
|
float r[] = {
|
|
0, 0, 0
|
|
};
|
|
|
|
// Frames per second
|
|
String fps = "0fps";
|
|
|
|
// Sprite draw position
|
|
int16_t xpos = 0;
|
|
int16_t ypos = 0;
|
|
|
|
// Prime number initial value
|
|
int prime_max = 2;
|
|
|
|
// 3 axis spin control
|
|
bool spinX = true;
|
|
bool spinY = true;
|
|
bool spinZ = true;
|
|
|
|
// Min and max of cube edges, "int" type used for compatibility with original sketch min() function
|
|
int xmin,ymin,xmax,ymax;
|
|
|
|
/////////////////////////////////////////////////////////// setup ///////////////////////////////////////////////////
|
|
void setup() {
|
|
|
|
Serial.begin(115200);
|
|
|
|
tft.init();
|
|
|
|
tft.fillScreen(TFT_BLACK);
|
|
|
|
xpos = 0;
|
|
ypos = (tft.height() - IHEIGHT) / 2;
|
|
|
|
// Define cprite colour depth
|
|
spr[0].setColorDepth(COLOR_DEPTH);
|
|
spr[1].setColorDepth(COLOR_DEPTH);
|
|
|
|
// Create the 2 sprites
|
|
sprPtr[0] = (uint16_t*)spr[0].createSprite(IWIDTH, IHEIGHT);
|
|
sprPtr[1] = (uint16_t*)spr[1].createSprite(IWIDTH, IHEIGHT);
|
|
|
|
// Define text datum and text colour for Sprites
|
|
spr[0].setTextColor(TFT_BLACK);
|
|
spr[0].setTextDatum(MC_DATUM);
|
|
spr[1].setTextColor(TFT_BLACK);
|
|
spr[1].setTextDatum(MC_DATUM);
|
|
|
|
#ifdef USE_DMA_TO_TFT
|
|
// DMA - should work with ESP32, STM32F2xx/F4xx/F7xx processors
|
|
// NOTE: >>>>>> DMA IS FOR SPI DISPLAYS ONLY <<<<<<
|
|
tft.initDMA(); // Initialise the DMA engine (tested with STM32F446 and STM32F767)
|
|
#endif
|
|
|
|
// Animation control timer
|
|
startMillis = millis();
|
|
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////// loop ///////////////////////////////////////////////////
|
|
void loop() {
|
|
uint32_t updateTime = 0; // time for next update
|
|
bool bounce = false;
|
|
int wait = 0; //random (20);
|
|
|
|
// Random movement direction
|
|
int dx = 1; if (random(2)) dx = -1;
|
|
int dy = 1; if (random(2)) dy = -1;
|
|
|
|
// Grab exclusive use of the SPI bus
|
|
tft.startWrite();
|
|
|
|
// Loop forever
|
|
while (1) {
|
|
|
|
// Pull it back onto screen if it wanders off
|
|
if (xpos < -xmin) {
|
|
dx = 1;
|
|
bounce = true;
|
|
}
|
|
if (xpos >= tft.width() - xmax) {
|
|
dx = -1;
|
|
bounce = true;
|
|
}
|
|
if (ypos < -ymin) {
|
|
dy = 1;
|
|
bounce = true;
|
|
}
|
|
if (ypos >= tft.height() - ymax) {
|
|
dy = -1;
|
|
bounce = true;
|
|
}
|
|
|
|
if (bounce) {
|
|
// Randomise spin
|
|
if (random(2)) spinX = true;
|
|
else spinX = false;
|
|
if (random(2)) spinY = true;
|
|
else spinY = false;
|
|
if (random(2)) spinZ = true;
|
|
else spinZ = false;
|
|
bounce = false;
|
|
//wait = random (20);
|
|
}
|
|
|
|
if (updateTime <= millis())
|
|
{
|
|
// Use time delay so sprtie does not move fast when not all on screen
|
|
updateTime = millis() + wait;
|
|
xmin = IWIDTH / 2; xmax = IWIDTH / 2; ymin = IHEIGHT / 2; ymax = IHEIGHT / 2;
|
|
drawCube();
|
|
|
|
#ifdef USE_DMA_TO_TFT
|
|
if (tft.dmaBusy()) prime_max++; // Increase processing load until just not busy
|
|
tft.pushImageDMA(xpos, ypos, IWIDTH, IHEIGHT, sprPtr[sprSel]);
|
|
sprSel = !sprSel;
|
|
#else
|
|
#ifdef PRIME_NUMBER_PROCESSOR_LOAD
|
|
prime_max = PRIME_NUMBER_PROCESSOR_LOAD;
|
|
#endif
|
|
spr[sprSel].pushSprite(xpos, ypos); // Blocking write (no DMA) 115fps
|
|
#endif
|
|
|
|
counter++;
|
|
// only calculate the fps every <interval> iterations.
|
|
if (counter % interval == 0) {
|
|
long millisSinceUpdate = millis() - startMillis;
|
|
fps = String((int)(interval * 1000.0 / (millisSinceUpdate))) + " fps";
|
|
Serial.println(fps);
|
|
startMillis = millis();
|
|
}
|
|
#ifdef PRIME_NUMBER_PROCESSOR_LOAD
|
|
// Add a processor task
|
|
uint32_t pr = computePrimeNumbers(prime_max);
|
|
Serial.print("Biggest = "); Serial.println(pr);
|
|
#endif
|
|
// Change coord for next loop
|
|
xpos += dx;
|
|
ypos += dy;
|
|
}
|
|
} // End of forever loop
|
|
|
|
// Release exclusive use of SPI bus ( here as a reminder... forever loop prevents execution)
|
|
tft.endWrite();
|
|
}
|
|
|
|
/**
|
|
Detected visible triangles. If calculated area > 0 the triangle
|
|
is rendered facing towards the viewer, since the vertices are CCW.
|
|
If the area is negative the triangle is CW and thus facing away from us.
|
|
*/
|
|
int shoelace(int x1, int y1, int x2, int y2, int x3, int y3) {
|
|
// (x1y2 - y1x2) + (x2y3 - y2x3)
|
|
return x1 * y2 - y1 * x2 + x2 * y3 - y2 * x3 + x3 * y1 - y3 * x1;
|
|
}
|
|
|
|
/**
|
|
Rotates and renders the cube.
|
|
**/
|
|
void drawCube()
|
|
{
|
|
double speed = 90;
|
|
if (spinX) r[0] = r[0] + PI / speed; // Add a degree
|
|
if (spinY) r[1] = r[1] + PI / speed; // Add a degree
|
|
if (spinZ) r[2] = r[2] + PI / speed; // Add a degree
|
|
|
|
if (r[0] >= 360.0 * PI / 90.0) r[0] = 0;
|
|
if (r[1] >= 360.0 * PI / 90.0) r[1] = 0;
|
|
if (r[2] >= 360.0 * PI / 90.0) r[2] = 0;
|
|
|
|
float ax[8] = {0, 0, 0, 0, 0, 0, 0, 0};
|
|
float ay[8] = {0, 0, 0, 0, 0, 0, 0, 0};
|
|
float az[8] = {0, 0, 0, 0, 0, 0, 0, 0};
|
|
|
|
// Calculate all vertices of the cube
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
float px2 = px[i];
|
|
float py2 = cos(r[0]) * py[i] - sin(r[0]) * pz[i];
|
|
float pz2 = sin(r[0]) * py[i] + cos(r[0]) * pz[i];
|
|
|
|
float px3 = cos(r[1]) * px2 + sin(r[1]) * pz2;
|
|
float py3 = py2;
|
|
float pz3 = -sin(r[1]) * px2 + cos(r[1]) * pz2;
|
|
|
|
ax[i] = cos(r[2]) * px3 - sin(r[2]) * py3;
|
|
ay[i] = sin(r[2]) * px3 + cos(r[2]) * py3;
|
|
az[i] = pz3 - 150;
|
|
|
|
p2x[i] = IWIDTH / 2 + ax[i] * CUBE_SIZE / az[i];
|
|
p2y[i] = IHEIGHT / 2 + ay[i] * CUBE_SIZE / az[i];
|
|
}
|
|
|
|
// Fill the buffer with colour 0 (Black)
|
|
spr[sprSel].fillSprite(TFT_BLACK);
|
|
|
|
for (int i = 0; i < 12; i++) {
|
|
|
|
if (shoelace(p2x[faces[i][0]], p2y[faces[i][0]], p2x[faces[i][1]], p2y[faces[i][1]], p2x[faces[i][2]], p2y[faces[i][2]]) > 0) {
|
|
int x0 = p2x[faces[i][0]];
|
|
int y0 = p2y[faces[i][0]];
|
|
int x1 = p2x[faces[i][1]];
|
|
int y1 = p2y[faces[i][1]];
|
|
int x2 = p2x[faces[i][2]];
|
|
int y2 = p2y[faces[i][2]];
|
|
|
|
xmin = min(xmin, x0);
|
|
ymin = min(ymin, y0);
|
|
xmin = min(xmin, x1);
|
|
ymin = min(ymin, y1);
|
|
xmin = min(xmin, x2);
|
|
ymin = min(ymin, y2);
|
|
xmax = max(xmax, x0);
|
|
ymax = max(ymax, y0);
|
|
xmax = max(xmax, x1);
|
|
ymax = max(ymax, y1);
|
|
xmax = max(xmax, x2);
|
|
ymax = max(ymax, y2);
|
|
|
|
spr[sprSel].fillTriangle(x0, y0, x1, y1, x2, y2, palette[i / 2]);
|
|
if (i % 2) {
|
|
int avX = 0;
|
|
int avY = 0;
|
|
for (int v = 0; v < 3; v++) {
|
|
avX += p2x[faces[i][v]];
|
|
avY += p2y[faces[i][v]];
|
|
}
|
|
avX = avX / 3;
|
|
avY = avY / 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
//spr[sprSel].drawString(fps, IWIDTH / 2, IHEIGHT / 2, 4);
|
|
//delay(100);
|
|
}
|
|
|
|
// This is to provide a processing load to see the improvement DMA gives
|
|
uint32_t computePrimeNumbers(int32_t n) {
|
|
if (n<2) return 1;
|
|
|
|
int32_t i, fact, j, p = 0;
|
|
|
|
//Serial.print("\nPrime Numbers are: \n");
|
|
for (i = 1; i <= n; i++)
|
|
{
|
|
fact = 0;
|
|
for (j = 1; j <= n; j++)
|
|
{
|
|
if (i % j == 0)
|
|
fact++;
|
|
}
|
|
if (fact == 2) {
|
|
p = i;//Serial.print(i); Serial.print(", ");
|
|
}
|
|
}
|
|
//Serial.println();
|
|
return p; // Biggest
|
|
}
|
|
|
|
/*
|
|
Original licence:
|
|
The MIT License (MIT)
|
|
Copyright (c) 2017 by Daniel Eichhorn
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
*/
|