首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> C 語言問答

8051單片機C語言秒表程式設計與常見錯誤修正

     編輯:C 語言問答

專案背景

在嵌入式系統開發中,利用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;
            }
        }
    }
}

核心改進點解析

1. 精確的定時器配置

針對 11.0592MHz 晶振,定時器 0 模式 1 (16 位) 設定初值 0xDC00,溢位週期約為 10ms (具體需查表計算)。中斷服務程式 (ISR) 僅負責核心計時變數遞增與進位,執行時間極短,不影響系統實時性。

2. 非阻塞動態掃描顯示

主迴圈中 Display_Scan() 采用靜態變數記錄當前掃描位,每次調用僅刷新一位數碼管,並通過 P0 = 0xFF 消除鬼影。此法無需 delay,CPU 佔用率極低,騰出時間處理按鍵、通訊等任務。

3. 狀態機與按鍵消抖

引入 RunState 枚舉管理秒表狀態。按鍵採用「計數法消抖」:按下時計數器遞增,達到閾值 (如 20ms) 確認有效動作,釋放時清零。長按復位鍵 1 秒觸發歸零,符合用戶直覺操作習慣。

4. 模組化與可移植性

將硬體相關操作 (鎖存腳定義、段碼表) 集中在文件頂部,邏輯層與硬體層分離。若更換單片機型號 (如 STC89C52RC) 僅需修改頭文件與定時器初值,核心邏輯無需變動。

常見坑點避雷指南

  • 定時器初值計算:務必根據實際晶振頻率與機器週期計算 TH0/TL0,切勿盲目複製網上代碼。
  • 共陽/共陰極性:段碼表需與硬體電路匹配,共陽極低電平點亮,代碼中 P0 = SEG_CODE[...] 已隱含此邏輯。
  • 中斷嵌套與優先級:若系統引入串口中斷,需合理配置 IP 寄存器,避免顯示掃描被長時間阻塞導致閃爍。
  • 變數原子性:主迴圈讀取多字節計時變數時,建議關閉總中斷 (EA=0) 讀取完畢再開啟,防止讀取到「半舊半新」的數據。

總結

通過引入狀態機、非阻塞掃描與標準化按鍵處理,將原本「跑不通、功能少、難擴展」的練習代碼重構為工程級秒表雛形。此架構可平滑擴展至多路定時器、外部中斷捕獲、串口數據上傳等複雜場景,是嵌入式軟體工程化思維的最佳入門實踐。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved