自己的學結文檔,有些亂,勿怪
1、51單片機的延時計算
void Delay10us() //@12.000MHz
{
unsigned char i;
_nop_();
_nop_();
i = 27;
while (--i);
}
上面這段代碼是用STC-ISP軟件中的軟件延時計算器給出的,選用的是8051指令集STC-Y5,延時10us。
以前都是直接這麼拿來用的,今天卻突然想搞個明白,為什麼代碼要這麼寫。
于是查了各方資料。
從單片機計時的源頭找起,它由下面幾部分依次組成。
首先是時鐘周期的算法:時鐘周期(T)=1(秒)/晶振頻率。
(比如:上面代碼的時鐘周期為1/12M(秒))。
這是單片機的基本時間單位。是由晶振震蕩出來的,也叫震蕩周期。
其次是機器周期:機器周期是由時鐘周期組成的,機器周期是單片機完成一個基本操作所需要的時間。
關于機器周期,每種單片機可能都不太一樣,我也隻用過傳統51和STC這兩款,就拿此來對比下
1 傳統的8051單片機:
它的1個機器周期是由12個時鐘周期組成的。
以12M晶振舉例,它的一個機器周期就是:12(個時鐘周期)*1(秒)/12MHz = 1(us)
2 STC單片機:
拿我常用的STC12C5A60S2這款單片機來講,它可以有兩個模式選擇,
一個是1T模式,在這個模式下STC單片機1個時鐘周期就是1個機器周期;
另一個是12T模式,這個模式下STC單片就和傳統的8051單片機一樣,12個時鐘周期組成1個機器周期。
由此可見1T模式的速度就是12T模式的12倍。
以12M晶振為例,1T模式下就可以算得機器周期是:
1(個時鐘周期)*1(秒)/12Mhz = 1/12(us)
最後是指令周期:這個是單片機執行一條指令所需要的時間,它是由機器周期組成的。
現在可以回到正文開頭的代碼中了。這個10us的函數是怎麼得出來的呢?
這個我之前查過很多資料,比如執行while語句需要多少個機器周期。賦值需要多少個周期。也就是查這個占用了我很大一部分時間。直到最後将上面的延時函數直接調到main函數中debug調試,才明白,問題其實很簡單啊。
無論是執行什麼語句,最終都會回到彙編上來,debug裡單步調試,所有的指令周期就會明明白白了。
我用main函數直接調用延時函數,如下:
void Delay10us() //@12.000MHz
{
unsigned char i;
_nop_();
_nop_();
i = 27;
while (--i);
}
main
{
Delay10us();
}
我用的keil軟件,将上述build之後,點擊debug,開始調試
看圖片上,開始debug,程序的起始就在C:0x0183 020171 LJMP Delay10us(C:0171),
這裡有個長轉移指令LJMP,它要轉移到C:0171行去執行Delay10us這個函數。
那執行LJMP這個指令需要多長時間呢,查找STC數據手冊,在1T模式下,此條指令在單片機上運行需要4個時鐘周期。
接下來,按單步調試F11鍵,如下圖:
程序成功轉移到C:0171行,跳轉到Delay10us函數中,此行程序執行NOP指令,空操作。查STC數據手冊,NOP指令占用1個時鐘周期。
接下來C:0172行,依然是NOP指令,1個時鐘周期。
接下來C:0173行,此行執行 MOV R7,#0x1B,将立即數送入寄存器。是将27賦值給i。依然查手冊,此條指令2個時鐘周期。
繼續:
此時執行到while語句了,這裡執行的指令時 DJNZ R7,C:0175,寄存器減1非0轉移。此條指令執行1次4個時鐘周期。上面已經将寄存器填入27了,因此這條指令将執行27次。繼續:
循環了27次,終于到0了,程序繼續向下執行,此行指令RET,子程序返回。此條指令4個時鐘周期。繼續:
程序又回到了起點。
好了,可以計算一下此次延時的時間了。1個LJMP,4時鐘;2個NOP,2時鐘;1個MOV,2時鐘;27個DJNZ,108時鐘;1個RET,4時鐘。
4 2 2 108 4=120。
單片機的時鐘周期是:1(S)/12MHz = 1/12(us)
此次延時的時間是:120 × 1/12(us)= 10(us)
總結
其實并沒有絕對的準确延時,上面隻是理想化的狀态,單片機的中斷或者其他事件都可能影響到延時的。
另外,同樣的STC單片機,同樣的延時10us,同樣的1T,官方給出的STC12系列和STC15系列的延時函數就不一樣,STC12系列在延時函數内部要少兩個NOP指令。debug對比,也是少量NOP,其他都一樣。按照12系列和15系列的手冊描述,他們的指令周期是相同的。
2、c51單片機紅外通信接收端編程
2.1 紅外遙控器發射
通常紅外遙控為了提高抗幹擾性能和降低電源消耗,紅外遙控器常用載波的方式傳送二進制編碼,常用的載波頻率為38kHz,這是由發射端所使用的455kHz晶振來決定的。在發射端要對晶振進行整數分頻,分頻系數一般取12,所以455kHz÷12≈37.9kHz≈38kHz。也有一些遙控系統采用36kHz、40 kHz、56 kHz等,一般由發射端晶振的振蕩頻率來決定。所以,通常的紅外遙控器是将遙控信号(二進制脈沖碼)調制在38KHz的載波上,經緩沖放大後送至紅外發光二極管,轉化為紅外信号發射出去的。
二進制脈沖碼的形式有多種,其中最為常用的是PWM碼(脈沖寬度調制碼)和PPM碼(脈沖位置調制碼,脈沖串之間的時間間隔來實現信号調制)。如果要開發紅外接收設備,一定要知道紅外遙控器的編碼方式和載波頻率,我們才可以選取一體化紅外接收頭和制定解碼方案。
2.2位定義
用戶碼或數據碼中的每一個位可以是位 ‘1’ ,也可以是位 ‘0’。區分 ‘0’和 ‘1’是利用脈沖的時間間隔來區分,這種編碼方式稱為脈沖位置調制方式,英文簡寫PPM
2.3數據格式
數據格式包括了引導碼、用戶碼、數據碼和數據碼反碼,編碼總占32位。數據反碼是數據碼反相後的編碼,編碼時可用于對數據的糾錯。注意:第二段的用戶碼也可以在遙控應用電路中被設置成第一段用戶碼的反碼。
2.4程序編寫要點
void ReadIr() interrupt 0
{
u8 j,k;
u16 err;
Time=0;
delay(700); //7ms
if(IRIN==0) //确認是否真的接收到正确的信号
{
err=1000; //1000*10us=10ms,超過說明接收到錯誤的信号,IRIN=0,中斷端口沒有打開。
/*當兩個條件都為真是循環,如果有一個條件為假的時候跳出循環,免得程序出錯的時
侯,程序死在這裡*/
while((IRIN==0)&&(err>0)) //等待前面9ms的低電平過去,這裡也是程序沒有正常打開
{
delay(1);
err--;
}
if(IRIN==1) //如果正确等到9ms低電平
{
err=500;
while((IRIN==1)&&(err>0)) //等待4.5ms的起始高電平過去
{
delay(1);
err--;
}
for(k=0;k<4;k ) //共有4組數據
{
for(j=0;j<8;j ) //接收一組數據
{
err=60;
while((IRIN==0)&&(err>0))//等待信号前面的560us低電平過去
{
delay(1);
err--;
}
err=500;
while((IRIN==1)&&(err>0)) //計算高電平的時間長度。這裡必須是IRIN=1必須一直打開,但
{ //端口中斷不會一直打開,必須滿足這兩個條件才能實現,
delay(10); //0.1ms
Time ;
err--;
if(Time>30) //時間過長,證明是錯誤輸出。
{
return;
}
}
IrValue[k]>>=1; //k表示第幾組數據,即因數據傳輸時二進制碼,所以,一個IrValue[k]的
// u8 IrValue[6];u8 Time;值需要轉化為二進制碼,因此一個值需要八次轉換,因為是U8一 //個字節
if(Time>=8) //如果高電平出現大于565us,那麼是1
{
IrValue[k]|=0x80; // 右移的時候,高位補0,用或補位,保證高位在移動過程的IrValue[k]的 // 值不變,
}
Time=0; //用完時間要重新賦值
}
}
}
if(IrValue[2]!=~IrValue[3])
{
return;
}
}
}
3、移位總結
IrValue[k]>>=1; //k表示第幾組數據 這是把變量整體右移一位,由于是無符号最高位補0if(Time>=8) //如果高電平出現大于565us,那麼是1{IrValue[k]|=0x80; //變量最高位置一}Time=0; //用完時間要重新賦值
這樣如果判斷是1了就把最高位置一,0的話不用清零,因為右移的時候已經補0了
4、單片機紅外通信(紅外編碼發射和紅外接收解碼代碼)
一、NEC 協議特征:1. 8 位地址和 8 位命令長度2. 每次傳輸兩遍地址(用戶碼)和命令(按鍵值)3. 通過脈沖串之間的時間間隔來實現信号的調制(PPM)4. 38Khz 載波5. 每位的周期為 1.12ms(低電平)或者 2.25ms(高電平)
二、NEC 協議的典型脈沖鍊:用戶碼和數據碼中的‘0’和‘1’是利用脈沖的時間間隔來區分,這種編碼方式稱為脈沖 位置調制方式(PPM)。其中位 0 首先為 0.56ms 的高電平,然後是 0.565ms 的低電平;位 1 首先是 0.56ms的高電平,然後是 1.69ms 的低電平。五、編程注意事項1.紅外接收頭引腳信号是相反的電平。 以上電平是從發射頭角度來看,紅外接收頭引腳輸出的是相反的電平。 如圖,即沒有數據傳輸時,P3.2 引腳保持為高電平,當接收到數據時,首先是引導 碼,9ms 的低電平和 4.5ms 的高電平,然後是 32 位數據和 1 位停止位。一般來說, P3.2 與單片機的某中斷引腳相連,當接收數據時,低電平會觸發中斷。2.數據從 LSB(低位)開始發送,所以選擇右移方式接收數據。 四個字節的數據都是先發送 D0,最後發送 D7。所以接收到 1 位數據後,給變量的 最高位賦值,右移。或者先右移,再給變量的最高位賦值。3.可以用一個數組保存 32 個數據的持續時間,用于後面判斷高低電平。 用定時器對兩個數據(中斷)之間的時間計時,并保存這個持續時間用于以後判斷 是位 1 還是位 0。4.可以用 2 字節,4 字節變量存儲 32 個數據,以節省代碼空間。可以用兩個 16 位的 int 型變量存儲數據,第一個 int 變量存儲用戶碼,第二個存儲數 據碼和數據反碼。也可以用一個 32 位 long 型的變量存儲所有數據。5.判斷停止位。 接收到停止位後可以屏蔽紅外引腳的中斷,防止後面數據的幹擾,解碼成功後在開 啟中斷。
4.1發射編碼部分核心代碼:
#include <stc8.h>
typedef unsigned char uchar;
typedef unsigned int uint;
sbit irsend = P7^5; // 紅外發送
sbit K = P0^7; // 按鍵總開關
sbit key1 = P0^0; // 按鍵1
sbit key2 = P0^1; // 按鍵2
uint hwcount, count; // 要進中斷的總次數、用于記錄進入中斷次數
uchar irsys[2]= {0x00,0xff}; // 16位用戶碼
bit hsflag = 0; // 發送38KHz載波标志位
uchar ircode; // 發送的紅外數據
void Timer1Init(void) // 13微秒@12.000MHz
{
AUXR &= 0xBF; // 定時器時鐘12T模式
TMOD &= 0x0F; // 設置定時器模式
TMOD |= 0x20; // 設置定時器模式
TL1 = 0xF3; // 設置定時初值
TH1 = 0xF3; // 設置定時重載值
TF1 = 0; // 清除TF1标志
TR1 = 0; // 定時器1關閉計時
ET1 = 1; // 開定時器1中斷
EA = 1; // 開總中斷
}
void Timer1_isr() interrupt 3
{
count ;
if(hsflag) // 有發射标志,則發射38khz
{
irsend = ~irsend;
}
else // 否則不發射,即相當于發射編碼中的低電平
irsend = 1;
}
void ir_SendByte() // 紅外發送一字節數據
{
uchar i;
for(i=0;i<8;i ) // 一字節八位,循環八次
{
hwcount = 43; // 0.56ms高電平,需要進43次定時器1中斷(560/13=43)
hsflag = 1; // 發射38KHz載波标志
count = 0; // count置0,從這時起記錄進入定時器1中斷的次數
TR1 = 1; // 定時器1開啟計時
while(count < hwcount); // 在此等待,直到進入中斷次數達到43次
TR1 = 0; // 定時器1關閉計時
if(ircode&0x01) // 數據是從最低位開始發送的,最低位是1則要進130次中斷
{
hwcount = 130; // 1.69ms低電平,進中斷總次數130(1690/13=130)
}
else // 最低位是0,則要進43次定時器1中斷
{
hwcount = 43; // 0.565ms低電平,進中斷總次數43(565/13=43)
}
hsflag = 0; // 低電平,不需要38kHz載波
count = 0;
TR1 = 1;
while(count < hwcount);
TR1 = 0;
ircode = ircode >> 1; // 将數據右移一位,即從低位到高位發送
}
}
void ir_Send(uchar date)
{
hwcount = 692; // (引導碼中的)9ms高電平,9000/13=692
hsflag = 1; // 高電平需要38kHz載波
count = 0;
TR1 = 1;
while(count < hwcount);
TR1 = 0;
hwcount = 346; // (引導碼中)4.5ms低電平,4500/13=346
hsflag = 0; // 低電平不需要38kHz載波
count = 0;
TR1 = 1;
while(count < hwcount);
TR1 = 0;
ircode = irsys[0]; // 發送用戶碼的前8位
ir_SendByte();
ircode = irsys[1]; // 發送用戶碼的後8位
ir_SendByte();
ircode = date; // 發送鍵值
ir_SendByte();
ircode = ~date; // 發送鍵值反碼
ir_SendByte();
hwcount = 43; // 0.56ms高電平,560/13=43
hsflag = 1; // 高電平需要38kHz載波
count = 0;
TR1 = 1; // 定時器1開啟計時
while(count < hwcount);
TR1 = 0; // 定時器1關閉計時
hwcount = 43; // (NEC協議中的停止碼)0.56ms低電平
hsflag = 0;
count = 0;
TR1 = 1;
while(count < hwcount);
TR1 = 0;
irsend = 1; // 關閉紅外發射
}
void main()
{
K = 0; // 按鍵總開關拉低
Timer1Init(); // 定時器1初始化
while(1)
{
if(key1 == 0) // 按鍵1
{
ir_Send(0x8a); // 發送鍵值8aH
}
if(key2 == 0) // 按鍵2
{
ir_Send(0xa6); // 發送鍵值a6H
}
}
}
4.2按鍵代碼
#include "key.h"
#define GPIO_KEY P0
bit flag = 0;
/**********************************************
* 函數名:Check_key
* 描述 :矩陣按鍵掃描(缺陷:不能通過按一次按鍵,給變量隻加一)
* 參數 :無
* 返回值:鍵值
* 調用 :外部調用
**********************************************/
//unsigned char Check_key(void)
//{
// unsigned char row,col,temp1,temp2,keyvalue;
// temp1 = 0x01;
// for(row=0;row<4;row ) // 行掃
// {
// P0 = 0xF0; // 先将P0.4~P0.7置高
// P0 = ~temp1; // 使P0.1~P0.3中有一位為0
// temp1 *= 2; // temp1左移一位
// if((P0 & 0xF0) < 0xF0) // 當按鍵按下時,(P0 & 0xF0) 高四位不在是F,可能為7或B或D或E。
// { // 這時可以确定按下的是(row 1)行
// temp2 = 0x80;
// for(col=0;col<4;col ) // 列掃
// {
// if((P0 & temp2)==0x00) // 當(P0 & temp2)等于0x00時,可以确定按下的位置是(col 1)列
// {
// keyvalue = row*4 col; // 得到所按下按鍵的鍵值
// return keyvalue; // 把得到的鍵值作為返回值
// }
// temp2 /= 2; // temp2右移一位
// }
// }
// }
// return 16; // 因為定義數碼管段選表中,16對應的是全滅,故無按鍵按下時返回16
//}
/*************************************************
* 函數名:delay_ms
* 描述 :延時函數
* 參數 :xms , xms是幾延時幾毫秒
* 返回值:無
* 調用 :内部調用
*************************************************/
void delay_ms(unsigned int xms)
{
unsigned char i, j;
unsigned int x;
for(x=xms;x>0;x--)
{
i = 16;
j = 147;
do
{
while (--j);
} while (--i);
}
}
/*************************************************
* 函數名:key_scan
* 描述 :把按下的矩陣按鍵的鍵值返回
* 參數 :無
* 返回值:按下的鍵值
* 調用 :外部調用
*************************************************/
unsigned char key_scan()
{
unsigned char keyvalue1,keyvalue2,a=0;
if(flag==0)
{
keyvalue2=16;
flag=1;
}
GPIO_KEY = 0xf0; // 高四位為1,低四位為0
if(GPIO_KEY != 0xf0)
{
delay_ms(10); // 延時消抖
if(GPIO_KEY != 0xf0)
{
GPIO_KEY=0xf0;
switch(GPIO_KEY)
{
case 0xe0: keyvalue1 = 3;break; // 确定矩陣按鍵被按下的位置是第幾列
case 0xd0: keyvalue1 = 2;break; // 0、1、2、3
case 0xb0: keyvalue1 = 1;break;
case 0x70: keyvalue1 = 0;break;
}
GPIO_KEY=0x0f;
// 确定矩陣按鍵被按下位置的鍵值:列(或0或1或2或3) 行(或0或4或8或12)
if((GPIO_KEY != 0x0d)||(GPIO_KEY != 0x0b)||(GPIO_KEY != 0x07))
keyvalue2 = keyvalue1;
if(GPIO_KEY == 0x0d)
keyvalue2 = keyvalue1 4;
if(GPIO_KEY == 0x0b)
keyvalue2 = keyvalue1 8;
if(GPIO_KEY == 0x07)
keyvalue2 = keyvalue1 12;
while((a<50)&&(GPIO_KEY!=0x0f))
{
delay_ms(10);
a ;
}
}
}
if(GPIO_KEY==0xF0)
keyvalue2 = 16;
return keyvalue2;
}
4.3單片機紅外解碼源程序如下
#include <stc8.h>
#include "hc595.h"
typedef unsigned char uchar;
typedef unsigned int uint;
sbit ir = P3^2; // 紅外接收
uchar irtime; // 記錄定時器0中斷次數
uchar irdata[33]; // 存放接收到的33位紅外數據的每位進入中斷的次數
uchar bitnum; // 數組下标,用于記錄是第幾位紅外數據
uchar startflag; // 開始接收标志
uchar irok; // 33位數據收集完成标志
uchar ircode[4]; // 用于存放16位用戶碼 8位鍵值 8位鍵值反碼
uchar irprosok; // 四個碼值轉化完成标志
uchar disnum[8]; // 把四個碼值分割成8位,用于數碼管顯示
void Int0Init(void) // 外部中斷0初始化
{
IT0 = 1; // 下降沿觸發
EX0 = 1; // 開啟外部中斷0
EA = 1; // 開總中斷
ir = 1; // 紅外接收置1
}
void Timer0Init(void) // 定時器0初始化,模式:12T,晶振:12MHz
{
TMOD = 0x02; // 定時器0模式2,8位自動重裝載
TH0 = 0x00; // 256*(1/12)*12 = 0.256ms
TL0 = 0x00;
ET0 = 1; // 開定時器0中斷
EA = 1; // 開總中斷
TR0 = 1; // 定時器0開始計時
}
void irpros(void) // 碼值轉換
{
uchar num, k, i, j;
k = 1;
for(j=0;j<4;j ) // 四個碼值,循環四次
{
for(i=0;i<8;i ) // 每個碼值八位,循環八次
{
num = num >> 1; // 從最低位開始接收
if(irdata[k]>6) // 判斷這位數據是0還是1:(0:1.12/0.256=4.4)(1:2.25/0.256=8.8)
{
num = num | 0x80;
}
k ;
}
ircode[j] = num; // 存放碼值
}
irprosok = 1; // 碼值轉換完成标志
}
void irwork(void) // 碼值分割,用于數碼管顯示
{
disnum[0] = ircode[0]/16;
disnum[1] = ircode[0];
disnum[2] = ircode[1]/16;
disnum[3] = ircode[1];
disnum[4] = ircode[2]/16;
disnum[5] = ircode[2];
disnum[6] = ircode[3]/16;
disnum[7] = ircode[3];
}
void Int0 () interrupt 0
{
if(startflag)
{
if(irtime>32 && irtime<63) // 8~16ms
{
bitnum = 0;
}
irdata[bitnum] = irtime; // 存放每位進中斷的次數
irtime = 0; // 清零,為下次計數做準備
bitnum ; // 下标加一
if(bitnum==33) // 判斷是否33位數據接收完
{
bitnum = 0;
irok = 1; // 接收完成标志
}
}
else
{
irtime = 0;
startflag = 1;
}
}
void Timer0() interrupt 1
{
irtime ;
}
void main()
{
Int0Init();
Timer0Init();
while(1)
{
if(irok == 1) // 接收完成
{
irpros();
irok = 0;
}
if(irprosok == 1) // 碼值轉換完成
{
irwork();
irprosok = 0;
}
display(0,disnum[4]); // 顯示鍵值
display(1,disnum[5]);
display(2,20); // 顯示"H"
}
}
總結下,下面這一個代碼比較清晰,思路,利于理解,明确了中斷和定時的時間,而不像前面的是利用單片機的機器周期,指令周期來确認。
,