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

时间:2021-09-27 19:49:53
写单片机程序也是程序,也要遵循写软件的一些基本原则,不是为了完成功能那么简单。我看过的所有的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、不思考曾经做过的程序是否还有改进的余地,写程序如果只是为了写而写,一辈子也长进不了多少

欢迎拍砖!

1132 个解决方案

#1


为了证明我以上的观点,特此发一下我对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

#2


下面是定时器的.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;
}

}
}

#3


上面的代码可以直接使用,我在多个项目中已经用到,几乎每个项目都用到定时器,而且是多个任务。如果哪位大侠还有更好的实现方法,请拿出来一起分享

#4


代码没看,不过上边说的那几条还是有道理的

#5


学习了。我也要建立自己的函数库~~嘻嘻!

#6


多谢LZ......学习中.........

#7


xuexi `

#8


好东西,学习了

#9


代码没看。但观点是正确的。

#10


数据结构...
晕倒...

#11


确实很不错,谢谢LZ

#12


稍微说一下这个定时器的使用:

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


#13


你这个代码有RTOS的思想在,但是实时性可能差点,但是cpu利用率高

#14


引用 13 楼 embedarmwince 的回复:
你这个代码有RTOS的思想在,但是实时性可能差点,但是cpu利用率高


最近一直在用stc的片子,指令周期较之普通的51系列快了平均8倍,用起来很爽。12MHz的晶振上定时器最小定时间隔10ms没问题

#15


引用 5 楼 rejoice818 的回复:
学习了。我也要建立自己的函数库~~嘻嘻!


我想更主要的是学习怎样做出通用性、易用性和可读性好的库函数,这可并非易事

#16


你的想法我很赞同,我是个新手,学了好长时间的 C单片机编程 我也感到了这一点,我主要做一些小的实验的程序

#17


小程序可是有大学问的。举个例子吧,断码管用过吧?它的解法有两种:共阴极和共阳极。断码管有abcdefg七个端子接入,一般我们会按顺序abcdef对应单片机某一端口的从低位到高位接。但你想过没有,若是正好把高低位顺序完全接反了怎么办(不要说不可能,我可碰到过)?再加上又可以共阴极和共阳极两种选择。是否可以把这四种情况都统一到一起形成一个.h头文件供日后随意使用呢?
这其实应该是程序员的直觉,一种天生的惰性,把经常用到的东西一次性做好,供日后使用。
我是这么实现的,参见以下代码:
 

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

#18


之前,学软件的不屑于写单片机上的程序

写单片机上的程序的工程师多都是做硬件出身

他们大多没有好的软件工程思想

像全局变量全局定义啥的那是很正常的事

呵呵,更别提代码重用了

再之一个,做硬件的思想向来也比较直接些,以实现为第一

所以这样的局面很正常

#19


另外我也添一个个人看法


最好把C和ASM那种“低级趣味”隔离开来

#20


不懂楼上的意思。能不能具体解释下“低级趣味”?
另强烈支持楼主前面的观点,做硬件的思想严重面对实现,对于封装理解太少。

#21


引用 18 楼 lbing7 的回复:
之前,学软件的不屑于写单片机上的程序

写单片机上的程序的工程师多都是做硬件出身

他们大多没有好的软件工程思想

像全局变量全局定义啥的那是很正常的事

呵呵,更别提代码重用了

再之一个,做硬件的思想向来也比较直接些,以实现为第一

所以这样的局面很正常


我算是一个另类吧,我的生活来源工作是些VC程序,业余写单片机程序。所以对国内单片机程序现状很是担忧,尤其是所谓的“教科书”根本就是在误人子弟。

#22


其实还有一个要考虑的问题

1K的ROM/RAM 通常来说要比2K的便宜

在PC层次的开发,大手大脚地用资源,一点都不心疼

但是,单片机层次的开发,产品以量的形态向外推

这样,硬件上省一块钱,一万台,一百万台

其节省的成本就不是一个小数目

#23


软件上的可维护,可重用,可读

这些东西基本上都是以损害资源为代价

在某些时候其实是不可取的

呵呵

#24


引用 20 楼 wangbinds 的回复:
不懂楼上的意思。能不能具体解释下“低级趣味”?
另强烈支持楼主前面的观点,做硬件的思想严重面对实现,对于封装理解太少。

总感觉纯做单片机的人更注重硬件相对可见性高的实现细节而往往忽视了软件这种比较抽象的东西。而程序其实往往是一个产品成功的关键。
我想还是视野开阔的问题,做单片机程序的视野往往停留在那些“教科书”或网上的那些经过“教科书”熏陶的代码


#25


引用 23 楼 lbing7 的回复:
软件上的可维护,可重用,可读

这些东西基本上都是以损害资源为代价

在某些时候其实是不可取的

呵呵


任何思想脱离了实际是没意义的。当前讨论的主题当然是在资源、性能、价格等多方因素下的一种折中。
在对极端苛刻的条件下当然得对很多东西进行妥协

#26


工程师不就是对各种设计进行权衡吗?

呵呵

#27


其实,还有一个很重要的问题忘了提及。那就是------优化。一个逻辑性很差的程序往往优化也不会高到哪里去。也就是说,一个较差的程序用模块化等思想再加上优化以后,不见得就比以前的rom占用多、ram占用大、速度慢。举几个例子吧:
1、对数组清零,我们可以写一个循环,也可以用memset()函数,但是性能差异可就差多了,无论从ram、rom和运行速度都是没法比的。

2、我见过这样的代码:

假设a,b都是unsigned char类型

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(...)//N多个分支来判断
   .
   .
   .

这样的代码看着着实不爽,可以优化吗?当然可以,优化如下:

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


我实际测试了一下,改版之前足足用了147个字节,而改版后的只是70个字节。相差竟然有一倍之多。改变前看似铺天盖地的代码,其实是非常简单的逻辑。用了表格驱动法后,无论从代码量、可读性和可扩展性来说无疑提高了非常多。

#28


本帖最后由 lbing7 于 2009-07-25 20:40:38 编辑
和LZ说一下,表驱动和IF-ELSE和SWITCH-CASE方式无关

这个个人觉得仅仅是技巧问题

呵呵

而且表驱动用在单纯的分支处理上,不管是从可读性还是维护性都不一定比IF-ELSE

当然,表驱动状态机在一般情况下是比分支语句处理的要漂亮些

#29


引用 28 楼 lbing7 的回复:
和LZ说一下,表驱动和IF-ELSE和SWITCH-CASE方式无关

这个个人觉得仅仅是技巧问题

呵呵


表驱动其实是面向对象里“多态”特性实现的基石,如果面向对象的多态特性都可以说是“仅仅是技巧”的话,我无话可说。
表驱动和IF-ELSE和SWITCH-CASE方式确实可以说无关,毕竟写程序依然可以各走各的路,可以不产生一丝一毫的联系。但,如果它们之间可以组合出更高效的行为,为什么不去用呢?
优化是一个很广泛的话题,有多方因素需要考虑,我们为什么要去限制自己的思路呢?

#30


引用 28 楼 lbing7 的回复:
而且表驱动用在单纯的分支处理上,不管是从可读性还是维护性都不一定比IF-ELSE

我举的那个用表驱动的例子是有些特性的,比如有明显的判断关系,很有规律可循。我仅仅使用这个比较有代表性的例子来说明一下表驱动的好处。可并没有想把所有if-else及switch-case语句全部用表驱动来代替。存在就是合理的,既然c语言有这么多关键字,那一定就有适用它们使用的地方,不可极端处理,即使是令人诟病的goto语句。

#31


hao好 我下学期就学了 我回好好看看的 

#32


飘过~~~~~

#33


个人觉得面向对象的设计思想在实时性上比较差。

#34


#35


lbing7说得有道理,很多学单片机的本来都是搞硬件的,可能连C都不大会,怎么可能去让他去搞代码。单片机程序最重要的是稳定性,即使代码看上去好看了,调试起来更麻烦也会令人头疼。

#36


引用 33 楼 goodboy2012 的回复:
个人觉得面向对象的设计思想在实时性上比较差。

面向对象包括三方面的内容:封装、继承和多态。而我提倡的单片机c语言编程是鼓励“封装”,使之更具模块化。其实继承和多态也并非就是“实时性”差,只是相对于单片机资源来说比较费ram和rom。毕竟单片机还很少有支持c++的,51系列的我是闻所未闻。

从面向对象思想出现的历史中我们也可以看到,是因为当程序规模超过10万行以上时,面向过程的方法就有些难于应对了,这样才慢慢出现了面向对象的设计方法。要注意的一点是,面向对象的设计方法更适用于“规模大”的程序,而单片机程序就算用64k的rom也就是一万多行。任何技术都有它的适用范围,切不可乱用。

#37


引用 35 楼 goodboy2012 的回复:
lbing7说得有道理,很多学单片机的本来都是搞硬件的,可能连C都不大会,怎么可能去让他去搞代码单片机程序最重要的是稳定性,即使代码看上去好看了,调试起来更麻烦也会令人头疼。


我想或许有些人对我的观点理解有失偏颇,我在这里明确一下:
1、写程序第一是给人看的,然后才是给机器看的。
2、写程序稳定性、正确性是第相对于运行速度是第一位的
3、在满足稳定性、正确性、芯片性价比等多方面制约因素的前提下,程序的复用性、可扩展性、可读性必须要考虑。难道所有程序写一次以后再也不需要修改,再也不需要别人来维护?

“好代码”是满足多方要求的代码(就如我上面第三点中提到的那些要求)可并非仅仅是“看上去好”,我想goodboy2012的理解有些不妥。

既然已经自己都很明白是从硬件出身,写程序是弱项,为什么不迎头赶上?却把弱项当理由?(别告诉我时间不够用,没时间学习)

#38


好东西,学习下

#39


引用 36 楼 jiqiang01234 的回复:
引用 33 楼 goodboy2012 的回复:
个人觉得面向对象的设计思想在实时性上比较差。

面向对象包括三方面的内容:封装、继承和多态。而我提倡的单片机c语言编程是鼓励“封装”,使之更具模块化。其实继承和多态也并非就是“实时性”差,只是相对于单片机资源来说比较费ram和rom。毕竟单片机还很少有支持c++的,51系列的我是闻所未闻。

从面向对象思想出现的历史中我们也可以看到,是因为当程序规模超过10万行以上时,面向过程的方法就有些难于应对了,这样才慢慢出现了面向对象的设计方法。要注意的一点是,面向对象的设计方法更适用于“规模大”的程序,而单片机程序就算用64k的rom也就是一万多行。任何技术都有它的适用范围,切不可乱用。


LZ你有点小看编译器了,我们以前写协议的时候

四五W行代码才二十多K,嘿嘿,64K还是相当大的空间的

不信你看一下当前流行的单片机,没有多少是有64K的ROM的

另外,提一个:不是单片机支持语言

面是IDE和相关的平台支持

因为,不论什么机器,至少当前,机器只认0和1

不论什么语言最终到机器层面都是机器指令

语言层次的提高带来了对程序员来说更加友好的接口

让他们可以从更高的层次去看机器

ASM就不说了

C带来了函数,函数的调用会在栈上产生消耗

后面的面向对象语言的特性带来的是什么?

为了支持对象,让机器多去处理怎么去调用哪个函数

而这个是不可预知的

如果是一个对时间要求严格的系统,这将是灾难性的

其实,前些天一些大哥就在坛子里说KEIL已经支持CPP

至于别的支持面向对象的平台,你可以看一下IAR,在430上我试过丫的CPP(仅是试过)

相信丫8051上也有

对于代码量,不知道LZ是不是真的敲过上W行的代码(不在IDE的帮助下)

当然,我支持用面向对象的方法来规划设计系统

但是,真正编码的时候,只要架构上清晰。

一个只有一年多经验的工程师也是不会出现所谓混乱

之前看软工的书的时候,印象我也见到过说10W行代码可怕

不过实践告诉我,这个分水岭并不是绝对的。

#40


引用 37 楼 jiqiang01234 的回复:
引用 35 楼 goodboy2012 的回复:
lbing7说得有道理,很多学单片机的本来都是搞硬件的,可能连C都不大会,怎么可能去让他去搞代码单片机程序最重要的是稳定性,即使代码看上去好看了,调试起来更麻烦也会令人头疼。

我想或许有些人对我的观点理解有失偏颇,我在这里明确一下:
1、写程序第一是给人看的,然后才是给机器看的。
2、写程序稳定性、正确性是第相对于运行速度是第一位的
3、在满足稳定性、正确性、芯片性价比等多方面制约因素的前提下,程序的复用性、可扩展性、可读性必须要考虑。难道所有程序写一次以后再也不需要修改,再也不需要别人来维护?

“好代码”是满足多方要求的代码(就如我上面第三点中提到的那些要求)可并非仅仅是“看上去好”,我想goodboy2012的理解有些不妥。

既然已经自己都很明白是从硬件出身,写程序是弱项,为什么不迎头赶上?却把弱项当理由?(别告诉我时间不够用,没时间学习)


写程序第一要是对的!!

LZ一个不正确的程序是没有任何的其它意义

呵呵

#41


赞同lz的思想
不过关于代码行多于10W面向过程难于应付还是设计管理的问题,
linux内核里面没有C++的代码吧

另外lZ的一个bug

BYTE i = 0;
for(i = 0; i < RANGE_TABLE_SIZE; i++)
{
    if(a > RangeTable[i].nLower && a <= RangeTable[i]. nUpper)
    {
        b = i;
        break;//少了这个,呵呵
    }
}


#42


总结:菜鸟要求正确性,稳定性。高手在兼顾的同时要求有更好的可读性和封装性,还有优化代码的效率

#43


写得真是好啊

#44


引用 39 楼 lbing7 的回复:
引用 36 楼 jiqiang01234 的回复:
引用 33 楼 goodboy2012 的回复:
个人觉得面向对象的设计思想在实时性上比较差。

面向对象包括三方面的内容:封装、继承和多态。而我提倡的单片机c语言编程是鼓励“封装”,使之更具模块化。其实继承和多态也并非就是“实时性”差,只是相对于单片机资源来说比较费ram和rom。毕竟单片机还很少有支持c++的,51系列的我是闻所未闻。

从面向对象思想出现的历史中我们也可以看到,是因为当程序规模超过10万行以上时,面向过程的方法就有些难于应对了,这样才慢慢出现了面向对象的设计方法。要注意的一点是,面向对象的设计方法更适用于“规模大”的程序,而单片机程序就算用64k的rom也就是一万多行。任何技术都有它的适用范围,切不可乱用。


LZ你有点小看编译器了,我们以前写协议的时候

四五W行代码才二十多K,嘿嘿,64K还是相当大的空间的

不信你看一下当前流行的单片机,没有多少是有64K的ROM的

另外,提一个:不是单片机支持语言

面是IDE和相关的平台支持

因为,不论什么机器,至少当前,机器只认0和1

不论什么语言最终到机器层面都是机器指令

语言层次的提高带来了对程序员来说更加友好的接口

让他们可以从更高的层次去看机器

ASM就不说了

C带来了函数,函数的调用会在栈上产生消耗

后面的面向对象语言的特性带来的是什么?

为了支持对象,让机器多去处理怎么去调用哪个函数

而这个是不可预知的

如果是一个对时间要求严格的系统,这将是灾难性的

其实,前些天一些大哥就在坛子里说KEIL已经支持CPP

至于别的支持面向对象的平台,你可以看一下IAR,在430上我试过丫的CPP(仅是试过)

相信丫8051上也有

对于代码量,不知道LZ是不是真的敲过上W行的代码(不在IDE的帮助下)

当然,我支持用面向对象的方法来规划设计系统

但是,真正编码的时候,只要架构上清晰。

一个只有一年多经验的工程师也是不会出现所谓混乱

之前看软工的书的时候,印象我也见到过说10W行代码可怕

不过实践告诉我,这个分水岭并不是绝对的。

"另外,提一个:不是单片机支持语言

面是IDE和相关的平台支持

因为,不论什么机器,至少当前,机器只认0和1"
没错,是我说的不严谨

我所主张的所谓C语言的“封装”,可不是要写一个c++类那种形式的封装,而是封装于模块之间,说白了就是模块化思想。我怎么感觉lbing7似乎理解成了我在鼓吹用c++来写单片机程序。我可从来没有过这种主张啊。我的最后一句话写的很明确“任何技术都有它的适用范围,切不可乱用。 ”我从来都是强调在适合的环境用适合的武器。

“对于代码量,不知道LZ是不是真的敲过上W行的代码(不在IDE的帮助下)”
我不太理解敲代码和用不用IDE有什么排斥的关系。keil我感觉无非是一个带语法加亮功能的编辑器而已,这和敲代码有什么冲突吗?keil又不像VC可以用ClassWizard生成一部分的代码,如果只是写c程序的话顶多自动生成一些单片机的启动代码。当然,keil集成了编译的过程。但和编辑代码好像没什么排斥关系吧?

我承认用keil确实没写过上万行的代码,但用vc应该还是没问题的,纯手工敲不包括编译器自动生成的。

#45


好的单片机程序一般是软硬件分离.

硬件关联性太强的,通用性都不怎么样.

同一源代码能够方便编译出不同硬件条件的程序才是不错的.

没写过汇编的单片机程序员写出来的c语言的效率一般都不怎么样.
一般 for(){} 满天飞的程序我是不看的.

#46


引用 40 楼 lbing7 的回复:
写程序第一要是对的!!

LZ一个不正确的程序是没有任何的其它意义

呵呵

我想这恐怕就是写单片机程序和写pc程序人之间最大的区别了。我们所谓程序的“对”或“正确性”其实都是相对的,描述为“在运行至今还没有发现问题的程序”更客观一些。谁都无法保证程序今天对那么明天一定也对。恰恰问题就出在这里,若是明天程序出了错误,而当时写程序是本着“写对是第一位”的思想,那么这个程序的可读性以及各种衡量标准那就可想而知了。在没有可读性等保障性标准作为前提,找一个程序的bug谈何容易?

#47


引用 41 楼 conry 的回复:
赞同lz的思想
不过关于代码行多于10W面向过程难于应付还是设计管理的问题,
linux内核里面没有C++的代码吧

另外lZ的一个bug
C/C++ code
BYTE i=0;for(i=0; i< RANGE_TABLE_SIZE; i++)
{if(a> RangeTable[i].nLower&& a<= RangeTable[i]. nUpper)
    {
        b= i;break;//少了这个,呵呵    }
}

没错,谢谢提醒。




#48


引用 45 楼 knate 的回复:
好的单片机程序一般是软硬件分离.

硬件关联性太强的,通用性都不怎么样.

同一源代码能够方便编译出不同硬件条件的程序才是不错的.

没写过汇编的单片机程序员写出来的c语言的效率一般都不怎么样.
一般 for(){} 满天飞的程序我是不看的.

很赞成前三那句话,至于最后两句有点不敢苟同。
固然学完汇编再学c有对效率天生的敏感?但既然选择了用c来写程序,那就不得不去考虑效率的问题,毕竟c是除了汇编基本上可以说是第二有效率的语言了。这和学没学过汇编似乎没什么必然联系。如果用c语言写程序却对效率漠不关心,那只能说这个c程序员不称职。当然,学了汇编之后对c语言的理解会更深入,这毋庸置疑。

我的主张是,学习c语言必须了解其语句的效率,这倒不一定要求一定要能用汇编写程序,但起码得能大概看懂。比如:
为什么51系列单片机主张用单字节做变量进行运算?为什么宏要比函数占用更多的rom,却速度可以更快?为什么函数名就是函数的入口地址?为什么会有函数重入性问题?为什么中断函数没有返回值没有参数?等等

当把这些底层的东西都了解了,那么写出来的c程序效率不回比汇编低多少的,就算是低一些但是由于c的开发效率较之汇编高,也会补偿回来的。

#49


引用 46 楼 jiqiang01234 的回复:
引用 40 楼 lbing7 的回复:

写程序第一要是对的!!

LZ一个不正确的程序是没有任何的其它意义

呵呵


我想这恐怕就是写单片机程序和写pc程序人之间最大的区别了。我们所谓程序的“对”或“正确性”其实都是相对的,描述为“在运行至今还没有发现问题的程序”更客观一些。谁都无法保证程序今天对那么明天一定也对。恰恰问题就出在这里,若是明天程序出了错误,而当时写程序是本着“写对是第一位”的思想,那么这个程序的可读性以及各种衡量标准那就可想而知了。在没有可读性等保障性标准作为前提,找一个程序的bug谈何容易?

同意楼主这个,可读性是一定要保证的。有可读性的不正确以后有机会变成正确,没有可读性的正确只能说是偶然。

#50


路过

#1


为了证明我以上的观点,特此发一下我对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

#2


下面是定时器的.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;
}

}
}

#3


上面的代码可以直接使用,我在多个项目中已经用到,几乎每个项目都用到定时器,而且是多个任务。如果哪位大侠还有更好的实现方法,请拿出来一起分享

#4


代码没看,不过上边说的那几条还是有道理的

#5


学习了。我也要建立自己的函数库~~嘻嘻!

#6


多谢LZ......学习中.........

#7


xuexi `

