當(dāng)前位置:首頁 > 單片機 > 單片機
[導(dǎo)讀] 從單片機初學(xué)者邁向單片機工程師目錄:一、LED 主題討論周第一章----寫在前面......................................................... 1二、LED 主題討論周第二章----學(xué)會釋放CPU............................

 從單片機初學(xué)者邁向單片機工程師

目錄:

一、LED 主題討論周第一章----寫在前面......................................................... 1

二、LED 主題討論周第二章----學(xué)會釋放CPU................................................. 2

三、LED 主題討論周第三章----模塊化編程初識..............................................8

四、LED 主題討論周第四章----漸明漸暗的燈................................................25

五、LED 主題討論周第五章----多任務(wù)環(huán)境下的數(shù)碼管編程設(shè)計................. 28

六、KEY 主題討論第一章——按鍵程序編寫的基礎(chǔ)..................................... 37

七、KEY 主題討論第二章——基于狀態(tài)轉(zhuǎn)移的獨立按鍵程序設(shè)計.............. 40

八、綜合應(yīng)用之一——如何設(shè)計復(fù)雜的多任務(wù)程序...................................... 47

九、綜合應(yīng)用之二——DS1320/DS18B20 應(yīng)用...............................................60

----------------------------------------------------------------------------------------------------------------------------------------

一、LED 主題討論周第一章----寫在前面

學(xué)習(xí)單片機也已經(jīng)有幾年了,藉此機會和大家聊一下我學(xué)習(xí)過程中的一些經(jīng)歷和想法吧。也感謝一線工人

提供了這個機會。希望大家有什么好的想法和建議都直接跟帖說出來。畢竟只有交流才能夠碰撞出火花來

^_^。

“賣弄”也好,“吹噓”也罷,我只是想認(rèn)真的寫寫我這一路走來歷經(jīng)的總總,把其中值得注意,以及經(jīng)

驗的地方寫出來,權(quán)當(dāng)是我對自己的一個總結(jié)吧。而作為看官的你,如果看到了我的錯誤,還請一定指正,

這樣對我以及其它讀者都有幫助,而至于你如果從中能夠收獲到些許,那便是我最大的欣慰了。姑妄言之,

姑妄聽之。如果有啥好的想法和建議一定要說出來。ϑ幾年前,和眾多初學(xué)者一樣,我接觸到了單片機,立

刻被其神奇的功能所吸引,從此不能自拔。很多個日夜就這樣陪伴著它度過了。期間也遇到過非常多的問

題,也一度被這些問題所困惑……等到回過頭來,看到自己曾經(jīng)走過的路,唏噓不已。經(jīng)?;燠E于論壇里,

也看到了很多初學(xué)者發(fā)的求助帖子,看到他們走在自己曾走過的彎路上,忽然想到了自己的那段日子,心

里竟然莫名的沖動,凡此總總,我總是盡自己所能去回帖。很多時候,都想寫一點什么東西出來,希望對

廣大的初學(xué)者有一點點幫助。但總是不知從何處寫起。今天借一線工人的臺,唱一唱我的戲

一路學(xué)習(xí)過來的過程中,幫助最大之一無疑來自于網(wǎng)絡(luò)了。很多時候,通過網(wǎng)絡(luò),我們都可以獲取到所

需要的學(xué)習(xí)資料。但是,隨著我們學(xué)習(xí)的深入,我們會慢慢發(fā)現(xiàn),網(wǎng)絡(luò)提供的東西是有限度的,好像大部

分的資料都差不多,或者說是適合大部分的初學(xué)者所需,而當(dāng)我們想更進一步提高時,卻發(fā)現(xiàn)能夠獲取到

的資料越來越少,相信各位也會有同感,鋪天蓋地的單片機資料中大部分不是流水燈就是LED,液晶,而

且也只是僅僅作功能性的演示。于是有些人選擇了放棄,或者是轉(zhuǎn)移到其他興趣上面去了,而只有少部分

人選擇了繼續(xù)摸索下去,結(jié)合市面上的書籍,然后在網(wǎng)絡(luò)上鍥而不舍的搜集資料,再從牛人的只言片語中

去體會,不斷動手實踐,慢慢的,也摸索出來了自己的一條路子。當(dāng)然這個過程必然是艱辛的,而他學(xué)會

了之后也不會在網(wǎng)絡(luò)上輕易分享自己的學(xué)習(xí)成果。如此惡性循環(huán)下去,也就不難理解為什么初級的學(xué)習(xí)資

料滿天飛,而深入一點的學(xué)習(xí)資料卻很少的原因了。相較于其他領(lǐng)域,單片機技術(shù)的封鎖更加容易。盡管

已經(jīng)問世了很多年了,有價值的資料還是相當(dāng)?shù)那啡?,大部分的資料都是止于入門階段或者是簡單的演示

實驗。但是在實際工程應(yīng)用中卻是另外一回事。有能力的高手無暇或者是不愿公開自己的學(xué)習(xí)經(jīng)驗。

很多時候,我也很困惑,看到國外愛好者毫不保留的在網(wǎng)絡(luò)上發(fā)布自己的作品,我忽然感覺到一絲絲的

悲哀。也許,我們真的該轉(zhuǎn)變一下思路了,幫助別人,其實也是在幫助自己。啰啰嗦嗦的說了這么多,相

信大家能夠明白說的是什么意思。在接下來的一段日子里,我將會結(jié)合電子工程師之家舉辦的主題周活動

寫一點自己的想法。盡可能從實用的角度去講述。希望能夠幫助更多的初學(xué)者更上一層樓。而關(guān)于這個主

題周的最大主題我想了這樣的一個名字“從單片機初學(xué)者邁向單片機工程師”。名字挺大挺響亮,給我的壓

力也挺大的,但我會努力,爭取使這樣的一系列文章能夠帶給大家一點幫助,而不是看后大跌眼鏡。這樣

的一系列文章主要的對象是初學(xué)者,以及想從初學(xué)者更進一步提高的讀者。而至于老手,以及那些牛XX

的人,希望能夠給我們這些初學(xué)者更多的一些指點哈~@_@~.

二、LED 主題討論周第二章----學(xué)會釋放CPU

從這一章開始,我們開始邁入單片機的世界。在我們開始這一章具體的學(xué)習(xí)之前,有必要給大家先說明一

下。在以后的系列文章中,我們將以51 內(nèi)核的單片機為載體,C 語言為編程語言,開發(fā)環(huán)境為KEIL uv3。

至于為什么選用C 語言開發(fā),好處不言而喻,開發(fā)速度快,效率高,代碼可復(fù)用率高,結(jié)構(gòu)清晰,尤其是

在大型的程序中,而且隨著編譯器的不斷升級,其編譯后的代碼大小與匯編語言的差距越來越小。而關(guān)于

C 語言和匯編之爭,就像那個啥,每隔一段時間總會有人挑起這個話題,如果你感興趣,可以到網(wǎng)上搜索

相關(guān)的帖子自行閱讀。不是說匯編不重要,在很多對時序要求非常高的場合,需要利用匯編語言和C 語言

----------------------------------------------------------------------------------------------------------------------------------------

混合編程才能夠滿足系統(tǒng)的需求。在我們學(xué)習(xí)掌握C 語言的同時,也還需要利用閑余的時間去學(xué)習(xí)了解匯

編語言。

1.從點亮LED(發(fā)光二極管)開始

在市面上眾多的單片機學(xué)習(xí)資料中,最基礎(chǔ)的實驗無疑于點亮LED 了,即控制單片機的I/O 的電平的變化。

如同如下實例代碼一般

void main(void)

{

LedInit() ;

While(1)

{

LED = ON ;

DelayMs(500) ;

LED = OFF ;

DelayMs(500) ;

}

}

程序很簡單,從它的結(jié)構(gòu)可以看出,LED 先點亮500MS,然后熄滅500MS,如此循環(huán)下去,形成的效

果就是LED 以1HZ 的頻率進行閃爍。下面讓我們分析上面的程序有沒有什么問題。

看來看出,好像很正常的啊,能有什么問題呢?這個時候我們應(yīng)該換一個思路去想了。試想,整個程序除

了控制LED = ON ; LED = OFF; 這兩條語句外,其余的時間,全消耗在了DelayMs(500)這兩個函數(shù)

上。而在實際應(yīng)用系統(tǒng)中是沒有哪個系統(tǒng)只閃爍一只LED 就其它什么事情都不做了的。因此,在這里我們

要想辦法,把CPU 解放出來,讓它不要白白浪費500MS 的延時等待時間。寧可讓它一遍又一遍的掃描看

有哪些任務(wù)需要執(zhí)行,也不要讓它停留在某個地方空轉(zhuǎn)消耗CPU 時間。

從上面我們可以總結(jié)出

(1) 無論什么時候我們都要以實際應(yīng)用的角度去考慮程序的編寫。

(2) 無論什么時候都不要讓CPU 白白浪費等待,尤其是延時(超過1MS)這樣的地方。

下面讓我們從另外一個角度來考慮如何點亮一顆LED。

先看看我們的硬件結(jié)構(gòu)是什么樣子的。

我手上的單片機板子是電子工程師之家的開發(fā)的學(xué)習(xí)板。就以它的實際硬件連接圖來分析吧。如下圖所

----------------------------------------------------------------------------------------------------------------------------------------

一般的LED 的正常發(fā)光電流為10~20MA 而低電流LED 的工作電流在2mA 以下(亮度與普通發(fā)光管

相同)。在上圖中我們可知,當(dāng)Q1~Q8 引腳上面的電平為低電平時,LED 發(fā)光。通過LED 的電流約為(VCC

- Vd)/ RA2 。其中Vd 為LED 導(dǎo)通后的壓降,約為1.7V 左右。這個導(dǎo)通壓降根據(jù)LED 顏色的不同,以

及工作電流的大小的不同,會有一定的差別。下面一些參數(shù)是網(wǎng)上有人測出來的,供大家參考。

紅色的壓降為1.82-1.88V,電流5-8mA,

綠色的壓降為1.75-1.82V,電流3-5mA,

橙色的壓降為1.7-1.8V,電流3-5mA

蘭色的壓降為3.1-3.3V,電流8-10mA,

白色的壓降為3-3.2V,電流10-15mA,

(供電電壓5V,LED 直徑為5mm)

74HC573 真值表如下:

通過這個真值表我們可以看出。當(dāng)OutputEnable 引腳接低電平的時候,并且LatchEnable 引腳為高電

平的時候,Q 端電平與D 端電平相同。結(jié)合我們的LED 硬件連接圖可以知道LED_CS 端為高電平時候,

----------------------------------------------------------------------------------------------------------------------------------------

P0 口電平的變化即Q 端的電平的變化,進而引起LED 的亮滅變化。由于單片機的驅(qū)動能力有限,在此,

74HC573 的主要作用就是起一個輸出驅(qū)動的作用。需要注意的是,通過74HC573 的最大電流是有限制的,

否則可能會燒壞74HC573 這個芯片。

上面這個圖是從74HC573 的DATASHEET 中截取出來的,從上可以看出,每個引腳允許通過的最大電流

為35mA 整個芯片允許通過的最大電流為75mA。在我們設(shè)計相應(yīng)的驅(qū)動電路時候,這些參數(shù)是相當(dāng)重要

的,而且是最容易被初學(xué)者所忽略的地方。同時在設(shè)計的時候,要留出一定量的余量出來,不能說單個引

腳允許通過的電流為35mA,你就設(shè)計為35mA,這個時候你應(yīng)該把設(shè)計的上限值定在20mA 左右才能保

證能夠穩(wěn)定的工作。

(設(shè)計相應(yīng)驅(qū)動電路時候,應(yīng)該仔細(xì)閱讀芯片的數(shù)據(jù)手冊,了解每個引腳的驅(qū)動能力,以及整個芯片的驅(qū)

動能力)

了解了相應(yīng)的硬件后,我們再來編寫驅(qū)動程序。

首先定義LED 的接口

#define LED P0

然后為亮滅常數(shù)定義一個宏,由硬件連接圖可以,當(dāng)P0 輸出為低電平時候LED 亮,P0 輸出為高

電平時,LED 熄滅。

#define LED_ON() LED = 0x00 ; //所有LED 亮

#define LED_OFF() LED = 0xff ; //所有LED 熄滅

下面到了重點了,究竟該如何釋放CPU,避免其做延時空等待這樣的事情呢。很簡單,我們?yōu)橄到y(tǒng)產(chǎn)

生一個1MS 的時標(biāo)。假定LED 需要亮500MS,熄滅500MS,那么我們可以對這個1MS 的時標(biāo)進行計數(shù),

當(dāng)這個計數(shù)值達到500 時候,清零該計數(shù)值,同時把LED 的狀態(tài)改變。

unsigned int g_u16LedTimeCount = 0 ; //LED 計數(shù)器

unsigned char g_u8LedState = 0 ; //LED 狀態(tài)標(biāo)志, 0 表示亮,1 表示熄滅

void LedProcess(void)

{

if(0 == g_u8LedState) //如果LED 的狀態(tài)為亮,則點亮LED

{

LED_ON() ;

}

else //否則熄滅LED

{

LED_OFF() ;

}

}

