资讯动态

STC8A8K单片机简易计算器:LCD1602与矩阵键盘驱动实战

发布时间:2026/10/13 14:23:14 来源:尧图企业网站定制
简介基于STC8A8K64S4单片机设计的简易计算器完整工程适合单片机初学者、课程设计及电子竞赛参考。功能上通过4×4矩阵键盘完成输入支持加减乘除四则运算与带符号数字处理并由LCD1602实时显示按键及运算过程。压缩包共24个文件约16.84MB核心内容为C源码main.c、key.c、1602.c、对应头文件、Keil工程配置文件uvproj/uvopt/plg、编译生成的hex/obj/lst文件以及一份docx开发说明文档结构完整便于直接编译烧录与对照学习。已有904人学习下载。配套开发文档对矩阵键盘扫描原理、LCD1602驱动方式、运算流程和模块划分均有说明能帮助读者快速理解STC8A8K外设使用与程序框架也方便在此基础上扩展功能是单片机项目从原理到实现的合适参考。1. 基于STC8A8K的简易计算器从下载到烧录一晚上复现做单片机课设的时候最尴尬的事不是不会点灯而是下载了一个号称“可直接运行”的计算器工程烧录进STC8A8K开发板之后屏幕上只有一行乱码方块。问题很少出在计算器算法本身而是集中在三个地方LCD1602的初始化时序、矩阵键盘的消抖方式、以及STC8A8K特有的IO口模式和冷启动下载方式。STC8A8K是一颗国产增强型8位机内置64KB程序Flash和8KB左右的SRAM跑一个四则运算计算器资源非常充裕真正决定成败的是外围电路和驱动代码是否匹配。这份基于STC8A8K的简易计算器资源打包了源码工程和开发文档源码按模块拆开引脚定义集中在头文件里适合正在做课设的学生也适合想快速上手STC8A8K但没有时间从零写驱动的工程师当参考底板。2. 通读工程与接线STC8A8K点亮LCD1602的引脚分工2.1 先看config.h引脚定义决定接线方式解压以后先别急着打开Keil工程建议先看源码里有没有一个集中放引脚定义的头文件通常叫config.h或者board.h。我习惯把这个文件当成装机的“电路图”只要引脚定义和实际接线一致后面代码基本不用大改。这类STC8A8K计算器工程的常见接法是P0口整组接LCD1602的数据线D0-D7P2口选三个脚控制RS、RW和E4x4矩阵键盘占P1和P7各四个脚。下面是一份典型的config.h内容。/* config.h —— 集中管理引脚定义 */ #ifndef __CONFIG_H #define __CONFIG_H #include stc8a8k.h /* LCD1602 接口 */ #define LCD_DB_PORT P0 // P0.0~P0.7 接 LCD1602 的 D0~D7 #define LCD_RS P2_4 // RS: 0写命令 1写数据 #define LCD_RW P2_5 // RW: 0写模式 1读模式 #define LCD_E P2_6 // E: 下降沿锁存数据 /* 4x4 矩阵键盘接口 */ #define KEY_ROW_OUT P1 // P1.0~P1.3 行输出扫描电平 #define KEY_COL_IN P7 // P7.0~P7.3 列输入检测电平 sbit RS P2_4; sbit RW P2_5; sbit EN P2_6; #endif这段代码里有两个关键点。第一LCD1602的数据口占了P0全部8个引脚而STC8A8K的P0口在复位后是准双向口驱动能力有限带8根数据线时通常要在P0上加10K排阻上拉否则高电平可能拉不强显示的字符会出现时好时坏的现象。第二RS、RW、EN用sbit单独定义这在C51里比直接用P2_4可读性好很多后面写LCD驱动时也不会频繁去操作P2寄存器。2.2 LCD1602初始化为什么第一个命令要写两遍LCD1602内部有一套自己的复位流程上电后它需要时间完成内部状态准备。常见的资料里写“延时15ms后再发命令”实际很多简化的例程只延时几毫秒结果就是屏幕亮但不出字或者第一个字符偶尔丢。工程里的初始化代码一般把延时和命令写在同一个模块里典型流程如下。/* lcd1602.c —— 初始化与基础命令 */ void LCD_Init(void) { EN 0; delay_ms(15); // 上电稳定等待至少15ms LCD_WriteCmd(0x38); // 8位数据模式2行显示5x7点阵 delay_ms(5); LCD_WriteCmd(0x38); // 再写一次弥补内部复位未完成的情况 LCD_WriteCmd(0x0C); // 开显示关光标不闪烁 LCD_WriteCmd(0x01); // 清屏 delay_ms(2); // 清屏指令需要更长的执行时间 LCD_WriteCmd(0x06); // 写入后地址自动加一光标右移 }第一次0x38是给LCD1602确认总线模式的如果主控上电速度和LCD不一致第一次命令可能在LCD内部复位完成之前被丢弃所以很多工程会连续写两次0x38。0x0C是显示控制命令关掉光标和闪烁可以避免计算器界面出现一闪一闪的下划线。0x01清屏命令执行时间比较长延时2ms是在给LCD内部DDRAM清零留时间。LCD_WriteCmd本身通常也封装了等待忙标志或固定延时的逻辑。工程的驱动里会写一个LCD_WriteByte函数先把RW拉低表示写数据再在EN下降沿把数据锁存进去。这里我建议初版固定延时就好每发一条命令延时1ms以内CPU占用不高计算器界面本身刷新频率也只有几赫兹。2.3 字符显示函数与数字显示有了初始化下一步就是把数字和符号画到屏幕上。LCD1602内部DDRAM有80字节分两行第一行地址从0x00开始映射到屏幕位置0第二行从0x40开始。工程里一般会用LCD_SetCursor封装行和列到地址码的换算。/* lcd1602.c —— 光标定位与显示 */ void LCD_SetCursor(unsigned char row, unsigned char col) { unsigned char addr; if (row 0) addr 0x80 col; // 第一行首地址0x80 else addr 0xC0 col; // 第二行首地址0xC0 LCD_WriteCmd(addr); } void LCD_WriteChar(unsigned char ch) { LCD_WriteData(ch); } void LCD_WriteString(unsigned char *str) { while (*str) { LCD_WriteData(*str); } }第二行的地址写成0xC0而不是0x40是因为命令最高位是DDRAM地址位0x40这个偏移要在写入命令寄存器时加上0x80合起来就是0xC0。如果直接给LCD_WriteCmd传0x40屏幕会定位到第一行第64列那种不存在的位置表现就是字符乱跑。数字显示方面计算器的核心操作是把long类型的运算结果转成字符串。工程里常见做法直接调用sprintf一句LCD_WriteString(buf)就能显示完整数字。sprintf在C51里会占用一些code空间但对64KB Flash的STC8A8K来说这是值得的消费至少不用自己写负数处理逻辑。void LCD_DisplayNumber(long num) { char buf[12]; sprintf(buf, %ld, num); // 转成字符串含负号 LCD_WriteString(buf); }这段代码看起来简单但隐藏了一个容易忽略的点C51中printf和sprintf默认不支持长整型格式化除非在工程设置里开启long printf支持。Keil C51中需要在Options for Target的Library设置里选上“Use Long Long”或勾选相应库选项否则这里的%ld打出来的是一个空字符串或乱码。第一次用这个函数如果发现数字不显示先去检查编译器库配置。3. 矩阵键盘扫描与消抖STC8A8K的IO模式决定按键识别是否可靠3.1 行列反转法先锁定列再回头读行4x4键盘有16个按键却只占8根IO线原理是让按键跨接在行线和列线上。扫描时常用的写法是“行列反转法”先给P1的低四位输出低电平P7的低四位配成带上拉的输入如果有按键按下按下的那一列会被拉低于是先确认列号然后反过来让列输出低电平再读行就能确认行号。这样得到的行列组合唯一对应一个按键。工程的keypad.c里通常有一个核心扫描函数下面的写法是常见实践。/* keypad.c —— 返回按键编码0xFF表示无按键 */ unsigned char Key_Scan(void) { unsigned char col, row, key_code; KEY_ROW_OUT 0x0F; // P1低四位输出低高四位保持高 delay_us(5); // 等待电平稳定 col KEY_COL_IN 0x0F; // 读P7低四位被拉低的位就是列 if (col 0x0F) return 0xFF; // 没有按键 delay_ms(10); // 软件消抖绕过按键抖动期 col KEY_COL_IN 0x0F; // 再读一次确认不是抖动 if (col 0x0F) return 0xFF; KEY_COL_IN 0x0F; // 反转让列输出低 delay_us(5); row KEY_ROW_OUT 0x0F; // 读行被拉低的位就是行 key_code row | col; // 组合成8位编码 return key_code; }这个函数看起来简单但有两个值得注意的细节。第一读列之前要加一个几微秒的短延时STC8A8K的IO口在推挽输出和准双向输入之间切换时电平建立需要一点时间没有这个延时可能在快速按键时读到一个半高电平导致列号错乱。第二反转读行时KEY_COL_IN 0x0F这句是把P7低四位改成输出在同一个端口上从输入切到输出务必在切换后重新读P1而不是用之前缓存的col值直接算。按这个函数返回的编码还要通过一张码表映射成具体的ASCII字符或按键序号。工程里的码表通常按4x4键盘实物排列即上面一排1、2、3、下面一排4、5、6、-。3.2 消抖延时阻塞还是状态机解耦延时消抖在扫描函数里占了两个10ms对计算器这种低实时性需求来说是够用的但它有一个副作用延时期间CPU被卡住如果LCD刷新和键盘扫描跑在同一个主循环里按键时会看到屏幕闪烁或刷新顿一下。更稳妥的做法是把消抖逻辑做成状态机放在定时器中断里每5ms扫一次只在状态连续变化时更新键值。/* keypad.c —— 状态机消抖结构 */ typedef struct { unsigned char state; // 0:等待按下 1:按下确认 2:长按 unsigned char last_code; // 上一次有效键码 } KeyDebounce; KeyDebounce key; void Key_Update(unsigned char code) { if (code 0xFF) { key.state 0; // 无按键回到初始状态 return; } switch (key.state) { case 0: key.state 1; // 第一次检测到按键先不确认 break; case 1: if (code key.last_code) { key.state 2; Key_Confirm(code); // 连续两次读到相同键确认按下 } else { key.last_code code; } break; case 2: // 长按状态只处理一次等待释放 key.state 0; break; } }这个状态机的核心思想是连续两次采样都读到同一个键码才认为是有效按键中间的间隔由定时器中断保证稳定。相比延时消抖它的好处是主循环不用等待LCD刷新平滑而且天然支持“长按某键触发特殊功能”的扩展。状态机每增加一个状态占的RAM只有少量几个字节对STC8A8K的容量来说可以忽略。3.3 STC8A8K的IO模式寄存器四种模式选错就翻车STC8A8K的每个IO引脚都可以独立配置成四种模式准双向口、高阻输入、推挽输出、开漏输出。老8051只有准双向一种模式很多从AT89C52转过来的人容易忽略这个差异。计算器工程里正确的配置应该是矩阵键盘的行线设为推挽输出确保扫描时能快速拉低列线设为高阻输入且外部接上拉电阻或者直接设成准双向口靠内部上拉读取电平。/* io_config.c —— 初始化IO模式 */ void GPIO_Init(void) { // P1低四位推挽输出用于键盘行扫描 P1M1 ~0x0F; P1M0 | 0x0F; // P7低四位准双向口用于键盘列输入 P7M1 ~0x0F; P7M0 ~0x0F; // P0整组准双向口驱动LCD1602数据线 P0M1 0x00; P0M0 0x00; // P2口准双向口控制LCD的控制线 P2M1 0x00; P2M0 0x00; }这里P1M1和P1M0是端口模式的高位和低位寄存器两个寄存器的对应位组合决定一个引脚的模式00是准双向01是高阻输入10是推挽输出11是开漏。上面代码里P1M1清零、P1M0低四位写1组合成10即推挽输出。P7M1、P7M0都清零保持默认的准双向口。如果某个开发板的列线没有接外部上拉电阻把列线设成高阻输入就会导致按键永远读不到低电平表现是“按什么键都没反应”这时改成准双向口或者加上拉电阻就能解决。这个细节在开发文档里通常会有一句“列线需外部上拉”的提示但拿到板子直接烧录的人往往没注意。4. 表达式求值与LCD刷新让计算器记住先乘除后加减4.1 按键映射与输入缓冲区计算器按键按下以后先转成对应的字符再存进一个表达式缓冲区。工程里keypad.h一般会导出Key_GetValue()接口内部维护一张4x4映射表把行列编码转换成数字、运算符、等号和清除键。/* keymap.c —— 按键布局映射 */ const unsigned char code KEY_MAP[4][4] { {1, 2, 3, }, {4, 5, 6, -}, {7, 8, 9, *}, {C, 0, , /} }; unsigned char Key_GetValue(unsigned char code) { unsigned char row, col; // 把行列组合拆开用于查表 row (code 0xF0) 4; // 高四位是行编码 col code 0x0F; // 低四位是列编码 return KEY_MAP[row][col]; }这套映射的排列顺序和常见4x4键盘模块一致左下角是0左上角是1到9最右边竖排是四个运算符。资源里的开发文档如果附了按键布局图通常就是这个排列少数版本会把加减乘除放在最上边一行查表时要和实物对齐。4.2 中缀转后缀优先级比较的“大于等于”是关键四则运算的难点不在计算而在让单片机理解“12*3”应该先算乘法。大多数工程直接用中缀表达式求值借助两个栈一个存操作数一个存运算符。逐字符读入表达式时遇到数字就压进操作数栈遇到运算符就与栈顶运算符比较优先级。下面的代码是运算符优先级比较的核心逻辑。/* calc.c —— 运算符优先级判断 */ unsigned char Op_Priority(unsigned char op) { switch (op) { case *: case /: return 2; // 乘除优先级高 case : case -: return 1; // 加减优先级低 default: return 0; } }真正决定计算顺序的是主循环里那条弹栈条件只要栈顶运算符优先级不低于当前运算符就先把栈顶运算符弹出来算一次再把当前运算符压栈。用“”而不是“”是为了处理从左到右的结合规则。例如输入“8-3-2”读到第二个减号时它和栈顶减号优先级相同必须先把前一个减法做掉否则会先算3-2再把8减去1得到错误答案7。/* calc.c —— 主循环中的运算符处理片段 */ while (op_depth 0 Op_Priority(op_stack[op_depth - 1]) Op_Priority(ch)) { // 弹出栈顶运算符和两个操作数执行运算 right Pop_Operand(); // 先弹右操作数 left Pop_Operand(); // 再弹左操作数 result ApplyOp(left, right, Pop_Operator()); Push_Operand(result); // 中间结果压回操作数栈 } Push_Operator(ch);这里的弹栈顺序很重要。表达式“8-3”对应的入栈顺序是8、3减法运算必须用左操作数8减右操作数3所以要先弹右操作数再弹左操作数。很多初版代码把顺序写反结果所有减法和除法都算错。ApplyOp内部就是对两个long型操作数做加减乘除除法要额外判断右操作数是否为零否则芯片进入除零异常程序飞到异常处理里出不来。4.3 C51内存模型下整型运算的取值范围STC8A8K是8051内核C51编译器默认的int是16位范围只有-32768到32767。如果操作数和结果都用int保存输入“999*99”直接溢出成负数。工程里为了避免这个问题会把操作数和结果统一定义为long类型32位范围足够容纳六位数以内的四则结果。下面是一个放溢出判断的显示片段。void Show_Result(long result) { char buf[16]; if (result 99999999L || result -99999999L) { LCD_WriteString(OVERFLOW!); // 超出显示范围 return; } sprintf(buf, %ld, result); LCD_WriteString(buf); }LIMIT值取±99999999是因为LCD1602一行只有16个字符总位数太多也显示不下。注意C51里long字面量要加后缀L否则编译器可能把它当int处理高位的数值被截断。这里的溢出标志位也可以在计算过程中提前置位不用等到全部算完再检查工程里通常会在ApplyOp里返回一个状态结构体把运算结果和是否溢出分开存。4.4 两行屏幕布局算式一行结果一行LCD1602只有两行每行16字符所以显示策略必须是“上排输入表达式下排输出结果”。工程里的刷新函数一般长这样。/* display.c —— 刷新整屏 */ void LCD_Refresh(void) { LCD_SetCursor(0, 0); LCD_WriteString(expr_buf); // 第一行显示正在输入的表达式 LCD_SetCursor(1, 0); LCD_WriteString(ans_buf); // 第二行显示最后结果 }如果expression超过16个字符直接整行写完会把尾部截断但前面输入的关键运算符可能被挤掉。工程里通常对buf做滚动处理保留最后输入的一段表达式具体做法是把字符串指针每次偏移一个字符直到剩下长度不超过15。比如用户输入“123456782”屏幕只显示“56782”而不是把最前面的数字切掉一半。这个细节虽然小但直接影响使用体验资源里的源码如果做了滚动一般会在display.c顶部留一个#define EXPR_MAX_LEN 16方便调整缓冲长度。5. STC8A8K下载与调试避坑最容易翻车的五件事5.1 下载失败提示“连接失败请重新上电”现象烧录软件发出下载指令后进度条一直停在“正在检测目标单片机”然后提示连接失败反复重试也不行。原因STC8A8K进入下载模式靠的是冷启动时序芯片必须在断电状态下等待下载指令然后重新上电引导程序才能接管串口。很多开发板的串口芯片已经占用了USB转串口如果下载软件里选择的COM口号和实际波特率不对也会卡在检测阶段。解决先点击下载按钮再给目标板重新上电也就是“先点下载再上电”的冷启动操作。确认下载软件里芯片型号选择为STC8A8K系列频率设置不要超过芯片支持上限。另外一些板载串口电路会影响下载脚的初始电平必要时按住板上的下载按键不放再上电。5.2 LCD1602背光亮但显示全是一排方块现象屏幕有背光第一行16个格子全部是实心黑块发任何命令都改不了。原因两个典型情况一是LCD1602的V0对比度引脚悬空或电压不合适二是上电后主控没等LCD内部复位完成就发了初始化命令。资源文档如果提到“调节电位器”多半是这类问题。解决V0脚接一个10K电位器中间抽头接地或接负压先把手感调到中间位置再观察。代码里把初始化的延时从几毫秒改成15ms以上并且在0x38命令后加5ms延时。这两个动作能解决九成以上的方块屏问题。5.3 按一次键触发两次或三次运算现象只按了一下数字键屏幕上出现两个相同的字符或者按一次等于号结果执行了两次计算。原因扫描函数检测到按键已经按下主循环处理完一次后没有等按键释放下一次循环又扫到同一个键重复处理。延时消抖只处理了按下瞬间的抖动没处理“持续按住”的状态。解决在按键处理逻辑里增加一个“松手检测”确认一次按键后等待Key_Scan()返回0xFF再接受下一个按键。用一个flag记住上次生效的键码主循环里只有键码变化时才触发计算器状态更新。if (key_code ! last_key_code key_code ! 0xFF) { Calculator_Input(key_code); // 只在键值变化时处理 } last_key_code key_code;这个写法是把“边沿触发”而不是“电平触发”的逻辑落实到位计算器界面不会因为手指按得稍微久一点就连蹦两个数字。5.4 程序烧进去能跑但一运行就复位或乱跳现象下载成功上电后LCD有时能亮有时不能亮或者程序跑到一半复位单步调试时又一切正常。原因看门狗复位。STC系列默认看门狗可能处于关闭状态但有些开发板例程里会在初始化时开启WDT而计算器主循环如果大量使用delay_ms阻塞忘了喂狗就会周期性复位。另一个原因是Keil工程设置的存储器模式与实际芯片不符导致变量分配到不存在的地址空间。解决检查工程代码里有没有操作WDT_CONTR寄存器的部分确认没有开启看门狗或者在主循环末尾定时喂狗。工程设置里Model选择Small模式RAM空间映射为IDATA对于STC8A8K的8KB SRAM来说Small和Compact都能满足但如果选成Large且实际配置错误编译能过运行会乱。5.5 负数结果显示成乱码或空白现象算“3-8”得到的结果不是-5而是屏幕上出现一个奇怪的符号或者整行空白。原因sprintf的%ld格式没有被正确支持。C51编译器的库默认只支持int和uchar的printf格式化对于long类型如果工程没有开启长久格式支持编译器会在底层调用一个不支持long的辅助函数输出结果就是空字符串。解决在Keil C51的Options for Target里进入Library页面把Printf的格式勾选为“Long Long”或“Advanced”确保使用支持long的库版本。如果不想依赖编译器库自己写一个数字转字符串函数先判断符号位再对绝对值逐位取模也能绕开这个坑。工程里如果给出了自定义的itoa函数建议优先用那个版本可控性更强。6. 让这个计算器更顺手定点小数与定时器扫描拿到这份资源把基础计算器跑通之后改造方向通常有三个支持小数、用定时器中断接管键盘扫描、以及做好验证流程。先说小数。整数四则运算最大的痛点是3除以2会被截断成1工程里的整数版一般直接丢弃余数想保留小数精度又不想引入浮点库的话可以用定点数方案把所有输入数值放大100倍保存显示的时候再除以100。比如输入1.23内部存123执行1.23*2得到246显示时把246拆成整数部分2和小数部分46输出“2.46”。这个方案避开了C51浮点printf占几KB code空间的问题而且结果精度对小计算器足够。第二个改造是把键盘扫描从主循环挪进Timer0中断。用一个10ms周期的定时器中断在中断服务函数里调用Key_Scan和消抖状态机把有效按键放入一个环形缓冲区主循环只负责取数据并更新LCD状态。改造后按键扫描和LCD刷新互不阻塞LCD不会再闪屏对后续加功能也更友好。中断服务函数里要注意变量使用volatile修饰避免编译器优化导致取到旧值。验证流程我个人习惯按三步走。第一步上电后LCD应该稳定显示光标和初始化的0不存在乱码或方块第二步逐个按键按压屏幕上应该出现与按键一一对应的字符重复按多次不能出现连击第三步算几组有代表性的式子比如32411验证优先级、8-3-23验证左结合、9999998901验证long范围、3-8-5验证负数。这三步过完这个基于STC8A8K的简易计算器才敢说真正可以交付给用户。做这类单片机工程做得多了我养成一个习惯每次拿到新的源码包第一件事不是编译烧录而是先打开config.h核对引脚分配再翻开发文档看芯片型号和时钟频率这两个参数确认无误才动调试器。这个习惯让我少踩了很多“代码没问题但硬件对不上”的坑希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