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

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

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

/*
 * 本节目标:把中景园 LCD 标准库例程的“显示部分”迁入新建的 HAL 工程。
 * 收录边界:只列出本节实际添加或改写的代码片段;这些片段来自不同文件,不能单独编译。
 * 当前状态:本节结束时移植尚未完成,SPI 发送、驱动内延时和 LCD_Fill 等仍在后续课程处理。
 */

/* main.h:为旧驱动沿用的短类型名建立 HAL/C99 固定宽度类型别名。 */
typedef uint8_t u8;                         /* 让旧代码中的 u8 继续表示 8 位无符号整数。 */
typedef uint16_t u16;                       /* 让旧代码中的 u16 继续表示 16 位无符号整数。 */
typedef uint32_t u32;                       /* 让旧代码中的 u32 继续表示 32 位无符号整数。 */

/* main.h:CubeMX 根据 User Label 生成的控制引脚名称。 */
#define RES_Pin GPIO_PIN_0                  /* LCD 硬件复位信号使用 PA0。 */
#define RES_GPIO_Port GPIOA                 /* RES 所在端口是 GPIOA。 */
#define DC_Pin GPIO_PIN_1                   /* LCD 命令/数据选择信号使用 PA1。 */
#define DC_GPIO_Port GPIOA                  /* DC 所在端口是 GPIOA。 */
#define BLK_Pin GPIO_PIN_2                  /* LCD 背光使能信号使用 PA2。 */
#define BLK_GPIO_Port GPIOA                 /* BLK 所在端口是 GPIOA。 */
#define CS1_Pin GPIO_PIN_3                  /* 显示控制器片选信号使用 PA3。 */
#define CS1_GPIO_Port GPIOA                 /* CS1 所在端口是 GPIOA。 */
#define CS2_Pin GPIO_PIN_4                  /* 触摸控制器片选信号使用 PA4。 */
#define CS2_GPIO_Port GPIOA                 /* CS2 所在端口是 GPIOA。 */
#define PEN_Pin GPIO_PIN_0                  /* 触摸按下状态信号使用 PB0。 */
#define PEN_GPIO_Port GPIOB                 /* PEN 所在端口是 GPIOB。 */

/* lcd.h:用 HAL 工程公共头文件替代标准库工程的 sys.h。 */
#include "main.h"                          /* 取得 HAL 声明、u8/u16/u32 别名和 GPIO 标签宏。 */

/* lcd_init.h:同样改为包含新工程的 main.h。 */
#include "main.h"                          /* 使底层 LCD 控制宏能使用 HAL_GPIO_WritePin()。 */

#define LCD_RES_Clr() HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, 0)  /* 把 PA0 写为低电平,使 LCD 进入复位。 */
#define LCD_RES_Set() HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, 1)  /* 把 PA0 写为高电平,释放 LCD 复位。 */

#define LCD_DC_Clr() HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, 0)     /* 把 PA1 写低,随后传输的字节按 LCD 命令解释。 */
#define LCD_DC_Set() HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, 1)     /* 把 PA1 写高,随后传输的字节按 LCD 数据解释。 */

#define LCD_BLK_Clr() HAL_GPIO_WritePin(BLK_GPIO_Port, BLK_Pin, 0)  /* 把 PA2 写低,关闭模块背光。 */
#define LCD_BLK_Set() HAL_GPIO_WritePin(BLK_GPIO_Port, BLK_Pin, 1)  /* 把 PA2 写高,打开模块背光。 */

#define LCD_CS_Clr() HAL_GPIO_WritePin(CS1_GPIO_Port, CS1_Pin, 0)   /* 把 PA3 写低,选中 ST7796 显示控制器。 */
#define LCD_CS_Set() HAL_GPIO_WritePin(CS1_GPIO_Port, CS1_Pin, 1)   /* 把 PA3 写高,释放 ST7796 显示控制器。 */

/* main.c:本节加入 CubeMX 用户代码区的头文件。 */
#include "lcd_init.h"                      /* 引入 LCD 初始化、屏幕尺寸和控制接口。 */
#include "lcd.h"                           /* 引入填充、文字、数字和图片显示接口。 */
#include "pic.h"                           /* 引入 gImage_1、gImage_2 两个取模图片数组。 */

/* main.c / USER CODE BEGIN 1:给显示演示准备局部变量。 */
u8 i, j;                                   /* 作为 8×7 小图片平铺循环的列、行计数器。 */
float t = 0;                               /* 保存演示用浮点数,初值为 0。 */