void LedStateChange(void)

{

----------------------------------------------------------------------------------------------------------------------------------------

if(g_bSystemTime1Ms) //系統(tǒng)1MS 時標(biāo)到

{

g_bSystemTime1Ms = 0 ;

g_u16LedTimeCount++ ; //LED 計數(shù)器加一

if(g_u16LedTimeCount >= 500) //計數(shù)達到500,即500MS 到了,改變LED 的狀態(tài)。

{

g_u16LedTimeCount = 0 ;

g_u8LedState = ! g_u8LedState ;

}

}

}

上面有一個變量沒有提到,就是g_bSystemTime1Ms 。這個變量可以定義為位變量或者是其它變量,在

我們的定時器中斷函數(shù)中對其置位,其它函數(shù)使用該變量后,應(yīng)該對其復(fù)位(清0) 。

我們的主函數(shù)就可以寫成如下形式(示意代碼)

void main(void)

{

while(1)

{

LedProcess() ;

LedStateChange() ;

}

}

因為LED 的亮或者滅依賴于LED 狀態(tài)變量(g_u8LedState)的改變,而狀態(tài)變量的改變,又依賴于LED 計

數(shù)器的計數(shù)值(g_u16LedTimeCount ,只有計數(shù)值達到一定后,狀態(tài)變量才改變)所以,兩個函數(shù)都沒有堵

塞CPU 的地方。讓我們來從頭到尾分析一遍整個程序的流程。

程序首先執(zhí)行LedProcess() ;函數(shù)

因為g_u8LedState 的初始值為0 (見定義,對于全局變量,在定義的時候最好給其一個確定的值)所以LED

被點亮,然后退出LedStateChange()函數(shù),執(zhí)行下一個函數(shù)LedStateChange()

在函數(shù)LedStateChange()內(nèi)部首先判斷1MS 的系統(tǒng)時標(biāo)是否到了,如果沒有到就直接退出函數(shù),如果到

了,就把時標(biāo)清0 以便下一個時標(biāo)消息的到來,同時對LED 計數(shù)器加一,然后再判斷LED 計數(shù)器是否到

達我們預(yù)先想要的值500,如果沒有,則退出函數(shù),如果有,對計數(shù)器清0,以便下次重新計數(shù),同時把

LED 狀態(tài)變量取反,然后退出函數(shù)。

由上面整個流程可以知道,CPU 所做的事情,就是對一些計數(shù)器加一,然后根據(jù)條件改變狀態(tài),再根據(jù)這

個狀態(tài)來決定是否點亮LED。這些函數(shù)執(zhí)行所花的時間都是相當(dāng)短的,如果主程序中還有其它函數(shù),則

CPU 會順次往下執(zhí)行下去。對于其它的函數(shù)(如果有的話)也要采取同樣的措施,保證其不堵塞CPU,如果

全部基于這種方法設(shè)計,那么對于不是非常龐大的系統(tǒng),我們的系統(tǒng)依舊可以保證多個任務(wù)(多個函數(shù))同

時執(zhí)行。系統(tǒng)的實時性得到了一定的保證,從宏觀上看來,就是多個任務(wù)并發(fā)執(zhí)行。

好了,這一章就到此為止,讓我們總結(jié)一下,究竟有哪些需要注意的吧。

(1) 無論什么時候我們都要以實際應(yīng)用的角度去考慮程序的編寫。

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 7

(2) 無論什么時候都不要讓CPU 白白浪費等待,尤其是延時(超過1MS)這樣的地方。

(3) 設(shè)計相應(yīng)驅(qū)動電路時候,應(yīng)該仔細(xì)閱讀芯片的數(shù)據(jù)手冊,了解每個引腳的驅(qū)動能力,

以及整個芯片的驅(qū)動能力

(4) 最重要的是,如何去釋放CPU(參考本章的例子),這是寫出合格程序的基礎(chǔ)。

附完整程序代碼(基于電子工程師之家的單片機開發(fā)板)

#include

sbit LED_SEG = P1^4; //數(shù)碼管段選

sbit LED_DIG = P1^5; //數(shù)碼管位選

sbit LED_CS11 = P1^6; //led 控制位

sbit ir=P1^7;

#define LED P0 //定義LED 接口

bit g_bSystemTime1Ms = 0 ; // 1MS 系統(tǒng)時標(biāo)

unsigned int g_u16LedTimeCount = 0 ; //LED 計數(shù)器

unsigned char g_u8LedState = 0 ; //LED 狀態(tài)標(biāo)志, 0 表示亮,1 表示熄滅

#define LED_ON() LED = 0x00 ; //所有LED 亮

#define LED_OFF() LED = 0xff ; //所有LED 熄滅

void Timer0Init(void)

{

TMOD &= 0xf0 ;

TMOD |= 0x01 ; //定時器0 工作方式1

TH0 = 0xfc ; //定時器初始值

TL0 = 0x66 ;

TR0 = 1 ;

ET0 = 1 ;

}

void LedProcess(void)

{

if(0 == g_u8LedState) //如果LED 的狀態(tài)為亮,則點亮LED

{

LED_ON() ;

}

else //否則熄滅LED

{

LED_OFF() ;

}

}

void LedStateChange(void)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 8

{

if(g_bSystemTime1Ms) //系統(tǒng)1MS 時標(biāo)到

{

g_bSystemTime1Ms = 0 ;

g_u16LedTimeCount++ ; //LED 計數(shù)器加一

if(g_u16LedTimeCount >= 500) //計數(shù)達到500,即500MS 到了,改變LED 的狀態(tài)。

{

g_u16LedTimeCount = 0 ;

g_u8LedState = ! g_u8LedState ;

}

}

}

void main(void)

{

Timer0Init() ;

EA = 1 ;

LED_CS11 = 1 ; //74HC595 輸出允許

LED_SEG = 0 ; //數(shù)碼管段選和位選禁止(因為它們和LED 共用P0 口)

LED_DIG = 0 ;

while(1)

{

LedProcess() ;

LedStateChange() ;

}

}

void Time0Isr(void) interrupt 1

{

TH0 = 0xfc ; //定時器重新賦初值

TL0 = 0x66 ;

g_bSystemTime1Ms = 1 ; //1MS 時標(biāo)標(biāo)志位置位

}

實際效果圖如下

點亮

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 9

熄滅

三、LED 主題討論周第三章----模塊化編程初識

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 10

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 11

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 12

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 13

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 14

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 15

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 16

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 17

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 18

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 19

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 20

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 21

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 22

OK ,到此一個簡單的工程模板就建立起來了,以后我們再新建源文件和頭文件的時候,

就可以直接保存到src 文件目錄下面了。

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 23

下面我們開始編寫各個模塊文件。

首先編寫Timer.c 這個文件主要內(nèi)容就是定時器初始化,以及定時器中斷服務(wù)函數(shù)。其內(nèi)容

如下。

#include

bit g_bSystemTime1Ms = 0 ; // 1MS 系統(tǒng)時標(biāo)

void Timer0Init(void)

{

TMOD &= 0xf0 ;

TMOD |= 0x01 ; //定時器0 工作方式1

TH0 = 0xfc ; //定時器初始值

TL0 = 0x66 ;

TR0 = 1 ;

ET0 = 1 ;

}

void Time0Isr(void) interrupt 1

{

TH0 = 0xfc ; //定時器重新賦初值

TL0 = 0x66 ;

g_bSystemTime1Ms = 1 ; //1MS 時標(biāo)標(biāo)志位置位

}

由于在Led.c 文件中需要調(diào)用我們的g_bSystemTime1Ms 變量。同時主函數(shù)需要調(diào)用

Timer0Init()初始化函數(shù),所以應(yīng)該對這個變量和函數(shù)在頭文件里作外部聲明。以方便其它

函數(shù)調(diào)用。

Timer.h 內(nèi)容如下。

#ifndef _TIMER_H_

#define _TIMER_H_

extern void Timer0Init(void) ;

extern bit g_bSystemTime1Ms ;

#endif

完成了定時器模塊后,我們開始編寫LED 驅(qū)動模塊。

Led.c 內(nèi)容如下:

#include

#include "MacroAndConst.h"

#include "Led.h"

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 24

#include "Timer.h"

static uint16 g_u16LedTimeCount = 0 ; //LED 計數(shù)器

static uint8 g_u8LedState = 0 ; //LED 狀態(tài)標(biāo)志, 0 表示亮,1 表示熄滅

#define LED P0 //定義LED 接口

#define LED_ON() LED = 0x00 ; //所有LED 亮

#define LED_OFF() LED = 0xff ; //所有LED 熄滅

void LedProcess(void)

{

if(0 == g_u8LedState) //如果LED 的狀態(tài)為亮,則點亮LED

{

LED_ON() ;

}

else //否則熄滅LED

{

LED_OFF() ;

}

}

void LedStateChange(void)

{

if(g_bSystemTime1Ms) //系統(tǒng)1MS 時標(biāo)到

{

g_bSystemTime1Ms = 0 ;

g_u16LedTimeCount++ ; //LED 計數(shù)器加一

if(g_u16LedTimeCount >= 500) //計數(shù)達到500,即500MS 到了,改變LED 的狀態(tài)。

{

g_u16LedTimeCount = 0 ;

g_u8LedState = ! g_u8LedState ;

}

}

}

這個模塊對外的借口只有兩個函數(shù),因此在相應(yīng)的Led.h 中需要作相應(yīng)的聲明。

Led.h 內(nèi)容:

#ifndef _LED_H_

#define _LED_H_

extern void LedProcess(void) ;

extern void LedStateChange(void) ;

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 25

#endif

這兩個模塊完成后,我們將其C 文件添加到工程中。然后開始編寫主函數(shù)里的代碼。

如下所示:

#include

#include "MacroAndConst.h"

#include "Timer.h"

#include "Led.h"

sbit LED_SEG = P1^4; //數(shù)碼管段選

sbit LED_DIG = P1^5; //數(shù)碼管位選

sbit LED_CS11 = P1^6; //led 控制位

void main(void)

{

LED_CS11 = 1 ; //74HC595 輸出允許

LED_SEG = 0 ; //數(shù)碼管段選和位選禁止(因為它們和LED 共用P0 口)

LED_DIG = 0 ;

Timer0Init() ;

EA = 1 ;

while(1)

{

LedProcess() ;

LedStateChange() ;

}

}

整個工程截圖如下:

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 26

至此,第三章到此結(jié)束。

一起來總結(jié)一下我們需要注意的地方吧

