关于单片机代码的风格

2019-04-15 18:05发布

概述

程序不仅要被计算机读,还要给程序员读。一个风格清爽而严谨的程序更容易被读懂,更容易被修改和排错。良好的编程风格和正确的习惯还有助于保持思维清晰,写出正确无误的代码。特别是一个开发团队共同工作时,保持一致的编程风格尤其重要。目前单片机开发人员对编程风格问题重视度还不够。事实上,每个初学者在项目初期都会因为不良编程习惯浪费大量时间,因此若能在开始写程序时就重视编程风格问题,对顺利渡过提高阶段有很大帮助。篇幅所限,本节仅浅述编程风格几个最基本原则。

一、变量命名规则

变量名尽量使用具有说明性的名称,避免使用 a,b,c,x,y,z 等无意义字符。使用范围大的变量,如全局变量,更应该有一个说明性的名称。变量名尽量使用名词,长度控制在1~4个单词最佳。若名称包含多个单词,每个单词首字母大写以便区分单词,例如: int InputVoltage; //输入电压 int Temperature; //温度
当单词间必须出现空格才好理解的时候,可以用下划线’_’替代空格: int Degree_C; //摄氏度 int Degree_F; //华氏度
当单词较长的时候,可以适当简写: int NumOfInputChr; //输入字符数 int InputChrCnt; //输入字符数 int Deg_F; //华氏度
一旦约定某方式简写,以后必须保持风格统一。
若多个模块都可能出现某个变量,可以按“模块名_变量名”的方式命名:
char ADC_Status; // ADC 的状态 char BT_IntervalFlag; // BasicTimer 定时到达标志 int UART_RxCharCnt; // 串口收到的字符总数
对于约定俗成的变量,如用变量 i、j 做为循环变量,p、q 作为指针,s、t 表示字符串等,不要改动。这里采用更长的变量名反而不符合习惯。

二、函数命名规则

和变量一样,函数名称也应具有说明性。函数名应使用动词或动作性名字,后面可以跟名词说明操作对象。按照“模块名_功能名”的方式命名:
unsigned int ADC16_Sample(); //16 位 ADC 采样 char LCD_Init(); //LCD 初始化 char RTC_GetVal(); //获取实时钟的数据 void PWM_SetPeriod(); //设置 PWM 周期 void PWM_SetDuty(); //设置 PWM 占空比 void Flash_WriteChar(); //向 Flash 写入一字节数据 char UART_GetChar(); //从串口读取一字节数据 char Key_GetKey(); //从键盘读取一次按键 char TouchPad_GetKey(); //从触摸板读取一次按键
每个单词首字母大写,便于阅读,专有名词或缩略词(ADC/LCD 等)全部大写。遇到太长的单词也可以在不影响阅读的情况下适当简写,例如用Tx替代单词Transmit,Rx替代单词Receive,Num替代单词Number,Cnt替换Count,数字2(Two)替代单词To等。和变量一样,一旦约定某种简写方式,以后必须保持风格统一。对于所有模块都通用的函数,如求均值函数,可以不写模块名。对于返回值是布尔类型值的函数(真或假),名称应清楚反映返回值情况,例如编写某函数检查串口发送缓冲区是否填满:
char UART_CheckTxBuff(); //不恰当的函数名 char UART_IsTxBuffFull(); //意义明确的函数名
第一个函数字面意思是“检查发送缓冲区”,若返回真(1)表示什么意思不明确。
第二个函数字面意思是“发送缓冲区满了么?”,若返回真(1)有明确的意思:满了。

三、表达式