/* main.c:CubeMX 生成并在 LCD_Init() 之前执行的初始化调用。 */
HAL_Init();                                /* 初始化 HAL、Flash 接口和 SysTick,为 HAL_Delay() 提供时基。 */
SystemClock_Config();                      /* 把系统时钟配置为本工程设定的 72 MHz。 */
MX_GPIO_Init();                            /* 初始化 PA0~PA4 控制脚和 PB0 触摸状态输入。 */
MX_SPI1_Init();                            /* 初始化 PA5/PA6/PA7 对应的 SPI1。 */
MX_USART1_UART_Init();                     /* 初始化备用调试串口;本节显示代码尚未使用它。 */

/* main.c / USER CODE BEGIN 2:从标准库例程复制来的显示业务,延时改用 HAL_Delay()。 */
LCD_Init();                                                       /* 执行 LCD 复位、背光开启和控制器寄存器初始化。 */
LCD_Fill(0, 0, LCD_W, LCD_H, RED);                               /* 尝试先把整个竖屏填成红色。 */

LCD_ShowPicture(0, 0, 320, 480, gImage_2);                       /* 尝试显示 320×480 的整屏图片。 */
HAL_Delay(1000);                                                  /* 使用 HAL 的毫秒延时保持图片 1 秒。 */
LCD_Fill(0, 0, LCD_W, LCD_H, WHITE);                             /* 清成白色,准备显示文字和小图片。 */
LCD_ShowChinese(0, 0, "中景园电子", RED, WHITE, 32, 0);          /* 显示 32 像素高的红色中文。 */
LCD_ShowString(0, 40, "LCD_W:", RED, WHITE, 16, 0);             /* 显示屏幕宽度标签。 */
LCD_ShowIntNum(48, 40, LCD_W, 3, RED, WHITE, 16);                /* 显示当前逻辑宽度 320。 */
LCD_ShowString(80, 40, "LCD_H:", RED, WHITE, 16, 0);            /* 显示屏幕高度标签。 */
LCD_ShowIntNum(128, 40, LCD_H, 3, RED, WHITE, 16);               /* 显示当前逻辑高度 480。 */
LCD_ShowString(80, 40, "LCD_H:", RED, WHITE, 16, 0);            /* 原源码重复写入同一标签,此处按课堂代码保留。 */
LCD_ShowString(0, 70, "Increaseing Nun:", RED, WHITE, 16, 0);   /* 显示原源码中的浮点数标签,拼写保持不变。 */
LCD_ShowFloatNum1(128, 70, t, 4, RED, WHITE, 16);                /* 显示当前值为 0 的浮点数。 */
t += 0.11;                                                       /* 将变量增加 0.11,但本段不会再次刷新该数值。 */
for (j = 0; j < 7; j++)                                         /* 纵向绘制 7 行小图片。 */
{                                                                /* 外层行循环开始。 */
    for (i = 0; i < 8; i++)                                     /* 横向绘制 8 列小图片。 */
    {                                                            /* 内层列循环开始。 */
        LCD_ShowPicture(40 * i, 200 + j * 40, 40, 40, gImage_1); /* 在计算出的 40×40 网格位置显示小企鹅。 */
    }                                                            /* 一列绘制完成。 */
}                                                                /* 一行绘制完成,共尝试显示 56 幅小图片。 */

本课是三节连续迁移课的第 1 部分。这里完成的是“搭好 HAL 工程骨架并接通 GPIO 名称”,不是已经得到可运行的 LCD HAL 驱动。课程在本节末尾仍有标准库 SPI、DMA 和 delay_ms()相关编译错误;第 204、205 课才继续替换发送与填充逻辑。

一、代码的移植从标准库移植成HAL库

“HAL 库”是 STM32 Hardware Abstraction Layer 的简称。转录中的“Helm 库”“Hail 库”“hello 库”均指 HAL 库;“处理化”应为“初始化”。迁移并不是把全部旧文件复制后改到不报红,而是把旧驱动依赖的时钟、GPIO、SPI、DMA和延时逐层替换为新工程中等价的 HAL 能力。

本节同时打开两个工程进行对照:

11中景园硬件SPI带触摸标准库例程   ← 已经能运行的参考工程
12LCD_Display                     ← 本节新建的 HAL 显示工程

1.1 建立新工程

