LCD触摸驱动标准库移植HAL库(1)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

207.3.6 LCD触摸驱动标准库移植HAL库(1)

/*
 * 本节目标:把标准库触摸示例的文件和主循环移入 HAL 工程,并完成 PEN 读取及 SPI 32 分频写法的迁移。
 * 收录边界:下列代码保持视频第 207 课结束时的状态;此时工程仍有旧标准库接口,尚未编译、上板通过。
 * 后续边界:恢复 LCD 所需的 4 分频、触摸 SPI 收发、片选和 delay_ms() 均在第 208 课继续迁移,本节不提前补入。
 */

/* main.c:加入触摸驱动头文件。 */
#include "touch.h"                                      /* 声明 TP_Init()、tp_dev 和触摸状态位。 */

/* main.c / main():从标准库示例复制进 HAL 工程的触摸演示业务代码。 */
u16 lastpos[2];                                         /* 保存上一次有效触摸点的 X、Y 坐标。 */
lastpos[0]=0XFFFF;                                      /* 用 0xFFFF 标记“当前还没有上一个点”。 */
LCD_Init();                                             /* 初始化 LCD;这是前几课已经移植好的显示接口。 */
LCD_Fill(0,0,LCD_W,LCD_H,WHITE);                       /* 把整个屏幕填成白色,作为画线背景。 */
TP_Init();                                              /* 装载当前屏幕方向对应的触摸校准参数。 */
LCD_ShowString(10,LCD_H-40,"X:",RED,WHITE,16,0);       /* 在屏幕底部显示 X 坐标标签。 */
LCD_ShowIntNum(26,LCD_H-40,0,3,RED,WHITE,16);          /* 先把 X 坐标显示为 0。 */
LCD_ShowString(10,LCD_H-20,"Y:",RED,WHITE,16,0);       /* 在下一行显示 Y 坐标标签。 */
LCD_ShowIntNum(26,LCD_H-20,0,3,RED,WHITE,16);          /* 先把 Y 坐标显示为 0。 */
while (1)                                               /* 持续轮询触摸屏。 */
{                                                       /* 主循环开始。 */
    tp_dev.scan(0);                                     /* 经函数指针调用 TP_Scan(0),读取并换算屏幕坐标。 */
    if(tp_dev.sta&TP_PRES_DOWN)                         /* 状态字节 bit7 为 1 时,表示触摸仍处于按下状态。 */
    {                                                   /* 进入按下处理分支。 */
        delay_ms(1);                                    /* 标准库遗留延时;本节尚未换成 HAL_Delay()。 */
        if((tp_dev.x[0]<(LCD_W-1)&&tp_dev.x[0]>=1)&&    /* 要求 X 位于屏幕内部,给画线边缘留出 1 像素。 */
           (tp_dev.y[0]<(LCD_H-1)&&tp_dev.y[0]>=1))     /* 要求 Y 也位于屏幕内部。 */
        {                                               /* 坐标有效时才绘制。 */
            if(lastpos[0]==0XFFFF)                      /* 第一次取得有效坐标时还没有线段起点。 */
            {                                           /* 初始化上一个点。 */
                lastpos[0]=tp_dev.x[0];                 /* 把当前 X 记为起点 X。 */
                lastpos[1]=tp_dev.y[0];                 /* 把当前 Y 记为起点 Y。 */
            }                                           /* 第一次触摸点初始化结束。 */
            LCD_DrawRoughLine(lastpos[0],lastpos[1],    /* 以保存的上一个点作为线段起点。 */
                              tp_dev.x[0],tp_dev.y[0],   /* 以当前触摸点作为线段终点。 */
                              BLUE);                    /* 使用蓝色绘制粗线。 */
            lastpos[0]=tp_dev.x[0];                     /* 当前 X 成为下一条线段的起点 X。 */
            lastpos[1]=tp_dev.y[0];                     /* 当前 Y 成为下一条线段的起点 Y。 */
            LCD_ShowString(10,LCD_H-40,"X:",RED,WHITE,16,0); /* 重绘 X 标签。 */
            LCD_ShowIntNum(26,LCD_H-40,tp_dev.x[0],3,RED,WHITE,16); /* 显示当前 X。 */
            LCD_ShowString(10,LCD_H-20,"Y:",RED,WHITE,16,0); /* 重绘 Y 标签。 */
            LCD_ShowIntNum(26,LCD_H-20,tp_dev.y[0],3,RED,WHITE,16); /* 显示当前 Y。 */
        }                                               /* 有效坐标处理结束。 */
    }                                                   /* 按下处理结束。 */
}                                                       /* 返回循环顶部,继续扫描。 */

