
1. 为什么 STM32F103C8T6 驱动 LCD1602 显示数字总翻车STM32F103C8T6 驱动 LCD1602 显示数字是很多嵌入式新手从点灯迈向“能显示数据”的第一道坎。LCD1602 本身不复杂它内置 HD44780 兼容控制器能显示 2 行、每行 16 个字符用来显示传感器数值、计数器、状态码都很合适。但问题在于STM32 的 GPIO 是 3.3V 电平LCD1602 通常按 5V 逻辑设计再加上裸机环境下没有现成的延时库、没有 Arduino 那种LiquidCrystal封装很多人一上来就卡在“屏幕亮了但不显示字”“显示乱码”“只亮背光没有黑块”这些现象上。我见过最多的场景是代码从网上抄了一份引脚对不上自己的板子或者延时函数用的是空循环编译优化一开时序就崩了。还有人把 RW 脚直接接地结果忙标志没法读只能靠死延时硬等稍微改个主频就全乱。这篇就按 STM32F103C8T6 裸机环境从 GPIO 初始化讲到数字显示逻辑给你一份能直接复制、能编译、能烧录验证的完整实现。适合刚学完 GPIO 输出、想做一个“能显示数字的小系统”的朋友也适合拿它当模板去接 DHT11、超声波、ADC 采样的显示层。核心检索词先摆出来STM32F103C8T6 怎么配置 GPIO 驱动 LCD1602、LCD1602 显示数字的程序怎么写、4 位数据线模式怎么接线、延时和时序怎么控制。下面按“接线 → GPIO 配置 → 驱动函数 → 显示数字 → 验证 → 排障”的顺序走每一步都给可复制的代码。2. 动手前的准备接线、电平与 TaoToken 辅助2.1 LCD1602 引脚与 STM32 接线对照LCD1602 有 16 个引脚实际驱动只需要关注这几组数据线 D4~D74 位模式、RS寄存器选择、RW读写选择、E使能、V0对比度、背光 A/K。为了省 IO我推荐用 4 位数据线模式这样 6 根控制线加 4 根数据线一共 10 根就能跑起来。LCD1602 引脚作用接到 STM32F103C8T6说明VSS地GND共地必须接VDD电源5V逻辑供电建议 5VV0对比度电位器中点调不出黑块多半是这里RS数据/指令PA00 指令 1 数据RW读/写PA1写模式接低E使能PA2下降沿锁存D4~D7数据PA4~PA74 位模式只用高 4 位A/K背光5V/GND串限流电阻注意STM32 的 PA 口是 3.3V 输出直接驱动 5V 的 LCD1602 数据线多数模块能识别为高电平但如果你遇到显示不稳可以在数据线上加电平转换或者把 LCD 的 VDD 也降到 3.3V 试试部分模块 3.3V 也能亮只是对比度偏淡。2.2 用 TaoToken 快速核对驱动写法与报错裸机开发最烦的是编译报错和时序问题排查。我习惯把不确定的寄存器配置、延时写法丢给模型对话去核对比如“STM32F103 推挽输出 50MHz 的 CRH 配置怎么写”“HD44780 4 位初始化为什么要发三次 0x03”。TaoToken 的模型对话入口可以直接问这类细节省去翻手册的时间https://taotoken.net/api 配合模型对话页 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat 就能用。如果你后面要长期写 STM32 工程、接 Agent 自动补全可以看 Coding Planhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan 。接入前先在控制台建 Keyhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole Key 管理在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys 文档在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 。这些只是辅助你查资料和写代码真正跑起来还得靠板子。3. 可复制配置GPIO 初始化与 LCD1602 驱动骨架3.1 GPIO 初始化代码下面这段是 STM32F103C8T6 标准外设库写法把 PA0~PA7 配成推挽输出。注意 CRL 寄存器控制 PA0~PA7每个引脚 4 位配置50MHz 输出 推挽就是 0x3。#include stm32f10x.h #define LCD_RS_PIN GPIO_Pin_0 #define LCD_RW_PIN GPIO_Pin_1 #define LCD_E_PIN GPIO_Pin_2 #define LCD_D4_PIN GPIO_Pin_4 #define LCD_D5_PIN GPIO_Pin_5 #define LCD_D6_PIN GPIO_Pin_6 #define LCD_D7_PIN GPIO_Pin_7 #define LCD_PORT GPIOA void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin LCD_RS_PIN | LCD_RW_PIN | LCD_E_PIN | LCD_D4_PIN | LCD_D5_PIN | LCD_D6_PIN | LCD_D7_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(LCD_PORT, GPIO_InitStructure); GPIO_ResetBits(LCD_PORT, LCD_RS_PIN | LCD_RW_PIN | LCD_E_PIN); }3.2 微秒延时与写字节函数HD44780 的时序要求 E 高电平脉宽至少 450ns写指令后要等 40us 以上。裸机没有delay_us用 SysTick 或者简单的空循环都行。下面用空循环实现主频 72MHz 下大致可用实际可以按示波器微调。void LCD_DelayUs(uint32_t us) { uint32_t i; for (i 0; i us * 8; i) { __NOP(); } } void LCD_WriteByte(uint8_t data, uint8_t rs) { if (rs) GPIO_SetBits(LCD_PORT, LCD_RS_PIN); else GPIO_ResetBits(LCD_PORT, LCD_RS_PIN); GPIO_ResetBits(LCD_PORT, LCD_RW_PIN); // 写模式 // 先发高 4 位 GPIO_ResetBits(LCD_PORT, LCD_D4_PIN | LCD_D5_PIN | LCD_D6_PIN | LCD_D7_PIN); if (data 0x10) GPIO_SetBits(LCD_PORT, LCD_D4_PIN); if (data 0x20) GPIO_SetBits(LCD_PORT, LCD_D5_PIN); if (data 0x40) GPIO_SetBits(LCD_PORT, LCD_D6_PIN); if (data 0x80) GPIO_SetBits(LCD_PORT, LCD_D7_PIN); GPIO_SetBits(LCD_PORT, LCD_E_PIN); LCD_DelayUs(1); GPIO_ResetBits(LCD_PORT, LCD_E_PIN); LCD_DelayUs(50); // 再发低 4 位 GPIO_ResetBits(LCD_PORT, LCD_D4_PIN | LCD_D5_PIN | LCD_D6_PIN | LCD_D7_PIN); if (data 0x01) GPIO_SetBits(LCD_PORT, LCD_D4_PIN); if (data 0x02) GPIO_SetBits(LCD_PORT, LCD_D5_PIN); if (data 0x04) GPIO_SetBits(LCD_PORT, LCD_D6_PIN); if (data 0x08) GPIO_SetBits(LCD_PORT, LCD_D7_PIN); GPIO_SetBits(LCD_PORT, LCD_E_PIN); LCD_DelayUs(1); GPIO_ResetBits(LCD_PORT, LCD_E_PIN); LCD_DelayUs(50); }3.3 初始化序列与显示数字函数4 位模式的初始化有个坑上电后必须先按 8 位模式发三次 0x03再切到 4 位模式发 0x02否则控制器不认。下面这段是标准流程。void LCD_WriteCmd(uint8_t cmd) { LCD_WriteByte(cmd, 0); } void LCD_WriteData(uint8_t dat) { LCD_WriteByte(dat, 1); } void LCD1602_Init(void) { LCD_GPIO_Init(); LCD_DelayUs(15000); // 强制 8 位模式三次 LCD_WriteByte(0x03, 0); LCD_DelayUs(5000); LCD_WriteByte(0x03, 0); LCD_DelayUs(200); LCD_WriteByte(0x03, 0); LCD_DelayUs(200); // 切 4 位 LCD_WriteByte(0x02, 0); LCD_DelayUs(200); LCD_WriteCmd(0x28); // 4 位2 行5x7 点阵 LCD_WriteCmd(0x0C); // 开显示关光标 LCD_WriteCmd(0x06); // 写入后光标右移 LCD_WriteCmd(0x01); // 清屏 LCD_DelayUs(2000); } void LCD1602_SetCursor(uint8_t row, uint8_t col) { uint8_t addr (row 0) ? 0x80 : 0xC0; LCD_WriteCmd(addr col); } void LCD1602_ShowChar(uint8_t ch) { LCD_WriteData(ch); } void LCD1602_ShowString(char *str) { while (*str) { LCD_WriteData(*str); } } void LCD1602_ShowNum(uint32_t num) { char buf[12]; int i 0; if (num 0) { LCD_WriteData(0); return; } while (num 0) { buf[i] 0 (num % 10); num / 10; } while (i 0) { LCD_WriteData(buf[--i]); } }4. 验证请求主函数与显示结果4.1 主函数显示一个递增数字把上面的函数拼起来主函数里做一个 0 到 999 的计数器每 500ms 加一显示在第二行。#include stm32f10x.h int main(void) { uint32_t count 0; LCD1602_Init(); LCD1602_SetCursor(0, 0); LCD1602_ShowString(Number:); while (1) { LCD1602_SetCursor(1, 0); LCD1602_ShowString( ); // 清掉旧数字 LCD1602_SetCursor(1, 0); LCD1602_ShowNum(count); count; if (count 999) count 0; for (volatile uint32_t i 0; i 7200000; i); // 约 500ms } }4.2 编译烧录后的预期现象烧录后你应该看到第一行固定显示Number:第二行从 0 开始每半秒加一到 999 后回到 0。如果第二行数字位数变化时残留旧字符说明清行没做干净可以在写新数字前先写 4 个空格。实测下来4 位模式下这套代码在 72MHz 主频、标准库环境下能稳定跑数字刷新不闪。提示如果你用的是 HAL 库把GPIO_SetBits/ResetBits换成HAL_GPIO_WritePin延时换成HAL_Delay或 DWT 计数即可逻辑完全一样。5. 本篇常见错排查5.1 屏幕只有背光没有黑块这是最典型的对比度问题。V0 脚必须接电位器中点调到能看到一排黑方块为止。如果 V0 直接接地黑块会过深接 5V 则完全看不到。先把对比度调出黑块再谈显示内容。5.2 显示乱码或只显示第一行先检查 4 位初始化序列有没有发够三次 0x03。少发一次控制器可能还停在 8 位模式后续所有数据都会错位。其次检查 D4~D7 的接线顺序很多人把 D4 接到 PA7、D7 接到 PA4顺序反了就是乱码。5.3 数字显示不全或跳变多半是延时不够。HD44780 写指令后需要 40us 以上清屏指令需要 1.64ms。如果你把LCD_DelayUs里的系数调太小或者编译器开了高优化把空循环优化掉时序就会崩。可以把延时函数改成基于 SysTick 的精确延时或者在空循环里加volatile防止被优化。5.4 编译报错找不到 GPIO_InitTypeDef这是没加标准库头文件或没配 include 路径。确认工程里包含了stm32f10x_gpio.c、stm32f10x_rcc.c并在 Keil 的 C/C 选项里把库目录加进 Include Paths。如果用的是寄存器版直接操作 CRL、BSRR 也行但可读性差一些。6. 继续往下走把显示层接到真实数据数字显示跑通后下一步通常是把LCD1602_ShowNum接到真实数据源比如 ADC 采样的电压值、DHT11 的温湿度、超声波测距的厘米数。这时候你会发现显示层和采集层要解耦建议把LCD1602_ShowNum扩展成支持小数和负数的版本或者直接格式化成字符串再LCD1602_ShowString。如果你在接传感器时遇到寄存器配置、时序对不上的问题可以用模型对话快速核对写法https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat 。长期写 STM32 工程、想让 Agent 帮你补全驱动代码的可以看 Coding Planhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan 。接入前先在控制台建 Keyhttps://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole Key 管理在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys 文档在 https://taotoken.net/api?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 。官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。最后留一个我踩过的坑LCD1602 的 E 使能脚一定要在数据稳定后再拉高拉高时间别太短1us 是保险值。如果你用示波器看E 的下降沿才是锁存时刻数据要在下降沿之前就稳定。把这一点吃透后面接什么传感器显示都不会慌。