[color=#FF0000]1. C語言源文件(*.c)的作用是什么

2. C語言頭文件(*.h)的作用是什么

3. typedef 的作用

4. 工程模板如何組織

5. 如何創(chuàng)建一個多模塊(多文件)的工程

四、LED 主題討論周第四章----漸明漸暗的燈

看著學(xué)習(xí)板上的LED 按照我們的意愿開始閃爍起來,你心里是否高興了,我相信你會的。但是很快你就會

感覺到太單調(diào),總是同一個頻率在閃爍,總是同一個亮度在閃爍。如果要是能夠由暗逐漸變亮,然后再由

亮變暗該多漂亮啊。嗯,想法不錯,可以該從什么地方入手呢。

在開始我們的工程之前,首先來了解一個概念:PWM。

PWM(Pulse Width Modulation)是脈沖寬度調(diào)制的英文單詞的縮寫。下面這段話是通信百科中對其的定義:

脈沖寬度調(diào)制(PWM)是利用微處理器的數(shù)字輸出來對模擬電路進行控制的一種非常有效的技術(shù),廣泛應(yīng)用

在從測量、通信到功率控制與變換的許多領(lǐng)域中。脈寬調(diào)制是開關(guān)型穩(wěn)壓電源中的術(shù)語。這是按穩(wěn)壓的控

制方式分類的,除了PWM 型,還有PFM 型和PWM、PFM 混合型。脈寬調(diào)制式開關(guān)型穩(wěn)壓電路是在控制

電路輸出頻率不變的情況下,通過電壓反饋調(diào)整其占空比,從而達到穩(wěn)定輸出電壓的目的。

讀起來有點晦澀難懂。其實簡單的說來,PWM 技術(shù)就是通過調(diào)整一個周期固定的方波的占空比,來調(diào)節(jié)

輸出電壓的平均當(dāng)電壓,電流或者功率等被控量。我們可以用一個水龍頭來類比,把1S 時間分成50 等份,

即每一個等份20MS。在這20MS 時間里如果我們把水龍頭水閥一直打開,那么在這20MS 里流過的水肯

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 27

定是最多的,如果我們把水閥打開15MS,剩下的5MS 關(guān)閉水閥,那么流出的水相比剛才20MS 全開肯定

要小的多。同樣的道理,我們可以通過控制20MS 時間里水閥開啟的時間的長短來控制流過的水的多少。

那么在1S 內(nèi)平均流出的水流量也就可以被控制了。

當(dāng)我們調(diào)整PWM 的占空比時,就會引起電壓或者電流的改變,LED 的明暗狀態(tài)就會隨之發(fā)生相應(yīng)的變化,

聽起來好像可以通過這種方法來實現(xiàn)我們想要的漸明漸暗的效果。讓我們來試一下吧。

大家都知道人眼有一個臨界頻率,當(dāng)LED 的閃爍頻率達到一定的時候,人眼就分辨不出LED 是否在閃爍

了。就像我們平常看電視一樣,看起來畫面是連續(xù)的,實質(zhì)不是這個樣子,所有連續(xù)動作都是一幀幀靜止

的畫面在1S 的時間里快速播放出來,譬如每秒24 幀的速度播放,由于人眼的視覺暫留效應(yīng),看起來畫面

就是連續(xù)的了。同樣的道理,為了讓我們的LED 在變化的過程中,我們感覺不到其在閃爍,可以將其閃爍

的頻率定在50Hz 以上。同時為了看起來明暗過渡的效果更加明顯,我們在這里定義其變化范圍為0~99(100

等分).即最亮的時候其灰度等級為99,為0 的時候最暗,也就是熄滅了。

于是乎我們定義PWM 的占空比上限為99, 下限定義為0

#define LED_PWM_LIMIT_MAX 99

#define LED_PWM_LIMIT_MIN 0

假定我們LED 的閃爍頻率為50HZ,而亮度變化的范圍為0~99 共100 等分。則每一等分所占用的時間為

1/(50*100) = 200us 即我們在改變LED 的亮滅狀態(tài)時,應(yīng)該是在200us 整數(shù)倍時刻時。在這里我們用

單片機的定時器產(chǎn)生200us 的中斷,同時每20MS 調(diào)整一次LED 的占空比。這樣在20MS * 100 = 2S 的

時間內(nèi)LED 可以從暗逐漸變亮,在下一個2S 內(nèi)可以從亮逐漸變暗,然后不斷循環(huán)。

由于大部分的內(nèi)容都可以在中斷中完成,因此,我們的大部分代碼都在Timer.c 這個文件中編寫,主函數(shù)

中除了初始化之外,就是一個空的死循環(huán)。

Timer.c 內(nèi)容如下。

#include

#include "MacroAndConst.h"

#define LED P0 //定義LED 接口

#define LED_ON() LED = 0x00 ; //所有LED 亮

#define LED_OFF() LED = 0xff ; //所有LED 熄滅

#define LED_PWM_LIMIT_MAX 99

#define LED_PWM_LIMIT_MIN 0

static uint8 s_u8TimeCounter = 0 ; //中斷計數(shù)

static uint8 s_u8LedDirection = 0 ; //LED 方向控制0 :漸亮1 :漸滅

static int8 s_s8LedPWMCounter = 0 ; //LED 占空比

void Timer0Init(void)

{

TMOD &= 0xf0 ;

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 28

TMOD |= 0x01 ; //定時器0 工作方式1

TH0 = 0xff ; //定時器初始值(200us 中斷一次)

TL0 = 0x47 ;

TR0 = 1 ;

ET0 = 1 ;

}

void Time0Isr(void) interrupt 1

{

static int8 s_s8PWMCounter = 0 ;

TH0 = 0xff ; //定時器重新賦初值

TL0 = 0x47 ;

if(++s_u8TimeCounter >= 100) //每20MS 調(diào)整一下LED 的占空比

{

s_u8TimeCounter = 0 ;

//如果是漸亮方向變化,則占空比遞增

if((s_s8LedPWMCounter <= LED_PWM_LIMIT_MAX) &&(0 == s_u8LedDirection))

{

s_s8LedPWMCounter++ ;

if(s_s8LedPWMCounter > LED_PWM_LIMIT_MAX)

{

s_u8LedDirection = 1 ;

s_s8LedPWMCounter = LED_PWM_LIMIT_MAX ;

}

}

//如果是漸暗方向變化,則占空比遞漸

if((s_s8LedPWMCounter >= LED_PWM_LIMIT_MIN) &&(1 == s_u8LedDirection))

{

s_s8LedPWMCounter-- ;

if(s_s8LedPWMCounter < LED_PWM_LIMIT_MIN)

{

s_u8LedDirection = 0 ;

s_s8LedPWMCounter = LED_PWM_LIMIT_MIN ;

}

}

s_s8PWMCounter = s_s8LedPWMCounter ; //獲取LED 的占空比

}

if(s_s8PWMCounter > 0) //占空比大于0,則點亮LED,否則熄滅LED

{

LED_ON() ;

s_s8PWMCounter-- ;

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 29

}

else

{

LED_OFF();

}

}

其實PWM 技術(shù)在我們實際生活中應(yīng)用的非常多。比較典型的應(yīng)用就是控制電機的轉(zhuǎn)速,控制充電電流的

大小,等等。而隨著技術(shù)的發(fā)展,也出現(xiàn)了其他類型的PWM 技術(shù),如相電壓PWM,線電壓PWM,SPWM

等等,如果有興趣可以到網(wǎng)上去獲取相應(yīng)資料學(xué)習(xí)。

關(guān)于漸明漸暗的燈就簡單的講到這里。

五、LED 主題討論周第五章----多任務(wù)環(huán)境下的數(shù)碼管編程

設(shè)計

[post]數(shù)碼管在實際應(yīng)用中非常廣泛,尤其是在某些對成本有限制的場合。編寫一個好用的

LED 程序并不是那么的簡單。曾經(jīng)有人這樣說過,如果用數(shù)碼管和按鍵,做一個簡易的可

以調(diào)整的時鐘出來,那么你的單片機就算入門了60%了。此話我深信不疑。我遇到過很多

單片機的愛好者,他們問我說單片機我已經(jīng)掌握了,該如何進一步的學(xué)習(xí)下去呢?我并不急

于回答他們的問題,而是問他們:會編寫數(shù)碼管的驅(qū)動程序了吧?“嗯”。會編寫按鍵程序了

吧?“嗯”。好,我給你出一個小題目,你做一下。用按鍵和數(shù)碼管以及單片機定時器實現(xiàn)一

個簡易的可以調(diào)整的時鐘,要求如下:

8 位數(shù)碼管顯示,顯示格式如下

時-分-秒

XX-XX-XX

要求:系統(tǒng)有四個按鍵,功能分別是調(diào)整,加,減,確定。在按下調(diào)整鍵時候,顯示時的

兩位數(shù)碼管以1 Hz 頻率閃爍。如果再次按下調(diào)整鍵,則分開始閃爍,時恢復(fù)正常顯示,依

次循環(huán),直到按下確定鍵,恢復(fù)正常的顯示。在數(shù)碼管閃爍的時候,按下加或者減鍵可以調(diào)

整相應(yīng)的顯示內(nèi)容。按鍵支持短按,和長按,即短按時,修改的內(nèi)容每次增加一或者減小一,

長按時候以一定速率連續(xù)增加或者減少。

結(jié)果很多人,很多愛好者一下子都理不清楚思路。其實問題的根源在于沒有以工程化的角度

去思考程序的編寫。很多人在學(xué)習(xí)數(shù)碼管編程的時候,都是照著書上或者網(wǎng)上的例子來進行

試驗。殊不知,這些例子代碼僅僅只是具有一個演示性的作用,拿到實際中是很難用的。舉

一個簡單的例子。

下面這段程序是在網(wǎng)上隨便搜索到的:

while(1)

{

for(num=0;num<9;num++)

{

P0=table[num];

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 30

P2=code[num] ;

delayms(2) ;

}

}

看出什么問題來了沒有,如果沒有看出來請仔細(xì)想一下,如果還沒有想出來,請回過頭去,

認(rèn)真再看一遍“學(xué)會釋放CPU”這一章的內(nèi)容。這個程序作為演示程序是沒有什么問題的,但

是實際應(yīng)用的時候,數(shù)碼管顯示的內(nèi)容經(jīng)常變化,而且還有很多其它任務(wù)需要執(zhí)行,因此這

樣的程序在實際中是根本就無法用的,更何況,它這里也調(diào)用了delayms(2)這個函數(shù)來延

時2 ϑms 這更是令我們深惡痛絕

本章的內(nèi)容正是探討如何解決多任務(wù)環(huán)境下(不帶OS)的數(shù)碼管程序設(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ā)板為例)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 31

由上圖可以看出。8 個數(shù)碼管的段碼由一個單獨的74HC573 驅(qū)動。同時每一個數(shù)碼管的公

共端連接在另外一個74HC573 的輸出上。當(dāng)送出第一位數(shù)碼管的段碼內(nèi)容時候,同時選通

第一位數(shù)碼管的位選,此時,第一位數(shù)碼管就顯示出相應(yīng)的內(nèi)容了。一段時間之后,送出第

二位數(shù)碼管段碼的內(nèi)容,選通第二位數(shù)碼管的位選,這時顯示的內(nèi)容就變成第二位數(shù)碼管的

內(nèi)容了……依次循環(huán)下去,就可以看到了所有數(shù)碼管同時顯示了。事實上,任意時刻,只有

一位數(shù)碼管是被點亮的。由于人眼的視覺暫留效應(yīng)以及數(shù)碼管的余輝效應(yīng),當(dāng)數(shù)碼管掃描的

頻率非??斓臅r候,人眼已經(jīng)無法分辨出數(shù)碼管的變化了,看起來就是同時點亮的。我們假

設(shè)數(shù)碼管的掃描頻率為50 Hz, 則完成一輪掃描的時間就是1 / 50 = 20 ms 。我們的系統(tǒng)共

有8 位數(shù)碼管,則每一位數(shù)碼管在一輪掃描周期中點亮的時間為20 / 8 = 2.5 ms 。

動態(tài)掃描對時間要求有一點點嚴(yán)格,否則,就會有明顯的閃爍。

假設(shè)我們程序中所有任務(wù)如下:

while(1)

{

LedDisplay() ; //數(shù)碼管動態(tài)掃描

ADProcess() ; //AD 采集處理

TimerProcess() ; //時間相關(guān)處理

DataProcess() ; //數(shù)據(jù)處理

}

LedDisplay() 這個任務(wù)的執(zhí)行時間,如同我們剛才計算的那樣,50 Hz 頻率掃描,則該函數(shù)

執(zhí)行的時間為20 ms 。假設(shè)ADProcess()這個任務(wù)執(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()這一個任務(wù)的時候得出來的結(jié)果。當(dāng)系統(tǒng)添加了其它任務(wù)后,當(dāng)然系統(tǒng)循環(huán)執(zhí)

行一次的總時間就增加了。如何解決這種現(xiàn)象了,還是離不開我們第二章所講的那個思想。

系統(tǒng)產(chǎn)生一個2.5 ms 的時標(biāo)消息。LedDisplay() , 每次接收到這個消息的時候, 掃描一位

數(shù)碼管。這樣8 個時標(biāo)消息過后,所有的數(shù)碼管就都被掃描一遍了??赡苡信笥褧羞@樣

的疑問:ADProcess() 以及DataProcess() 等函數(shù)執(zhí)行的時間還是需要十幾ms 啊,在這

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 32

十幾ms 的時間里,已經(jīng)產(chǎn)生好幾個2.5 ms 的時標(biāo)消息了,這樣豈不是漏掉了掃描,顯示

起來還是會閃爍。能夠想到這一點,很不錯,這也就是為什么我們要學(xué)會釋放CPU 的原因。

對于ADProcess(),TimerProcess(),DataProcess(),等任務(wù)我們依舊要采取此方法對CPU

進行釋放,使其執(zhí)行的時間盡可能短暫,關(guān)于如何做到這一點,在以后的講解如何設(shè)計多任

務(wù)程序設(shè)計的時候會講解到。

下面我們基于此思路開始編寫具體的程序。

首先編寫Timer.c 文件。該文件中主要為系統(tǒng)提供時間相關(guān)的服務(wù)。必要的頭文件包含。

#include

#include "MacroAndConst.h"

為了方便計算,我們?nèi)?shù)碼管掃描一位的時間為2 ms。設(shè)置定時器0 為2 ms 中斷一次。

同時聲明一個位變量,作為2 ms 時標(biāo)消息的標(biāo)志

bit g_bSystemTime2Ms = 0 ; // 2msLED 動態(tài)掃描時標(biāo)消息

初始化定時器0

void Timer0Init(void)

{

TMOD &= 0xf0 ;

TMOD |= 0x01 ; //定時器0 工作方式1

TH0 = 0xf8 ; //定時器初始值

TL0 = 0xcc ;

TR0 = 1 ;

ET0 = 1 ;

}

在定時器0 中斷處理程序中,設(shè)置時標(biāo)消息。

void Time0Isr(void) interrupt 1

{

TH0 = 0xf8 ; //定時器重新賦初值

TL0 = 0xcc ;

g_bSystemTime2Ms = 1 ; //2MS 時標(biāo)標(biāo)志位置位

}

然后我們開始編寫數(shù)碼管的動態(tài)掃描函數(shù)。

新建一個C 源文件,并包含相應(yīng)的頭文件。

#include

#include "MacroAndConst.h"

#include "Timer.h"

先開辟一個數(shù)碼管顯示的緩沖區(qū)。動態(tài)掃描函數(shù)負(fù)責(zé)從這個緩沖區(qū)中取出數(shù)據(jù),并掃描顯示。

而其它函數(shù)則可以修改該緩沖區(qū),從而改變顯示的內(nèi)容。

