006 单片机嵌入式中的C语言与代码风格规范——常识
00 环境准备:
配置MDK支持C99
内置stdint.h介绍
stdint.h 是从 C99 中引进的一个标准 C 库的文件
路径:D:\MDK\ARM\ARMCC\include
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)
\' 单引号(撇号)
\" 双引号
\\ 代表一个反斜线字符''\' 等,详细可百度“转义字符”。
建议使用 do{ ... }while(0) 来 构造宏定义 这样不会受到大括号、分号、运算符优先级等的影响,总是会按你期望的方式调用运行!
3.条件编译
与普通if的区别:让编译器只对满足条件的代码进行编译,不满足条件的不参与编译!
应用:
头文件
#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声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错
系统将总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问
03 代码规范
一般的开发团队,总是会对C 语言代码规范和风格做出约束,方便代码维护和团队协助
代码规范整体
排版格式和注释
代码缩进
统一规定:TAB 键为 4 个字符
1.在一些关键字后面要添加空格,如if、swich、case、for、do、while
但是不要在 sizeof 、 typedof 、 alignof 或者 __attribute__ 这些关键字后面添加空格,因为这些大多数后面都会跟着小括号,因此看起来像个函数
如:s = sizeof(struct file);
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;
}
}
代码行相关规范
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));
应改为:
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 (((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 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 相关中断 */
命名规则
变量、函数命名
注意事项:
int book_number;
具有互斥意义的变量或者动作相反的函数应该是用互斥词组命名,如:
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
函数功能
int system_is_up(void)
{
return system_state == SYSTEM_RUNNING;
}
EXPORT_SYMBOL(system_is_up);
8、源文件范围内定义和声明的函数,除非外部可见,否则都应该用 static 函数
如果一个函数只在同一个文件的其它地方调用,那么就应该用 static,static 确保这个函数只在声明它的文件是可见的,这样可以避免和其它库中相同标识符的函数或变量发生混淆
变量功能
int time;
time = 200; /* 表示时间 */
time = getvalue(); /* 用作返回值 */
int time,ret;
time = 200;
ret = getvalue();
局部变量和全局变量重名会容易使人误解
/* 不可取的初始化:无意义 * /
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;