1. 项目概述:2英寸电容触摸LCD的定位与价值
最近在捣鼓一个需要紧凑人机交互界面的小项目,选型时一眼看中了2英寸电容触摸LCD这个组合。你可能觉得,现在满大街都是大屏,为啥还要折腾这么小的屏幕?其实在嵌入式、便携设备、智能穿戴或者一些对空间和功耗极度敏感的工控场景里,这种小尺寸高集成度的屏幕方案才是真正的“刚需”。它不像SPI TFT裸屏那样需要外挂电阻触摸,电容触摸带来的直接、流畅的滑动和点击体验,能让你的产品交互感提升一个档次。这次我用的核心是ST7789驱动的LCD和CST816D触摸芯片,通过SPI总线与主控(比如STM32)通信。整个折腾下来,从点亮屏幕到驱动触摸,再到优化刷新和抗干扰,踩了不少坑,也总结了一套比较稳的实操流程。如果你也在为如何高效驱动一块小尺寸电容屏而头疼,或者好奇SPI驱动LCD和触摸芯片的细节,那这篇从硬件连接到软件调试的全程记录,应该能给你省下不少时间。
2. 核心组件深度解析:为什么是ST7789和CST816D?
2.1 屏幕驱动IC:ST7789T3的能耐与局限
ST7789是一颗非常经典的TFT LCD驱动芯片,支持最高240x320的分辨率,对于2英寸屏来说,240x320(即所谓的QVGA)是一个甜点分辨率,在精细度和驱动开销之间取得了很好的平衡。它内部集成了显存(GRAM),这意味着主控MCU只需要通过SPI接口将图像数据“喂”给ST7789,后续的扫描和维持显示都由它自己完成,大大减轻了MCU的负担,特别适合那些没有专用LCD控制器或内存有限的MCU(比如STM32F1系列)。
ST7789通常支持3线或4线SPI模式。3线模式只使用SDA(数据线)、SCL(时钟线)和CS(片选),通过DC(数据/命令选择)引脚的高低电平来区分发送的是命令还是数据。4线模式则把SDA拆分成SDO和SDI,实现全双工,但在驱动屏幕时,我们通常只使用半双工通信,所以3线模式更常见,也更节省IO口。它的通信速率可以跑到几十MHz,但对于2英寸屏,实际上不需要那么高,因为像素总量不大,合理的速率设置(如20-40MHz)既能保证刷新率,又能降低EMI干扰。
需要注意的是,ST7789的初始化序列比较长,包含伽马校正、电源控制、内存访问控制等一大堆寄存器配置。不同厂家、不同分辨率的屏幕,初始化序列可能有细微差别,最好直接从卖家那里获取确切的初始化代码,否则可能出现颜色不对、花屏、显示偏移等问题。
2.2 触摸控制IC:CST816D的交互逻辑
CST816D是一颗专注于小尺寸电容屏的触摸芯片,支持单点触摸(有些型号支持两点),通过I2C接口上报触摸事件。选择它而不是更常见的FT系列或其他SPI触摸芯片,主要基于几点考虑:首先是功耗,CST816D在待机模式下功耗极低,非常适合电池供电设备;其次是集成度,它内部集成了手势识别(如上下左右滑动),可以减轻MCU的运算负担;最后是它的中断功能,有触摸事件时才通过INT引脚唤醒MCU,而不是让MCU不停地去轮询I2C总线,这是实现低功耗交互的关键。
它的工作原理是持续监测屏体的电容变化。当手指触摸时,会改变局部电场,芯片检测到这种变化,通过算法计算出触摸点的坐标(X, Y),并通过I2C接口输出。这里有一个关键参数:报点率。报点率太低,触摸会感觉“卡顿”;太高,又会增加不必要的功耗和总线负载。CST816D通常可以配置在10Hz到100Hz之间,对于2英寸屏和一般的手指操作,30-60Hz是一个比较均衡的设置。
2.3 SPI与I2C双总线架构的协同考量
这个方案采用了SPI驱动显示、I2C驱动触摸的双总线架构。这几乎是这类分立芯片方案的标配。为什么不用统一的SPI?因为功能侧重点不同。ST7789需要高速、单向(主要是MCU到屏)地传输大量像素数据,SPI的推流能力更强。而CST816D只需要在触摸事件发生时,低速、双向地传输几个字节的坐标和状态数据,I2C的两线制(SDA, SCL)更节省IO,且支持多设备挂载(虽然这里只有一个)。
在实际布线时,要特别注意两条总线的走线分离,尤其是SPI的时钟线(SCLK),它是高速开关信号,容易成为噪声源。如果可能,尽量让SPI走线和I2C走线不要长距离平行,或者在中间加地线隔离。同时,给两个芯片的电源引脚加上足够的去耦电容(通常用0.1uF和10uF并联),并且尽可能靠近芯片的VCC和GND引脚,这是保证稳定工作的基础。
3. 硬件连接与电路设计要点
3.1 引脚定义与连接对照表
拿到一块2英寸电容屏模块,背面通常会有一个FPC排线接口。我们需要将其引脚一一对应地连接到MCU。以下是一个典型的连接示意表格,以STM32F103系列MCU为例:
| 屏幕模块引脚 | 功能说明 | 连接至STM32引脚 | 备注 |
|---|---|---|---|
| VCC | 电源正极 (3.3V) | 3.3V | 绝对禁止接5V! |
| GND | 电源地 | GND | |
| SCL / SCLK | SPI时钟线 | PA5 (SPI1_SCK) | |
| SDA / MOSI | SPI主出从入数据线 | PA7 (SPI1_MOSI) | 发送数据/命令给屏幕 |
| RESET | 屏幕硬件复位 | PA0 (自定义GPIO) | 低电平有效,用于彻底复位屏幕 |
| DC / RS | 数据/命令选择 | PA1 (自定义GPIO) | 高电平:数据;低电平:命令 |
| CS | SPI片选 | PA4 (SPI1_NSS) | 低电平选中屏幕SPI |
| SDO / MISO | SPI主入从出数据线 | PA6 (SPI1_MISO) | 本例中ST7789未使用此功能,可不接 |
| TP_INT | 触摸中断输出 | PA8 (自定义GPIO,外部中断) | 触摸发生时,CST816D拉低此引脚 |
| TP_SDA | 触摸I2C数据线 | PB7 (I2C1_SDA) | 需配置为上拉模式 |
| TP_SCL | 触摸I2C时钟线 | PB6 (I2C1_SCL) | 需配置为上拉模式 |
注意:不同厂家的模块引脚顺序可能不同,务必以卖家提供的资料为准。
RESET和DC引脚必须用普通GPIO控制,不能用SPI的硬件控制信号替代。
3.2 电源与信号完整性设计
电源是第一个要保障的。ST7789和CST816D通常都是3.3V供电,必须确保你的MCU系统也是3.3V电平。如果MCU是5V系统,则需要电平转换电路,或者寻找支持5V容忍IO的MCU并确保其IO口输出高电平为3.3V。直接接入5V会永久损坏芯片。
去耦电容的布置是硬件调试中最容易忽视但至关重要的一环。理想情况下,在每个芯片的VCC和GND引脚之间,最近的位置,应该并联一个0.1uF(104)的陶瓷电容和一个10uF的钽电容或电解电容。0.1uF负责滤除高频噪声(比如SPI时钟产生的高频谐波),10uF负责提供瞬时大电流(比如屏幕全白刷新时电流会骤增)。如果屏幕出现闪烁、花屏或者触摸乱跳,首先检查电源纹波和去耦电容是否到位。
对于信号线,如果连接线较长(超过10cm),或者环境干扰较大,可以考虑串联一个22Ω到100Ω的小电阻在SCLK、MOSI等高速线上,这有助于抑制信号过冲和振铃,改善信号质量。触摸芯片的I2C线(SDA, SCL)通常需要外部上拉电阻,一般取值4.7kΩ到10kΩ,如果MCU内部上拉足够强(如STM32的GPIO可配置为上拉模式),也可以省略外部电阻,但为了可靠性,我建议还是加上。
4. 底层驱动软件实现详解
4.1 ST7789 SPI显示驱动编写
驱动ST7789,本质上是按照它的指令集,通过SPI总线发送一系列命令和数据来配置它,然后向它的GRAM中写入像素数据。我们通常会用一组基本的底层函数来封装这些操作。
首先是GPIO和SPI的初始化。以STM32 HAL库为例:
// SPI初始化 (以SPI1为例) void SPI1_Init(void) { hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 双线模式,只发不收 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // 时钟极性CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // 时钟相位CPHA=0,模式0 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制片选,非常重要! hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; // 分频系数,决定速率 hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi1.Init.CRCPolynomial = 10; if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); } }这里的关键是SPI_NSS_SOFT(软件片选)。硬件片选(NSS)在多个SPI设备时很有用,但对于单个屏幕,软件控制CS引脚更加灵活可靠。时钟模式(CPOL和CPHA)必须严格按照屏幕数据手册设置,ST7789通常使用模式0。
接着,我们需要编写最基本的写命令和写数据函数:
// 写命令 void LCD_Write_Cmd(uint8_t cmd) { LCD_DC_GPIO_Port->BSRR = (uint32_t)LCD_DC_Pin << 16; // DC置低,表示命令 LCD_CS_GPIO_Port->BSRR = (uint32_t)LCD_CS_Pin << 16; // CS置低,选中设备 HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); LCD_CS_GPIO_Port->BSRR = LCD_CS_Pin; // CS置高,释放 } // 写数据 void LCD_Write_Data(uint8_t data) { LCD_DC_GPIO_Port->BSRR = LCD_DC_Pin; // DC置高,表示数据 LCD_CS_GPIO_Port->BSRR = (uint32_t)LCD_CS_Pin << 16; HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY); LCD_CS_GPIO_Port->BSRR = LCD_CS_Pin; }有了这两个函数,就可以发送初始化序列了。初始化序列是一长串固定的命令和数据,用于开启内部振荡器、设置驱动方向、伽马曲线、像素格式等等。这部分代码通常很长,有上百行,建议直接从卖家提供的示例代码中复制,这是最稳妥的。
初始化完成后,最重要的函数是设置显示窗口和写入GRAM。由于屏幕是240x320,但我们可能只需要更新其中一部分(比如一个图标),为了加速,可以先设置一个矩形窗口,然后连续向这个窗口内写入像素数据。
// 设置显示窗口 (x_start, y_start) 到 (x_end, y_end) void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_Write_Cmd(0x2A); // 列地址设置命令 LCD_Write_Data(x1 >> 8); LCD_Write_Data(x1 & 0xFF); LCD_Write_Data(x2 >> 8); LCD_Write_Data(x2 & 0xFF); LCD_Write_Cmd(0x2B); // 行地址设置命令 LCD_Write_Data(y1 >> 8); LCD_Write_Data(y1 & 0xFF); LCD_Write_Data(y2 >> 8); LCD_Write_Data(y2 & 0xFF); LCD_Write_Cmd(0x2C); // 内存写命令,接下来发送的都是像素数据 } // 填充颜色到指定窗口 void LCD_FillColor(uint16_t color, uint32_t size) { uint8_t hi = color >> 8, lo = color & 0xFF; LCD_DC_GPIO_Port->BSRR = LCD_DC_Pin; LCD_CS_GPIO_Port->BSRR = (uint32_t)LCD_CS_Pin << 16; for(uint32_t i=0; i<size; i++) { // 连续发送,效率较低,后面会讲DMA优化 HAL_SPI_Transmit(&hspi1, &hi, 1, HAL_MAX_DELAY); HAL_SPI_Transmit(&hspi1, &lo, 1, HAL_MAX_DELAY); } LCD_CS_GPIO_Port->BSRR = LCD_CS_Pin; }像素颜色格式通常是RGB565,即16位色,R(5位)、G(6位)、B(5位)。color参数就是一个16位的无符号整数。
4.2 CST816D I2C触摸驱动与中断处理
触摸驱动的核心是配置I2C和中断引脚,并在中断服务函数中读取坐标。首先初始化I2C,同样使用STM32 HAL库:
void I2C1_Init(void) { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 400000; // 400kHz,标准模式 hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } }CST816D的I2C设备地址通常是0x15(7位地址)。触摸中断引脚(TP_INT)需要配置为外部中断下降沿触发(因为触摸时通常是低电平有效)。
// 配置触摸中断引脚 void TP_EXTI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin = TP_INT_Pin; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发 GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉,确保空闲时为高电平 HAL_GPIO_Init(TP_INT_GPIO_Port, &GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI9_5_IRQn, 0, 0); HAL_NVIC_EnableIRQ(EXTI9_5_IRQn); }在中断服务函数中,我们读取触摸状态和坐标。CST816D有几个关键寄存器:
- 0x00: 手势ID(如滑动方向)
- 0x01: 触摸点数(通常为1)
- 0x03: X坐标高8位
- 0x04: X坐标低8位
- 0x05: Y坐标高8位
- 0x06: Y坐标低8位
// 在EXTI中断服务函数中调用 void TP_IRQHandler(void) { if(__HAL_GPIO_EXTI_GET_IT(TP_INT_Pin) != RESET) { __HAL_GPIO_EXTI_CLEAR_IT(TP_INT_Pin); uint8_t data[7]; // 从寄存器0x00开始,连续读取7个字节 if(HAL_I2C_Mem_Read(&hi2c1, CST816D_ADDR<<1, 0x00, I2C_MEMADD_SIZE_8BIT, data, 7, 10) == HAL_OK) { if(data[1] > 0) { // 有触摸点 uint16_t touchX = ((data[3] & 0x0F) << 8) | data[4]; uint16_t touchY = ((data[5] & 0x0F) << 8) | data[6]; // 将坐标存入全局变量,供主循环处理 g_touchX = touchX; g_touchY = touchY; g_touchEvent = 1; // 设置触摸事件标志 } } } }这里有一个关键点:从I2C读取数据后,需要根据屏幕的实际安装方向对坐标进行转换。因为触摸芯片的坐标系是固定的,但屏幕可能被旋转了90°或180°显示。转换公式需要根据你调用LCD_SetRotation()函数设置的方向来调整。
4.3 使用DMA优化SPI刷屏性能
用HAL_SPI_Transmit一个字节一个字节地发数据来刷全屏(240*320=76800个像素,每个像素2字节,共153600字节),速度会非常慢,可能只有几帧每秒,动画效果根本没法看。这时就必须请出DMA(直接存储器访问)。
DMA可以让SPI控制器直接从内存中读取数据并发送,而不需要CPU参与。配置步骤稍复杂:
- 开启DMA时钟,并配置DMA流(对于STM32F4/H7等)或通道(对于F1)。
- 将SPI的发送请求与DMA通道绑定。
- 准备一个存放像素数据的缓冲区(比如
uint16_t frameBuffer[320][240])。 - 调用
HAL_SPI_Transmit_DMA启动传输。
// 假设使用SPI1的TX DMA,流3,通道3 (STM32F4) uint16_t lcd_buffer[76800]; // 全屏缓冲区,大小=240*320 void SPI1_DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); hdma_spi1_tx.Instance = DMA2_Stream3; hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3; hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 注意是字节! hdma_spi1_tx.Init.Mode = DMA_NORMAL; // 普通模式,传输一次 hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH; hdma_spi1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; if (HAL_DMA_Init(&hdma_spi1_tx) != HAL_OK) { Error_Handler(); } __HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx); } // 使用DMA刷屏 void LCD_Refresh_DMA(uint16_t *buf, uint32_t size) { LCD_SetWindow(0, 0, 239, 319); // 设置全屏窗口 LCD_DC_GPIO_Port->BSRR = LCD_DC_Pin; // DC=1,写数据 LCD_CS_GPIO_Port->BSRR = (uint32_t)LCD_CS_Pin << 16; // CS=0 // 注意:buf中的数据必须是已经按RGB565排列好的字节流。 // 如果buf是uint16_t数组,需要转换为uint8_t指针,且size是字节数(像素数*2) HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)buf, size*2); // 传输完成后,SPI的TX完成中断或DMA传输完成中断会被触发 }使用DMA后,刷屏速度可以提升几十倍,轻松达到30fps以上。但要注意内存对齐和缓冲区管理。另外,在DMA传输期间,CPU可以去处理其他任务(比如解析触摸坐标),实现并行。
5. 高级优化与实战调试技巧
5.1 显示性能优化:从全屏刷新到局部更新
即使用了DMA,频繁刷新整个153600字节的帧缓冲区也是不小的负担。真正的优化在于“只更新变化的部分”。这就需要引入“脏矩形”机制。基本思路是:在应用层,当某个UI元素(如按钮、图标)状态改变时,只标记它所在的屏幕矩形区域为“脏区”。在显示驱动层,用一个任务或定时器,检查所有脏矩形,然后只向ST7789发送这些矩形区域内的像素数据。
例如,你有一个在(10,10)位置,大小为50x30的按钮需要从灰色变成红色。
- 应用层调用:
GUI_MarkDirtyRect(10, 10, 60, 40);。 - 在VSync(垂直同步)或一个低优先级任务中,驱动层收集所有脏矩形,合并(如果重叠),然后对每个矩形:
LCD_SetWindow(dirtyRect.x, dirtyRect.y, dirtyRect.x+dirtyRect.w-1, dirtyRect.y+dirtyRect.h-1); // 然后只从帧缓冲区中提取这个矩形对应的数据,通过DMA发送 uint32_t offset = dirtyRect.y * SCREEN_WIDTH + dirtyRect.x; LCD_Refresh_DMA(&frameBuffer[offset], dirtyRect.w * dirtyRect.h); - 清空脏矩形列表。
这样可以极大减少SPI总线上的数据量,降低功耗,并让CPU有更多时间处理业务逻辑。
5.2 触摸响应与滤波算法
从CST816D读出的原始坐标可能会有抖动和噪声,直接使用会导致光标跳动。必须加入软件滤波。最简单有效的是“滑动平均滤波”。
#define TOUCH_FILTER_DEPTH 5 uint16_t touchX_history[TOUCH_FILTER_DEPTH] = {0}; uint16_t touchY_history[TOUCH_FILTER_DEPTH] = {0}; uint8_t history_index = 0; uint16_t Filter_TouchCoordinate(uint16_t newX, uint16_t newY) { touchX_history[history_index] = newX; touchY_history[history_index] = newY; history_index = (history_index + 1) % TOUCH_FILTER_DEPTH; uint32_t sumX = 0, sumY = 0; for(int i=0; i<TOUCH_FILTER_DEPTH; i++) { sumX += touchX_history[i]; sumY += touchY_history[i]; } g_filteredX = sumX / TOUCH_FILTER_DEPTH; g_filteredY = sumY / TOUCH_FILTER_DEPTH; }更高级的可以加入“野值剔除”,即判断新读到的坐标如果与前一个滤波后的坐标差值超过某个阈值(比如20个像素),则认为是噪声,将其丢弃或用历史值替代。
对于手势识别,CST816D本身能上报简单手势(通过0x00寄存器),但有时不够用。我们可以自己实现:在触摸按下时记录起始坐标和时间戳,在触摸释放时计算位移和耗时,从而判断是单击、长按、还是向某个方向的滑动。
5.3 低功耗策略设计
在电池供电的设备中,屏幕和触摸是耗电大户。一个完整的低功耗策略包括:
- 屏幕背光控制:通过PWM控制背光LED的亮度,在无操作一段时间后自动调暗或关闭。这是省电的大头。
- 屏幕睡眠:ST7789支持睡眠命令(
0x10),进入睡眠模式后功耗可以降到微安级。在设备待机时,可以依次执行:关闭背光 -> 让ST7789进入睡眠 -> 关闭其电源(如果硬件支持独立电源控制)。 - 触摸芯片睡眠:CST816D也有低功耗模式。可以通过I2C发送命令让其进入休眠,此时它只会消耗极小的电流,但依然能检测到触摸,并通过INT引脚唤醒MCU。MCU自身也可以进入Stop或Sleep模式,等待触摸中断唤醒整个系统。
- 动态刷新率:在显示静态画面时,可以降低SPI的时钟频率,或者延长全屏刷新的间隔时间。
实现这些需要硬件上支持背光独立供电、屏幕电源控制,以及软件上良好的状态机管理。
6. 常见问题排查与解决方案实录
在实际调试中,你几乎一定会遇到下面这些问题。我把它们和排查思路整理成了表格,方便快速对照。
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 屏幕白屏或完全不亮 | 1. 电源接错(如接了5V) 2. 背光未开启 3. 复位时序不对 4. SPI根本不通 | 1.立即断电,用万用表测量屏幕VCC引脚电压是否为3.3V。 2. 检查背光LED引脚(可能叫BLK或LED)是否接在了可控的GPIO或直接接3.3V,尝试给高电平。 3. 用逻辑分析仪或示波器抓取 RESET引脚波形,确保有一个>10ms的低电平脉冲,然后拉高。4. 用逻辑分析仪抓SPI的CS、DC、SCLK、MOSI信号,看初始化序列是否正常发出。 |
| 屏幕花屏、错位、颜色异常 | 1. 初始化序列不正确 2. 像素格式设置错误 3. 内存访问控制(MADCTL)寄存器设置错误,导致扫描方向不对 4. SPI时钟极性/相位不对 | 1. 这是最高发问题。务必使用卖家提供的、针对你这款屏幕的确切初始化代码,不同批次的屏都可能不同。 2. 检查发送的像素数据格式是否为RGB565(通常命令 0x3A后面跟0x55或0x65)。3. 调整MADCTL寄存器的值(命令 0x36),尝试0x00, 0xC0, 0xA0等不同值,结合LCD_SetRotation函数,找到正确的显示方向。4. 确认SPI的CPOL和CPHA设置与屏幕要求一致。 |
| 触摸无反应 | 1. I2C通信失败 2. 触摸中断引脚配置错误 3. 触摸芯片未正确初始化 4. 坐标转换错误 | 1. 用逻辑分析仪抓I2C总线,看是否能正确读到设备地址(0x15)的应答。检查上拉电阻。 2. 测量TP_INT引脚,触摸时是否产生下降沿。检查GPIO中断配置。 3. CST816D有时也需要简单的初始化(如复位),查阅其数据手册。 4. 在中断中读出原始坐标后,打印出来看是否在合理范围(如0~240,0~320),然后根据屏幕旋转方向应用转换公式。 |
| 触摸坐标跳变、不准确 | 1. 电源噪声 2. 没有软件滤波 3. 屏体被外力挤压或贴合不良 | 1. 用示波器测量触摸芯片的VCC,看是否有毛刺。加强电源去耦。 2. 实现如前文所述的滑动平均滤波和野值剔除算法。 3. 检查屏幕的FPC排线是否连接牢固,触摸屏与LCD之间是否有气泡或异物。 |
| 使用DMA刷屏时系统卡死或数据错乱 | 1. DMA缓冲区溢出或内存访问冲突 2. SPI和DMA时钟未使能 3. 数据传输未完成时修改了缓冲区数据 | 1. 确保DMA缓冲区大小正确,且位于有效的内存区域(如CCM RAM或普通SRAM)。 2. 检查 __HAL_RCC_SPI1_CLK_ENABLE()和__HAL_RCC_DMA2_CLK_ENABLE()是否被调用。3. 在DMA传输完成中断回调函数中设置一个标志位,主循环检测这个标志位为“就绪”后,再准备下一帧数据或修改已发送的缓冲区。 |
| 整体功耗过高 | 1. 背光常亮且亮度高 2. 屏幕未进入睡眠 3. MCU未进入低功耗模式 4. SPI时钟速率过高 | 1. 引入背光PWM调光,并在空闲时降低亮度或关闭。 2. 在长时间无显示更新时,发送ST7789的睡眠命令( 0x10)。3. 在系统空闲时,调用 __WFI()或HAL_PWR_EnterSLEEPMode()让MCU休眠,由触摸中断唤醒。4. 在低速刷新静态画面时,动态降低SPI的波特率分频系数。 |
调试这类问题,逻辑分析仪是比示波器更趁手的工具。它能同时捕获SPI、I2C的多条数据线,并以时序和协议解析的方式直观显示,对于检查初始化序列、触摸数据包是否正确至关重要。
最后,分享一个我踩过的坑:有一次调试,屏幕显示正常,但触摸完全没反应。查了半天电路和代码都没问题。最后发现,是屏幕的FPC排线座上有一个透明的塑料保护盖没有撕掉!它绝缘了触摸屏的感应层。所以,硬件检查一定要从最物理、最傻的地方开始。