uint8 g_u8LedDisplayBuffer[8] = {0} ; //顯示緩沖區(qū)

然后定義共陽數(shù)碼管的段碼表以及相應(yīng)的硬件端口連接。

code uint8 g_u8LedDisplayCode[]=

{

0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,

0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E,

0xbf, //'-'號代碼

} ;

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 33

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) ; //消隱,只需要設(shè)置位選不為0~7 即可

if(pBuffer[s_LedDisPos] == '-') //顯示'-'號

{

SendLedSegData(g_u8LedDisplayCode[16]) ;

}

else

{

SendLedSegData(g_u8LedDisplayCode[pBuffer[s_LedDisPos]]) ;

}

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 34

SendLedBitData(s_LedDisPos);

if(++s_LedDisPos > 7)

{

s_LedDisPos = 0 ;

}

}

}

函數(shù)內(nèi)部定義一個靜態(tài)的變量s_LedDisPos,用來表示掃描數(shù)碼管的位置。每當(dāng)我們執(zhí)行

該函數(shù)一次的時候,s_LedDisPos 的值會自加1,表示下次掃描下一個數(shù)碼管。然后判斷

g_bSystemTime2Ms 時標(biāo)消息是否到了。如果到了,就開始執(zhí)行相關(guān)掃描,否則就直接跳

出函數(shù)。SendLedBitData(8) ;的作用是消隱。因為我們的系統(tǒng)的段選和位選是共用P0 口的。

在送段碼之前,必須先關(guān)掉位選,否則,因為上次位選是選通的,在送段碼的時候會造成相

應(yīng)數(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]]) ;送出相應(yīng)的段碼數(shù)

據(jù)后,然后通過SendLedBitData(s_LedDisPos);打開相應(yīng)的位選。這樣對應(yīng)的數(shù)碼管就被

點亮了。

if(++s_LedDisPos > 7)

{

s_LedDisPos = 0 ;

}

然后s_LedDisPos 自加1,以便下次執(zhí)行本函數(shù)時,掃描下一個數(shù)碼管。因為我們的系統(tǒng)

共有8 個數(shù)碼管,所以當(dāng)s_LedDisPos > 7 后,要對其進行清0 。否則,沒有任何一個數(shù)

碼管被選中。這也是為什么我們可以用

SendLedBitData(8) ; //消隱,只需要設(shè)置位選不為0~7 即可

對數(shù)碼管進行消隱操作的原因。

下面我們來編寫相應(yīng)的主函數(shù),并實現(xiàn)數(shù)碼管上面類似時鐘的效果,如顯示10-20-30

即10 點20 分30 秒。

Main.c

#include

#include "MacroAndConst.h"

#include "Timer.h"

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 35

#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) ;

}

}

ϑ將整個工程進行編譯,看看效果如何

動起來

既然我們想要模擬一個時鐘,那么時鐘肯定是要走動的,不然還稱為什么時鐘撒。下面我們

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 36

在前面的基礎(chǔ)之上,添加一點相應(yīng)的代碼,讓我們這個時鐘走動起來。

我們知道,之前我們以及設(shè)置了一個掃描數(shù)碼管用到的2 ms 時標(biāo)。如果我們再對這個時

標(biāo)進行計數(shù),當(dāng)計數(shù)值達到500,即500 * 2 = 1000 ms 時候,即表示已經(jīng)逝去了1 S 的時

間。我們再根據(jù)這個1 S 的時間更新顯示緩沖區(qū)即可。聽起來很簡單,讓我們實現(xiàn)它吧。

首先在Timer.c 中聲明如下兩個變量:

bit g_bTime1S = 0 ; //時鐘1S 時標(biāo)消息

static uint16 s_u16ClockTickCount = 0 ; //對2 ms 時標(biāo)進行計數(shù)

再在定時器中斷函數(shù)中添加如下代碼:

if(++s_u16ClockTickCount == 500)

{

s_u16ClockTickCount = 0 ;

g_bTime1S = 1 ;

}

從上面可以看出,s_u16ClockTickCount 計數(shù)值達到500 的時候,g_bTime1S 時標(biāo)消息產(chǎn)

生。然后我們根據(jù)這個時標(biāo)消息刷新數(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)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 37

{

g_u8LedDisplayBuffer[1] = 0 ;

g_u8LedDisplayBuffer[0] = 0 ;

}

}

}

}

}

}

}

}

這個函數(shù)的作用就是對每個數(shù)碼管緩沖位的值進行判斷,判斷的標(biāo)準(zhǔn)就是我們熟知的24 小

時制。如秒的個位到了10 就清0,同時秒的十位加1….諸如此類,我就不一一詳述了。

同時,我們再編寫一個時鐘初始值設(shè)置函數(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) ; //設(shè)置初始時間為10 點20 分30 秒

EA = 1 ;

while(1)

{

LedDisplay(g_u8LedDisplayBuffer) ;

RunClock();

}

}

編譯好之后,下載到我們的實驗板上,怎么樣,一個簡單的時鐘就這樣誕生了。

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 38

至此,本章所訴就告一段落了。至于如何完成數(shù)碼管的閃爍顯示,就像本章開頭所說的那個

數(shù)碼管時鐘的功能,就作為一個思考的問題留給大家思考吧。

同時整個LED 篇就到此結(jié)束了,在以后的文章中,我們將開始學(xué)習(xí)如何編寫實用的按鍵掃

描程序。

[/post

本章所附例程在EE21 學(xué)習(xí)板上調(diào)試通過,擁有板子的朋友可以直接下載附件對照學(xué)習(xí)

六、KEY 主題討論第一章——按鍵程序編寫的基礎(chǔ)

從這一章開始,我們步入按鍵程序設(shè)計的殿堂。在基于單片機為核心構(gòu)成的應(yīng)用系統(tǒng)中,用

戶輸入是必不可少的一部分。輸入可以分很多種情況,譬如有的系統(tǒng)支持PS2 鍵盤的接口,

有的系統(tǒng)輸入是基于編碼器,有的系統(tǒng)輸入是基于串口或者USB 或者其它輸入通道等等。

在各種輸入途徑中,更常見的是,基于單個按鍵或者由單個鍵盤按照一定排列構(gòu)成的矩陣鍵

盤(行列鍵盤)。我們這一篇章主要討論的對象就是基于單個按鍵的程序設(shè)計,以及矩陣鍵盤

的程序編寫。

◎按鍵檢測的原理

常見的獨立按鍵的外觀如下,相信大家并不陌生,各種常見的開發(fā)板學(xué)習(xí)板上隨處可以看到

他們的身影。

總共有四個引腳,一般情況下,處于同一邊的兩個引腳內(nèi)部是連接在一起的,如何分辨

兩個引腳是否處在同一邊呢?可以將按鍵翻轉(zhuǎn)過來,處于同一邊的兩個引腳,有一條突起的

線將他們連接一起,以標(biāo)示它們倆是相連的。如果無法觀察得到,用數(shù)字萬用表的二極管擋

位檢測一下即可。搞清楚這點非常重要,對于我們畫PCB 的時候的封裝很有益。

它們和我們的單片機系統(tǒng)的I/O 口連接一般如下:

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 39

對于單片機I/O 內(nèi)部有上拉電阻的微控制器而言,還可以省掉外部的那個上拉電阻。簡

單分析一下按鍵檢測的原理。當(dāng)按鍵沒有按下的時候,單片機I/O 通過上拉電阻R 接到VCC,

我們在程序中讀取該I/O 的電平的時候,其值為1(高電平); 當(dāng)按鍵S 按下的時候,該I/O

被短接到GND,在程序中讀取該I/O 的電平的時候,其值為0(低電平) 。這樣,按鍵的按

下與否,就和與該按鍵相連的I/O 的電平的變化相對應(yīng)起來。結(jié)論:我們在程序中通過檢測到

該I/O 口電平的變化與否,即可以知道按鍵是否被按下,從而做出相應(yīng)的響應(yīng)。一切看起來很美好,是

這樣的嗎?

◎現(xiàn)實并非理想

在我們通過上面的按鍵檢測原理得出上述的結(jié)論的時候,其實忽略了一個重要的問題,那就

是現(xiàn)實中按鍵按下時候的電平變化狀態(tài)。我們的結(jié)論是基于理想的情況得出來的,就如同下

面這幅按鍵按下時候?qū)?yīng)電平變化的波形圖一樣:

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 40

而實際中,由于按鍵的彈片接觸的時候,并不是一接觸就緊緊的閉合,它還存在一定的

抖動,盡管這個時間非常的短暫,但是對于我們執(zhí)行時間以us 為計算單位的微控制器來說,

它太漫長了。因而,實際的波形圖應(yīng)該如下面這幅示意圖一樣。

這樣便存在這樣一個問題。假設(shè)我們的系統(tǒng)有這樣功能需求:在檢測到按鍵按下的時候,將

某個I/O 的狀態(tài)取反。由于這種抖動的存在,使得我們的微控制器誤以為是多次按鍵的按下,

從而將某個I/O 的狀態(tài)不斷取反,這并不是我們想要的效果,假如該I/O 控制著系統(tǒng)中某個

重要的執(zhí)行的部件,那結(jié)果更不是我們所期待的。于是乎有人便提出了軟件消除抖動的思想,

道理很簡單:抖動的時間長度是一定的,只要我們避開這段抖動時期,檢測穩(wěn)定的時候的電

平不久可以了嗎?聽起來確實不錯,而且實際應(yīng)用起來效果也還可以。于是,各種各樣的書

籍中,在提到按鍵檢測的時候,總也不忘說道軟件消抖。就像下面的偽代碼所描述的一樣。

(假設(shè)按鍵按下時候,低電平有效)

If(0 == io_KeyEnter) //如果有鍵按下了

{

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 41

Delayms(20) ; //先延時20ms 避開抖動時期

If(0 == io_KeyEnter) //然后再檢測,如果還是檢測到有鍵按下

{

return KeyValue ; //是真的按下了,返回鍵值

}

else

{

return KEY_NULL //是抖動,返回空的鍵值

}

while(0 == io_KeyEnter) ; //等待按鍵釋放

}

乍看上去,確實挺不錯,實際中呢?在實際的系統(tǒng)中,一般是不允許這么樣做的。為什么呢?

首先,這里的Delayms(20) , 讓微控制器在這里白白等待了20 ms 的時間,啥也沒干,考

慮我在《學(xué)會釋放CPU》一章中所提及的幾點,這是不可取的。其次while(0 == io_KeyEnter)

所以合理的分配好微控制的處理時間,是編寫按鍵程序的基礎(chǔ)。ϑ;更是程序設(shè)計中的大忌(極

少的特殊情況例外)。任何非極端情況下,都不要使用這樣語句來堵塞微控制器的執(zhí)行進程。

原本是等待按鍵釋放,結(jié)果CPU 就一直死死的盯住該按鍵,其它事情都不管了,那其它事

情不干了嗎?你同意別人可不會同意

◎消除抖動有必要嗎?

的確,軟件上的消抖確實可以保證按鍵的有效檢測。但是,這種消抖確實有必要嗎?有人提

出了這樣的疑問。抖動是按鍵按下的過程中產(chǎn)生的,如果按鍵沒有按下,抖動會產(chǎn)生嗎?如

果沒有按鍵按下,抖動也會在I/O 上出現(xiàn),我會立刻把這個微控制器錘了,永遠(yuǎn)不用這樣一

款微控制器。所以抖動的出現(xiàn)即意味著按鍵已經(jīng)按下,盡管這個電平還沒有穩(wěn)定。所以只要

我們檢測到按鍵按下,即可以返回鍵值,問題的關(guān)鍵是,在你執(zhí)行完其它任務(wù)的時候,再次

執(zhí)行我們的按鍵任務(wù)的時候,抖動過程還沒有結(jié)束,這樣便有可能造成重復(fù)檢測。所以,如

何在返回鍵值后,避免重復(fù)檢測,或者在按鍵一按下就執(zhí)行功能函數(shù),當(dāng)功能函數(shù)的執(zhí)行時

間小于抖動時間時候,如何避免再次執(zhí)行功能函數(shù),就成為我們要考慮的問題了。這是一個

仁者見仁,智者見智的問題,就留給大家去思考吧。所以消除抖動的目的是:防止按鍵一次

按下,多次響應(yīng)。

七、KEY 主題討論第二章——基于狀態(tài)轉(zhuǎn)移的獨立按鍵程序

設(shè)計

本章所描述的按鍵程序要達到的目的:檢測按鍵按下,短按,長按,釋放。即通過按鍵的返

回值我們可以獲取到如下的信息:按鍵按下(短按),按鍵長按,按鍵連發(fā),按鍵釋放。不知

道大家還記得小時候玩過的電子鐘沒有,就是外形類似于CALL 機(CALL )的那種,有一個

小液晶屏,還有四個按鍵,功能是時鐘,鬧鐘以及秒表。在調(diào)整時間的時候,短按+鍵每次

調(diào)整值加一,長按的時候調(diào)整值連續(xù)增加。小的時候很好奇,這樣的功能到底是如何實現(xiàn)的

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 42

呢,今天就讓我們來剖析它的原理吧。ϑ機,好像是很古老的東西了

