/** * @file st7789.c * @brief ST7789 Display Driver - 270° Rotation (USB left) */ #include #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"); }