2.1嵌入式C语言开发技巧
前言¶
这里首先介绍在嵌入式平台C语言的相关技巧和语法特性,利用好这些可以大大提高程序的可维护性和可读性。然后再介绍嵌入式程序常用的程序设计架构,使得读者能够站在更高的角度看待整个飞控系统的架构。
语法特性¶
位操作¶
位操作在嵌入式C语言代码中经常出现,尤其是在对寄存器进行直接操作时。比如:
-
在不改变其他位的值的情况下,对某几个位设值
这种操作在嵌入式开发中经常使用,方式就是 先对需要置值的位用"&"操作符清零,然后用 "|"操作符设值,比如要改变GPIOA的状态,可以先对寄存器内的值进行清零操作:
1
GIOA->CRL &= 0XFFFFFF0F // 将4~7位清零
然后再将其与需要设置的值进行"|"运算:
1
GPIOA->CRL |= 0X00000040;
-
移位操作提高代码可读性
移位操作在嵌入式开发同样非常重要,比如下面这行代码:
1
GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
这个操作就是将BSRR寄存器的第pinpos位设置为1,当然也可以通过直接置值的方式,比如:
1
GPIOx->BSRR = 0X0030;
上面介绍的两种代码的写法所实现的功能是完全一样的,但是第一种写法更好,这是因为可以很直接地知道 这个操作置的是BSRR寄存器的哪个功能位,这样通过查看微控制器的寄存器手册就可以知道这个操作所实现的功能是什么了。而第二种写法则还需要一个转换的过程。
-
取反操作
定时器TIMx的SR寄存器的每一位都表示一个外设的状态,如果某个时刻想要去置某一位为0,同时保留其他位为1,则简单的方式是给寄存器直接置值:
1
TIMx->SR = 0XFFFE;
这行代码的功能是将SR寄存器的第0位设置为0,但是这种写法的可读性比较差,一个更好的写法是:
1
TIMx->SR = (uint16_t) ~TIM_FLAG;
而 TIM_FLAG 是通过宏定义得到的:
1 2
#define TIM_FLAG_Update ((uint16_t) 0X0001) #define TIM_FLAG_CC1 ((uint16_t) 0X0002)
这样通过取反的方式就可以看出操作的目的就是将第0位置零。
宏定义¶
define是C语言中的预处理命令,正确使用 define 命令可以有效提高代码的可读性,常见的格式为:
1 | #define 标识符 字符串
|
条件编译¶
在嵌入式开发过程中经常会遇到:当满足某个条件时才对一组语句进行编译,而当条件不满足时则编译另一组语句,常见的格式为:
1 2 3 4 5 | #ifdef 标识符 程序1 #else 程序2 #endif |
格式也可以是:
1 2 3 | #ifdef 程序1 #endif |
外部声明¶
C语言中的extern声明可以置于变量或函数前,以表示变量或函数的定义在别的文件中,提示编译器遇到此变量或函数时在其他文件中去寻找其定义。需要注意的是,可以在多个文件中进行extern声明,但是只能定义一次。而且编译器默认头文件中变量和函数的声明都是 "外部声明的",即看做是进行了 隐式extern声明,不需要额外进添加extern标识符,但是为了代码的风格统一,建议加上。
比如在某个
类型重定义¶
typedef 标识符用于为现有的类型创建一个新的别名,用于简化变量定义或使得变量名具有更好的可读性。比如:
1 2 3 4 5 6 7 | struct _GPIO { __IO uint32_t CRL; __IO uint32_t CRH; __IO uint32_t IDR; ... } |
定义了一个结构体_GPIO,这样的话如果要定义该类型变量的代码是:
1 | struct _GPIO GPIOA; |
这样虽然也可以,但是使用起来比较繁琐,而且结构体名 _GPIO 也没有具体的意义,这样如果定义_GPIO的别名GPIO_TypeDef,代码如下:
1 2 3 4 5 6 7 | struct _GPIO { __IO uint32_t CRL; __IO uint32_t CRH; __IO uint32_t IDR; ... } GPIO_TypeDef; |
这样的话定义该类型变量的代码可以改成:
1 | GPIO_TypeDef GPIOA; |
不仅可以少写一个 "struct",而且结构体名 GPIO_TypeDef 一看就知道其意义是 "GPIO口的类型定义",使可读性增加。