#8


好东西,学习了

#9


代码没看。但观点是正确的。

#10


数据结构...
晕倒...

#11


确实很不错,谢谢LZ

#12


稍微说一下这个定时器的使用:

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


#13


你这个代码有RTOS的思想在,但是实时性可能差点,但是cpu利用率高

#14


引用 13 楼 embedarmwince 的回复:
你这个代码有RTOS的思想在,但是实时性可能差点,但是cpu利用率高


最近一直在用stc的片子,指令周期较之普通的51系列快了平均8倍,用起来很爽。12MHz的晶振上定时器最小定时间隔10ms没问题

#15


引用 5 楼 rejoice818 的回复:
学习了。我也要建立自己的函数库~~嘻嘻!


我想更主要的是学习怎样做出通用性、易用性和可读性好的库函数,这可并非易事

#16


你的想法我很赞同,我是个新手,学了好长时间的 C单片机编程 我也感到了这一点,我主要做一些小的实验的程序

#17


小程序可是有大学问的。举个例子吧,断码管用过吧?它的解法有两种:共阴极和共阳极。断码管有abcdefg七个端子接入,一般我们会按顺序abcdef对应单片机某一端口的从低位到高位接。但你想过没有,若是正好把高低位顺序完全接反了怎么办(不要说不可能,我可碰到过)?再加上又可以共阴极和共阳极两种选择。是否可以把这四种情况都统一到一起形成一个.h头文件供日后随意使用呢?
这其实应该是程序员的直觉,一种天生的惰性,把经常用到的东西一次性做好,供日后使用。
我是这么实现的,参见以下代码:
 

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

