Bootstrap

006 单片机嵌入式中的C语言与代码风格规范——常识

00 环境准备:

配置MDK支持C99

c3b2c9a881eb4261b300b8d970bb5b8a.png

内置stdint.h介绍

stdint.h 是从 C99 中引进的一个标准 C 库的文件(c99是C语言发展路上的一种版本)
路径:D:\MDK\ARM\ARMCC\include

03659c592b8041259e208498d2e0f57b.png

 01 C语言基础语法

一般的bug很有可能是C语言功底不扎实导致……

1.结构体

由若干基本数据类型集合组成的一种自定义数据类型,也叫聚合类型

struct student
{
    char  	*name;		/* 姓名 */
    int     	num;  		/* 学号 */
    int     	age; 			/* 年龄 */
    char	group;  		/* 所在学习小组 */
    float  	score;  		/* 成绩 */
};
struct student stu3,stu4;
stu3.name = "张三";
stu3.num = 1; 
stu3.age = 18; 
stu3.group = 'A';
stu3.score = 80.9;

2.枚举

定义枚举变量:

    enum{FALSE = 0, TRUE = 1} EnumName;

3.指针

指针就是内存的地址,指针变量是保存了地址的变量

char * p_str = “This is a test!”;
*p_str:取p_str 变量的值
&p_str:取p_str变量的地址
uint8_t  buf[5] = {1, 3, 5, 7, 9}; 
uint8_t  * p_buf = buf;
*p_buf = ? 
p_buf[0] = ?
p_buf[1] = ?
p_buf++;
*p_buf = ?
p_buf[0] = ?

指针使用的2大最常见问题:

1,未分配(申请)内存就用,如下:

char * p_buf;
p_buf[0] = 100;

2,越界使用,如下:

int8_t  buf[5] = {1, 3, 5, 7, 9}; 
uint8_t  * p_buf = buf;
p_buf[5] = 200;
p_buf[6] = 250;

02 嵌入式常用语法

1.位操作

运算符

含义

运算符

含义

&

按位与

~

按位取反

|

按位或

<<

左移

^

按位异或

>>

右移

//& 与操作
0 & 0 = 0  
1 & 0 = 0    
1 & 1 = 1
//| 或操作
0 | 0 = 0  
1 | 0 = 1   
1 | 1 = 1
//^ 异或操作
0 ^ 0 = 0  
1 ^ 0 = 1  
0 ^ 1 = 1  
1 ^ 1 = 0
//~ 取反操作
~ 11100100 = 00011011
//>>、<< 移位操作
11100100 << 2 =10010000
11100100 >> 2 =00111001

 举例:给第6位赋值为0
temp &= ~(1<<6);

2.宏定义

是指在编译前对源代码进行文本替换。宏定义本身并不分配内存空间,而是在预处理阶段将宏名在全局作用域中替换为相应的替换列表。
替换不会占用运行时间,只占用编译时间

#define   目标标识符   原替换列表

应用:

#define     PI    3.14159

带参数的宏定义 

#define LED1(x)   do{  x ? \
                      HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_SET) : \
                      HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_RESET); \
                  }while(0)

//上述等效=#define LED1(x)   do{  x ?  HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_SET) :  HAL_GPIO_WritePin(LED1_GPIO_PORT, LED1_GPIO_PIN, GPIO_PIN_RESET);}while(0)

其中“\”表示继续符,用于一行的结尾,表示本行与下一行连接起来 

另外还有:

\n 换行符(LF)

\r 回车(CR) ,相当于键盘上的"Enter"

\t 跳到下一个TAB位置

\0 空字符(NULL)

\' 单引号(撇号)

\" 双引号

\\ 代表一个反斜线字符''\' 等,详细可百度“转义字符”。

#define _LV_CONCAT3(x, y, z) x ## y ## z

 其中“##”表示把两个语言符号组合成单个语言符号

另外还有:

#的功能是将其后面的宏参数进行字符串化操作如:

#define MAKE_STR(s) (#s)
printf("hello\n");
printf(MAKE_STR(hello\n));
输出结果: 
 hello
 hello

建议使用  do{ ... }while(0) 来 构造宏定义 这样不会受到大括号、分号、运算符优先级等的影响,总是会按你期望的方式调用运行!

3.条件编译

与普通if的区别:让编译器只对满足条件的代码进行编译,不满足条件的不参与编译!

2a1db6cc78ff4864aef71f39d28963dc.png

应用:

头文件

#ifndef 	_LED_H
#define 	_LED_H
#include "./SYSTEM/sys/sys.h"
……
#endif

 预编译处理

