STM32 零基础可移植教程 21:1602A 并口 4 位模式,先显示 Hello
前面我们一直在用串口看结果。
串口很好用,但它有一个前提:
bash
你旁边得有电脑和串口助手
很多小项目最后还是希望板子自己能显示一点状态,比如:
bash
电压:1.65V
温度:28.5C
电机:RUN
计数:1234
所以这一篇开始加一个很经典的小显示器:
bash
1602A 字符屏
你手里的是裸 16 针并口屏,不是背面带 I2C 小板的那种。
这一篇就按裸 16 针来做,先不用 I2C。
这一篇只做一个明确目标:
bash
用 STM32 GPIO 以 4 位并口模式驱动 1602A,显示 Hello STM32
先不显示中文,不做滚动,不做自定义字符,也不读忙标志。
先把最小显示链路跑通。
本篇目标
最终现象:
bash
1602A 第一行显示:Hello STM32
1602A 第二行显示:LCD1602 OK
本篇用到的外设:
bash
GPIO Output
本篇跑通标准:
-
Keil 编译通过;
-
程序能下载到开发板;
-
1602A 背光能亮;
-
调节对比度后能看到字符;
-
第一行能显示
Hello STM32; -
第二行能显示
LCD1602 OK; -
能说清楚
RS、E、D4~D7分别干什么; -
能说清楚换引脚时 CubeMX 和代码哪里要改。
准备工作
你需要准备:
|
项目
|
说明
|
| --- | --- |
|
STM32 开发板
|
任意 STM32 开发板
|
|
1602A 裸 16 针字符屏
|
本篇按并口屏写,不按 I2C 小板写
|
|
电位器
|
常见 10k,用来调 VO 对比度
|
|
杜邦线
|
连接 LCD 和 STM32
|
|
下载器
|
ST-LINK/V2 或板载 ST-LINK
|
|
CubeMX 工程
|
可以从任意 GPIO 工程继续
|
1602A 是 16 列 2 行字符屏。
也就是:
bash
一行最多显示 16 个字符
一共 2 行
它不是 OLED,也不是 TFT,也不是点阵图形屏。
所以第一版我们就显示英文、数字、符号。
中文显示不在这一篇处理。
1602A 16 个引脚先看懂
裸 16 针 1602A 一般是下面这些引脚。
不同厂家丝印可能略有差异,但大体一致:
|
引脚
|
名称
|
作用
|
| --- | --- | --- |
|
1
|
VSS
|
GND
|
|
2
|
VDD
|
电源,常见 5V
|
|
3
|
VO
|
对比度调节
|
|
4
|
RS
|
选择命令或数据
|
|
5
|
RW
|
读写选择
|
|
6
|
E
|
使能信号
|
|
7
|
D0
|
数据位 0
|
|
8
|
D1
|
数据位 1
|
|
9
|
D2
|
数据位 2
|
|
10
|
D3
|
数据位 3
|
|
11
|
D4
|
数据位 4
|
|
12
|
D5
|
数据位 5
|
|
13
|
D6
|
数据位 6
|
|
14
|
D7
|
数据位 7
|
|
15
|
A / LED+
|
背光正极
|
|
16
|
K / LED-
|
背光负极
|
这一篇用 4 位模式,所以只接:
bash
D4, D5, D6, D7
不接:
bash
D0, D1, D2, D3
为什么用 4 位模式?
因为它省 GPIO。
8 位模式要 8 根数据线。
4 位模式只要 4 根数据线,再加 RS 和 E,一共 6 个 STM32 GPIO 就能驱动。

RS、RW、E 到底干什么
先把三个控制脚讲清楚。
RS 用来区分你发的是命令还是字符数据:
|
RS
|
含义
|
| --- | --- |
|
0
|
发送命令,比如清屏、设置光标
|
|
1
|
发送数据,比如字符 H、e、1
|
RW 用来区分读还是写:
|
RW
|
含义
|
| --- | --- |
|
0
|
写 LCD
|
|
1
|
读 LCD
|
本篇为了简单,建议:
bash
RW 直接接 GND
也就是只写不读。
这样 STM32 不需要读取 LCD 的忙标志,也避免 LCD 的 5V 数据线反向输出到 STM32。
E 是使能脚。
你可以先这样理解:
bash
数据线先摆好
给 E 一个脉冲
LCD 在 E 的变化过程中把数据收进去
所以写 LCD 的动作不是"把 GPIO 拉高就完事"。
它是一个小流程:
bash
设置 RS
设置 D4~D7
拉高 E
短暂等待
拉低 E
LCD 接收这 4 位数据