在 STM32CubeMX 中按以下步骤建立工程:

  1. 选择 ACCESS TO MCU SELECTOR,器件选择 STM32F103ZET6。它是 144 引脚、512 KiB Flash 的 STM32F103 高容量型号。
  2. RCC选择外部高速晶振 HSE;SYS选择 Serial Wire,保留 PA13/PA14 的 SWD 调试功能。
  3. 在 Clock Configuration 中把系统时钟配置为 72 MHz。本工程实际由 8 MHz HSE 经 PLL×9 得到 72 MHz;AHB 和 APB2 为 72 MHz,APB1 为 36 MHz。
  4. 工程名设为 12LCD_Display,路径选择专题的 uint3code目录。
  5. Toolchain/IDE 选择 MDK-ARM V5.32。
  6. 在 Code Generator 中启用“每个外设生成独立的 .c/.h文件”。这样会得到 gpio.c/.h、spi.c/.h、usart.c/.h等文件。

SPI1 选择 Full-Duplex Master。CubeMX 将三个固定引脚配置为:

SPI1 信号STM32 引脚屏幕模块信号
SPI1_SCKPA5CLK
SPI1_MISOPA6MISO,主要由 XPT2046 返回数据
SPI1_MOSIPA7MOSI,向显示/触摸芯片发送数据

分频选择 4 分频。SPI1 位于 APB2,总线时钟为 72 MHz,因此 CubeMX 计算的串行时钟为:

72 MHz ÷ 4 = 18 Mbit/s

视频中四分频一度出现红叉,切换到其他分频再切回后消失。这是当时 CubeMX 界面的状态刷新现象;真正生成代码后应检查 .ioc和 spi.c,不能只凭红叉是否消失判断配置正确。

本工程生成的 SPI 参数还包括:8 位数据、MSB 先发、软件 NSS、CPOL 低、第一边沿采样,即 SPI Mode 0。上一课的标准库例程使用 CPOL 高、第二边沿采样,即 Mode 3。老师本节只调整了分频,没有逐项复制旧配置;迁移其他 SPI 器件时必须查器件时序并核对模式,不能假定“默认值一定等价”。

USART1 以异步模式一并启用,只是为了以后需要时输出调试信息;本节没有串口业务代码。启用一个外设会占用相应引脚和时钟资源,若后续确定不用,可以再关闭。

1.2 复制标准库代码到新工程

从工程 11 的 HARDWARE/LCD复制整个 LCD 文件夹到新工程:

12LCD_Display/
└─ BSP/
   └─ LCD/
      ├─ lcd.c
      ├─ lcd.h
      ├─ lcd_init.c
      ├─ lcd_init.h
      ├─ lcdfont.h
      └─ pic.h

BSP是 Board Support Package(板级支持包),适合集中存放与具体板卡、模块和引脚相关的驱动。触摸代码暂不复制:先让显示功能迁移成功,再处理 XPT2046 触摸,能减小一次排错的范围。

在 Keil 中的操作是:

  1. 打开 12LCD_Display/MDK-ARM下的工程文件;
  2. 在 Manage Project Items 中新建 BSP代码组;
  3. 向该组添加 lcd.c和 lcd_init.c两个 C 文件;
  4. 在 Options for Target → C/C++ → Include Paths中加入 BSP/LCD。

头文件不需要作为独立编译单元,但编译器必须能通过包含路径找到它们。lcdfont.h和 pic.h体积较大,却仍只是被 C 文件包含的数据定义文件。

main.c中的代码应放在 CubeMX 预留的用户区:

内容放置位置
lcd_init.h、lcd.h、pic.hUSER CODE BEGIN Includes
u8 i,j、float tUSER CODE BEGIN 1
LCD_Init()及显示演示USER CODE BEGIN 2,位于所有 MX_*_Init()之后、while(1)之前

这样重新生成 CubeMX 代码时,Keep User Code机制才会保留手写内容。把用户代码写在标记区外,重新 Generate Code 时可能被覆盖。

本节把 main.c中的两个 delay_ms(1000)改为 HAL_Delay(1000)。HAL_Init()已经配置 SysTick 时基,所以参数仍以毫秒为单位。这里仅改了主函数中的两处;lcd_init.c内部残留的 delay_ms()到下一课才继续替换。

1.3 修改新工程代码

第一次编译的作用是建立错误清单,不代表应逐个机械消除红线。本节按以下依赖关系处理:

sys.h 找不到
  → lcd.h、lcd_init.h 改为包含 main.h
u8/u16/u32 未定义
  → main.h 用户类型区建立 typedef 别名
delay.h、dma.h 找不到
  → 暂时移除旧头文件依赖,但其函数调用仍需后续迁移
