本系列文章探討的主題都是在Keil uVision3集成編譯環境下完成的,針對的是51系列單片機。
下面就介紹一下在我的單片機程序里必須要包含的一個頭文件----"const.h",完整內容如下:
#ifndef _CONST_H_
#define _CONST_H_
#include <intrins.h>
#define TRUE 1
#define FALSE 0
typedef unsigned char BYTE;
typedef unsigned int WORD;
typedef unsigned long DWORD;
typedef float FLOAT;
typedef char CHAR;
typedef unsigned char UCHAR;
typedef int INT;
typedef unsigned int UINT;
typedef unsigned long ULONG;
typedef UINT WPARAM;
typedef ULONG LPARAM;
typedef ULONG LRESULT;
typedef void VOID;
typedef const CONST;
typedef void *PVOID;
typedef bit BOOL;
#define MAKEWORD(lo, hi) ((WORD)(((BYTE)(lo)) | ((WORD)((BYTE)(hi))) << 8))
#define MAKEDWORD(lo, hi) ((DWORD)(((WORD)(lo)) | ((DWORD)((WORD)(hi))) << 16))
#define LOWORD(dw) ((WORD)(dw)
#define HIWORD(dw) ((WORD)(((DWORD)(dw) >> 16) & 0xFFFF))
#define LOBYTE(w) ((BYTE)(w))
#define HIBYTE(w) ((BYTE)(((WORD)(w) >> 8) & 0xFF))
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define SET_STATE_FLAG(state, mask) ((state) |= (mask))
#define RESET_STATE_FLAG(state, mask) ((state) &= ~(mask))
#define TEST_STATE_FLAG(state, mask) ((state) & (mask))
//偏移量從0開始
#define TEST_BIT(b, offset) (1 & ((b) >> (offset)))
#define SET_BIT(b, offset) ((b) |= (1 << (offset)))
#define RESET_BIT(b, offset) ((b) &= (~(1 << (offset))))
//將BCD碼變為十進制,如將0x23變為23
//注意:高四位和低四位均不能大於9
#define BCD_TO_DECIMAL(bcd) ((BYTE)((((BYTE)(bcd)) >> 4) * 10 + (((BYTE)(bcd)) & 0x0f)))
#define DECIMAL_TO_BCD(decimal) ((BYTE)(((((BYTE)(decimal)) / 10) << 4) | ((BYTE)(decimal)) % 10))
#define NOP() _nop_()
#define BYTE_ROTATE_LEFT(b, n) _crol_(b, n)
#define BYTE_ROTATE_RIGHT(b, n) _cror_(b, n)
#define WORD_ROTATE_LEFT(w, n) _irol_(w, n)
#define WORD_ROTATE_RIGHT(w, n) _iror_(w, n)
#define DWORD_ROTATE_LEFT(dw, n) _lrol_(dw, n)
#define DWORD_ROTATE_RIGHT(dw, n) _lror_(dw, n)
#define ENABLE_ALL_INTERRUPTS() (EA = 1)
#define DISABLE_ALL_INTERRUPTS() (EA = 0)
#endif
其實,里面的大部分內容都是從VC的頭文件里拷貝過來的沒什么創新,而且從命名也比較好判斷出實現的功能,也就不一一介紹了。下面說一下幾個常用的:
1、LOBYTE( )和HIBYTE( )。從名字就可以看出,取一個字長的低字節和高字節。這兩個宏在定時器的初值裝載中經常要用到。在網上或書上幾乎所有的程序都是這樣:
TH0 = (65536- X) / 256;
TL0 = (65536 - X) % 256;
其實這樣賦值是非常不直觀的,高字節為什么要除以256?低字節為什么要對256取余?如果換成如下的寫法是不是很明了呢?
TH0 = HIBYTE(65536- X);
TL0 = LOBYTE(65536 - X);
2、TEST_BIT( )、SET_BIT( )和RESET_BIT( )。單片機的資源比較緊張,經常要用到以“位”為單位。這三個宏就是為了方便位操作的。
3、BCD_TO_DECIMAL( )和DECIMAL_TO_BCD( )。用過ds1302的朋友都知道,從中度取的都是BCD格式的信息,經常需要與十進制之間進行轉換。
當然,這個頭文件只是起到一個拋磚引玉的作用,隨時都加入需要的功能。這樣做的好處是把經常用到的功能提煉出來,提高了代碼的復用率。更重要的是,今后所有自己的庫文件的編寫都用到了此頭文件中的內容。就像所有Windows程序都需要包含windows.h頭文件一樣。
單片機的串口是經常使用的功能之一,封裝起來也相對簡單一些,讓我們慢慢體會c語言中封裝的含義......
#ifndef _SERIAL_CONFIG_H_ #define _SERIAL_CONFIG_H_ #include "const.h" #ifndef OSC_FREQUENCY #error undefined OSC_FREQUENCY #endif /****************************************************************************** 僅限於: 串口方式1的工作模式,即1位起始位,8位數據位和1位停止位,無校驗位,波特率不倍增 ******************************************************************************/ #define OSC_FREQUENCY 11.0592 typedef enum tagBAUD { b_2400 = 2400, b_4800 = 4800, b_9600 = 9600, b_19200 = 19200, INVALID_BAUD, }BAUD; typedef void (*RECVPROC)(BYTE byte); BOOL OpenSerial(BAUD Baud, RECVPROC pRecvFunc); BOOL SendData(const BYTE* pData, BYTE nSize); void CloseSerial(); #endif
我寫單片機程序的的原則很簡單,就是要好看~_~不過這個“好看”的含義可是很廣的,基本上可以概括為代碼必須簡潔、優美、高效。
有人也許會問,上來為什么先讓看一個不知道函數內部細節的頭文件,而不是直接給出具體實現?這個問題其實就需要用“封裝”的本質來回答了:封裝就是讓調用端不用去關心具體的實現,從而達到信息的隱藏。注意:這里的“封裝”是一種邏輯含義,是一種編程規范或准則。沒有人可以約束你不去遵守。一看到頭文件就能馬上了解封裝的這個功能模塊提供了哪些功能,因為寫程序就是需要通過合理的結構把各功能模塊連接起來達到協調運作的過程。
好了,大道理說了不少了,看看具體的東西吧。.c文件如下:
#include "serialconfig.h" #include "chiptypedef.h" ECVPROC g_pfnRecvFunc = NULL; BOOL OpenSerial(BAUD Baud, RECVPROC pRecvFunc) { BYTE LoadValue = 0; if(pRecvFunc == NULL) return FALSE; g_pfnRecvFunc = pRecvFunc; switch(Baud) //確保輸入的波特率是正確的 { case b_1200: case b_2400: case b_4800: case b_9600: break; default: return FALSE; break; } /***************************************************************************** LoadValue = 256 - OSC_FREQUENCY * 10^6 / (384 * Baud) 因每次運算的結果上限限制,故做了變換 ******************************************************************************/ LoadValue = 256 - (BYTE)(1000 * 1.0f * (float)OSC_FREQUENCY / 384 * 1000 * 1.0f / Baud); TMOD |= T1_M1_; //定時器T1工作方式2 TH1 = LoadValue; TL1 = LoadValue; //不可TL1 = TH1賦值 PCON = 0x00; //波特率不倍增 SCON = 0x50; //串行通信方式1,允許接收 SM1 = 1; // REN = 1; TR1 = 1; //啟動定時器1 ES = 1; //開串行中斷 EA = 1; //開總中斷 return TRUE; } void CloseSerial() { TR1 = 0; //關定時器1 ES = 0; //關串行中斷 } BOOL SendData(const BYTE* pData, BYTE nSize) { BYTE i = 0; if(pData == NULL || nSize == 0) return FALSE; for(i = 0; i < nSize; i++) { SBUF = pData[i]; while(!TI); TI = 0; } return TRUE; } void SerialISR() interrupt SIO_VECTOR { RI = 0; (*g_pfnRecvFunc)(SBUF); }
#include "serialconfig.h" void RecvProc(BYTE byte);//串口回調函數的聲明 void main() { OpenSerial(b_9600, RecvProc); while(TRUE) { //做一些處理后,調用SendData()發送數據 } } void RecvProc(BYTE byte) { //這里就是串口中斷的回調函數,byte就是接收到的數據
讓我們看一看把串口經過封裝后main()函數的程序流程,是不是很清晰呢?更重要的是,從主函數中根本看不到單片機底層的實現,完全像是在寫上位機程序,這樣的好處是可以全身心地注重程序的實現流程,而不要關心具體的實現細節。否則,錯綜復雜的東西都攪和到一塊很影響程序功能的實現。畢竟人的腦子同時思考的事情有限。
我的理解,程序就是通過抽象把易變的和不易改變的組合在一起。我們可以這樣來思考程序:每一個單片機程序自身需要完成很多相對獨立的功能。那么,什么是易變的呢?顯然,各種功能的順序流程是易變的,每個程序都不一樣。那么,什么又是不易改變的呢?是各相對獨立的功能模塊,比如:串口功能、定時器功能、LDC顯示功能......好了,現在我們已經把串口功能分離出來了,也就是把不易改變的功能分離了出來了。但仔細想想具體實現會發現,依然還有易變的因素在里面,比如:不同的波特率、晶振的頻率。於是,我們想到了波特率可以作為函數的參數來適應不同的需求,把晶振頻率分離出來單獨的頭文件以供該項目下所有的文件使用。最后,所有的易變因素都確定了下來,變成了為不易改變的因素,這樣的功能封裝基本上就達到可以“復用”的目的了。所以,我實現的這個串口封裝可以適應所有51系列的單片機,原因也在於此。
通過一個小小的串口功能封裝體會一下博大精深的“封裝”思想還是很不錯的嘛~_~
http://blog.csdn.net/jiqiang01234/article/details/5132522