硬件连接
本篇示例连接如下。
具体 STM32 引脚你可以自己换,只要 CubeMX 里 User Label 保持一致即可。
|
1602A 引脚
|
连接
|
说明
|
| --- | --- | --- |
|
VSS
|
GND
|
地
|
|
VDD
|
5V
|
LCD 供电,按你的模块要求
|
|
VO
|
电位器中间脚
|
调对比度
|
|
RS
|
STM32 GPIO,User Label=LCD_RS
|
命令/数据选择
|
|
RW
|
GND
|
只写不读
|
|
E
|
STM32 GPIO,User Label=LCD_E
|
使能脉冲
|
|
D0~D3
|
不接
|
4 位模式不用
|
|
D4
|
STM32 GPIO,User Label=LCD_D4
|
数据线
|
|
D5
|
STM32 GPIO,User Label=LCD_D5
|
数据线
|
|
D6
|
STM32 GPIO,User Label=LCD_D6
|
数据线
|
|
D7
|
STM32 GPIO,User Label=LCD_D7
|
数据线
|
|
A
|
背光正极
|
按模块说明接 5V 或串电阻
|
|
K
|
GND
|
背光负极
|
对比度电位器一般这样接:
bash
电位器一端接 5V
电位器另一端接 GND
电位器中间脚接 VO
如果你没有看到字符,但背光亮,不要急着怀疑代码。
先慢慢转对比度电位器。
1602A 的 VO 不对时,很容易出现:
bash
只亮背光
完全没字
或者一排黑块

关于 5V LCD 和 3.3V STM32
很多 1602A 模块常用 5V 供电。
STM32 GPIO 输出是 3.3V。
不少 1602A 模块能识别 STM32 的 3.3V 高电平,但这件事最好不要想当然。
本篇为了降低风险,做两个处理:
-
RW接 GND,只写不读; -
先用短线连接,确认模块能识别 3.3V 控制信号。
为什么 RW 接 GND 很重要?
因为如果你把 RW 接到 STM32 并读 LCD,LCD 的数据线可能会向 STM32 输出 5V 电平。
对新手来说,第一版没有必要增加这个风险。
CubeMX 配置步骤
1. 配置 6 个 GPIO 输出
在 CubeMX 里选择 6 个普通 GPIO。
例如你可以选:
bash
LCD_RS
LCD_E
LCD_D4
LCD_D5
LCD_D6
LCD_D7
每个引脚都配置为:
bash
GPIO_Output
然后进入:
bash
System Core -> GPIO
把 User Label 分别改成:
bash
LCD_RS
LCD_E
LCD_D4
LCD_D5
LCD_D6
LCD_D7
这一步很关键。
因为后面的 app_lcd1602.c 会直接使用 CubeMX 生成的这些宏:
bash
LCD_RS_GPIO_Port
LCD_RS_Pin
LCD_E_GPIO_Port
LCD_E_Pin
LCD_D4_GPIO_Port
LCD_D4_Pin
LCD_D5_GPIO_Port
LCD_D5_Pin
LCD_D6_GPIO_Port
LCD_D6_Pin
LCD_D7_GPIO_Port
LCD_D7_Pin
如果 User Label 不一致,就会编译报错。

2. GPIO 参数建议
6 个 LCD 引脚建议这样配:
|
配置项
|
推荐值
|
说明
|
| --- | --- | --- |
|
GPIO output level
|
Low
|
上电先保持低电平
|
|
GPIO mode
|
Output Push Pull
|
普通推挽输出
|
|
GPIO Pull-up/Pull-down
|
No pull-up and no pull-down
|
输出模式一般不用上下拉
|
|
Maximum output speed
|
Low
|
LCD 很慢,不需要高速
|
|
User Label
|
LCD_RS 等
|
生成可移植宏
|
LCD1602 速度很慢。
这里不需要高频翻转 GPIO。

3. 生成 Keil 工程
配置完成后点击:
bash
GENERATE CODE
打开 Keil 后先编译一次,确认 CubeMX 工程本身没问题。

Keil 工程生成和编译
打开 Keil 后,先编译:
bash
Build / F7
确认输出里没有错误:
bash
0 Error(s)