/* touch.h:去掉标准库工程的 sys.h,改用 HAL 工程已有的 main.h。 */
//#include "sys.h"                                     /* 标准库专用头文件在 HAL 工程中不存在,因此停用。 */
#include "main.h"                                      /* 提供 u8/u16/u32 以及 PEN、CS2 等 CubeMX 引脚宏。 */
#define Adujust      1                                  /* 保留课程源码拼写:1 用预设校准值,0 进入手动校准。 */
#define TP_PRES_DOWN 0x80                               /* 触摸状态字节 bit7:当前处于按下状态。 */
#define TP_CATH_PRES 0x40                               /* 触摸状态字节 bit6:捕获到一次按下事件。 */
#define CT_MAX_TOUCH  5                                 /* 坐标数组长度沿用原驱动;本电阻屏实际使用 x[0]/y[0]。 */
typedef struct                                          /* 定义触摸设备的数据和接口集合。 */
{                                                       /* 结构体成员开始。 */
    u8 (*init)(void);                                   /* 函数指针,最终指向 TP_Init()。 */
    u8 (*scan)(u8);                                     /* 函数指针,最终指向 TP_Scan()。 */
    void (*adjust)(void);                               /* 函数指针,最终指向 TP_Adjust()。 */
    u16 x[CT_MAX_TOUCH];                                /* 保存 X 坐标;电阻屏主要使用下标 0 和 4。 */
    u16 y[CT_MAX_TOUCH];                                /* 保存 Y 坐标;电阻屏主要使用下标 0 和 4。 */
    u8 sta;                                             /* bit7 表示按下,bit6 表示捕获到按下事件。 */
    float xfac;                                         /* 原始 X 到屏幕坐标的比例系数。 */
    float yfac;                                         /* 原始 Y 到屏幕坐标的比例系数。 */
    short xoff;                                         /* 屏幕 X 坐标的偏移量。 */
    short yoff;                                         /* 屏幕 Y 坐标的偏移量。 */
}_m_tp_dev;                                             /* 触摸设备结构体类型定义结束。 */
extern _m_tp_dev tp_dev;                                /* 声明 touch.c 中定义的全局触摸设备对象。 */

/* touch.c:把结构体中的三个接口绑定到实际函数。 */
_m_tp_dev tp_dev=                                      /* 定义全局触摸设备对象。 */
{                                                       /* 聚合初始化开始。 */
    TP_Init,                                            /* init 函数指针绑定 TP_Init()。 */
    TP_Scan,                                            /* scan 函数指针绑定 TP_Scan()。 */
    TP_Adjust,                                          /* adjust 函数指针绑定 TP_Adjust()。 */
    0,                                                  /* x[] 全部清零。 */
    0,                                                  /* y[] 全部清零。 */
    0,                                                  /* sta 清零。 */
    0,                                                  /* xfac 清零,稍后由 TP_Init() 写入。 */
    0,                                                  /* yfac 清零,稍后由 TP_Init() 写入。 */
    0,                                                  /* xoff 清零,稍后由 TP_Init() 写入。 */
    0,                                                  /* yoff 清零,稍后由 TP_Init() 写入。 */
};                                                      /* 全局触摸设备对象初始化结束。 */

/* lcd_init.h:把标准库的 PBin(0) 改成 HAL GPIO 读取。 */
#define PEN HAL_GPIO_ReadPin(PEN_GPIO_Port, PEN_Pin)    /* 读取 PB0;XPT2046 的 PENIRQ 为低电平有效。 */

