1. 选型之前先想清楚:3.5寸屏到底要什么硬件方案
很多人拿到一块3.5寸触摸屏的第一反应是"赶紧接上去、跑个Demo看看效果"。但以我做过的几个项目来看,硬件选型这一步如果走偏,后面会有数不清的坑要填。ESP32-S3本身是一颗非常适合做人机交互界面的芯片,Flash和PSRAM的搭配灵活、外设丰富,但具体到"驱动3.5寸触摸屏"这个场景,你需要从主控、屏幕接口、触摸芯片、电源几个维度一起权衡,而不是单单看屏的参数页。
1.1 主控选择:ESP32-S3相比其他MCU的关键优势
做GUI开发,最常见的两个备选方案是STM32系列和ESP32-S3。STM32的优势在于生态成熟、资料多、很多工程师从入门就在用它,但做中大型GUI时它的短板也很明显:内存小,哪怕是F407也只有192KB RAM,跑一个稍微复杂的LVGL界面就要精打细算。ESP32-S3则完全不同,它支持外挂PSRAM,常见模组直接给你8MB、16MB的Octal PSRAM,LVGL的帧缓冲、控件对象、字体缓冲都可以放得很宽裕,不用天天纠结"这块内存能不能省"。
另外,ESP32-S3的SPI外设支持DMA,最高主频240MHz,驱动320x480分辨率的屏幕时,刷新率能做到比较理想的状态。它还内置了Wi-Fi和蓝牙,如果你的GUI需要跟手机App联动、走MQTT上报数据、做OTA固件升级,这些功能在STM32上要么外挂模块、要么通信协议自己写一堆,而在ESP32-S3上是用官方组件直接调用的。我实际做过的项目里,一个是空压机触摸屏控制器,一个是小型的工业参数设置面板,都用到了ESP32-S3的联网能力——本地GUI负责显示和交互,云端负责远程监控和日志,这种组合在STM32上实现起来工作量会翻倍。
1.2 屏幕本体:SPI接口、分辨率、驱动IC的选择边界
3.5寸屏幕在市场上主要分两种:SPI接口和RGB接口。SPI接口的屏引脚少,常见的是4线SPI(SCK、MOSI、DC、CS)加上复位和背光,总共6根信号线,接线非常省事,适合树莓派Pico、ESP32这类MCU。RGB接口的屏则需要至少16根以上的数据线,还得配合像素时钟,一般得用RGB565格式、带LTDC外设的MCU或者MPU才能驱动。ESP32-S3没有原生的RGB LCD控制器,虽然可以用IO模拟或加转换桥接芯片,但复杂度明显上升。所以我建议,面对3.5寸这个尺寸,选择SPI接口的屏幕是最务实的方案。
分辨率方面,3.5寸屏常见的两种是320x480和480x320。后者是横屏分辨率,竖屏时正好是320x480。驱动IC方面,市场占有率最高的是ST7789和ILI9488。ST7789支持的常见分辨率是240x320和320x480,如果你的屏幕标称320x480,大概率用的是ST7789V或者兼容型号;ILI9488则更多出现在480x320的横屏3.5寸屏上。选屏之前一定要跟卖家确认驱动IC型号和华辈子列号,因为后续写初始化序列、设置偏移量都要依赖这些信息。遇到过不少朋友买回屏幕后才发现驱动IC不是资料里写的那个,结果显示方向错乱、颜色不对,排查起来很耗时。
提示:买屏的时候一定要问清楚"驱动IC是什么"以及"触摸部分是什么芯片",不要只问分辨率。有了这两个信息,你才能在网上海量资料里快速找到能用的驱动代码。
1.3 触摸方案:电容触摸芯片与I2C接口适配
3.5寸触摸屏目前以电容触摸为主流,常见触摸芯片有CST816S、FT6236U、GT911、NS2009等。CST816S和FT6236U属于入门级电容触摸芯片,支持单点到两点触摸,I2C接口,寄存器简单,很适合MCU驱动。GT911支持五点触摸,寄存器稍微复杂一些,但它的优势是支持更大的触摸面积和更好的抗干扰能力,如果做的是工业设备面板,选GT911会更稳。NS2009其实是电阻触摸方案,用ADC采样,现在在3.5寸屏上已经不多见了。
触摸芯片与主控的通信基本都是I2C,这里有个小细节要提前确认:触摸芯片的I2C地址是否可以通过引脚配置。比如CST816S默认地址是0x15,FT6236U是0x38,GT911则是通过INT引脚电平选择0x5D或0x14。如果你在初始化的时候读不到触摸数据,第一件事就是检查地址是否匹配,再用逻辑分析仪抓一下I2C总线上设备是否有ACK响应。我踩过最典型的一个坑是FT6236U的地址被我写成了0x38,但实际硬件接的是0x14,结果触摸完全没反应,排查了很久才发现是地址问题而不是代码问题。
1.4 电源与电平转换:稳定运行的隐性关键
屏幕的背光LED通常需要20mA-40mA电流,模组工作电压一般是3.3V或5V。ESP32-S3是3.3V逻辑,如果你的屏是5V供电的版本,SCK、MOSI这些信号线就需要做电平转换。虽然有些屏模组内部已经集成了电平转换电路,但那是"有些",不是"所有"。选型的时候应该直接找3.3V供电版本的屏幕,省掉电平转换电路,也让信号完整性更有保障。
还有一个容易被忽略的问题是电源纹波。ESP32-S3在Wi-Fi开启的瞬间电流会突然拉高,如果电源设计余量不足,屏幕会闪、触摸会误触发,甚至系统直接复位。我建议至少使用500mA以上的稳压芯片,模组供电和背光供电分开走线,在电源输入端放一个100uF电解电容加一个0.1uF陶瓷电容做滤波。这个习惯帮我避免了很多莫名其妙的问题,在工控场景里电源的稳定性怎么强调都不过分。
2. 开发环境与底层驱动:先点亮屏幕再谈界面
选型确定之后,第一件事不是急着移植LVGL,而是先用最朴素的方式把屏幕点亮、把触摸数据读出来。这就像盖房子先打地基:底层驱动不稳,LVGL界面上出现花屏、闪烁、触摸漂移时,你根本分不清是应用层的问题还是驱动层的问题。我的做法是先跑通一个"纯屏幕点亮"工程和一个"触摸读取"工程,确认硬件本身工作正常,再开始搞GUI。
2.1 显示驱动IC的初始化序列(以ST7789为例)
ST7789的驱动逻辑并不复杂,但要严格按照它所要求的时序来写。初始化时先拉低复位引脚至少10ms,然后拉高,接着发送一系列命令字节和参数。常见的初始化命令包括:SLPOUT(退出睡眠)、COLMOD(设置像素格式为RGB565)、MADCTL(设置扫描方向和颜色顺序)、CASET/RASET(设置显示窗口)、RAMWR(开启写显存)、DISPON(打开显示)等。
很多新手直接抄一份网上的初始化数组,结果发现屏幕颜色偏蓝或者上下颠倒。这里大概率是MADCTL设置的问题。MADCTL字节控制着RGB/BGR顺序、行扫描方向、列扫描方向,不同屏幕厂家在生产时贴屏的朝向不同,所以同一套初始化代码在不同批次的屏上显示方向可能就不一样。调试的时候可以在MADCTL的bit位里把RGB改成BGR试试,颜色对不上通常都是这一个寄存器导致的。
// ST7789 初始化的一部分示例 void ST7789_Init(void) { ST7789_Reset(); ST7789_WriteCmd(0x11); // Sleep Out delay_ms(50); ST7789_WriteCmd(0x3A); ST7789_WriteData(0x05); // 16bit/pixel (RGB565) ST7789_WriteCmd(0x36); ST7789_WriteData(0x00); // MADCTL: 扫描方向 // 初始化Gamma、 porch等寄存器,不同面板略有差异 ST7789_WriteCmd(0x29); // Display ON }不同厂家屏的"porch"参数(VBPD、VFPD、VBP等)可能不一样,有些屏直接用ST官方典型值就可以,有些屏会出现上部或下部有小范围花屏的问题,这时候就需要根据屏厂提供的手册微调porch参数。如果你拿不到屏厂手册,可以在网上搜索"对应驱动IC + 屏幕分辨率 + 初始化代码",通常能找到其他开发者验证过的版本。
2.2 DMA + SPI:让帧率从"能看"到"流畅"
点亮屏幕之后,马上要面对的就是"刷新率"问题。SPI接口带宽有限,3.5寸320x480的分辨率,一帧RGB565数据是320x480x2 = 307200字节。如果用26.67MHz的SPI时钟来算,理论传输时间大约是307200 x 8 / 26.67MHz ≈ 92ms,也就是只能跑到10帧左右。如果SPI时钟拉到40MHz、甚至80MHz,帧率可以提升到20帧以上。ESP32-S3的SPI最高可达80MHz,配合DMA传输,实际体感比理论值要好很多,因为LVGL不是每一帧都全屏刷新,而是只刷新变化区域。
在ESP-IDF里使用SPI DMA时,需要注意缓冲区对齐。传统的spi_transaction_t使用tx_buffer发送数据时,如果数据存放在内部RAM,性能差异还不明显;但如果存放PSRAM地址,必须保证8字节对齐,并且DMA描述符要能够访问PSRAM地址。ESP32-S3的DMA可以访问PSRAM,不过性能会比内部RAM慢一些,因此高频率刷新的缓冲最好放在内部RAM里,大块不常变的数据可以放PSRAM。
下面是ESP-IDF中SPI发送的典型代码片段:
spi_transaction_t t = { .length = len * 8, .tx_buffer = data, .user = NULL, }; spi_device_polling_transmit(spi, &t); // 或使用spi_device_queue_trans进行异步传输实际做GUI时,显示缓冲可能不止一块。LVGL通常配置一个或两个缓冲区,每个缓冲区大小可以设为屏幕总像素的1/10左右,比如一个320x480的屏,开一个50行高的缓冲区(320x50x2=32000字节)是合理的起点。配合DMA异步传输,LVGL在往缓冲区绘制的同时,DMA可以把上一块缓冲区的数据送入屏幕,实现流水线作业。
2.3 触摸芯片I2C读取与坐标回归
触摸芯片的驱动相对简单,核心就是两个职责:从I2C寄存器里读出触摸点的坐标,然后把它映射到屏幕的像素坐标系。CST816S的寄存器布局大概是:0x01寄存器表示有多少个触摸点,0x02、0x03是高8位和低8位的X坐标,0x04、0x05是Y坐标。读取流程如下:先触发一次I2C读操作,然后解析坐标,最后判断触摸状态(按下、抬起、滑动)。
坐标回归是触摸驱动里最容易出问题的环节。触摸芯片输出的坐标通常是0到240或者0到320这样的范围,而屏幕分辨率是320x480,两者之间需要按比例换算。更关键的是,触摸的X轴方向可能和屏幕的X轴显示方向相反。很多初学者第一次画了一个按钮,点击时发现按钮没反应,但乱点能看到鼠标的位置是反的,这就是因为读取到的触摸坐标没有做镜像或旋转处理。调试办法是先在屏幕上画一个网格,把触摸到的点用调试串口打印出来,对比"物理点击位置"和"上报坐标"的关系,然后写清楚映射公式。
还有一个常见问题是"长按不触发"。LVGL对触摸长按的判定依赖触摸状态的持续上报,如果你的驱动在触摸未抬起时只上报一次坐标就不再上报,LVGL会认为手指已经离开,长按事件自然无法触发。正确的做法是只要触摸芯片的INT引脚为低(表示有触摸),就要持续读取坐标并上报给LVGL,直到检测到抬起事件为止。
3. LVGL移植与FreeRTOS集成
底层驱动调通后,下一步就是把LVGL跑起来。LVGL移植到ESP32-S3有两条路:一条是直接用lvgl的官方仓库配合lvgl_esp32_drivers组件,另一条是手动移植,把lvgl源码放进工程,自己编写显示刷新函数和触摸读取函数。我个人的习惯是手动移植,虽然前期工作量多一点,但每一步都清楚工程里发生了什么,调起问题来更有把握。如果你急着做原型,也可以直接用lvgl_esp32_drivers,它封装了常见屏幕和触摸芯片的驱动。
3.1 LVGL版本选择与组件配置
LVGL目前主流版本是8.x,V9也已经发布但持续迭代中,很多第三方组件还没完全跟上。如果你做的是产品而不是技术试验,我建议用8.3.x系列,资料多、社区活跃、坑相对少。V7.11虽然是很多老工程的选择,但API设计和内存管理比8.x差不少,新项目真的没必要再用V7了。
LVGL使用一个lv_conf.h配置文件控制几乎所有功能开关,包括是否启用字体、是否启用动画、是否使用操作系统接口、内存池大小、颜色深度等。在ESP32-S3上,我们一般设置LV_COLOR_DEPTH为16,因为屏幕是RGB565;LV_MEM_SIZE设为64KB到128KB,然后让LVGL内部自己管理一部分动态内存;LV_TICK_CUSTOM设为1,让LVGL使用FreeRTOS的tick作为心跳源。
LV_HOR_RES和LV_VER_RES这两个宏在8.x里已经不是主要配置项了,取而代之的是在运行时通过lv_disp_draw_buf_init和lv_disp_drv_register来指定分辨率。这种变化让同一份固件可以适配不同分辨率的屏幕,但也提醒我们:如果你的工程是拷贝别人的,一定要检查这些运行时参数是否改成了自己屏的参数。
3.2 显示刷新接口、触摸输入接口注册
LVGL与底层硬件之间的桥接是通过两个结构体完成的:lv_disp_drv_t和lv_indev_drv_t。显示驱动结构体需要注册一个flush_cb回调函数,它的职责是把LVGL绘制完成的一整块缓冲区数据发送到屏幕。这个函数必须处理"发送完成"的信号,通过lv_disp_flush_ready告诉LVGL可以继续绘制下一块,否则LVGL会一直等待。
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); lcd_set_window(area->x1, area->y1, area->x2, area->y2); lcd_send_color_data((uint8_t *)color_p, w * h * 2); // 如果使用了DMA异步发送,需在DMA完成中断中调用 lv_disp_flush_ready(disp); // 如果同步发送,直接在这里调用 lv_disp_flush_ready(disp); }触摸输入驱动结构体需要注册一个read_cb回调函数,它的职责是告诉LVGL当前有没有触摸点、坐标是多少。>