可以让同一套代码适用于微小变化的开发板或芯片之间

#define  SYS_SUPPORT_OS 1
#if   (SYS_SUPPORT_OS==1)
    ……
#elif  (SYS_SUPPORT_OS==2)
    ……
#else
    ……
#endif

以宏定义的方式控制某些功能开|关

		#if ( ( configUSE_PREEMPTION == 1 ) && ( configIDLE_SHOULD_YIELD == 1 ) )
		{
			/* When using preemption tasks of equal priority will be
			timesliced.  If a task that is sharing the idle priority is ready
			to run then the idle task should yield before the end of the
			timeslice.

			A critical region is not required here as we are just reading from
			the list, and an occasional incorrect value will not matter.  If
			the ready list at the idle priority contains more than one task
			then a task other than the idle task is ready to execute. */
			if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ tskIDLE_PRIORITY ] ) ) > ( UBaseType_t ) 1 )
			{
				taskYIELD();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}

错误提示

#error error-message
#if configMAX_TASK_NAME_LEN < 1
	#error configMAX_TASK_NAME_LEN must be set to a minimum of 1 in FreeRTOSConfig.h
#endif

4.类型别名(typedef)

typedef   原类型   新名字

为现有数据类型创建一个新的名字,或称为类型别名,用来简化变量的定义

应用: 

简化标识符

typedef   unsigned  char 	uint8_t;
typedef struct
{
     __IO uint32_t   CRL;
     __IO uint32_t   CRH;
     …
} GPIO_TypeDef;
GPIO_TypeDef    gpiox
/*原:
Struct GPIO_TypeDef
{
     __IO uint32_t   CRL;
     __IO uint32_t   CRH;
     …
};
Struct    GPIO_TypeDef    gpiox
*/

5.外部声明

extern

放在函数/变量前,表示此函数/变量在其他文件定义,以便本文件引用

应用:

	extern void KeyTask(void *params);
xTaskCreate( KeyTask, "KEYTask", 128, NULL, osPriorityNormal, NULL);

6.关键字

static:

使定义为静态变量,进而不能被其他文件调用,适合大工程防重复作用域

static void ModeShow_Task(void *params)//界面显示任务

__attribute__

 在程序中,当需要指定某个变量的内存地址时,MDK 提供了一个关键字“__attribute__”实现该功能,这种用法通常也是为了把变量指定到外部扩展的存储器,而 sct 文件存储器管理取代或改进了这种地址分配方式。在此处先补充一下关键字“__attribute__”的使用说明

1 /* 定义一个要指定的地址 */
2 #define USER_ADDR ((uint32_t)0x20005000)
3 /* 使用 atribute 指定该变量存储到 USER_ADDR, 这种方式必须定义成全局变量 */
4 uint8_t testValue __attribute__((at(USER_ADDR)));
5 testValue = 0xDD;

这种方式使用“__attribute__((at()))”来指定变量的地址,代码中指定 testValue 存储USER_ADDR地址 0x20005000 中0

volatile:

防止被编译优化

volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错

系统将总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问

const 定义来代替宏

03 代码规范

一般的开发团队,总是会对C 语言代码规范和风格做出约束,方便代码维护和团队协助

代码规范整体

需要做到以下几点:
1 、简单、明了、清晰:
代码写出来重点是给人看的,因此简单、明了、清晰是第一要务
2 、精简
代码越长越难看懂,变量等一定要及时的清理掉!
3 、保持第三方代码风格
司内部代码风格必须做到统一,方便维护,如果有第三方代码允许两种风格并存
4 、减少封装
切忌对第三方代码库进行再封装,不要为了让第三方代码和我们的风格统一,而去修改第三方源码风格,或者重新写一套接口函数以便和我们代码风格统一。不做这个封装的话虽然影响到了代码风格的统一,但是却给学习者减少了困惑

排版格式和注释

代码缩进

统一规定:TAB 键为 4 个字符

1.在一些关键字后面要添加空格,如if、swich、case、for、do、while

但是不要在 sizeof、typedof、alignof 或者__attribute__这些关键字后面添加空格,因为这些
大多数后面都会跟着小括号,因此看起来像个函数
如:
s = sizeof(struct file);
2.如果要定义指针类型,或者函数返回指针类型时,“*”应该放到靠近变量名的一侧,而不是类型名,如:
char *linux_banner;
unsigned long long memparse(char *ptr, char **retptr);
char *match_strdup(substring_t *s);

3.基于二元或三元对象操作符两侧都要有一个空格,例如下面所示操作符:

= + - < > * / % | & ^ <= >= == != ? :

4..一元操作符后不要加空格,自加或者自减的一元操作符、“.”和“->”这两个结构体成员操作符也是一样,如:

& * + - ~ ! sizeof typeof alignof __attribute__ defined ++ --
“.”和“->”

5.注释符“/*”和“*/”与注释内容之间要添加一个空格

6.逗号、分号只在后面添加空格,如:

int a, b, c;
以上举例如下:
规范的写法:

void test_error(datax *p, int num, char baseval)
{
    int x1, x2;
    int t = 0;
     x1 = 32; 
    x2 = 23;
    for (t = 0; t <= num; t++) /* 循环赋值 */
    {
     datax->buf[t] = x1 * (x2 + t) + baseval;
    }
}

代码行相关规范

每一行的代码长度限制在 80 列。如果大于 80 列的话就要分成多行编写,并且在低优先级操作符处划分新行,操作符放在新行之首,划分出的新行要适当进行缩进与上一行代码对齐,如下所示:
1.相对独立的程序块之间、变量说明之后,必须加空行。函数之间,必须加空行
if ((taskno < max_act_task_number)
    && (n7stat_stat_item_valid (stat_item)))
{
        ... /* program code */
}

report_or_not_flag = ((taskno < MAX_ACT_TASK_NUMBER)
                        && (n7stat_stat_item_valid (stat_item))
                        && (act_task_table[taskno].result_data != 0));
2.不要把多个语句放到一行里面,一行只写一条语句,不要在一行里面放置多个赋值语句。如下所示:
应改为:
a=x+y;
b=x-y;

3.if、for、do、while、case、swich、default 等语句单独占用一行。且 if、for、do、while 等 语句的执行语句部分无论多少都要加括号{},当且仅当 while 后为空,可以不加{}

不规范的写法: 
if (p_gpiox->IDR & pinx) return 1; /* pinx 的状态为 1 */
else return 0;                     /* pinx 的状态为 0 */

应改为:
if (p_gpiox->IDR & pinx)
{
    return 1;    /* pinx 的状态为 1 */
}
else
{
     return 0;   /* pinx 的状态为 0 */
}
当 while 语句没有代码的时候,可以不用加“{}”,但是,只要 while 有哪怕 1 条语句,就
必须加“{}”如下所示:
不规范的写法:
while (((RCC->CR & (1 << 17)) == 0) && (retry < 0X7FFF)){ retry++; }

应改为:
while (((RCC->CR & (1 << 17)) == 0) && (retry < 0X7FFF))
{
    retry++;
}

//while 后面没有代码的时候,可以不要“{}”
while ((QUADSPI->SR & (1 << 1)) == 0); /* 等待指令发送完成 */