/* touch.c:扫描逻辑本身保持课程源码原样,只通过上面的 PEN 宏适配 HAL。 */
u8 TP_Scan(u8 tp)                                       /* tp=0 返回屏幕坐标,tp=1 保留原始物理坐标。 */
{                                                       /* 扫描函数开始。 */
    if(PEN==0)                                          /* PB0 为低电平表示触摸笔已经按下。 */
    {                                                   /* 处理按下状态。 */
        if(tp)TP_Read_XY2(&tp_dev.x[0],&tp_dev.y[0]);   /* 校准模式读取原始物理坐标。 */
        else if(TP_Read_XY2(&tp_dev.x[0],&tp_dev.y[0])) /* 普通模式读取原始坐标,成功后再换算。 */
        {                                               /* 双次采样成功。 */
            tp_dev.x[0]=tp_dev.xfac*tp_dev.x[0]+tp_dev.xoff; /* 把原始 X 映射为屏幕 X。 */
            tp_dev.y[0]=tp_dev.yfac*tp_dev.y[0]+tp_dev.yoff; /* 把原始 Y 映射为屏幕 Y。 */
        }                                               /* 坐标换算结束。 */
        if((tp_dev.sta&TP_PRES_DOWN)==0)                /* 若上一轮尚未记录“按下”。 */
        {                                               /* 处理新按下事件。 */
            tp_dev.sta=TP_PRES_DOWN|TP_CATH_PRES;       /* 同时设置持续按下位和捕获按下位。 */
            tp_dev.x[4]=tp_dev.x[0];                    /* 保存第一次按下时的 X。 */
            tp_dev.y[4]=tp_dev.y[0];                    /* 保存第一次按下时的 Y。 */
        }                                               /* 新按下事件处理结束。 */
    }else                                               /* PEN 为高电平,当前没有触摸。 */
    {                                                   /* 处理松开状态。 */
        if(tp_dev.sta&TP_PRES_DOWN)                     /* 如果上一轮仍记录为按下。 */
        {                                               /* 从按下转为松开。 */
            tp_dev.sta&=~(1<<7);                        /* 清除 bit7,留下 bit6 供上层识别一次点击。 */
        }else                                           /* 此前也没有按下。 */
        {                                               /* 把无触摸时的坐标恢复为哨兵值。 */
            tp_dev.x[4]=0;                              /* 清除首次按下 X。 */
            tp_dev.y[4]=0;                              /* 清除首次按下 Y。 */
            tp_dev.x[0]=0xffff;                         /* 当前 X 置为无效值 0xFFFF。 */
            tp_dev.y[0]=0xffff;                         /* 当前 Y 置为无效值 0xFFFF。 */
        }                                               /* 无触摸状态维护结束。 */
    }                                                   /* PEN 电平判断结束。 */
    return tp_dev.sta&TP_PRES_DOWN;                     /* 仅返回 bit7,因此结果是 0 或 0x80。 */
}                                                       /* 扫描函数结束。 */

/* touch.c:本节只完成进入触摸读取前的 32 分频改写。 */
#define ERR_RANGE 50                                    /* 两次采样在每个轴上的允许偏差小于 50。 */
u8 TP_Read_XY2(u16 *x,u16 *y)                          /* 连续读取两组 X/Y,并进行一致性判断。 */
{                                                       /* 加强滤波函数开始。 */
    u16 x1,y1;                                          /* 保存第一组原始坐标。 */
    u16 x2,y2;                                          /* 保存第二组原始坐标。 */
    u8 flag;                                            /* 保存底层读取成功或失败状态。 */
    //SPI1_SetSpeed(SPI_BaudRatePrescaler_32);          /* 原标准库接口已停用。 */
    hspi1.Instance->CR1&=0XFFC7;                        /* 清零 SPI1_CR1 的 BR[2:0],即 bit5~bit3。 */
    hspi1.Instance->CR1|=4<<3;                          /* 把 100 写入 BR[2:0],选择外设时钟 32 分频。 */
    flag=TP_Read_XY(&x1,&y1);                           /* 读取第一组 X/Y;底层接口将在下一课迁移。 */
    if(flag==0)return(0);                               /* 第一组读取失败则立即返回 0。 */
    flag=TP_Read_XY(&x2,&y2);                           /* 读取第二组 X/Y。 */
    if(flag==0)return(0);                               /* 第二组读取失败也立即返回 0。 */
    SPI1_SetSpeed(SPI_BaudRatePrescaler_4);             /* 本节尚未迁移的标准库接口,第 208 课才改为 HAL 写法。 */
    if(((x2<=x1&&x1<x2+ERR_RANGE)||(x1<=x2&&x2<x1+ERR_RANGE))&& /* 判断两次 X 的差值是否小于 50。 */
       ((y2<=y1&&y1<y2+ERR_RANGE)||(y1<=y2&&y2<y1+ERR_RANGE))) /* 判断两次 Y 的差值是否小于 50。 */
    {                                                   /* 两个轴的两次采样都足够接近。 */
        *x=(x1+x2)/2;                                   /* 取两个 X 的平均值并通过指针输出。 */
        *y=(y1+y2)/2;                                   /* 取两个 Y 的平均值并通过指针输出。 */
        return 1;                                       /* 返回 1 表示本次读取可信。 */
    }else return 0;                                     /* 偏差过大则丢弃本次结果。 */
}                                                       /* 加强滤波函数结束。 */

