写在前面
本文将详细讲解如何在Proteus中,使用80C51单片机,编写汇编程序,实现出租车计费器,实现实时速度显示,行使里程统计及费用统计,以及自动的清零。
该题包含两个输入和三个输出,其中一个输入是车轮转动的更新信号,每更新一次代表车轮转了一圈,另一个输入信号是费用计费/清零输入按钮。而输出是三个数字,从左至右分别代表总价,总里程,以及当前速度。
完成版本的效果图如下:
问题基础分析
经过一番严谨的审题,我们大致给出了这道题目的做题方案。
我们可以将整个汇编代码分为以下两个部分:
代码总体框架
我们需要设定好整个代码,分为多少个循环,以及多少个模块。
在总体框架中,我们将设计好完整的输出刷新,旋转圈数更新,每秒旋转圈数更新,以及计费按钮的追踪。
为了验证框架的可行性,笔者撰写了一份c++代码,作为整个代码框架的辅助设计工具及验证工具,总代码如下:
1 #include
2 #include
3 using namespace std;
4
5 int sum_circle = 0;
6 int current_circle = 0;
7 int button_status = 0;
8
9 //等待函数,在实际场景下为1/600s
10 void wait(){
11 Sleep(1);
12 }
13
14 //更新转的圈数
15 //为了模拟输入信号,这里采用了跟踪空格是否按下来更新旋转圈数
16 void updateroll(){
17 if(button_status)
18 return;
19 bool isupdate = GetAsyncKeyState(VK_SPACE);
20 sum_circle += isupdate;
21 current_circle += isupdate;
22 }
23
24 //更新屏幕缓冲区的速度和总里程显示部分
25 //本代码做了简化,只输出总的旋转圈数
26 void update_expense_and_distance_on_screen_buf(){
27 printf('sum_circle = %dn',sum_circle);
28 }
29
30 //更新屏幕缓冲去的速度显示部分
31 //本代码做了简化,直接输出一秒钟转的圈数来查看情况
32 void update_speed_on_screen_buf(){
33 printf('current_circle = %dn', current_circle);
34 }
35
36 //更新输入的按钮,用于控制是否清零圈数
37 //采用键盘按钮A来跟踪清零按键
38 void solvebutton(){
39 bool is_key_down = GetAsyncKeyState('A');
40 if(is_key_down){
41 printf('An');
42 //keybd_event('A', 0, 2, 0);
43 button_status^=1;
44 if(button_status == 0){
45 sum_circle = 0;
46 }
47 }
48 }
49
50 //本函数将从屏幕缓冲区中,取出新的第update_pos位的信息更新到屏幕上
51 void updateDisplayBit(int update_pos){
52 //代码略
53 }
54
55 int main(){
56 while(true){ //最外层循环一秒执行一次
57 for(int i=0;i<50;i++){
58 //j会被用于更新屏幕,代表每秒更新50次屏幕
59 for(int j=0;j<12;j++){
60 wait(); //执行延时
61 updateroll(); //更新车轮圈数信息
62 updateDisplayBit(j); //刷新显示信息
63 }
64
65 update_expense_and_distance_on_screen_buf(); //更新屏幕显示信息
66 solvebutton(); //处理计费和更新按钮
67 }
68 update_speed_on_screen_buf(); //更新屏幕的速度缓冲区
69 current_circle = 0; //每秒计算一次
70 }
71 }
上述代码已经实现了除数据计算部分,以及具体显示部分的几乎全部内容。相比于汇编程序,它的可读性更好,更可以提前将代码的bug找出。
下面笔者将介绍一些代码中的关键细节。
Q:为什么最外层的循环,要设计为双层循环,第一层50第二层12?
我们必须保证屏幕的刷新次数,总共有12个显示位,每个显示位的刷新次数要达到50Hz,则需要采用双重循环来实现。
如果采用单层循环,则要循环600次,而600超过了char的表示范围,不容易实现。
此处还得考虑wait函数的计时范围,不过这里似乎不是个大问题。
Q:采用C++验证的优势是什么。
1,我们可以非常清晰地将整个代码逻辑实现表达,相对于流程图,其更接近要写的汇编代码。
2,它可以实现一系列的验证,在上述的框架中验证了除输出部分和数值计算部分外的几乎全部内容。
3,我们可以提前知道,需要开哪几个变量,特别是需要多少个全局变量,汇编中无法像高级语言一样实现传参,这一点尤为重要。
输出数据计算模块
输出的三个数本质上依赖于车轮旋转总圈数,以及最近一秒钟内车轮旋转次数的统计。
和上面一样,笔者写了一个C++程序,来实现相关内容的验证。
1 #include
2 using namespace std;
3
4 //总历史累加数据
5 int sum_circle = 0;
6
7 //每次的增加值
8 int current_circle = 0;
9
10 //表示要支付的费用(单位为角)
11 int answer_fabi;
12
13 //当前的速度(单位为毫米每秒)
14 int answer_speed;
15
16 //当前走过的路 (单位为厘米)
17 int answer_route ;
18
19
20 int main(){
21 scanf('%d', &sum_circle);
22 scanf('%d', ¤t_circle);
23
24 sum_circle += current_circle;
25 answer_route = sum_circle * 183;
26 answer_speed = current_circle * 183 * 36;
27
28 if(answer_route < 200000)
29 answer_fabi = 8000000;
30 else
31 answer_fabi = answer_route * 26 + 2800000;
32
33 cout<<'费用='<34 printf('%d.%2dn', answer_fabi / (1000000), answer_fabi / (10000) % 100);
35
36 cout<<'路程='<37 printf('%d.%2dn', answer_route / (1000 * 100) , (answer_route / 1000) % 100);
38 cout<<'速度='<39 printf('%d.%1dn', answer_speed / (1000), (answer_speed / 100) % 10);
40 }
上述代码补全了主框架中被暂缓编写的部分。
在该步中,我们直观地撰写了,如何用保存的总圈数和秒圈数,计算出总里程和费用。
为了便于在单片机中计算,我们需要确定每一个计算的数值,分别需要占用多少个字节,而且如何尽量避免使用除法,以及出现多字节使用的情况。
因此,笔者设计了一套计算公式,这套计算公式的优势如下:
1,除了输出阶段,不存在需要使用除法的情况,无精度损失
2,计算过程中最多使用到四字节乘以单字节,相交双字节乘法较容易编写
3,除法只需要编写四字节除以十,并保留余数的模块即可,大大降低了撰写的难度
4,只包含一次常数判断
通过这个代码,结合题目的要求,我们可以确认一些信息:
1,总圈数的存储采用双字节存储即可,速度采用单字节存储即可。
2,总路程计算时,需要采用不低于三字节(最多100千米=10^7厘米)进行计算。
3,总费用计算时,需采用四字节(最多两百多元=两个多亿法币)
在此,我们已经完成了全部关键细节的分析。
编写顺序
该代码体积较大,需要合理规划好编写顺序及验证顺序,以及最后的组装步骤。
笔者采用的编写步骤如下:
搭建硬件电路
笔者1:1搭建了与图示中完全一致的电路图,电路图可在最上方的图中见到全貌。
但在这里,有一个变通的地方,即在P3.2处额外连接了一个开关。
这处开关允许了我们采用波形发生器以外的方法,手动地输入信号,这对我们初期手动调试,有着极大的帮助。
完成搭建后,笔者进行了一些基础测试,即通过指令从P1和P2直接输出信号,查看显示的情况,以确保电路正常。
硬件IO模块交互代码
笔者下一步撰写的,即接收并处理按钮信号的代码,以及向屏幕输出的代码。
具体来说,笔者定义了sum_circleH和sum_circleL两个变量,该双字节变量用于存储车轮旋转的总圈数,由P3.2的信号变化进行增加。
笔者定义了控制按钮状态的button_status和last_button_status,用于记录按钮的状态,以便于实现停止计费/清零费用的功能。
对于屏幕输出模块,笔者采用了12字节的cDisplayBuffer,每个byte中写入需要显示的0-9的数,由输出模块转化为对应的数码管显示数据输出。
下面是该部分关键代码的解析
计费控制代码
该代码首先判断当前是否处于计费模式,如果处于计费模式,则从IE0处读取当前是否有更新
如有更新,则说明车轮转了一圈,将累加信息累加进sec_circle(即每秒转了多少圈)和sum_circle(总圈数)中。
注意处理sum_circleL向sum_circleH的进位信息。
1 ;对车轮的转动次数进行记录和更新,包括每秒的信息
2 update_roll:
3 MOV A, button_status
4 CJNE A, #1, update_roll_jmp ; 判断当前是否计费,为1代表正在计费
5 JNB IE0, update_roll_jmp
6 INC sec_circle
7 MOV A, sum_circleL ; 对低8位执行操作
8 ADD A, #1
9 MOV sum_circleL, A
10 MOV A, sum_circleH ; 将高8位移至累加器
11 ADDC A, #0
12 MOV sum_circleH, A ; 将结果返回至高8位
13 CLR IE0
14 update_roll_jmp:
15 RET
按钮控制
以下是按钮控制,以及按钮控制的圈数清零代码
笔者设置了一个变量last_button_status,首先与该变量进行比较,确定按钮的状态是否有更新
若更新,则更新button_status,表示当前是否处于计费模式
如果从非计费模式向计费模式更新,则清空圈数
1 solvebutton:
2 MOV A, P3 ;当前按钮状态
3 ANL A, #80H ;进行比较,如果与last_button_status不同,则说明按钮被按下或者弹起
4 CJNE A, last_button_status, solvebutton_need_solve
5 RET
6 solvebutton_need_solve: ;进入这里,说明按钮的情况有更新
7
8 XRL last_button_status, #80H
9
10 MOV A, last_button_status
11 CJNE A, #80H, solvebutton_jmp
12
13 MOV P0, button_status
14
15 MOV A, button_status
16 XRL A, #1
17 MOV button_status, A
18 CJNE A, #1, solvebutton_jmp
19
20 ;MOV P0, #33H
21
22 MOV sum_circleH, #0
23 MOV sum_circleL, #0
24
25 solvebutton_jmp:
26 RET
屏幕缓冲区控制
以下是基于屏幕缓冲区向屏幕输出的代码
该代码大致的过程为:显示的数据传入在cDisplayBuffer中,当前向数码管输出的是其中的第cDisplayBit位
为何要设置缓冲区?1,方便刷新。2,方便调试。
输出的过程为:将buffer中的数取出,然后从DispTable中取出对应的数码管显示数据,最后套上小数点显示的逻辑
1 ;显示程序
2 DispTable: DB 3FH,06H,5BH,4FH,66H,6DH,7DH,07H,7FH,6FH
3 Display:
4 MOV A,cDisplayBit
5 MOV P2,A
6 MOV DPTR,#DispTable
7 MOV A,#cDisplayBuffer
8 ADD A,cDisplayBit
9 MOV R0,A
10 MOV A,@R0
11 MOVC A,@A+DPTR
12
13 MOV R1, cDisplayBit
14 ;增加小数点适配
15 CHECK_BIT:
16 CJNE R1, #1, BIT_5 ; 检查cDisplayBit是否等于1
17 ORL A, #128 ; 将A与128进行OR运算
18 JMP NEXT ; 跳转到下一步
19
20 BIT_5:
21 CJNE R1, #5, BIT_10 ; 检查cDisplayBit是否等于5
22 ORL A, #128 ; 将A与128进行OR运算
23 JMP NEXT ; 跳转到下一步
24
25 BIT_10:
26 CJNE R1, #10, NEXT ; 检查cDisplayBit是否等于10
27 ORL A, #128 ; 将A与128进行OR运算
28
29 NEXT:
30
31 MOV P1,A
32 INC cDisplayBit
33
34 MOV A, cDisplayBit
35 CJNE A, #12, LessThan12
36 MOV cDisplayBit, #0
37 LessThan12:
38 RET
硬件交互模块单测
我们可以通过简单的测试来测试这些模块的有效性。
如直接用P1和P2口,裸输出sum_circle的信息,并按下两个按钮,观察数值是否发生变化,来测试该模块的有效性。
同样地,我们也可以通过直接对输出缓冲区进行信息输入(比如第2个位置输出7),观察显示的效果,来进行测试。
待这两部都完成后,我们可以启动波形发生器,并撰写一个将sum_circleH输出至第一块屏幕,将sum_circleL输出至第二块屏幕的程序,进行更加深度的测试。
相关的输出代码如下(该代码在正式作业中不参与运行):
1 ;输出车轮总转数的高3位
2 Show_high:
3 MOV A, sum_circleH
4 MOV B, #100
5 DIV AB
6 MOV cDisplayBuffer+1, A
7
8 MOV A, B
9 MOV B, #10
10 DIV AB