#18


之前,学软件的不屑于写单片机上的程序

写单片机上的程序的工程师多都是做硬件出身

他们大多没有好的软件工程思想

像全局变量全局定义啥的那是很正常的事

呵呵,更别提代码重用了

再之一个,做硬件的思想向来也比较直接些,以实现为第一

所以这样的局面很正常

#19


另外我也添一个个人看法


最好把C和ASM那种“低级趣味”隔离开来

#20


不懂楼上的意思。能不能具体解释下“低级趣味”?
另强烈支持楼主前面的观点,做硬件的思想严重面对实现,对于封装理解太少。

#21


引用 18 楼 lbing7 的回复:
之前,学软件的不屑于写单片机上的程序

写单片机上的程序的工程师多都是做硬件出身

他们大多没有好的软件工程思想

像全局变量全局定义啥的那是很正常的事

呵呵,更别提代码重用了

再之一个,做硬件的思想向来也比较直接些,以实现为第一

所以这样的局面很正常


我算是一个另类吧,我的生活来源工作是些VC程序,业余写单片机程序。所以对国内单片机程序现状很是担忧,尤其是所谓的“教科书”根本就是在误人子弟。

#22


其实还有一个要考虑的问题

1K的ROM/RAM 通常来说要比2K的便宜

在PC层次的开发,大手大脚地用资源,一点都不心疼

