ESP32/main/st7305_rlcd.c
2026-06-23 20:38:23 +08:00

366 lines
13 KiB
C
Raw 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 st7305_rlcd.c
* @brief ST7305 反射式 LCD 驱动实现(纯 C)
*
* 移植自微雪官方 custom_lcd_display.cc。
* 关键逻辑(初始化序列、横屏 LUT、像素打包)100% 保留,
* 只是把 C++ class 改成模块级全局变量 + 函数。
*/
#include "st7305_rlcd.h"
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "driver/gpio.h"
static const char *TAG = "st7305";
/* ===========================================================
* 模块内部状态(等价于原来 C++ 类的成员变量)
* =========================================================== */
/* SPI 设备句柄:用 ESP-IDF 的 esp_lcd panel io 框架来管 SPI 通信 */
static esp_lcd_panel_io_handle_t s_io_handle = NULL;
/* 显存:每 8 像素打包成 1 字节,所以显存大小 = W*H/8 = 400*300/8 = 15000 字节。
* 这个 buffer 放在 PSRAM 里(8MB PSRAM 完全够)。 */
#define DISPLAY_BUFFER_LEN ((ST7305_RLCD_WIDTH * ST7305_RLCD_HEIGHT) / 8)
static uint8_t *s_disp_buffer = NULL;
/* 横屏 LUT:把 (x,y) 像素坐标映射到 (字节索引, 位掩码)。
* ST7305 的显存布局很奇怪(2列×4行打包),没有 LUT 的话
* 每次画一个像素都要重新算一遍位运算,LUT 让它变成 O(1) 查表。
* 索引方式:pixel_index = x * HEIGHT + y(跟官方一致) */
static uint16_t *s_pixel_index_lut = NULL; /* 大小: W*H 个 uint16 */
static uint8_t *s_pixel_bit_lut = NULL; /* 大小: W*H 个 uint8 */
/* ===========================================================
* SPI 底层:发命令 / 发数据
* 用 esp_lcd panel io 框架,它会自动管 DC 引脚
* (DC=0 表示命令, DC=1 表示数据)
* =========================================================== */
static void send_command(uint8_t cmd)
{
/* 用 -1 作为 dis_flags 表示"不带参数的命令" */
esp_lcd_panel_io_tx_param(s_io_handle, cmd, NULL, 0);
}
static void send_data(uint8_t data)
{
esp_lcd_panel_io_tx_param(s_io_handle, -1, &data, 1);
}
static void send_buffer(uint8_t *data, int len)
{
esp_lcd_panel_io_tx_color(s_io_handle, -1, data, len);
}
/* ===========================================================
* 硬件复位(官方 RLCD_Reset 翻译)
* =========================================================== */
static void hardware_reset(void)
{
/* RST 引脚高 → 低 → 高 的脉冲,跟官方时序一致 */
gpio_set_level(ST7305_RLCD_PIN_RST, 1);
vTaskDelay(pdMS_TO_TICKS(50));
gpio_set_level(ST7305_RLCD_PIN_RST, 0);
vTaskDelay(pdMS_TO_TICKS(20));
gpio_set_level(ST7305_RLCD_PIN_RST, 1);
vTaskDelay(pdMS_TO_TICKS(50));
}
/* ===========================================================
* ST7305 初始化序列(官方 RLCD_Init 完整翻译)
* 这 50+ 条寄存器命令是从微雪官方代码原样复制来的,
* 不要随便改顺序或值 —— 它们是微雪调好的参数。
* 每条命令的中文注释参考 ST7305 datasheet 和官方命名。
* =========================================================== */
static void init_display(void)
{
/* NVM Load Control - 从非易失存储加载设置 */
send_command(0xD6);
send_data(0x17);
send_data(0x02);
/* Booster Enable - 使能电荷泵 */
send_command(0xD1);
send_data(0x01);
/* Gate Voltage Control - VGH/VGL 电压 */
send_command(0xC0);
send_data(0x11);
send_data(0x04);
/* VSHP Setting - 正向源电压(高功耗模式) */
send_command(0xC1);
send_data(0x69); send_data(0x69); send_data(0x69); send_data(0x69);
/* VSLP Setting - 正向源电压(低功耗模式) */
send_command(0xC2);
send_data(0x19); send_data(0x19); send_data(0x19); send_data(0x19);
/* VSHN Setting - 负向源电压(高功耗模式) */
send_command(0xC4);
send_data(0x4B); send_data(0x4B); send_data(0x4B); send_data(0x4B);
/* VSLN Setting - 负向源电压(低功耗模式) */
send_command(0xC5);
send_data(0x19); send_data(0x19); send_data(0x19); send_data(0x19);
/* OSC Setting - 振荡器频率控制 */
send_command(0xD8);
send_data(0x80);
send_data(0xE9);
/* Frame Rate Control - 帧率 */
send_command(0xB2);
send_data(0x02);
/* Gate EQ Control (High Power Mode) - 更新周期时序 */
send_command(0xB3);
send_data(0xE5); send_data(0xF6); send_data(0x05); send_data(0x46);
send_data(0x77); send_data(0x77); send_data(0x77); send_data(0x77);
send_data(0x76); send_data(0x45);
/* Gate EQ Control (Low Power Mode) - 更新周期时序 */
send_command(0xB4);
send_data(0x05); send_data(0x46); send_data(0x77); send_data(0x77);
send_data(0x77); send_data(0x77); send_data(0x76); send_data(0x45);
/* Gate Timing Control */
send_command(0x62);
send_data(0x32); send_data(0x03); send_data(0x1F);
/* Source EQ Enable */
send_command(0xB7);
send_data(0x13);
/* Gate Line Setting - 门极线数(板子相关)
* 0x64 = 100,100*3 = 300 行,对应 300 高 */
send_command(0xB0);
send_data(0x64);
/* Sleep Out - 退出睡眠(datasheet 要求 200ms 延时) */
send_command(0x11);
vTaskDelay(pdMS_TO_TICKS(200));
/* Source Voltage Select - 选 VSHP1/VSLP1/VSHN1/VSLN1 */
send_command(0xC9);
send_data(0x00);
/* MADCTL (Memory Data Access Control) - MX=1, DO=1 */
send_command(0x36);
send_data(0x48);
/* 1-bit 单色模式 */
send_command(0x3A);
send_data(0x11);
/* Gamma Mode - 单色 */
send_command(0xB9);
send_data(0x20);
/* Panel Setting - 1-dot 反转,帧反转,隔行 */
send_command(0xB8);
send_data(0x29);
/* Display Inversion On */
send_command(0x21);
/* Column Address Set - 板子相关 */
send_command(0x2A);
send_data(0x12);
send_data(0x2A);
/* Row Address Set - 板子相关 */
send_command(0x2B);
send_data(0x00);
send_data(0xC7);
/* Tearing Effect Line On */
send_command(0x35);
send_data(0x00);
/* Auto Power Down Control */
send_command(0xD0);
send_data(0xFF);
/* High Power Mode On */
send_command(0x38);
/* Display On */
send_command(0x29);
}
/* ===========================================================
* 横屏像素 LUT 构造(官方 InitLandscapeLUT 翻译)
*
* ST7305 横屏显存布局:每字节装 8 个像素,排列是 2列×4行 的块
*
* 一个字节(8 bit)的物理含义:
* Bit 7: (row 0, col 0) Bit 6: (row 0, col 1)
* Bit 5: (row 1, col 0) Bit 4: (row 1, col 1)
* Bit 3: (row 2, col 0) Bit 2: (row 2, col 1)
* Bit 1: (row 3, col 0) Bit 0: (row 3, col 1)
*
* 所以给一个逻辑坐标 (x, y),要算出:
* 1. 它在显存里的字节索引
* 2. 在该字节里的位掩码
* 这就是 LUT 干的事,预计算一次,后面画像素就 O(1) 查表。
* =========================================================== */
static void init_landscape_lut(void)
{
const uint16_t H4 = ST7305_RLCD_HEIGHT >> 2; /* 高度方向块数 = 300/4 = 75 */
for (uint16_t y = 0; y < ST7305_RLCD_HEIGHT; y++) {
/* y 方向:ST7305 是从下往上扫的,所以要反转 */
uint16_t inv_y = ST7305_RLCD_HEIGHT - 1 - y;
uint16_t block_y = inv_y >> 2; /* 在哪个 4 行块里 */
uint8_t local_y = inv_y & 3; /* 块内第几行 */
for (uint16_t x = 0; x < ST7305_RLCD_WIDTH; x++) {
uint16_t byte_x = x >> 1; /* 2 列共用 1 字节 */
uint8_t local_x = x & 1; /* 块内第几列 */
/* 字节索引 = 列块号 * 高度块数 + 行块号 */
uint32_t buffer_idx = (uint32_t)byte_x * H4 + block_y;
/* 位掩码:bit 位置 = 7 - (local_y * 2 + local_x) */
uint8_t bit_pos = 7 - ((local_y << 1) | local_x);
uint8_t bit_mask = (uint8_t)(1u << bit_pos);
/* LUT 索引方式:x * HEIGHT + y(跟官方一致) */
uint32_t pixel_idx = (uint32_t)x * ST7305_RLCD_HEIGHT + y;
s_pixel_index_lut[pixel_idx] = (uint16_t)buffer_idx;
s_pixel_bit_lut[pixel_idx] = bit_mask;
}
}
}
/* ===========================================================
* 对外 API 实现
* =========================================================== */
esp_err_t st7305_rlcd_init(void)
{
esp_err_t ret;
ESP_LOGI(TAG, "Step 1: 配置 RST 引脚为输出");
gpio_config_t gpio_conf = {
.pin_bit_mask = (1ULL << ST7305_RLCD_PIN_RST),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&gpio_conf);
ESP_LOGI(TAG, "Step 2: 初始化 SPI 总线 (SPI3, MOSI=%d, SCK=%d)",
ST7305_RLCD_PIN_MOSI, ST7305_RLCD_PIN_SCK);
spi_bus_config_t buscfg = {0};
buscfg.mosi_io_num = ST7305_RLCD_PIN_MOSI;
buscfg.miso_io_num = -1; /* 屏幕只写不读,不需要 MISO */
buscfg.sclk_io_num = ST7305_RLCD_PIN_SCK;
buscfg.quadwp_io_num = -1;
buscfg.quadhd_io_num = -1;
buscfg.max_transfer_sz = ST7305_RLCD_WIDTH * ST7305_RLCD_HEIGHT; /* 一次传整屏 */
ret = spi_bus_initialize(ST7305_RLCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SPI 总线初始化失败: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "Step 3: 把屏幕挂到 SPI 总线上(用 esp_lcd panel io 框架)");
esp_lcd_panel_io_spi_config_t io_config = {0};
io_config.dc_gpio_num = ST7305_RLCD_PIN_DC;
io_config.cs_gpio_num = ST7305_RLCD_PIN_CS;
io_config.pclk_hz = ST7305_RLCD_SPI_HZ;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
io_config.spi_mode = 0;
io_config.trans_queue_depth = 7;
ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)ST7305_RLCD_SPI_HOST,
&io_config, &s_io_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "panel io 创建失败: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "Step 4: 分配显存到 PSRAM (%d 字节)", DISPLAY_BUFFER_LEN);
s_disp_buffer = (uint8_t *)heap_caps_malloc(DISPLAY_BUFFER_LEN, MALLOC_CAP_SPIRAM);
if (s_disp_buffer == NULL) {
ESP_LOGE(TAG, "显存分配失败(PSRAM?)");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "Step 5: 分配像素 LUT (index=%dKB, bit=%dKB)",
(ST7305_RLCD_WIDTH * ST7305_RLCD_HEIGHT * 2) / 1024,
(ST7305_RLCD_WIDTH * ST7305_RLCD_HEIGHT) / 1024);
uint32_t total_pixels = (uint32_t)ST7305_RLCD_WIDTH * ST7305_RLCD_HEIGHT;
s_pixel_index_lut = (uint16_t *)heap_caps_malloc(total_pixels * sizeof(uint16_t),
MALLOC_CAP_SPIRAM);
s_pixel_bit_lut = (uint8_t *)heap_caps_malloc(total_pixels * sizeof(uint8_t),
MALLOC_CAP_SPIRAM);
if (!s_pixel_index_lut || !s_pixel_bit_lut) {
ESP_LOGE(TAG, "LUT 分配失败(PSRAM?)");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "Step 6: 构建横屏 LUT");
init_landscape_lut();
ESP_LOGI(TAG, "Step 7: 硬件复位 + 发送 ST7305 初始化序列");
hardware_reset();
init_display();
ESP_LOGI(TAG, "Step 8: 清屏(全白)");
st7305_rlcd_clear(ST7305_COLOR_WHITE);
st7305_rlcd_flush();
ESP_LOGI(TAG, "屏幕初始化完成 ✓");
return ESP_OK;
}
void st7305_rlcd_set_pixel(int x, int y, uint8_t color)
{
if (x < 0 || x >= ST7305_RLCD_WIDTH) return;
if (y < 0 || y >= ST7305_RLCD_HEIGHT) return;
/* O(1) 查 LUT */
uint32_t pixel_idx = (uint32_t)x * ST7305_RLCD_HEIGHT + y;
uint16_t buffer_idx = s_pixel_index_lut[pixel_idx];
uint8_t bit_mask = s_pixel_bit_lut[pixel_idx];
uint8_t *p = &s_disp_buffer[buffer_idx];
if (color == ST7305_COLOR_BLACK) {
*p &= ~bit_mask; /* 该位清零 = 像素亮(黑) */
} else {
*p |= bit_mask; /* 该位置 1 = 像素灭(白) */
}
}
void st7305_rlcd_flush(void)
{
/* 官方 RLCD_Display:每次刷新都重新设置地址窗口,然后发整屏数据 */
send_command(0x2A); /* Column Address Set */
send_data(0x12);
send_data(0x2A);
send_command(0x2B); /* Page/Row Address Set */
send_data(0x00);
send_data(0xC7);
send_command(0x2C); /* Memory Write */
send_buffer(s_disp_buffer, DISPLAY_BUFFER_LEN);
}
void st7305_rlcd_clear(uint8_t color)
{
memset(s_disp_buffer, color, DISPLAY_BUFFER_LEN);
}
int st7305_rlcd_get_width(void) { return ST7305_RLCD_WIDTH; }
int st7305_rlcd_get_height(void) { return ST7305_RLCD_HEIGHT; }