文章目录
这篇文章主要是以FreeRTOS操作系统的源码作为参考
一、基本原则
- Freertos编程风格
- 不知道大家有没有这样的感受:看到不规范(杂乱差)的代码,瞬间就没有看下去的欲望了。相信大家看到标题都应该能明白编程的规范及原则对于每一个软件开发的工程师来说是多么重要。初学者编写测试程序、小的模块程序也许不能感受它的重要性;但有经验及大型项目开发的人就知道程序的规范性对他们来说是有多么的重要。
- 编程规范也就是编写出简洁、可维护、可靠、可测试、高效、可移植的代码,提高产品代码的质量。本文针对嵌入式,主要结合C语言编程的规范给大家讲述。
二、返回值类型
表1-1
变量前缀 | 含义 |
---|---|
c | char |
s | short |
l | long |
x | 其他非标准类型,_t结尾,如BaseType_t |
p | 指针 |
v | 空类型 |
ppuc | 二级指针,unsigned char **ppucIdleTaskTCBBuffer |
uc | uint8_t,unsigned char |
pc | char指针 |
ux | unsigned long |
prv | 针对函数的返回类型是 static |
重定义数据类型
如果重新定义了long数据类型,那么就是x (非标准) 类型了,就应该 -t 结尾。
如果需要unsigned long重新定义,那么就是ux类型。并且类型需要 U 开头。
typedef long BaseType_t;
typedef unsigned long UBaseType_t; //ux
-
补充1:
在不同计算机的long int和int所占的字节数不同,- 32位单片机下
long int 是32位,int也是32位,所以这个时候通常认为long同等与int,就是 l 类型, - 8位单片机下
long int 是32位,int是16位,所以long int 是 l 类型,int 是 i 类型。
- 32位单片机下
-
补充2:
- 在32位单片机下
为什么 unsigned int是ul类型,定义是这样的 typedef unsigned int uint32_t ,并没有使用typedef 先定义 int,再去定义 unisgned int,而是直接使用 typedef 定义了 unsigned int ,所以是 ul 类型。如果先用typedef 先去重定义了int,再来重定义unsigned int。那么这个时候unsigned int就是 ux,
- 在32位单片机下
所以:
因为我们所说的 int 就是指的就是 long int,也就是long,所以unsigned int 是 ul 类型,
其实这个使用成 x类型也没有错,但是为了区分 UBaseType_t 和uint32_t 使用ux,ul比较合适。
三、变量
关键语句哥关键语句之间使用 _ 分隔(视情况而定)
所有前缀参考表1-1
- 局部变量
前缀+实体
实体: 实体使用大驼峰命名方式Timer_t *pxNew_Timer;
- 函数参数
前缀+实体
实体: 实体使用大驼峰命名方式TimerHandle_t xTimerCreate( const char * const pcTimer_Name, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */ const TickType_t xTimerPeriodIn_Ticks, const UBaseType_t uxAuto_Reload, void * const pvTimer_ID, TimerCallbackFunction_t pxCallback_Function )
- 宏参数
前缀+实体
实体: 实体使用大驼峰命名方式#define xTaskNotifyFromISR( xTaskToNotify, ulValue, eAction, pxHigherPriorityTaskWoken ) xTaskGenericNotifyFromISR( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL, ( pxHigherPriorityTaskWoken ) )
- 结构体字段
前缀+实体
实体: 实体使用大驼峰命名方式typedef struct __DMA_Handle_t_ { uint32_t ulStreamBase_Address; uint32_t ulStream_Index; }DMA_Handle_t_;
- 联合体成员
前缀+实体
实体: 实体使用大驼峰命名方式typedef union { TimerParameter_t xTimerPara_meters; #if ( INCLUDE_xTimerPendFunctionCall == 1 ) CallbackParameters_t xCallbackParameters; #endif } TimerParame_u;
- 全局变量
g_+前缀+实体
实体: 实体使用大驼峰命名方式int g_lMin_Max
- 静态变量(static)
s_+前缀+实体
实体: 实体使用大驼峰命名方式int s_lMin_Max
- 宏(不包括宏函数)
全大写下划线分割#define ACK_SUCCESS 0x01
- 枚举值
全大写下划线分割typedef enum { HAL_OK = 0x00U, HAL_ERROR = 0x01U, HAL_BUSY = 0x02U, HAL_TIMEOUT = 0x03U } HAL_Status_e;
- goto标签
全大写下划线分割goto AUTO;
- 函数式的宏
全大写下划线分割#define ROTR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))
- 常量(const)
全大写下划线分割
前缀+实体const uint8_T ucREAD_STRING[10] = "Hello work";
四、结构体
后续的使用过程中,使用的是结构体别名,并且结构体别名用 _t 作为后缀,结构体名使用两个 __ 开头。
这里有点疑问的是大驼峰命名方式是首字母大写,为什么是UART而不是Uart,这个是因为UART是缩写,每一个字母都是一个单词,UART的缩写是Universal Asynchronous Receiver/Transmitter
结构体名用 大驼峰 命名
不建议在.C文件中定义结构体
使用typedef重新定义的数据也要用 _t 作为后缀
typedef unsigned long UBaseType_t;
typedef struct __UART_Handle_t
{
......
}UARTHandle_t
UARTHandle_t xRtuen;
五、枚举(enum)
枚举名使用 _e 作为后缀,使用大驼峰命名。
/*举例*/
typedef enum
{
......
}TaskState_e;
六、共用体/联合体(union)
共用体使用 _u 作为后缀,使用大驼峰命名。
typedef union
{
...
}Data_u;
七、函数
返回值的类型参考表1-1。
普通函数由两个部分组成 返回值类型+函数体
中断函数和回调函数使用三部分 返回值类型+函数体+后缀
关键语句和关键语句之间使用 _ 分隔
-
返回值是 x (结构体) 类型
QueueHandle_t xQueue_GenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )
-
静态函数类型 prv,就不需要做返回值的识别了
static size_t prvWrite_BytesToBuffer( StreamBuffer_t * const pxStreamBuffer, const uint8_t *pucData, size_t xCount ) PRIVILEGED_FUNCTION;
-
返回值是 void(空类型)
void vList_InitialiseItem( ListItem_t * const pxItem );
-
中断函数 (_IRQHandler ) 后缀
IRQ 代表 “Interrupt Request”,表示中断请求。而 Handler 表示处理中断请求的函数。因此,IRQHandler 可以理解为中断处理函数的一般命名。
void USART6_IRQHandler(void)
-
中断服务函数 ( _ISR ) 后缀
ISR 是中断服务例程 (Interrupt Service Routine) 的缩写,在嵌入式系统中,特别是在使用类似于ARM Cortex-M架构的微控制器时,中断服务例程是处理中断事件的函数。中断服务例程是用于处理中断事件的特殊类型的函数。在嵌入式系统和操作系统中,中断是一种异步事件,它会打断正常的程序执行,以响应某个硬件或软件事件。BaseType_t xQueueIsQueueEmptyFrom_ISR( const QueueHandle_t xQueue )
-
回调函数 (Callback)
“Callback” 是一种编程概念,表示在一个函数中注册的另一个函数,以便在满足某些条件时被调用。实际上,Callback 是一种回调机制,通过这种机制,可以将一个函数的执行控制权交给另一个函数,使得代码更加灵活和可扩展。
一个简单的例子是事件处理。调用方负责处理某个事件,而回调函数是在事件发生时要执行的逻辑。这样,通过注册不同的回调函数,可以在相同的事件处理框架下执行不同的逻辑。回调函数更多的是目的处理void HAL_GPIOEXTI_Callback(uint16_t GPIO_Pin)
-
钩子函数 Hook
钩子函数是回调函数的一种,钩子函数更多的是过程监控,而钩子函数一般是在系统中运行的
extern void vApplicationIdle_Hook( void ); vApplicationIdle_Hook( void );
回调函数和钩子函数的具体区别,移步到回调函数和钩子函数的区别
八、指针
- 指向空类型的指针pv
void * const pvBuffer
- 指向结构体类型的指针
BaseType_t * const pxHigherPriorityTaskWoken
其他类型的指针参考表1-1依此推
- 指向非基本(x)类型的 二级指针
StaticTask_t **ppxIdleTaskTCBBuffer
九、注释
- 不使用 C++风格的双斜线(//)注释
- 使用 /**/ 注释
- 单行注释原则上不超过80列,特殊情况除外
- 可多行注释
- 尽量捉住关键点,尽量简短
- 函数内不添加过多注释,尽量在函数外添加注释,即在函数说明中写注释
十、格式
10.1 大括号
使用 K&R 缩进风格
K&R风格 换行时,函数(不包括lambda表达式)左大括号另起一行放行首,并独占一行;其他左大括号跟随语句放行末。 右大括号独占一行,除非后面跟着同一语句的剩余部分,如 do 语句中的 while,或者 if 语句的 else/else if,或者逗号、分号。
struct MyType { // 跟随语句放行末,前置1空格
...
};
int Foo(int a)
{ // 函数左大括号独占一行,放行首
if (...) {
...
} else {
...
}
}
推荐这种风格的理由:
- 代码更紧凑;
- 相比另起一行,放行末使代码阅读节奏感上更连续;
- 符合后来语言的习惯,符合业界主流习惯;
- 现代集成开发环境(IDE)都具有代码缩进对齐显示的辅助功能,大括号放在行尾并不会对缩进和范围产生理解上的影响。
对于空函数体,可以将大括号放在同一行:
MyClass() {}
10.2 函数声明和定义
函数声明和定义的返回类型和函数名在同一行;函数参数列表超出行宽时要换行并合理对齐
在声明和定义函数的时候,函数的返回值类型应该和函数名在同一行;如果行宽度允许,函数参数也应该放在一行;否则,函数参数应该换行,并进行合理对齐。 参数列表的左圆括号总是和函数名在同一行,不要单独一行;右圆括号总是跟随最后一个参数。
换行举例:
ReturnType FunctionName(ArgType paramName1, ArgType paramName2) // Good:全在同一行
{
...
}
ReturnType VeryVeryVeryLongFunctionName(ArgType paramName1, // 行宽不满足所有参数,进行换行
ArgType paramName2, // Good:和上一行参数对齐
ArgType paramName3)
{
...
}
ReturnType LongFunctionName(ArgType paramName1, ArgType paramName2, // 行宽限制,进行换行
ArgType paramName3, ArgType paramName4, ArgType paramName5) // Good: 换行后 4 空格缩进
{
...
}
ReturnType ReallyReallyReallyReallyLongFunctionName( // 行宽不满足第1个参数,直接换行
ArgType paramName1, ArgType paramName2, ArgType paramName3) // Good: 换行后 4 空格缩进
{
...
}
10.3 函数调用
函数调用入参列表应放在一行,超出行宽换行时,保持参数进行合理对齐
函数调用时,函数参数列表放在一行。参数列表如果超过行宽,需要换行并进行合理的参数对齐。 左圆括号总是跟函数名,右圆括号总是跟最后一个参数。
换行举例:
ReturnType result = FunctionName(paramName1, paramName2); // Good:函数参数放在一行
ReturnType result = FunctionName(paramName1,
paramName2, // Good:保持与上方参数对齐
paramName3);
ReturnType result = FunctionName(paramName1, paramName2,
paramName3, paramName4, paramName5); // Good:参数换行,4 空格缩进
ReturnType result = VeryVeryVeryLongFunctionName( // 行宽不满足第1个参数,直接换行
paramName1, paramName2, paramName3); // 换行后,4 空格缩进
如果函数调用的参数存在内在关联性,按照可理解性优先于格式排版要求,对参数进行合理分组换行。
// Good:每行的参数代表一组相关性较强的数据结构,放在一行便于理解
int result = DealWithStructureLikeParams(left.x, left.y, // 表示一组相关参数
right.x, right.y); // 表示另外一组相关参数
10.4 if语句
if语句必须要使用大括号
我们要求if语句都需要使用大括号,即便只有一条语句。
理由:
- 代码逻辑直观,易读;
- 在已有条件语句代码上增加新代码时不容易出错;
- 对于在if语句中使用函数式宏时,有大括号保护不易出错(如果宏定义时遗漏了大括号)
if (objectIsNotExist) { // Good:单行条件语句也加大括号
return CreateNewObject();
}
禁止 if/else/else if 写在同一行
条件语句中,若有多个分支,应该写在不同行。
如下是正确的写法:
if (someConditions) {
DoSomething();
...
} else { // Good: else 与 if 在不同行
...
}
下面是不符合规范的案例:
if (someConditions) { ... } else { ... } // Bad: else 与 if 在同一行
10.5 循环语句
循环语句必须使用大括号
和条件表达式类似,我们要求for/while循环语句必须加上大括号,即便循环体是空的,或循环语句只有一条。
for (int i = 0; i < someRange; i++) { // Good: 使用了大括号
DoSomething();
}
while (condition) { } // Good:循环体是空,使用大括号
while (condition) {
continue; // Good:continue 表示空逻辑,使用大括号
}
坏的例子:
for (int i = 0; i < someRange; i++)
DoSomething(); // Bad: 应该加上括号
while (condition); // Bad:使用分号容易让人误解是while语句中的一部分
10.6 switch语句
switch 语句的 case/default 要缩进一层
switch 语句的缩进风格如下:
switch (var) {
case 0: // Good: 缩进
DoSomething1(); // Good: 缩进
break;
case 1: { // Good: 带大括号格式
DoSomething2();
break;
}
default:
break;
}
switch (var) {
case 0: // Bad: case 未缩进
DoSomething();
break;
default: // Bad: default 未缩进
break;
}
10.7 表达式
表达式换行要保持换行的一致性,运算符放行末
较长的表达式,不满足行宽要求的时候,需要在适当的地方换行。一般在较低优先级运算符或连接符后面截断,运算符或连接符放在行末。 运算符、连接符放在行末,表示“未结束,后续还有”。 例:
假设下面第一行已经不满足行宽要求
if ((currentValue > threshold) && // Good:换行后,逻辑操作符放在行尾
someCondition) {
DoSomething();
...
}
int result = reallyReallyLongVariableName1 + // Good
reallyReallyLongVariableName2;
表达式换行后,注意保持合理对齐,或者4空格缩进。参考下面例子
int sum = longVariableName1 + longVariableName2 + longVariableName3 +
longVariableName4 + longVariableName5 + longVariableName6; // Good: 4空格缩进
int sum = longVariableName1 + longVariableName2 + longVariableName3 +
longVariableName4 + longVariableName5 + longVariableName6; // Good: 保持对齐
10.8 变量赋值
多个变量定义和赋值语句不允许写在一行
每行只有一个变量初始化的语句,更容易阅读和理解。
int maxCount = 10;
bool isCompleted = false;
下面是不符合规范的示例:
int maxCount = 10; bool isCompleted = false; // Bad:多个变量初始化需要分开放在多行,每行一个变量初始化
int x, y = 0; // Bad:多个变量定义需要分行,每行一个
int pointX;
int pointY;
...
pointX = 1; pointY = 2; // Bad:多个变量赋值语句放同一行
例外:for 循环头、if 初始化语句(C++17)、结构化绑定语句(C++17)中可以声明和初始化多个变量。这些语句中的多个变量声明有较强关联,如果强行分成多行会带来作用域不一致,声明和初始化割裂等问题。
10.9 初始化
初始化包括结构体、联合体、及数组的初始化
初始化换行时要有缩进,并进行合理对齐
结构体或数组初始化时,如果换行应保持4空格缩进。 从可读性角度出发,选择换行点和对齐位置。
const int rank[] = {
16, 16, 16, 16, 32, 32, 32, 32,
64, 64, 64, 64, 32, 32, 32, 32
};
10.10 指针与引用
指针类型"*"跟随变量名或者类型,不要两边都留有或者都没有空格
指针命名: *靠左靠右都可以,但是不要两边都有或者都没有空格。
int* p = nullptr; // Good
int *p = nullptr; // Good
int*p = nullptr; // Bad
int * p = nullptr; // Bad
例外:当变量被 const 修饰时,“*” 无法跟随变量,此时也不要跟随类型。
const char * const VERSION = "V100";
引用类型"&"跟随变量名或者类型,不要两边都留有或者都没有空格
int i = 8;
int& p = i; // Good
int &p = i; // Good
int*& rp = pi; // Good,指针的引用,*& 一起跟随类型
int *&rp = pi; // Good,指针的引用,*& 一起跟随变量名
int* &rp = pi; // Good,指针的引用,* 跟随类型,& 跟随变量名
int & p = i; // Bad
int&p = i; // Bad
10.11 空格和空行
水平空格应该突出关键字和重要信息,避免不必要的留白
水平空格应该突出关键字和重要信息,每行代码尾部不要加空格。总体规则如下:
- if, switch, case, do, while, for等关键字之后加空格;
- 小括号内部的两侧,加空格;
- 大括号内部两侧有无空格,左右必须保持一致;
- 一元操作符(& * + ‐ ~ !)之后不要加空格;
- 二元操作符(= + ‐ < > * / % | & ^ <= >= == != )左右两侧加空格
- 三目运算符(? :)符号两侧均需要空格
- 前置和后置的自增、自减(++ --)和变量之间不加空格
- 结构体成员操作符(. ->)前后不加空格
- 逗号(,)前面不加空格,后面增加空格
- 对于模板和类型转换(<>)和类型之间不要添加空格
- 域操作符(::)前后不要添加空格
- 冒号(:)前后根据情况来判断是否要添加空格
10.12 函数
函数设计
避免函数过长,函数不超过50行(非空非注释)
函数应该可以一屏显示完 (50行以内),只做一件事情,而且把它做好。
过长的函数往往意味着函数功能不单一,过于复杂,或过分呈现细节,未进行进一步抽象。
例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的bug。 建议将其拆分为更加简短并易于管理的若干函数,以便于他人阅读和修改代码。
每一个函数的结束都有一行破折号,破折号与下面的第一个函数之间留一行空白
/*-----------------------------------------------------------*/
十一、FreeRTos风格指南
- 缩进:缩进使用制表符,一个制表符等于4个空格。
- 注释:注释单行不超过80列,特殊情况除外。不使用C++风格的双斜线(//)注释
- 布局:FreeRTOS的源代码被设计成尽可能的易于查看和阅读。下面的代码片中,第一部分展示文件布局,第二部分展示C代码设计格式。
/* 首先在这里包含库文件... */
#include <stdlib.h>
/* ...然后是FreeRTOS的头文件... */
#include "FreeRTOS.h"
/* ...紧接着包含其它头文件. */
#include "HardwareSpecifics.h"
/* 随后是#defines, 在合理的位置添加括号. */
#define A_DEFINITION ( 1 )
/*
* 随后是Static (文件内部的)函数原型,
* 如果注释有多行,参照本条注释风格---每一行都以’*’起始.
*/
static void prvAFunction( uint32_t ulParameter );
/* 文件作用域变量(本文件内部使用)紧随其后,要在函数体定义之前. */
static BaseType_t xMyVariable.
/* 每一个函数的结束都有一行破折号,破折号与下面的第一个函数之间留一行空白。*/
/*-----------------------------------------------------------*/
void vAFunction( void )
{
/* 函数体在此定义,注意要用大括号括住 */
}
/*-----------------------------------------------------------*/
static UBaseType_t prvNextFunction( void )
{
/* 函数体在此定义. */
}
/*-----------------------------------------------------------*/
/*
* 函数名字总是占一行,包括返回类型。 左括号之前没有空格左括号之后有一个空格,
* 每个参数后面有一个空格参数的命名应该具有一定的描述性.
*/
void vAnExampleFunction( long lParameter1, unsigned short usParameter2 )
{
/* 变量声明没有缩进. */
uint8_t ucByte;
/* 代码要对齐. 大括号占独自一行. */
for( ucByte = 0U; ucByte < fileBUFFER_LENGTH; ucByte++ )
{
/* 这里再次缩进. */
}
}
/*
* for、while、do、if结构具有相似的模式。这些关键字和左括号之间没有空格。
* 左括号之后有一个空格,右括号前面也有一个空格,每个分号后面有一个空格。
* 每个运算符的前后各一个空格。使用圆括号明确运算符的优先级。不允许有0
* 以外的数字(魔鬼数)出现,必要时将这些数字换成能表示出数字含义的常量或
* 宏定义。
*/
for( ucByte = 0U; ucByte < fileBUFFER_LENGTH; ucByte++ )
{
}
while( ucByte < fileBUFFER_LENGTH )
{
}
/*
* 由于运算符优先级的复杂性,我们不能相信自己对运算符优先级时刻保持警惕
* 并能正确的使用,因此对于多个表达式运算时,使用括号明确优先级顺序
*/
if( ( ucByte < fileBUFFER_LENGTH ) && ( ucByte != 0U ) )
{
ulResult = ( ( ulValue1 + ulValue2 ) - ulValue3 ) * ulValue4;
}
/* 条件表达式也要像其它代码那样对齐。 */
#if( configUSE_TRACE_FACILITY == 1 )
{
/* 向TCB增加一个用于跟踪的计数器. */
pxNewTCB->uxTCBNumber = uxTaskNumber;
}
#endif
/*方括号前后各留一个空格*/
ucBuffer[ 0 ] = 0U;
ucBuffer[ fileBUFFER_LENGTH - 1U ] = 0U;
文章是自己总结而记录,有些知识点没说明白的,请各位看官多多提意见,多多交流,欢迎大家留言
如果技术交流可以加以下群,方便沟通
QQ群:370278903
点击链接加入群聊【蜡笔小芯的嵌入式交流群】