但是,单片机层次的开发,产品以量的形态向外推

这样,硬件上省一块钱,一万台,一百万台

其节省的成本就不是一个小数目

#23


软件上的可维护,可重用,可读

这些东西基本上都是以损害资源为代价

在某些时候其实是不可取的

呵呵

#24


引用 20 楼 wangbinds 的回复:
不懂楼上的意思。能不能具体解释下“低级趣味”?
另强烈支持楼主前面的观点,做硬件的思想严重面对实现,对于封装理解太少。

总感觉纯做单片机的人更注重硬件相对可见性高的实现细节而往往忽视了软件这种比较抽象的东西。而程序其实往往是一个产品成功的关键。
我想还是视野开阔的问题,做单片机程序的视野往往停留在那些“教科书”或网上的那些经过“教科书”熏陶的代码


#25


引用 23 楼 lbing7 的回复:
软件上的可维护,可重用,可读

这些东西基本上都是以损害资源为代价

在某些时候其实是不可取的

呵呵


任何思想脱离了实际是没意义的。当前讨论的主题当然是在资源、性能、价格等多方因素下的一种折中。
在对极端苛刻的条件下当然得对很多东西进行妥协

#26


工程师不就是对各种设计进行权衡吗?

呵呵

#27


其实,还有一个很重要的问题忘了提及。那就是------优化。一个逻辑性很差的程序往往优化也不会高到哪里去。也就是说,一个较差的程序用模块化等思想再加上优化以后,不见得就比以前的rom占用多、ram占用大、速度慢。举几个例子吧:
1、对数组清零,我们可以写一个循环,也可以用memset()函数,但是性能差异可就差多了,无论从ram、rom和运行速度都是没法比的。

