尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32驱动LCD1602简易计算器:从硬件时序到表达式解析

STM32驱动LCD1602简易计算器:从硬件时序到表达式解析 简介基于STM32F103C8T6单片机实现的简易计算器工程采用4×4矩阵键盘输入、LCD1602液晶显示适合嵌入式入门学习者及课程设计参考。压缩包共78个文件约197KB包含32个C源文件、33个头文件、8个汇编启动文件、Keil工程文件以及可直接烧录的hex程序工程内部分为CORE、STM32_FWLIB、SYSTEM、USER等模块涵盖系统时钟、延时、串口、LCD1602驱动及按键扫描等完整实现。该工程基于Keil MDK开发使用标准外设库代码模块化清晰读者可以直观理解STM32从启动文件到外设初始化的整体流程掌握矩阵键盘扫描去抖、LCD显示控制及运算逻辑的代码组织方法并可直接借助Keil工程编译烧录便于在开发板上动手验证。项目采用模块化设计按键扫描与液晶显示相互独立便于移植复用。已有534人浏览学习适合需要快速搭建一个完整外设应用范例的开发者参考。1. 从LCD1602到计算器STM32显示之外的核心在运算逻辑用STM32驱动LCD1602做简易计算器是很多人在标准库或HAL库里完成的第一个完整小项目。表面看它把「LCD1602液晶显示屏程序」和「矩阵键盘扫描」拼在一起就能跑通实际做起来会发现显示和按键都只是外围真正决定项目难易的是运算逻辑怎么设计——是只做两个操作数的加减乘除还是支持带括号和优先级的表达式求值这两种代码量差了三倍不止。这篇围绕「基于STM32的简易计算器」这个具体的做法展开先搭硬件引脚和时序再写LCD1602的初始化驱动最后把按键扫描、表达式解析和显示刷新串成完整闭环。新手可以按这套路线做通第一版老手也能在参数和状态机设计上找到值得调整的地方。2. STM32计算器的硬件方案与引脚规划LCD1602走4位还是8位2.1 LCD1602接口原理简述LCD1602是字符型液晶内部有HD44780兼容控制器支持8位和4位两种数据总线模式。8位模式一次将DB0DB7和RS、RW、EN全部接给MCU占用至少11个GPIO4位模式只使用DB4DB7配合RS、RW、EN共7个引脚极大节省IO资源。STM32F103C8T6这类主流型号的GPIO引脚数量虽然充足但考虑到后续可能扩展按键、传感器或通信模块「给项目留引脚余量」比「多接几根线」更能体现工程化意识这也是我一般建议直接采用4位模式的原因。4位模式的核心工作原理是一次完整的8位数据传输被拆成高4位和低4位两次写入先通过DB4DB7发送高4位再发送低4位使能信号EN在每次写入时都要产生一个下降沿锁存数据。时序上需要关注的最小时间参数包括EN高电平持续时间、两次写入之间的延时。标准做法是在STM32中插入微秒级延时函数来满足时序要求。需要特别留意的是4位模式下LCD1602的初始化时序和8位模式不同不能只靠「每次写一个字节」的通用函数而必须严格按以下顺序执行等待上电稳定、发送初始化指令0x33、发送初始化指令0x32、设置显示模式。这个细节是初学者最容易踩的坑也是区分代码能否在硬件上真正点亮LCD1602的关键。2.2 STM32引脚分配表下表给出一份可直接使用的引脚规划基于STM32F103C8T6的PA/PB口LCD1602使用4位模式矩阵键盘占用一组空闲GPIO模块信号STM32引脚说明LCD1602RSPB00写指令1写数据LCD1602RWPB1接地固定为写模式LCD1602ENPB10使能信号下降沿锁存LCD1602DB4PB12数据线D4LCD1602DB5PB13数据线D5LCD1602DB6PB14数据线D6LCD1602DB7PB15数据线D7矩阵键盘ROW0~3PA0~PA3行输出矩阵键盘COL0~3PA4~PA7列输入引脚规划的逻辑是优先让LCD1602数据线接在同一组端口的连续引脚上方便后续修改GPIO配置时批量操作矩阵键盘的行列方向取决于编程习惯常见做法是行设置为推挽输出、列设置为上拉输入按键按下时列读到低电平。提示V0对比度调节引脚串一个10k电位器到地第一版调试时把对比度调到字符边缘不模糊即可。RW引脚直接接地意味着MCU不需要读取忙标志每次写入前用固定延时代替忙检测代码会简洁很多代价是整体刷新速度略慢但显示计算器结果完全够用。2.3 按键矩阵与扫描方式选型简易计算器至少需要16个按键0到9十个数字键、加、减、乘、除四个运算符、等号键、清除键。传统做法是用4x4矩阵键盘行接输出列接输入扫描时逐行拉低然后读列电平判断哪个位置被按下。键盘扫描函数返回的是一个0到15的键值编码。矩阵扫描的关键在于消抖和释放检测。按键按下瞬间电平会有约5到20毫秒的抖动如果不做消抖一次物理按压可能被识别成多次触发计算器就会把同一个数字重复输入。常见做法是在检测到电平变化后延时10毫秒再确认确认有效后还要等待按键释放再继续扫描否则按住不放会导致连发。我在实际项目中偏向使用「状态机加定时器」方式做扫描主循环不再阻塞等待按键而是由定时器中断每10毫秒读一次矩阵记录按下事件到缓冲区主程序从缓冲区取事件这样就不会因为消抖延时拖慢LCD1602的刷新。3. LCD1602初始化与字符输出时序参数、初始化序列与最小驱动代码3.1 用STM32标准库写最小驱动LCD1602驱动的核心是三个底层函数延时、写指令、写数据。写指令和写数据的区别只在RS引脚电平RW已经接地所以读操作相关逻辑可以完全不写。下面是基于STM32标准库的最小驱动代码开发环境按Keil5安装STM32芯片包后可直接编译// 引脚宏定义方便后续调整 #define LCD_RS_GPIO GPIOB #define LCD_RS_PIN GPIO_Pin_0 #define LCD_EN_GPIO GPIOB #define LCD_EN_PIN GPIO_Pin_10 #define LCD_D4_GPIO GPIOB #define LCD_D4_PIN GPIO_Pin_12 #define LCD_D5_GPIO GPIOB #define LCD_D5_PIN GPIO_Pin_13 #define LCD_D6_GPIO GPIOB #define LCD_D6_PIN GPIO_Pin_14 #define LCD_D7_GPIO GPIOB #define LCD_D7_PIN GPIO_Pin_15 // 微秒级延时系统时钟72MHz时可用简单循环 void Delay_us(uint32_t us) { uint32_t i; for (i 0; i us * 4; i) { __NOP(); } } // 将数据线DB4~DB7设置为要输出的值 void LCD_SetData(uint8_t data) { GPIO_WriteBit(LCD_D4_GPIO, LCD_D4_PIN, (data 0) 0x01); GPIO_WriteBit(LCD_D5_GPIO, LCD_D5_PIN, (data 1) 0x01); GPIO_WriteBit(LCD_D6_GPIO, LCD_D6_PIN, (data 2) 0x01); GPIO_WriteBit(LCD_D7_GPIO, LCD_D7_PIN, (data 3) 0x01); } // 使能脉冲下降沿锁存数据 void LCD_EnablePulse(void) { GPIO_SetBits(LCD_EN_GPIO, LCD_EN_PIN); Delay_us(2); GPIO_ResetBits(LCD_EN_GPIO, LCD_EN_PIN); Delay_us(2); } // 通过4位模式写入一个字节分高4位和低4位两次 void LCD_WriteByte(uint8_t data, uint8_t rs) { if (rs) { GPIO_SetBits(LCD_RS_GPIO, LCD_RS_PIN); } else { GPIO_ResetBits(LCD_RS_GPIO, LCD_RS_PIN); } LCD_SetData(data 4); // 先送高4位 LCD_EnablePulse(); LCD_SetData(data 0x0F); // 再送低4位 LCD_EnablePulse(); } // 写指令 void LCD_WriteCmd(uint8_t cmd) { LCD_WriteByte(cmd, 0); } // 写字符数据 void LCD_WriteData(uint8_t dat) { LCD_WriteByte(dat, 1); }这段代码的逻辑说明LCD_SetData根据传入的字节取出相应的位分别赋给DB4到DB7LCD_EnablePulse产生一个下降沿HD44780在下降沿锁存数据线上的状态。由于4位模式一次只能传4位写一个字节需要两次发送。RS引脚决定这次传输是指令还是数据写指令时RS为0写字符时RS为1。Delay_us的循环次数需要根据实际编译优化等级微调在Keil的-O0优化下一般能保证时序余量足够。3.2 LCD1602初始化序列HD44780的初始化序列是固定流程不能精简也不能调换顺序。网上能找到的「哪里可以下载lcd1602的驱动代码」很多但去掉有效的初始化后直接调用写字符函数液晶屏不会有任何显示原因就在这里。下面是经过验证的初始化函数// LCD1602 4位模式初始化 void LCD_Init(void) { GPIOB-CRL ~(0xF (4 * 0)); // PB0配置为推挽输出50MHz GPIOB-CRL | (0x3 (4 * 0)); GPIOB-CRL ~(0xF (4 * 1)); GPIOB-CRL | (0x3 (4 * 1)); GPIOB-CRH ~(0xF (4 * (10 - 8))); GPIOB-CRH | (0x3 (4 * (10 - 8))); // 数据线端口配置略去同样为推挽输出 Delay_us(50000); // 等待LCD上电稳定手册要求大于40ms LCD_WriteCmd(0x33); // 第一次初始化指令8位模式 Delay_us(5000); LCD_WriteCmd(0x32); // 第二次初始化指令切换到4位模式 Delay_us(5000); LCD_WriteCmd(0x28); // 4位模式2行显示5x7点阵 LCD_WriteCmd(0x0C); // 显示开光标关闪烁关 LCD_WriteCmd(0x01); // 清屏 LCD_WriteCmd(0x06); // 写入后地址自动加1光标右移 LCD_WriteCmd(0x80); // 设置DDRAM地址0x00即第一行第一个字符 }初始化序列里0x33和0x32是专门给4位模式准备的。0x33的意思是告诉控制器你现在还没进入4位模式我用8位方式发0x30三次中的第一次。发完0x33后LCD仍处于8位模式再发0x32真正切换到4位模式。之后0x28才是4位模式、双行、5x7字符的显示配置。这个流程对应HD44780数据手册里「software initialization」的表格每一条指令之间的延时是为了让控制器内部状态机稳定。3.3 设置DDRAM地址与字符写入LCD1602内部有两块DDRAM区域第一行地址范围为0x00到0x27第二行为0x40到0x67。写字符之前必须先用指令设置当前地址指令的bit7为1时表示这是一个DDRAM地址设置命令所以第一行第一个字符的命令是0x80第二行第一个字符是0xC0。// 在指定位置显示字符串 void LCD_ShowString(uint8_t line, uint8_t pos, char *str) { if (line 0) { LCD_WriteCmd(0x80 pos); } else { LCD_WriteCmd(0xC0 pos); } while (*str) { LCD_WriteData(*str); } }这个函数使用时需要注意一点LCD1602每行只有16个字符位置pos超过0x0F时会超出DDRAM边界写入后字符不会显示在第二行开头而是隐藏在DDRAM的连续区域里。所以计算器的结果字符串长度必须限制在16字符以内超出部分要么截断要么换行处理。实际调试中经常看到两行文本重叠在同一行原因就是第二行显示前没有发送0xC0。4. 计算器的按键扫描与表达式解析矩阵键盘状态机和中缀转后缀4.1 矩阵键盘扫描及键值编码矩阵键盘扫描的代码模式相对固定4行分别拉低读取4列表状态。下面给出一个定时器驱动的扫描函数返回0到15的键值16表示无按键#define KEY_NONE 16 uint8_t MatrixScan(void) { uint8_t row, col; uint8_t colVal; for (row 0; row 4; row) { // 将当前行拉低 GPIO_ResetBits(GPIOA, GPIO_Pin_0 row); // 其他行拉高 GPIO_SetBits(GPIOA, GPIO_Pin_0 ((row 1) % 4)); GPIO_SetBits(GPIOA, GPIO_Pin_0 ((row 2) % 4)); GPIO_SetBits(GPIOA, GPIO_Pin_0 ((row 3) % 4)); Delay_us(20); // 等待电平稳定 colVal (GPIO_ReadInputDataBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7) 4) 0x0F; if (colVal ! 0x0F) { // 找到按下位置 for (col 0; col 4; col) { if ((colVal (1 col)) 0) { return row * 4 col; } } } } return KEY_NONE; }这段代码放在定时器中断里调用每10毫秒一次。返回值需要和按键布局对应。我在计算器项目里用的映射关系是键值0到9对应数字键10对应清除11对应等于号12到15对应加减乘除。有了这个映射按键处理代码就不需要再关心物理位置逻辑层见到的只是语义键值。4.2 中缀表达式转后缀表达式简易计算器如果只支持两个数和一个运算符那代码里用几个变量存操作数就能处理但一旦要支持连续的表达式比如1234*5就必须处理运算符优先级。常见做法是把「中缀表达式」转换成「后缀表达式」RPN再用栈求值。中缀转后缀使用一个运算符栈// 运算符优先级 uint8_t OpPriority(char op) { if (op || op -) return 1; if (op * || op /) return 2; return 0; } // 中缀转后缀输出存于postfix void InfixToPostfix(char *infix, char *postfix) { char opStack[32]; uint8_t top 0; uint8_t pi 0; uint8_t i 0; char token; while ((token infix[i]) ! #) { if (token 0 token 9) { postfix[pi] token; // 数字直接进后缀表达式 } else if (token () { opStack[top] token; // 左括号直接压栈 } else if (token )) { while (top 0 opStack[top-1] ! () { postfix[pi] opStack[--top]; } if (top 0) top--; // 弹出左括号 } else { // 运算符优先级不高于栈顶时弹出栈顶 while (top 0 opStack[top-1] ! ( OpPriority(opStack[top-1]) OpPriority(token)) { postfix[pi] opStack[--top]; } opStack[top] token; } } while (top 0) { postfix[pi] opStack[--top]; } postfix[pi] #; }这段代码的关键点在于处理优先级时的比较条件只有新运算符优先级「不高于」栈顶时才弹出。也就是说当前运算符和栈顶优先级相等时也要弹出栈顶这保证同级运算符按从左到右的顺序计算。括号在栈内只作为弹栈的停止标志弹出后不进入后缀表达式。数字字符直接进入输出多位数的处理需要在外部解析时额外处理上面的代码只涉及单数字。4.3 后缀表达式求值后缀表达式的求值过程同样基于栈只是这个栈存的是操作数。遇到数字就压栈遇到运算符就弹出两个数计算结果再压回栈表达式结束后栈顶就是结果。int EvalPostfix(char *postfix) { int numStack[32]; uint8_t top 0; uint8_t i 0; char token; while ((token postfix[i]) ! #) { if (token 0 token 9) { numStack[top] token - 0; } else { int b numStack[--top]; // 注意弹栈顺序先弹出的是右操作数 int a numStack[--top]; // 后弹出的是左操作数 switch (token) { case : numStack[top] a b; break; case -: numStack[top] a - b; break; case *: numStack[top] a * b; break; case /: if (b 0) { return 0xFFFF; // 除零错误标记 } numStack[top] a / b; break; default: break; } } } return numStack[top - 1]; }弹栈顺序是求值环节最容易出错的点。假设表达式是5-2后缀形式为52-压栈时5在栈底2在栈顶。扫描到减号时先弹出2再弹出5所以先弹出的变量赋值给右操作数b后弹出的赋值给左操作数a否则会算出负数甚至出现除零。这个细节在整数除法下尤其重要因为无法用交换律规避。4.4 综合键盘事件处理与显示刷新将按键、LCD、表达式计算串起来的主流程可以看作有限状态机等待输入、构建中缀表达式字符串、收到等号后转换并求值、显示结果。下面给出综合代码骨架#define KEY_NUM_0 0 // ... 按键宏定义略 void Calculator_Task(void) { char expr[32]; // 中缀表达式缓存末尾加#标识结束 char postfix[32]; // 后缀表达式缓存 uint8_t exprLen 0; int16_t result; while (1) { uint8_t key KeyBuffer_Get(); // 从环形缓冲区取按键事件 if (key KEY_NONE) { continue; } if (key KEY_NUM_0 key KEY_NUM_9) { expr[exprLen] 0 key; expr[exprLen] #; LCD_ShowString(0, 0, expr); } else if (key KEY_EQUAL) { expr[exprLen] #; // 终止符 InfixToPostfix(expr, postfix); result EvalPostfix(postfix); if (result 0xFFFF) { LCD_ShowString(1, 0, Error: /0); } else { // 将整数转字符串输出 char buf[17]; IntToString(result, buf); LCD_ShowString(1, 0, buf); } exprLen 0; // 清空表达式 } else if (key KEY_CLEAR) { exprLen 0; LCD_WriteCmd(0x01); // 清屏 } // 运算符按键处理逻辑类似不再重复 } }综合流程中「按键事件放入环形缓冲区」这一步切断了物理扫描和业务逻辑的直接耦合。扫描函数负责检测按下和消抖事件缓冲区负责暂存任务函数负责处理。这样做的好处是后续代码想增加连续运算、括号功能或者改为中断驱动时不需要改动扫描函数本身。表达式长度限制也在这个位置做保护直接丢弃超出缓冲区的字符。5. LCD1602显示布局与输入回显技巧游标移动、对齐和边界处理5.1 两行显示区域划分LCD1602只有两行每行16个字符。计算器界面常见的划分方式是第一行显示当前输入的表达式第二行第一列显示运算结果。这样用户在按等号之前可以直观看到自己输入了什么避免误输入。显示刷新策略上频繁调用清屏指令0x01会带来明显的屏幕闪烁因为清屏需要约1.5毫秒执行时间期间光标回到原点显示内容瞬间消失再出现。更平滑的做法是只更新发生变化的字符位置。// 强制更新整行 void LCD_UpdateLine(uint8_t line, char *str) { LCD_ShowString(line, 0, ); // 先清空整行 LCD_ShowString(line, 0, str); }整行更新函数以「先空格覆盖、再写入新内容」的方式避免清屏闪烁。空格覆盖的长度必须是16个字符否则旧内容会残留。实际调试时可以将按键的物理键值打印在第一行表达式显示在第二行辅助排查按键映射错误验证通过后再将界面布局调整为最终方案。5.2 超长表达式和结果溢出处理表达式缓冲区只有32字节实际可容纳的输入字符数有限。用户连续输入多位数字或长表达式时需要处理越界情况。常见做法是在添加字符前检查表达式长度if (exprLen (sizeof(expr) - 1)) { // 缓冲将满拒绝本次输入并给出提示 LCD_ShowString(1, 12, E:MAX); } else { expr[exprLen] ch; }结果显示方面假设计算器的计算范围限制在int16_t内-32768到32767结果显示时需要考虑负号和字符串转换。自己写的IntToString函数需要处理负数符号和数字位数以下是简洁的转换实现void IntToString(int val, char *buf) { char tmp[16]; uint8_t i 0; uint8_t len 0; if (val 0) { buf[len] -; val -val; } if (val 0) { tmp[i] 0; } else { while (val 0) { tmp[i] 0 (val % 10); val / 10; } } while (i 0) { buf[len] tmp[--i]; } buf[len] \0; }负数处理时有一个容易忽略的问题int16_t的取值范围是-32768到32767如果计算结果恰好是-32768取反操作会导致溢出因为32768超出了int16_t正数上限。将中间值用int类型保存可以规避这个边界问题这是简易计算器在数值范围上最值得预留的一个保护点。5.3 调试LCD1602和按键的关键技巧LCD1602不显示内容时先从电位器调整对比度入手这是最常见的原因。初次上电如果屏幕有方块但无字符说明LCD已经初始化成功但对比度不对拧电位器让方块变浅或消失如果第一行满格方块且不响应指令对照度电位器调整无效后检查EN脉冲宽度再看MCU和LCD的电平标准是否匹配。Proteus模拟可以在没有实物的情况下先跑通逻辑但模拟器对时序的宽容度远高于真实器件有时模拟正常实物不亮。实物调试时一般使用示波器查看EN引脚是否有下降沿、数据线电平是否和代码预期一致。按键部分处于「按下没反应」时先在扫描函数里设置断点确认GPIO配置是否正确。我用过的最快验证方法是将矩阵键盘16个键值逐一显示在LCD1602上按下按键后查看键值变化是否和物理布局对应。这个方法能直接区分「硬件接触不良」「GPIO配置错误」「扫描算法错误」三个层级的问题。本文还有配套的精品资源点击获取
返回列表