ESP32 Arduino ILI9341 Bitcoin TFT Binance API Hardware Experiment

Bitcoin Preisanzeige mit ESP32 & ILI9341

Ein selbst gebautes Bitcoin-Dashboard auf einem 2,4"-TFT-Display – ESP32 WROOM holt alle 30 Sekunden den BTC/EUR-Kurs und 4H-Candlestick-Daten live von der Binance API.

Bitcoin Dashboard auf dem ILI9341 – aktueller BTC/EUR-Kurs mit 4H-Candlestick-Chart

Idee

Statt Bitcoin-Kurs im Browser nachschauen – ein dediziertes Display, das dauerhaft läuft und immer den aktuellen Preis zeigt. Mit einem ESP32 und einem günstigen TFT-Display lässt sich das in einem Nachmittag zusammenbauen.

Das Dashboard zeigt:

  • Aktueller BTC/EUR-Kurs in Echtzeit
  • 24h-Änderung als farbiges Badge (grün/rot)
  • Candlestick-Chart der letzten 5 Tage im 4H-Intervall (30 Kerzen)
  • Automatisches Update alle 30 Sekunden via Binance API

Hardware

ESP32 WROOM-32 Microcontroller

Verdrahtung

Das Display kommuniziert über SPI mit dem ESP32. Touch-Pins teilen sich den SPI-Bus mit dem Display.

Verkabelung zwischen ESP32 und ILI9341 Display

Schaltplan: ILI9341 Display ↔ ESP32 WROOM Pin-Belegung

ILI9341 PinESP32 GPIOBeschreibung
VCC3.3VStromversorgung
GNDGNDMasse
CSGPIO 5Chip Select Display
RESETGPIO 4Reset
DCGPIO 2Data/Command
SDI (MOSI)GPIO 23SPI Daten (Master→Slave)
SCKGPIO 18SPI Takt
LED3.3VHintergrundbeleuchtung
SDO (MISO)GPIO 19SPI Daten (Slave→Master)
T_CLKGPIO 18Touch Takt (geteilt)
T_CSGPIO 15Chip Select Touch
T_DINGPIO 23Touch Daten (geteilt)
T_DOGPIO 19Touch Ausgabe (geteilt)
T_IRQGPIO 36 (VP)Touch Interrupt

Software

Benötigte Libraries

In der Arduino IDE über den Library Manager installieren:

  • TFT_eSPI – Hochperformante TFT-Bibliothek für ESP32/ESP8266
  • ArduinoJson – JSON-Parsing für die Binance API-Antworten
  • WiFi und HTTPClient – bereits in der ESP32-Boardunterstützung enthalten

TFT_eSPI konfigurieren

Vor dem ersten Upload muss die User_Setup.h in der TFT_eSPI-Bibliothek angepasst werden – dort die Pin-Belegung eintragen, die zur Verdrahtung oben passt:

#define TFT_CS   5
#define TFT_DC   2
#define TFT_RST  4
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_MISO 19
#define ILI9341_DRIVER

Sketch

WLAN-Zugangsdaten am Anfang des Sketches eintragen – der Rest läuft automatisch:

#include <TFT_eSPI.h>
#include <SPI.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// ============================================================
//  HIER DEINE WLAN-DATEN EINTRAGEN
// ============================================================
const char* WIFI_SSID     = "DEIN_WLAN_NAME";
const char* WIFI_PASSWORD = "DEIN_WLAN_PASSWORT";
// ============================================================

TFT_eSPI tft = TFT_eSPI();

#define COL_BG       TFT_BLACK
#define COL_HEADER   0x1082
#define COL_GOLD     0xFEA0
#define COL_GREEN    0x07E0
#define COL_RED      0xF800
#define COL_WHITE    TFT_WHITE
#define COL_GRAY     0x8410
#define COL_DARKGRAY 0x2104
#define COL_GRIDLINE 0x1862

#define CANDLE_COUNT 30

float candleOpen[CANDLE_COUNT];
float candleClose[CANDLE_COUNT];
float candleHigh[CANDLE_COUNT];
float candleLow[CANDLE_COUNT];
long  candleTime[CANDLE_COUNT];

float currentPrice = 0;
float priceChange  = 0;

unsigned long lastFetch = 0;
#define FETCH_INTERVAL 30000

#define CHART_X   0
#define CHART_Y   130
#define CHART_W   240
#define CHART_H   155
#define DATE_Y    286
#define FOOTER_Y  310

void setup() {
  Serial.begin(115200);
  tft.init();
  tft.setRotation(0);
  tft.fillScreen(COL_BG);
  drawBootScreen();
  connectWiFi();
  fetchData();
}

void loop() {
  if (millis() - lastFetch >= FETCH_INTERVAL) {
    fetchData();
  }
}

