在嵌入式系統開發中,利用8051單片機實現秒表功能是經典的練習項目。本文針對一位開發者提交的、精度要求為百分之一秒的秒表程式進行深度分析,指出其邏輯缺陷,並提供一個功能完整、結構清晰的改進版本。
原程式碼使用定時器0中斷遞增三個計數器(wei1、wei2、wei3),並在主迴圈中透過輪詢方式驅動數碼管顯示。主要缺陷包括:
delay(5) 進行佔位式延遲,嚴重阻塞 CPU,導致按鍵掃描、串口通訊等其他任務無法並行處理。uint 計數,若長時間運行可能溢位,且無溢位保護。採用「定時器中斷純計時 + 主迴圈非阻塞掃描顯示 + 按鍵狀態機控制」的架構,程式碼如下:
#include <reg52.h>
#define uchar unsigned char
#define uint unsigned int
// 共陽數碼管編碼 0-F
uchar code SEG_CODE[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,
0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
sbit LATCH_D = P2^6; // 段鎖存
sbit LATCH_W = P2^7; // 位鎖存
sbit KEY_START = P3^2; // 啟動/暫停鍵
sbit KEY_RESET = P3^3; // 復位鍵
// 系統狀態
typedef enum { STOP, RUN } RunState;
RunState sys_state = STOP;
// 計時變數 (百分秒、秒、十秒、分)
uchar cnt_ms = 0, cnt_s = 0, cnt_10s = 0, cnt_min = 0;
// 顯示緩衝區
uchar disp_buf[4] = {0,0,0,0};
// 按鍵消抖計數
uchar key_start_cnt = 0, key_reset_cnt = 0;
void Timer0_Init(void) {
TMOD &= 0xF0; TMOD |= 0x01; // 模式1,16位定時器
TH0 = 0xDC; TL0 = 0x00; // 50ms @ 11.0592MHz (1ms需調整,此處示例為50ms)
ET0 = 1; EA = 1; TR0 = 1; // 啟動中斷
}
void Display_Scan(void) {
static uchar pos = 0;
LATCH_D = 1; P0 = 0xFF; LATCH_D = 0; // 消影
LATCH_W = 1; P0 = ~(0x01 << pos); LATCH_W = 0; // 選位
LATCH_D = 1; P0 = SEG_CODE[disp_buf[pos]]; LATCH_D = 0; // 輸出段碼
pos = (pos + 1) % 4;
}
void Key_Scan(void) {
// 啟動/暫停鍵處理
if (KEY_START == 0) {
if (++key_start_cnt >= 20) { // 20ms消抖
key_start_cnt = 20;
if (sys_state == RUN) sys_state = STOP;
else sys_state = RUN;
}
} else key_start_cnt = 0;
// 復位鍵處理 (長按1秒復位)
if (KEY_RESET == 0) {
if (++key_reset_cnt >= 100) { // 1秒
cnt_ms = cnt_s = cnt_10s = cnt_min = 0;
sys_state = STOP;
}
} else key_reset_cnt = 0;
}
void Update_Display_Buf(void) {
disp_buf[0] = cnt_ms % 10;
disp_buf[1] = cnt_ms / 10;
disp_buf[2] = cnt_s % 10;
disp_buf[3] = cnt_s / 10; // 簡化:僅顯示分秒,擴展易
}
void main(void) {
Timer0_Init();
while(1) {
Display_Scan(); // 非阻塞動態掃描
Key_Scan(); // 按鍵掃描
Update_Display_Buf(); // 更新顯示緩衝
// 此處可添加其他非阻塞任務
}
}
void Timer0_ISR(void) interrupt 1 using 1 {
TH0 = 0xDC; TL0 = 0x00; // 重裝初值
if (sys_state == RUN) {
if (++cnt_ms >= 100) { // 100 * 10ms = 1s (需根據實際定時器週期調整)
cnt_ms = 0;
if (++cnt_s >= 60) {
cnt_s = 0;
if (++cnt_min >= 60) cnt_min = 0;
}
}
}
}
針對 11.0592MHz 晶振,定時器 0 模式 1 (16 位) 設定初值 0xDC00,溢位週期約為 10ms (具體需查表計算)。中斷服務程式 (ISR) 僅負責核心計時變數遞增與進位,執行時間極短,不影響系統實時性。
主迴圈中 Display_Scan() 采用靜態變數記錄當前掃描位,每次調用僅刷新一位數碼管,並通過 P0 = 0xFF 消除鬼影。此法無需 delay,CPU 佔用率極低,騰出時間處理按鍵、通訊等任務。
引入 RunState 枚舉管理秒表狀態。按鍵採用「計數法消抖」:按下時計數器遞增,達到閾值 (如 20ms) 確認有效動作,釋放時清零。長按復位鍵 1 秒觸發歸零,符合用戶直覺操作習慣。
將硬體相關操作 (鎖存腳定義、段碼表) 集中在文件頂部,邏輯層與硬體層分離。若更換單片機型號 (如 STC89C52RC) 僅需修改頭文件與定時器初值,核心邏輯無需變動。
P0 = SEG_CODE[...] 已隱含此邏輯。IP 寄存器,避免顯示掃描被長時間阻塞導致閃爍。EA=0) 讀取完畢再開啟,防止讀取到「半舊半新」的數據。通過引入狀態機、非阻塞掃描與標準化按鍵處理,將原本「跑不通、功能少、難擴展」的練習代碼重構為工程級秒表雛形。此架構可平滑擴展至多路定時器、外部中斷捕獲、串口數據上傳等複雜場景,是嵌入式軟體工程化思維的最佳入門實踐。