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.

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 mit integriertem WLAN
- ILI9341 TFT-Display 2,4” – 240×320 px, SPI, mit XPT2046 Touch-Controller
- Jumper-Kabel

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

| ILI9341 Pin | ESP32 GPIO | Beschreibung |
|---|---|---|
| VCC | 3.3V | Stromversorgung |
| GND | GND | Masse |
| CS | GPIO 5 | Chip Select Display |
| RESET | GPIO 4 | Reset |
| DC | GPIO 2 | Data/Command |
| SDI (MOSI) | GPIO 23 | SPI Daten (Master→Slave) |
| SCK | GPIO 18 | SPI Takt |
| LED | 3.3V | Hintergrundbeleuchtung |
| SDO (MISO) | GPIO 19 | SPI Daten (Slave→Master) |
| T_CLK | GPIO 18 | Touch Takt (geteilt) |
| T_CS | GPIO 15 | Chip Select Touch |
| T_DIN | GPIO 23 | Touch Daten (geteilt) |
| T_DO | GPIO 19 | Touch Ausgabe (geteilt) |
| T_IRQ | GPIO 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
WiFiundHTTPClient– 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:
/ticker/24hr?symbol=BTCEUR– aktueller Preis und prozentuale 24h-Änderung/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().