void connectWiFi() {
  tft.setTextColor(COL_WHITE, COL_BG);
  tft.setTextSize(1);
  tft.setCursor(10, 160);
  tft.print("Verbinde mit WLAN...");
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  int tries = 0;
  while (WiFi.status() != WL_CONNECTED && tries < 40) {
    delay(500);
    tries++;
  }
  tft.setCursor(10, 175);
  if (WiFi.status() == WL_CONNECTED) {
    tft.setTextColor(COL_GREEN, COL_BG);
    tft.print("WLAN verbunden!    ");
  } else {
    tft.setTextColor(COL_RED, COL_BG);
    tft.print("WLAN Fehler!       ");
  }
  delay(800);
}

void fetchData() {
  if (WiFi.status() != WL_CONNECTED) return;

  // Preis + 24h Änderung in EUR
  HTTPClient http;
  http.begin("https://api.binance.com/api/v3/ticker/24hr?symbol=BTCEUR");
  if (http.GET() == HTTP_CODE_OK) {
    String body = http.getString();
    StaticJsonDocument<512> doc;
    if (!deserializeJson(doc, body)) {
      currentPrice = doc["lastPrice"].as<float>();
      priceChange  = doc["priceChangePercent"].as<float>();
    }
  }
  http.end();

  // 4H Candles in EUR
  http.begin("https://api.binance.com/api/v3/klines?symbol=BTCEUR&interval=4h&limit=30");
  if (http.GET() == HTTP_CODE_OK) {
    String body = http.getString();
    DynamicJsonDocument doc(12288);
    if (!deserializeJson(doc, body)) {
      for (int i = 0; i < CANDLE_COUNT; i++) {
        candleTime[i]  = doc[i][0].as<long>();
        candleOpen[i]  = doc[i][1].as<float>();
        candleHigh[i]  = doc[i][2].as<float>();
        candleLow[i]   = doc[i][3].as<float>();
        candleClose[i] = doc[i][4].as<float>();
      }
    }
  }
  http.end();

  drawDashboard();
  lastFetch = millis();
}

void drawBootScreen() {
  tft.fillScreen(COL_BG);
  tft.fillCircle(120, 90, 50, COL_GOLD);
  tft.setTextColor(COL_BG);
  tft.setTextSize(4);
  tft.setCursor(102, 72);
  tft.print("B");
  tft.setTextColor(COL_GOLD, COL_BG);
  tft.setTextSize(2);
  tft.setCursor(40, 155);
  tft.print("BTC Dashboard");
  tft.setTextColor(COL_GRAY, COL_BG);
  tft.setTextSize(1);
  tft.setCursor(50, 180);
  tft.print("4H EUR - by Binance");
}

void drawDashboard() {
  tft.fillScreen(COL_BG);

  // Header
  tft.fillRect(0, 0, 240, 36, COL_HEADER);
  tft.fillCircle(18, 18, 12, COL_GOLD);
  tft.setTextColor(COL_BG);
  tft.setTextSize(1);
  tft.setCursor(14, 14);
  tft.print("B");
  tft.setTextColor(COL_WHITE, COL_HEADER);
  tft.setTextSize(2);
  tft.setCursor(35, 11);
  tft.print("BTCEUR  4H");

  // Preis
  tft.setTextColor(COL_GRAY, COL_BG);
  tft.setTextSize(1);
  tft.setCursor(8, 44);
  tft.print("Aktueller Preis (EUR)");

  char priceFormatted[24];
  sprintf(priceFormatted, "%.2f", currentPrice);
  tft.setTextColor(COL_GOLD, COL_BG);
  tft.setTextSize(3);
  tft.setCursor(22, 55);
  tft.print(priceFormatted);

  // 24h Badge
  uint16_t changeColor = (priceChange >= 0) ? COL_GREEN : COL_RED;
  char changeStr[12];
  sprintf(changeStr, "%+.2f%%", priceChange);
  tft.fillRoundRect(8, 100, 115, 20, 4, changeColor);
  tft.setTextColor(COL_BG);
  tft.setTextSize(1);
  tft.setCursor(13, 106);
  tft.print("24h: ");
  tft.print(changeStr);

  // Live-Punkt
  tft.fillCircle(185, 110, 4, COL_GREEN);
  tft.setTextColor(COL_GRAY, COL_BG);
  tft.setCursor(193, 106);
  tft.print("Live");

  tft.drawLine(0, 126, 240, 126, COL_HEADER);

  tft.setTextColor(COL_GRAY, COL_BG);
  tft.setTextSize(1);
  tft.setCursor(4, CHART_Y - 14);
  tft.print("BTC/EUR  4H  letzte 5 Tage");

  drawCandlesticks();

  tft.fillRect(0, FOOTER_Y, 240, 10, COL_HEADER);
  tft.setTextColor(COL_GRAY, COL_HEADER);
  tft.setTextSize(1);
  tft.setCursor(50, FOOTER_Y + 1);
  tft.print("Update alle 30 Sek.");
}

