简介:本资源是一套基于STM32F103ZET6单片机实现的嵌入式计算器工程源码,面向嵌入式初学者与课程设计实践者,解决基础人机交互与算术运算逻辑在裸机环境下的落地问题。项目适配正点原子战舰开发板及2.5寸LCD显示屏,完整实现数字输入、四则运算、结果实时显示等核心功能,配套效果视频直观展示运行流程。压缩包共177个文件,含32个C源文件(如jisuanqi.c、paint.c)、25个头文件(.h)、33个编译中间文件(.o/.d)及Keil工程配置文件(.uvprojx、.uvoptx)、可执行镜像(.hex)和调试配置(.dbgconf),总大小4.27MB,结构清晰,便于理解STM32固件库开发全流程。已有4857人学习下载,提供可直接编译烧录的完整工程,涵盖硬件驱动、GUI绘制、按键扫描与表达式解析等关键模块,是掌握STM32外设协同与小型应用系统开发的实用参考。 最近整理了一份基于STM32设计的计算器工程源码,支持加、减、乘、除四则基本运算,配套矩阵按键输入和OLED显示,压缩包里有完整的Keil工程和烧录说明。这次就顺着这个项目,把从硬件选型、按键扫描、运算逻辑到调试踩坑的整个过程拆开讲讲。无论你是刚学完STM32基础想找个练手项目,还是想搞懂计算器这种实时交互系统怎么设计,这篇都可以直接照着做。
很多人会觉得计算器太简单,无非是读取按键、算个结果、显示出来。但真正实现起来,里面藏着不少坑:按键抖动怎么消,连续运算怎么写,除数为零怎么处理,负数显示要不要支持,浮点结果怎么格式化。这些细节恰恰是嵌入式开发的典型问题,做完这个项目,对中断、定时器、状态机、外设驱动这些概念的理解都会上个台阶。
1. 内容整体设计与思路拆解
1.1 为什么选STM32实现计算器
做计算器,用51单片机也能做,用树莓派也能做,但STM32是嵌入式领域最主流的MCU,选它有几个实在的理由。
首先是外设资源够用。计算器需要按键输入和显示输出,STM32的GPIO、定时器、I2C/SPI接口都绰绰有余。如果用带触摸屏的方案,甚至可以用FSMC接口驱动TFT,为后续扩展留足余量。其次是开发环境成熟,Keil MDK、STM32CubeIDE、标准外设库、HAL库、LL库,随便选,资料多,遇到问题随便搜都有答案。第三是性能冗余,STM32F103系列主频72MHz,做计算器连1%的性能都用不到,但这正好意味着你能在上面跑更复杂的算法,比如中缀表达式解析、带优先级运算、甚至科学计算器功能,而不是被性能卡住。
这个项目用的主控是STM32F103C8T6,也就是俗称的“蓝丸”核心板上的那颗芯片。72MHz主频、64KB Flash、20KB RAM,对于这套计算器逻辑绰绰有余。你完全可以用别的型号,比如STM32F407或者G030,只要引脚配置对应改一下就行。核心逻辑是平台无关的,这也是这个项目源码值得参考的原因。
1.2 基本运算功能需求梳理
所谓“基本运算”,我们定义为四则运算:加(+)、减(-)、乘()、除(/)。在这个基础上,还需要配套的操作符优先级判断,也就是乘除优先于加减。例如输入"2+34",结果应该是14,而不是20。
具体功能需求列表如下:
- 支持整数和浮点数输入,小数点在显示区可以切换
- 支持正负数显示,负数用"-"前缀标识
- 支持连续运算,例如"12+34*5-"这种边输入边显示的过程
- 支持等号后继续运算,按下等号出结果后,再按数字键会以当前结果为起点继续输入
- 支持清除(C)、退格(Backspace)等基本编辑操作
- 除数为0时显示"Error",并允许用户清除后继续操作
- 结果显示位数要截断到合理范围,不能出现"1.9999999"这种浮点误差
这些需求看似简单,但每一条都对应一个实现细节。比如连续运算,你既要保存当前结果,又要保留待运算的运算符,还要处理好“等号后继续输入”的状态切换。这就是典型的有限状态机应用场景。
1.3 系统架构与状态机设计
从软件架构上,我把整个系统分成三层:输入层、处理层、显示层。
输入层负责按键扫描、去抖、键值映射,最终产出一个“按键事件”。处理层是一个核心状态机,维护当前输入值、操作数栈、运算符,以及各种状态标志。显示层将处理层的结果格式化成字符串,刷到屏幕上。
核心状态机的状态划分如下:
| 状态 | 含义 | 触发条件 | 动作 |
|---|---|---|---|
| INPUT_A | 输入第一个操作数 | 开机或清除后 | 接收数字键、小数点 |
| OP_WAIT | 等待选择运算符 | 操作数输入完成 | 接收+ - * /,必要时计算 |
| INPUT_B | 输入第二个操作数 | 已选运算符 | 接收数字键、小数点 |
| CALC_DONE | 运算完成 | 按下等号 | 显示结果,支持继续运算 |
| ERROR | 错误状态 | 除数为0或数据溢出 | 显示Error,仅清除有效 |
举个例子:按下"12+"时,状态从INPUT_A切到OP_WAIT,同时把12暂存。按下"34"时,状态切到INPUT_B,显示区变成34。再按""时,处理层会先完成12+34的加法,得到46,然后被当作新运算符,状态回到OP_WAIT,显示区显示46。这种即时计算方式叫“逐步计算法”,适合基本四则运算,不需要复杂的表达式转后缀。
如果你想实现带括号和优先级更高的计算器,那就要用中缀转后缀表达式(逆波兰式),再配合双栈计算。但这次是基本运算,逐步计算法代码量小、状态清晰,非常适合教学和二次开发。
2. 硬件电路设计与按键方案
2.1 主控最小系统与引脚分配
硬件上我直接用了市面上常见的STM32F103C8T6最小系统板,板载晶振、复位电路、USB转串口,省去自己画底板的麻烦。但如果你要自己做PCB,最小系统必须包含:8MHz主晶振及两个20pF负载电容、32.768kHz低速晶振(若使用RTC)、上电复位电路(10kΩ上拉 + 100nF电容)、VTref/VDDA/VSSA等去耦电容、BOOT0和BOOT1配置。
引脚分配上,按键矩阵用PA0-PA3做行,PB0-PB3做列;OLED显示屏用I2C1,PA5做SCL、PA6做SDA。串口使用PA9、PA10,方便打印调试信息。每个按键的GPIO都配置为上拉输入,列线配置为推挽输出,扫描时逐列拉低。
有一件事必须注意:STM32的PB3、PB4引脚默认复用为JTAG调试口(JTDO和NJTRST),如果你们把矩阵键盘配到这两个脚上,会发现在线调试正常,但脱离调试器后按键不工作。解决方法是禁用JTAG,仅保留SWD——调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)。这个坑我在第一个版本踩过,花了一个晚上才定位到。
2.2 矩阵键盘 vs 独立按键
为什么选择矩阵键盘而不是独立按键?原因很简单:节省GPIO。一个4x4矩阵键盘用8个GPIO就能支持16个按键,而独立按键要16个GPIO。STM32F103C8T6虽然有37个GPIO,但还要留给出显示和串口,独立按键资源紧张。矩阵键盘的原理是行列分时导通:每列输出低电平,其余列输出高电平,然后读取所有行引脚电平,若某行读到低,说明该行与当前列交叉的按键被按下。
实际按键布局我用了16个键,分别是:
| 按键 | 功能 | 按键 | 功能 |
|---|---|---|---|
| 0-9 | 数字 | + | 加 |
| . | 小数点 | - | 减 |
| = | 计算 | * | 乘 |
| C | 清除 | / | 除 |
| Back | 退格 | +/- | 负号切换 |
如果你不需要负号切换,可以把4x4改成3x4矩阵,节省2个IO给其他用途。拓展一下,想要更多按键(比如三角函数、括号、pi),可以换成7x11或者用I2C扩展芯片PCF8574,一块芯片就能扩8个IO。
2.3 显示方案怎么选
显示方案常见的有三种:数码管、OLED、LCD1602/2004。数码管显示数字美观但无法显示字符"Error";LCD1602可以显示两行16个字符,价格便宜,但模块体积大、接线多(除I2C转接板以外);OLED(0.96英寸,SSD1306驱动)体积小、支持I2C接口、显示自由,很适合计算器这种信息量不大的场景。
我最终用的0.96寸OLED,分辨率128x64。一屏能显示8行16个英文字符,或者4行三号大字。为了显示效果,我把结果区放在底部一行,用大字体显示;把表达式历史或操作提示放在上部。OLED的控制核心是SSD1306控制器,通过I2C发送命令和数据。需要熟悉的关键命令有:显示开关(0xAE/0xAF)、页地址设置(0xB0-B7)、列地址设置(0x00-0x0F低四位 + 0x10-0x1F高四位)、对比度设置(0x81)。如果使用HAL库,可以简化成两个函数:OLED_WriteCommand和OLED_WriteData,然后用DMA或阻塞方式发送。
有一点值得提醒:很多OLED模块的I2C地址是0x3C,但也有0x3D的。驱动不了时先检查地址,这个比排查代码快得多。还有一种方式是使用4线SPI,速度快一些,但对于刷新一帧只有几十字节的场景,I2C完全够用。
2.4 电路连接与电源注意事项
连接关系非常简单,但有几个电源和信号完整性的坑。
按键矩阵的行线分别接PA0-PA3,列线接PB0-PB3。所有按键的一端接行线,另一端接列线,通过行列交叉点连通。这里要注意的是,行线必须配置为上拉输入模式,因为扫描列时,未被选中的列输出高电平,被选中的列输出低电平,行线通过按键与列线相连。若按键按下,行线被拉低,单片机读到低电平。如果没有内部上拉,悬空状态下电平不确定,按键误触发分分钟教你做人。
OLED的SDA和SCL必须接上拉电阻。很多模块本身带了上拉,如果没带,在I2C总线上加两个4.7kΩ到3.3V。电源方面,STM32和OLED都是3.3V供电,按键不需要额外电源。注意不要让5V进入I2C引脚,否则时间长了会损坏MCU。
静电问题也要重视。在干燥环境中,手指触摸按键容易产生静电,虽然有内部保护二极管,但反复放电也可能导致复位或数据错误。可以在每个按键靠近MCU的一端加一个100pF电容到地,起到简单的ESD滤波作用。实际测试中,加与不加在普通环境下区别不明显,但在工业环境就会体现差异。
3. 软件核心模块实现
3.1 按键扫描与消抖处理
按键扫描的基本思路是每10ms执行一次,每次驱动一列输出低电平,然后读取4行电平,翻到下一条列线继续。一个完整周期扫描4列需要40ms,对应25Hz的扫描率,人手的按键持续时间通常超过100ms,不会丢键。
消抖处理有两种做法:硬件消抖(RC滤波)和软件消抖。我采用软件消抖,其实也是滤波思想:连续两次扫描得到的键值一致,才认为按键有效。具体状态如下:
uint8_t KeyScan(void) { uint8_t key = 0; Key_CtrlColumn(LOW, COL0); // 拉低第0列 Key_DelayUs(10); // 等待电平稳定 if (Key_ReadRow() & ROW0_MASK) key = '1'; // 读取行 if (Key_ReadRow() & ROW1_MASK) key = '4'; //... Key_SetAllColumns(HIGH); // 恢复所有列 // 依次扫描其他列 return key; }但更实用的消抖方式是使用一个“状态机+定时器”的结构。我定义了一个key_debounce_driver,每个按键有释放态、按下确认态、长按态。对于计算器这种短促输入,只需要“按下确认”和“释放”两种事件。每次扫描到按下时,记录时间戳,如果同一个键持续被扫描到超过20ms,就认为是一次有效按键,然后把键值放入一个环形缓冲区。这个缓冲区的存在能有效防止按键期间CPU正在做耗时运算(比如浮点格式化)导致丢键。
3.2 核心状态机与运算逻辑
整个运算逻辑可以写成一个大函数ProcessKey(uint8_t key),内部根据当前状态决定处理方式。这是我最想分享的部分,因为很多初学者会把运算过程写成“按一个键处理一锤子”,结果按等号时不知道该怎么把结果带出来。
我把关键状态机用结构体表示:
typedef struct { char display[16]; // 显示缓冲区 double accumulator; // 累加器,存第一个操作数或中间结果 double current_input; // 当前输入的操作数 char op; // 当前运算符 uint8_t state; // 状态机状态 uint8_t decimal_place; // 小数位计数 } Calc_t; Calc_t calc;加法减法很容易,但乘除的过程必须注意“运算符优先级”在逐步计算中的正确处理。比如输入"2+34"时,按照逐步计算法,按下+先不做加法,按下3后,再按时,系统会判断当前运算符是+,而新运算符优先级更高,于是不能立刻计算2+3,必须先算34?不对,这种思路在逐步计算法里反而复杂化了。
更简单的做法是:不处理优先级,直接从左到右计算。这样"2+3*4"会得到20。想要得到14,就必须采用“待定运算符”的机制:当新运算符优先级大于当前运算符时,不计算,仅保存当前运算符和新运算符,等后续低优先级运算符进来时再统一计算。但这样会引入更复杂的控制逻辑。
这里我选择一种折中方案:使用“双变量 + 单运算符”的传统简单计算器逻辑。它不处理同级运算的优先级问题,但可以支持连续四则运算。例如:
- 输入 "12+34",当前是INPUT_B,显示34。
- 再按 "",因为当前运算符是+,先把12+34算出来得到46,显示46,并保存运算符。
- 再输入 "5",显示5。
- 按 "=",计算46*5=230,显示230。
这种逻辑适合学习,也基本符合普通用户的使用预期。如果你希望严格按数学优先级,那需要像前面说的一样实现表达式解析。源码包我注释里也加了宏定义,可以通过开关切换“严格优先级模式”,但严格优先级模式会引入中缀转后缀逻辑,代码量多出200多行。建议先从简单模式跑通,再逐步加深。
等号处理有一个容易忽略的点:按等号时如果当前没有第二个操作数(例如用户按了"12+"后直接按=),应该将第二个操作数视为与第一个相同,执行"12+12=24",还是忽略?我采用的是忽略,直接不响应等号。这更符合直觉。
3.3 浮点显示与精度控制
基本运算中,除法会产生小数。例如10/3=3.3333333333333335。直接格式化到串口会显示一长串,非常难看。因此显示模块需要把double型结果按最多8位有效数字显示,并去掉尾部多余的0。
实现方式:
snprintf(buffer, sizeof(buffer), "%g", value);如果value是整数,%g会输出整数格式;如果是小数,输出科学计数法(当数值过大或过小时)。为了避免科学计数法,可以用自定义格式化函数,遍历小数点后的位数,当超过6位时截断。
对于整数结果,也要防止输出"12.000000"。我的做法是:先检查value与round(value)的差值是否小于1e-6,如果是,直接按整数显示,否则按浮点显示。
这里还要注意float和double的选择。STM32F103做乘法时使用浮点运算是通过软件模拟的,性能不高,但只要你不做高频率计算,10毫秒内完成一次乘除法没压力。如果你改用STM32F4系列,硬件FPU会让浮点运算快十几倍。不过计算器场景,F1足够。
3.4 OLED显示驱动与刷新策略
显示刷新要避免“闪屏”。如果你的OLED驱动是整屏全刷,每秒刷新几十次,会出现闪烁。正确做法是用“分区刷新”或“帧缓冲”。我采用128字节的RAM缓存,先在内存里画好一帧,然后调用一次OLED_Clear,再整屏写入。OLED本身有显存,不需要外置帧缓冲,但为了简化绘制,我维护了一个屏幕缓冲区并映射到GPIO。
实际驱动SSD1306的I2C写一个像素的方法是设置页地址(0~7)和列地址(0~127)。我封装的OLED_DrawChar和OLED_DrawString函数,会从字模数组中取模数据,填充到缓冲区。用到的大字模是16x32点阵,适合显示大号数字。
由于计算器需要频繁更新数字,我采用“局部刷新”策略:只在结果区域的矩形范围内刷新。比如结果区是屏幕的第5行到第7行,页地址0x05~0x07,列地址0x20~0x5F。每次刷新只写这3页数据,速度显著提升,肉眼看不到闪烁。
代码实现中需要注意I2C通信的超时处理。如果总线挂死,read/ write函数会一直卡住。用HAL库时,将I2C通信的超时设置为10ms,超过就重启I2C外设。
4. 实操过程与核心环节实现
4.1 源码目录结构与工程配置
拿到工程源码后,先看清目录结构,大致如下:
STM32_Calculator/ |-- Core/ | |-- Inc/ // 头文件 | |-- Src/ // 主函数、中断 |-- Drivers/ | |-- BSP/ // OLED、MatrixKey、Systick | |-- CMSIS/ |-- MDK-ARM/ // Keil工程文件 |-- Doc/ // 说明文档工程基于标准外设库(Standard Peripheral Library)3.5版本,如果你不习惯,可以改到HAL库。源码里所有的驱动都放在BSP层,硬件相关的实现与上层逻辑隔离。这样换硬件平台时,只需要改BSP,不需要动核心计算逻辑。
在Keil MDK中打开工程前,先确认自己安装了对应芯片的Device Family Pack,比如Keil.STM32F1xx_DFP。工程选项里,Target标签页的晶振频率要填8.0MHz,后面代码里延时和波特率都依赖这个配置。如果是用内部时钟(HSI),需要修改SystemInit中的宏,这点容易忽略。
还有一点,如果使用V6编译器,标准外设库的很多代码会报warning甚至error。建议选择AC5编译器,兼容性最好。如果你坚持用AC6,需要关闭“非标准语法”等编译选项,工程里我已经配置好了,但其他项目不一定。
4.2 编译环境与烧录流程
Windows下推荐直接装Keil MDK5,安装包比较庞大,但一次性配置好。安装完成后,需要安装STM32F1系列支持包。打开Pack Installer,搜索F1,点击Install即可。
打开工程后,先编译。如果一切正常,会在MDK-ARM目录下生成Calculator.hex。如果没有生成,检查Output标签页是否勾选了Create HEX File。
烧录有两种方式:ST-Link和USB转串口ISP。ST-Link需要接线:SWDIO接PA13,SWCLK接PA14,GND接GND,3.3V接电源。然后打开ST-Link Utility,连接后点击Programming按钮,选择刚编译的hex文件,地址默认0x08000000。烧录成功后,拔掉ST-Link,按一下复位按键,计算器就开始运行。
如果你用ST-Link仿真,还可以在线查看calc结构体的值,跟踪状态机的变化,这是调试逻辑问题的强大利器。我强烈建议在开发过程中使用仿真,而不是盲目烧录看显示结果。
4.3 核心代码详解:按键事件与计算流程
下面截取一段核心的ProcessKey处理代码,方便大家理解整体流程:
void ProcessKey(uint8_t key) { if (key >= '0' && key <= '9') { if (calc.state == CALC_DONE) { // 等号后按数字,重新开始新运算 ResetCalc(); calc.state = INPUT_A; } // 追加数字到当前输入 AppendDigit(key - '0'); UpdateDisplay(); } else if (key == '+' || key == '-' || key == '*' || key == '/') { if (calc.state == ERROR) return; if (calc.state == OP_WAIT) { // 当前已有一个运算符,替换之 calc.op = key; } else { // 先执行上一次运算 CalcResult(); calc.op = key; calc.state = OP_WAIT; } // 清空当前输入,准备第二个操作数 calc.current_input = 0; calc.decimal_place = 0; UpdateDisplay(); } // 其他按键处理类似 }AppendDigit函数负责把数字追加到current_input上,同时处理小数位:
void AppendDigit(uint8_t digit) { if (calc.decimal_place == 0) { calc.current_input = calc.current_input * 10 + digit; } else { double factor = pow(10, calc.decimal_place); calc.current_input += digit / factor; calc.decimal_place++; } }CalcResult函数根据运算符执行运算:
void CalcResult(void) { switch(calc.op) { case '+': calc.accumulator += calc.current_input; break; case '-': calc.accumulator -= calc.current_input; break; case '*': calc.accumulator *= calc.current_input; break; case '/': if (calc.current_input == 0) { calc.state = ERROR; SetDisplayString("Error"); return; } calc.accumulator /= calc.current_input; break; default: break; } // 结果保存到累加器 calc.current_input = calc.accumulator; FormatResult(calc.accumulator, calc.display); }这里有个关键点:计算完成后把累积器值赋给current_input,这样“按等号后再按数字键”就能从结果继续输入。如果不这样做,新输入会把结果清掉。
4.4 仿真与验证用例
编译烧录后,建议按下面的测试用例逐一验证:
| 操作序列 | 期望显示 | 说明 |
|---|---|---|
| 12 + 34 = | 46 | 简单加法 |
| 12 + 34 * 5 = | 230或(严格优先级170) | 取决于模式 |
| 100 / 7 = | 14.2857 | 浮点除法截断 |
| 1 / 0 = | Error | 除数为0 |
| 3 . 14 = | 3.14 | 小数输入 |
| 5 + = | 5 | 等号无操作 |
| 12 + 34 C | 0 | 清除 |
| 8 - 2 = 6 + 3 = | 9 | 等号后连续运算 |
注意,严格优先级模式需要打开源码中的USE_PRECEDENCE宏,并重新编译。两种模式在源码包中都预置了测试用例。
仿真调试时,建议在ProcessKey函数设置断点,观察每一次按键后calc结构体的状态变化。逐个比对状态字段是否按预期跳转。如果发现state在某个按键后跳到了意外值,通常是某个分支缺少else,或者未在分支末尾更新state。这种问题用仿真很容易定位。
5. 常见问题与排查技巧实录
5.1 按键失灵或误触发的处理
按键失灵最常见的原因是引脚配置错误。检查GPIO初始化代码,确保行线是上拉输入,列线是推挽输出。其次要检查扫描函数中列电平的翻转顺序,例如扫描完第0列后,是否把第0列恢复高电平,再拉低第1列。否则两个列同时为低,按键会发生串键。
误触发多半是电平不稳定,或者扫描间隔太短。把扫描周期改为10ms,并在扫描到按键后延时20ms再确认一次,左右两边的抖动都能滤掉。如果在按键扫描期间发生串口中断,中断里如果执行了延时函数,也会导致扫描周期不固定,可以在进入扫描前关中断,扫描完再开。
5.2 显示乱码、花屏的排查
OLED显示乱码通常有两种原因:I2C地址错误或SCL/SDA接反。先检查代码中的OLED_ADDR是否等于模块实际地址。然后用逻辑分析仪抓I2C波形,看设备地址后是否有ACK。如果没有ACK,说明模块地址不对或外部上拉缺失。
还有一种情况是字模取模方式不匹配。我用的字模是纵向取模,高位在前,如果你换了一个字模软件,取模方式可能变成了横向取模,就会显示乱码。解决方法是统一使用Standard模式,纵向取模,8位为一行,宽度倍数为16。计算器数字使用的字模我放在了oled_font.c中,可以直接复用。
5.3 运算结果不对的常见原因
排除硬件问题后,运算结果错误一般出在状态解析上。例如,输入"10+20="得到30,但输入"10+="却得到20,那是因为执行运算时把current_input当成了0,但等号处理逻辑里没有初始化current_input。正确的思路是按下等号时,如果当前没有第二操作数,应判断为无效操作,或者复用当前显示结果。
另外,计算中途更改运算符也要注意。比如输入"10+",又按下"",用户期望是"10",而不是"10+*"。我在处理中把新运算符直接赋给calc.op,同时保持accumulator不变,这样显示区不会出现多余的加号。如果此时显示逻辑仍然把旧运算符打印出来,用户会困惑。建议显示区只显示当前部分,或者显示"表达式提示行"。
浮点数精度也是常见坑。比如"0.1+0.2"可能得到0.30000000000000004。我在FormatResult函数里对结果做了近似处理:如果误差小于1e-6,调整到合法小数位。具体做法是将结果加上0.0000005后取小数点后6位,再去掉尾部0。这个补丁能消除绝大部分浮点误差问题。
5.4 扩展建议:升级为科学计算器
运行起来之后,可以往这几个方向扩展:
- 增加括号和复杂表达式解析:使用中缀表达式转后缀表达式,实现带优先级的完整计算
- 增加32位浮点或64位浮点模式切换,甚至实现大数运算(用数组模拟十进制数)
- 增加历史记录功能:用EEPROM或Flash存储最近20条运算记录
- 增加键盘背光或声音反馈:用无源蜂鸣器播放按键音,效果会好很多
- 更换更大屏幕,比如1.3寸OLED或2.4寸TFT,显示更多信息
- 增加低功耗模式:在无操作一段时间后进入STOP模式,按键唤醒
源码中有一个扩展接口:Calc_ExecuteWithExpression(char* expr),预留了字符串表达式运算入口。如果后续接上位机或蓝牙,可以通过串口直接把表达式发过来计算,而不需要按键逐个输入。这个接口目前没有完全实现,但给你留好了位置。
6. 源码使用与后续学习建议
如果你下载了这个工程源码,建议不要只停留在烧录成功。把源代码完整读一遍,特别是BSP驱动的写法,然后尝试自己重写一遍Calc逻辑。重写时可以先不接OLED和矩阵键盘,用串口调试助手输入按键字符,观察计算器逻辑是否正确。这个“无硬件调试法”能省下很多焊接和接线的时间。
我实际测试时,用串口发送字符 '1','2','+','3','4','=' 就能得到计算结果。如果你也想用串口调试,只需要在串口中断回调里把接收到的字符代换成ProcessKey即可。
最后说一个个人经验:做计算器这类交互系统,工程上的难点不是计算本身,而是输入输出时序的配合。按键扫描、显示刷新、运算逻辑三者的调度关系,远比算法本身有趣。你把这三个模块捋顺之后,再做菜单系统、波形显示、甚至简单游戏,都会顺手很多。
这套源码里的BSP驱动是通用的,矩阵键盘扫描和OLED驱动copy到其他项目也能直接用。用STM32做项目,最重要的就是攒一套自己的底层驱动库,以后换项目就是拼积木。计算器这个项目正好帮你把这套积木的首批组件打磨出来。
本文还有配套的精品资源,点击获取