一个简易计算器的实现
Author: bripengandre
相关源码见链接:http://download.youkuaiyun.com/source/982727
摘要:嵌入式系统,以其占用资源少、专用性强,在汽车电子、航空和工控领域得到了广泛地应用。本文为嵌入式系统课程设计的报告,文中给出了一个简易计算器的设计与实现过程。本文先通过需求分析,介绍该计算器要实现的功能;接着通过概要设计,给出了该计算器的整体框架;然后通过详细设计及实现,讲述该计算器的具体实现;最后通过测试结果及分析,给出了对该计算器的评价及改进意见。
关键词:嵌入式;计算器;栈
本课程设计的项目名称是“一个简易计算器的实现”。本项目要实现的计算器的名称为“计算精灵(Calculator Daemon)”。本文的后续部分将使用“计算精灵”来称呼本项目。
计算精灵的功能框图如 图 1 所示,从左到右,对应计算精灵从输入到输出所要具备的功能。

图 1 计算精灵的功能框图
输入模块要有的功能是:采用键盘中断的方式来读取输入,能修改计算表达式(如支持后退功能)。
处理模块是计算精灵的核心,所支持的功能有:能够检查计算表达式的有效性(如输入不合法,能及时给出提示),支持加减乘除四则运算,操作式支持多位数和浮点数,同时预留扩充接口(以后可能将增添支持其它操作符、支持十六进制计算、支持函数、支持表达式记忆、支持保存计算结果等功能)。
输出模块的功能简单,只需支持串口输出和lcd(液晶显示屏)显示即可。但为了使输出美观,对串口输出和lcd的显示还要做一定的美工设计。
在需求分析的基础上,得到如 图 2 所示的系统框图。

图 2 计算精灵的系统框图
系统分为键盘输入模块、计算处理模块、串口及LCD输出模块,各模块分别提供需求分析中提到的输入模块、处理模块和输出模块的功能。
图 2 中的箭头代表数据的流动方向。系统的IPO表(输入、处理和输出表)如 表 1 所示。
| 编号 | 输入数据 | 处理 | 输出数据 | 备注 |
| 1 | 用户操作键盘 | 键盘输入模块读取键值,形成计算式 | 计算式 | 无 |
| 2 | 计算式 | 计算处理模块按流程处理计算式,如果中途判断出计算式非法,则及时给出提示 | 计算结果或非法提示 | 计算结果和非法提示都看成是处理结果 |
| 3 | 计算处理结果 | 串口和LCD根据结果得出要显示的内容 | 处理结果通过串口输出,并显示在LCD上 | 无 |
表 1 计算精灵系统的IPO表
本项目由彭令鹏和肖品负责,具体的分工如 表 2 所示。
| 任务 | 负责人 | 起止时间 | 备注 |
| 需求分析 | 彭令鹏、肖品 | 2008.12.15-2008.12.18 | 明确需求很关键,所以花了较长的时间 |
| 概要设计 | 彭令鹏 | 2008.12.19-2008.12.20 | 无 |
| 键盘输入模块的设计与实现 | 肖品 | 2008.12.21-2008.12.22 | 该部分还包括模块测试 |
| 计算处理模块的设计与实现 | 彭令鹏 | 2008.12.21-2008.12.23 | 该部分还包括模块测试 |
| 串口及LCD输出模块的设计与实现 | 肖品 | 2008.12.23-2008.12.23 | 该部分还包括模块测试 |
| 集成测试 | 彭令鹏、肖品 | 2008.12.24-2008.12.25 | 测试是发现问题并解决问题的关键环节 |
| 项目总结 | 彭令鹏、肖品 | 2008.12.26-2008.12.28 | 无 |
表 2 项目的分工和进度安排
该部分仅介绍我所负责的计算处理模块的设计与实现。
5.1. 计算处理模块的设计
5.1.1. 模块框图
根据数据在模块中的流动,得到如 图 3 所示的设计框图。

