ST7735S驱动移植与性能优化实战

ST7735S驱动移植与性能优化实战

1. ST7735S驱动移植基础

第一次接触ST7735S这块1.8寸SPI屏时,我踩了不少坑。记得当时用STM32硬件SPI驱动,屏幕死活不亮,最后发现是复位时序没处理好。这个经历让我意识到,驱动移植看似简单,但细节决定成败。

硬件连接是第一步。ST7735S的接口其实很简洁:

  • VCC接3.3V或5V(注意逻辑电平匹配)
  • GND接地
  • SCL接SPI时钟线
  • SDA接SPI数据线
  • RESET接MCU的GPIO
  • DC(数据/命令选择)接GPIO
  • CS接SPI片选(如果不用可接地)

最容易被忽略的是复位时序。实测发现,复位信号拉低至少要保持10μs,之后还要延迟120ms等待屏幕初始化完成。我曾偷懒只延迟1ms,结果屏幕花屏。正确的复位函数应该这样写:

void lcd_reset() { RST_L; // 拉低复位 HAL_Delay(1); // 保持10μs以上 RST_H; // 拉高 HAL_Delay(120); // 等待初始化 }

SPI配置有讲究。官方例程常用软件模拟SPI,但在ESP32这类高速MCU上,硬件SPI能大幅提升性能。配置时要注意:

  1. SPI模式选Mode0或Mode3(CPOL=0/CPHA=0或CPOL=1/CPHA=1)
  2. 时钟频率建议8MHz以内(太高可能导致通信不稳定)
  3. MSB先行模式

如果是Arduino平台,记得在setup()里加上SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0))

2. 跨平台移植实战

去年给一家客户做智能家居中控,需要在ESP32和STM32F103上共用同一套ST7735S驱动。这让我积累了不少跨平台经验。

抽象硬件接口是关键。我通常会创建一个st7735s_compat.h文件,里面定义平台相关的宏和函数:

// STM32版本 #define SPI_SEND(data) HAL_SPI_Transmit(&hspi1, &data, 1, 100) #define DC_HIGH() HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET) #define DC_LOW() HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_RESET) // ESP32版本 #define SPI_SEND(data) spi_transaction_t t; \ memset(&t, 0, sizeof(t)); \ t.length=8; \ t.tx_buffer=&data; \ spi_device_transmit(spi, &t)

初始化流程的坑。不同平台对SPI初始化的要求不同:

  • STM32CubeMX生成的代码需要手动添加DMA配置
  • ESP32的VSPI和HSPI引脚是固定的(别像我一开始试图改到任意GPIO)
  • Arduino环境下要注意SPI.begin()的调用时机

实测发现,ESP32的SPI时钟可以跑到20MHz,但ST7735S在超过16MHz时会出现雪花点。建议保守点用8MHz:

// ESP32 SPI配置示例 spi_bus_config_t buscfg = { .miso_io_num = -1, // 不需要MISO .mosi_io_num = GPIO_NUM_23, .sclk_io_num = GPIO_NUM_18, .quadwp_io_num = -1, .quadhd_io_num = -1 }; spi_device_interface_config_t devcfg = { .clock_speed_hz = 8*1000*1000, .mode = 0, .spics_io_num = GPIO_NUM_5, .queue_size = 7 };

3. 三种缓冲区模式对比

ST7735S的刷新速度慢是个通病。通过测试三种缓冲模式,我发现内存和性能的平衡点很有趣。

无缓冲模式(BUFFER1)

  • 内存占用:0字节
  • 刷新速度:12FPS(128x160全屏刷新)
  • 适用场景:内存<4KB的MCU(如ATmega328)
  • 缺点:每次画点都要发13字节SPI数据

行列缓冲(HVBUFFER)

  • 内存占用:512字节(128x2x2)
  • 刷新速度:35FPS
  • 实现技巧:检测像素连续性,如果是同行/同列就批量发送
  • 实测代码:
void hv_buffer_draw_pixel(int x, int y, uint16_t color) { static int last_x = -1, last_y = -1; if(x == last_x + 1 && y == last_y) { // 水平连续 h_buffer[h_count++] = color; } else if(y == last_y + 1 && x == last_x) { // 垂直连续 v_buffer[v_count++] = color; } else { flush_buffer(); // 发送缓存的像素 h_buffer[0] = color; h_count = 1; } last_x = x; last_y = y; }

全帧缓冲(BUFFER)

  • 内存占用:40KB(128x160x2)
  • 刷新速度:58FPS
  • 惊喜发现:ESP32的PSRAM可以完美支持,成本仅增加¥2
  • 优化技巧:用DMA异步传输,CPU几乎零开销

性能对比表:

模式内存占用全屏刷新速度适用场景
无缓冲0KB12FPS超低内存设备
行列缓冲0.5KB35FPS通用场景
全帧缓冲40KB58FPS有外部RAM的设备

4. 性能优化技巧

经过三个项目的迭代,我总结出这些实战经验:

SPI优化四板斧

  1. 启用DMA传输:STM32的HAL_SPI_Transmit_DMA能提升30%速度
  2. 批量发送数据:把多个像素打包成一次SPI传输
  3. 提高时钟频率:先试8MHz,稳定再逐步上调
  4. 减少DC引脚切换:连续写命令时保持DC为命令模式

刷新策略很重要

  • 局部刷新:只更新变化区域(如游戏中的精灵移动)
  • 脏矩形技术:记录需要重绘的区域
  • 双缓冲:在PSRAM中准备下一帧,完成后一次性切换

有个有趣的发现:ST7735S的GRAM写入速度比读取快。利用这个特性,我在UI刷新时避免读取当前屏幕内容,而是维护一个本地帧缓冲。

电源管理技巧

  • 背光PWM频率建议1kHz以上(避免肉眼可见闪烁)
  • 睡眠模式下电流可从15mA降到0.1mA
  • 正确的睡眠唤醒序列:
void lcd_sleep() { lcd_write_cmd(0x10); // 进入睡眠 HAL_GPIO_WritePin(BLK_GPIO_Port, BLK_Pin, GPIO_PIN_RESET); // 关闭背光 } void lcd_wakeup() { HAL_GPIO_WritePin(BLK_GPIO_Port, BLK_Pin, GPIO_PIN_SET); // 先开背光 lcd_write_cmd(0x11); // 退出睡眠 HAL_Delay(120); // 必须的延迟 }

5. 常见问题解决方案

花屏问题

  • 现象:屏幕出现彩色噪点
  • 排查步骤:
    1. 检查复位时序(确保复位脉冲>10μs)
    2. 确认SPI相位(Mode0/Mode3)
    3. 测量电源电压(3.3V±10%)
    4. 检查接地(所有GND必须共地)

颜色异常

  • 典型原因:颜色格式设置错误
  • ST7735S支持RGB565和BGR565,通过0x36寄存器的MY/MX/MV位控制
  • 我曾遇到红色蓝色互换,最终发现是BGR模式没配置:
// 正确的颜色方向设置 lcd_write_cmd(0x36); lcd_write_data(0xA0); // RGB顺序

显示偏移

  • 现象:图像不在屏幕中央
  • 解决方法:调整列/行起始地址
  • 示例代码:
void lcd_set_offset(uint8_t x, uint8_t y) { lcd_write_cmd(0x2A); lcd_write_data(0x00); lcd_write_data(x); // X起始 lcd_write_data(0x00); lcd_write_data(x+127); // X结束 lcd_write_cmd(0x2B); lcd_write_data(0x00); lcd_write_data(y); // Y起始 lcd_write_data(0x00); lcd_write_data(y+159); // Y结束 }

6. 高级功能实现

图片显示优化

  • 使用Image2Lcd软件生成C数组
  • 启用压缩算法(如RLE)可减少50%存储空间
  • 渐进式加载技巧:先显示低分辨率,再逐步细化

动态效果实现

  • 跑马灯效果:利用ST7735S的水平滚动指令(0x33)
  • 动画优化:将多帧打包成一个SPI传输
  • 实测案例:在STM32F407上实现60FPS的粒子动画

触摸屏集成

  • 电阻屏常用XPT2046控制器
  • 布线技巧:将触摸SPI与显示SPI共用(通过CS区分)
  • 校准算法:
void calibrate_touch() { // 获取四个角坐标 get_touch_point(20, 20, &calib[0]); get_touch_point(20, 140, &calib[1]); get_touch_point(140, 20, &calib[2]); // 计算校准参数 a = (display_width * calib[1].y - display_width * calib[0].y) / (calib[1].x * calib[0].y - calib[0].x * calib[1].y); b = (display_width - a * calib[0].x) / calib[0].y; }

7. 移植到RTOS的注意事项

在FreeRTOS上驱动ST7735S时,我发现几个关键点:

SPI总线竞争

  • 解决方案:创建互斥锁
  • 示例代码:
SemaphoreHandle_t spi_mutex; void spi_send_safe(uint8_t data) { xSemaphoreTake(spi_mutex, portMAX_DELAY); SPI_SEND(data); xSemaphoreGive(spi_mutex); }

双缓冲策略

  1. 在PSRAM创建两个帧缓冲
  2. 后台任务渲染到缓冲B
  3. 完成时通过信号量通知显示任务切换缓冲

性能监控

  • 使用FreeRTOS的xTaskGetTickCount()测量刷新时间
  • 发现DMA传输期间CPU利用率从80%降到12%

8. 低功耗优化方案

为电池供电设备优化时,我测得这些数据:

各模式功耗

  • 全速运行:15mA
  • 关闭背光:5mA
  • 睡眠模式:0.1mA
  • 深度睡眠+定时唤醒:50μA

优化措施

  • 动态调整刷新率(静态画面用1Hz,动画用30Hz)
  • 智能背光控制(根据环境光调节PWM占空比)
  • 代码示例:
void set_refresh_rate(uint8_t fps) { uint8_t rate = (fps > 30) ? 0x01 : 0x08; lcd_write_cmd(0xB1); // FRMCTR1 lcd_write_data(rate); }

最近一个智能手表项目,通过上述优化将续航从3天提升到2周。关键点是合理利用ST7735S的局部刷新功能,只更新时间数字区域而非整个屏幕。