一、中斷
1.1 基本概念
其實中斷的概念很好理解:試想一下你正在教室裏面搞單片機(這相當於與CPU正在執行主程序)這時,防空警報響了(一箇中斷信號),因此你得停下你手頭的工作,去響應這個防空警報(中斷響應),這個防空警報需要你執行一些行爲(比如逃出教室或者躲在課桌底下),當你執行了一段時間之後,防空警報結束了(中斷服務執行完了),那麼你就從新回到教室繼續搞單片機。
這個過程就是中斷。
我們先看看中斷都有哪幾種:(1)外部中斷0【中斷號 0】,它的優先級是最高的。(2)定時器/計數器0 【中斷號1】 (3)外部中斷1【中斷號 2】 (4)定時器/計數器 1【中斷號 3】
(5)串口中斷【中斷號 4】 (6)定時器/計數器 2 【中斷號 5】
先看看下面的圖:其中,TCON, IE, IP 等都是與中斷有關的寄存器。
要想在單片機中給一箇中斷(我們以外部中斷0爲例),看看需要什麼條件:
- 我們看到上圖的 EA,它是中斷總允許,也就是說,我們得把 EA 置成 1 ,纔可能輸入中斷信號
- EA=1 之後,我們就要看中斷源允許,也就是說,我們要選擇輸入的中斷種類。EX0 表示外部中斷0,ET0 表示計數器/定時器 T0 中斷、、、
- 如果我們想給外部中斷0(其實外部中斷0和外部中斷1原理一樣),那麼,我們還需要給出 ,當這個信號是0時,表示低電平觸發、1表示下降沿觸發。
1.2 代碼和實驗部分
下面,我們想實現這樣一個效果:
【代碼一】:實現數碼管1,3,5亮,每隔一定時間同時亮一個數,然後我們在一些特定時刻給它外部中斷0,中斷時,讓CPU點亮第一個LED燈,我們看看是什麼效果:
#include<reg52.h>
#define unchar unsigned char
#define uint unsigned int
sbit WEI = P2^7; //定義控制數碼管的位選信號的單片機端口位(或者說定義單片機的P2.7口是WEI)
sbit DULA = P2^6; //定義單片機P2.6口是DULA(控制數碼管的段選信號)
sbit D1 = P1^0; //定義控制第一個LED的單片機端口位
sbit int0 = P2^3;
uchar num;
uchar code table[] = {0x3f, 0x06,0x5b,0x4f,0x66,0x6d,
0x7d,0x07,0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71}; //定義數碼管的編碼表,爲了節約內存空間
void delay(uint);
void main()
{
EA=1; //先打開中斷總允許
EX0=1; //打開中斷源允許EX0,表示允許外部中斷0輸入
IT0 = 1; //表示下降沿觸發(因爲一上電默認單片機P3.2口是高電平的)
//下面是數碼管的顯示:先位選
WEI = 1; //打開位選
P0 = 0xea;
WEI = 0; //鎖住位選信號
while(1)
{
for(num=0;num<16;num++)
{
DULA = 1; //打開段選
P0 = table[num];
DULA = 0;
if(num == 3)
{
//如果num == 3那麼就給一個下降沿,即讓P3.2口變成低電平
int0 = 0;
}
}
}
}
void exter0() interrupt 0
{
D1 = 0; //中斷響應就是要先讓第一個LED亮
delay(1000); //延時
D1 = 1; //滅LED
int0 = 1; //把P3.2口變回高電平,用以準備下一次的中斷。
}
void delay(uint z)
{
//延時函數
uint x,y;
for(x=z;x>0;x--)
for(y=1000;y>0;y--);
}
也就是說:我們想要給出一箇中斷信號(這裏我們暫時先不管定時器和串口)就以外部中斷0、外部中斷1爲例。那麼我們就首先需要打開總中斷允許(即令 EA=1);接下來,就要選取中斷種類(即令EX0, ET0, EX1, ET1,ES),選哪個就把那個置1,最後一步就是選擇觸發方式:(IT0, IT1)置1表示下降沿觸發、置0表示低電平觸發
二、定時器
2.1 原理簡析
這是單片機裏面一個相當重要的概念:首先,51單片機裏面有兩個定時器 T0, T1。
定時器一但啓動,它便在原來的數值上開始加1計數,若在程序開始時,我們沒有設置 TH0 和TL0,它們的默認值都是0,假設時鐘頻率爲12MHz,12個時鐘週期爲一個機器週期,那麼此時機器週期爲1us.
假如定時器工作在模式一:也就是我們先在 TL0 這8位裏面記錄時間,每當記滿 TL0 的八位後,才向 TH0 進 1. 那麼,在TH0、TL0的初始值都是0時,我們記滿整個定時器需要的機器週期就是:x = 個機器週期,再來多一個週期,那麼計數器就溢出,那麼隨機向CPU 發出中斷請求。
而上面的情況,從計數開始到向CPU發出中斷,所花的時間是:1us x 65536 = 65536us =65.536ms
那麼,假如我們給定計數器一個初始值,那麼我們就可以控制計數器在經過規定的時間後產生中斷。
要知道,在此之前,我們想要計時都是通過 去做的,不僅要手工調參,而且定時精度不佳,但是利用定時器定時就非常精確!
2.2 調用定時器的步驟
首先,51單片機的定時器由兩個寄存器 和 控制:
TMOD也是一個八位寄存器,低4位控制 T0 定時器,高四位控制 T1 定時器。
如果我們只是使用T0定時器,那麼我們就直接把高四位置0即可。
我們看看:00表示模式0、01表示模式1、10表示模式2、11表示模式3.
我們分析一下模式1:模式1就是我們在上文提到的那種計數方式:也就是我們先在 TL0 這8位裏面記錄時間,每當記滿 TL0 的八位後,才向 TH0 進 1.
=========================================
那麼舉個例子:假設我們要定時 50ms,也就是50000us.那麼我們看一下TH0和TL0裏面的值到底怎麼算:
因爲我們想要計數器精準記到50000us,之後這個計數器得告訴我們從開始到現在剛好走完了50000us,這個“告訴我們”的過程,就是計數器發出中斷(即計數到65536的時候)。那麼也就是說,這個計數器就不可能是從0開始的,它得是從某一個值開始計數,記了50000次剛好到65535.
那麼,很簡單,這個初始值就是 (65536-50000) = 15536
現在我們要做的,就是把這個15536分配到 TH0 和 TL0 中。我們會議一下模式一,是在 TL0 得8位都記滿,即256個機器週期,纔會向 TH0 進一位。因此,TH0我們可以這樣表示:
這裏,’/’ 表示取模,也就是商(整數)
TL0 我們可以這樣表示:
這裏的 ‘%’ 表示 取餘數
對於 TCON的設置,我們不用把所有位的置一次,只需要定義 (用於控制 T0 的啓動和停止)
如果要同時用 T0 和 T1,那麼我們就令 TR0 = 1; TR1 = 1; 即可。
由於定時器在計數滿了溢出的時候也會向 CPU 發送中斷請求,因此,我們也需要向調用外部中斷0那樣設置一些東西:比如打開中斷總允許 EA(EA=1)、在打開 ET0(ET0=1)
重點:使用定時器的步驟!!
- 首先規定定時器的初始值 TH0 和 TL0 (如果是Mode 1 可以套用上面的公式)
- 然後我們設置控制定時器的第一個寄存器 TMOD
- 設置中斷允許的信號
- 最後設置控制定時器的第二個寄存器 TCON
2.3 代碼與實驗
現在,我們還是希望點亮1、3、5三個數碼管,每一次顯示一個數字,但是閃爍的間隔時間我們不再使用之前的 函數,而是採用定時器。我們看看代碼:
注意:我們這裏是假定了單片機的振盪頻率是 12MHz,因爲在12MHz時,晶振週期是 1/12M,而恰好1個機器週期等於 12 個晶振週期,那麼一個機器週期也就是 (1/12)x12 = 1us。如果不是12MHz,那麼一個機器週期的時間需要重新算一下。
#include<reg52.h>
#define uchar unsigned char
#define uint unsigned int
sbit WEI = P2^7; //定義位選信號端
sbit DULA = P2^6; //定義段選信號端
uchar num,t;
uchar code table[] = {0x3f, 0x06,0x5b,0x4f,0x66,0x6d,
0x7d,0x07,0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71};
void main()
{
TH0 = (65536-50000)/256; //定義定時器初始高位
TL0 = (65536-50000)%256; //定義定時器初始低位
TMOD = 0x01; //定義TMOD(也就是控制T0的第一個寄存器)
EA=1; //打開中斷總允許
ET0=1; //打卡定時器T0的中斷允許
TR0 = 1; //定時器T0啓動
WEI=1;
P0=0Xea;
WEI=0;
for(num=0;num<8;num++)
{
if(t == 20)
{
DULA=1;
P0=table[num];
DULA=0;
}
}
}
void extraT0() interrupt 1
{
TH0=(65536-50000)/256;
TL0=(65536-50000)%256; //因爲在CPU執行這一段中斷代碼時,計數器已經溢出了,所以要把它的初始值置成一開始的那個
t++; //這裏 t 每自增一次,代表已經經過了50ms,那麼t = 20時也就是1s
}