2、我见过这样的代码:

假设a,b都是unsigned char类型

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(...)//N多个分支来判断
   .
   .
   .

这样的代码看着着实不爽,可以优化吗?当然可以,优化如下:

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


我实际测试了一下,改版之前足足用了147个字节,而改版后的只是70个字节。相差竟然有一倍之多。改变前看似铺天盖地的代码,其实是非常简单的逻辑。用了表格驱动法后,无论从代码量、可读性和可扩展性来说无疑提高了非常多。

#28


本帖最后由 lbing7 于 2009-07-25 20:40:38 编辑
和LZ说一下,表驱动和IF-ELSE和SWITCH-CASE方式无关

这个个人觉得仅仅是技巧问题

呵呵

而且表驱动用在单纯的分支处理上,不管是从可读性还是维护性都不一定比IF-ELSE

当然,表驱动状态机在一般情况下是比分支语句处理的要漂亮些

#29


引用 28 楼 lbing7 的回复:
和LZ说一下,表驱动和IF-ELSE和SWITCH-CASE方式无关

这个个人觉得仅仅是技巧问题

呵呵


表驱动其实是面向对象里“多态”特性实现的基石,如果面向对象的多态特性都可以说是“仅仅是技巧”的话,我无话可说。
表驱动和IF-ELSE和SWITCH-CASE方式确实可以说无关,毕竟写程序依然可以各走各的路,可以不产生一丝一毫的联系。但,如果它们之间可以组合出更高效的行为,为什么不去用呢?
优化是一个很广泛的话题,有多方因素需要考虑,我们为什么要去限制自己的思路呢?