狀態(tài)在生活中隨處可見。譬如早上的時候,鬧鐘把你叫醒了,這個時候,你便處于清醒的狀

態(tài),馬上你就穿衣起床洗漱吃早餐,這一系列事情就是你在這個狀態(tài)做的事情。做完這些后

你會去等車或者開車去上班,這個時候你就處在上班途中的狀態(tài)…..中午下班時間到了,你

就處于中午下班的狀態(tài),諸如此類等等,在每一個狀態(tài)我們都會做一些不同的事情,而總會

有外界條件促使我們轉(zhuǎn)換到另外一種狀態(tài),譬如鬧鐘叫醒我們了,下班時間到了等等。對于

狀態(tài)的定義出發(fā)點不同,考慮的方向不同,或者會有些許細(xì)節(jié)上面的差異,但是大的狀態(tài)總

是相同的。生活中的事物同樣遵循同樣的規(guī)律,譬如,用一個智能充電器給你的手機電池充

電,剛開始,它是處于快速充電狀態(tài),隨著電量的增加,電壓的升高,當(dāng)達到規(guī)定的電壓時

候,它會轉(zhuǎn)換到恒壓充電??偠灾?xì)心觀察,你會發(fā)現(xiàn)生活中的總總都可以歸結(jié)為一個

個的狀態(tài),而狀態(tài)的變換或者轉(zhuǎn)移總是由某些條件引起同時伴隨著一些動作的發(fā)生。我們的

按鍵亦遵循同樣的規(guī)律,下面讓我們來簡單的描繪一下它的狀態(tài)流程轉(zhuǎn)移圖。

下面對上面的流程圖進行簡要的分析。

首先按鍵程序進入初始狀態(tài)S1,在這個狀態(tài)下,檢測按鍵是否按下,如果有按下,則進入

按鍵消抖狀態(tài)2,在下一次執(zhí)行按鍵程序時候,直接由按鍵消抖狀態(tài)進入按鍵按下狀態(tài)3,

在此狀態(tài)下檢測按鍵是否按下,如果沒有按鍵按下,則返回初始狀態(tài)S1,如果有則可以返

回鍵值,同時進入長按狀態(tài)S4,在長按狀態(tài)下每次進入按鍵程序時候?qū)Π存I時間計數(shù),當(dāng)

計數(shù)值超過設(shè)定閾值時候,則表明長按事件發(fā)生,同時進入按鍵連發(fā)狀態(tài)S5。如果按鍵鍵

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 43

值為空鍵,則返回按鍵釋放狀態(tài)S6,否則繼續(xù)停留在本狀態(tài)。在按鍵連發(fā)狀態(tài)下,如果按

鍵鍵值為空鍵則返回按鍵釋放狀態(tài)S6,如果按鍵時間計數(shù)超過連發(fā)閾值,則返回連發(fā)按鍵

值,清零時間計數(shù)后繼續(xù)停留在本狀態(tài)。

看了這么多,也許你已經(jīng)有一個模糊的概念了,下面讓我們趁熱打鐵,一起來動手編寫按鍵

驅(qū)動程序吧。

下面是我使用的硬件的連接圖。

硬件連接很簡單,四個獨立按鍵分別接在P3^0------P3^3 四個I/O 上面。

因為51 單片機I/O 口內(nèi)部結(jié)構(gòu)的限制,在讀取外部引腳狀態(tài)的時候,需要向端口寫1.在51

單片機復(fù)位后,不需要進行此操作也可以進行讀取外部引腳的操作。因此,在按鍵的端口沒

有復(fù)用的情況下,可以省略此步驟。而對于其它一些真正雙向I/O 口的單片機來說,將引腳

設(shè)置成輸入狀態(tài),是必不可少的一個步驟。

下面的程序代碼初始化引腳為輸入。

void KeyInit(void)

{

io_key_1 = 1 ;

io_key_2 = 1 ;

io_key_3 = 1 ;

io_key_4 = 1 ;

}

根據(jù)按鍵硬件連接定義按鍵鍵值

#define KEY_VALUE_1 0x0e

#define KEY_VALUE_2 0x0d

#define KEY_VALUE_3 0x0b

#define KEY_VALUE_4 0x07

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 44

#define KEY_NULL 0x0f

下面我們來編寫按鍵的硬件驅(qū)動程序。

根據(jù)第一章所描述的按鍵檢測原理,我們可以很容易的得出如下的代碼:

static uint8 KeyScan(void)

{

if(io_key_1 == 0)return KEY_VALUE_1 ;

if(io_key_2 == 0)return KEY_VALUE_2 ;

if(io_key_3 == 0)return KEY_VALUE_3 ;

if(io_key_4 == 0)return KEY_VALUE_4 ;

return KEY_NULL ;

}

其中io_key_1 等是我們按鍵端口的定義,如下所示:

sbit io_key_1 = P3^0 ;

sbit io_key_2 = P3^1 ;

sbit io_key_3 = P3^2 ;

sbit io_key_4 = P3^3 ;

KeyScan()作為底層按鍵的驅(qū)動程序,為上層按鍵掃描提供一個接口,這樣我們編寫的上層

按鍵掃描函數(shù)可以幾乎不用修改就可以拿到我們的其它程序中去使用,使得程序復(fù)用性大大

提高。同時,通過有意識的將與底層硬件連接緊密的程序和與硬件無關(guān)的代碼分開寫,使得

程序結(jié)構(gòu)層次清晰,可移植性也更好。對于單片機類的程序而言,能夠做到函數(shù)級別的代碼

重用已經(jīng)足夠了。

在編寫我們的上層按鍵掃描函數(shù)之前,需要先完成一些宏定義。

//定義長按鍵的TICK 數(shù),以及連發(fā)間隔的TICK 數(shù)

#define KEY_LONG_PERIOD 100

#define KEY_CONTINUE_PERIOD 25

//定義按鍵返回值狀態(tài)(按下,長按,連發(fā),釋放)

#define KEY_DOWN 0x80

#define KEY_LONG 0x40

#define KEY_CONTINUE 0x20

#define KEY_UP 0x10

//定義按鍵狀態(tài)

#define KEY_STATE_INIT 0

#define KEY_STATE_WOBBLE 1

#define KEY_STATE_PRESS 2

#define KEY_STATE_LONG 3

#define KEY_STATE_CONTINUE 4

#define KEY_STATE_RELEASE 5

接著我們開始編寫完整的上層按鍵掃描函數(shù),按鍵的短按,長按,連按,釋放等等狀態(tài)的判

斷均是在此函數(shù)中完成。對照狀態(tài)流程轉(zhuǎn)移圖,然后再看下面的函數(shù)代碼,可以更容易的去

理解函數(shù)的執(zhí)行流程。完整的函數(shù)代碼如下:

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 45

void GetKey(uint8 *pKeyValue)

{

static uint8 s_u8KeyState = KEY_STATE_INIT ;

static uint8 s_u8KeyTimeCount = 0 ;

static uint8 s_u8LastKey = KEY_NULL ; //保存按鍵釋放時候的鍵值

uint8 KeyTemp = KEY_NULL ;

KeyTemp = KeyScan() ; //獲取鍵值

switch(s_u8KeyState)

{

case KEY_STATE_INIT :

{

if(KEY_NULL != (KeyTemp))

{

s_u8KeyState = KEY_STATE_WOBBLE ;

}

}

break ;

case KEY_STATE_WOBBLE : //消抖

{

s_u8KeyState = KEY_STATE_PRESS ;

}

break ;

case KEY_STATE_PRESS :

{

if(KEY_NULL != (KeyTemp))

{

s_u8LastKey = KeyTemp ; //保存鍵值,以便在釋放按鍵狀態(tài)返回鍵值

KeyTemp |= KEY_DOWN ; //按鍵按下

s_u8KeyState = KEY_STATE_LONG ;

}

else

{

s_u8KeyState = KEY_STATE_INIT ;

}

}

break ;

case KEY_STATE_LONG :

{

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 46

if(KEY_NULL != (KeyTemp))

{

if(++s_u8KeyTimeCount > KEY_LONG_PERIOD)

{

s_u8KeyTimeCount = 0 ;

KeyTemp |= KEY_LONG ; //長按鍵事件發(fā)生

s_u8KeyState = KEY_STATE_CONTINUE ;

}

}

else

{

s_u8KeyState = KEY_STATE_RELEASE ;

}

}

break ;

case KEY_STATE_CONTINUE :

{

if(KEY_NULL != (KeyTemp))

{

if(++s_u8KeyTimeCount > KEY_CONTINUE_PERIOD)

{

s_u8KeyTimeCount = 0 ;

KeyTemp |= KEY_CONTINUE ;

}

}

else

{

s_u8KeyState = KEY_STATE_RELEASE ;

}

}

break ;

case KEY_STATE_RELEASE :

{

s_u8LastKey |= KEY_UP ;

KeyTemp = s_u8LastKey ;

s_u8KeyState = KEY_STATE_INIT ;

}

break ;

default : break ;

}

*pKeyValue = KeyTemp ; //返回鍵值

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 47

}

關(guān)于這個函數(shù)內(nèi)部的細(xì)節(jié)我并不打算花過多筆墨去講解。對照著按鍵狀態(tài)流程轉(zhuǎn)移圖,然后

去看程序代碼,你會發(fā)現(xiàn)其實思路非常清晰。最能讓人理解透徹的,莫非就是將整個程序自

己看懂,然后想象為什么這個地方要這樣寫,抱著思考的態(tài)度去閱讀程序,你會發(fā)現(xiàn)自己的

程序水平會慢慢的提高。所以我更希望的是你能夠認(rèn)認(rèn)真真的看完,然后思考。也許你會收

獲更多。

不管怎么樣,這樣的一個程序已經(jīng)完成了本章開始時候要求的功能:按下,長按,連按,釋

放。事實上,如果掌握了這種基于狀態(tài)轉(zhuǎn)移的思想,你會發(fā)現(xiàn)要求實現(xiàn)其它按鍵功能,譬如,

多鍵按下,功能鍵等等,亦相當(dāng)簡單,在下一章,我們就去實現(xiàn)它。

在主程序中我編寫了這樣的一段代碼,來演示我實現(xiàn)的按鍵功能。

void main(void)

{

uint8 KeyValue = KEY_NULL;

uint8 temp = 0 ;

LED_CS11 = 1 ; //流水燈輸出允許

LED_SEG = 0 ;

LED_DIG = 0 ;

Timer0Init() ;

KeyInit() ;

EA = 1 ;

while(1)

{

Timer0MainLoop() ;

KeyMainLoop(&KeyValue) ;

if(KeyValue == (KEY_VALUE_1 | KEY_DOWN)) P0 = ~1 ;

if(KeyValue == (KEY_VALUE_1 | KEY_LONG)) P0 = ~2 ;

if(KeyValue == (KEY_VALUE_1 | KEY_CONTINUE)) { P0 ^= 0xf0;}

if(KeyValue == (KEY_VALUE_1 | KEY_UP)) P0 = 0xa5 ;

}

}

按住第一個鍵,可以清晰的看到P0 口所接的LED 的狀態(tài)的變化。當(dāng)按鍵按下時候,第

一個LED 燈亮,等待2 S 后第二個LED 亮,第一個熄滅,表示長按事件發(fā)生。再過500 ms

第5~8 個LED 閃爍,表示連按事件發(fā)生。當(dāng)釋放按鍵時候,P0 口所接的LED 的狀態(tài)為:

滅亮滅亮亮滅亮滅,這也正是P0 = 0xa5 這條語句的功能

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 48

八、綜合應(yīng)用之一——如何設(shè)計復(fù)雜的多任務(wù)程序

我們在入門階段,一般面對的設(shè)計都是單一的簡單的任務(wù),流程圖可以如圖1 所示,通

常會用踏步循環(huán)延時來滿足任務(wù)需要。

面對多任務(wù),稍微復(fù)雜的程序設(shè)計,沿用圖1 的思想,我們會做出如圖2 所示的程序,

在大循環(huán)體中不斷增加任務(wù),通常還要用延時來滿足特定任務(wù)節(jié)拍,這種程序設(shè)計思想它有

明顯的不足,主要是各個任務(wù)之間相互影響,增加新的任何之后,以前很好的運行的任務(wù)有

可能不正常,例如數(shù)碼管動態(tài)掃描,本來顯示效果很好的驅(qū)動函數(shù),在增加新的任務(wù)后出現(xiàn)

閃爍,顯示效果變差了。

(原文件名:1.JPG)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 49

引用圖片

圖1 單一任務(wù)簡單流程圖圖2 多任務(wù)簡單流程圖

很明顯,初學(xué)者在設(shè)計程序時,需要從程序構(gòu)架思想上下功夫,在做了大量基本模塊練

習(xí)之后,需要總結(jié)提煉自己的程序設(shè)計思路(程序架構(gòu)思想)。

首先我們來理解“任務(wù)”,所謂任務(wù),就是需要CPU 周期“關(guān)照”的事件,絕大多數(shù)任

務(wù)不需要CPU 一直“關(guān)照” ,例如啟動ADC 的啟動讀取。甚至有些任務(wù)“害怕”CPU 一直