标准库 RCC/GPIO/SPI/DMA 符号不存在
  → 从 main()调用链开始,逐层换成 CubeMX/HAL 实现

类型别名的对应关系是:

旧工程类型HAL/C99 固定宽度类型位宽
u8uint8_t8 位
u16uint16_t16 位
u32uint32_t32 位

把别名放在 main.h的 USER CODE BEGIN ET区域,并让 lcd.h、lcd_init.h包含 main.h,旧驱动中的所有声明便能看到这些类型。另一种做法是把全部旧类型名改成标准类型,但本课为了减少机械修改而保留别名。

需要注意:注释掉 delay.h或 dma.h只解决“头文件不存在”,不会自动替换 delay_ms()、DMA 函数或 SPI 标准库调用。本节结束仍未通过完整移植验证,不能把“错误暂时减少”当成驱动已经正确。

随后从 main()追入 LCD_Init():

  • 旧的 SPI1_Init()不再调用,因为 main()已在前面执行 MX_SPI1_Init();
  • 旧的 LCD_GPIO_Init()不再调用,因为控制引脚改由 MX_GPIO_Init()生成;
  • lcd_init.c中整段标准库版 LCD_GPIO_Init()被停用,但必须先在 CubeMX 中完成等价引脚配置,不能只删除旧代码。

二、结束

这一标题在原笔记中实际对应 PA2、PA3 的继续配置,并不是整节课程已经结束:

  • PA2 标记为 BLK,推挽输出,初始高电平;高电平打开背光。
  • PA3 标记为 CS1,推挽输出,初始高电平;它是 ST7796 的低有效片选,初始化为高表示上电时先不选中显示控制器。

片选线不能仅理解成“开关”。当 CS1=0时,ST7796 才接收 SCK/MOSI 上的传输;CS1=1时,应忽略属于其他 SPI 设备的时钟和数据。

三、CS2引脚配置

PA4 配置为推挽输出,User Label 设为 CS2,初始高电平。CS2是 XPT2046 触摸控制器的低有效片选,与 PA3/CS1 分工如下:

CS1=0、CS2=1:SPI1 与 ST7796 显示控制器通信
CS1=1、CS2=0:SPI1 与 XPT2046 触摸控制器通信
CS1=1、CS2=1:两个器件均未选中

本课虽暂不迁移触摸业务,仍先配置 CS2,是因为旧 LCD_GPIO_Init()原本统一初始化了 PA0~PA4;接管这段函数时不能漏掉其中任何硬件控制脚。

四、代码配置B0B0引脚

原标题和转录中的“B0B0”都应理解为 PB0。PB0 配置为 GPIO_Input、内部上拉,User Label 设为 PEN。

PEN连接 XPT2046 的触摸状态输出,低电平表示检测到按压。上拉使没有按压时输入稳定为高,避免引脚悬空造成随机触发;这项上拉配置对输入脚是有实际电气作用的,不能省略。

本课只是配置引脚,还没有把 PEN宏和 HAL_GPIO_ReadPin()迁移完成,也没有配置 EXTI。后续触摸驱动仍通过轮询读取该脚,不应把它误写成已经启用 MCU 外部中断。

五、配置GPIOB引脚

PB0 配置完成后再次点击 Generate Code,并在弹窗中确认打开/更新 Keil 工程。生成的 MX_GPIO_Init()会:

  1. 开启 GPIOA、GPIOB 外设时钟;
  2. 先把 PA0~PA4 的输出锁存值设为高;
  3. 将 PA0~PA4 配成推挽输出;
  4. 将 PB0 配成上拉输入。

先写输出值、再切换引脚模式,可以减少初始化瞬间出现意外低脉冲的机会。对于低有效的 RES、CS1、CS2,高电平也是比较安全的默认状态。

CubeMX 的用户标签还会在 main.h中生成成对宏,例如 PEN_Pin和 PEN_GPIO_Port。前者是端口内的位掩码,后者是 GPIO 外设实例;调用 HAL GPIO 接口时二者缺一不可。

六、配置GPIOA引脚

PA0~PA4 的最终映射为:

引脚标签模式初始值作用
PA0RES推挽输出高LCD 低有效硬件复位
PA1DC推挽输出高低为命令,高为数据
PA2BLK推挽输出高高电平打开背光
PA3CS1推挽输出高ST7796 低有效片选
PA4CS2推挽输出高XPT2046 低有效片选

视频中 PA3 的标签一度写错,随后回到 CubeMX 修正为 CS1并重新生成代码。标签拼错会直接改变生成的宏名,使 CS1_Pin或 CS1_GPIO_Port不存在;这不是仅影响注释的文字错误。

