lego-esp32s3-gameboy/components/st7789/st7789.c

380 lines
11 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @file st7789.c
* @brief ST7789 Display Driver - 270° Rotation (USB left)
*/
#include <string.h>
#include "esp_log.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "st7789.h"
#include "hardware_config.h"
static const char *TAG = "ST7789";
static spi_device_handle_t spi_handle = NULL;
// ST7789 Commands
#define ST7789_NOP 0x00
#define ST7789_SWRESET 0x01
#define ST7789_RDDID 0x04
#define ST7789_RDDST 0x09
#define ST7789_SLPIN 0x10
#define ST7789_SLPOUT 0x11
#define ST7789_PTLON 0x12
#define ST7789_NORON 0x13
#define ST7789_INVOFF 0x20
#define ST7789_INVON 0x21
#define ST7789_DISPOFF 0x28
#define ST7789_DISPON 0x29
#define ST7789_CASET 0x2A
#define ST7789_RASET 0x2B
#define ST7789_RAMWR 0x2C
#define ST7789_RAMRD 0x2E
#define ST7789_PTLAR 0x30
#define ST7789_COLMOD 0x3A
#define ST7789_MADCTL 0x36
// MADCTL bits
#define MADCTL_MY 0x80
#define MADCTL_MX 0x40
#define MADCTL_MV 0x20
#define MADCTL_ML 0x10
#define MADCTL_RGB 0x00
#define MADCTL_BGR 0x08
#define MADCTL_MH 0x04
static void st7789_send_cmd(uint8_t cmd)
{
gpio_set_level(LCD_PIN_DC, 0);
spi_transaction_t t = {
.length = 8,
.tx_buffer = &cmd,
.flags = SPI_TRANS_USE_TXDATA
};
t.tx_data[0] = cmd;
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
}
static void st7789_send_data(uint8_t data)
{
gpio_set_level(LCD_PIN_DC, 1);
spi_transaction_t t = {
.length = 8,
.tx_buffer = &data,
.flags = SPI_TRANS_USE_TXDATA
};
t.tx_data[0] = data;
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
}
static void st7789_send_data_buf(const uint8_t *data, size_t len)
{
if (len == 0) return;
gpio_set_level(LCD_PIN_DC, 1);
spi_transaction_t t = {
.length = len * 8,
.tx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
}
static void st7789_set_address_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
st7789_send_cmd(ST7789_CASET);
st7789_send_data(x0 >> 8);
st7789_send_data(x0 & 0xFF);
st7789_send_data(x1 >> 8);
st7789_send_data(x1 & 0xFF);
st7789_send_cmd(ST7789_RASET);
st7789_send_data(y0 >> 8);
st7789_send_data(y0 & 0xFF);
st7789_send_data(y1 >> 8);
st7789_send_data(y1 & 0xFF);
st7789_send_cmd(ST7789_RAMWR);
}
static void st7789_backlight_init(void)
{
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = LEDC_TIMER_8_BIT,
.freq_hz = 5000,
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&timer_conf));
ledc_channel_config_t channel_conf = {
.gpio_num = LCD_PIN_BCKL,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.duty = 0,
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&channel_conf));
}
esp_err_t st7789_init(void)
{
ESP_LOGI(TAG, "Initializing ST7789 display...");
// WICHTIG: GPIO Hold deaktivieren falls wir aus Deep Sleep aufwachen
// Sonst können die Pins nicht neu konfiguriert werden!
gpio_hold_dis(LCD_PIN_CS);
gpio_hold_dis(LCD_PIN_DC);
gpio_hold_dis(LCD_PIN_BCKL);
gpio_hold_dis(LCD_PIN_MOSI);
gpio_hold_dis(LCD_PIN_SCLK);
// DC-Pin konfigurieren (immer vorhanden)
gpio_config_t io_conf = {};
io_conf.pin_bit_mask = (1ULL << LCD_PIN_DC);
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_config(&io_conf);
// RST-Pin konfigurieren (falls vorhanden)
if (LCD_PIN_RST >= 0) {
io_conf.pin_bit_mask = (1ULL << LCD_PIN_RST);
gpio_config(&io_conf);
}
spi_bus_config_t buscfg = {
.mosi_io_num = LCD_PIN_MOSI,
.miso_io_num = LCD_PIN_MISO,
.sclk_io_num = LCD_PIN_SCLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
// WICHTIG: Großer Buffer für Display UND SD-Karte (teilen sich den Bus!)
// Display braucht: 320×240×2 = 153600 Bytes
// SD-Karte braucht auch Platz für Transfers
.max_transfer_sz = 4096 * 64, // 256 KB (genug für beide)
.flags = SPICOMMON_BUSFLAG_MASTER,
};
ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO));
spi_device_interface_config_t devcfg = {
.clock_speed_hz = LCD_PIXEL_CLOCK_HZ,
.mode = 0,
.spics_io_num = LCD_PIN_CS,
.queue_size = 7,
.flags = SPI_DEVICE_NO_DUMMY,
};
ESP_ERROR_CHECK(spi_bus_add_device(LCD_SPI_HOST, &devcfg, &spi_handle));
st7789_backlight_init();
st7789_set_backlight(0);
// Hardware-Reset (falls RST-Pin vorhanden)
if (LCD_PIN_RST >= 0) {
gpio_set_level(LCD_PIN_RST, 0);
vTaskDelay(pdMS_TO_TICKS(100));
gpio_set_level(LCD_PIN_RST, 1);
vTaskDelay(pdMS_TO_TICKS(100));
} else {
// Kein Hardware-Reset → Software-Reset reicht
vTaskDelay(pdMS_TO_TICKS(20));
}
st7789_send_cmd(ST7789_SWRESET);
vTaskDelay(pdMS_TO_TICKS(150));
st7789_send_cmd(ST7789_SLPOUT);
vTaskDelay(pdMS_TO_TICKS(10));
st7789_send_cmd(ST7789_COLMOD);
st7789_send_data(0x55);
vTaskDelay(pdMS_TO_TICKS(10));
// MADCTL: 270° = 90° CCW = USB on LEFT side
// MV=1 (Row/Column exchange) + MY=1 (Mirror Y)
st7789_send_cmd(ST7789_MADCTL);
st7789_send_data(MADCTL_MV | MADCTL_MY | MADCTL_RGB);
st7789_send_cmd(ST7789_INVON);
vTaskDelay(pdMS_TO_TICKS(10));
st7789_send_cmd(ST7789_NORON);
vTaskDelay(pdMS_TO_TICKS(10));
st7789_send_cmd(ST7789_DISPON);
vTaskDelay(pdMS_TO_TICKS(100));
ESP_LOGI(TAG, "✓ ST7789 initialized (%dx%d - 270° rotated, USB left)", LCD_WIDTH, LCD_HEIGHT);
st7789_set_backlight(LCD_BCKL_DEFAULT);
st7789_fill_screen(0x0000);
return ESP_OK;
}
void st7789_set_backlight(uint8_t brightness)
{
if (brightness > 100) brightness = 100;
uint32_t duty = (brightness * 255) / 100;
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}
void st7789_fill_screen(uint16_t color)
{
st7789_set_address_window(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1);
uint16_t line_buf[LCD_WIDTH];
for (int i = 0; i < LCD_WIDTH; i++) {
line_buf[i] = (color >> 8) | (color << 8);
}
gpio_set_level(LCD_PIN_DC, 1);
for (int y = 0; y < LCD_HEIGHT; y++) {
spi_transaction_t t = {
.length = LCD_WIDTH * 16,
.tx_buffer = line_buf
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
}
}
void st7789_draw_pixel(int16_t x, int16_t y, uint16_t color)
{
if (x < 0 || x >= LCD_WIDTH || y < 0 || y >= LCD_HEIGHT) return;
st7789_set_address_window(x, y, x, y);
uint8_t data[2] = {color >> 8, color & 0xFF};
gpio_set_level(LCD_PIN_DC, 1);
spi_transaction_t t = {
.length = 16,
.tx_buffer = data
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
}
void st7789_draw_buffer(const uint16_t *buffer, int16_t x, int16_t y,
int16_t width, int16_t height)
{
if (buffer == NULL || width <= 0 || height <= 0) return;
if (x < 0 || y < 0 || x + width > LCD_WIDTH || y + height > LCD_HEIGHT) {
ESP_LOGW(TAG, "Buffer out of bounds: (%d,%d) %dx%d", x, y, width, height);
return;
}
st7789_set_address_window(x, y, x + width - 1, y + height - 1);
size_t buf_size = width * height;
uint16_t *swap_buf = heap_caps_malloc(buf_size * 2, MALLOC_CAP_DMA);
if (swap_buf == NULL) {
ESP_LOGE(TAG, "Failed to allocate swap buffer!");
return;
}
for (size_t i = 0; i < buf_size; i++) {
swap_buf[i] = (buffer[i] >> 8) | (buffer[i] << 8);
}
gpio_set_level(LCD_PIN_DC, 1);
spi_transaction_t t = {
.length = buf_size * 16,
.tx_buffer = swap_buf
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
heap_caps_free(swap_buf);
}
void st7789_draw_buffer_preswapped(const uint16_t *buffer, int16_t x, int16_t y,
int16_t width, int16_t height)
{
if (buffer == NULL || width <= 0 || height <= 0) return;
if (x < 0 || y < 0 || x + width > LCD_WIDTH || y + height > LCD_HEIGHT) {
ESP_LOGW(TAG, "Buffer out of bounds: (%d,%d) %dx%d", x, y, width, height);
return;
}
// Set address window once for entire region
st7789_set_address_window(x, y, x + width - 1, y + height - 1);
gpio_set_level(LCD_PIN_DC, 1);
// Split into DMA-safe chunks (max 32KB per transfer to be safe)
// SPI will auto-split into smaller DMA descriptors internally
const size_t max_chunk_size = 32768; // 32KB in bytes
size_t total_bytes = width * height * 2;
size_t offset = 0;
while (offset < total_bytes) {
size_t chunk_size = (total_bytes - offset > max_chunk_size) ?
max_chunk_size : (total_bytes - offset);
spi_transaction_t t = {
.length = chunk_size * 8, // in bits
.tx_buffer = (uint8_t*)buffer + offset
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi_handle, &t));
offset += chunk_size;
}
}
void st7789_sleep(void)
{
ESP_LOGI(TAG, "Display in Sleep-Modus versetzen...");
// 1. ZUERST: Bildschirm schwarz füllen (entfernt letztes Frame)
st7789_fill_screen(0x0000);
// 2. Hintergrundbeleuchtung über PWM ausschalten
st7789_set_backlight(0);
// 3. LEDC (PWM) Kanal komplett stoppen!
// Sonst läuft der PWM-Timer weiter und kann Glitches verursachen
ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); // 0 = LOW level
// 4. Display ausschalten (DISPOFF)
st7789_send_cmd(ST7789_DISPOFF);
vTaskDelay(pdMS_TO_TICKS(20));
// 5. Sleep-Modus aktivieren (SLPIN)
// Im Sleep-Modus verbraucht das Display minimal Strom
// und die Ausgänge sind in einem definierten Zustand (kein Blinken!)
st7789_send_cmd(ST7789_SLPIN);
vTaskDelay(pdMS_TO_TICKS(120)); // ST7789 braucht 120ms für Sleep-Eintritt
// 6. KRITISCH: SPI-Pins in definierten Zustand bringen!
// Während Deep Sleep floaten die Pins sonst und verursachen
// zufälliges Blinken auf dem Display.
gpio_set_level(LCD_PIN_CS, 1); // CS HIGH = Display ignoriert SPI
gpio_set_level(LCD_PIN_DC, 0); // DC auf definierten Pegel
// 7. Backlight-Pin manuell auf OUTPUT setzen und LOW
// (überschreibt LEDC-Konfiguration)
gpio_reset_pin(LCD_PIN_BCKL);
gpio_set_direction(LCD_PIN_BCKL, GPIO_MODE_OUTPUT);
gpio_set_level(LCD_PIN_BCKL, 0);
// 8. GPIO-Hold aktivieren für Deep Sleep
// Dies hält die Pin-Zustände während Deep Sleep stabil
gpio_hold_en(LCD_PIN_CS);
gpio_hold_en(LCD_PIN_DC);
gpio_hold_en(LCD_PIN_BCKL);
// Auch SPI-Pins auf definierte Pegel setzen und halten
gpio_set_level(LCD_PIN_MOSI, 0);
gpio_set_level(LCD_PIN_SCLK, 0);
gpio_hold_en(LCD_PIN_MOSI);
gpio_hold_en(LCD_PIN_SCLK);
ESP_LOGI(TAG, "Display schläft - GPIO Hold aktiviert");
}