完整代码
新建两个文件:
bash
Core/Inc/app_lcd1602.h
Core/Src/app_lcd1602.c
main.c 只负责调用。
1. Core/Inc/app_lcd1602.h
bash
#ifndef APP_LCD1602_H
#define APP_LCD1602_H
#include "main.h"
#include <stdint.h>
void App_LCD1602_Init(void)
;
void App_LCD1602_Clear(void)
;
void App_LCD1602_SetCursor(uint8_t row, uint8_t col)
;
void App_LCD1602_Print(const char *str)
;
void App_LCD1602_PrintLine(uint8_t row, const char *str)
;
#endif
2. Core/Src/app_lcd1602.c
bash
#include "app_lcd1602.h"
/* * LCD1602 4-bit parallel mode. * * CubeMX User Labels required: * LCD_RS, LCD_E, LCD_D4, LCD_D5, LCD_D6, LCD_D7 * * RW is recommended to connect to GND in this beginner version. */
#ifndef LCD_RS_GPIO_Port
#error "LCD_RS_GPIO_Port is not defined. Set LCD RS pin User Label to LCD_RS in CubeMX."
#endif
#ifndef LCD_RS_Pin
#error "LCD_RS_Pin is not defined. Set LCD RS pin User Label to LCD_RS in CubeMX."
#endif
#ifndef LCD_E_GPIO_Port
#error "LCD_E_GPIO_Port is not defined. Set LCD E pin User Label to LCD_E in CubeMX."
#endif
#ifndef LCD_E_Pin
#error "LCD_E_Pin is not defined. Set LCD E pin User Label to LCD_E in CubeMX."
#endif
#ifndef LCD_D4_GPIO_Port
#error "LCD_D4_GPIO_Port is not defined. Set LCD D4 pin User Label to LCD_D4 in CubeMX."
#endif
#ifndef LCD_D4_Pin
#error "LCD_D4_Pin is not defined. Set LCD D4 pin User Label to LCD_D4 in CubeMX."
#endif
#ifndef LCD_D5_GPIO_Port
#error "LCD_D5_GPIO_Port is not defined. Set LCD D5 pin User Label to LCD_D5 in CubeMX."
#endif
#ifndef LCD_D5_Pin
#error "LCD_D5_Pin is not defined. Set LCD D5 pin User Label to LCD_D5 in CubeMX."
#endif
#ifndef LCD_D6_GPIO_Port
#error "LCD_D6_GPIO_Port is not defined. Set LCD D6 pin User Label to LCD_D6 in CubeMX."
#endif
#ifndef LCD_D6_Pin
#error "LCD_D6_Pin is not defined. Set LCD D6 pin User Label to LCD_D6 in CubeMX."
#endif
#ifndef LCD_D7_GPIO_Port
#error "LCD_D7_GPIO_Port is not defined. Set LCD D7 pin User Label to LCD_D7 in CubeMX."
#endif
#ifndef LCD_D7_Pin
#error "LCD_D7_Pin is not defined. Set LCD D7 pin User Label to LCD_D7 in CubeMX."
#endif
#define LCD1602_CMD_CLEAR_DISPLAY 0x01u
#define LCD1602_CMD_RETURN_HOME 0x02u
#define LCD1602_CMD_ENTRY_MODE 0x06u
#define LCD1602_CMD_DISPLAY_ON 0x0Cu
#define LCD1602_CMD_FUNCTION_SET 0x28u
#define LCD1602_CMD_SET_DDRAM 0x80u
static void App_LCD1602_DelayShort(void)
{
volatile
uint32_t
i;
for
(i =
0u
; i <
200u
; i++)
{
__NOP();
}
}
static void App_LCD1602_Write4Bits(uint8_t value)
{
HAL_GPIO_WritePin(LCD_D4_GPIO_Port, LCD_D4_Pin, ((value &
0x01
u) !=
0u
) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_D5_GPIO_Port, LCD_D5_Pin, ((value &
0x02
u) !=
0u
) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_D6_GPIO_Port, LCD_D6_Pin, ((value &
0x04
u) !=
0u
) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_D7_GPIO_Port, LCD_D7_Pin, ((value &
0x08
u) !=
0u
) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_E_GPIO_Port, LCD_E_Pin, GPIO_PIN_SET);
App_LCD1602_DelayShort();
HAL_GPIO_WritePin(LCD_E_GPIO_Port, LCD_E_Pin, GPIO_PIN_RESET);
App_LCD1602_DelayShort();
}
static void App_LCD1602_WriteByte(uint8_t rs, uint8_t value)
{
HAL_GPIO_WritePin(LCD_RS_GPIO_Port, LCD_RS_Pin, (rs !=
0u
) ? GPIO_PIN_SET : GPIO_PIN_RESET);
App_LCD1602_Write4Bits((
uint8_t
)(value >>
4
));
App_LCD1602_Write4Bits((
uint8_t
)(value &
0x0F
u));
HAL_Delay(
1
);
}
static void App_LCD1602_WriteCommand(uint8_t command)
{
App_LCD1602_WriteByte(
0u
, command);
if
((command == LCD1602_CMD_CLEAR_DISPLAY) || (command == LCD1602_CMD_RETURN_HOME))
{
HAL_Delay(
2
);
}
}
static void App_LCD1602_WriteData(uint8_t data)
{
App_LCD1602_WriteByte(
1u
, data);
}
void App_LCD1602_Init(void)
{
HAL_GPIO_WritePin(LCD_RS_GPIO_Port, LCD_RS_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_E_GPIO_Port, LCD_E_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_D4_GPIO_Port, LCD_D4_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_D5_GPIO_Port, LCD_D5_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_D6_GPIO_Port, LCD_D6_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LCD_D7_GPIO_Port, LCD_D7_Pin, GPIO_PIN_RESET);
HAL_Delay(
40
);
/* * HD44780 compatible 4-bit initialization sequence. * At this moment LCD is not in 4-bit mode yet, so send nibbles directly. */
App_LCD1602_Write4Bits(
0x03
u);
HAL_Delay(
5
);
App_LCD1602_Write4Bits(
0x03
u);
HAL_Delay(
1
);
App_LCD1602_Write4Bits(
0x03
u);
HAL_Delay(
1
);
App_LCD1602_Write4Bits(
0x02
u);
HAL_Delay(
1
);
App_LCD1602_WriteCommand(LCD1602_CMD_FUNCTION_SET);
App_LCD1602_WriteCommand(LCD1602_CMD_DISPLAY_ON);
App_LCD1602_WriteCommand(LCD1602_CMD_ENTRY_MODE);
App_LCD1602_Clear();
}
void App_LCD1602_Clear(void)
{
App_LCD1602_WriteCommand(LCD1602_CMD_CLEAR_DISPLAY);
}
void App_LCD1602_SetCursor(uint8_t row, uint8_t col)
{
uint8_t
address;
if
(row >
1u
)
{
row =
1u
;
}
if
(col >
15u
)
{
col =
15u
;
}
address = (row ==
0u
) ? col : (
uint8_t
)(
0x40
u + col);
App_LCD1602_WriteCommand((
uint8_t
)(LCD1602_CMD_SET_DDRAM | address));
}
void App_LCD1602_Print(const char *str)
{
if
(str ==
0
)
{
return
;
}
while
(*str !=
'\0'
)
{
App_LCD1602_WriteData((
uint8_t
)(*str));
str++;
}
}
void App_LCD1602_PrintLine(uint8_t row, const char *str)
{
uint8_t
i;
App_LCD1602_SetCursor(row,
0u
);
for
(i =
0u
; i <
16u
; i++)
{
if
((str !=
0
) && (*str !=
'\0'
))
{
App_LCD1602_WriteData((
uint8_t
)(*str));
str++;
}
else
{
App_LCD1602_WriteData((
uint8_t
)
' '
);
}
}
}
3. 把 app_lcd1602.c 加入 Keil 工程
手动新建 .c 文件后,Keil 不一定会自动编译它。
在 Keil 工程树里右键:
bash
Application/User/Core
选择:
bash
Add Existing Files to Group 'Application/User/Core'
添加:
bash
Core/Src/app_lcd1602.c