图 3 计算处理子模块的设计框图
从 图 3 中看出,计算处理模块根据功能又可细分为解析表达式子模块、组合计算token子模块和格式化结果子模块等三个子模块。
这三个子模块的功能等信息如 表 3 所示。
| 子模块名称 | 输入数据 | 功能 | 输出结果 |
| 解析表达式子模块 | 来自键盘输入模块的计算式 | 从计算式中解析出操作符和操作数 | 操作数和操作符(统称token),或错误提示 |
| 组合计算token子模块 | 来自解析表达式子模块的token | 将各token按数学运算规则组织起来,进行数学运算 | 计算结果或错误 |
| 格式化结果子模块 | 来自组合计算token模块的计算结果,以及来自前面各子模块的错误提示 | 将计算结果或错误提示,分别格式化成相应的格式,以方便输出模块直接读取 | 格式化后的结果,该结果流向串口及LCD显示模块 |
表 3 各子模块的责任表
5.1.2. 接口数据结构设计
计算处理模块与键盘输入处理模块的接口数据结构
因为计算处理模块和键盘输入处理模块最终都会以模块的形式被主程序调用,所以这两者之间的接口可以通过一个在两者的生存期内一直存在的字符串数组实现。具体设计中,为既避免传形参的麻烦,又避免该数据结构全局可见,采用如下所示的静态数据结构。
typedef struct _expr_info
{
char expr[MAX_EXPR_LEN]; /* 存放计算式 */
int len; /* 计算式的长度 */
}expr_info_t;
计算处理模块与键盘输出处理模块的接口数据结构
同样地,基于与上一个数据结构同样的理由,采用如下的静态数据结构。
typedef struct _display_info
{
char expr_buf[MAX_EXPR_LEN]; /* 存放计算式(有可能包含计算结果) */
int expr_len; /* 计算式长度 */
char alert_buf[MAX_ALERT_LEN]; /* 存放提示信息 */
int alert_len; /* 提示信息长度 */
char isdirty; /* reserved */
char isrefresh; /* 是否刷新(输出完一个计算表达式的结果后,再输入新计算式需刷新) */
}display_info_t;
注意,结构体提供了expr_buf来存放格式化后的计算式(可能包含计算结果,如“3+5=8”),提供了alert_buf来存放格式化了的错误提示信息。
5.2. 计算处理模块的实现
该部分介绍各个子模块的实现。
5.2.1. 解析表达式子模块的实现
(1) 关键的数据结构的实现
从计算式中解析出token(操作数和操作符)是该子模块最重要的功能。因为操作符仅仅是‘+’、‘-’、‘*’和‘/’这样的字符,而操作数有则是一串数字(如3.14159),因此解析出操作数是该子模块的的难点和重点。为解析出操作数,设计如下的数据结构。
struct _opnd
{
char isfloat; /* 是否是浮点数 */
double float_factor; /* 当前的小数因子,第i位小数的因子为(0.1)^i */
double value; /* 操作数当前的值 */
};
(2) 程序流程
解析表达式子模块得到一个输入字符后,首先判断该字符是不是数字或小数点,如果是的话,则说明这字符及之后的若干字符构成一个操作数,因此将从这字符开始的连续的字符或小数点解析成一个操作数,否则将当前字符解析成一个操作符。整个程序流程如 图 4 所示。
图 4 解析表达式子模块的程序流程
5.2.2. 组合计算token子模块的实现
(1) 关键的数据结构的实现
经解析子模块后,本模块得到了一个个操作数或操作符,本模块所要实现的就是将这些操作token组合起来并按照数学规则来运算,这依赖于两个栈来处理计算顺序。
这两个栈一个用来存放操作数,一个用来存放操作符,前者的数据类型是double,后者的数据类型是char,所以如果将栈单元实现成double类型,无疑会造成存储浪费,这在嵌入式系统中是不大允许的。为此,该子模块设计了可自定义栈单元大小的栈,相关的数据结构如下。
typedef struct _seq_stack
{
void *base; /* 栈底指针 */
void *top; /* 栈顶指针 */
int node_cnt; /* 栈内当前的数据个数 */
int node_size; /* 栈单元的大小 */
int stack_size; /* 栈空间的大小 */
}seq_stack_t, *pseq_stack_t;
利用这个结构体,我们就可以来管理自定义栈单元大小的栈了。那我们是通过什么接口来自定义栈大小的呢?看下面这个函数
int init_stack(seq_stack_t * stack, void *buf, int stack_size, int node_size);
通过这个函数,可以初始化栈,第一个参数用来返回栈指针(指向管理栈的结构体),第二个参数用来传入栈所用的空间,第三个参数用来传入栈空间的大小(栈单元的个数),第四个参数用来传入栈单元的大小(自定义栈单元的大小)。
另外为方便扩充操作符,以及预留支持函数运算,采用了如下所示的两个表。
static optr_info_t optr_info[] =
{
{'+', 0, add},
{'-', 1, sub},
{'*', 2, mul},
{'/', 3, div},
/* {'%', 4, mod}, */
{'(', 4, NULL},
{')', 5, NULL},
{'=', 6, NULL},
{0, 0, NULL} /* indicate array size */
};
static char optr_pri_table[][sizeof(optr_info)/sizeof(optr_info_t)-1]=
{
{GREAT_PRI, GREAT_PRI, LOW_PRI, LOW_PRI/*, LOW_PRI*/, LOW_PRI, GREAT_PRI, GREAT_PRI},
{GREAT_PRI, GREAT_PRI, LOW_PRI, LOW_PRI/*, LOW_PRI*/,LOW_PRI, GREAT_PRI, GREAT_PRI},
{GREAT_PRI, GREAT_PRI, GREAT_PRI, GREAT_PRI/*, GREAT_PRI*/, LOW_PRI, GREAT_PRI, GREAT_PRI},
{GREAT_PRI, GREAT_PRI, GREAT_PRI, GREAT_PRI/*, GREAT_PRI*/, LOW_PRI, GREAT_PRI, GREAT_PRI},
/* {GREAT_PRI, GREAT_PRI, GREAT_PRI, GREAT_PRI, GREAT_PRI, LOW_PRI, GREAT_PRI, GREAT_PRI},*/
{LOW_PRI, LOW_PRI, LOW_PRI, LOW_PRI/*, LOW_PRI*/, LOW_PRI, EQUAL_PRI, OTHER_PRI},
{GREAT_PRI, GREAT_PRI, GREAT_PRI, GREAT_PRI/*, GREAT_PRI*/, OTHER_PRI, GREAT_PRI, GREAT_PRI},
{LOW_PRI, LOW_PRI, LOW_PRI, LOW_PRI/*,LOW_PRI*/, LOW_PRI, OTHER_PRI, EQUAL_PRI}
};
第一个表是用结构体数组实现的,结构体的第一个元素是操作符,第二个元素为操作符索引,第三个元素是对应的操作函数。很明显当需要增加sin函数时,只需在表中添加{'sin', 7, sin}一项,这是非常方便的。当然当表过大时,查找可能比较耗时,可采用hash查找等算法改进之。
第二个表是操作数的优先级表。新增操作符时,只需改表即可。当然随着操作数的增加,该表层O(
)增长,这可通过将操作符分类的方法来避免(如像C语言中一样,因为加和减的优先级和结合性一样,可划为一类;同样地,乘和除也可划为一类)。
(2) 程序流程
组合计算token采用的是经典的表达式计算算法,这里不再详述,可参考参考文献 [3] 。
整个程序流程如 图 5 所示,需要注意的是为表达简洁,图中省去了出错处理。
图 5 组合计算token子模块的程序流程
5.2.3. 格式化结果子模块的实现
(1) 关键的数据结构的实现
本模块相对比较简单,关键的数据结构,就是用来与串口及LCD显示输出模块交互接口数据结构,已在 0 中给出。
(2) 程序流程
本模块的程序流程如 图 6 所示。需要注意的是当计算并输出完计算结果后,再输出新的计算表达式时,需刷新屏幕(针对LCD而言)。
图 6 格式化结果子模块
5.3. 计算处理模块的测试
该模块的测试分功能测试和性能测试两部分。因为并不与硬件直接相关,所以测试在vc6.0下进行。
功能性测试。生成一个有多个计算式的文件,本模块读取这个文件,然后对这些计算式进行计算处理,将处理结果与计算式应得到的结果比较,如果相同,则可认为功能基本达到,否则有问题。我通过这种方法,前前后后改了不下5次,才通过功能测试。
性能测试。因为该部分是纯粹的运算,对于小型运算其速度应该不错,所以没有测试。有一个性能改进点是“边输入边计算”,而不是像现在一样只当表达式全部读取完后才开始计算,但这使能修改表达式的实现难度增加了。
将各模块级联成一个系统,然后输入多组测试用例数据,看输出是否满足要求。注意,在联调较大的系统时,宜把相关性较大的几个模块先联成一个较大的模块,只有当这个较大的模块通过测试时,才能进行更大程度的集成。
考虑到我们的系统很小,所以我们将三个模块一次性集成到了一次,编译倒是非常顺利,只改了一些包含语句就搞定了,但在输入输出的处理上还是出现了很多bug。比如最开始时,我们的键盘中断输入死活工作不正常,一运行程序,程序就跑飞了,后来发现原来是中断服务程序里没有清除中断标志。所以干事情还是要心细。
本项目成功实现了一个简易的计算器。在项目开发中遇到了不少问题,我们所做的是硬着头皮去解决它们。虽然这个解决问题的过程几近于痛苦,但我们最终还是熬过去了,于是也就尝到了大苦之后的甘甜。通过该项目,我们掌握了嵌入式开发的基本流程,以及遇到问题后分析并解决问题的方法,并且培养了团队合作的意识,这些都是难能可贵的。
至于项目的改进方案,在之前的各节中已有论述,这里总结下。本项目在功能上还可以添加支持其它操作符、支持十六进制计算、支持函数运算、支持表达式记忆、支持保存计算结果等。在性能上,计算表达式时,可采用“边输入边尽量计算”的方法,这可充分利用用户与程序的交互空闲;同时,当扩充的功能比较多时,程序中用到的表会很大,这可通过优化表存储来减少存储空间,并通过建立hash索引等来优化表查找。
参考资料
[1] 鄢舒. 嵌入式系统设计课件. 武汉:华中科技大学内部资料, 2008
[2] 孙天泽, 袁文菊等. 嵌入式设计及Linux驱动开发指南——基于ARM9处理器. 北京:电子工业出版社, 2005
[3] 严蔚敏, 吴伟民. 数据结构(C语言版). 北京:清华大学出版社, 1997
本文是关于一个嵌入式系统课程设计的报告,详细介绍了简易计算器"计算精灵"的设计与实现过程,包括需求分析、概要设计、详细设计与实现以及测试。计算器功能包括加减乘除和键盘中断输入,采用C语言编写,涉及数据结构和token处理。测试结果显示功能基本达到要求,提出了功能扩展和性能改进方案。
1737





