作者:
jiqiang01234
http://topic.csdn.net/u/20090722/15/6009c1d2-93bc-47dc-a646-70bc2968ac49.html
写单片机程序也是程序,也要遵循写软件的一些基本原则,不是为了完成功能那么简单。我看过的所有的C语言单片机书籍基本都不注重模块化思想,完全是拿着C当汇编用,简直是在糟蹋C语言!
如下问题,几乎所有的单片机书籍中都大量存在(更别说网上的和现实中的代码了,书上都写的那么差劲,学的人能好到哪里去):
1、变量到处定义,根本不管变量的生命周期是否合适(请回答:全局变量、局部变量、静态变量、volatile变量有什么区别联系?)
2、变量名称极不规范,根本从名字上看不出来这个变量类型是什么,到底想干什么。
3、函数定义几乎不用参数,全都是void
4、 语句写的一点都不直观,根本就是在用汇编。比如:想取一个字长的高字节和低字节,应该定义一个宏或是函数来做,如#define HIBYTE(w) ((BYTE)((DWORD)(w) >> 8)),以后直接用HIBYTE()多直观,难道非得用(BYTE)((DWORD)(w) >> 8)代表你的移位操作的水平很高吗?
5、最重要的一点,没有建立模块化的编程思想。一个程序往往要很多部分协同工作,需要把不同的功能分离出来单独创建一个.h和.c的文件,然后在头文件中把可以访问的函数暴露出来。
6、不思考曾经做过的程序是否还有改进的余地,写程序如果只是为了写而写,一辈子也长进不了多少
为了证明我以上的观点,特此发一下我对c51定时器的封装,此定时器可以同时设定多个定时任务,定时精度由晶振精度决定。我的项目中一般用的是12MHZ的晶振,最小定时在20ms基本可以接受,再小的不能保证。
Code:
-
- 头文件
-
- #ifndef _TIMER_CONFIG_H_
- #define _TIMER_CONFIG_H_
- #include "const.h"
- #include "oscfrequencydef.h"
-
- #ifndef OSC_FREQUENCY
- #error undefined OSC_FREQUENCY
- #endif
-
-
- #warning **********************************************************************************
- #warning !! make sure MAX_TIMER_EVENT_NUM and TIMER0_BASE_INTERVAL has appropriate value!!
- #warning **********************************************************************************
-
-
-
-
- #define MAX_TIMER_EVENT_NUM 5 //可设置不同定时事件的最大个数(至少为2)
- #define TIMER0_BASE_INTERVAL 20 //单位:毫秒
-
-
-
-
- typedef void (*TIMERPROC)(BYTE nID);
-
- void InitTimer0();
- BOOL SetTimerCallback(TIMERPROC lpTimerFunc);
- BOOL SetTimer0(BYTE nID, WORD wInterval);
-
-
-
-
-
-
-
-
-
- typedef struct tagTIMERINFO
- {
- BYTE nID;
- WORD wInterval;
- WORD wElapse;
-
- }TIMERINFO;
- static BOOL AddTail(const TIMERINFO* pTimerInfo);
- static BOOL Remove(BYTE nID);
- static BYTE FindID(BYTE nID);
-
- #endif
其中用到的的const.h定义如下:
Code:
- #ifndef _CONST_H_
- #define _CONST_H_
- #include
-
- #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(l) ((WORD)(l))
- #define HIWORD(l) ((WORD)(((DWORD)(l) >> 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))
-
-
-
- #define TEST_BIT(b, offset) (1 & ((b) >> (offset)))
- #define SET_BIT(b, offset) ((b) |= (1 << (offset)))
- #define RESET_BIT(b, offset) ((b) &= (~(1 << (offset))))
-
-
-
-
-
- #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
下面是定时器的.c文件的具体实现:
实现中用到了一点数据结构中“队列”的概念
Code:
- #include "timerconfig.h"
- #include "chiptypedef.h"
- #include
- #include
-
-
-
- code const WORD TIMER0_INIT_VALUE = UINT_MAX - ((WORD)((float)OSC_FREQUENCY * 1.0f / 12 * 1000)) * TIMER0_BASE_INTERVAL;
-
-
- idata TIMERINFO TimerInfoArray[MAX_TIMER_EVENT_NUM] = {0};
- TIMERPROC g_pfnTimerFunc = NULL;
- BYTE g_nTimerInfoNum = 0;
-
-
- void InitTimer0()
- {
- TMOD |= T0_M0_;
- TH0 = HIBYTE(TIMER0_INIT_VALUE);
- TL0 = LOBYTE(TIMER0_INIT_VALUE);
- TR0 = 0;
- ET0 = 0;
- EA = 1;
- }
-
- BOOL SetTimerCallback(TIMERPROC lpTimerFunc)
- {
- if(lpTimerFunc == NULL)
- return FALSE;
-
-
- g_pfnTimerFunc = lpTimerFunc;
-
- return TRUE;
-
- }
-
- BOOL SetTimer0(BYTE nID, WORD wInterval)
- {
- TIMERINFO ti;
- if(g_pfnTimerFunc == NULL || nID == 0 || wInterval == 0)
- return FALSE;
-
- if(wInterval % TIMER0_BASE_INTERVAL != 0)
- return FALSE;
-
- if(FindID(nID) != MAX_TIMER_EVENT_NUM)
- return FALSE;
-
-
-
-
- ti.nID = nID;
- ti.wInterval = wInterval;
- ti.wElapse = wInterval;
-
- if(!AddTail(&ti))
- return FALSE;
-
- TR0 = 1;
- ET0 = 1;
-
- return TRUE;
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
- static BYTE FindID(BYTE nID)
- {
- BYTE i = 0;
- for(i = 0; i < MAX_TIMER_EVENT_NUM; i++)
- {
- if(TimerInfoArray[i].nID == nID)
- return i;
- }
-
- return MAX_TIMER_EVENT_NUM;
- }
-
- static BOOL AddTail(const TIMERINFO* pTimerInfo)
- {
- if(g_nTimerInfoNum == MAX_TIMER_EVENT_NUM || pTimerInfo == NULL)
- return FALSE;
-
-
- memcpy(&TimerInfoArray[g_nTimerInfoNum], pTimerInfo, sizeof(TIMERINFO));
- g_nTimerInfoNum++;
-
- return TRUE;
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- void Timer0ISR() interrupt TF0_VECTOR
- {
- BYTE i = 0;
- TF0 = 0;
-
- TH0 = HIBYTE(TIMER0_INIT_VALUE);
- TL0 = LOBYTE(TIMER0_INIT_VALUE);
-
- for(i = 0; i < g_nTimerInfoNum; i++)
- {
- TimerInfoArray[i].wElapse -= TIMER0_BASE_INTERVAL;
- if(TimerInfoArray[i].wElapse == 0)
- {
- (*g_pfnTimerFunc)(TimerInfoArray[i].nID);
- TimerInfoArray[i].wElapse = TimerInfoArray[i].wInterval;
- }
-
- }
- }
上面的代码可以直接使用,我在多个项目中已经用到,几乎每个项目都用到定时器,而且是多个任务。
稍微说一下这个定时器的使用:
1、设定晶振频率标识符OSC_FREQUENCY为所需,比如12MHz就设置为12
2、更改预 定义标识符的值MAX_TIMER_EVENT_NUM和TIMER0_BASE_INTERVAL 。注意MAX_TIMER_EVENT_NUM的值至少为2,TIMER0_BASE_INTERVAL 的值不能超过当前晶振频率下定时器0的最大溢出时间。如:12MHz下,定时器0的溢出时间为65.535ms,即 TIMER0_BASE_INTERVAL 的值不能超过65的整数
3、初始化,调用InitTimer0()
4、设定回调函数SetTimerCallback(TimerProc),TimerProc的原型为typedef void (*TIMERPROC)(BYTE nID),即参数是unsigned char,返回值为void的函数。
5、设定定时事件SetTimer0(),注意定时间隔必须是TIMER0_BASE_INTERVAL的整数倍
6、具体实现回调函数void TimerProc(BYTE nID)
这样每当一个定时事件触发后便会自动调用TimerProc函数,程序员具体的任务只需要在函数中实现定时器到时后需要处理的事情,通过判断nID来表明是哪个定时事件触发的当前定时事件
把定时器做成这样有什么好处呢?
1、体现了模块化的思想,达到了代码的复用目的,因为定时器几乎是每个单片机项目都需要用到的资源
2、屏蔽了定时器使用者需要了解定时器内部设定的细节,达到了一定的抽象,因为调用者只需要简单地设置几个预定义的标示符即可使用了,不需要了解定时器初始值的计算、定时器中断函数中初始值还需重新装载等很多琐碎容易出错的问题
3、可以设定多个定时任务,因为往往定时器的使用并非为了解决一个任务而设定的。如果用最原始的实现方法来完成多个定时任务,那么就需要很多标志位变量来区别不同的定时事件,大量的全局性的标志位变量势必会影响程序的结构,使各函数之间的耦合无形中增大了
但也有如下的不足:
1、为了完成各定时事件的调度,需要额外占用单片机的ram和rom资源,所以这个定时器不太适用仅有128字节的c51芯片,适合256字节以上的系列
2、 各个定时事件是依次调用的,这样会造成实时性和定时精度不佳,实测基本最小时间间隔基本10~20ms,当然这晶振频率和定时事件中处理的任务量有关系 了。如果需要更高的定时精度那只能:一、提高晶振频率,二、老老实实用最原始的定时器来实现,三、再不行就只能用汇编了
小程序可是有大学问的。举个例子吧,断码管用过吧?它的解法有两种:共阴极和共阳极。断码管有abcdefg七个端子接入,一般我们会按顺序abcdef 对应单片机某一端口的从低位到高位接。但你想过没有,若是正好把高低位顺序完全接反了怎么办(不要说不可能,我可碰到过)?再加上又可以共阴极和共阳极两 种选择。是否可以把这四种情况都统一到一起形成一个.h头文件供日后随意使用呢?
这其实应该是程序员的直觉,一种天生的惰性,把经常用到的东西一次性做好,供日后使用。
我是这么实现的,参见以下代码:
Code:
- #ifndef _LED_NUM_H_
- #define _LED_NUM_H_
- #include "const.h"
-
- typedef enum tagLEDNUM
- {
- LED_0,
- LED_1,
- LED_2,
- LED_3,
- LED_4,
- LED_5,
- LED_6,
- LED_7,
- LED_8,
- LED_9,
-
-
-
-
-
-
-
- LED_MINUS,
-
-
-
- LED_ALL_OFF,
- LED_ALL_ON,
-
-
-
- LED_TABLE_SIZE
- }LEDNUM;
-
-
- #if defined COMMON_CATHODE //共阴极
-
- #ifdef COMMON_CODE
-
- code BYTE g_LEDNumTable[LED_TABLE_SIZE] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x80, 0x00, 0xff};
-
- #elif defined REVERSE_CODE //反序字节
-
- code BYTE g_LEDNumTable[LED_TABLE_SIZE] = {0xfc, 0x60, 0xda, 0xf2, 0x66, 0xb6, 0xae, 0xe0, 0xfe, 0xf6, 0x01, 0x00, 0xff};
-
- #else
-
- #error must indicate COMMON_CODE or REVERSE_CODE identifier
-
- #endif
-
- #elif defined COMMON_ANTICATHODE //共阳极
-
- #ifdef COMMON_CODE
-
- code BYTE g_LEDNumTable[LED_TABLE_SIZE] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0x7f, 0xff, 0x00};
-
- #elif defined REVERSE_CODE //反序字节
-
- code BYTE g_LEDNumTable[LED_TABLE_SIZE] = {0x03, 0x9f, 0x25, 0x0d, 0x99, 0x49, 0x41, 0x1f, 0x01, 0x09, 0xfe, 0xff, 0x00};
-
- #else
-
- #error must indicate COMMON_CODE or REVERSE_CODE identifier
-
- #endif
-
-
-
- #else
-
- #error must indicate COMMON_CATHODE or COMMON_ANTICATHODE identifier
-
- #endif
-
-
-
-
-
- #define GET_LED_CODE(num) (g_LEDNumTable[num])
-
- #endif
这 样,以后使用的时候定义一下 COMMON_CATHODE 或COMMON_ANTICATHODE,REVERSE_CODE或COMMON_CODE 标识符就行了,这四个标识符完成了那四种的可能组合。定义过之后,直接用GET_LED_CODE()这个宏就可以取得数字所对应的断码了。比如你想在 P0口输出4,那么直接P0 = GET_LED_CODE(4)就可以了,就这么简单。
写成这样一个头文件有如下的好处:
1、达到了代码复用,只要用到段码管就可以直接用这个头文件了,不用每次都重写一遍
2、将运行期获得的参数转化到了编译期来完成,提高了运行速度,当然会占用一些rom
这个头文件里用到了一些可能不太多见的预编译宏,这可是c语言的一大特点,需要多熟悉一下,如果掌握了会大大提高功力。
其实,还有一个很重要的问题忘了提及。那就是------优化。一个逻辑性很差的程序往往优化也不会高到哪里去。也就是说,一个较差的程序用模块化等思想再加上优化以后,不见得就比以前的rom占用多、ram占用大、速度慢。举几个例子吧:
1、对数组清零,我们可以写一个循环,也可以用memset()函数,但是性能差异可就差多了,无论从ram、rom和运行速度都是没法比的。
2、我见过这样的代码:
假设a,b都是unsigned char类型
Code:
- if(a > 1 && a <= 10)
- {
- b = 0;
- }
- else if(a > 10 && a <= 20)
- {
- b = 1;
- }
- else if(a > 20 && a <= 30)
- {
- b = 2;
- }
- else if(...)
- .
- .
- .
这样的代码看着着实不爽,可以优化吗?当然可以,优化如下:
Code:
- #define RANGE_TABLE_SIZE 10 //假设是十个判断分支
- typedef unsigned char BYTE;
- typedef struct tagRANGE { BYTE nLower; BYTE nUpper; }RANGE;
- code const RANGE g_RangeTable[RANGE_TABLE_SIZE] = { {1, 10}, {10, 20}, {20, 30}, ... };
-
- BYTE i = 0;
- for(i = 0; i < RANGE_TABLE_SIZE; i++)
- {
- if(a > RangeTable[i].nLower && a <= RangeTable[i]. nUpper)
- {
- b = i;
- break;
- }
- }
我实际测试了一下,改版之前足足用了147个字节,而改版后的只是70个字节。相差竟然有一倍之多。改变前看似铺天盖地的代码,其实是非常简单的逻辑。用了表格驱动法后,无论从代码量、可读性和可扩展性来说无疑提高了非常多。