4. 屏幕驱动与触摸校准:TFT-LCD驱动(FSMC/SPI)、触摸屏(XPT2046)校准算法
好,咱们进入第四章。这一章可以说是整个嵌入式理疗仪的门面担当——屏幕和触摸。你想想看,用户每天面对的就是这块屏,点按滑动都靠触摸。如果屏幕闪、触摸飘,那体验感直接归零。
我个人习惯把屏幕驱动和触摸校准放在一起讲,因为这两者在物理上就是叠在一起的。LCD负责显示,触摸屏负责感知,中间隔着一层玻璃。嗯,这里要注意,驱动方式和校准算法选对了,后面UI开发才能顺风顺水。
4.1 TFT-LCD驱动:FSMC vs SPI,怎么选?
驱动TFT-LCD,主流就两条路:FSMC(Flexible Static Memory Controller)和SPI。我在项目中遇到过不少新手,一上来就选SPI,觉得简单。结果刷一帧画面要几百毫秒,动画根本跑不起来。
FSMC方式,说白了就是把LCD当成一块外部内存来操作。你往某个地址写数据,LCD控制器自动把数据送到屏幕上。这种方式速度极快,16位并口一次传两个字节,刷个320×240的屏毫无压力。
SPI方式呢,串行传输,线少(4根线),但速度慢。适合小尺寸、低刷新率的屏。比如128×128的OLED,或者静态显示界面。
我的建议:如果你的理疗仪屏幕分辨率超过240×320,或者需要显示动态波形、动画,老老实实用FSMC。别为了省几个IO口,把用户体验搭进去。
下面是一个FSMC初始化LCD的代码片段,我用的是ILI9341驱动芯片,很常见:
// FSMC配置 - 以STM32F4为例
void LCD_FSMC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
FSMC_NORSRAM_TimingTypeDef Timing;
// 使能FSMC时钟
__HAL_RCC_FSMC_CLK_ENABLE();
// 配置控制引脚:CS, RS, WR, RD, RESET
// ... 引脚初始化代码省略 ...
// FSMC配置:使用Bank1, 片选NE1
hLCD.FSMC_InitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM_1;
hLCD.FSMC_InitStructure.FSMC_DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
hLCD.FSMC_InitStructure.FSMC_MemoryType = FSMC_MEMORY_TYPE_SRAM;
hLCD.FSMC_InitStructure.FSMC_MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16;
hLCD.FSMC_InitStructure.FSMC_BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE;
hLCD.FSMC_InitStructure.FSMC_WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;
hLCD.FSMC_InitStructure.FSMC_WrapMode = FSMC_WRAP_MODE_DISABLE;
hLCD.FSMC_InitStructure.FSMC_WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS;
hLCD.FSMC_InitStructure.FSMC_WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
hLCD.FSMC_InitStructure.FSMC_WaitSignal = FSMC_WAIT_SIGNAL_DISABLE;
hLCD.FSMC_InitStructure.FSMC_ExtendedMode = FSMC_EXTENDED_MODE_DISABLE;
hLCD.FSMC_InitStructure.FSMC_AsyncWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
hLCD.FSMC_InitStructure.FSMC_WriteBurst = FSMC_WRITE_BURST_DISABLE;
// 时序配置 - 关键!太快会花屏,太慢会卡顿
Timing.AddressSetupTime = 3;
Timing.AddressHoldTime = 1;
Timing.DataSetupTime = 5;
Timing.BusTurnAroundDuration = 0;
Timing.CLKDivision = 0;
Timing.DataLatency = 0;
Timing.AccessMode = FSMC_ACCESS_MODE_A;
HAL_SRAM_Init(&hLCD, &Timing, NULL);
}
避坑指南:我曾经在时序配置上栽过跟头。AddressSetupTime设得太小,结果屏幕出现横纹。后来用示波器一量,发现数据还没稳定,FSMC就把数据锁存了。调大两个时钟周期,问题解决。
4.2 触摸屏(XPT2046)驱动:SPI通信与数据读取
触摸屏这块,XPT2046是电阻屏的经典方案。它通过SPI接口输出触摸点的电压值。注意,它输出的是原始ADC值,不是像素坐标。你需要做转换和校准。
驱动XPT2046其实不复杂,就是SPI读写。但有个坑——它需要连续读取两次,取平均值,才能得到稳定的数据。我见过有人只读一次,结果触摸点跳来跳去,用户点个按钮都费劲。
// XPT2046 触摸读取函数
uint16_t TP_ReadADC(uint8_t cmd)
{
uint8_t buf[2];
uint16_t adc_val;
HAL_GPIO_WritePin(TP_CS_GPIO_Port, TP_CS_Pin, GPIO_PIN_RESET);
// 发送命令字节
HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
// 读取两个字节的ADC结果
HAL_SPI_Receive(&hspi1, buf, 2, HAL_MAX_DELAY);
HAL_GPIO_WritePin(TP_CS_GPIO_Port, TP_CS_Pin, GPIO_PIN_SET);
// 合并数据:12位有效位
adc_val = ((uint16_t)buf[0] << 8) | buf[1];
adc_val >>= 3; // 右移3位,得到12位值
return adc_val;
}
// 读取X坐标(连续读两次取平均)
uint16_t TP_ReadX(void)
{
uint16_t x1, x2;
x1 = TP_ReadADC(CMD_READ_X);
delay_us(100); // 等待采样稳定
x2 = TP_ReadADC(CMD_READ_X);
return (x1 + x2) / 2;
}
注意:XPT2046的参考电压VREF必须稳定。我遇到过用LDO供电时,触摸数据随电池电压波动。后来在VREF引脚加了个10uF电容,数据就稳了。硬件上的小细节,往往决定成败。
4.3 触摸校准算法:从ADC值到像素坐标
好,现在你能读到触摸点的ADC值了。但ADC值和屏幕像素坐标之间,不是简单的线性关系。为什么?因为触摸屏和LCD贴合时有偏移、旋转,甚至缩放不一致。
校准的目的,就是找到ADC值和像素坐标之间的映射关系。最常用的方法是三点校准法,说白了就是解一个线性方程组。
假设屏幕左上角、右下角、中心三个点的像素坐标已知,你让用户点这三个点,得到对应的ADC值。然后解出转换矩阵的系数。
| 校准点 | 像素坐标 (x, y) | ADC值 (adc_x, adc_y) |
|---|---|---|
| 左上角 | (20, 20) | (3800, 3800) |
| 右下角 | (300, 220) | (200, 200) |
| 中心 | (160, 120) | (2000, 2000) |
转换公式是这样的:
// 线性校准公式
// pixel_x = A * adc_x + B * adc_y + C
// pixel_y = D * adc_x + E * adc_y + F
// 通过三个校准点,解出A、B、C、D、E、F六个系数
// 实际代码中,我用的是矩阵求逆法
void TP_Calibrate(CalPoint *p1, CalPoint *p2, CalPoint *p3)
{
// 构建矩阵并求解
// 这里省略具体数学推导,直接给出结果
// 系数存储在全局变量中,供后续转换使用
}
核心思想:校准不是一次性的。每次设备上电,或者环境温度变化较大时,建议重新校准。我在理疗仪项目中,把校准数据存到了EEPROM里,下次开机直接读取,省去用户重复校准的麻烦。
4.4 实战经验:触摸抖动与滤波
你可能会问,校准完了,触摸就准了吗?不一定。电阻屏天生有抖动问题,尤其是手指按压时,接触电阻会变化。我做过测试,同一个点按十次,ADC值能差出50-100。
怎么办?加滤波。我常用的方法是中值滤波+滑动平均:
- 连续采集5个触摸点数据
- 去掉最大值和最小值
- 剩下的3个取平均
这样处理下来,抖动基本控制在±5个像素以内,用户完全感觉不到。
// 中值+平均滤波
uint16_t TP_Filter(uint8_t cmd)
{
uint16_t buf[5];
uint16_t temp;
int i, j;
// 采集5个点
for(i = 0; i < 5; i++)
{
buf[i] = TP_ReadADC(cmd);
delay_ms(5);
}
// 冒泡排序(数据量小,够用)
for(i = 0; i < 4; i++)
{
for(j = i+1; j < 5; j++)
{
if(buf[i] > buf[j])
{
temp = buf[i];
buf[i] = buf[j];
buf[j] = temp;
}
}
}
// 去掉最大最小,取中间三个的平均
return (buf[1] + buf[2] + buf[3]) / 3;
}
一个小技巧:在UI层面,我还会加一个"触摸释放检测"。用户手指离开后,延迟50ms再响应下一次触摸。这样可以防止用户点击时手指微颤导致的误触发。说白了,就是给用户一点"容错空间"。
好了,这一章的内容就到这里。屏幕驱动和触摸校准,是嵌入式UI开发的基石。FSMC还是SPI,三点校准还是五点校准,滤波算法怎么选——这些决策直接影响用户体验。我个人建议,前期多花点时间把底层调稳,后面UI开发会轻松很多。