#30


引用 28 楼 lbing7 的回复:
而且表驱动用在单纯的分支处理上,不管是从可读性还是维护性都不一定比IF-ELSE

我举的那个用表驱动的例子是有些特性的,比如有明显的判断关系,很有规律可循。我仅仅使用这个比较有代表性的例子来说明一下表驱动的好处。可并没有想把所有if-else及switch-case语句全部用表驱动来代替。存在就是合理的,既然c语言有这么多关键字,那一定就有适用它们使用的地方,不可极端处理,即使是令人诟病的goto语句。

#31


hao好 我下学期就学了 我回好好看看的 

#32


飘过~~~~~

#33


个人觉得面向对象的设计思想在实时性上比较差。

#34


#35


lbing7说得有道理,很多学单片机的本来都是搞硬件的,可能连C都不大会,怎么可能去让他去搞代码。单片机程序最重要的是稳定性,即使代码看上去好看了,调试起来更麻烦也会令人头疼。

#36


引用 33 楼 goodboy2012 的回复:
个人觉得面向对象的设计思想在实时性上比较差。

面向对象包括三方面的内容:封装、继承和多态。而我提倡的单片机c语言编程是鼓励“封装”,使之更具模块化。其实继承和多态也并非就是“实时性”差,只是相对于单片机资源来说比较费ram和rom。毕竟单片机还很少有支持c++的,51系列的我是闻所未闻。

从面向对象思想出现的历史中我们也可以看到,是因为当程序规模超过10万行以上时,面向过程的方法就有些难于应对了,这样才慢慢出现了面向对象的设计方法。要注意的一点是,面向对象的设计方法更适用于“规模大”的程序,而单片机程序就算用64k的rom也就是一万多行。任何技术都有它的适用范围,切不可乱用。

#37


引用 35 楼 goodboy2012 的回复:
lbing7说得有道理,很多学单片机的本来都是搞硬件的,可能连C都不大会,怎么可能去让他去搞代码单片机程序最重要的是稳定性,即使代码看上去好看了,调试起来更麻烦也会令人头疼。


我想或许有些人对我的观点理解有失偏颇,我在这里明确一下:
1、写程序第一是给人看的,然后才是给机器看的。
2、写程序稳定性、正确性是第相对于运行速度是第一位的
3、在满足稳定性、正确性、芯片性价比等多方面制约因素的前提下,程序的复用性、可扩展性、可读性必须要考虑。难道所有程序写一次以后再也不需要修改,再也不需要别人来维护?

“好代码”是满足多方要求的代码(就如我上面第三点中提到的那些要求)可并非仅仅是“看上去好”,我想goodboy2012的理解有些不妥。

既然已经自己都很明白是从硬件出身,写程序是弱项,为什么不迎头赶上?却把弱项当理由?(别告诉我时间不够用,没时间学习)

#38


好东西,学习下

#39


引用 36 楼 jiqiang01234 的回复:
引用 33 楼 goodboy2012 的回复:
个人觉得面向对象的设计思想在实时性上比较差。

面向对象包括三方面的内容:封装、继承和多态。而我提倡的单片机c语言编程是鼓励“封装”,使之更具模块化。其实继承和多态也并非就是“实时性”差,只是相对于单片机资源来说比较费ram和rom。毕竟单片机还很少有支持c++的,51系列的我是闻所未闻。

从面向对象思想出现的历史中我们也可以看到,是因为当程序规模超过10万行以上时,面向过程的方法就有些难于应对了,这样才慢慢出现了面向对象的设计方法。要注意的一点是,面向对象的设计方法更适用于“规模大”的程序,而单片机程序就算用64k的rom也就是一万多行。任何技术都有它的适用范围,切不可乱用。


