嵌入式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编码规范的学习到此稍稍停一下,关键还是要在编码实践中去遵循,以后我也会回过头来逐步修改完善本篇日志的。
由于笔者的编码经验还很不足,所以以上内容仅供参考啦,不妥之处还请大家多多批评指正!