CAN/CANFD
CCP/UDS
Bootloader/OTA
ECU/VCU/FCU
Simulink/ECUCoder
Ethernet
Hardware
Download
上一篇
下一篇
嵌入式C语言进阶参考手册
一.关键字static
-1.static修饰局部变量
-2.static修饰全局变量
-3.static修饰函数
二.关键字const
三.关键字volatile
四.关键字sizeof
-1.指针变量的sizeof
-2.数组的sizeof
-3.结构体的sizeof
五.关键字typedef
-1.基础类型的typedef
-2.结构体的typedef
-3.函数指针的typedef
六.结构体封装函数
七.位操作的应用
-1.清除位
-2.设置位
-3.反转位
-4.检查位
八.高效数学运算
-1.计算平方与立方
-2.用移位代替乘除法
-3.用位操作来求余数
-4.使用查表计算平方根
-5.使用自增、自减运算符
回到顶部
嵌入式C语言进阶参考手册
一.关键字static
-1.static修饰局部变量
-2.static修饰全局变量
-3.static修饰函数
二.关键字const
-1.const修饰函数形参
三.关键字volatile
-1.何时使用volatile
四.关键字sizeof
-1.指针变量的sizeof
-2.数组的sizeof
-3.结构体的sizeof
五.关键字typedef
-1.基础类型的typedef
-2.结构体的typedef
-3.函数指针的typedef
六.结构体封装函数
七.位操作的应用
-1.清除位
-2.设置位
-3.反转位
-4.检查位
八.高效数学运算
-1.计算平方与立方
-2.用移位代替乘除法
-3.用位操作来求余数
-4.使用查表计算平方根
-5.使用自增、自减运算符
回到顶部
# 嵌入式C语言进阶参考手册 ## 一.关键字static
在嵌入式C语言中,关键字static主要有三个作用: ### 1.static修饰局部变量
static修饰的局部变量称为静态局部变量,静态局部变量的作用域与局部变量一样,只在函数内部可以访问,因此还是局部变量。静态局部变量的生命周期与局部变量不同,局部变量在每次调用函数时都会初始化,而静态局部变量只在第一次调用函数时初始化。基于静态局部变量的这个特性,静态局部变量可以用来统计函数被调用的次数,示例: ```c void fcn1(void) { static int no = 0; //do others no++; printf("fcn1函数第%d次被调用。\n", no); } int main() { for (int i = 0; i<10; i++) { fcn1(); } getchar(); return 1; } ``` ### 2.static修饰全局变量
static修饰的全局变量称为全局静态变量,全局静态变量只在本文件内使用,不能跨文件使用。全局静态变量不会被其它文件所访问,其它文件中可以使用相同名字的变量,不会发生冲突。大型C项目中往往有非常多的C源文件,变量重名是常见现象,全局静态变量只在本文件内有效,是大型C项目中非常有用的特性。 ### 3.static修饰函数
static修饰的函数称为静态函数,静态函数只在本文件内使用,不能跨文件使用。静态函数不会被其它文件所访问,其它文件中可以使用相同名字的函数,不会发生冲突。大型C项目中往往有非常多的C源文件,函数重名是常见现象,静态函数只在本文件内有效,是大型C项目中非常有用的特性。 有些C源文件内部包含很多函数,但是其中的大部分函数只在本文件内使用,并不需要被其它文件访问,将不需要被其它文件访问的函数用static修饰为静态函数是一种行之有效的方法,既提高了程序的模块化程度,又改善了代码可读性。 综上,static修饰全局变量与修饰函数产生的效果是类似的,实现了变量与函数的本地化,提高了程序的内聚性。 ## 二.关键字const
关键字const用来声明变量是不能被改变的,const关键字修饰的变量在声明时必须进行初始化,const修饰的变量在声明完成之后可以当作常量使用,常量不能被改变。 ```c const int i = 100; int j = i; //i = 200; //错误,因为i不能被改变 ``` const修饰指针,可以指定指针本身const,也可以指定指针所指的数据const,区别在于const的位置不同。const修饰指针比较晦涩难懂,一般不单独使用而在函数形参中使用。 ### 1.const修饰函数形参
const最常见的应用是修饰函数形参,比如C语言标准库函数: ```c char *strcpy(char* dest, const char* src) ``` 为了保护源字符串src,形参用const限定src所指的内容,如果函数体的语句试图改动src的内容,编译器将报错。 ## 三.关键字volatile
关键字volatile被称作易变关键字,作用是告诉编译器这个变量可能被修改,需要每次都从内存中访问这个变量,达到稳定访问内存。 只要变量可能被意外修改,就需要把该变量声明为volatile,在嵌入式应用中,有三种情况变量可能被意外修改: 1. 外设寄存器地址映射。 2. 在中断服务程序中修改全局变量。 3. 在多线程、多任务应用中,全局变量被多个任务读写。 在以下示例代码(来自于某个处理器厂家)中,__I/__O/__IO是用于修饰所有的寄存器变量的,可以看到本质上都是volatile。 ```c #ifndef __IO #define __I volatile const /*!< Defines 'read only' permissions */ #define __O volatile /*!< Defines 'write only' permissions */ #define __IO volatile /*!< Defines 'read / write' permissions */ #endif ``` ### 1.何时使用volatile
通常,外设寄存器地址映射是处理器/编译器厂家预先写好的,用户可以去查看这些变量的声明都是用volatile修饰的。对于一个自定义的全局变量,如果这个全局变量会在中断服务程序中使用或者这个变量会被不同线程的多个任务使用,就需要把该变量声明为volatile。 如果变量可以排除以上三种情况,则不需要使用volatile修饰。 ## 四.关键字sizeof
关键字sizeof是C语言中的一个操作符,作用是返回一个变量或者类型所占的内存字节数。sizeof的基本用法: ```c char c = 'a'; short s = 1000; int i = 10000; long l = 80000; float f = 3.14; double d = 3.14159265; printf("类型char的字节数:%d\n", sizeof(char)); //输出1 printf("char型变量的字节数:%d\n", sizeof(c)); //输出1 printf("类型short的字节数:%d\n", sizeof(short)); //输出2 printf("类型int的字节数:%d\n", sizeof(int)); //输出4 printf("int型变量的字节数:%d\n", sizeof(i)); //输出4 printf("类型long的字节数:%d\n", sizeof(long)); //输出4 printf("类型float的字节数:%d\n", sizeof(float)); //输出4 printf("float型变量的字节数:%d\n", sizeof(f)); //输出4 printf("类型double的字节数:%d\n", sizeof(double)); //输出8 printf("double型变量的字节数:%d\n", sizeof(d)); //输出8 printf("double型变量的字节数:%d\n\n", sizeof d); //输出8 ``` sizeof的用法非常类似函数,但是sizeof并不是函数,从上述最后一行代码可以看出sizeof并不是函数。 ### 1.指针变量的sizeof
指针变量的sizeof等于计算机地址总线的字节宽度,所以对于32位程序,指针变量的sizeof等于4,对于64位程序,指针变量的sizeof等于8。 ```c int *p1 = &i; double *p2 = &d; printf("指针p1的字节数:%d\n", sizeof(p1)); //4 printf("指针p2的字节数:%d\n\n", sizeof(p2)); //4 ``` ### 2.数组的sizeof
数组的sizeof等于数组所占用的内存字节数: ```c int arr[10] = {0}; char arrc[] = {'A', 'B', 'C', 'D'}; char str2[] = "ABCD"; printf("数组arr的字节数:%d\n", sizeof(arr)); //4*10=40 printf("数组arr的元素数量:%d\n", sizeof(arr)/sizeof(int)); //40/4=10 printf("字符数组arrc的字节数:%d\n", sizeof(arrc)); //4 printf("字符数组str2的字节数:%d\n", sizeof(str2)); //4+1=5 ``` 上述代码中,str2会自动在字符“D”后添加"\0",因此str2的sizeof为4+1=5。 ### 3.结构体的sizeof
结构体的sizeof等于所有结构体成员所占用的内存字节数之和加上内存对齐需要填充的字节之和。枚举的sizeof等于int的sizeof。共用体的sizeof等于共用体中占用字节最多之成员的sizeof。 ```c struct stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在学习小组 float score; //成绩 } stu1, stu2; enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 }; union udata{ int n; char ch[4]; short m[2]; }; printf("结构体stu的字节数:%d\n", sizeof(struct stu)); //4+4+4+4+4=20 printf("枚举week的字节数:%d\n", sizeof(enum week)); //4 printf("共用体udata的字节数:%d\n", sizeof(union udata)); //4 ``` 虽然结构体成员group的sizeof为1,但是由于内存对齐的原因,内存中会自动填充3个字节从而与其它结构体成员内存对齐,所以结构体stu的sizeof等于20而不是等于17。 ## 五.关键字typedef
C语言允许为一个数据类型起一个新的别名,就像给人起“绰号”一样。使用关键字 typedef可以为类型起一个新的别名。typedef的用法一般为: ```c typedef oldName newName; ``` 为类型起别名的目的不是为了提高程序运行效率,而是为了编码方便,提高代码的可读性与可维护性。typedef的常用场合有以下几种: ### 1.基础类型的typedef
基础类型的typedef可以使基础类型看起来更加清晰,示例: ```c typedef int int32; int32 i = 1000; //等效于int i = 1000; ``` 显然,int32比int要更加清晰易用。 ### 2.结构体的typedef
结构体的typedef可以使声明结构体类型变量更加简单,示例: ```c typedef struct stu { char name[20]; int age; char sex; } STU; STU Tongtong; //等效于struct stu Tongtong; ``` 显然,使用别名STU比struct stu要更加简单。 共用体的typedef用法与结构体几乎完全一样,示例: ```c typedef union eedata { unsigned char c[4]; float f; } EEDATA; EEDATA B1; //等效于union eedata B1; ``` 显然,使用别名EEDATA比union eedata要更加简单。 ### 3.函数指针的typedef
函数指针的typedef可以使函数指针的定义更加简单,示例: ```c typedef int32 (*PTR_TO_FUNC)(int32, int32); //typedef int32 (*PTR_TO_FUNC)(); //与上一行代码等效 int32 max(int32 a, int32 b) { return a>b ? a : b; } PTR_TO_FUNC pfcn = max; // 等效于int32 (*pfcn)(int32, int32) = max; ``` 显然,使用别名PTR_TO_FUNC比int32 (*pfcn)(int32, int32)要更加简单。 ## 六.结构体封装函数
在C语言中,结构体不仅可以封装数据,还可以封装函数指针。这种方式可以用于实现回调函数、状态机等,提高代码的复用性和可维护性。特别是在嵌入式当中,应用是非常多的。 使用结构体封装函数可以实现类似面向对象编程语言(比如C++与C#)中类的效果。在嵌入式系统中,结构体封装函数可以用于对于嵌入式硬件资源进行抽象和封装,从而提高软件的可维护性和可移植性。比如可以将硬件驱动函数封装在结构体中,方便对外提供统一的API接口,同时也便于代码的移植和扩展。另外,结构体封装函数还可以用于实现状态机、任务调度等复杂的系统功能。 结构体封装函数示例: ```c typedef struct { int x; int y; void (*move_up)(); void (*move_down)(); void (*move_left)(); void (*move_right)(); } Point; void move_up(Point *p, int steps) { p->y += steps; } void move_down(Point *p, int steps) { p->y -= steps; } void move_left(Point *p, int steps) { p->x -= steps; } void move_right(Point *p, int steps) { p->x += steps; } int main() { Point point = {0, 0, move_up, move_down, move_left, move_right }; point.move_up(&point, 10); point.move_right(&point, 20); printf("当前点的坐标:%d, %d。\n", point.x, point.y); getchar(); return 1; } ``` 在上面的示例代码中,我们定义了一个结构体Point,其中包含了两个整型变量x和y,以及四个函数指针move_up、move_down、move_left和move_right。每个函数指针指向一个移动函数,用于在平面坐标系中移动点的位置。通过使用结构体封装函数,我们可以将函数和数据封装在一起,方便地进行操作和管理。 ## 七.位操作的应用
嵌入式位运算操作是嵌入式系统中常用的优化技巧之一,它可以通过位运算操作来实现一些常见的数学运算、逻辑运算等,从而提高程序的执行效率。 ### 1.清除位
可以使用位运算符将数据中特定的位设置为0(与0进行位与运算)。例如,如果需要清除一个8位数据的最高位,可以使用以下代码: ```c uint8 a = 0xFF; a &= 0x7F; //清除最高位 ``` ### 2.设置位
可以使用位运算符将数据中特定的位设置为1(与1进行位或运算)。例如,如果需要将一个8位数据的第4位设置为1,可以使用以下代码: ```c uint8 b = 0x00; b |= 0x10; //设置第4位 ``` ### 3.反转位
可以使用位运算符将数据中特定的位取反(与1进行位异或运算)。例如,如果需要反转一个8位数据的所有位,可以使用以下代码: ```c uint8 c = 0x00; c ^= 0xFF; //反转所有位 ``` ### 4.检查位
可以使用位运算符检查数据中特定的位是否设置为1或0(与1进行位与运算)。例如,如果需要检查一个8位数据的第2位是否为1,可以使用以下代码: ```c uint8 d = 0x00; if(d & 0x02){ printf("第2位为1\n"); } else{ printf("第2位不为1\n"); } ``` ## 八.高效数学运算
有一些数学运算可以有多种方法来获得计算结果,在嵌入式C语言中,人们总结出了一些高效数学运算方法。 ### 1.计算平方与立方
C语言库函数提供了pow(u, v)函数来计算u的v次幂,但是对于平方与立方计算,使用pow(u, v)函数的效率不高,直接相乘的运算效率要高得多。 ```c uint32 a = 128; uint32 b = 0; b = pow(a, 2); printf("128的平方:%d\n", b); b = a * a; //运算效率高 printf("128的平方:%d\n", b); b = pow(a, 3); printf("128的立方:%d\n", b); b = a * a * a; //运算效率高 printf("128的立方:%d\n", b); ``` ### 2.用移位代替乘除法
对于无符号整型数,用左移代替乘法,用右移代替除法,是一种常见的提高运算效率的方法。由于现代嵌入式计算芯片性能越来越强大,而乘除法又是常用的数学运算,芯片集成硬件乘法器与硬件除法器来加速乘除法计算成为一种主流做法。因此,用移位代替乘除法在性能较弱的嵌入式计算芯片(比如8位单片机)上会有明显效果,而在高性能嵌入式计算芯片上未必有效果。 ### 3.用位操作来求余数
对于无符号整型数,当分母为2的N次方(比如2、4、8、16、32、64、128、256)时,可以使用位操作(与分母减1做与运算)来求余数。 ```c uint32 c = 65535; uint32 d = 0; d = c % 64; printf("65535除以64的余数:%d\n", d); d = c & 0x3F; //运算效率高 printf("65535除以64的余数:%d\n", d); ``` ### 4.使用查表计算平方根
C语言库函数提供了sqrt(u)函数来计算u的平方根,sqrt(u)函数计算量较大,如果u在一个比较有限的范围(比如1~100)内,先将平方根的值预存在一个表格内,然后使用查表来计算平方根是一种更加高效的方法。 ### 5.使用自增、自减运算符
对于绝大部分C编译器,使用自增(i++)运算符的代码效率要比使用先加后赋值(i = i + 1)的代码效率要高,自减运算符同理。 即使使用自增运算符编译后的汇编代码与使用先加后赋值编译后的汇编代码完全一样,自增运算符的C代码更加简短,输入代码更加快捷方便,因此自增/自减运算符得到了广泛应用。