一、触摸部分代码移植

本节从第 11 个“中景园硬件 SPI 带触摸标准库例程”中提取触摸代码,移入上一课已经能正常显示 LCD 的 HAL 工程。屏幕控制器是 ST7796,电阻触摸控制器是 XPT2046;两者共用 SPI1 的时钟和数据线,但使用不同片选,并需要不同 SPI 分频。

本课只完成迁移的前半段。结束时已经完成文件导入、类型与 GPIO 读取适配,以及进入触摸读取前的 32 分频设置;恢复 4 分频和真正收发触摸数据等工作留到第 208 课。因此不能把本课结尾理解成“触摸驱动已经移植成功”。

1.1 打开之前的工程

视频同时打开两个基础工程:

  • 第 11 个工程:厂家提供的标准外设库触摸示例,是本节代码来源;
  • 第 12 个工程:前几课完成的 HAL LCD 显示工程,显示已正常、触摸尚不可用。

复制第 12 个工程并改名为 13LCD_Display_Touch,再在副本上移植触摸。这样既保留一个可回退的纯显示版本,也能随时对照标准库原实现。

从标准库 main.c 复制的业务流程是:清成白屏,调用 TP_Init(),显示 X/Y 标签,在主循环中调用 tp_dev.scan(0),按下时连接前后两个触摸点并刷新坐标。原来的图片、文字和小图标演示不属于触摸验证,视频将其移除。

1.2 复制touch文件

标准库工程中的 HARDWARE/TOUCH 文件夹同时包含 touch.c 和 touch.h。视频把整个文件夹复制到第 13 个工程的 BSP 下,而不是只复制某一个 .c 文件。

这里容易混淆第 12、13 号工程。文件虽然同名,但 Keil 工程项和包含路径都保存实际磁盘路径;误选第 12 号目录会导致修改 A 工程、实际编译 B 工程,甚至让两个工程意外共用一份源码。

1.3 添加touch文件

复制文件并不会自动把它加入 Keil 构建。视频继续完成三项配置:

  1. 在工程的 BSP 分组中添加第 13 号目录下的 touch.c;
  2. 在 Include Paths 中添加第 13 号工程的 BSP/TOUCH;
  3. 在 main.c 的用户头文件区加入 #include "touch.h"。

原笔记“将 touch.h 文件内容复制到 main.c”不准确。视频复制的是一行包含指令,不是把整个头文件正文粘入 main.c。

1.4 编译错误处理

第一次编译暴露的是标准库工程依赖,处理顺序如下:

现象根因本节处理
无法打开 sys.hHAL 工程没有标准库示例的系统头文件在 touch.h 注释 #include "sys.h"
随后出现约 32 个 u8、u16 未定义错误这些短类型原由 sys.h 提供在 touch.h 包含 HAL 工程的 main.h
错误降到约 3 个,其中无法打开 delay.h复制来的 touch.c 仍依赖标准库延时头文件暂时注释 #include "delay.h"
再编译仍有约 6 个错误SPI1_SetSpeed() 等标准库接口尚未迁移从主流程开始逐项迁移

