[post]數碼管在實際應用中非常廣泛,尤其是在某些對成本有限制的場合。編寫一個好用的LED程序并不是那么的簡單。曾經有人這樣說過,如果用數碼管和按鍵,做一個簡易的可以調整的時鐘出來,那么你的單片機就算入門了60%了。此話我深信不疑。我遇到過很多單片機的愛好者,他們問我說單片機我已經掌握了,該如何進一步的學習下去呢?我并不急于回答他們的問題,而是問他們:會編寫數碼管的驅動程序了吧?“嗯”。會編寫按鍵程序了吧?“嗯”。好,我給你出一個小題目,你做一下。用按鍵和數碼管以及單片機定時器實現一個簡易的可以調整的時鐘,要求如下:
8位數碼管顯示,顯示格式如下
時-分-秒
XX-XX-XX
要求:系統有四個按鍵,功能分別是 調整,加,減,確定。在按下調整鍵時候,顯示時的兩位數碼管以1 Hz 頻率閃爍。如果再次按下調整鍵,則分開始閃爍,時恢復正常顯示,依次循環,直到按下確定鍵,恢復正常的顯示。在數碼管閃爍的時候,按下加或者減鍵可以調整相應的顯示內容。按鍵支持短按,和長按,即短按時,修改的內容每次增加一或者減小一,長按時候以一定速率連續增加或者減少。
結果很多人,很多愛好者一下子都理不清楚思路。其實問題的根源在于沒有以工程化的角度去思考程序的編寫。很多人在學習數碼管編程的時候,都是照著書上或者網上的例子來進行試驗。殊不知,這些例子代碼僅僅只是具有一個演示性的作用,拿到實際中是很難用的。舉一個簡單的例子。
下面這段程序是在網上隨便搜索到的:
while(1)
{
for(num=0;num<9;num++)
{
P0=table[num];
P2=code[num] ;
delayms(2) ;
}
}
看出什么問題來了沒有,如果沒有看出來請仔細想一下,如果還沒有想出來,請回過頭去,認真再看一遍“學會釋放CPU”這一章的內容。這個程序作為演示程序是沒有什么問題的,但是實際應用的時候,數碼管顯示的內容經常變化,而且還有很多其它任務需要執行,因此這樣的程序在實際中是根本就無法用的,更何況,它這里也調用了delayms(2)這個函數來延時2 ms這更是令我們深惡痛絕?
本章的內容正是探討如何解決多任務環境下(不帶OS)的數碼管程序設計的編寫問題。理解了其中的思想,無論要求我們顯示的形式怎么變化(如數碼管閃爍,移位等),我們都可以很方便的解決問題。
數碼管的顯示分為動態顯示和靜態顯示兩種。靜態顯示是每一位數碼管都用一片獨立的驅動芯片進行驅動。比較常見的有74LS164,74HC595等。利用這類芯片的好處就是可以級聯,留給單片機的接口只需要時鐘線,數據線,因此比較節省I/O口。如下圖所示:
利用74LS164級聯驅動8個單獨的數碼管
靜態顯示的優點是程序編寫簡單。但是由于涉及到的驅動芯片數量比較多,同時考慮到PCB的布線等等因素,在低成本要求的開發環境下,單純的靜態驅動并不合適。這個時候就可以考慮到動態驅動了。
動態驅動的圖如下所示(以EE21開發板為例)
由上圖可以看出。8個數碼管的段碼由一個單獨的74HC573驅動。同時每一個數碼管的公共端連接在另外一個74HC573的輸出上。當送出第一位數碼管的段碼內容時候,同時選通第一位數碼管的位選,此時,第一位數碼管就顯示出相應的內容了。一段時間之后,送出第二位數碼管段碼的內容,選通第二位數碼管的位選,這時顯示的內容就變成第二位數碼管的內容了……依次循環下去,就可以看到了所有數碼管同時顯示了。事實上,任意時刻,只有一位數碼管是被點亮的。由于人眼的視覺暫留效應以及數碼管的余輝效應,當數碼管掃描的頻率非常快的時候,人眼已經無法分辨出數碼管的變化了,看起來就是同時點亮的。我們假設數碼管的掃描頻率為50 Hz, 則完成一輪掃描的時間就是1 / 50 = 20 ms 。我們的系統共有8位數碼管,則每一位數碼管在一輪掃描周期中點亮的時間為20 / 8 = 2.5 ms 。
動態掃描對時間要求有一點點嚴格,否則,就會有明顯的閃爍。
假設我們程序 中所有任務如下:
while(1)
{
LedDisplay() ; //數碼管動態掃描
ADProcess() ; //AD采集處理
TimerProcess() ; //時間相關處理
DataProcess() ; //數據處理
}
LedDisplay() 這個任務的執行時間,如同我們剛才計算的那樣,50 Hz頻率掃描,則該函數執行的時間為20 ms 。 假設ADProcess()這個任務執行的的時間為2 ms ,TimerProcess()這個函數執行的時間為 1 ms ,DataProcess() 這個函數執行的時間為10 ms 。 那么整個主函數執行一遍的總時間為 20 + 2 + 1 + 10 = 33 ms 。即LedDisplay() 這個函數的掃描頻率已經不為50 Hz 了,而是 1 / 33 = 30.3 Hz 。這個頻率數碼管已經可以感覺到閃爍了,因此不符合我們的要求。為什么會出現這種情況呢? 我們剛才計算的50 Hz 是系統只有LedDisplay()這一個任務的時候得出來的結果。當系統添加了其它任務后,當然系統循環執行一次的總時間就增加了。如何解決這種現象了,還是離不開我們第二章所講的那個思想。
系統產生一個2.5 ms 的時標消息。LedDisplay() , 每次接收到這個消息的時候, 掃描一位數碼管。這樣8個時標消息過后,所有的數碼管就都被掃描一遍了。可能有朋友會有這樣的疑問:ADProcess() 以及 DataProcess() 等函數執行的時間還是需要十幾ms 啊,在這十幾ms 的時間里,已經產生好幾個2.5 ms的時標消息了,這樣豈不是漏掉了掃描,顯示起來還是會閃爍。能夠想到這一點,很不錯,這也就是為什么我們要學會釋放CPU的原因。對于ADProcess(),TimerProcess(),DataProcess(),等任務我們依舊要采取此方法對CPU進行釋放,使其執行的時間盡可能短暫,關于如何做到這一點,在以后的講解如何設計多任務程序設計的時候會講解到。
下面我們基于此思路開始編寫具體的程序。
首先編寫Timer.c文件。該文件中主要為系統提供時間相關的服務。必要的頭文件包含。
#include <reg52.h>
#include "MacroAndConst.h"
為了方便計算,我們取數碼管掃描一位的時間為2 ms。設置定時器0為2 ms中斷一次。
同時聲明一個位變量,作為2 ms時標消息的標志
bit g_bSystemTime2Ms = 0 ; // 2msLED動態掃描時標消息
初始化定時器0
void Timer0Init(void)
{
TMOD &= 0xf0 ;
TMOD |= 0x01 ; //定時器0工作方式1
TH0 = 0xf8 ; //定時器初始值
TL0 = 0xcc ;
TR0 = 1 ;
ET0 = 1 ;
}
在定時器0中斷處理程序中,設置時標消息。
void Time0Isr(void) interrupt 1
{
TH0 = 0xf8 ; //定時器重新賦初值
TL0 = 0xcc ;
g_bSystemTime2Ms = 1 ; //2MS時標標志位置位
}
然后我們開始編寫數碼管的動態掃描函數。
新建一個C源文件,并包含相應的頭文件。
#include <reg52.h>
#include "MacroAndConst.h"
#include "Timer.h"
先開辟一個數碼管顯示的緩沖區。動態掃描函數負責從這個緩沖區中取出數據,并掃描顯示。而其它函數則可以修改該緩沖區,從而改變顯示的內容。
uint8 g_u8LedDisplayBuffer[8] = {0} ; //顯示緩沖區
然后定義共陽數碼管的段碼表以及相應的硬件端口連接。
code uint8 g_u8LedDisplayCode[]=
{
0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E,
0xbf, //'-'號代碼
} ;
sbit io_led_seg_cs = P1^4 ;
sbit io_led_bit_cs = P1^5 ;
#define LED_PORT P0
再分別編寫送數碼管段碼函數,以及位選通函數。
static void SendLedSegData(uint8 dat)
{
LED_PORT = dat ;
io_led_seg_cs = 1 ; //開段碼鎖存,送段碼數據
io_led_seg_cs = 0 ;
}
static void SendLedBitData(uint8 dat)
{
uint8 temp ;
temp = (0x01 << dat ) ; //根據要選通的位計算出位碼
LED_PORT = temp ;
io_led_bit_cs = 1 ; //開位碼鎖存,送位碼數據
io_led_bit_cs = 0 ;
}
下面的核心就是如何編寫動態掃描函數了。
如下所示:
void LedDisplay(uint8 * pBuffer)
{
static uint8 s_LedDisPos = 0 ;
if(g_bSystemTime2Ms)
{
g_bSystemTime2Ms = 0 ;
SendLedBitData(8) ; //消隱,只需要設置位選不為0~7即可
if(pBuffer[s_LedDisPos] == '-') //顯示'-'號
{
SendLedSegData(g_u8LedDisplayCode[16]) ;
}
else
{
SendLedSegData(g_u8LedDisplayCode[pBuffer[s_LedDisPos]]) ;
}
SendLedBitData(s_LedDisPos);
if(++s_LedDisPos > 7)
{
s_LedDisPos = 0 ;
}
}
}
函數內部定義一個靜態的變量s_LedDisPos,用來表示掃描數碼管的位置。每當我們執行該函數一次的時候,s_LedDisPos的值會自加1,表示下次掃描下一個數碼管。然后判斷g_bSystemTime2Ms時標消息是否到了。如果到了,就開始執行相關掃描,否則就直接跳出函數。SendLedBitData(8) ;的作用是消隱。因為我們的系統的段選和位選是共用P0口的。在送段碼之前,必須先關掉位選,否則,因為上次位選是選通的,在送段碼的時候會造成相應數碼管的點亮,盡管這個時間很短暫。但是因為我們的數碼管是不斷掃描的,所以看起來還是會有些微微亮。為了消除這種影響,就有必要再送段碼數據之前關掉位選。
if(pBuffer[s_LedDisPos] == '-') //顯示'-'號這行語句是為了顯示’-’符號特意加上去的,大家可以看到在定義數碼管的段碼表的時候,我多加了一個字節的代碼0xbf:
code uint8 g_u8LedDisplayCode[]=
{
0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E,
0xbf, //'-'號代碼
} ;
通過SendLedSegData(g_u8LedDisplayCode[pBuffer[s_LedDisPos]]) ;送出相應的段碼數據后,然后通過SendLedBitData(s_LedDisPos);打開相應的位選。這樣對應的數碼管就被點亮了。
if(++s_LedDisPos > 7)
{
s_LedDisPos = 0 ;
}
然后s_LedDisPos自加1,以便下次執行本函數時,掃描下一個數碼管。因為我們的系統共有8個數碼管,所以當s_LedDisPos > 7后,要對其進行清0 。否則,沒有任何一個數碼管被選中。這也是為什么我們可以用
SendLedBitData(8) ; //消隱,只需要設置位選不為0~7即可
對數碼管進行消隱操作的原因。
下面我們來編寫相應的主函數,并實現數碼管上面類似時鐘的效果,如顯示10-20-30
即10點20分30秒。
Main.c
#include <reg52.h>
#include "MacroAndConst.h"
#include "Timer.h"
#include "Led7Seg.h"
sbit io_led = P1^6 ;
void main(void)
{
io_led = 0 ; //發光二極管與數碼管共用P0口,這里禁止掉發光二極管的鎖存輸出
Timer0Init() ;
g_u8LedDisplayBuffer[0] = 1 ;
g_u8LedDisplayBuffer[1] = 0 ;
g_u8LedDisplayBuffer[2] = '-' ;
g_u8LedDisplayBuffer[3] = 2 ;
g_u8LedDisplayBuffer[4] = 0 ;
g_u8LedDisplayBuffer[5] = '-' ;
g_u8LedDisplayBuffer[6] = 3 ;
g_u8LedDisplayBuffer[7] = 0 ;
EA = 1 ;
while(1)
{
LedDisplay(g_u8LedDisplayBuffer) ;
}
}
將整個工程進行編譯,看看效果如何?
動起來
既然我們想要模擬一個時鐘,那么時鐘肯定是要走動的,不然還稱為什么時鐘撒。下面我們在前面的基礎之上,添加一點相應的代碼,讓我們這個時鐘走動起來。
我們知道,之前我們以及設置了一個掃描數碼管用到的2 ms時標。 如果我們再對這個時標進行計數,當計數值達到500,即500 * 2 = 1000 ms 時候,即表示已經逝去了1 S的時間。我們再根據這個1 S的時間更新顯示緩沖區即可。聽起來很簡單,讓我們實現它吧。
首先在Timer.c中聲明如下兩個變量:
bit g_bTime1S = 0 ; //時鐘1S時標消息
static uint16 s_u16ClockTickCount = 0 ; //對2 ms 時標進行計數
再在定時器中斷函數中添加如下代碼:
if(++s_u16ClockTickCount == 500)
{
s_u16ClockTickCount = 0 ;
g_bTime1S = 1 ;
}
從上面可以看出,s_u16ClockTickCount計數值達到500的時候,g_bTime1S時標消息產生。然后我們根據這個時標消息刷新數碼管顯示緩沖區:
void RunClock(void)
{
if(g_bTime1S )
{
g_bTime1S = 0 ;
if(++g_u8LedDisplayBuffer[7] == 10)
{
g_u8LedDisplayBuffer[7] = 0 ;
if(++g_u8LedDisplayBuffer[6] == 6)
{
g_u8LedDisplayBuffer[6] = 0 ;
if(++g_u8LedDisplayBuffer[4] == 10)
{
g_u8LedDisplayBuffer[4] = 0 ;
if(++g_u8LedDisplayBuffer[3] == 6)
{
g_u8LedDisplayBuffer[3] = 0 ;
if( g_u8LedDisplayBuffer[0]<2)
{
if(++g_u8LedDisplayBuffer[1]==10)
{
g_u8LedDisplayBuffer[1] = 0 ;
g_u8LedDisplayBuffer[0]++;
}
}
else
{
if(++g_u8LedDisplayBuffer[1]==4)
{
g_u8LedDisplayBuffer[1] = 0 ;
g_u8LedDisplayBuffer[0] = 0 ;
}
}
}
}
}
}
}
}
這個函數的作用就是對每個數碼管緩沖位的值進行判斷,判斷的標準就是我們熟知的24小時制。如秒的個位到了10 就清0,同時秒的十位加1….諸如此類,我就不一一詳述了。
同時,我們再編寫一個時鐘初始值設置函數,這樣,可以很方便的在主程序開始的時候修改時鐘初始值。
void SetClock(uint8 nHour, uint8 nMinute, uint8 nSecond)
{
g_u8LedDisplayBuffer[0] = nHour / 10 ;
g_u8LedDisplayBuffer[1] = nHour % 10 ;
g_u8LedDisplayBuffer[2] = '-' ;
g_u8LedDisplayBuffer[3] = nMinute / 10 ;
g_u8LedDisplayBuffer[4] = nMinute % 10 ;
g_u8LedDisplayBuffer[5] = '-' ;
g_u8LedDisplayBuffer[6] = nSecond / 10 ;
g_u8LedDisplayBuffer[7] = nSecond % 10 ;
}
然后修改下我們的主函數如下:
void main(void)
{
io_led = 0 ; //發光二極管與數碼管共用P0口,這里禁止掉發光二極管的鎖存輸出
Timer0Init() ;
SetClock(10,20,30) ; //設置初始時間為10點20分30秒
EA = 1 ;
while(1)
{
LedDisplay(g_u8LedDisplayBuffer) ;
RunClock();
}
}
編譯好之后,下載到我們的實驗板上,怎么樣,一個簡單的時鐘就這樣誕生了。
至此,本章所訴就告一段落了。至于如何完成數碼管的閃爍顯示,就像本章開頭所說的那個數碼管時鐘的功能,就作為一個思考的問題留給大家思考吧。
同時整個LED篇就到此結束了,在以后的文章中,我們將開始學習如何編寫實用的按鍵掃描程序。
[/post
本章所附例程在EE21學習板上調試通過,擁有板子的朋友可以直接下載附件對照學習。
[ 此貼被紅金龍吸味在2010-01-09 13:54重新編輯 ]