找回密碼
         注冊會員
        搜索附件  
        MCU資訊論壇 附件中心 單片機論壇 51單片機論壇 5_50410_bc70e0fe3ba6253.jpg

        5_50410_bc70e0fe3ba6253.jpg

         

        “從單片機初學者邁向單片機工程師”之LED主題討論周第五章--多:
        [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重新編輯 ]


        5_50410_bc70e0fe3ba6253.jpg

        QQ|手機版|MCU資訊論壇 ( 京ICP備18035221號-2 )|網(wǎng)站地圖

        GMT+8, 2025-5-7 16:12 , Processed in 0.041629 second(s), 9 queries , Redis On.

        Powered by Discuz! X3.5

        © 2001-2025 Discuz! Team.

        返回頂部
        日本不卡一区二区| 91精品久久久久久无码| 国产成人亚洲精品影院| 国产在线精品一区二区高清不卡| 久久精品国产一区二区电影| 青青草原视频在线| 2022国内精品免费福利视频 | 国产成人精品免费视频网页大全| 亚洲国产精品久久久久网站| 国产精品视频久久久| 91亚洲国产成人久久精品| 久久精品成人免费国产片小草| 亚洲精品麻豆av| 经典国产乱子伦精品视频| www亚洲欲色成人久久精品| 亚洲成网777777国产精品| 国产精品天天看天天狠| 国产精品无码素人福利| 青青草精品视频| 精品国产日产一区二区三区| 精品无码久久久久久久久久| 亚洲精品无码不卡在线播放HE| 精品视频第一页| 合区精品久久久中文字幕一区| 国产精品亚韩精品无码a在线| 国产精品无码免费播放| 亚洲av午夜成人片精品网站| 大胸国产精品视频| 亚洲精品美女久久777777| 91自慰精品亚洲| 中文字幕在线亚洲精品| 9999国产精品欧美久久久久久| 欧美亚洲国产激情| 国产福利精品在线观看| 亚洲精品亚洲人成人网| 国产精品亚洲欧美大片在线看| 午夜精品一区二区三区免费视频 | 国产精品区牛牛影院| 国产精品美女WWW爽爽爽视频| 乱精品一区字幕二区| 精品伦精品一区二区三区视频 |