本工程的 main.h 已有 typedef uint8_t u8;、typedef uint16_t u16; 和 typedef uint32_t u32;,所以包含它可以解决短类型定义。注释 delay.h 只解决“头文件不存在”,不会自动迁移源码中的 delay_ms();后者仍是下一课任务。

1.5 从头开始看代码

面对多个报错,视频没有随机逐条修改,而是沿运行链检查:

main() → TP_Init() → tp_dev.scan(0) → TP_Scan(0) → TP_Read_XY2()

这种顺序能把每个旧接口放回它的业务语境:先明确函数为什么被调用,再决定它应该如何适配 HAL。

1.5.1 触摸屏初始化

TP_Init() 在本节不需要改。它没有完成 GPIO 或 SPI 外设初始化;这些硬件初始化已经由 CubeMX 生成的 MX_GPIO_Init()、MX_SPI1_Init() 完成。它在这里的主要作用是装载触摸坐标校准参数。

课程源码把开关拼写成了 Adujust,虽然英文拼写不规范,但它是实际标识符,精校源码不能擅自改名:

  • Adujust == 1:根据 USE_HORIZONTAL 使用预设的 xfac/yfac/xoff/yoff;
  • Adujust == 0:调用 TP_Adjust(),让用户手动校准。

普通扫描成功后使用如下线性关系把 ADC 原始坐标映射为屏幕坐标:

[ x_{screen}=xfac\times x_{raw}+xoff ]

[ y_{screen}=yfac\times y_{raw}+yoff ]

预设值可能因触摸屏批次、贴合和方向差异而不准;课程本节为了先完成移植,选择直接使用预设值,并不意味着所有硬件都永远无需校准。

1.5.2 触摸屏扫描函数

tp_dev.scan(0) 不是 HAL 回调函数。scan 是 _m_tp_dev 结构体中的普通函数指针,tp_dev 初始化时把它绑定到 TP_Scan,所以该调用等价于:

TP_Scan(0);

参数为 0 时,TP_Scan() 在读取原始值后应用校准系数,输出屏幕坐标;参数为 1 时保留原始物理坐标,供校准流程使用。TP_Read_XY2() 连续读取两次,仅当两个轴的两次结果都相差小于 ERR_RANGE(50)时取平均值,以减少抖动和偶发误码。

1.5.2.1 触摸引脚功能解析

与本节有关的模块引脚如下:

信号STM32 引脚作用
SCKPA5LCD 与触摸控制器共用的 SPI1 时钟
MISOPA6从触摸控制器读回 ADC 数据
MOSIPA7向控制器发送命令或显示数据
CS1PA3选择 ST7796 显示控制器
CS2PA4选择 XPT2046 触摸控制器
PENPB0读取 XPT2046 的 PENIRQ/按下状态

共用 SPI 总线不表示两个芯片会同时工作。主机通过 CS1、CS2 决定当前哪一个从机响应;未选中的芯片必须保持片选无效,避免总线冲突。第 207 课只改了 PEN 和分频入口,CS2 与实际 SPI 收发还未迁移。

PB0 在 CubeMX 生成的 gpio.c 中配置为 GPIO_MODE_INPUT + GPIO_PULLUP。没有触摸时,上拉使读取结果为高电平;按下后 XPT2046 的 PENIRQ 拉低,因此 PEN == 0 明确表示“有触摸按下”。原笔记写成“0 表示无按键”与源码注释和硬件逻辑相反。

1.5.2.2 检测单片机输入

标准库使用:

#define PEN PBin(0)

PBin(0) 是旧工程自定义的位带访问宏,HAL 工程没有这个宏。视频在 HAL GPIO 驱动中找到读引脚接口,并在 lcd_init.h 中改为:

#define PEN HAL_GPIO_ReadPin(PEN_GPIO_Port, PEN_Pin)

