嵌入式c语言学习技巧及特点:
•位屏蔽(Bit-mask)法位运算
位屏蔽法位运算是一种使用二进制掩码配合算术逻辑运算(与运算“&”、运算“|”、非运算“~”和异或运算“^”)进行的位运算。这种方法不存在大/小端系统兼容性问题。
•位域(Bit-field)法位运算
位域法位运算是一种使用位域配合算术逻辑运算进行位运算的方法。其使用收到存储器大/小端排列方式以及内存地址对其方式的影响,兼容性较差。
•标准变量类型
由于C的基本变量类型并没有规定int型变量的长度,因此,在某些平台中int型变量长度为16位、某些平台是32位。为了解决C语言代码移植时int型变量宽度问题带来的不确定性,ANSI-C在C99标准中加入了一个头文件stdint.h用于指定对应平台下int型变量的长度。例如,uint16_t表示长度为16位的无符号整数;int8_t表示长度为8位的有符号整数。具体细节请参考文档:ISO/IEC 9899:1999 (E) 。
•在线汇编
嵌入式C语言中为了实现某些底层硬件的操作而在C语言源代码中加入对局部使用汇编语言的支持称为在线汇编。
•基本数据类型的标准定义
在不同字长的微处理器中整型变量int往往具有不同的长度,例如在AVR单片机8位系统中int型变量的长度为16位二进制;在32位微处理器AVR32中int型变量长度为32位。在某些编译器中char型变量默认是有符号的,在某些系统中char型变量是无符号的,可以表示0~255之间的任意整数。为了解决不同平台间代码的移植问题,与ANSI-C99标准兼容的编译器都会为专门的处理器平台提供了一个标准的库函数stdint.h。该头文件为我们定义了常用的基本变量类型:
1 /* 针对AVR单片机8位系统的专属的stdint.h */
2 typedef unsigned char uint8_t;
3 typedef signed char int8_t;
4 typedef unsigned short uint16_t;
5 typedef signed short int16_t;
6 typedef unsigned long uint32_t;
7 typedef signed long int32_t;
8 ……
编写程序时,我们应该统一使用C99标准规定的标准数据类型以提高代码在不同平台间的可移植性。
•自定义布尔类型
ANSI-C中并没有定义布尔型变量(Boolean),我们可以利用unsigned char型变量自行进行定义,以下定义方式是嵌入式系统中常见的一种:
1 /* 自定义的布尔型变量 */
2 typedef unsigned char BOOL;
3 /* 定义布尔型常量 */
4 # define TRUE 0x01
5 # define FALSE 0x00
根据布尔型变量的精确定义,FALSE应该为“0”值;TRUE应该为非“0”。非“0”值可以是“1”但也可以是其它任何的非“0”整数,因此将TRUE直接定义为“1”是不妥当的。另一种推荐的定义方式如下:
1 /* 定义布尔型常量 */
2 # define FALSE 0x00
3 # define TRUE (!FALSE)
注意,正因为存在上面两种不确定的定义方式,为了避免带来歧义尽可能不要在逻辑表达式中与TRUE进行“==”或“!=”的判断。下面的代码是非常不妥当的:
1 /* 判断布尔型变量 bFlag 是否为TRUE */
2 if (bFlag == TRUE)
3 {
4 ……
5 }正确的写法应为:
1 if (bFlag != FALSE)
2 {
3 ……
4 }或者简写为:
1 if (bFlag)
2 {
3 ……
4 }有时候我们会在别人的代码中看到类似下面的代码:
1 if (FALSE == bFlag)
2 {
3 ……
4 }这种方法可以有效防止将“==”误写为“=”,因为表达式的左边是一个常量,如果错误发生,编译器会提示:“lvalue required”,我们就能发现这一误写的逻辑表达式。
•掩码位操作
不同的处理器在内存的对齐方式上可能存在大端对齐(Big-endian)和小端对齐(Little-endian)的差别。使用位域进行位运算时,随着对齐方式的不同运算的结果也不同,例如,对于下面的位域,大端对齐和小端对齐中产生的结果是完全不同的;
1 /* 定义一个联合体,用于观察结果 */
2 union
3 {
4 struct
5 {
6 unsigned BIT0 : 1;
7 }LSB;
8 uint8_t Value;
9 }Example;
10 /* 测试代码 */
11 Example.Value = 0x00; /* 对变量进行初始化 */
12 Example.LSB.BIT0 = 1; /* 尝试将BIT0设置为1 */
13 printf( “Value = %x”,Example.Value); /* 以HEX方式输出Example的值 */在小端对齐的系统中,Example.Value的值为0x01;而在大端对齐的系统中,Example.Value的值为0x80,可见使用位域进行位运算时可能存在歧义,因而不适合代码的跨平台复用。使用掩码位运算则不存在这一问题,例如:
1 /* 定义一个位掩码 */
2 # define MASK_BIT0 0x01
3 /* 定一个侧使用的变量 */
4 uint8_t Example = 0x00;
5 /* 测试代码,将LSB设置为1 */
6 Example |= MASK_BIT0; /* 将LSB设置为1 */
7 printf(“ Value = %x”,Example); /* 输出测试结果 */通过测试可以发现,无论系统是大端对齐还是小端对齐方式,Example的值都为0x01。关于大端对齐和小端对齐的具体内容请参考第十二章存储器与指针。常见的掩码位运算有置位、清零、取反和取数四种。置位运算通常配合“|=”实现仅针掩码指定位进行置位操作,例如:
1 /* 将BIT0设置为1 */
2 Example |= MASK_BIT0; /* 仅仅将BIT0设置为1 */清零运算通常配合“&=”并将掩码取反后实现将指定二进制位进行清零操作,例如:
1 /* 将BIT0清零*/
2 Example &= ~MASK_BIT0; /* 仅仅将BIT0清零 */取反运算通常配合“^=”实现将指定二进制位进行取反操作,例如:
1 /* 将BIT0取反 */
2 Example ^= MASK_BIT0; /* 仅仅将BIT0取反 */取数运算用于从指定的变量中单独取出指定的一个或一些二进制位,例如:
1 /* 判断BIT0的值是否为0 */
2 if ((Example & MASK_BIT0) == 0) /* 从Example中提取BIT0 */
3 {
4 ……
5 }用于位运算的掩码通常有两种方式产生:一种是事先定义好指定的宏,并通过宏的命名为指定的掩码一个明确的意义,很多针对寄存器操作的掩码采用的都是这种方式;另一种是通过参数宏的方式在需要的时候临时定义掩码。例如:
1 /* 使用参数宏的方式来产生掩码 */
2 # define _BV(__VALUE) (1 << (__VALUE))
3 另外一种实现同样功能的参数宏:
4 # define BIT(__VALUE) (1 << (__VALUE))使用时,在_BV()或BIT()中填入的整数,用于产生针对整数所指定二进制位的掩码,例如BIT(0)等效于针对BIT0的掩码,其值为0x01。
•代码尺寸优化
1.在不影响功能的前提下,尽可能提供编译器的优化等级;
2.尽可能使用局部变量,局部静态变量不在此列;
3.尽可能使用小的数据类型,尽可能使用unsigned来提高小数据类型的整数表达上限;
4.如果一个非局部变量仅仅被一个函数使用,应该将该变量声明为局部静态变量;
5.尽可能将多个全局变量或局部静态变量分类组合成结构体,以增加使用指针进行间接寻址的机会,提高访问效率;
6.使用指针加偏移量的方法或者结构体地址绑定的方法访问应设在I/O空间的存储器。
7.对于非映射的I/O地址,尽可能直接访问。
8.如果可能,请使用do{}while()结构。
9.如果可能,请使用递减型while()循环,并且使自减运算“--”优先于逻辑判断进行;
10.当所调用的函数只有2~3句代码时,可以使用宏定义的方法来实现内联函数;
11.对于常用的功能,尽可能的封装为函数;
12.极端情况下,如果编译器支持,可以逐个函数的设置优化等级;
13.如果可能,尽量不要在中断处理程序中调用其它函数;
14.在某些编译器中,使用小内存模式。
•RAM尺寸优化
1.所有常量和字符串都应该放置在FLASH中;
2.尽可能避免使用全局变量和局部静态变量;
3.在函数内部,使用中括号来限制某些大数据类型的有效范围;
4.正确的评估软件堆栈和硬件堆栈的大小;