表达式应该尽量自然、简洁、无歧义。写代码的时候,要杜绝各类“技巧”。我们的目标是要写最清晰的代码,而不是最巧妙的代码。下面2个表达式所表达的条件是等价的,第一个逻辑拐了个弯,难以理解,写成第二种表达方式就清晰许多。
if(!(RxCharNum<20)||(!(RxCharNum>=16)) //晦涩的表达式 if((RxCharNum>=20)||(RxCharNum<16)) //清晰的表达式
再看下面的表达式想要干什么?
SubKey=SubKey>>(Bits-(Bits/8)*8); //难懂的表达式
最内层表达式把Bits除以8再乘以8,实际上相当于把最低三位清零。再从 Bits 原值减去这个结果,实际上相当于取Bits的最低3位。最后用这三位确定SubKey的移位次数。实际上与下面简洁的表达式等价:
SubKey=SubKey >> (Bits & 0x07); //清晰的表达式 SubKey >>= (Bits & 0x07); //清晰的表达式 SubKey >>= (Bits%8); //清晰的表达式
一个好的表达式应该能够用英语朗读出来。可以作为检验表达式好坏的依据。例如:
if(UART_IsTxBuffFull()) UART_ClearTxBuff(); else UART_PutChar(0x55);
上面的表达式可以被朗读出来: “如果串口发送缓冲区满了,就清空发送缓冲区,否则从串口发送字符 0x55”,可读性很好。
表达式不仅要清晰,还要消除歧义。若初学者对i++ 和++i顺序比较含糊,完全可以将表达式拆开避免歧义。若对运算符优先级问题没有把握,可以用括号消除可能出现的歧义。

四、风格一致性

对于书写格式,向来争议很大,例如括号配对就有 2 种流行的写法:
for(i=0;i<100;i++){ for(j=0;j<200;j++){ //括号配对风格 1 ... } } while(a==b){ if(c==d){ //括号配对风格 1 ... } else{ ... } }
另一种写法是
for(i=0;i<100;i++) { //括号配对风格 2for(j=0;j<200;j++) { ... } } while(a==b) { if(c==d) { //括号配对风格 2 ... } else { ... } }
除了书写风格,命名方法也有很多种标准,且经常可以看到哪种格式更好的讨论。事实上,最好的格式、最好的命名方法是和惯用风格保持一致。如果加入一个开发团队,团队目前所使用的风格就是最好的格式;如果改写别人写的程序,保持原程序的风格就是最好的风格。总之,程序的一致性比本人的习惯更重要。如果初学者还没有形成自己的风格,那么可以参考官方提供的范例程序,或者与平台供应商的代码保持风格一致。

五、注释

注释是帮助程序读者的一种手段,但是如果注释只是代码的重复,将会变得毫无意义。若注释与代码矛盾,反倒会帮倒忙。最好的注释是简洁明了的点明程序的突出特征,或者阐明思路,或提供宏观的功能解释,或者指出特殊之处,以帮助别人理解程序。比较同一段代码的两种注释方法:
for(i=6;i>DOT;i--) //从第 6 位(最高位)到小数点之间依次递减 { if (DispBuff[i]==0) DispBuff[i]=’ ’; //如果该位数值是 0,则替换成空格else break; //如果不是,则跳出循环 }
另一种注释方法
for(i=6;i>DOT;i--) { if (DispBuff[i]==0) DispBuff[i]=’ ’; //消隐显示数据小数点前的无效 0else break; }
第1种注释每行都有注释,但读者仍然不明白这段程序功能或目的是什么。因为每个注释无非是代码的解释和重复。第2种注释虽然只有1行,但简明扼要的说明了这个for循环的功能,帮助读者理解程序。写注释的时候要从读程序的思路来考虑而不要以设计者的角度思考。对于每个函数,特别是底层函数,都要注释。在函数前面注释该函数的名称、参数、参数值域、返回值、设计思路、功能、注意事项等。如果参与团队开发,还应写若干典型应用范例,供他人参考。商业化的代码中,注释比程序长的情况很常见。在代码维护、调试与排错时,若修改代码,要养成立即修改注释的习惯,否则很容易出现代码与注释不一致的情况,很可能造成难以排查的错误,严重影响工作效率。

六、宏定义

数值没有任何能表达自身含义的可读性,因此对于程序中出现的数值,他们应该有自己的名字。一般可以用宏定义来实现,并且用宏定义处理数据之间的关系。在寄存器操作章节中,我们已经看到头文件中对寄存器地址、标志位等都作了宏定义,用宏对寄存器赋值后,程序立刻有了可读性。类似的,我们可以用宏定义来对常数数值赋予可读性。
#define TXBUFF_SIZE (128) /*发送缓冲大小*/ #define LCM_ROW (64) /*点阵液晶行数*/ #define LCM_CLUMN (128) /*点阵液晶列数*/ #define LCM_BUF_SIZE (LCD_CLUMN*LCD_ROW/8) /*点阵液晶缓冲区大小*/
将常数值用宏定义之后写出来的代码可读性增强了:
unsigned char TxBuff[TXBUFF_SIZE]; //定义发送缓冲区char IsTxBuffFull() { if(NumOfTxChars>=TXBUFF_SIZE) return(1); //缓冲区是否满?else return(0); }
在上例中,一旦需要改变缓冲区大小,只需要修改宏定义即可。宏定义属于字符型替代,因此在使用宏定义的时候要注意防止产生歧义。例如数据全部加括号,以免和程序前后文构成意料之外的运算优先级。宏定义后的注释使用 /**/而不要用// ,以免某些版本的编译器在代码中将宏定义连同注释全部替换造成错误。
使用宏定义还要防止定点计算溢出,例如:
#define VOLT_RATE (1000) /*比例系数*/ ... int Voltage; int InputValue; ... Voltage=InputValue*VOLT_RATE; //可能溢出
若将宏定义做如下修改即可避免溢出问题:
#define VOLT_RATE ((long)1000) /*比例系数,强整成 long*/


摘自《MSP430系列单片机系统设计与实践》第一章第六节