两个实参都来自 CubeMX 生成的 main.h:PEN_GPIO_Port 为 GPIOB,PEN_Pin 为 GPIO_PIN_0。HAL_GPIO_ReadPin() 返回 GPIO_PIN_RESET 或 GPIO_PIN_SET,数值分别是 0 和 1,所以原有的 if(PEN==0) 判断可以保留。

1.5.2.3 更改触摸屏标准库

PEN 宏适配后,TP_Scan() 的判断和状态机不必重写。按下时它根据参数决定输出原始坐标还是屏幕坐标,并在第一次按下时设置:

tp_dev.sta = TP_PRES_DOWN | TP_CATH_PRES;

其中 bit7 表示当前按下,bit6 记录捕获到一次按下事件。松开时先清 bit7;如果持续无触摸,再把当前坐标设为 0xFFFF。主循环检查的是 tp_dev.sta & TP_PRES_DOWN,而不是直接再次读取 PB0。

继续进入 TP_Read_XY2() 后出现下一处旧接口:触摸读取前调用 SPI1_SetSpeed(..._32),读取完成后再调用 SPI1_SetSpeed(..._4)。原因是 ST7796 和 XPT2046 共用 SPI1,但课程配置让 LCD 使用 4 分频、触摸使用更慢的 32 分频。

1.5.2.4 SPI分频处理

标准库自定义的 SPI1_SetSpeed() 核心只有两次寄存器位操作:

SPI1->CR1 &= 0XFFC7;
SPI1->CR1 |= SPI_BaudRatePrescaler;

STM32F1 的 SPI 控制寄存器 1(SPI_CR1)中,BR[2:0] 位于 bit5~bit3。先用掩码清零这三位,再把新的编码写入,便能只修改波特率分频而保留 CR1 的其他位。

本节在 HAL 工程中改成 hspi1.Instance->CR1。这不是调用新的 HAL 配置函数,而是借 HAL 句柄找到同一个硬件寄存器后直接操作:

  • hspi1 是 SPI_HandleTypeDef 对象;

  • hspi1.Instance 是指向 SPI1 寄存器组的指针;

  • hspi1.Instance->CR1 才是 SPI1 的 CR1 寄存器。

  • 1.5.2.4.1 找到SPI手册

视频查阅 STM32F1 中文参考手册的 SPI 章节和 SPI_CR1 寄存器。BR[2:0] 的对应关系为:

BR[2:0]SPI 时钟
000外设时钟 / 2
001外设时钟 / 4
010外设时钟 / 8
011外设时钟 / 16
100外设时钟 / 32
101外设时钟 / 64
110外设时钟 / 128
111外设时钟 / 256

“外设时钟”对 SPI1 来说是其所在 APB2 域提供的时钟,不应把“32 分频”误解成固定的某个 MHz;实际 SCK 还取决于系统和 APB2 时钟配置。

  • 1.5.2.4.2 cre寄存器解析

原标题和转写中的 “cre” 实际是 CR1。清零掩码的正确二进制为:

0xFFC7 = 1111 1111 1100 0111₂
                       ^^^
                    bit5~bit3

其中 0xC = 1100₂,0x7 = 0111₂。转写一度把 7 说成 1110,这是错误的;从完整掩码也能验证,只有 bit5、bit4、bit3 为 0。

32 分频的字段编码是 100₂,其未移位数值为 4。把它放入 bit5~bit3:

[ 4\ll3=0b100\ll3=0b0010,0000=0x20 ]

所以本节代码 hspi1.Instance->CR1 |= 4 << 3; 写入的是 0x20。原笔记的 0x24 错误:0x24 除了 BR 字段外还会置 bit2(MSTR),不能当作纯 32 分频值。

二、结束

本节的实质是完成标准库触摸驱动迁移的“入口”:工程文件进入 HAL 项目,类型、PEN 读取和 32 分频寄存器写法已经处理,但触摸通信链尚未闭合。视频结束时没有重新编译,也没有触摸效果验证。

2.1 二进制与16进制换算

二进制转十六进制应从右向左每四位一组,使用 8、4、2、1 权重。例如:

0010₂ = 2₁₆
0111₂ = 7₁₆
1100₂ = C₁₆
0010 0000₂ = 0x20