main.c 调用方式
1. Includes 区域添加头文件
找到:
bash
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
改成:
bash
/* USER CODE BEGIN Includes */
#include "app_lcd1602.h"
/* USER CODE END Includes */
2. 初始化区域显示两行文字
确保:
bash
MX_GPIO_Init();
已经执行。
然后在 USER CODE BEGIN 2 里添加:
bash
/* USER CODE BEGIN 2 */
App_LCD1602_Init();
App_LCD1602_PrintLine(
0
,
"Hello STM32"
);
App_LCD1602_PrintLine(
1
,
"LCD1602 OK"
);
/* USER CODE END 2 */
为什么要放在 MX_GPIO_Init() 后面?
因为 LCD 的 6 根控制线和数据线必须先被初始化成 GPIO 输出。
否则你调用 App_LCD1602_Init() 时,GPIO 还没准备好。
3. while 循环先不写
第一版可以先让 LCD 显示固定内容。
while (1) 里不用写 LCD 逻辑。
如果你想显示一个每秒变化的计数,可以这样写:
bash
/* USER CODE BEGIN Includes */
#include "app_lcd1602.h"
#include <stdio.h>
/* USER CODE END Includes */
然后在 USER CODE BEGIN 3 里写:
bash
/* USER CODE BEGIN 3 */
static
uint32_t
count =
0
;
char
line[
17
];
snprintf
(line,
sizeof
(line),
"Count:%lu"
, count++);
App_LCD1602_PrintLine(
1
, line);
HAL_Delay(
1000
);
/* USER CODE END 3 */
注意 line[17]。
1602A 一行 16 个字符,字符串还需要一个结尾的 \0。
所以数组至少要 17。
编译、下载和验证
代码加完后,先编译:
bash
Build / F7
没有错误后下载:
bash
Download
正常现象是:
bash
第一行:Hello STM32
第二行:LCD1602 OK
如果背光亮但没字,先别急着改代码。
第一优先级是:
bash
慢慢调 VO 对比度
很多 1602A 第一次不显示,都是对比度没调到合适位置。

