/** * @file st7305_rlcd.c * @brief ST7305 反射式 LCD 驱动实现(纯 C) * * 移植自微雪官方 custom_lcd_display.cc。 * 关键逻辑(初始化序列、横屏 LUT、像素打包)100% 保留, * 只是把 C++ class 改成模块级全局变量 + 函数。 */ #include "st7305_rlcd.h" #include #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; }