打印

【转帖】单片机C语言程序该这样写!不是教科书上教的那样

[复制链接]
4370|19
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
zhangxxyuan|  楼主 | 2010-6-6 10:19 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 zhangxxyuan 于 2010-6-6 10:22 编辑

转自CSDN社区,作者:jiqiang01234
原文地址:http://topic.csdn.net/u/20090722/15/6009c1d2-93bc-47dc-a646-70bc2968ac49.html?80322

写单片机程序也是程序,也要遵循写软件的一些基本原则,不是为了完成功能那么简单。我看过的所有的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基本可以接受,再小的不能保证。


/////////////////////////////////////////
头文件
//////////////////////////////////////////
#ifndef _TIMER_CONFIG_H_
#define _TIMER_CONFIG_H_
#include "const.h"
#include "oscfrequencydef.h"

#ifndef OSC_FREQUENCY
#error undefined OSC_FREQUENCY
#endif

//#warning must be used in AT89C52 or later version because of "idata"
#warning **********************************************************************************
#warning !! make sure MAX_TIMER_EVENT_NUM and TIMER0_BASE_INTERVAL has appropriate value!!  
#warning **********************************************************************************

/****************************************************************************
定时中断每TIMER0_BASE_INTERVAL毫秒产生一次,用户定义的中断时间必须是它的整数倍
****************************************************************************/
#define MAX_TIMER_EVENT_NUM 5 //可设置不同定时事件的最大个数(至少为2)
#define TIMER0_BASE_INTERVAL 20 //单位:毫秒




typedef void (*TIMERPROC)(BYTE nID);

void InitTimer0();
BOOL SetTimerCallback(TIMERPROC lpTimerFunc); //必须在SetTimer0之前调用
BOOL SetTimer0(BYTE nID, WORD wInterval); //通过nID(nID>0)来区分  
//BOOL KillTimer0(BYTE nID);





/////////////////////////////////////////////////
//以下为内部使用

typedef struct tagTIMERINFO
{
BYTE nID;         //定时器ID
WORD wInterval;         //此定时器的设定间隔时间
WORD wElapse;         //剩余的时间

}TIMERINFO;
static BOOL AddTail(const TIMERINFO* pTimerInfo);
static BOOL Remove(BYTE nID);
static BYTE FindID(BYTE nID);

#endif


其中用到的的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(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))))



//将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

相关帖子

沙发
zhangxxyuan|  楼主 | 2010-6-6 10:20 | 只看该作者
下面是定时器的.c文件的具体实现:

实现中用到了一点数据结构中“队列”的概念

#include "timerconfig.h"
#include "chiptypedef.h"
#include <limits.h>
#include <string.h>



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_; //定时器0,工作方式1
TH0 = HIBYTE(TIMER0_INIT_VALUE);
TL0 = LOBYTE(TIMER0_INIT_VALUE);
TR0 = 0;         //停止定时器0
ET0 = 0;         //关定时器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) //定时间隔必须是TIMER0_BASE_INTERVAL的整数倍
return FALSE;

if(FindID(nID) != MAX_TIMER_EVENT_NUM)        //若已经有相同的ID存在
return FALSE;




ti.nID = nID;
ti.wInterval = wInterval;
ti.wElapse = wInterval;

if(!AddTail(&ti))
return FALSE;

TR0 = 1;         //启动定时器0
ET0 = 1;         //开定时器0中断       

return TRUE;

}
  /*
BOOL KillTimer0(BYTE nID)
{

if(!Remove(nID) || nID == 0)
return FALSE;

if(g_nTimerInfoNum == 0) //若最后一个定时事件已经停止,则关定时器中断
ET0 = 0;

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;       
}

/*
static BOOL Remove(BYTE nID)
{
BYTE nIndex = FindID(nID);
BYTE nRest = g_nTimerInfoNum - nIndex - 1;

if(nIndex == MAX_TIMER_EVENT_NUM || nID == 0)
return FALSE;       

if(nRest == 0) //已经是队列尾元素
{
memset(&TimerInfoArray[nIndex], 0, sizeof(TIMERINFO));       
}
else
{
//删除后,前移
memcpy(&TimerInfoArray[nIndex], &TimerInfoArray[nIndex + 1], sizeof(TIMERINFO) * nRest);
memset(&TimerInfoArray[nIndex + nRest], 0, sizeof(TIMERINFO) * (MAX_TIMER_EVENT_NUM - (nIndex + nRest) - 1));       
}

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;       
}

}
}

使用特权

评论回复
板凳
zhangxxyuan|  楼主 | 2010-6-6 10:20 | 只看该作者
这个定时器的使用:

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,当然这晶振频率和定时事件中处理的任务量有关系了。如果需要更高的定时精度那只能:一、提高晶振频率,二、老老实实用最原始的定时器来实现,三、再不行就只能用汇编了

使用特权

评论回复
地板
zhangxxyuan|  楼主 | 2010-6-6 10:21 | 只看该作者
举个例子吧,断码管用过吧?它的解法有两种:共阴极和共阳极。断码管有abcdefg七个端子接入,一般我们会按顺序abcdef对应单片机某一端口的从低位到高位接。但你想过没有,若是正好把高低位顺序完全接反了怎么办(不要说不可能,我可碰到过)?再加上又可以共阴极和共阳极两种选择。是否可以把这四种情况都统一到一起形成一个.h头文件供日后随意使用呢?
这其实应该是程序员的直觉,一种天生的惰性,把经常用到的东西一次性做好,供日后使用。
我是这么实现的,参见以下代码:[code=C/C++][/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_A,
LED_B,
LED_C,
LED_D,
LED_E,
LED_F,*/

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语言的一大特点,需要多熟悉一下,如果掌握了会大大提高功力。