移植到其他板子的修改点
这篇移植点主要有 6 个。
|
要改的地方
|
为什么要改
|
在哪里改
|
| --- | --- | --- |
| RS
引脚
|
不同板子可用 GPIO 不同
|
CubeMX Pinout
|
| E
引脚
|
LCD 使能脚必须接对
|
CubeMX Pinout
|
| D4~D7
引脚
|
4 位数据线顺序不能乱
|
CubeMX Pinout 和接线
|
|
User Label
|
代码依赖 LCD_RS_Pin 等宏
|
CubeMX GPIO 页面
|
|
LCD 供电
|
有的 LCD 5V,有的模块可 3.3V
|
看模块说明
|
|
对比度 VO
|
不调对比度可能看不到字
|
电位器
|
换板子的推荐顺序:
-
先选 6 个空闲 GPIO;
-
在 CubeMX 里配置成
GPIO_Output; -
User Label 分别填
LCD_RS、LCD_E、LCD_D4、LCD_D5、LCD_D6、LCD_D7; -
按 User Label 接线,不要把 D4~D7 顺序接乱;
-
RW先接 GND; -
重新生成代码、编译、下载;
-
上电后先调对比度,再判断程序是否正常。
常见问题排查
1. 背光亮,但没有任何字符
优先检查:
-
VO对比度有没有调; -
RS/E/D4~D7有没有接错; -
app_lcd1602.c有没有加入 Keil 工程; -
App_LCD1602_Init()有没有放在MX_GPIO_Init()后面; -
User Label 是否和代码一致。
2. 出现一排黑块
一排黑块通常说明 LCD 有供电,对比度也能显示出来,但初始化没成功。
重点检查:
-
4 位初始化时序是否执行;
-
E引脚是否接错; -
D4~D7顺序是否接反; -
RW是否接 GND; -
STM32 GPIO 是否配置成输出。
3. 显示乱码
常见原因:
-
D4、D5、D6、D7 顺序接错;
-
E脉冲线接触不好; -
供电不稳;
-
字符串超过一行但没有正确设置光标;
-
初始化延时太短。
本篇代码延时偏保守,就是为了先保证新手能跑通。
4. 编译报 LCD_RS_GPIO_Port is not defined
说明 CubeMX 没有生成:
bash
LCD_RS_GPIO_Port
LCD_RS_Pin
去 Core/Inc/main.h 看一下。
如果你看到的是:
bash
#define RS_Pin ...
#define RS_GPIO_Port ...
那说明 User Label 不是 LCD_RS。
解决方法:
-
回 CubeMX;
-
找到 RS 对应 GPIO;
-
把 User Label 改成
LCD_RS; -
其他引脚同理;
-
重新 Generate Code。
5. 编译报 undefined symbol App_LCD1602_Init
通常是 app_lcd1602.c 没加入 Keil 工程。
解决方法:
-
右键
Application/User/Core; -
Add Existing Files to Group; -
添加
Core/Src/app_lcd1602.c; -
重新编译。
6. 背光不亮
背光一般和显示控制逻辑分开。
优先检查:
-
A/K 是否接反;
-
背光是否需要限流电阻;
-
模块背光是否已经自带电阻;
-
电源是否正常。
背光不亮不一定代表 LCD 控制代码错。
本篇小结
这一篇我们用 STM32 GPIO 驱动了裸 16 针 1602A。
你现在至少应该知道:
-
1602A 是 16x2 字符屏,不是图形屏;
-
裸 16 针屏可以用 4 位并口模式节省 GPIO;
-
RS=0写命令,RS=1写字符数据; -
RW入门时建议接 GND,只写不读; -
E是让 LCD 接收数据的使能脉冲; -
D4~D7 顺序不能接乱;
-
VO对比度不调好,代码对了也可能看不到字; -
应用层代码放在
app_lcd1602.h/.c,main.c只调用。
下一篇我们把第 14 篇 ADC 的结果显示到 1602A 上:
STM32 1602A 显示数字:把 ADC 电压显示到屏幕上。