“關(guān)照”例如LCD 的刷新,因為LCD 是顯示給人看的,并不需要高速刷新,即便是顯示的

內(nèi)容在高速變化,也不需要高速刷新,道理是一樣的。這樣看來,讓CPU 做簡單任務(wù)一定

浪費,事實也是如此,絕大多數(shù)簡單任務(wù),CPU 都是在“空轉(zhuǎn)” (循環(huán)踏步延時) 。對任務(wù)

結(jié)還可以知道,很多任務(wù)需要CPU 不斷“關(guān)照” ,其實這種“不斷”也是有極限的,比如數(shù)

碼管動態(tài)掃描,能夠做到40Hz 就可以了,又如鍵盤掃描,能夠做到20Hz(經(jīng)驗值),基本

也就不會丟有效按鍵鍵值了,再如LCD 刷新,我覺得做到10Hz 就可以了,等等。看來,

大多數(shù)任務(wù)都是工作在低速頻度。而我們的CPU 一旦運行起來,速度又很快,CPU 本身就

靠很快的速度執(zhí)行很簡單的指令來勝任復(fù)雜的任務(wù)(邏輯)的。如果有辦法把“快”的CPU

分成多個慢的CPU,然后給不同的任務(wù)分配不同速度的CPU,這種設(shè)想是不是很好呢!確

很好,下面就看如何將“快”的CPU 劃分成多個“慢”的CPU。

根據(jù)這種想法,我們需要合理分配CPU 資源來“關(guān)照”不同的任務(wù),最好能夠根據(jù)任務(wù)

本身合理占用CPU 資源,首先看如圖3 所示的流程圖,各個任務(wù)流程獨立,各任務(wù)通過全

變量來交互信息,在流程中有一個重要的模塊“任務(wù)切換”,就是任務(wù)切換模塊實現(xiàn)CPU 合

理分配,這個任務(wù)切換模塊是怎么實現(xiàn)的呢?

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 50

(原文件名:2.JPG)

引用圖片

圖3 多任務(wù)復(fù)雜流程圖

首先需要理解,CPU 一旦運行起來,就無法停止(硬件支持時鐘停止的不在這里討論),

誰能夠控制一批脫韁的馬呢?對了,有中斷,中斷能夠讓CPU 回到特定的位置,設(shè)想,能

能用一個定時中斷,周期性的將CPU 這匹運行著的脫韁的馬召喚回來,重新給它安排特定

任務(wù),事實上,任務(wù)切換就是這樣實現(xiàn)的。

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 51

(原文件名:3.JPG)

引用圖片

圖4 定時中斷實現(xiàn)任務(wù)切換

如圖4A 所示,CPU 在空閑任務(wù)循環(huán)等待,定時中斷將CPU 周期性喚回,根據(jù)任務(wù)設(shè)計

了不同的響應(yīng)頻度,滿足條件的任務(wù)將獲得CPU 資源,CPU 為不同任務(wù)“關(guān)照”完成后,再

次返回空閑任務(wù),如此周而復(fù)始,對于各個任務(wù)而言,好像各自擁有一個獨立的CPU,各

獨立運行。用這種思想構(gòu)建的程序框架,最大的好處是任務(wù)很容易裁剪,系統(tǒng)能夠做得很復(fù)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 52

雜。

在充分考慮單片機中斷特性(在哪里中斷就返回到哪里)后,實際可行的任務(wù)切換如圖

4B 所示,定時中斷可能發(fā)生在任務(wù)調(diào)度,隨機任務(wù)執(zhí)行的任何時候,圖中最大的框框所示,

不管中斷在何時發(fā)生,它都會正常返回,定時中斷所產(chǎn)生的影響只在任務(wù)調(diào)度模塊起作用,

即依次讓不同的任務(wù)按不同的節(jié)拍就緒。任務(wù)調(diào)度會按一定的優(yōu)先級執(zhí)行就緒任務(wù)。

總結(jié)不同的任務(wù)需要CPU 關(guān)照的頻度,選擇最快的那個頻度來設(shè)定定時器中斷的節(jié)拍,

一般選擇200Hz,或者100Hz 都可以。另外再給每個任務(wù)設(shè)定一個節(jié)拍控制計數(shù)器C,也就

是定時器每中斷多少次后執(zhí)行任務(wù)一次。例如取定時中斷節(jié)拍為200Hz,給任務(wù)設(shè)定的C=

10,

則任務(wù)執(zhí)行頻度為200/10=20Hz,如果是數(shù)碼管掃描,按40Hz 不閃爍規(guī)律,則任務(wù)節(jié)拍控

計數(shù)器C=5 即可。在程序設(shè)計中,C 代表著任務(wù)運行的節(jié)拍控制參數(shù),我們習(xí)慣用delay 來

描述,不同的任務(wù)用task0,task1……來描述。

明天繼續(xù)寫如何用代碼實現(xiàn)!2009-6-29

下面我們來用代碼實現(xiàn)以上多任務(wù)程序設(shè)計思想。

首先是任務(wù)切換

while(1)

{

if(task_delay[0]==0) task0(); //task0 就緒,

if(task_delay[1]==0) task1(); //task1 就緒,

……

}

很顯然,執(zhí)行任務(wù)的條件是任務(wù)延時量task_delay=0,那么任務(wù)延時量誰來控制呢?定時

器啊!定時器中斷對任務(wù)延時量減一直到歸零,標(biāo)志任務(wù)就緒。當(dāng)沒有任務(wù)就緒時,任務(wù)切

換本身就是一個Idle 任務(wù)。

void timer0(void) interrupt 1

{

if(task_delay[0]) task_delay[0]--;

if(task_delay[1]) task_delay[1]--;

……

}

例如timer0 的中斷節(jié)拍為200Hz,task0_delay 初值為10,則task0()執(zhí)行頻度為

200/10=20Hz。

有了以上基礎(chǔ),我們來設(shè)計一個簡單多任務(wù)程序,進一步深入理解這種程序設(shè)計思想。

任務(wù)要求:用單片機不同IO 腳輸出1Hz,5Hz,10Hz,20Hz 方波信號,這個程序很短,將

直接給出。

#include "reg51.h"

#define TIME_PER_SEC 200 //定義任務(wù)時鐘頻率,200Hz

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 53

#define CLOCK 22118400 //定義時鐘晶振,單位Hz

#define MAX_TASK 4 //定義任務(wù)數(shù)量

extern void task0(void); //任務(wù)聲明

extern void task1(void);

extern void task2(void);

extern void task3(void);

sbit f1Hz = P1^0; //端口定義

sbit f5Hz = P1^1;

sbit f10Hz = P1^2;

sbit f20Hz = P1^3;

unsigned char task_delay[4]; //任務(wù)延時變量定義

//定時器0 初始化

void timer0_init(void)

