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