基于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", ¤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<<"费用="<<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
至此,我们已经完成了全部的任务,任务效果见开头。
完整代码本文暂不提供,待作业结束提交后将会在底部贴出。
(实际上已经把全部的代码贴出来了)
完结撒花~