基于80C51单片机的出租车计费器设计

写在前面

本文将详细讲解如何在Proteus中,使用80C51单片机,编写汇编程序,实现出租车计费器,实现实时速度显示,行使里程统计及费用统计,以及自动的清零。

该题包含两个输入和三个输出,其中一个输入是车轮转动的更新信号,每更新一次代表车轮转了一圈,另一个输入信号是费用计费/清零输入按钮。而输出是三个数字,从左至右分别代表总价,总里程,以及当前速度。

完成版本的效果图如下:

 

问题基础分析

经过一番严谨的审题,我们大致给出了这道题目的做题方案。

我们可以将整个汇编代码分为以下两个部分:

代码总体框架

我们需要设定好整个代码,分为多少个循环,以及多少个模块。

在总体框架中,我们将设计好完整的输出刷新,旋转圈数更新,每秒旋转圈数更新,以及计费按钮的追踪。

为了验证框架的可行性,笔者撰写了一份c++代码,作为整个代码框架的辅助设计工具及验证工具,总代码如下:

 1 #include<bits/stdc++.h>
 2 #include<windows.h>
 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 = %d\n",sum_circle);
28 }
29 
30 //更新屏幕缓冲去的速度显示部分
31 //本代码做了简化,直接输出一秒钟转的圈数来查看情况 
32 void update_speed_on_screen_buf(){
33     printf("current_circle = %d\n", current_circle); 
34 }
35 
36 //更新输入的按钮,用于控制是否清零圈数
37 //采用键盘按钮A来跟踪清零按键 
38 void solvebutton(){
39     bool is_key_down = GetAsyncKeyState('A');
40     if(is_key_down){
41         printf("A\n");
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<bits/stdc++.h>
 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", &current_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<<"费用="<<answer_fabi<<"法币"<<endl;
34     printf("%d.%2d\n", answer_fabi / (1000000), answer_fabi / (10000) % 100); 
35     
36     cout<<"路程="<<answer_route<<"厘米"<<endl;
37     printf("%d.%2d\n", answer_route / (1000 * 100) , (answer_route / 1000) % 100);
38     cout<<"速度="<<answer_speed<<"米每小时"<<endl;
39     printf("%d.%1d\n", 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
11    MOV cDisplayBuffer+2, A
12    MOV cDisplayBuffer+3, B
13    RET
14 
15 ;输出车轮总转数的低3位
16 Show_low:
17    MOV A, sum_circleL
18    MOV B, #100
19    DIV AB
20    MOV cDisplayBuffer+5, A
21    
22    MOV A, B
23    MOV B, #10
24    DIV AB
25    MOV cDisplayBuffer+6, A
26    MOV cDisplayBuffer+7, B
27    RET

确认完毕后,我们即确认该部分有效。

 

组合单测

该部分的有效性完成测试后,我们可以嵌套上屏幕的大刷新部分(即每秒50次大刷新,每次大刷新将12个数字进行刷新,并等待一定时间),并监测相关的按钮信息,以及调用Show_high和Show_low更新屏幕缓冲区。

 

代码骨干搭建

至此,我们完成了最关键的几个单元的搭建。

此时,我们可以开始将这三个核心硬件交互,与刷新结构,循环结构,等待结构进行组合,完成除计算外的全部模块。

中断计时代码

注意该计时的结构,该结构保证了相邻两次调用Wait的耗时均为1/600s

1 ;延时函数,延时恰好1/600s
2 Wait:
3    JNB   TF1,$
4    CLR   TF1
5    MOV   TH1,#0F4H
6    MOV   TL1,#00H
7    RET

刷新结构

代码整体上分为两重循环,其中每秒运行一次大循环

一次大循环包含50次小循环,当大循环结束后,会更新过去一秒汽车的速度。

一次小循环包含12个循环刻,每个循环刻将对汽车走过的圈数进行更新,并且对显示屏的某一个位置显示的内容进行更新,每个循环刻用时1/600s

当12个循环刻运行完毕后,将调用显示圈数和价格的代码,对显示缓冲区进行更新。

嵌套的大刷新和小刷新代码如下

 1 ;跑一秒,迭代50次,然后清除每秒的圈数
 2 Run_a_second:
 3    loop_outer:
 4    INC RefreshOuter
 5    
 6    LCALL Run_a_tick
 7    
 8    ;LCALL Show_high
 9    ;LCALL Show_low
10    LCALL SHOW_ROUTE
11    LCALL SHOW_PRICE
12    
13    MOV A, RefreshOuter
14    CJNE A, #50, loop_outer
15    LCALL SHOW_SPEED
16    MOV sec_circle, #0
17    MOV RefreshOuter, #0
18    RET
19 
20 Run_a_tick:
21    loop_inner:
22    INC RefreshInner
23    
24    LCALL Wait ;等待1/600s
25    LCALL update_roll ;更新车轮转的次数
26    LCALL solvebutton ;更新暂停和清空按钮
27    
28    LCALL Display ;显示要去到每个位置每秒更新50次
29    
30    MOV A, RefreshInner
31    CJNE A, #12, loop_inner
32    MOV RefreshInner, #0
33    RET

当上方的SHOW_ROUTE和SHOW_PRICE没有写好的时候,可以先用输出circleH和circleL的代码代替,进行测试,输出Speed的同理。

我们在测试的时候,在主函数中不断地调用Run_a_second即可实现测试。

 

数值计算部分

计算属于整个任务中代码最难编写的部分,我们需要编写四字节乘以单字节,和四字节除以10,并采用这两个模块组装出数值计算部分。

四字节乘法

在80C51中,MUL AB执行完毕后,溢出的部分会保存在B中。

利用这一性质,我们可以较为便利地计算进位信息。

在接下来的位中(不妨以mul[i]乘以mull举例),其中上次的进位为R1,我们可以先将两个数相乘,随后将上次的进位R1累加进来,然后更新mul[i],并将R1更新为新的进位信息。

代码如下

 1 ;计算四字节乘单字节,结果放在原地
 2 calc_mul:
 3    ; 初始化累加器
 4    CLR C ; 清除进位标志
 5    MOV A, #0 ; 清空累加器
 6 
 7    ; 计算 MUL0 * MULL 并存储结果在 MUL0
 8    MOV B, MULL ; 将 MULL 的值加载到 B
 9    MOV A, MUL0 ; 将 MUL0 的值加载到 A
10    MUL AB ; 执行 8 位乘法
11    MOV MUL0, A ; 将乘法的低 8 位结果存回 MUL0
12    MOV R1, B ; 保存乘法的高 8 位结果到 R1 以备后续使用
13 
14    ; 计算 MUL1 * MULL + 进位 并存储结果在 MUL1
15    MOV A, MUL1 ; 将 MUL1 的值加载到 A
16    MOV B, MULL ; 将 MULL 的值加载到 B
17    MUL AB ; 执行 8 位乘法
18    ADD A, R1 ; 加上上一步骤的进位
19    MOV MUL1, A ; 将结果存回 MUL1
20    MOV A, B
21    ADDC A, #0 ;加上可能产生的进位
22    MOV R1, A ; 保存乘法的高 8 位结果到 R1 以备后续使用
23 
24    ; 计算 MUL2 * MULL + 进位 并存储结果在 MUL2
25    MOV A, MUL2 ; 将 MUL2 的值加载到 A
26    MOV B, MULL ; 将 MULL 的值加载到 B
27    MUL AB ; 执行 8 位乘法
28    ADD A, R1 ; 加上上一步骤的进位
29    MOV MUL2, A ; 将结果存回 MUL2
30    MOV A, B
31    ADDC A, #0 ;加上可能产生的进位
32    MOV R1, A ; 保存乘法的高 8 位结果到 R1 以备后续使用
33 
34    ; 计算 MUL3 * MULL + 进位 并存储结果在 MUL3
35    MOV A, MUL3 ; 将 MUL3 的值加载到 A
36    MOV B, MULL ; 将 MULL 的值加载到 B
37    MUL AB ; 执行 8 位乘法
38    ADD A, R1 ; 加上上一步骤的进位
39    MOV MUL3, A ; 将结果存回 MUL3
40    MOV A, B
41    ADDC A, #0 ;加上可能产生的进位
42    MOV R1, A ; 保存乘法的高 8 位结果到 R1 以备后续使用
43    RET

 

四字节除法

该部分相较于四字节乘法难度较高,因为在除法中无法“借位”实现双字节除以单字节,必须自己想办法处理借位的情况。

笔者采用了一个十分投机取巧的方法,笔者将每个8位二进制数,切分为了两个4位的二进制数,分别进行借位运算,这样可以保证每次计算都不会超过255

具体而言,我们假设上次得到的退位为R1,被除数在这位的值为DIV[i],除数固定为10。

则令DIVH为DIV[i]的高4位,DIVL为DIV[i]的低4位。我们设结果为ANSH和ANSL(与DIV同理)

则有ANSH = int( (R1 * 16 + DIVH) / 10)

计算完ANSH后,更新R1= (R1 * 16 + DIVH) % 10

接下来计算ANSL = int( (R1 * 16 + DIVL) / 10) ,注意此处的R1就是计算完ANSH后更新出的R1,不是之前的退位。

更新R1 = (R1 * 16 + DIVL) % 10

至此,我们即完成了某一个位的除法运算,四字节的除法,即该过程重复三次(DIV[0]直接除即可)。

代码如下:

 1 ;计算某个BYTE的除以10的情况,采用分2段移位的计算方式
 2 CALC_DIV_BYTE:
 3    MOV A, @R0
 4    MOV B, #16
 5    DIV AB
 6    MOV R1, A
 7    
 8    MOV A, #16
 9    MOV B, R4
10    MUL AB
11    ADD A, R1
12    MOV B, #10
13    DIV AB
14    
15    MOV R2, A
16    MOV R4, B
17    
18    MOV A, @R0
19    MOV B, #16
20    DIV AB
21    MOV R1, B
22    
23    MOV A, #16
24    MOV B, R4
25    MUL AB
26    ADD A, R1
27    MOV B, #10
28    DIV AB
29 
30    MOV R3, A
31    MOV R4, B
32    
33    MOV A, R2
34    MOV B, #16
35    MUL AB
36    ADD A, R3
37    MOV @R0, A
38 
39    RET
40 
41 ;实现四字节除以十,且最后的余数保存在MULL中
42 calc_div:
43    CLR C
44    MOV A, MUL3
45    MOV B, #10
46    DIV AB
47    MOV MUL3, A
48    MOV R4, B ;完成MUL3的除法计算
49    
50    MOV R0, #MUL2
51    LCALL CALC_DIV_BYTE
52    
53    MOV R0, #MUL1
54    LCALL CALC_DIV_BYTE
55    
56    MOV R0, #MUL0
57    LCALL CALC_DIV_BYTE
58    
59    MOV MULL, R4
60    
61    RET

 

乘除法测试

同样地,我们需要对乘除法模块的准确性进行测试。

我们可以参照上述直接输出在P1和P2的方法,对其运算结果进行验证,避免后续错了以后不知从何开始调试。

 

速度计算和输出部分

有了上面两个模块,以及C++代码中的计算公式和输出范围,我们可以直接撰写。

对于十进制逐数字输出的要求,我们可以直接调用除以十的模块,并输出对应的余数即可。

 1 SHOW_SPEED:
 2    MOV MUL3, #0
 3    MOV MUL2, #0
 4    MOV MUL1, #0
 5    MOV MUL0, sec_circle
 6 
 7    MOV MULL, #183
 8    LCALL calc_mul
 9    MOV MULL, #36
10    LCALL calc_mul
11    
12    LCALL calc_div 
13    LCALL calc_div
14    
15    LCALL calc_div ;获得小数点后第2位
16    MOV cDisplayBuffer+11, MULL
17 
18    LCALL calc_div ;获得小数点后第1位
19    MOV cDisplayBuffer+10, MULL
20 
21    LCALL calc_div ;获得小数点前第1位
22    MOV cDisplayBuffer+9, MULL
23 
24    LCALL calc_div ;获得小数点前第2位
25    MOV cDisplayBuffer+8, MULL
26    
27    RET

 

里程计算和输出部分

里程也同理,直接将C++代码人肉翻译为汇编语句并调用输出模块即可

 1 ;输出总路程
 2 SHOW_ROUTE:
 3    MOV MUL3, #0
 4    MOV MUL2, #0
 5    MOV MUL1, sum_circleH
 6    MOV MUL0, sum_circleL
 7    MOV MULL, #183
 8    LCALL calc_mul ;获得厘米数
 9    LCALL calc_div
10    LCALL calc_div
11    LCALL calc_div 
12    
13    LCALL calc_div ;获得小数点后第2位
14    MOV cDisplayBuffer+7, MULL
15 
16    LCALL calc_div ;获得小数点后第1位
17    MOV cDisplayBuffer+6, MULL
18 
19    LCALL calc_div ;获得小数点前第1位
20    MOV cDisplayBuffer+5, MULL
21 
22    LCALL calc_div ;获得小数点前第2位
23    MOV cDisplayBuffer+4, MULL
24    
25    RET

 

费用计算和输出部分

费用计算大体上类似。但是增加了一个四字节的比较(这里比较恶心),以及四字节的手动赋值和加法。

这方面手动进行完善即可。

 1 SHOW_PRICE:
 2    MOV MUL3, #0
 3    MOV MUL2, #0
 4    MOV MUL1, sum_circleH
 5    MOV MUL0, sum_circleL
 6    MOV MULL, #183
 7    LCALL calc_mul ;获得厘米数
 8    
 9    ;通过判断圈数,来测定金额
10    ; 检查 sum_circleH 是否大于 4
11    MOV     A, MUL2
12    CLR     C               ; 清除进位标志
13    SUBB    A, #4        ; 减去4,如果sum_circleH大于4,CY会被清零
14    JNC     PRICE_ENOUGH    ; 如果CY被清零,跳转到PRICE_ENOUGH
15 
16    ; 检查 sum_circleH 是否大于 3 并且 sum_circleL 是否大于 68
17    MOV     A, MUL2
18    CLR     C               ; 清除进位标志
19    SUBB    A, #3        ; 减去3,如果sum_circleH大于3,CY会被清零
20    JNC     CHECK_SUM_CIRCLEL   ; 如果CY被清零,检查sum_circleL
21 
22 PRICE_NOT_ENOUGH:
23       ;写入八百万法币
24       MOV MUL3, #0
25       MOV MUL2, #122
26       MOV MUL1, #18
27       MOV MUL0, #0
28       
29       SJMP PRICE_EXIT
30    
31 CHECK_SUM_CIRCLEL:
32    MOV     A, MUL1
33    CLR     C                   ; 清除进位标志
34    SUBB    A, #13              ; 减去68,如果sum_circleL大于68,CY会被清零
35    JNC     PRICE_ENOUGH        ; 如果CY被清零,跳转到PRICE_ENOUGH
36     JMP    PRICE_NOT_ENOUGH
37    
38 PRICE_ENOUGH:
39        ; price_enough 的代码
40        MOV MULL, #26
41        LCALL calc_mul
42        
43        ; 还要加上280万法币
44        MOV A, MUL0
45        ADD A, #128
46        MOV MUL0, A
47        
48        MOV A, MUL1
49        ADDC A, #0 ;处理进位
50        ADD A, #185
51        MOV MUL1, A
52        
53        MOV A, MUL2
54        ADDC A, #0 ;处理进位
55        ADD A, #42
56        MOV MUL2, A  
57   
58        MOV A, MUL3
59        ADDC A, #0
60        MOV MUL3, A
61        
62        JMP PRICE_EXIT
63 
64 PRICE_EXIT:
65    LCALL calc_div
66    LCALL calc_div
67    LCALL calc_div
68    LCALL calc_div
69    
70    LCALL calc_div ;获得小数点后第2位
71    MOV cDisplayBuffer+3, MULL
72 
73    LCALL calc_div ;获得小数点后第1位
74    MOV cDisplayBuffer+2, MULL
75 
76    LCALL calc_div ;获得小数点前第1位
77    MOV cDisplayBuffer+1, MULL
78 
79    LCALL calc_div ;获得小数点前第2位
80    MOV cDisplayBuffer+0, MULL   
81        ; 程序结束
82    RET

 

结尾

初始化代码

不要忘记定义上面所需的变量,以及对定时器进行初始化

 1 Init:
 2 ;外部中断初始化(轮子计数)
 3     SETB EX0  ;允许外部中断0中断
 4     SETB ET0  ;允许定时器T0中断
 5     SETB IT0  ;外部中断0触发方式为边沿触发
 6     SETB EA  ;CPU开中断
 7     SETB  TR0  ;定时器T0启动计时
 8 
 9 ;设置定时器
10     MOV TMOD, #11H
11     SETB  TR1
12     SETB  TR0
13 
14 ;定义初始的转圈次数
15     sum_circleL EQU 30H
16     sum_circleH EQU 31H
17     sec_circle  EQU 33H
18     MOV sum_circleL, #00H
19     MOV sum_circleH, #00H
20     MOV sec_circle, #00H
21 
22 ;控制计费和清零系统
23     button_status EQU 34H
24     MOV button_status, #01H
25     last_button_status EQU 35H
26     MOV last_button_status, #80H
27 
28 ;显示控制程序
29     cDisplayBuffer EQU 40H ;显示的内容在40H-4BH
30     cDisplayBit EQU 4DH     ;当前显示的位
31 
32 ;控制刷新次数
33     RefreshOuter EQU 36H
34     RefreshInner EQU 37H
35     MOV RefreshOuter, #0
36     MOV RefreshInner, #0
37     
38 ;四字节乘法,注意除法也用这四个位
39    MUL0 EQU 50H
40    MUL1 EQU 51H
41    MUL2 EQU 52H
42    MUL3 EQU 53H
43    MULL EQU 54H
44    
45    DIV_A EQU 55H
46    DIV_B EQU 56H
47     
48     RET

 

至此,我们已经完成了全部的任务,任务效果见开头。

完整代码本文暂不提供,待作业结束提交后将会在底部贴出。

(实际上已经把全部的代码贴出来了)

完结撒花~

posted @ 2023-06-11 12:31  AlphaInf  阅读(346)  评论(2编辑  收藏  举报