国产成人av人人爽人人澡-亚洲国产日韩欧美一区-好吊日视频这里只有精品-日本高清精品视频在线

你好!歡迎來到深圳市穎特新科技有限公司!
語言
當前位置:首頁 >> 技術中心 >> 單片機入門 >> 從單片機初學者邁向單片機工程師(對初學者非常有用)

從單片機初學者邁向單片機工程師(對初學者非常有用)

關鍵字:單片機 初學者 工程師 作者:admin 來源:不詳 發(fā)布時間:2018-05-18  瀏覽:7

 從單片機初學者邁向單片機工程師

目錄:

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

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

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

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

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

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

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

八、綜合應用之一——如何設計復雜的多任務程序...................................... 47

九、綜合應用之二——DS1320/DS18B20 應用...............................................60

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

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

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

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

^_^。

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

驗的地方寫出來,權當是我對自己的一個總結吧。而作為看官的你,如果看到了我的錯誤,還請一定指正,

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

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

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

題,也一度被這些問題所困惑……等到回過頭來,看到自己曾經走過的路,唏噓不已。經常混跡于論壇里,

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

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

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

一路學習過來的過程中,幫助最大之一無疑來自于網絡了。很多時候,通過網絡,我們都可以獲取到所

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

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

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

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

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

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

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

料滿天飛,而深入一點的學習資料卻很少的原因了。相較于其他領域,單片機技術的封鎖更加容易。盡管

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

實驗。但是在實際工程應用中卻是另外一回事。有能力的高手無暇或者是不愿公開自己的學習經驗。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

編語言。

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

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

如同如下實例代碼一般

void main(void)

{

LedInit() ;

While(1)

{

LED = ON ;

DelayMs(500) ;

LED = OFF ;

DelayMs(500) ;

}

}

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

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

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

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

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

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

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

從上面我們可以總結出

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

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

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

先看看我們的硬件結構是什么樣子的。

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

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

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

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

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

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

紅色的壓降為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 真值表如下:

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

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

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

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

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

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

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

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

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

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

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

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

動能力)

了解了相應的硬件后,我們再來編寫驅動程序。

首先定義LED 的接口

#define LED P0

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

電平時,LED 熄滅。

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

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

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

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

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

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

unsigned char g_u8LedState = 0 ; //LED 狀態(tài)標志, 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 時標到

{

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ù)使用該變量后,應該對其復位(清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()內部首先判斷1MS 的系統(tǒng)時標是否到了,如果沒有到就直接退出函數(shù),如果到

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

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

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

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

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

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

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

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

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

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

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

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

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

以及整個芯片的驅動能力

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

附完整程序代碼(基于電子工程師之家的單片機開發(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)時標

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

unsigned char g_u8LedState = 0 ; //LED 狀態(tài)標志, 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 時標到

{

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 時標標志位置位

}

實際效果圖如下

點亮

--------------------------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 這個文件主要內容就是定時器初始化,以及定時器中斷服務函數(shù)。其內容

如下。

#include

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

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 時標標志位置位

}

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

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

函數(shù)調用。

Timer.h 內容如下。

#ifndef _TIMER_H_

#define _TIMER_H_

extern void Timer0Init(void) ;

extern bit g_bSystemTime1Ms ;

#endif

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

Led.c 內容如下:

#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)標志, 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 時標到

{

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ù),因此在相應的Led.h 中需要作相應的聲明。

Led.h 內容:

#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

至此,第三章到此結束。

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

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

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

3. typedef 的作用

4. 工程模板如何組織

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

了。就像我們平?措娨曇粯樱雌饋懋嬅媸沁B續(xù)的,實質不是這個樣子,所有連續(xù)動作都是一幀幀靜止

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

就是連續(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)時,應該是在200us 整數(shù)倍時刻時。在這里我們用

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

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

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

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

Timer.c 內容如下。

#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 調整一下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 技術在我們實際生活中應用的非常多。比較典型的應用就是控制電機的轉速,控制充電電流的

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

等等,如果有興趣可以到網上去獲取相應資料學習。

關于漸明漸暗的燈就簡單的講到這里。

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

設計

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

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

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

單片機的愛好者,他們問我說單片機我已經掌握了,該如何進一步的學習下去呢?我并不急

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

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

個簡易的可以調整的時鐘,要求如下:

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

時-分-秒

XX-XX-XX

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

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

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

整相應的顯示內容。按鍵支持短按,和長按,即短按時,修改的內容每次增加一或者減小一,

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

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

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

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

一個簡單的例子。

下面這段程序是在網上隨便搜索到的:

while(1)

{

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

{

P0=table[num];

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

P2=code[num] ;

delayms(2) ;

}

}

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

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

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

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

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

本章的內容正是探討如何解決多任務環(huán)境下(不帶OS)的數(shù)碼管程序設計的編寫問題。理解

了其中的思想,無論要求我們顯示的形式怎么變化(如數(shù)碼管閃爍,移位等),我們都可以很方

便的解決問題。

數(shù)碼管的顯示分為動態(tài)顯示和靜態(tài)顯示兩種。靜態(tài)顯示是每一位數(shù)碼管都用一片獨立的驅動

芯片進行驅動。比較常見的有74LS164,74HC595 等。利用這類芯片的好處就是可以級聯(lián),

留給單片機的接口只需要時鐘線,數(shù)據(jù)線,因此比較節(jié)省I/O 口。如下圖所示:

利用74LS164 級聯(lián)驅動8 個單獨的數(shù)碼管

靜態(tài)顯示的優(yōu)點是程序編寫簡單。但是由于涉及到的驅動芯片數(shù)量比較多,同時考慮到PCB

的布線等等因素,在低成本要求的開發(fā)環(huán)境下,單純的靜態(tài)驅動并不合適。這個時候就可以

考慮到動態(tài)驅動了。

動態(tài)驅動的圖如下所示(以EE21 開發(fā)板為例)

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

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

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

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

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

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

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

頻率非?斓臅r候,人眼已經無法分辨出數(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() ; //時間相關處理

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ù)的掃描頻

率已經不為50 Hz 了,而是1 / 33 = 30.3 Hz 。這個頻率數(shù)碼管已經可以感覺到閃爍了,

因此不符合我們的要求。為什么會出現(xiàn)這種情況呢? 我們剛才計算的50 Hz 是系統(tǒng)只有

LedDisplay()這一個任務的時候得出來的結果。當系統(tǒng)添加了其它任務后,當然系統(tǒng)循環(huán)執(zhí)

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

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

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

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

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

十幾ms 的時間里,已經產生好幾個2.5 ms 的時標消息了,這樣豈不是漏掉了掃描,顯示

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

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

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

務程序設計的時候會講解到。

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

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

#include

#include "MacroAndConst.h"

為了方便計算,我們取數(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

#include "MacroAndConst.h"

#include "Timer.h"

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

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

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, //'-'號代碼

} ;

--------------------------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) ; //消隱,只需要設置位選不為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ù)內部定義一個靜態(tài)的變量s_LedDisPos,用來表示掃描數(shù)碼管的位置。每當我們執(zhí)行

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

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

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

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

應數(shù)碼管的點亮,盡管這個時間很短暫。但是因為我們的數(shù)碼管是不斷掃描的,所以看起來

還是會有些微微亮。為了消除這種影響,就有必要再送段碼數(shù)據(jù)之前關掉位選。

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

#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

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

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

標進行計數(shù),當計數(shù)值達到500,即500 * 2 = 1000 ms 時候,即表示已經逝去了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 時標消息產

生。然后我們根據(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)

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

{

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

}

}

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

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

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

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

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

描程序。

[/post

本章所附例程在EE21 學習板上調試通過,擁有板子的朋友可以直接下載附件對照學習

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

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

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

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

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

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

的程序編寫。

◎按鍵檢測的原理

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

他們的身影。

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

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

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

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

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

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

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

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

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

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

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

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

這樣的嗎?

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

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

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

面這幅按鍵按下時候對應電平變化的波形圖一樣:

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

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

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

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

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

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

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

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

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

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

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

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

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 的時間,啥也沒干,考

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

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

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

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

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

◎消除抖動有必要嗎?

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

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

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

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

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

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

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

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

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

按下,多次響應。

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

設計

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

回鍵值,同時進入長按狀態(tài)S4,在長按狀態(tài)下每次進入按鍵程序時候對按鍵時間計數(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)。

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

驅動程序吧。

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

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

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

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

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

設置成輸入狀態(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

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

根據(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()作為底層按鍵的驅動程序,為上層按鍵掃描提供一個接口,這樣我們編寫的上層

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

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

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

重用已經足夠了。

在編寫我們的上層按鍵掃描函數(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)流程轉移圖,然后再看下面的函數(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

}

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

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

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

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

獲更多。

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

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

多鍵按下,功能鍵等等,亦相當簡單,在下一章,我們就去實現(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)的變化。當按鍵按下時候,第

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

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

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

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

八、綜合應用之一——如何設計復雜的多任務程序

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

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

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

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

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

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

閃爍,顯示效果變差了。

(原文件名:1.JPG)

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

引用圖片

圖1 單一任務簡單流程圖圖2 多任務簡單流程圖

很明顯,初學者在設計程序時,需要從程序構架思想上下功夫,在做了大量基本模塊練

習之后,需要總結提煉自己的程序設計思路(程序架構思想)。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(原文件名:2.JPG)

引用圖片

圖3 多任務復雜流程圖

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

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

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

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

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

(原文件名:3.JPG)

引用圖片

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

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

了不同的響應頻度,滿足條件的任務將獲得CPU 資源,CPU 為不同任務“關照”完成后,再

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

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

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

雜。

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

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

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

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

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

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

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

10,

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

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

描述,不同的任務用task0,task1……來描述。

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

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

首先是任務切換

while(1)

{

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

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

……

}

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

器啊!定時器中斷對任務延時量減一直到歸零,標志任務就緒。當沒有任務就緒時,任務切

換本身就是一個Idle 任務。

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。

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

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

直接給出。

#include "reg51.h"

#define TIME_PER_SEC 200 //定義任務時鐘頻率,200Hz

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

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

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

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

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]; //任務延時變量定義

//定時器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 定時中斷服務

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é)拍對任務延時變量減1 ,減至0 后,任務就緒。

}

/*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;}

//要產生1hz 信號,翻轉周期就是2Hz,以下同

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

//要產生5hz 信號,翻轉周期就是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 仿真波形圖

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

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

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

對任務本身有什么要求?

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

任務間如何延時?

……

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

(原文件名:5.JPG)

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

引用圖片

圖6 CPU 運行情況示意圖

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

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

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

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

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

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

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

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

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

不行。

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

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

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

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

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

得短了)

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

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

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

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

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

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

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

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

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

時,

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

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

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

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

要我們人等待10 年一樣,顯然這樣的任務是其它任務所耽擱不起的。像類似DS18B20 這樣

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

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

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

間段,不要CPU 關照,允許CPU 去關照其它任務。

將一個任務分成若干段,確保每段需要CPU 關照時長小于定時器

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

Easy51RTOS

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

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

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

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

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

大。

為了便于初學者學習,將代碼文件壓縮至4 個文件。

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

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

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

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

文件解讀如下:

os_cfg.h

#include "reg51.h"

#define TIME_PER_SEC 200 //定義任務時鐘頻率,200Hz

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

#define MAX_TASK 4 //定義任務數(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]; //定義任務延時量變量

//定時器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 定時中斷服務

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é)拍對任務延時變量減1 ,減至0 后,任務就緒。

}

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

void run(void (*ptask)())

{

(*ptask)();

}

main.c

#include "os_cfg.h"

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

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

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

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

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

void (* code task[])() = {task0,task1,task2,task3}; //獲得任務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;} //就緒任務調度

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

}

void task0(void) //任務0

{

LED0 = !LED0;

task_delay[0] = TASK_DELAY0;

}

void task1(void) //任務1

{

LED1 = !LED1;

task_delay[1] = TASK_DELAY1;

}

void task2(void) //任務2

{

LED2 = !LED2;

task_delay[2] = TASK_DELAY2;

}

void task3(void) //任務內分段設計

{

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)先級設定:

for(i=0;i

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

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

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

低。

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

一種方法。

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

九、綜合應用之二——DS1320/DS18B20 應用

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

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

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

感器。下面讓我們分別來認識并學會應用這兩種芯片。

首先依舊是看DS1302 的datasheet 中的相關介紹。

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

(原文件名:1.jpg)

引用圖片

上面是它的一些基本的應用介紹。

下面是它的引腳的描述。

(原文件名:2.jpg)

引用圖片

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

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

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

(原文件名:3.jpg)

引用圖片

這是DS1302 內部的31 個RAM 寄存器。在某些應用場合我們可以應用到。如我們想要做

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

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

會丟失~~

(原文件名:4.jpg)

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

引用圖片

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

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

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

路是怎么樣的。

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

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

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

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

續(xù)工作。

下面讓我們來驅動它。

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ù)的結構體*

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

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é)的內容*

* 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 當前設定的地址讀取一個字節(jié)的內容*

* 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é)的內容*

* 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é)的內容*

* 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 時鐘寄存器的值(主程序中只需調用一次即可) *

* 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 ) ; //判斷是否初始化一次標識寫入

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

}

}

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

* Function: void v_ClockUpdata_f( void ) *

* Description:讀取時間數(shù)據(jù),并保存在結構體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 進行操作了。當我們想要獲取當前時間時,只需

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

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

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

^_^.

看看顯示效果如何~~

(原文件名:5.jpg)

引用圖片

下面再讓我們看看DS18B20 吧。

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

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

(原文件名:1.jpg)

引用圖片

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

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

(原文件名:2.jpg)

引用圖片

DS18B20 的內部存儲器分為以下幾部分

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

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

碼。

(原文件名:3.jpg)

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

引用圖片

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

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

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

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

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

DS18B20 的通訊協(xié)議:

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

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

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

這樣才能對DS18B20 進行預定的操作。

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

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

(原文件名:4.jpg)

引用圖片

上圖即DS18B20 的復位時序圖。

下面是讀操作的時序圖

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

(原文件名:5.jpg)

引用圖片

這是寫操作的時序圖

(原文件名:6.jpg)

引用圖片

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

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ù)組.依次存放正負標志,溫度值十位,個位,和小數(shù)位*

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

uint8 Temperature[ 4 ] ;

void v_Delay10Us_f( uint16 Count )

{

while( --Count )

{

_nop_();

}

}

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

* Function: uint8 v_Ds18b20Init_f( void ) *

* Description: 初始化DS18B20 *

* Parameter: *

* *

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

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

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,則復位成功,否則復位失敗

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 寫周期結束

Cmd >>= 1 ;

}

}

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

* Function: uint8 v_Ds18b20Read_f( void ) *

* Description: 向DS18B20 讀取一個字節(jié)的內容*

* 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: 讀取當前的溫度數(shù)據(jù)(只保留了一位小數(shù)) *

* Parameter: *

--------------------------cn---------------------------------------------------www.ourDev.cn-------------------------- 75

* *

* Return: 讀取到的溫度值*

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

uint16 v_Ds18b20ReadTemp_f( void )

{

uint8 TempH, TempL ;

uint16 ReturnTemp ;

/* if( v_Ds18b20Init_() ) return ; //復位失敗,在這里添加錯誤處理的代碼*/

v_Ds18b20Init_f() ; /復位DS18B20

v_Ds18b20Write_f( 0xcc ) ; //跳過ROM

v_Ds18b20Write_f( 0x44 ) ; //啟動溫度轉換

v_Ds18b20Init_f() ;

v_Ds18b20Write_f( 0xcc ) ; //跳過ROM

v_Ds18b20Write_f( 0xbe ) ; //讀取DS18B20 內部的寄存器內容

TempL = v_Ds18b20Read_f() ; //讀溫度值低位(內部RAM 的第0 個字節(jié))

TempH = v_Ds18b20Read_f() ; //讀溫度值高位(內部RAM 的第1 個字節(jié))

ReturnTemp = TempH ;

ReturnTemp <<= 8 ;

ReturnTemp |= TempL ; //溫度值放在變量ReturnTemp 中

return ReturnTemp ;

}

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

* Function: void v_TemperatureUpdate_f( void ) *

* Description:讀取當前的溫度數(shù)據(jù)并轉化存放在數(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 ; //溫度正負標志

Temperature[ 1 ] = TempDat / 100 + '0' ; //溫度十位值

Temperature[ 2 ] = TempDat % 100 / 10 + '0' ; //溫度個位值

Temperature[ 3 ] = TempDat % 10 + '0' ;//溫度小數(shù)位

}

如果想獲取當前的溫度數(shù)據(jù),在主函數(shù)中調用v_TemperatureUpdate_f( void )就可以了。溫度

數(shù)據(jù)就保存到Temperature 中去了。至于如何顯示,就不用多說了吧~@_@~

時間和溫度一起顯示出來看看

(原文件名:7.jpg)

引用圖片

OK,至此ds18b20 和ds1302 的應用告一段落。如果有不懂的,記得多看datasheet,多交流。

編輯:admin  最后修改時間:2018-05-18

聯(lián)系方式

0755-82591179

傳真:0755-82591176

郵箱:vicky@yingtexin.net

地址:深圳市龍華區(qū)民治街道民治大道973萬眾潤豐創(chuàng)業(yè)園A棟2樓A08

Copyright © 2014-2023 穎特新科技有限公司 All Rights Reserved.  粵ICP備14043402號-4

日本不卡片一区二区三区| 日本加勒比在线观看一区| 中文字幕不卡欧美在线| 丰满人妻少妇精品一区二区三区| 亚洲人午夜精品射精日韩| 一区二区免费视频中文乱码国产| 国产精品日韩精品一区| 国产丝袜女优一区二区三区| 亚洲熟妇中文字幕五十路| 国产精品日韩精品一区| 国内尹人香蕉综合在线| 国产又黄又爽又粗视频在线| 亚洲最大福利在线观看| 午夜国产精品国自产拍av| 亚洲中文在线中文字幕91| 午夜小视频成人免费看| 婷婷亚洲综合五月天麻豆 | 欧美成人黄色一级视频| 日本午夜免费福利视频| 美女极度色诱视频在线观看| 年轻女房东2中文字幕| 国产中文字幕久久黄色片| 真实国产乱子伦对白视频不卡| 国产真人无遮挡免费视频一区| 国产精品亚洲欧美一区麻豆| 国产一区二区三中文字幕| 国产精品免费自拍视频| 日韩一区二区免费在线观看| 日本成人中文字幕一区| 少妇福利视频一区二区| 日韩熟妇人妻一区二区三区| 国产三级不卡在线观看视频| 亚洲男人的天堂久久a| 国产一区二区三中文字幕 | 久久精品国产99精品最新| 日本本亚洲三级在线播放| 色婷婷久久五月中文字幕| 国产高清一区二区白浆| 99热在线精品视频观看| 亚洲欧美国产中文色妇| 日本女人亚洲国产性高潮视频|