一、前言
本系統為基於紅外和超聲波的手動/自動調速風扇系統,風扇轉速的調節模式可分為自動模式與手動模式:在自動模式下,由超聲波檢測人與風扇的距離,根據距離調節風扇轉速;在手動模式下,可通過紅外遙控的按鍵調節風扇轉速。相應引數資訊通過LCD液晶螢幕顯示。本系統的主控晶片採用STC89C52微控制器,測距採用HC-SR04超聲波模組,風扇電機由L298N電機驅動模組驅動,遙控部分用傳統的紅外遙控器,顯示部分用LCD1602液晶螢幕。電機驅動模組採用12V供電,微控制器及其他各部分採用5V供電。
該專案是筆者在大一暑假時完成的,在大三上學期又把程式碼整理、優化了一次,拿去充當了一次課程設計。正好趕上這兩天有空,決定把這個小玩意整理成部落格。一來這個東西確實是當時用心做了的,且以後筆者可能也不會再碰微控制器相關的東西了,整理出來留作念想;二來希望能在學弟學妹們做課設的時候提供一些思路,拋磚引玉;僅此而已。這裡先附醜圖一張:
二、思路分析
2.1 系統供電問題
STC89C52微控制器及超聲波感測器、紅外遙控接收頭、液晶螢幕均為+5V標準供電,可以直接使用電腦USB介面引出電壓。但考慮到電機用到PWM調速,需要大電壓和大電流,因此決定使用電池盒額外供電。
2.2 自動/手動模式的切換
主函式內部用一個while大迴圈,超聲波資料採集及電機驅動等程式均放在迴圈內部。在while內部有兩段程式,一段為手動模式,一段為自動模式,分別放在if…else…的兩個分支內。定義全域性變數flag,在紅外遙控中斷內部可改變flag的值,通過flag的值控制if…else…選擇結構的走向,進而實現兩種模式的切換。
2.3 PWM訊號的產生
電機轉速調節需用到PWM訊號,需由微控制器內部產生。有兩種可行方案:其一為通過軟體延時,不斷地改變某一引腳電平的高低,由該引腳向外輸出PWM訊號;其二為通過中斷計時,計滿後進入中斷服務程式,在中斷服務程式中改變某一引腳電平的高低,由該引腳向外輸出PWM訊號。考慮到系統較為複雜,用方案一在時間上會佔用微控制器的大量資源,影響到系統的穩定性和實時性,因此採用方案二。
2.4 微控制器內部資源的分配
在本系統中,用到兩個定時器和兩個中斷:超聲波測距時等待返回波用到一個定時器,控制PWM訊號的發生用到一個定時器;紅外遙控的響應用到一個外部中斷,PWM訊號的發生用到定時器中斷。考慮到系統的實時性,給紅外遙控分配優先順序最高的外部中斷0,PWM訊號發生使用定時器T0並開中斷,超聲波測距使用定時器T1,不開中斷。
三、硬體搭建
由於硬體部分中的很多模組在模擬軟體中都沒有,且各模組之間的連線關係比較簡單,因此在這裡不提供電路圖,僅用語言描述各引腳之間的連線關係。
3.1 微控制器最小系統
對51 系列微控制器來說, 最小系統一般應該包括: 微控制器、時鐘電路、復位電路、輸入/ 輸出裝置等。最小系統的焊接有一套標準的流程,為基本功,這裡不做贅述。
3.2 電機驅動模組
本系統電機驅動模組使用常見的L298N電機驅動模組。L298N晶片可以驅動兩個二相電機,也可以驅動一個四相電機,輸出電壓最高可達50V,可以直接通過電源來調節輸出電壓;可以直接用微控制器的IO口提供訊號;而且電路簡單,使用比較方便。
在本系統中,只使能了EnA來驅動一個電機,其中EnA接微控制器引腳P20,IN1接P21,IN2接P22。L298N電機驅動模組實物圖如下所示:
3.3 超聲波測距模組
在自動調速模式下,需用到超聲波模組採集距離資訊。本系統採用HC-SR04超聲波模組, HC-SR04超聲波測距模組可提供2cm-400cm的非接觸式距離感測功能,測距精度可達高到3mm;模組包括超聲波發射器、接收器與控制電路。基本工作原理:
(1)採用I0口TRIG觸發測距,給至少10us的高電平訊號;
(2)模組自動傳送8個40khz的方波,自動檢測是否有訊號返回;
(3)有訊號返回,通過I0口ECHO輸出一個高電平,高電平持續的時間就是超聲往返所用的時間。
(4)根據聲音在空氣中的速度為344米/秒,即可計算出所測的距離。
在本系統中,超聲波模組的Trig腳接微控制器引腳P36,Echo腳接微控制器引腳P22。HC-SR04工作時序圖如下所示:
3.4 紅外遙控模組
根據使用的編碼晶片不同,紅外遙控編碼的格式也不同,較普遍的有NEC標準和PHILIPS標準。最常用的是NEC標準,本系統採用的也是NEC標準。
NEC標準:遙控載波的頻率為38KHz(佔空比1:3)當某個鍵按下時,發射端首先發射一個完整的全碼,如果按鍵超過108ms仍未鬆開,接下來發射的程式碼(連發程式碼)將由起始碼(9ms)和結束碼(2.5ms)組成,並每隔108ms重複。
一個完整的全碼由引導碼、使用者碼、使用者碼、資料碼、資料碼以及資料反碼共同組成。其中,引導碼高電平9ms,低電平4.5ms;系統碼8位,資料碼8位,共32位;其中前16位為使用者識別碼,能區別不同的紅外遙控裝置,以防止不同的機種遙控碼互相干擾。後16位為8位的操作碼和8位的操作反碼,用於核對資料是否接收準確。收端根據資料碼做出應該執行上面動作的判斷。連發程式碼是在持續按鍵時傳送的碼。它告知接收端。某鍵是在被連續的按著。
NEC標準下的發射碼錶示:發射資料0時用“0.56ms高電平 + 0.565ms低電平 = 1.125ms”表示;發射資料1用“0.56ms高電平 + 1.69ms低電平 = 2.25ms”表示。
在本系統中,紅外接收器的INIR腳接微控制器引腳P32。NEC標準完整碼組成及NEC標準發射碼如下所示:
3.5 液晶顯示模組
本系統的液晶顯示部分採用LCD1602液晶螢幕。1602液晶也叫1602字元型液晶,它是一種專門用來顯示字母、數字、符號等的點陣型液晶模組 它有若干個5X7或者5X11等點陣字元位組成,每個點陣字元位都可以顯示一個字元。每位之間有一個點距的間隔,每行之間也有也有間隔,起到了字元間距和行間距的作用。
LCD1602是指顯示的內容為16X2,即可以顯示兩行,每行16個字元液晶模組(顯示字元和數字)。目前市面上字元液晶絕大多數是基於HD44780液晶晶片的,控制原理是完全相同的,因此基於HD44780寫的控制程式可以很方便地應用於市面上大部分的字元型液晶。
在本系統中,液晶螢幕接法如下所示:
3.6 供電模組
為了驅動電機,需採用+12V供電,結合手上現有資源,決定採用4節3.7V的鋰電池串聯供電。串聯後的輸出電壓在+15V左右,使用LM2596S直流降壓模組,將電壓降至+12V後提供給電機驅動模組L298N,微控制器所需的+5V電可直接從電機驅動模組中引出。
四、程式碼分享
程式碼用C語言編寫,在Keil4環境下開發的。給每個模組都寫了驅動,每個模組的驅動拿出後略加改動,都能單獨使用。程式碼工程結構如下所示:
4.1 總標頭檔案
把常用的巨集定義和硬體的引腳連線定義到了reg51.h裡面,更名為my52.h。所以整個工程程式碼中每個檔案都#include "my52.h"而不是 #include "reg52.h"。
1 #ifndef __MY52_H__ 2 #define __MY52_H__ 3 4 /* BYTE Registers */ 5 sfr P0 = 0x80; 6 sfr P1 = 0x90; 7 sfr P2 = 0xA0; 8 sfr P3 = 0xB0; 9 sfr PSW = 0xD0; 10 sfr ACC = 0xE0; 11 sfr B = 0xF0; 12 sfr SP = 0x81; 13 sfr DPL = 0x82; 14 sfr DPH = 0x83; 15 sfr PCON = 0x87; 16 sfr TCON = 0x88; 17 sfr TMOD = 0x89; 18 sfr TL0 = 0x8A; 19 sfr TL1 = 0x8B; 20 sfr TH0 = 0x8C; 21 sfr TH1 = 0x8D; 22 sfr IE = 0xA8; 23 sfr IP = 0xB8; 24 sfr SCON = 0x98; 25 sfr SBUF = 0x99; 26 27 /* 8052 Extensions */ 28 sfr T2CON = 0xC8; 29 sfr RCAP2L = 0xCA; 30 sfr RCAP2H = 0xCB; 31 sfr TL2 = 0xCC; 32 sfr TH2 = 0xCD; 33 34 35 /* BIT Registers */ 36 /* PSW */ 37 sbit CY = PSW^7; 38 sbit AC = PSW^6; 39 sbit F0 = PSW^5; 40 sbit RS1 = PSW^4; 41 sbit RS0 = PSW^3; 42 sbit OV = PSW^2; 43 sbit P = PSW^0; //8052 only 44 45 /* TCON */ 46 sbit TF1 = TCON^7; 47 sbit TR1 = TCON^6; 48 sbit TF0 = TCON^5; 49 sbit TR0 = TCON^4; 50 sbit IE1 = TCON^3; 51 sbit IT1 = TCON^2; 52 sbit IE0 = TCON^1; 53 sbit IT0 = TCON^0; 54 55 /* IE */ 56 sbit EA = IE^7; 57 sbit ET2 = IE^5; //8052 only 58 sbit ES = IE^4; 59 sbit ET1 = IE^3; 60 sbit EX1 = IE^2; 61 sbit ET0 = IE^1; 62 sbit EX0 = IE^0; 63 64 /* IP */ 65 sbit PT2 = IP^5; 66 sbit PS = IP^4; 67 sbit PT1 = IP^3; 68 sbit PX1 = IP^2; 69 sbit PT0 = IP^1; 70 sbit PX0 = IP^0; 71 72 /* P3 */ 73 sbit RD = P3^7; 74 sbit WR = P3^6; 75 sbit T1 = P3^5; 76 sbit T0 = P3^4; 77 sbit INT1 = P3^3; 78 sbit INT0 = P3^2; 79 sbit TXD = P3^1; 80 sbit RXD = P3^0; 81 82 /* SCON */ 83 sbit SM0 = SCON^7; 84 sbit SM1 = SCON^6; 85 sbit SM2 = SCON^5; 86 sbit REN = SCON^4; 87 sbit TB8 = SCON^3; 88 sbit RB8 = SCON^2; 89 sbit TI = SCON^1; 90 sbit RI = SCON^0; 91 92 /* P1 */ 93 sbit T2EX = P1^1; // 8052 only 94 sbit T2 = P1^0; // 8052 only 95 96 /* T2CON */ 97 sbit TF2 = T2CON^7; 98 sbit EXF2 = T2CON^6; 99 sbit RCLK = T2CON^5; 100 sbit TCLK = T2CON^4; 101 sbit EXEN2 = T2CON^3; 102 sbit TR2 = T2CON^2; 103 sbit C_T2 = T2CON^1; 104 sbit CP_RL2 = T2CON^0; 105 106 /*------------------一下為新增部分---------------------*/ 107 108 #define uint unsigned int 109 #define uchar unsigned char 110 111 #define HighGear 4 112 #define MiddleGear 3 113 #define LowGear 2 114 115 //電機驅動 116 sbit ENA = P2^0; //PWM輸入埠 117 sbit IN1 = P2^1; //0 118 sbit IN2 = P2^2; //1 119 120 //超聲波 121 sbit Trig=P3^6; 122 sbit Echo=P3^7; 123 124 //LCD1602 125 sbit RS=P1^2; // 資料/命令選擇端(H/L) 126 sbit RW=P1^1; //讀寫選擇端(H/L) 127 sbit E=P1^0; //使能訊號 128 129 //紅外遙控 130 sbit IRIN=P3^2;// 紅外接收器埠定義,外部中斷0優先順序最高 131 132 133 #endif
4.2 電機驅動及標頭檔案
電機驅動檔案motor_driver.c:
1 #include "my52.h" 2 3 uchar motor_gear,pwm_num; 4 5 void motor_run(uchar gear) 6 { 7 motor_gear = gear; //擋位設定,分2,3,4檔 8 TMOD = 0x11; //設定定時器1為工作方式1 9 TH1 = (65536-100)/256; //裝初值,每0.1ms中斷一次 10 TL1 = (65536-100)%256; 11 ET1 = 1; //開定時器1中斷 12 TR1 = 1; //啟動定時器1 13 } 14 15 void T1_PWM() interrupt 3 16 { 17 TH1 = (65536-100)/256; //裝初值 18 TL1 = (65536-100)%256; 19 pwm_num++; 20 if(pwm_num == 5) 21 pwm_num = 0; 22 if(pwm_num <= motor_gear) 23 ENA = 1; 24 else 25 ENA = 0; 26 }
電機驅動標頭檔案motor_driver.h:
1 #ifndef __MOTOR_DRIVER_C__ 2 #define __MOTOR_DRIVER_C__ 3 extern motor_run(uchar gear); //可填2,3,4,佔空比分別為0.6,0.8,1 4 #endif
4.3 超聲波驅動及標頭檔案
超聲波驅動檔案sr04_driver.c:
1 #include "my52.h" 2 #include "motor_driver.h" 3 #include <intrins.h> // _nop_()延時 4 5 extern uchar InfraredGear; 6 7 uint distance() //HC-SR04超聲波測距模組工作函式 8 { 9 uint dis = 0; 10 uint time = 0; 11 uchar i = 10; 12 13 Trig = 0;//初始化 14 Echo = 0; 15 16 17 TMOD = 0x11; 18 TH0=0;//給T0裝初值0 19 TL0=0; 20 21 Trig = 1; 22 while(i--) 23 _nop_(); 24 while(Echo==0); 25 TR0=1;//啟動T0 26 while(Echo==1);//等待返回訊號的接收完畢 27 time=TH0*256+TL0;//微秒 28 dis=(time*1.7+5)/10;//340米每秒即0.34毫米每微秒,1.7=0.34/2×10,來回除2,四捨五入先乘10 29 TR0=0;//關閉T0 30 31 return dis; 32 } 33 34 void sr04_motor(uint dist) 35 { 36 if(dist <= 300) 37 InfraredGear = LowGear; 38 else if(dist > 600) 39 InfraredGear = HighGear; 40 else 41 InfraredGear = MiddleGear; 42 }
超聲波驅動標頭檔案sr04_driver.h:
1 #ifndef __SR04_DRIVER_C__ 2 #define __SR04_DRIVER_C__ 3 extern uint distance(); //單位是毫米 4 extern void sr04_motor(uint dist); 5 #endif
4.4 紅外遙控驅動及標頭檔案
紅外遙控驅動檔案infrared_driver.c:
1 #include "my52.h" 2 #include "delay.h" 3 4 extern uchar InfraredGear; 5 extern uchar Flag ; 6 7 uchar IrValue[4];//兩位使用者碼,一位資料碼,一位資料反碼 8 uchar num; 9 10 void read() interrupt 0{//紅外中斷讀取檔位資料 11 uchar j,k,t; 12 uint i; 13 num=0; 14 t=IrValue[2]; 15 delay_ms(7);//起始碼前9ms為低電平,在這裡等待7ms 16 if(IRIN==0){//確認真的收到訊號後執行以下程式 17 i=1000; //如果出錯利用i跳出以下等待,以免程式在這裡死迴圈 18 while((IRIN==0)&&(i>0)){ //等待前9ms結束 19 delay_10us(1); 20 i--; 21 } 22 if(IRIN==1){//起始碼前9ms結束,後4.5ms為高電平 23 i=500; //用i防止死迴圈 24 while((IRIN==1)&&(i>0)){//等待起始碼的後4.5ms高電平 25 delay_10us(1); 26 i--; 27 } 28 for(k=0;k<4;k++){//2個使用者碼,1個資料碼,1個資料反碼,共4個位元組 29 for(j=0;j<8;j++){ //每個位元組8位,以下程式用於確定每位電平的高低 30 i=60; 31 while((IRIN==0)&&(i>0)){//等待0.56ms的低電平,每位前面都有0.56ms的低電平 32 delay_10us(1); //後面高電平0.565ms(565us)為0, 1.69ms(1690us)為1 33 i--; 34 } 35 i=500; 36 while((IRIN==1)&&(i>0)){//低電平結束,高電平到來後進入,用於計算高電平持續時間 37 delay_10us(10);//延時100us 38 num++; 39 i--; 40 if(num>30){//超出3000us(3ms),本程式出錯(最大不能超過2.25ms),返回主調函式 41 return; 42 } 43 } 44 IrValue[k]>>=1;//騰出最高位用於接收本位資料 45 if(num>=8){//高電平持續時間大於800us,該位為1 46 IrValue[k]|=0x80; //給最高位寫1 47 } 48 num=0; //計數變數清零 49 } 50 } 51 } 52 if(IrValue[2]!=~IrValue[3]){//資料位校驗 53 IrValue[2]=t; 54 return; 55 } 56 } 57 if(IrValue[2]==69) 58 InfraredGear=LowGear; 59 else if(IrValue[2]==70) 60 InfraredGear=MiddleGear; 61 else if(IrValue[2]==71) 62 InfraredGear=HighGear; 63 else if(IrValue[2]==68) //切換自動模式 64 Flag = 0; 65 else if(IrValue[2]==67) //切換手動模式 66 Flag = 1; 67 else if(IrValue[2]==64) //急停 68 IN1 = 1; 69 else if(IrValue[2]==21) 70 IN1 = 0; 71 }
紅外遙控驅動標頭檔案infrared_driver.h:
1 #ifndef __INFRARED_DRIVER_C__ 2 #define __INFRARED_DRIVER_C__ 3 4 #endif
4.5 液晶顯示驅動及標頭檔案
液晶顯示驅動檔案lcd1602_driver.c:
1 #include "my52.h" 2 3 extern uchar InfraredGear; 4 5 void delays(uint i) 6 { 7 uchar j; 8 while(i--) 9 for(j=50;j>0;j--); 10 } 11 void write_com(uchar com) 12 { 13 RS=0; 14 P0=com; 15 delays(1); 16 E=1; 17 delays(1); 18 E=0; 19 } 20 void write_date(uchar date) 21 { 22 RS=1; 23 P0=date; 24 delays(1); 25 E=1; 26 delays(1); 27 E=0; 28 } 29 void lcdinit() 30 { 31 RW=0; 32 E=0; 33 write_com(0x38); //設定16X2顯示,5X7點陣,8位資料介面 34 write_com(0x0c); //設定開顯示,不顯示游標 35 write_com(0x06); //寫一個字元後地址指標加1 36 write_com(0x01); //顯示清零,資料指標清零 37 } 38 39 void display0(uint dis) //手動調速下1602顯示 40 { 41 uchar g1,g2,g3,g4; 42 g1=dis%10; 43 g2=(dis/10)%10; 44 g3=(dis/100)%10; 45 g4=dis/1000; 46 lcdinit(); 47 write_com(0x80); 48 write_date('M'); 49 write_date('o'); 50 write_date('d'); 51 write_date('e'); 52 write_date('l'); 53 write_date(':'); 54 write_date('A'); 55 write_date('U'); 56 write_date('T'); 57 write_date('O'); 58 59 write_date(' ');//擋位顯示 60 write_date(0x30+InfraredGear-1); 61 62 write_com(0x80+0x40);//換行顯示 63 64 write_date('D');//距離顯示 65 write_date('i'); 66 write_date('s'); 67 write_date(':'); 68 write_date(0x30+g4); 69 write_date(0x30+g3); 70 write_date(0x30+g2); 71 write_date(0x30+g1); 72 write_date('m'); 73 write_date('m'); 74 } 75 76 void display1() 77 { 78 lcdinit(); 79 write_com(0x80); 80 81 write_date('M'); 82 write_date('o'); 83 write_date('d'); 84 write_date('e'); 85 write_date('l'); 86 write_date(':'); 87 write_date('M'); 88 write_date('A'); 89 write_date('N'); 90 write_date('U'); 91 92 write_date(' ');//擋位顯示 93 write_date(0x30+InfraredGear-1); 94 }
液晶顯示驅動標頭檔案lcd1602_driver.h:
1 #ifndef __LCD1602_DRIVER_C__ 2 #define __LCD1602_DRIVER_C__ 3 extern void delays(uint i); 4 extern void write_com(uchar com); 5 extern void write_date(uchar date); 6 extern void lcdinit(); 7 extern void display0(uint dis); 8 extern void display1(); 9 #endif
4.6 延時函式及標頭檔案
延時函式所在檔案delay.c:
1 #include "my52.h" 2 3 void delay_ms(uint t) 4 { 5 uint i,j; 6 for(i=0;i<t;i++) 7 for(j=0;j<114;j++); 8 } 9 10 void delay_10us(uint t) //延時函式,t=1延時10us 11 { 12 while(t--); 13 }
對應標頭檔案delay.h:
1 #ifndef __DELAY_C__ 2 #define __DELAY_C__ 3 extern void delay_ms(uint t); 4 extern void delay_10us(uint t); 5 #endif
4.7 主函式
1 #include "my52.h" 2 #include "motor_driver.h" 3 #include "sr04_driver.h" 4 #include "delay.h" 5 #include "lcd1602_driver.h" 6 #include "infrared_driver.h" 7 8 uchar InfraredGear = LowGear;//手動調節擋位,紅外驅動檔案中改變該值 9 uchar Flag = 0;//自動0/手動1模式 10 11 void main() 12 { 13 uint dis; 14 ENA = 1; 15 IN1 = 0; 16 IN2 = 1; 17 EA = 1;//開總中斷 18 EX0 = 1; //開外部中斷0,接收紅外訊號 19 while(1) 20 { 21 if(Flag == 0) //自動 22 { 23 dis = distance(); //超聲波測距 24 sr04_motor(dis); //根據距離調節擋位 25 motor_run(InfraredGear); 26 display0(dis); //液晶顯示 27 delay_ms(100); //延時,每100ms更新一次資料 28 } 29 else 30 { 31 motor_run(InfraredGear); //手動調節轉速 32 display1(); //液晶顯示 33 delay_ms(100); //延時,每100ms更新一次液晶內容 34 } 35 } 36 }