本系列文章探讨的主题都是在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