void timestampToDate(long tsMs, char* buf) {
  long ts   = tsMs / 1000;
  long days = ts / 86400;
  long z    = days + 719468;
  long era  = z / 146097;
  long doe  = z - era * 146097;
  long yoe  = (doe - doe/1460 + doe/36524 - doe/146096) / 365;
  long y    = yoe + era * 400;
  long doy  = doe - (365*yoe + yoe/4 - yoe/100);
  long mp   = (5*doy + 2) / 153;
  long d    = doy - (153*mp + 2)/5 + 1;
  long m    = mp < 10 ? mp + 3 : mp - 9;
  if (m <= 2) y++;
  sprintf(buf, "%02ld.%02ld", d, m);
}

void drawCandlesticks() {
  float minVal = candleLow[0];
  float maxVal = candleHigh[0];
  for (int i = 1; i < CANDLE_COUNT; i++) {
    if (candleLow[i]  < minVal) minVal = candleLow[i];
    if (candleHigh[i] > maxVal) maxVal = candleHigh[i];
  }
  float range = maxVal - minVal;
  if (range == 0) return;

  int scaleW = 46;
  int chartX = scaleW;
  int chartW = CHART_W - scaleW;

  tft.fillRect(chartX, CHART_Y, chartW, CHART_H, COL_DARKGRAY);

  tft.setTextColor(COL_GRAY, COL_BG);
  tft.setTextSize(1);
  for (int g = 0; g <= 3; g++) {
    float val = minVal + (range / 3.0) * g;
    int   gy  = CHART_Y + CHART_H - (int)((val - minVal) / range * CHART_H);
    tft.drawLine(chartX, gy, chartX + chartW, gy, COL_GRIDLINE);
    char buf[10];
    sprintf(buf, "%.0fk", val / 1000.0);
    tft.setCursor(0, gy - 4);
    tft.print(buf);
  }

  float slotW = (float)chartW / CANDLE_COUNT;
  int   cw    = max(1, (int)(slotW * 0.65));

  for (int i = 0; i < CANDLE_COUNT; i++) {
    int cx = chartX + (int)(i * slotW) + (int)((slotW - cw) / 2);
    int highY  = CHART_Y + CHART_H - (int)((candleHigh[i]  - minVal) / range * CHART_H);
    int lowY   = CHART_Y + CHART_H - (int)((candleLow[i]   - minVal) / range * CHART_H);
    int openY  = CHART_Y + CHART_H - (int)((candleOpen[i]  - minVal) / range * CHART_H);
    int closeY = CHART_Y + CHART_H - (int)((candleClose[i] - minVal) / range * CHART_H);
    bool     bullish = candleClose[i] >= candleOpen[i];
    uint16_t col     = bullish ? COL_GREEN : COL_RED;
    int midX = cx + cw / 2;
    tft.drawLine(midX, highY, midX, lowY, col);
    int bodyTop = min(openY, closeY);
    int bodyH   = abs(closeY - openY);
    if (bodyH < 1) bodyH = 1;
    tft.fillRect(cx, bodyTop, cw, bodyH, col);
  }

  tft.fillRect(chartX, DATE_Y, chartW, FOOTER_Y - DATE_Y, COL_BG);
  char lastDate[6] = "";
  for (int i = 0; i < CANDLE_COUNT; i += 6) {
    char dateStr[6];
    timestampToDate(candleTime[i], dateStr);
    if (strcmp(dateStr, lastDate) != 0) {
      strcpy(lastDate, dateStr);
      int lx = chartX + (int)(i * slotW);
      tft.drawLine(lx + cw/2, CHART_Y + CHART_H, lx + cw/2, DATE_Y + 3, COL_GRAY);
      tft.setTextColor(COL_WHITE, COL_BG);
      tft.setTextSize(1);
      int textX = lx - 8;
      if (textX < chartX)               textX = chartX;
      if (textX > chartX + chartW - 30) textX = chartX + chartW - 30;
      tft.setCursor(textX, DATE_Y + 5);
      tft.print(dateStr);
    }
  }
}

Wie es funktioniert

Beim Start zeigt das Display einen Bootscreen mit Bitcoin-Logo. Dann verbindet sich der ESP32 mit dem WLAN und holt zwei API-Endpunkte von Binance ab:

  1. /ticker/24hr?symbol=BTCEUR – aktueller Preis und prozentuale 24h-Änderung
  2. /klines?symbol=BTCEUR&interval=4h&limit=30 – die letzten 30 Vier-Stunden-Kerzen

Aus den Kerzen werden automatisch die Preisskala (in k€-Notation) und die Datumsleiste berechnet. Grüne Kerzen = steigender Kurs in diesem 4h-Intervall, rote Kerzen = fallend. Der grüne Punkt oben rechts zeigt an, dass die Daten aktuell sind.

Der gesamte Datenabruf und Neuzeichnung läuft alle 30 Sekunden im Hintergrund – ohne Delay im Loop, nicht-blockierend über millis().