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。

怎么办?加滤波。我常用的方法是中值滤波+滑动平均

  1. 连续采集5个触摸点数据
  2. 去掉最大值和最小值
  3. 剩下的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开发会轻松很多。