使用特权

评论回复
5
一棵小草| | 2010-6-6 12:24 | 只看该作者
赞成jiqiang01234的思想,我也有这样的共性。

不过我还没有那么的彻底,总感觉复杂化了些,同时也不是每个人都能这样写的,我只想着怎么简单化。

使用特权

评论回复
6
zhangxxyuan|  楼主 | 2010-6-6 13:12 | 只看该作者
5# 一棵小草 那些只是达到一定程度才能写出来的,写出来的不多

使用特权

评论回复
7
NE5532| | 2010-6-6 15:42 | 只看该作者
岔一句,程序好在架构规划上,而不是好在语句上,当然细节决定成败。

使用特权

评论回复
8
k9999| | 2010-6-6 19:18 | 只看该作者
看完此帖我把我的单片机的书全扔了

使用特权

评论回复
9
一棵小草| | 2010-6-6 19:38 | 只看该作者
5# 一棵小草 那些只是达到一定程度才能写出来的,写出来的不多
zhangxxyuan 发表于 2010-6-6 13:12


恩,我看出来了,能写出这样的框架,说明功底很好

感觉有OS和C++的思想吧,我自己就写不出来

使用特权

评论回复
10
一棵小草| | 2010-6-6 19:39 | 只看该作者
岔一句,程序好在架构规划上,而不是好在语句上,当然细节决定成败。
NE5532 发表于 2010-6-6 15:42


嘿嘿,很有同感,我一向主张细心细心再细心的

现在对程序结构很感兴趣了,正找书看。。。

使用特权

评论回复
11
tigedtp| | 2010-6-6 21:37 | 只看该作者
教科书是教人入门的。郭靖学完了降龙十八掌,再去鄙视江南七怪,合适吗?

使用特权

评论回复
12
lhj200304| | 2010-6-6 21:41 | 只看该作者
3、函数定义几乎不用参数,全都是void
对于这条有异议

使用特权

评论回复
13
zhf0964| | 2010-6-7 10:53 | 只看该作者
能写出这样程序的一定是高手。

使用特权

评论回复
14
Wxy8030| | 2010-6-7 10:53 | 只看该作者
教科书确实都是教人入门的,它是让你能把C用起来,而不是用好——至于用好,那就是“师傅引进门,修行在个人了”!

郭靖学完了降龙十八掌,还一样很尊敬江南七怪的!

使用特权

评论回复
15
wswh2o| | 2010-6-7 10:59 | 只看该作者
从武术角度看,郭靖当然是鄙视江南奇怪的,尊敬不是因为武术是因为别的

使用特权

评论回复
16
Wxy8030| | 2010-6-7 11:01 | 只看该作者
不同意楼上的看法!

不是江南七怪,郭靖能直接学降龙十八掌吗?

使用特权

评论回复
17
edward8421| | 2010-6-7 11:07 | 只看该作者
一步一步来么
先用起来再说

使用特权

评论回复
18
zhangxxyuan|  楼主 | 2010-6-7 18:36 | 只看该作者
12# lhj200304 有异议是因为一般的单片机程序都很简单,如果碰到复杂的那些可能就必须得加有返回值的函数了

使用特权

评论回复
19
ytfdhb| | 2010-6-7 19:02 | 只看该作者
高手,学习了……

使用特权

评论回复
20
mohanwei| | 2010-6-7 19:30 | 只看该作者
资源多了当然怎么挥霍都可以……
你叫他玩玩Flash/ROM只有一两K的单片机看看,随便调用库函数算一下float,long,可能600字节的程序空间就没了;再printf一下,又没了1K

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

0

主题

15

帖子

1

粉丝