嵌入式C51 编程规范[转]

  为单片机编写C51代码,程序的可行性当然是必须保证的。但是包括笔者在内的很多新手,都忽略了程序的另一面——可读性、可维护性以及可扩展性。只要稍微有些嵌入式开发经验的读者,若看到笔者在“Zigbee之旅”系列博文中的源码,可能都会从其代码编写习惯中得出一个结论——“菜鸟”。呵呵,笔者决定抽时间学习一下C51嵌入式开发的编程规范,于是在网上收集了一些资料,结合自己的经验,一并分享如下。

一、注释

(1)文件注释

  这里说的文件,一般是 .h.c 文件。

/***********************************************************
文件名称: hal.h
作 者: hustlzp
日 期:  2011/3/5
版 本: 1.1
功能说明: 硬件抽象层
函数列表: (略)
修改记录:
***********************************************************/

  其实一个人学习的话,诸如“文件名称”、“作者”、“版本”、“日期”这些内容,不是特别必要。上述的规范一般在公司内要求比较严格(在多人作业的情况下,对于软件开发的流程控制非常重要)。

  但“说明”和“函数列表”这两项,我想还是方便的话写写比较好。当你对这个项目比较淡忘的时候,你只需要扫一下文件头部注释,就能一下子知道这个文件到底是干什么的,明白都有哪些函数。

(2)函数注释

  如果对上述的文件注释不怎么感冒的话,我想大家函数注释都应比 较熟悉。博客园的园友中大多都是使用vs的,相信vs中对函数注释的支持一定不会忘记(敲三个///,啥都出来了,只需要一个个填就行)。虽然嵌入式开发 IDE没有如此强大的功能,但是还是很有必要对“函数功能”、“入口参数”、“出口参数”进行说明:

/***********************************************************
函数名称: SetTimer1Period
函数功能: 设置定时器1的定时时长
入口参数: DWORD period
      定时时长
出口参数: WORD
      确定PWM脉冲宽度  
***********************************************************/
WORD halSetTimer1Period(DWORD period);

(3)代码注释

  至于函数内部的代码注释,需要注意几点:

  • 同一函数内的注释最好左对齐
  • 少量注释放在代码右边,较多则放在正上方
  • 在代码的功能层次上注释,逻辑结构分明
void setSleepTimer(unsigned int sec)
{
  unsigned long sleepTimer = 0;
  
  sleepTimer |= ST0; //取得目前的睡眠定时器的计数值
  sleepTimer |= (unsigned long)ST1 << 8;
  sleepTimer |= (unsigned long)ST2 << 16;
 
  sleepTimer += ((unsigned long)sec * 32768); //加上所需要的定时时长
 
  ST2 = (unsigned char)(sleepTimer >> 16); //设置睡眠定时器的比较值
  ST1 = (unsigned char)(sleepTimer >> 8);
  ST0 = (unsigned char)sleepTimer;
}

二、命名

(1)要具有明确含义

  这一点我想不用多说,相信稍具经验的C#程序员都已熟练掌握:

  • 尽量使用完整的英文单词,不会的查灵格斯,no拼音
  • 可使用一些大家公认的缩写,若使用自定义的缩写,需注释说明
  • 函数的命名一般选择动宾短语,即动词+宾语,这样做可较好地说明函数的功能
  •  

(2)命名风格的选择

  C51编程中,一般会有两种用得较多的命名风格:

  大小写风格:

unsigned char get_value();
{...}

  下划线风格:

unsigned char getValue()
{...}

  其实选用何种风格并不重要,重要的是坚持在整个项目代码中统一风格,尽量避免混用。

(3)宏及常数的命名

  宏及常数的命名必须全用大写字母,而且词与词之间用下划线分隔:

#define GRADIENT 0.03114
#define OFFSET -303
#define ADC14_TO_CELSIUS(ADC_VALUE)
((
float)ADC_VALUE *(float)GRADIENT + OFFSET)

三、排版

(1)缩进

  • 在花括号内{}的代码,都应相对于花括号做出一定的缩进
void LCD_write_english_string(unsigned char X,unsigned char Y,char *s)
{
unsigned
char i=0;

LCD_set_XY(
0,Y);

for(i=0;i<84;i++)
{
LCD_write_byte(
0,1);
}

LCD_set_XY(X,Y);

while (*s)
{
LCD_write_char(
*s);
s
++;
i
++;
if(i>=14)return;
}
}
  • 充分利用IDE的缩进功能

  • 手动缩进时,建时议使用空格键,而不使用 Tab 键,以免用不同的编辑器阅读程序时,因 Tab 键所设置的空格数目不同而造成程序布局的不整齐

(2)空格

  • 定义变量时的逗号,只在后面加空格
int a, b, c;
  • 比较操作符 , 赋值操作符 "=" 、"+=" ,算术操作符 "+" 、"%" ,逻辑操作符 "&&" 、"&" ,位域操作符 "<<" 、 "^" 等双目操作符的前后建议加空格
a = b + c;
a
*= 2;
a
= b ^ 2;

(3)行

  • 一行只写一句代码
  • 过长的表达式(超过80个字符)要分成多行书写,在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要进适当的缩进,使排版整齐,语句可读。对齐建议使用空格键,不使用 Tab 键,以免用不同的编辑器阅读程序时,因 Tab 键所设置的空格数目不同而造成程序布局混乱:
report_or_not_flag = ((taskno < MAX_ACT_TASK_NUMBER)
          && (n7stat_stat_item_valid (stat_item))
          && (act_task_table[taskno].result_data != 0));
  • 函数中参数较长,则以“,”进行适当的分行
stateCompare(state_object,
       act_task_table[taskno].stat_object),
       sizeof (STAT_OBJECT));
  • 函数中相对独立的程序块之间,建议加空行