对于 单片机开发来说,左括号“{”一律新起一行, 且位于程序块开始的同一列

注释风格

让别人一看你的代码就明白其中的含义和用法,但是不要过度注释,不要在函数注释 里解释代码是任何运行的,一般你的函数注释应该告诉别人代码做了什么,而代码注释才是怎么做的。
放弃使用:
// ……… 
/// ………

具体代码的注释:
/* ……… */
函数/文件说明注释格式:
/** 
* ………
* ………
* ……… 
*/
当且仅当屏蔽掉部分功能代码(以便后续调试/修改使用)时,可以使用 // 注释,如下:
 if (timeout == 0)
 {
    //printf("r fifo time out\r\n");
    SDMMC1->ICR = 0X1FE00FFF; /* 清除所有标记 */
     sys_intx_enable(); /* 开启总中断 */
     return SD_DATA_TIMEOUT;
 }

文件信息注释

在文件开始的地方应该对本文件做一个总体的、概括性的注释,比如:文件名(@file)、
作者(@author)、当前版本(@version)、修改日期(data)、简要说明(@brief)、版权声明
(@copyright)和注意事项(@attention)等,注意事项包括:平台、网站、版本修改等内容。
具体风格如下:
/**
****************************************************************************** 
* @file delay.c
* @author 正点原子团队(ALIENTEK)
* @version V1.0
* @date 2020-03-12
* @brief 串口初始化代码(一般是串口 1)
* @copyright Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
******************************************************************************
* @attention
*
* 实验平台:正点原子 STM32H750 开发板
* 在线视频:www.yuanzige.com
* 技术论坛:www.openedv.com
* 公司网址:www.alientek.com
* 购买地址:openedv.taobao.com
*
* 修改说明
* V1.0 20200312
* 第一次发布
*
******************************************************************************
*/
#include "delay.h"

函数的注释

/**
* @brief GPIO 通用设置
* @param p_gpiox: GPIOA~GPIOK, GPIO 指针
* @param pinx: 0X0000~0XFFFF, 引脚位置, 每个位代表一个 IO, 
*        第 0 位代表 Px0, 第 1 位代表 Px1, 依次类推.
*        比如 0X0101, 代表同时设置 Px0 和 Px8.
* @arg SYS_GPIO_PIN0~SYS_GPIO_PIN15, 1<<0 ~ 1<<15
*
* @param mode: 0~3; 模式选择, 设置如下:
* @arg SYS_GPIO_MODE_IN, 0, 输入模式(系统复位默认状态)
* @arg SYS_GPIO_MODE_OUT, 1, 输出模式
* @arg SYS_GPIO_MODE_AF, 2, 复用功能模式
* @arg SYS_GPIO_MODE_AIN, 3, 模拟输入模式
*
* @param otype:0~3; 输出类型选择, 设置如下:
*   @arg SYS_GPIO_MODE_IN, 0, 输入模式(系统复位默认状态)
*   @arg SYS_GPIO_MODE_OUT, 1, 输出模式
*   @arg SYS_GPIO_MODE_AF, 2, 复用功能模式
*   @arg SYS_GPIO_MODE_AIN, 3, 模拟输入模式
*
* @param ospeed:0~3; 输出速度, 设置如下:
*   @arg SYS_GPIO_SPEED_LOW, 0, 低速
*   @arg SYS_GPIO_SPEED_MID, 1, 中速
*   @arg SYS_GPIO_SPEED_FAST, 2, 快速
*   @arg SYS_GPIO_SPEED_HIGH, 3, 高速
*
* @param pupd:0~3: 上下拉设置, 设置如下:
*   @arg SYS_GPIO_PUPD_NONE, 0, 不带上下拉
*   @arg SYS_GPIO_PUPD_PU, 1, 上拉
*   @arg SYS_GPIO_PUPD_PD, 2, 下拉
*   @arg SYS_GPIO_PUPD_RES, 3, 保留
*
* @note: 注意: 在输入模式(普通输入/模拟输入)下, OTYPE 和 OSPEED 参数无效!!
*@retval 无
*/
void sys_gpio_set(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint32_t mode, uint32_t otype, 
uint32_t ospeed, uint32_t pupd)
{
    uint32_t pinpos = 0, pos = 0, curpin = 0;
    …… /* 省略代码 */
}

代码注释

void sys_stm32_clock_init(uint32_t plln, uint32_t pllm, uint32_t pllp, uint32_t pllq)
{
    RCC->CR = 0x00000001; /* 设置 HISON, 开启 RC 振荡,其他位全清零 */
    RCC->CFGR = 0x00000000; /* CFGR 清零 */
    RCC->D1CFGR = 0x00000000; /* D1CFGR 清零 */
    RCC->D2CFGR = 0x00000000; /* D2CFGR 清零 */
    RCC->D3CFGR = 0x00000000; /* D3CFGR 清零 */
    RCC->PLLCKSELR = 0x00000000; /* PLLCKSELR 清零 */
    RCC->PLLCFGR = 0x00000000; /* PLLCFGR 清零 */
    RCC->CIER = 0x00000000; /* CIER 清零, 禁止所有 RCC 相关中断 */

命名规则

变量、函数命名

我们使用大部分软件工程师常用的命名方式(unix 风格):单词用小写,然后每个单词用下划线“_”连接在一起,比如:read_adc1_value()

注意事项:

1、命名一定要清晰!清晰是首位,要使用完整的单词或者大家都知道的缩写,让别人一读
就懂,避免不必要的误会,比如:
int book_number;
2.除了常用的缩写以外,不要使用单词缩写,更不要用汉语拼音!!!
具有互斥意义的变量或者动作相反的函数应该是用互斥词组命名,如:
add/remove   begin/end           create/destroy      insert/delete 
first/last   get/release         increment/decrement put/get add/delete
lock/unlock  open/close          min/max             old/new 
start/stop   next/previous       source/target       show/hide 
send/receive source/destination  copy/paste          up/down

变量命名可以使用 g_、p_开头,来表示该变量是一个:全局变量、指针等

u8、u16、u32 等不再使用,统一改成更为规范的简写:

int8_t /* 8 位有符号 char 型 */
int16_t /* 16 位有符号 short 型 */
int32_t /* 32 位有符号 int 型 */
int64_t /* 64 位有符号 long long 型(对 stm32 来说) */
uint8_t /* 8 位无符号 unsigned char 型 */
uint16_t /* 16 位无符号 unsigned short 型 */
uint32_t /* 32 位无符号 unsigned int 型 */
uint64_t /* 64 位无符号 unsigned long long 型(对 stm32 来说) */

文件命名

文件统一采用小写命名

宏命名

常量宏和枚举标签,一般采用大写定义,特殊情况下可以用小写,对于数值等常量宏定义的命名,如非特殊情况,一般使用大写,单词之间使用下划线“_”连接在一起,比如:

#define PI_ROUNDED 3.14

函数功能

函数要简短而且漂亮、并且只能完成一件事,函数的本地变量数量最好不超过 5-10 个,函数要注意的事项如下:
1 、一个函数只能完成一个功能
如果一个函数实现多个功能的话将会给开发、使用和维护带来很大的困难,因此,在跟函数无关或者关联很弱的代码不要放到函数里面,否则的话会导致函数职责不明确,难以理解和修改.
2 、重复代码提炼成函数
重复的代码给人的直观感受就是啰嗦,明明说一遍别人就能记住的事情,非要说好几遍! 因此一定要消除重复代码,将其提炼成函数。
3 、不同函数用空行隔开
不同的函数使用空行隔开,如果函数需要导出的话,它的 EXPORT*宏应该紧贴在他的结束
大括号下,比如:
int system_is_up(void)
{
    return system_state == SYSTEM_RUNNING;
}
EXPORT_SYMBOL(system_is_up);
4 、函数集中退出方法
如果不需要清理操作的话就直接使用 return 即可
5 、函数嵌套不能过深,新增函数最好不超过 4
函数嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch 等)之间互相包含的深度,嵌套会增加阅读代码时候的脑力,嵌套太深非常不利于阅读!
6 、对函数的参数做合法性检查
函数要对其参数做合法性的检查,比如参数如果有指针类型数据的话如果不做检查,当传入野指针的话就会出错。比如参数的范围不做检查的话可能会传递进来一个超出范围的参数值
7 、对函数的错误返回要做全面的处理
一个函数一般都会提供一些指示错误发生的方法,一般都用返回值来表示,因此我们必须对这些错误标识做处理

 8、源文件范围内定义和声明的函数,除非外部可见,否则都应该用 static 函数
如果一个函数只在同一个文件的其它地方调用,那么就应该用 static,static 确保这个函数只在声明它的文件是可见的,这样可以避免和其它库中相同标识符的函数或变量发生混淆

 变量功能

1 、一个变量只能有一个功能,不能把一个变量当作多用途
一个变量只能有一个特定功能,不能把一个变量作为多用途使用,即一个变量取值不同的
时候其代表的含义也不同,如:
错误示范:
int time;
time = 200; /* 表示时间 */
time = getvalue(); /* 用作返回值 */
上述代码中变量 time 用作时间,但是也用作了函数 getvalue 的返回值。应该改为如下:
int time,ret;
time = 200;
ret = getvalue();
2 、不用或者少用全局变量
单个文件内可以使用 static 修饰的全局变量,这可以为文件的私有变量,全局变量应该是
模块的私有数据,不能作用对外的接口,使用 static 类型的定义,可以防止外部文件对本文件的
全局变量的非正常访问。直接访问其它模块的私有数据,会增强模块之间的耦合关系。
局部变量和全局变量重名会容易使人误解
3 、严禁使用未经初始化的变量作为右值
如果使用变量作为右值的话,在使用前一定要初始化变量,禁止使用未经初始化的变量作
为右值,而且在首次使用前初始化变量的地方离使用的地方越近越好!未初始化变量是 C 和
C++最常见的错误源,因此在变量首次使用前一定要确保正确初始化,如:
/* 不可取的初始化:无意义 * /
int num = 2;
if(a)
{
num = 3;
}
else
{
num=4
}
/* 不可取的初始化:初始化和声明分离 */
int num;
if(a)
{
num = 3;
}
else
{
num=4
}
/* 较好的初始化:使用默认有意义的初始化 */
int num = 3;
if(a)
{
num = 4;
}
/* 较好的初始化:?:减少数据流和控制流的混合 */
int num=a?4:3;
5 、明确全局变量的初始化顺序
系统启动阶段,使用全局变量前,要考虑到全局变量该在什么地方初始化!使用全局变量
和初始化全局变量之间的时序关系一定要分析清楚!
6 、尽量减少不必要的数据类型转换
进行数据类型转换的时候,其数据的意义、转换后的取值等都有可能发生变化,因此尽量
减少不必要的数据类型转换。

;