LZ你有点小看编译器了,我们以前写协议的时候

四五W行代码才二十多K,嘿嘿,64K还是相当大的空间的

不信你看一下当前流行的单片机,没有多少是有64K的ROM的

另外,提一个:不是单片机支持语言

面是IDE和相关的平台支持

因为,不论什么机器,至少当前,机器只认0和1

不论什么语言最终到机器层面都是机器指令

语言层次的提高带来了对程序员来说更加友好的接口

让他们可以从更高的层次去看机器

ASM就不说了

C带来了函数,函数的调用会在栈上产生消耗

后面的面向对象语言的特性带来的是什么?

为了支持对象,让机器多去处理怎么去调用哪个函数

而这个是不可预知的

如果是一个对时间要求严格的系统,这将是灾难性的

其实,前些天一些大哥就在坛子里说KEIL已经支持CPP

至于别的支持面向对象的平台,你可以看一下IAR,在430上我试过丫的CPP(仅是试过)

相信丫8051上也有

对于代码量,不知道LZ是不是真的敲过上W行的代码(不在IDE的帮助下)

当然,我支持用面向对象的方法来规划设计系统

但是,真正编码的时候,只要架构上清晰。

一个只有一年多经验的工程师也是不会出现所谓混乱

之前看软工的书的时候,印象我也见到过说10W行代码可怕

不过实践告诉我,这个分水岭并不是绝对的。

#40


引用 37 楼 jiqiang01234 的回复:
引用 35 楼 goodboy2012 的回复:
lbing7说得有道理,很多学单片机的本来都是搞硬件的,可能连C都不大会,怎么可能去让他去搞代码单片机程序最重要的是稳定性,即使代码看上去好看了,调试起来更麻烦也会令人头疼。

我想或许有些人对我的观点理解有失偏颇,我在这里明确一下:
1、写程序第一是给人看的,然后才是给机器看的。
2、写程序稳定性、正确性是第相对于运行速度是第一位的
3、在满足稳定性、正确性、芯片性价比等多方面制约因素的前提下,程序的复用性、可扩展性、可读性必须要考虑。难道所有程序写一次以后再也不需要修改,再也不需要别人来维护?

“好代码”是满足多方要求的代码(就如我上面第三点中提到的那些要求)可并非仅仅是“看上去好”,我想goodboy2012的理解有些不妥。

既然已经自己都很明白是从硬件出身,写程序是弱项,为什么不迎头赶上?却把弱项当理由?(别告诉我时间不够用,没时间学习)


写程序第一要是对的!!

LZ一个不正确的程序是没有任何的其它意义

呵呵

#41


赞同lz的思想
不过关于代码行多于10W面向过程难于应付还是设计管理的问题,
linux内核里面没有C++的代码吧

另外lZ的一个bug

BYTE i = 0;
for(i = 0; i < RANGE_TABLE_SIZE; i++)
{
    if(a > RangeTable[i].nLower && a <= RangeTable[i]. nUpper)
    {
        b = i;
        break;//少了这个,呵呵
    }
}


#42


总结:菜鸟要求正确性,稳定性。高手在兼顾的同时要求有更好的可读性和封装性,还有优化代码的效率

#43


写得真是好啊

#44


引用 39 楼 lbing7 的回复:
引用 36 楼 jiqiang01234 的回复:
引用 33 楼 goodboy2012 的回复:
个人觉得面向对象的设计思想在实时性上比较差。

面向对象包括三方面的内容:封装、继承和多态。而我提倡的单片机c语言编程是鼓励“封装”,使之更具模块化。其实继承和多态也并非就是“实时性”差,只是相对于单片机资源来说比较费ram和rom。毕竟单片机还很少有支持c++的,51系列的我是闻所未闻。

从面向对象思想出现的历史中我们也可以看到,是因为当程序规模超过10万行以上时,面向过程的方法就有些难于应对了,这样才慢慢出现了面向对象的设计方法。要注意的一点是,面向对象的设计方法更适用于“规模大”的程序,而单片机程序就算用64k的rom也就是一万多行。任何技术都有它的适用范围,切不可乱用。


LZ你有点小看编译器了,我们以前写协议的时候

四五W行代码才二十多K,嘿嘿,64K还是相当大的空间的

不信你看一下当前流行的单片机,没有多少是有64K的ROM的

另外,提一个:不是单片机支持语言

面是IDE和相关的平台支持

因为,不论什么机器,至少当前,机器只认0和1

不论什么语言最终到机器层面都是机器指令

语言层次的提高带来了对程序员来说更加友好的接口

让他们可以从更高的层次去看机器

ASM就不说了

C带来了函数,函数的调用会在栈上产生消耗

后面的面向对象语言的特性带来的是什么?

为了支持对象,让机器多去处理怎么去调用哪个函数

而这个是不可预知的

如果是一个对时间要求严格的系统,这将是灾难性的

其实,前些天一些大哥就在坛子里说KEIL已经支持CPP

至于别的支持面向对象的平台,你可以看一下IAR,在430上我试过丫的CPP(仅是试过)

相信丫8051上也有

对于代码量,不知道LZ是不是真的敲过上W行的代码(不在IDE的帮助下)

