380 lines
11 KiB
C
380 lines
11 KiB
C
/**
|
||
* @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");
|
||
}
|