{

unsigned char i;

for(i=0;i

TMOD = (TMOD & 0XF0) | 0X01; //定時器0 工作在模式1, 16Bit 定時器模

TH0 = 255-CLOCK/TIME_PER_SEC/12/256;

TL0 = 255-CLOCK/TIME_PER_SEC/12%256;

TR0 =1;

ET0 =1; //開啟定時器和中斷

}

// 系統(tǒng)OS 定時中斷服務(wù)

void timer0(void) interrupt 1

{

unsigned char i;

TH0 = 255-CLOCK/TIME_PER_SEC/12/256;

TL0 = 255-CLOCK/TIME_PER_SEC/12%256;

for(i=0;i

//每節(jié)拍對任務(wù)延時變量減1 ,減至0 后,任務(wù)就緒。

}

/*main 主函數(shù)*/

void main(void)

{

timer0_init();

EA=1;//開總中斷

while(1)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 54

{

if(task_delay[0]==0) {task0(); task_delay[0] = TIME_PER_SEC/ 2;}

//要產(chǎn)生1hz 信號,翻轉(zhuǎn)周期就是2Hz,以下同

if(task_delay[1]==0) {task1(); task_delay[1] = TIME_PER_SEC/10;}

//要產(chǎn)生5hz 信號,翻轉(zhuǎn)周期就是10Hz,以下同

if(task_delay[2]==0) {task2(); task_delay[2] = TIME_PER_SEC/20;}

if(task_delay[3]==0) {task3(); task_delay[3] = TIME_PER_SEC/40;}

}

}

void task0(void)

{

f1Hz = !f1Hz;

}

void task1(void)

{

f5Hz = !f5Hz;

}

void task2(void)

{

f10Hz = !f10Hz;

}

void task3(void)

{

f20Hz = !f20Hz;

}

仿真效果如圖5 所示。

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 55

(原文件名:4.JPG)

引用圖片

圖5 仿真波形圖

同樣的程序,同學(xué)們可以考慮用圖2 所示的思想設(shè)計,看看容易不容易,如果你的程序

實現(xiàn)了相同的功能,如果我改變要求,改變信號的頻率,你的程序容易修改嗎?

要進一步完善這種程序設(shè)計思想,有幾個問題還需要考慮:

對任務(wù)本身有什么要求?

不同任務(wù)之間有沒有優(yōu)先級?(不同的事情總有個輕重緩急吧!)

任務(wù)間如何延時?

……

為了回答這些問題,下面我們來分析CPU 的運行情況。

(原文件名:5.JPG)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 56

引用圖片

圖6 CPU 運行情況示意圖

CPU 運行情況如圖6 所示,黑色區(qū)域表示CPU 進程,系統(tǒng)啟動后, CPU 將無休止的運行,

CPU 資源將如何分配呢?程序首先進入“任務(wù)切換”進程,如果當(dāng)前沒有任務(wù)就緒,就在任

務(wù)切換進程循環(huán)(也可以理解為空閑進程),定時中斷將CPU 當(dāng)前進程打斷,在定時中斷進

程可能讓某些任務(wù)就緒,中斷返回任務(wù)切換進程,很快會進入就緒任務(wù)0,CPU“關(guān)照”完

任務(wù)0,再次回到任務(wù)切換進程,如果還有其它任務(wù)就緒,還會再次進入其它任務(wù),沒有任

務(wù)就循環(huán)等待,定時中斷會不斷讓新的任務(wù)就緒,CPU 也會不斷進入任務(wù)“關(guān)照” 。這樣不

同的任務(wù)就會獲得不同的CPU 資源,每一個任務(wù)都像是擁有一個獨立的CPU 為之服務(wù)。

從這種進程切換我們可以看出,在定時中斷和任務(wù)切換過程中,額外的占用了一些CPU

資源, 這就是定時中斷頻度不宜太快, 否則將大大降低CPU 的有效資源率, 當(dāng)然太慢也

不行。

另外就是CPU 每次關(guān)照任務(wù)的時間不能太長,如果超過一個中斷周期,就會影響到其它任

務(wù)

的實時性。所謂的實時性就是按定時中斷設(shè)定的節(jié)拍,準(zhǔn)時得到CPU 關(guān)照。這樣,每一個

任務(wù)就必須簡單,每次“關(guān)照”時間最好不要超過定時中斷節(jié)拍周期(5ms 或10ms,初學(xué)者

要對ms 有一個概念,機器周期為us 級的單片機,1ms 可以執(zhí)行上千條指令,對于像數(shù)碼管

掃描,鍵盤掃描,LCD 顯示等常規(guī)任務(wù)都是綽綽有余的,只是遇到大型計算,數(shù)據(jù)排序就

得短了)

關(guān)于任務(wù)優(yōu)先級的問題:一個復(fù)雜系統(tǒng),多個任務(wù)之間總有“輕重緩急”之區(qū)別,那些

需要嚴(yán)格實時的任務(wù)通常用中斷實現(xiàn),中斷能夠保證第一時間相應(yīng),我們這里討論的不是那

種實時概念,是指在最大允許時差內(nèi)能夠得到CPU“關(guān)照” ,例如鍵盤掃描,為了保證較好

的操作效果,快的/慢的/長的/短的(不同人按鍵不一樣)都能夠正確識別,這就要保證足夠

的掃描速度,這種掃描速度對不同的按鍵最好均等,如果我們按50Hz 來設(shè)計,那么就要保

證鍵盤掃描速度在任何情況下都能夠做到50Hz 掃描頻度,不會因為某個新任務(wù)的開啟而被

破壞,如果確實有新的任務(wù)有可能破壞這個50Hz 掃描頻度,我們就應(yīng)該在優(yōu)先級安排上讓

鍵盤掃描優(yōu)先級高于那個可能影響鍵盤掃描的任務(wù)。這里體現(xiàn)的就是當(dāng)同時多個任務(wù)就緒

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 57

時,

最先執(zhí)行哪個的問題,任務(wù)調(diào)度時要優(yōu)先執(zhí)行級別高的任務(wù)。

關(guān)于“長”任務(wù)的問題:有些任務(wù)雖然很獨立,但完成一次任務(wù)執(zhí)行需要很長時間,例

如DS18B20,從復(fù)位初始化到讀回溫度值,最長接近1s,這主要是DS18B20 溫度傳感器完

成一次溫度轉(zhuǎn)換需要500 到750ms,這個時間對CPU 而言,簡直是太長了,就像一件事情

要我們?nèi)说却?0 年一樣,顯然這樣的任務(wù)是其它任務(wù)所耽擱不起的。像類似DS18B20 這樣

的器件(不少ADC 也是這樣) ,怎么設(shè)計任務(wù)體解決“長”的問題。進一步研究這些器件發(fā)

現(xiàn),真正需要CPU“關(guān)照”它們的時間并不長,關(guān)鍵是等待結(jié)果要很長時間。解決的辦法就

是把類似的器件驅(qū)動分成多個段:初始化段、啟動段、讀結(jié)果段,而在需要花長時間等待時

間段,不要CPU 關(guān)照,允許CPU 去關(guān)照其它任務(wù)。

將一個任務(wù)分成若干段,確保每段需要CPU 關(guān)照時長小于定時器

中斷節(jié)拍長,這樣CPU 在處理這些長任務(wù)時,就不會影響到其它任務(wù)的執(zhí)行。

Easy51RTOS

正是基于以上程序設(shè)計思想,總結(jié)完善后提出一種耗費資源特別少并且不使用堆棧的多

線程操作系統(tǒng),這個操作系統(tǒng)以純C 語言實現(xiàn),無硬件依賴性,需要單片機的資源極少。

名為Easy51RTOS,特別適合初學(xué)者學(xué)習(xí)使用。有任務(wù)優(yōu)先級,通過技巧可以任務(wù)間延時,

缺點是高優(yōu)先級任務(wù)不具有搶占功能,一個具有搶占功能的操作系統(tǒng),一定要涉及到現(xiàn)場保

護與恢復(fù),需要更多的RAM 資源,涉及到堆棧知識,文件系統(tǒng)將很復(fù)雜,初學(xué)者學(xué)習(xí)難度

大。

為了便于初學(xué)者學(xué)習(xí),將代碼文件壓縮至4 個文件。

Easy51RTOS.Uv2 Keil 工程文件,KEIL 用戶很熟悉的

main.c main 函數(shù)和用戶任務(wù)task 函數(shù)文件

os_c.c Easy51RTOS 相關(guān)函數(shù)文件

os_cfg.h Easy51RTOS 相關(guān)配置參數(shù)頭文件

文件解讀如下:

os_cfg.h

#include "reg51.h"

#define TIME_PER_SEC 200 //定義任務(wù)時鐘頻率,200Hz

#define CLOCK 22118400 //定義時鐘晶振,單位Hz

#define MAX_TASK 4 //定義任務(wù)數(shù)量

//函數(shù)變量聲明,在需要用以下函數(shù)或變量的文件中包含此頭文件即可

extern void task0(void);

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 58

extern void task1(void);

extern void task2(void);

extern void task3(void);

extern unsigned char task_delay[MAX_TASK];

extern void run(void (*ptask)());

extern void os_timer0_init(void);

os_c.c

#include "os_cfg.h"

unsigned char task_delay[MAX_TASK]; //定義任務(wù)延時量變量

//定時器0 初始化

void os_timer0_init(void)

{

unsigned char i;

for(i=0;i

TMOD = (TMOD & 0XF0) | 0X01; //定時器0 工作在模式1,16Bit 定時器模式

TH0 = 255-CLOCK/TIME_PER_SEC/12/256;

//CRY_OSC,TIME_PER_SEC 在os_cfg.h 中定義

TL0 = 255-CLOCK/TIME_PER_SEC/12%256;

TR0 =1;

ET0 =1; //開啟定時器和中斷

}

// 系統(tǒng)OS 定時中斷服務(wù)

void os_timer0(void) interrupt 1

{

unsigned char i;

TH0 = 255-CLOCK/TIME_PER_SEC/12/256;

TL0 = 255-CLOCK/TIME_PER_SEC/12%256;

for(i=0;i

//每節(jié)拍對任務(wù)延時變量減1 ,減至0 后,任務(wù)就緒。

}

//指向函數(shù)的指針函數(shù)

void run(void (*ptask)())

{

(*ptask)();

}

main.c

#include "os_cfg.h"

#define TASK_DELAY0 TIME_PER_SEC/1 //任務(wù)執(zhí)行頻度為1Hz

#define TASK_DELAY1 TIME_PER_SEC/2 //任務(wù)執(zhí)行頻度為2Hz

#define TASK_DELAY2 TIME_PER_SEC/10 //任務(wù)執(zhí)行頻度為10Hz

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 59

#define TASK_DELAY3 TIME_PER_SEC/20 //任務(wù)執(zhí)行頻度為20Hz

void (* code task[])() = {task0,task1,task2,task3}; //獲得任務(wù)PC 指針

sbit LED0 = P1^0; //演示用LED 接口定義

sbit LED1 = P1^1;

sbit LED2 = P1^2;

sbit LED3 = P1^3;

/*main 主函數(shù)*/

void main(void)

{

unsigned char i;

os_timer0_init(); //節(jié)拍發(fā)生器定時器初始化

EA = 1; //開總中斷

while(1)

{

for(i=0;i

if (task_delay[i]==0) {run(task[i]); break;} //就緒任務(wù)調(diào)度

} //上一行break 有特殊作用,詳細(xì)解釋見后文

}

void task0(void) //任務(wù)0

{

LED0 = !LED0;

task_delay[0] = TASK_DELAY0;

}

void task1(void) //任務(wù)1

{

LED1 = !LED1;

task_delay[1] = TASK_DELAY1;

}

void task2(void) //任務(wù)2

{

LED2 = !LED2;

task_delay[2] = TASK_DELAY2;

}

void task3(void) //任務(wù)內(nèi)分段設(shè)計

{

static unsigned char state=0; //定義靜態(tài)局部變量

switch (state)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 60

{

case 0:

LED3 = !LED3;

state = 1;

task_delay[3] = TASK_DELAY3;

break;

case 1:

LED3 = !LED3;

state = 2;

task_delay[3] = TASK_DELAY3*2;

break;

case 2:

LED3 = !LED3;

state = 0;

task_delay[3] = TASK_DELAY3*4;

break;

default:

state = 0;

task_delay[3] = TASK_DELAY3;

break;

}

}

仿真圖如圖8 所示

(原文件名:6.JPG)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 61

引用圖片

圖8 仿真波形圖

主程序巧妙實現(xiàn)優(yōu)先級設(shè)定:

for(i=0;i

if (task_delay[i]==0) {run(task[i]); break;} //就緒任務(wù)調(diào)度

這里的break 將跳出for 循環(huán),使得每次重新任務(wù)調(diào)度總是從task0 開始,就意味著優(yōu)先

級高的任務(wù)就緒會先執(zhí)行。這樣task0 具有最高優(yōu)先級,task1、task2、task3 優(yōu)先級依次降

低。

特別是void task3(void)用switch(state)狀態(tài)機實現(xiàn)了任務(wù)分段,這也是任務(wù)內(nèi)系統(tǒng)延時的

一種方法。

我會繼續(xù)更新的。。。。。。。。。。。

九、綜合應(yīng)用之二——DS1320/DS18B20 應(yīng)用

好幾天沒有更新了,呵呵~~今天我把咱們常用的傳感器DS1320 DS18B20 給大家介紹下。

對于市面上的大多數(shù)51 單片機開發(fā)板來說。ds1302 和ds18b20 應(yīng)該是比較常見的兩種外圍

芯片。ds1302 是具有SPI 總線接口的時鐘芯片。ds18b20 則是具有單總線接口的數(shù)字溫度傳

感器。下面讓我們分別來認(rèn)識并學(xué)會應(yīng)用這兩種芯片。

首先依舊是看DS1302 的datasheet 中的相關(guān)介紹。

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 62

(原文件名:1.jpg)

引用圖片

上面是它的一些基本的應(yīng)用介紹。

下面是它的引腳的描述。

(原文件名:2.jpg)

引用圖片

下面是DS1302 的時鐘寄存器。我們要讀取的時間數(shù)據(jù)就是從下面這些數(shù)據(jù)寄存器中讀取出

來的。當(dāng)我們要想調(diào)整時間時,可以把時間數(shù)據(jù)寫入到相應(yīng)的寄存器中就可以了。

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 63

(原文件名:3.jpg)

引用圖片

這是DS1302 內(nèi)部的31 個RAM 寄存器。在某些應(yīng)用場合我們可以應(yīng)用到。如我們想要做

一個帶定時功能的鬧鐘。則可以把鬧鐘的時間寫入到31 個RAM 寄存器中的任意幾個。當(dāng)

單片機掉電時,只要我們的DS1302 的備用電池還能工作,那么保存在其中的鬧鐘數(shù)據(jù)就不

會丟失~~

(原文件名:4.jpg)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 64

引用圖片

由于對于這些器件的操作基本上按照數(shù)據(jù)手冊上面提供的時序圖和相關(guān)命令字來進行操作

就可以了。因此在我們應(yīng)用這些器件的時候一定要對照著手冊上面的要求來進行操作。如果

覺得還不夠放心的話??梢缘骄W(wǎng)上下載一些參考程序。對著手冊看別人的程序,看別人的思

路是怎么樣的。

DS1302 和單片機的連接很簡單。只需一根復(fù)位線,一根時鐘線,一根數(shù)據(jù)線即可。同時

它本身還需要接一個32.768KHz 的晶振來提供時鐘源。對于晶振的兩端可以分別接一個6PF

左右的電容以提高晶振的精確度。同時可以在第8 腳接上一個3.6V 的可充電的電池。當(dāng)系

統(tǒng)正常工作時可以對電池進行涓流充電。當(dāng)系統(tǒng)掉電時,DS1302 由這個電池提供的能量繼

續(xù)工作。

下面讓我們來驅(qū)動它。

sbit io_DS1302_RST = P2^0 ;

sbit io_DS1302_IO = P2^1 ;

sbit io_DS1302_SCLK = P2^2 ;

//-------------------------------------常數(shù)宏---------------------------------//

#define DS1302_SECOND_WRITE 0x80 //寫時鐘芯片的寄存器位置

#define DS1302_MINUTE_WRITE 0x82

#define DS1302_HOUR_WRITE 0x84

#define DS1302_WEEK_WRITE 0x8A

#define DS1302_DAY_WRITE 0x86

#define DS1302_MONTH_WRITE 0x88

#define DS1302_YEAR_WRITE 0x8C

#define DS1302_SECOND_READ 0x81 //讀時鐘芯片的寄存器位置

#define DS1302_MINUTE_READ 0x83

#define DS1302_HOUR_READ 0x85

#define DS1302_WEEK_READ 0x8B

#define DS1302_DAY_READ 0x87

#define DS1302_MONTH_READ 0x89

#define DS1302_YEAR_READ 0x8D

//-----------------------------------操作宏----------------------------------//

#define DS1302_SCLK_HIGH io_DS1302_SCLK = 1 ;

#define DS1302_SCLK_LOW io_DS1302_SCLK = 0 ;

#define DS1302_IO_HIGH io_DS1302_IO = 1 ;

#define DS1302_IO_LOW io_DS1302_IO = 0 ;

#define DS1302_IO_READ io_DS1302_IO

#define DS1302_RST_HIGH io_DS1302_RST = 1 ;

#define DS1302_RST_LOW io_DS1302_RST = 0 ;

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 65

/******************************************************

* 保存時間數(shù)據(jù)的結(jié)構(gòu)體*

******************************************************/

struct

{

uint8 Second ;

uint8 Minute ;

uint8 Hour ;

uint8 Day ;

uint8 Week ;

uint8 Month ;

uint8 Year ;

}CurrentTime ;

/******************************************************************************

* Function: static void v_DS1302Write_f( uint8 Content ) *

* Description:向DS1302 寫一個字節(jié)的內(nèi)容*

* Parameter:uint8 Content : 要寫的字節(jié)*

* *

******************************************************************************/

static void v_DS1302Write_f( uint8 Content )

{

uint8 i ;

for( i = 8 ; i > 0 ; i-- )

{

if( Content & 0x01 )

{

DS1302_IO_HIGH

}

else

{

DS1302_IO_LOW

}

Content >>= 1 ;

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 66

DS1302_SCLK_HIGH

DS1302_SCLK_LOW

}

}

/******************************************************************************

* Function: static uint8 v_DS1302Read_f( void ) *

* Description: 從DS1302 當(dāng)前設(shè)定的地址讀取一個字節(jié)的內(nèi)容*

* Parameter: *

* Return: 返回讀出來的值(uint8) *

******************************************************************************/

static uint8 v_DS1302Read_f( void )

{

uint8 i, ReadValue ;

DS1302_IO_HIGH

for( i = 8 ; i > 0 ; i-- )

{

ReadValue >>= 1 ;

if( DS1302_IO_READ )

{

ReadValue |= 0x80 ;

}

else

{

ReadValue &= 0x7f ;

}

DS1302_SCLK_HIGH

DS1302_SCLK_LOW

}

return ReadValue ;

}

/******************************************************************************

* Function: void v_DS1302WriteByte_f( uint8 Address, uint8 Content ) *

* Description: 從DS1302 指定的地址寫入一個字節(jié)的內(nèi)容*

* Parameter: Address: 要寫入數(shù)據(jù)的地址*

* Content: 寫入數(shù)據(jù)的具體值*

* Return: *

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 67

******************************************************************************/

void v_DS1302WriteByte_f( uint8 Address, uint8 Content )

{

DS1302_RST_LOW

DS1302_SCLK_LOW

DS1302_RST_HIGH

v_DS1302Write_f( Address ) ;

v_DS1302Write_f( Content ) ;

DS1302_RST_LOW

DS1302_SCLK_HIGH

}

/******************************************************************************

* Function: uint8 v_DS1302ReadByte_f( uint8 Address ) *

* Description:從DS1302 指定的地址讀出一個字節(jié)的內(nèi)容*

* Parameter:Address: 要讀出數(shù)據(jù)的地址*

* *

* Return: 指定地址讀出的值(uint8) *

******************************************************************************/

uint8 v_DS1302ReadByte_f( uint8 Address )

{

uint8 ReadValue ;

DS1302_RST_LOW

DS1302_SCLK_LOW

DS1302_RST_HIGH

v_DS1302Write_f( Address ) ;

ReadValue = v_DS1302Read_f() ;

DS1302_RST_LOW

DS1302_SCLK_HIGH

return ReadValue ;

}

/******************************************************************************

* Function: void v_ClockInit_f( void ) *

* Description:初始化寫入DS1302 時鐘寄存器的值(主程序中只需調(diào)用一次即可) *

* Parameter:

*

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 68

* *

* Return: *

******************************************************************************/

void v_ClockInit_f( void )

{

if( v_DS1302ReadByte_f( 0xc1) != 0xf0 )

{

v_DS1302WriteByte_f( 0x8e, 0x00 ) ; //允許寫操作

v_DS1302WriteByte_f( DS1302_YEAR_WRITE, 0x08 ) ; //年

v_DS1302WriteByte_f( DS1302_WEEK_WRITE, 0x04 ) ; //星期

v_DS1302WriteByte_f( DS1302_MONTH_WRITE, 0x12 ) ; //月

v_DS1302WriteByte_f( DS1302_DAY_WRITE, 0x11 ) ; //日

v_DS1302WriteByte_f( DS1302_HOUR_WRITE, 0x13 ) ; //小時

v_DS1302WriteByte_f( DS1302_MINUTE_WRITE, 0x06 ) ; //分鐘

v_DS1302WriteByte_f( DS1302_SECOND_WRITE, 0x40 ) ; //秒

v_DS1302WriteByte_f( 0x90, 0xa5 ) ; //充電

v_DS1302WriteByte_f( 0xc0, 0xf0 ) ; //判斷是否初始化一次標(biāo)識寫入

v_DS1302WriteByte_f( 0x8e, 0x80 ) ; //禁止寫操作

}

}

/******************************************************************************

* Function: void v_ClockUpdata_f( void ) *

* Description:讀取時間數(shù)據(jù),并保存在結(jié)構(gòu)體CurrentTime 中*

* Parameter:

*

* *

* Return: *

******************************************************************************/

void v_ClockUpdata_f( void )

{

CurrentTime.Second = v_DS1302ReadByte_f( DS1302_SECOND_READ ) ;

CurrentTime.Minute = v_DS1302ReadByte_f( DS1302_MINUTE_READ ) ;

CurrentTime.Hour = v_DS1302ReadByte_f( DS1302_HOUR_READ ) ;

CurrentTime.Day = v_DS1302ReadByte_f( DS1302_DAY_READ ) ;

CurrentTime.Month = v_DS1302ReadByte_f( DS1302_MONTH_READ ) ;

CurrentTime.Week = v_DS1302ReadByte_f( DS1302_WEEK_READ ) ;

CurrentTime.Year = v_DS1302ReadByte_f( DS1302_YEAR_READ ) ;

}

有了上面的這些函數(shù)我們就可以對DS1302 進行操作了。當(dāng)我們想要獲取當(dāng)前時間時,只需

要調(diào)用v_ClockUpdata_f( void )這個函數(shù)即可。讀取到的時間數(shù)據(jù)保存在CurrentTime 這個結(jié)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 69

構(gòu)體中。至于如何把時間數(shù)據(jù)在數(shù)碼管或者是液晶屏上顯示出來我相信大家應(yīng)該都會了吧

^_^.

看看顯示效果如何~~

(原文件名:5.jpg)

引用圖片

下面再讓我們看看DS18B20 吧。

DS18B20 是單總線的數(shù)字溫度傳感器。其與單片機的接口只需要一根數(shù)據(jù)線即可。當(dāng)然連

線簡單意味著軟件處理上可能要麻煩一點。下面來看看它的優(yōu)點:

(原文件名:1.jpg)

引用圖片

看看它的靚照。外形和我們常用的三極管沒有什么兩樣哦。

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 70

(原文件名:2.jpg)

引用圖片

DS18B20 的內(nèi)部存儲器分為以下幾部分

ROM:存放該器件的編碼。前8 位為單線系列的編碼(DS18B20 的編碼是19H)后面48 位為芯

片的唯一序列號。在出場的時候就已經(jīng)設(shè)置好,用戶無法更改。最后8 位是以上56 位的CRC

碼。

(原文件名:3.jpg)

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 71

引用圖片

RAM:DS18B20 的內(nèi)部暫存器共9 個字節(jié)。其中第一個和第二個字節(jié)存放轉(zhuǎn)換后的溫度值。

第二個和第三個字節(jié)分別存放高溫和低溫告警值。(可以用RAM 指令將其拷貝到EEPROM

中)第四個字節(jié)為配置寄存器。第5~7 個字節(jié)保留。第9 個字節(jié)為前8 個字節(jié)的CRC 碼。

DS18B20 的溫度存放如上圖所示。其中S 位符號位。當(dāng)溫度值為負(fù)值時,S = 1 ,反之則S = 0 。

我們把得到的溫度數(shù)據(jù)乘上對應(yīng)的分辨率即可以得到轉(zhuǎn)換后的溫度值。

DS18B20 的通訊協(xié)議:

在對DS18B20 進行讀寫編程時,必須嚴(yán)格保證讀寫的時序。否則將無法讀取測溫結(jié)果。

根據(jù)DS18B20 的通訊協(xié)議,主機控制DS18B20 完成溫度轉(zhuǎn)換必須經(jīng)過3 個步驟:每一次讀

寫之前都要對DS18B20 進行復(fù)位,復(fù)位成功后發(fā)送一條ROM 指令,最后發(fā)送RAM 指令。

這樣才能對DS18B20 進行預(yù)定的操作。

復(fù)位要求主機將數(shù)據(jù)線下拉500us,然后釋放,DS18B20 收到信號后等待16~160us 然后發(fā)

出60~240us 的存在低脈沖,主機收到此信號表示復(fù)位成功。

(原文件名:4.jpg)

引用圖片

上圖即DS18B20 的復(fù)位時序圖。

下面是讀操作的時序圖

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 72

(原文件名:5.jpg)

引用圖片

這是寫操作的時序圖

(原文件名:6.jpg)

引用圖片

下面讓我們來看看它的驅(qū)動程序如何寫吧。

sbit io_DS18B20_DQ = P2^3 ;

#define DS18B20_DQ_HIGH io_DS18B20_DQ = 1 ;

#define DS18B20_DQ_LOW io_DS18B20_DQ = 0 ;

#define DS18B20_DQ_READ io_DS18B20_DQ

/*******************************************************************

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 73

* 保存溫度值的數(shù)組.依次存放正負(fù)標(biāo)志,溫度值十位,個位,和小數(shù)位*

*******************************************************************/

uint8 Temperature[ 4 ] ;

void v_Delay10Us_f( uint16 Count )

{

while( --Count )

{

_nop_();

}

}

/**************************************************************************

* Function: uint8 v_Ds18b20Init_f( void ) *

* Description: 初始化DS18B20 *

* Parameter: *

* *

* Return: 返回初始化的結(jié)果(0:復(fù)位成功1:復(fù)位失敗) *

**************************************************************************/

uint8 v_Ds18b20Init_f( void )

{

uint8 Flag ;

DS18B20_DQ_HIGH //稍作延時

v_Delay10Us_f( 3 ) ;

DS18B20_DQ_LOW //總線拉低

v_Delay10Us_f( 80 ) ; //延時大于480us

DS18B20_DQ_HIGH //總線釋放

v_Delay10Us_f( 15 ) ;

Flag = DS18B20_DQ_READ ; //如果Flag 為0,則復(fù)位成功,否則復(fù)位失敗

return Flag ;

}

/******************************************************************************

* Function: void v_Ds18b20Write_f( uint8 Cmd ) *

* Description: 向DS18B20 寫命令*

* Parameter: Cmd: 所要寫的命令*

* *

* Return: *

******************************************************************************/

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 74

void v_Ds18b20Write_f( uint8 Cmd )

{

uint8 i ;

for( i = 8 ; i > 0 ; i-- )

{

DS18B20_DQ_LOW //拉低總線,開始寫時序

DS18B20_DQ_READ = Cmd & 0x01 ; //控制字的最低位先送到總線

v_Delay10Us_f( 5 ) ; //稍作延時,讓DS18B20 讀取總線上的數(shù)據(jù)

DS18B20_DQ_HIGH //拉高總線,1bit 寫周期結(jié)束

Cmd >>= 1 ;

}

}

/******************************************************************************

* Function: uint8 v_Ds18b20Read_f( void ) *

* Description: 向DS18B20 讀取一個字節(jié)的內(nèi)容*

* Parameter: *

* *

* Return: 讀取到的數(shù)據(jù)*

******************************************************************************/

uint8 v_Ds18b20Read_f( void )

{

uint8 ReadValue, i ;

for( i = 8 ; i > 0 ; i-- )

{

DS18B20_DQ_LOW

ReadValue >>= 1 ;

DS18B20_DQ_HIGH

if( DS18B20_DQ_READ == 1 )

ReadValue |= 0x80 ;

v_Delay10Us_f( 3 ) ;

}

return ReadValue ;

}

/******************************************************************************

* Function: uint16 v_Ds18b20ReadTemp_f( void ) *

* Description: 讀取當(dāng)前的溫度數(shù)據(jù)(只保留了一位小數(shù)) *

* Parameter: *

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 75

* *

* Return: 讀取到的溫度值*

******************************************************************************/

uint16 v_Ds18b20ReadTemp_f( void )

{

uint8 TempH, TempL ;

uint16 ReturnTemp ;

/* if( v_Ds18b20Init_() ) return ; //復(fù)位失敗,在這里添加錯誤處理的代碼*/

v_Ds18b20Init_f() ; /復(fù)位DS18B20

v_Ds18b20Write_f( 0xcc ) ; //跳過ROM

v_Ds18b20Write_f( 0x44 ) ; //啟動溫度轉(zhuǎn)換

v_Ds18b20Init_f() ;

v_Ds18b20Write_f( 0xcc ) ; //跳過ROM

v_Ds18b20Write_f( 0xbe ) ; //讀取DS18B20 內(nèi)部的寄存器內(nèi)容

TempL = v_Ds18b20Read_f() ; //讀溫度值低位(內(nèi)部RAM 的第0 個字節(jié))

TempH = v_Ds18b20Read_f() ; //讀溫度值高位(內(nèi)部RAM 的第1 個字節(jié))

ReturnTemp = TempH ;

ReturnTemp <<= 8 ;

ReturnTemp |= TempL ; //溫度值放在變量ReturnTemp 中

return ReturnTemp ;

}

/******************************************************************************

* Function: void v_TemperatureUpdate_f( void ) *

* Description:讀取當(dāng)前的溫度數(shù)據(jù)并轉(zhuǎn)化存放在數(shù)組Temperature(只保留了一位小數(shù)) *

* Parameter:

*

* *

* Return: *

******************************************************************************/

void v_TemperatureUpdate_f( void )

{

uint8 Tflag = 0 ;

uint16 TempDat ;

float Temp ;

TempDat = v_Ds18b20ReadTemp_f() ;

if( TempDat & 0xf000 )

{

Tflag = 1 ;

TempDat = ~TempDat + 1 ;

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 76

}

Temp = TempDat >> 4; (TempDat * 0.0625 ) 請大家不要用乘以,不知道為什么可以看我

上面的貼子

TempDat = Temp * 10 ; ;小數(shù)部用可以用查表法,大家有什么好辦法來討論

下,呵呵

Temperature[ 0 ] = Tflag ; //溫度正負(fù)標(biāo)志

Temperature[ 1 ] = TempDat / 100 + '0' ; //溫度十位值

Temperature[ 2 ] = TempDat % 100 / 10 + '0' ; //溫度個位值

Temperature[ 3 ] = TempDat % 10 + '0' ;//溫度小數(shù)位

}

如果想獲取當(dāng)前的溫度數(shù)據(jù),在主函數(shù)中調(diào)用v_TemperatureUpdate_f( void )就可以了。溫度

數(shù)據(jù)就保存到Temperature 中去了。至于如何顯示,就不用多說了吧~@_@~

時間和溫度一起顯示出來看看

(原文件名:7.jpg)

引用圖片

OK,至此ds18b20 和ds1302 的應(yīng)用告一段落。如果有不懂的,記得多看datasheet,多交流。

本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學(xué)會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