当然,我支持用面向对象的方法来规划设计系统

但是,真正编码的时候,只要架构上清晰。

一个只有一年多经验的工程师也是不会出现所谓混乱

之前看软工的书的时候,印象我也见到过说10W行代码可怕

不过实践告诉我,这个分水岭并不是绝对的。

"另外,提一个:不是单片机支持语言

面是IDE和相关的平台支持

因为,不论什么机器,至少当前,机器只认0和1"
没错,是我说的不严谨

我所主张的所谓C语言的“封装”,可不是要写一个c++类那种形式的封装,而是封装于模块之间,说白了就是模块化思想。我怎么感觉lbing7似乎理解成了我在鼓吹用c++来写单片机程序。我可从来没有过这种主张啊。我的最后一句话写的很明确“任何技术都有它的适用范围,切不可乱用。 ”我从来都是强调在适合的环境用适合的武器。

“对于代码量,不知道LZ是不是真的敲过上W行的代码(不在IDE的帮助下)”
我不太理解敲代码和用不用IDE有什么排斥的关系。keil我感觉无非是一个带语法加亮功能的编辑器而已,这和敲代码有什么冲突吗?keil又不像VC可以用ClassWizard生成一部分的代码,如果只是写c程序的话顶多自动生成一些单片机的启动代码。当然,keil集成了编译的过程。但和编辑代码好像没什么排斥关系吧?

我承认用keil确实没写过上万行的代码,但用vc应该还是没问题的,纯手工敲不包括编译器自动生成的。

#45


好的单片机程序一般是软硬件分离.

硬件关联性太强的,通用性都不怎么样.

同一源代码能够方便编译出不同硬件条件的程序才是不错的.

没写过汇编的单片机程序员写出来的c语言的效率一般都不怎么样.
一般 for(){} 满天飞的程序我是不看的.

#46


引用 40 楼 lbing7 的回复:
写程序第一要是对的!!

LZ一个不正确的程序是没有任何的其它意义

呵呵

我想这恐怕就是写单片机程序和写pc程序人之间最大的区别了。我们所谓程序的“对”或“正确性”其实都是相对的,描述为“在运行至今还没有发现问题的程序”更客观一些。谁都无法保证程序今天对那么明天一定也对。恰恰问题就出在这里,若是明天程序出了错误,而当时写程序是本着“写对是第一位”的思想,那么这个程序的可读性以及各种衡量标准那就可想而知了。在没有可读性等保障性标准作为前提,找一个程序的bug谈何容易?

#47


引用 41 楼 conry 的回复:
赞同lz的思想
不过关于代码行多于10W面向过程难于应付还是设计管理的问题,
linux内核里面没有C++的代码吧

另外lZ的一个bug
C/C++ code
BYTE i=0;for(i=0; i< RANGE_TABLE_SIZE; i++)
{if(a> RangeTable[i].nLower&& a<= RangeTable[i]. nUpper)
    {
        b= i;break;//少了这个,呵呵    }
}

没错,谢谢提醒。




#48


引用 45 楼 knate 的回复:
好的单片机程序一般是软硬件分离.

硬件关联性太强的,通用性都不怎么样.

同一源代码能够方便编译出不同硬件条件的程序才是不错的.

没写过汇编的单片机程序员写出来的c语言的效率一般都不怎么样.
一般 for(){} 满天飞的程序我是不看的.

很赞成前三那句话,至于最后两句有点不敢苟同。
固然学完汇编再学c有对效率天生的敏感?但既然选择了用c来写程序,那就不得不去考虑效率的问题,毕竟c是除了汇编基本上可以说是第二有效率的语言了。这和学没学过汇编似乎没什么必然联系。如果用c语言写程序却对效率漠不关心,那只能说这个c程序员不称职。当然,学了汇编之后对c语言的理解会更深入,这毋庸置疑。

我的主张是,学习c语言必须了解其语句的效率,这倒不一定要求一定要能用汇编写程序,但起码得能大概看懂。比如:
为什么51系列单片机主张用单字节做变量进行运算?为什么宏要比函数占用更多的rom,却速度可以更快?为什么函数名就是函数的入口地址?为什么会有函数重入性问题?为什么中断函数没有返回值没有参数?等等

当把这些底层的东西都了解了,那么写出来的c程序效率不回比汇编低多少的,就算是低一些但是由于c的开发效率较之汇编高,也会补偿回来的。

#49


引用 46 楼 jiqiang01234 的回复:
引用 40 楼 lbing7 的回复:

写程序第一要是对的!!

LZ一个不正确的程序是没有任何的其它意义

呵呵


我想这恐怕就是写单片机程序和写pc程序人之间最大的区别了。我们所谓程序的“对”或“正确性”其实都是相对的,描述为“在运行至今还没有发现问题的程序”更客观一些。谁都无法保证程序今天对那么明天一定也对。恰恰问题就出在这里,若是明天程序出了错误,而当时写程序是本着“写对是第一位”的思想,那么这个程序的可读性以及各种衡量标准那就可想而知了。在没有可读性等保障性标准作为前提,找一个程序的bug谈何容易?

同意楼主这个,可读性是一定要保证的。有可读性的不正确以后有机会变成正确,没有可读性的正确只能说是偶然。

#50


路过