LCD触摸驱动标准库移植HAL库(1)
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 构建。视频继续完成三项配置:
- 在工程的
BSP分组中添加第 13 号目录下的touch.c; - 在 Include Paths 中添加第 13 号工程的
BSP/TOUCH; - 在
main.c的用户头文件区加入#include "touch.h"。
原笔记“将 touch.h 文件内容复制到 main.c”不准确。视频复制的是一行包含指令,不是把整个头文件正文粘入 main.c。
1.4 编译错误处理
第一次编译暴露的是标准库工程依赖,处理顺序如下:
| 现象 | 根因 | 本节处理 |
|---|---|---|
无法打开 sys.h | HAL 工程没有标准库示例的系统头文件 | 在 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 引脚 | 作用 |
|---|---|---|
| SCK | PA5 | LCD 与触摸控制器共用的 SPI1 时钟 |
| MISO | PA6 | 从触摸控制器读回 ADC 数据 |
| MOSI | PA7 | 向控制器发送命令或显示数据 |
| CS1 | PA3 | 选择 ST7796 显示控制器 |
| CS2 | PA4 | 选择 XPT2046 触摸控制器 |
| PEN | PB0 | 读取 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
还有两个必须留意的工程边界:
- 本节结尾仍保留恢复 4 分频的旧
SPI1_SetSpeed()调用,所以源码尚不能完成纯 HAL 构建;第 208 课开头才迁移它。 - 课程代码直接修改 CR1,没有在本节展示“等待 SPI 空闲、禁用 SPI、修改 BR、重新启用”的完整保护流程。实际项目中不应在传输进行时改变波特率,并且
TP_Read_XY2()的两个提前返回路径也可能在恢复分频前退出。笔记保留课程源码,不把这些改进擅自写回代码,但调试时必须知道这些边界。