if (!valid_ni(ni))
{
  ...
// program code
}

repssn_ind
= ssn_data[index].repssn;
repssn_ni
= ssn_data[index].ni;

四、宏定义

(1)定义片内/片外资源

  对片内资源的定义就不多说了,看一下 ioCC2430.h 就知道了~  

  还可定义一些片外资源,例如,定义接在P0端口的LED灯:

#define led1 P1_0
#define led2 P1_1
#define led3 P1_2
#define led4 P1_3

(2)定义有意义的常数

  有具体意义的常数应在宏中定义,以便日后集中修改:

#define GRADIENT 0.03114
#define OFFSET -303

(3)定义控制参数

  在“Zigbee之旅”系列博文中,经常出现需要对某一SFR进行赋值以达到控制的目的,我之前的处理方法是,直接用一个具体的数去赋值,如下所示:

/*UART0通信初始化
------------------------------------------------------------------
*/
void Uart0Init(unsigned char StopBits,unsigned char Parity)
{
P0SEL
|= 0x0C; //初始化UART0端口,设置P0.2与P0.3为外部设备IO口
PERCFG&= ~0x01; //选择UART0为可选位置一,即RXD接P0.2,TXD接P0.3

U0CSR
= 0xC0; //设置为UART模式,并使能接受器

U0GCR
= 11;
U0BAUD
= 216; //设置UART0波特率为115200bps

U0UCR
|= StopBits|Parity; //设置停止位与奇偶校验
}


/*主函数
------------------------------------------------------------------
*/
void main(void)
{
...

Uart0Init(
0x00,0x00); //初始化UART0,设置1个停止位,无奇偶校验

...
}

  在上面的代码中,首先定义了一个初始化串口的函数 Uart0Unit。然后在 main 函数中使用它,参数分别为0x00(一个停止位),0x00(无奇偶校验)。

  这样做的确可以图一时的方便,但是会造成较差的可读性:假如另外一个不太懂CC2430的SFR具体用法的程序员来修改你的代码,他怎么知道这0x00有着什么含义?

  为了解决这个问题,我们建议的处理方法是:将可能的选项值全部用宏定义,如下:

//停止位的设置
#define TWO_STOP_BITS 0x04
#define ONE_STOP_BITS 0x00
 

//奇偶校验的使能
#define PARITY_ENABLE 0x08
#define PARITY_DISABLE 0x00

  OK,下面我们来使用这些宏作为函数 Uart0Unit 的参数:

void main(void)
{
...

Uart0Unit(ONE_STOP_BITS, PARITY_DISABLE);

...
}

  当别人看到这样的代码时,就会瞬间明白:一位停止位、无奇偶校验。修改起来也大为方便了,只需去查找宏定义就OK~

(4)定义一些常用的、功能单一的、有具体意义的代码组合

  • 常用的代码组合,如一些赋值、配置/初始化系统资源等代码,都可用宏来定义,如下:

//设置电源模式
#define SET_POWER_MODE(mode) \
do { \
SLEEP
&= ~0x03; \
SLEEP
|= mode; \
PCON
|= 0x01; \
}
while (0)
//将某16位变量的值分别赋给两个8位变量
#define SET_WORD(regH, regL, word)
do{ \
(regH)
= HIBYTE( word ); \
(regL)
= LOBYTE( word ); \
}
while (0)

  使用的时候,直接将其当做一般的函数调用就行。

CC2430 小贴士

(1)do{...}while(0)

  看到这里,很多读者朋友可能对宏定义中的 do{...}while(0) 语句不太理解:既然是while(0),那么去掉do{...}while(0),程序也应该是对的呀?为什么还留着呢?

  其实,应用这种看似无用的do/while将代码框起来,是为了提高代码的健壮性,减少编译时可能产生的错误,具体可以参考此处

(2)反斜杠

  细心的读者还会发现,在多行宏定义中,除最后一行外每一行都有一个反斜杠 "/",这有啥用呢?

  在宏定义中,规定必须在一行内完成,但是用一行的话会大大降低代码的可读性。于是,我们可以加一个 "/" 表示续行的意思。当然,最后一行不能加。

五、程序结构

(1)根据功能模块划分文件

  一个项目,最好按功能分成几个逻辑清晰的模块,每一个模块还可以由一到多个职责各不相同的 .c 源文件来实现。这样一来可降低模块间的耦合性,提高可读性与维护性。

  此时,我们可以为每一个模块内的 .c 文件建立共同的 .h 头文件,然后再用一个 total.h 文件引用之前各模块的头文件。以后需引用时,只需引入 total.h 即可。

  例如,我们打算实施一个“温度采集系统”,则可以按下面的流程进行:

  • 将项目分为三大模块:LCD(屏显)、HAL(硬件抽象)、RF(无线收发)
  • 建立三个模块对应的头文件:LCD.h、HAL.h、RF.h
  • 建立total.h文件,引用LCD.h、HAL.h、RF.h
  • 然后建立3个模块对应的的 .c 源文件
  • 在含有main函数的.c文件中,通过引用 total.h 来使用各模块中的代码,实现系统功能

(2)头文件结构

  .h头文件的结构顺序一般为:

    头文件引入(include) → 宏定义(define) → 函数签名

  如下所示:

/*******************************************************************************

Filename: lcd.h
Target: cc2430
Author: KJA
Revised: 16/12-2005
Revision: 1.0

Description:
Function declarations for common LCD functions for use with the SmartRF04EB.

All functions defined here are implemented in lcd.c.

*****************************************************************************
*/
 

#include
"total.h"
 

#define LINE_SIZE 14 // Line length of LCD
 
#define LINE1_ADDR 0x80 // Upper line of LCD
#define LINE2_ADDR 0xC0 // Lower line of LCD
 
//symbol codes
#define ARROW_LEFT        124
#define ARROW_RIGHT       125
#define ARROW_UP          126
#define ARROW_DOWN        127 

 

//Setup I/O, configure display and clear LCD.
void initLcd(void);


//Converts the two text strings from ASCII to the character
void lcdUpdate(char *pLine1, char *pLine2);


//Write one line of text to LCD.
void lcdUpdateLine(UINT8 line, char *line_p);


//Write a single character to LCD.
void lcdUpdateChar(UINT8 line, UINT8 position, char c);
 

六、结语

  OK,C51编码规范的学习到此稍稍停一下,关键还是要在编码实践中去遵循,以后我也会回过头来逐步修改完善本篇日志的。

  由于笔者的编码经验还很不足,所以以上内容仅供参考啦,不妥之处还请大家多多批评指正!


posted @ 2011-03-07 00:03  guangrou  阅读(429)  评论(1编辑  收藏  举报