课堂把这五个输出都选择为 Pull-up。对 STM32F1 当前 HAL 实现而言,推挽输出分支使用模式和速度生成 CRL 配置,Pull只在输入分支决定上拉/下拉,因此这些输出脚的 Pull-up 选项实际上不起作用;真正确定上电软件状态的是生成的 HAL_GPIO_WritePin(..., GPIO_PIN_SET)。PB0 作为输入时,上拉才真正生效。

当前生成代码把这些控制输出设置为 GPIO_SPEED_FREQ_LOW,而旧标准库写的是 50 MHz。GPIO Speed 表示输出驱动边沿能力,不是 CPU 或 SPI 频率。RES、DC、BLK等低速控制一般不要求 50 MHz;若以后高频逐字节翻转 CS,应结合波形和器件时序选择合适速度。

七、配置GPIOA和GPIOB引脚

标准库控制引脚使用:

GPIO_ResetBits(GPIOA, GPIO_Pin_0); /* 标准库:把 PA0 拉低。 */
GPIO_SetBits(GPIOA, GPIO_Pin_0);   /* 标准库:把 PA0 拉高。 */

HAL 工程则使用:

HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, 0); /* HAL:把 RES 对应引脚写成复位态,即低电平。 */
HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, 1); /* HAL:把 RES 对应引脚写成置位态,即高电平。 */

HAL_GPIO_WritePin()的三个参数依次是端口、引脚掩码和目标状态。源码使用字面量 0/1,它们在当前 HAL 的 GPIO_PinState枚举中分别对应 GPIO_PIN_RESET/GPIO_PIN_SET;工程代码原样保留,阅读新代码时则优先写枚举名会更直观。

以复位宏为例,调用关系是:

LCD_RES_Clr()
  → HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET)
  → PA0 输出低电平
  → ST7796 保持复位

LCD_RES_Set()
  → HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET)
  → PA0 输出高电平
  → ST7796 退出硬件复位

不要把 CubeMX 生成的 RES_Pin、RES_GPIO_Port等定义再复制一份到 lcd_init.h。正确做法是让 lcd_init.h包含 main.h并直接引用这些宏;重复定义会增加两处配置不一致的风险。

八、配置端口

按照相同模式,本节把 RES、DC、BLK、CS1 共八个高/低电平宏全部改成 HAL_GPIO_WritePin()。其映射如下:

驱动宏端口/引脚写入值结果
LCD_RES_Clr()RES_GPIO_Port, RES_Pin0进入复位
LCD_RES_Set()RES_GPIO_Port, RES_Pin1释放复位
LCD_DC_Clr()DC_GPIO_Port, DC_Pin0选择命令
LCD_DC_Set()DC_GPIO_Port, DC_Pin1选择数据
LCD_BLK_Clr()BLK_GPIO_Port, BLK_Pin0关闭背光
LCD_BLK_Set()BLK_GPIO_Port, BLK_Pin1打开背光
LCD_CS_Clr()CS1_GPIO_Port, CS1_Pin0选中显示芯片
LCD_CS_Set()CS1_GPIO_Port, CS1_Pin1释放显示芯片

宏定义末尾不写分号,是因为调用处本身写成 LCD_RES_Clr();。如果宏体也带分号,简单场景通常仍能编译,但在 if/else等语境中容易出现多余空语句,因而不应依赖这种写法。

本节结束时已经完成:

  • 新建 HAL 工程并生成 72 MHz、SPI1、USART1 和 GPIO 框架;
  • 把 LCD 显示文件放入 BSP/LCD并加入 Keil;
  • 让旧驱动类型名通过 typedef兼容新工程;
  • 让 CubeMX 接管 SPI1 和控制 GPIO 的初始化;
  • 把 RES/DC/BLK/CS1 的电平控制宏换成 HAL GPIO 接口。

仍未完成的工作包括:

  • 把 lcd_init.c中的 delay_ms()换为 HAL_Delay();
  • 把 SPI1_ReadWriteByte()换成 HAL SPI 发送接口;
  • 把 LCD_Fill()和 LCD_ShowPicture()中的标准库 SPI/DMA路径迁移掉;
  • 通过编译、烧录和屏幕现象验证显示结果;
  • 迁移 CS2、PEN 的触摸读写宏及完整触摸业务。

因此本课正确的验收点是“HAL 工程骨架和 GPIO 适配已经搭好”,而不是“LCD 已经显示成功”。