寫在前面
本文將詳細講解如何在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,總費用計算時,需採用四字節(最多兩百多元=兩個多億法幣)
在此,我們已經完成了全部關鍵細節的分析。