为单片机编写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#程序员都已熟练掌握:
(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)行
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)
使用的时候,直接将其当做一般的函数调用就行。
(1)do{...}while(0)
看到这里,很多读者朋友可能对宏定义中的 do{...}while(0) 语句不太理解:既然是while(0),那么去掉do{...}while(0),程序也应该是对的呀?为什么还留着呢?
其实,应用这种看似无用的do/while将代码框起来,是为了提高代码的健壮性,减少编译时可能产生的错误,具体可以参考此处。
(2)反斜杠
细心的读者还会发现,在多行宏定义中,除最后一行外每一行都有一个反斜杠 "/",这有啥用呢?
在宏定义中,规定必须在一行内完成,但是用一行的话会大大降低代码的可读性。于是,我们可以加一个 "/" 表示续行的意思。当然,最后一行不能加。
(1)根据功能模块划分文件
一个项目,最好按功能分成几个逻辑清晰的模块,每一个模块还可以由一到多个职责各不相同的 .c 源文件来实现。这样一来可降低模块间的耦合性,提高可读性与维护性。
此时,我们可以为每一个模块内的 .c 文件建立共同的 .h 头文件,然后再用一个 total.h 文件引用之前各模块的头文件。以后需引用时,只需引入 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编码规范的学习到此稍稍停一下,关键还是要在编码实践中去遵循,以后我也会回过头来逐步修改完善本篇日志的。
由于笔者的编码经验还很不足,所以以上内容仅供参考啦,不妥之处还请大家多多批评指正!