324 lines
8.8 KiB
C
324 lines
8.8 KiB
C
#include "display.h"
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "driver/gpio.h"
|
|
#include "driver/spi_master.h"
|
|
#include "esp_check.h"
|
|
#include "esp_log.h"
|
|
#include "esp_rom_sys.h"
|
|
|
|
#include "font5x7.h"
|
|
|
|
#define TAG "display"
|
|
|
|
#define PIN_NUM_MOSI 35
|
|
#define PIN_NUM_CLK 36
|
|
#define PIN_NUM_CS 42
|
|
#define PIN_NUM_DC 40
|
|
#define PIN_NUM_RST 41
|
|
#define PIN_NUM_BL 45
|
|
#define PIN_NUM_TFT_POWER 7
|
|
|
|
#define LCD_HOST SPI2_HOST
|
|
|
|
#define ST7789_SWRESET 0x01
|
|
#define ST7789_SLPOUT 0x11
|
|
#define ST7789_COLMOD 0x3A
|
|
#define ST7789_MADCTL 0x36
|
|
#define ST7789_CASET 0x2A
|
|
#define ST7789_RASET 0x2B
|
|
#define ST7789_RAMWR 0x2C
|
|
#define ST7789_DISPON 0x29
|
|
#define ST7789_INVON 0x21
|
|
|
|
#define X_OFFSET 40
|
|
#define Y_OFFSET 52
|
|
|
|
#define CHAR_SCALE 2
|
|
#define CHAR_W (6 * CHAR_SCALE)
|
|
#define CHAR_H (8 * CHAR_SCALE)
|
|
|
|
static spi_device_handle_t s_lcd = NULL;
|
|
static bool s_initialized = false;
|
|
|
|
static esp_err_t lcd_send_cmd(uint8_t cmd)
|
|
{
|
|
gpio_set_level(PIN_NUM_DC, 0);
|
|
spi_transaction_t t = {
|
|
.length = 8,
|
|
.tx_buffer = &cmd,
|
|
};
|
|
return spi_device_transmit(s_lcd, &t);
|
|
}
|
|
|
|
static esp_err_t lcd_send_data(const void *data, size_t len)
|
|
{
|
|
if (len == 0) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
gpio_set_level(PIN_NUM_DC, 1);
|
|
spi_transaction_t t = {
|
|
.length = len * 8,
|
|
.tx_buffer = data,
|
|
};
|
|
return spi_device_transmit(s_lcd, &t);
|
|
}
|
|
|
|
static esp_err_t lcd_set_window(int x, int y, int w, int h)
|
|
{
|
|
if (w <= 0 || h <= 0) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
uint16_t x0 = (uint16_t)(x + X_OFFSET);
|
|
uint16_t x1 = (uint16_t)(x + w - 1 + X_OFFSET);
|
|
uint16_t y0 = (uint16_t)(y + Y_OFFSET);
|
|
uint16_t y1 = (uint16_t)(y + h - 1 + Y_OFFSET);
|
|
|
|
uint8_t col[4] = { x0 >> 8, x0 & 0xFF, x1 >> 8, x1 & 0xFF };
|
|
uint8_t row[4] = { y0 >> 8, y0 & 0xFF, y1 >> 8, y1 & 0xFF };
|
|
|
|
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_CASET), TAG, "CASET failed");
|
|
ESP_RETURN_ON_ERROR(lcd_send_data(col, sizeof(col)), TAG, "CASET data failed");
|
|
|
|
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_RASET), TAG, "RASET failed");
|
|
ESP_RETURN_ON_ERROR(lcd_send_data(row, sizeof(row)), TAG, "RASET data failed");
|
|
|
|
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_RAMWR), TAG, "RAMWR failed");
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t lcd_hw_reset(void)
|
|
{
|
|
gpio_set_level(PIN_NUM_RST, 1);
|
|
esp_rom_delay_us(10000);
|
|
gpio_set_level(PIN_NUM_RST, 0);
|
|
esp_rom_delay_us(10000);
|
|
gpio_set_level(PIN_NUM_RST, 1);
|
|
esp_rom_delay_us(120000);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t lcd_panel_init(void)
|
|
{
|
|
ESP_RETURN_ON_ERROR(lcd_hw_reset(), TAG, "HW reset failed");
|
|
|
|
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_SWRESET), TAG, "SWRESET failed");
|
|
esp_rom_delay_us(150000);
|
|
|
|
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_SLPOUT), TAG, "SLPOUT failed");
|
|
esp_rom_delay_us(120000);
|
|
|
|
const uint8_t colmod = 0x55; // 16-bit
|
|
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_COLMOD), TAG, "COLMOD cmd failed");
|
|
ESP_RETURN_ON_ERROR(lcd_send_data(&colmod, 1), TAG, "COLMOD data failed");
|
|
esp_rom_delay_us(10000);
|
|
|
|
const uint8_t madctl = 0xA0; // MV | MY, landscape for Reverse TFT Feather
|
|
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_MADCTL), TAG, "MADCTL cmd failed");
|
|
ESP_RETURN_ON_ERROR(lcd_send_data(&madctl, 1), TAG, "MADCTL data failed");
|
|
|
|
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_INVON), TAG, "INVON failed");
|
|
esp_rom_delay_us(10000);
|
|
|
|
ESP_RETURN_ON_ERROR(lcd_send_cmd(ST7789_DISPON), TAG, "DISPON failed");
|
|
esp_rom_delay_us(120000);
|
|
|
|
gpio_set_level(PIN_NUM_BL, 1);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static inline uint16_t clamp_u16_color(uint16_t c)
|
|
{
|
|
return (uint16_t)((c >> 8) | (c << 8)); // swap endian for wire order
|
|
}
|
|
|
|
esp_err_t display_init(void)
|
|
{
|
|
if (s_initialized) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
gpio_config_t io_conf = {
|
|
.pin_bit_mask = (1ULL << PIN_NUM_DC) | (1ULL << PIN_NUM_RST) | (1ULL << PIN_NUM_BL) |
|
|
(1ULL << PIN_NUM_TFT_POWER),
|
|
.mode = GPIO_MODE_OUTPUT,
|
|
.pull_up_en = GPIO_PULLUP_DISABLE,
|
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
|
.intr_type = GPIO_INTR_DISABLE,
|
|
};
|
|
ESP_RETURN_ON_ERROR(gpio_config(&io_conf), TAG, "gpio_config failed");
|
|
|
|
// Reverse TFT Feather requires TFT/I2C power gate enabled for panel logic.
|
|
gpio_set_level(PIN_NUM_TFT_POWER, 1);
|
|
esp_rom_delay_us(10000);
|
|
|
|
spi_bus_config_t buscfg = {
|
|
.mosi_io_num = PIN_NUM_MOSI,
|
|
.miso_io_num = -1,
|
|
.sclk_io_num = PIN_NUM_CLK,
|
|
.quadwp_io_num = -1,
|
|
.quadhd_io_num = -1,
|
|
.max_transfer_sz = DISPLAY_WIDTH * 20 * sizeof(uint16_t),
|
|
};
|
|
|
|
ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO), TAG, "spi_bus_initialize failed");
|
|
|
|
spi_device_interface_config_t devcfg = {
|
|
.clock_speed_hz = 40 * 1000 * 1000,
|
|
.mode = 0,
|
|
.spics_io_num = PIN_NUM_CS,
|
|
.queue_size = 4,
|
|
};
|
|
|
|
ESP_RETURN_ON_ERROR(spi_bus_add_device(LCD_HOST, &devcfg, &s_lcd), TAG, "spi_bus_add_device failed");
|
|
ESP_RETURN_ON_ERROR(lcd_panel_init(), TAG, "panel init failed");
|
|
|
|
s_initialized = true;
|
|
ESP_RETURN_ON_ERROR(display_clear(RGB565_BLACK), TAG, "clear failed");
|
|
|
|
ESP_LOGI(TAG, "ST7789 initialized");
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t display_fill_rect(int x, int y, int w, int h, uint16_t color)
|
|
{
|
|
if (!s_initialized) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (x >= DISPLAY_WIDTH || y >= DISPLAY_HEIGHT || w <= 0 || h <= 0) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
if (x < 0) {
|
|
w += x;
|
|
x = 0;
|
|
}
|
|
if (y < 0) {
|
|
h += y;
|
|
y = 0;
|
|
}
|
|
if (x + w > DISPLAY_WIDTH) {
|
|
w = DISPLAY_WIDTH - x;
|
|
}
|
|
if (y + h > DISPLAY_HEIGHT) {
|
|
h = DISPLAY_HEIGHT - y;
|
|
}
|
|
if (w <= 0 || h <= 0) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
const uint16_t wire_color = clamp_u16_color(color);
|
|
const int chunk_pixels = DISPLAY_WIDTH * 8;
|
|
static uint16_t line_buf[DISPLAY_WIDTH * 8];
|
|
|
|
for (int i = 0; i < chunk_pixels; ++i) {
|
|
line_buf[i] = wire_color;
|
|
}
|
|
|
|
ESP_RETURN_ON_ERROR(lcd_set_window(x, y, w, h), TAG, "set window failed");
|
|
|
|
int total = w * h;
|
|
while (total > 0) {
|
|
int batch = total > chunk_pixels ? chunk_pixels : total;
|
|
ESP_RETURN_ON_ERROR(lcd_send_data(line_buf, batch * sizeof(uint16_t)), TAG, "fill data failed");
|
|
total -= batch;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t display_clear(uint16_t color)
|
|
{
|
|
return display_fill_rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, color);
|
|
}
|
|
|
|
static esp_err_t draw_char(int x, int y, char c, uint16_t fg, uint16_t bg)
|
|
{
|
|
if (c < 0x20 || c > 0x7F) {
|
|
c = '?';
|
|
}
|
|
|
|
const uint8_t *glyph = font5x7[(uint8_t)c - 0x20];
|
|
|
|
for (int col = 0; col < 5; ++col) {
|
|
uint8_t bits = glyph[col];
|
|
for (int row = 0; row < 7; ++row) {
|
|
bool on = (bits >> row) & 0x1;
|
|
ESP_RETURN_ON_ERROR(
|
|
display_fill_rect(x + (col * CHAR_SCALE), y + (row * CHAR_SCALE), CHAR_SCALE, CHAR_SCALE, on ? fg : bg),
|
|
TAG,
|
|
"pixel draw failed");
|
|
}
|
|
}
|
|
|
|
// spacing column + bottom row
|
|
ESP_RETURN_ON_ERROR(display_fill_rect(x + (5 * CHAR_SCALE), y, CHAR_SCALE, 7 * CHAR_SCALE, bg), TAG, "spacing failed");
|
|
ESP_RETURN_ON_ERROR(display_fill_rect(x, y + (7 * CHAR_SCALE), 6 * CHAR_SCALE, CHAR_SCALE, bg), TAG, "baseline failed");
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t display_draw_text(int x, int y, const char *text, uint16_t fg, uint16_t bg)
|
|
{
|
|
if (!s_initialized) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
if (!text) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
int cursor_x = x;
|
|
int cursor_y = y;
|
|
|
|
for (size_t i = 0; text[i] != '\0'; ++i) {
|
|
char c = text[i];
|
|
if (c == '\n') {
|
|
cursor_x = x;
|
|
cursor_y += CHAR_H;
|
|
continue;
|
|
}
|
|
|
|
if (cursor_x + CHAR_W > DISPLAY_WIDTH) {
|
|
cursor_x = x;
|
|
cursor_y += CHAR_H;
|
|
}
|
|
if (cursor_y + CHAR_H > DISPLAY_HEIGHT) {
|
|
break;
|
|
}
|
|
|
|
ESP_RETURN_ON_ERROR(draw_char(cursor_x, cursor_y, c, fg, bg), TAG, "draw_char failed");
|
|
cursor_x += CHAR_W;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t display_draw_button_status(int btn_id, bool pressed)
|
|
{
|
|
if (btn_id < 0 || btn_id > 2) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
const int x = 10;
|
|
const int y = 30 + btn_id * 32;
|
|
const int w = DISPLAY_WIDTH - 20;
|
|
const int h = 24;
|
|
|
|
uint16_t box_color = pressed ? RGB565_GREEN : RGB565_RED;
|
|
uint16_t text_color = RGB565_WHITE;
|
|
|
|
char line[32];
|
|
snprintf(line, sizeof(line), "D%d: %s", btn_id, pressed ? "PRESSED" : "RELEASED");
|
|
|
|
ESP_RETURN_ON_ERROR(display_fill_rect(x, y, w, h, box_color), TAG, "status box failed");
|
|
ESP_RETURN_ON_ERROR(display_draw_text(x + 6, y + 8, line, text_color, box_color), TAG, "status text failed");
|
|
|
|
return ESP_OK;
|
|
}
|