位域计算不能只看 100₂ = 4,还必须考虑它在寄存器中的位置。BR 起始于 bit3,因此还要左移 3 位。

2.2 SPI波特率设置与代码实现

修改 BR 字段分两步完成:

hspi1.Instance->CR1 &= 0xFFC7;  /* 清除旧 BR[2:0]。 */
hspi1.Instance->CR1 |= 4 << 3;  /* 写入 100,即 32 分频。 */

必须先清后写。如果只使用按位或,旧 BR 位中的 1 无法被清除,新旧编码叠加后可能得到完全不同的分频值。

2.3 SPI速度设置函数与分频设置

标准库的分频宏已经是移位后的寄存器值,例如 32 分频为 0x0020;本节改写时使用的是未移位字段值 4,再显式执行 << 3。两种写法结果相同,但不能把“字段编码 4”和“最终寄存器掩码 0x20”混为一谈。

上一课 CubeMX 初始化 SPI1 时选择了 4 分频。进入 TP_Read_XY2() 后暂时改成 32 分频,是为了按课程方案与 XPT2046 通信;结束后还应恢复 4 分频,避免后续 LCD 刷新继续使用慢速时钟,但恢复代码属于第 208 课。

2.4 SPI移植与代码修改

视频没有重新编写整个 SPI 驱动,而是回到标准库 spi.c,找到 SPI1_SetSpeed() 真正执行的两行,再把寄存器访问路径适配 HAL 句柄。迁移关系是:

SPI1->CR1
    ↓
hspi1.Instance->CR1

改变的是“如何找到寄存器”,不是 CR1 和 BR 位本身的硬件含义。

2.5 SPI标准库与HELP库的修改

原笔记中的 “HELP 库”“hive 库”都是语音识别错误,正确名称是 HAL 库。SPI1_SetSpeed()、SPI_BaudRatePrescaler_32 属于旧标准库示例及其封装,不能直接假设在 HAL 工程中仍存在同名接口;本课因此按它们的寄存器效果迁移。

需要区分“使用 HAL 工程”与“只调用 HAL API”。本课的 hspi1.Instance->CR1 仍是直接操作寄存器,只是通过 HAL 句柄取得外设地址。它不会自动更新 hspi1.Init.BaudRatePrescaler 中保存的配置副本。

2.6 SPI回调函数与结构体解析

这里不存在 SPI 回调函数。视频查看的是 HAL 的 SPI 句柄结构体:hspi1 的类型为 SPI_HandleTypeDef,其中 Instance 指向 SPI_TypeDef;后者才包含 CR1、CR2、SR、DR 等硬件寄存器成员。

同样,tp_dev.scan 也是函数指针,不是 HAL 回调。准确理解结构体和指针层级,才能解释为什么正确写法是 hspi1.Instance->CR1,而不是不存在的 hspi1.CR1。

2.7 SPI大结构体与寄存器操作

访问链可以读成:

hspi1(HAL 句柄对象)
  └─ Instance(指向 SPI1 寄存器组)
       └─ CR1(SPI 控制寄存器 1)

. 用于访问结构体对象成员,-> 用于经结构体指针访问成员,因此代码同时出现 . 和 ->。IDE 红线消失只表示名称和类型在语法/语义分析上可解析,不代表硬件时序已经正确;最终仍需编译和上板验证。

2.8 SPI波特率计算与二进制转换

本节的 32 分频计算可完整写成:

BR[2:0] = 100₂
字段值   = 4
位偏移   = 3
写入值   = 4 << 3 = 32 = 0x20

还有两个必须留意的工程边界:

  1. 本节结尾仍保留恢复 4 分频的旧 SPI1_SetSpeed() 调用,所以源码尚不能完成纯 HAL 构建;第 208 课开头才迁移它。
  2. 课程代码直接修改 CR1,没有在本节展示“等待 SPI 空闲、禁用 SPI、修改 BR、重新启用”的完整保护流程。实际项目中不应在传输进行时改变波特率,并且 TP_Read_XY2() 的两个提前返回路径也可能在恢复分频前退出。笔记保留课程源码,不把这些改进擅自写回代码,但调试时必须知道这些边界。