1 嵌入式C措辞总结
从语法上来说C措辞并不繁芜, 但编写优质可靠的嵌入式C程序并非易事,不仅须要熟知硬件特性和毛病,还须要对编译事理和打算机技能知识有着一定的理解。在这么多年的嵌入式开拓中,我也积累了一些这方面的履历和思考,就希望总结下来,系统的阐述嵌入式C措辞的主要知识点,便是这篇文章的由来。本文以自己在嵌入式上的实践为根本,在结合干系资料, 阐述嵌入式须要理解的C措辞知识和重点,希望每个读到这篇文章的人都能有所收成。
1. 关键字

关键字是C措辞中具有分外功能的保留标示符,按照功能可分为
1). 数据类型(常用char, short, int, long, unsigned, float, double)
2). 运算和表达式( =, +, -, , while, do-while, if, goto, switch-case)
3). 数据存储(auto, static, extern,const, register,volatile,restricted),
4). 构造(struct, enum, union,typedef),
5). 位操作和逻辑运算(<<, >>, &, |, ~,^, &&),
6). 预处理(#define, #include, #error,#if...#elif...#else...#endif等),
7). 平台扩展关键字(__asm, __inline,__syscall)
这些关键字共同构成了嵌入式平台的C语法。
嵌入式的运用从逻辑上可以抽象为三个部分:
1). 数据的输入(如传感器,旗子暗记,接口输入),
2). 数据的处理(如协议的解码和封包,AD采样值的转换等)
3). 数据的输出(GUI的显示,输出的引脚状态,DA的输出掌握电压,PWM波的占空比等),
对付数据的管理就贯穿着全体嵌入式运用的开拓,它包含数据类型,存储空间管理,位和逻辑操作,以及数据构造,C措辞从语法上支撑上述功能的实现,并供应相应的优化机制,以应对嵌入式下更受限的资源环境。
2 数据类型
C措辞支持常用的字符型,整型,浮点型变量,有些编译器如keil还扩展支持bit(位)和sfr(寄存器)等数据类型来知足分外的地址操作。C措辞只规定了每种基本数据类型的最小取值范围,因此在不同芯片平台上相同类型可能占用不同长度的存储空间,这就须要在代码实现时考虑后续移植的兼容性,而C措辞供应的typedef便是用于处理这种情形的关键字,在大部分支持跨平台的软件项目中被采取,范例的如下:
typedef unsigned char uint8_t;typedef unsigned short uint16_t;typedef unsigned int uint32_t;......typedef signed int int32_t;
既然不同平台的基本数据宽度不同,那么如何确定当前平台的根本数据类型如int的宽度,这就须要C措辞供应的接口sizeof,实现如下。
printf(\公众int size:%d, short size:%d, char size:%d\n\"大众, sizeof(int), sizeof(char), sizeof(short));
这里还有主要的知识点,便是指针的宽度,如
char p;printf(\"大众point p size:%d\n\"大众, sizeof(p));
实在这就和芯片的可寻址宽度有关,如32位MCU的宽度便是4,64位MCU的宽度便是8,在有些时候这也是查看MCU位宽比较大略的办法。
3.内存管理和存储架构
C措辞许可程序变量在定义时就确定内存地址,通过浸染域,以及关键字extern,static,实现了风雅的处理机制,按照在硬件的区域不同,内存分配有三种办法(节选自C++高质量编程):
1). 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的全体运行期间都存在。例如全局变量,static 变量。
2). 在栈上创建。在实行函数时,函数内局部变量的存储单元都可以在栈上创建,函数实行结束时这些存储单元自动被开释。栈内存分配运算内置于处理器的指令集中 ,效率很高,但是分配的内存容量有限。
3). 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己卖力在何时用 free 或 delete 开释内存。动态内存的生存期由程序员决定,利用非常灵巧,但同时碰着问题也最多。
这里先看个大略的C措辞实例。
//main.c#include <stdio.h>#include <stdlib.h>static int st_val; //静态全局变量 -- 静态存储区int ex_val; //全局变量 -- 静态存储区int main(void){ int a = 0; //局部变量 -- 栈上申请 int ptr = NULL; //指针变量 static int local_st_val = 0; //静态变量 local_st_val += 1; a = local_st_val; ptr = (int )malloc(sizeof(int)); //从堆上申请空间 if(ptr != NULL) { printf(\公众p value:%d\"大众, ptr); free(ptr); ptr = NULL; //free后须要将ptr置空,否则会导致后续ptr的校验失落效,涌现野指针 } }
C措辞的浸染域不仅描述了标识符的可访问的区域,实在也规定了变量的存储区域,在文件浸染域的变量st_val和ex_val被分配到静态存储区,个中static关键字紧张限定变量能否被其它文件访问,而代码块浸染域中的变量a, ptr和local_st_val则要根据类型的不同,分配到不同的区域,个中a是局部变量,被分配到栈中,ptr作为指针,由malloc分配空间,因此定义在堆中,而local_st_val则被关键字限定,表示分配到静态存储区,这里就涉及到主要知识点,static在文件浸染域和代码块浸染域的意义是不同的:在文件浸染域用于限定函数和变量的外部链接性(能否被其它文件访问), 在代码块浸染域则用于将变量分配到静态存储区。
对付C措辞,如果理解上述知识对付内存管理基本就足够,但对付嵌入式C来说,定义一个变量,它不一定在内存(SRAM)中,也有可能在FLASH空间,或直接由寄存器存储(register定义变量或者高优化等级下的部分局部变量),如定义为const的全局变量定义在FLASH中,定义为register的局部变量会被优化到直接放在通用寄存器中,在优化运行速率,或者存储受限时,理解这部分知识对付代码的掩护就很故意义。此外,嵌入式C措辞的编译器中会扩展内存管理机制,如支持分散加载机制和__attribute__((section(\公众用户定义区域\公众))),许可指定变量存储在分外的区域如(SDRAM, SQI FLASH), 这强化了对内存的管理,以适应繁芜的运用环境场景和需求。
LD_ROM 0x00800000 0x10000 { ;load region size_region EX_ROM 0x00800000 0x10000 { ;load address = execution address .o (RESET, +First) (InRoot$$Sections) .ANY (+RO) } EX_RAM 0x20000000 0xC000 { ;rw Data .ANY (+RW +ZI) } EX_RAM1 0x2000C000 0x2000 { .ANY(MySection) } EX_RAM2 0x40000000 0x20000{ .ANY(Sdram) }}int a[10] __attribute__((section(\"大众Mysection\公众)));int b[100] __attribute__((section(\公众Sdram\"大众)));
采取这种办法,我们就可以将变量指定到须要的区域,这在某些情形下是必须的,如做GUI或者网页时由于要存储大量图片和文档,内部FLASH空间可能不敷,这时就可以将变量声明到外部区域,其余内存中某些部分的数据比较主要,为了避免被其它内容覆盖,可能须要单独划分SRAM区域,避免被误修正导致致命性的缺点,这些履历在实际的产品开拓中是常用且主要,不过由于篇幅缘故原由,这里只简单的供应例子,如果事情中碰着这种需求,建议详细去理解下。
至于堆的利用,对付嵌入式Linux来说,利用起来和标准C措辞同等,把稳malloc后的检讨,开释后记得置空,避免\公众野指针“,不过对付资源受限的单片机来说,利用malloc的场景一样平常较少,如果须要频繁申请内存块的场景,都会构建基于静态存储区和内存块分割的一套内存管理机制,一方面效率会更高(用固定大小的块提前分割,在利用时直接查找编号处理),另一方面对于内存块的利用可控,可以有效避免内存碎片的问题,常见的如RTOS和网络LWIP都是采取这种机制,我个人习气也采取这种办法,以是关于堆的细节不在描述,如果希望理解,可以参考<C Primer Plus>中关于存储干系的解释。
4. 指针和数组
数组和指针每每是引动身序bug的紧张缘故原由,如数组越界,指针越界,造孽地址访问,非对齐访问,这些问题背后每每都有指针和数组的影子,因此理解和节制指针和数组,是成为合格C措辞开拓者的必经之路。
数组是由相同类型元素构成,当它被声明时,编译器就根据内部元素的特性在内存等分配一段空间,其余C措辞也供应多维数组,以应对分外场景的需求,而指针则是供应利用地址的符号方法,只有指向详细的地址才故意义,C措辞的指针具有最大的灵巧性,在被访问前,可以指向任何地址,这大大方便了对硬件的操作,但同时也对开拓者有了更高的哀求。参考如下代码:
int main(void){ char cval[] = \"大众hello\"大众; int i; int ival[] = {1, 2, 3, 4}; int arr_val[][2] = {{1, 2}, {3, 4}}; const char pconst = \"大众hello\"大众; char p; int pi; int pa; int par; p = cval; p++; //addr增加1 pi = ival; pi+=1; //addr增加4 pa = arr_val[0]; pa+=1; //addr增加4 par = arr_val; par++; //addr增加8 for(i=0; i<sizeof(cval); i++) { printf(\"大众%d \"大众, cval[i]); } printf(\"大众\n\"大众); printf(\"大众pconst:%s\n\公众, pconst); printf(\公众addr:%d, %d\n\公众, cval, p); printf(\"大众addr:%d, %d\n\"大众, icval, pi); printf(\"大众addr:%d, %d\n\"大众, arr_val, pa); printf(\公众addr:%d, %d\n\公众, arr_val, par);}/ PC端64位系统下运行结果0x68 0x65 0x6c 0x6c 0x6f 0x0pconst:helloaddr:6421994, 6421995addr:6421968, 6421972addr:6421936, 6421940addr:6421936, 6421944 /
对付数组来说,一样平常从0开始获取值,以length-1作为结束,通过[0, length)半开半闭区间访问,这一样平常不会出问题,但是某些时候,我们须要倒着读取数组时,有可能缺点的将length作为起始点,从而导致访问越界,其余在操作数组时,有时为了节省空间,将访问的下标变量i定义为unsigned char类型,而C措辞中unsigned char类型的范围是0~255,如果数组较大,会导致数组超过时无法截止,从而陷入去世循环,这种在最初代码构建时很随意马虎避免,但后期如果变动需求,在加大数组后,在利用数组的其它地方都会有隐患,须要特殊把稳。
在前面提到过,指针霸占的空间与芯片的寻址宽度有关,32位平台为4字节,64位为8字节,而指针的加减运算中的长度又与它的类型干系,如char类型为1,int类型为4,如果你仔细不雅观察上面的代码就会创造par的值增加了8,这是由于指向指针的指针,对应的变量是指针,也便是长度便是指针类型的长度,在64位平台下为8,如果在32位平台则为4,这些知识理解起来并不困难,但是这些特性在工程利用中稍有不慎,就会埋下不易察觉的问题。其余指针还支持逼迫转换,这在某些情形下相称有用,参考如下代码:
#include <stdio.h>typedef struct{ int b; int a;}STRUCT_VAL;static __align(4) char arr[8] = {0x12, 0x23, 0x34, 0x45, 0x56, 0x12, 0x24, 0x53};int main(void){ STRUCT_VAL pval; int ptr; pval = (STRUCT_VAL )arr; ptr = (int )&arr[4]; printf(\公众val:%d, %d\公众, pval->a, pval->b); printf(\"大众val:%d,\"大众, ptr);}//0x45342312 0x53241256//0x53241256
基于指针的逼迫转换,在协议解析,数据存储管理中高效快捷的办理了数据解析的问题,但是在处理过程中涉及的数据对齐,大小端,是常见且十分易错的问题,如上面arr字符数组,通过__align(4)逼迫定义为4字节对齐是必要的,这里可以担保后续转换成int指针访问时,不会触发非对齐访问非常,如果没有逼迫定义,char默认是1字节对齐的,当然这并不便是一定触发非常(由全体内存的布局决定arr的地址,也与实际利用的空间是否支持非对齐访问有关,如部分SDRAM利用非对齐访问时,会触发非常), 这就导致可能增减其它变量,就可能触发这种非常,而出非常的地方每每和添加的变量毫无关系,而且代码在某些平台运行正常,切换平台后触发非常,这种暗藏的征象是嵌入式中很难查找办理的问题。其余,C措辞指针还有分外的用法便是通过逼迫转换给特定的物理地址访问,通过函数指针实现回调,如下:
#include <stdio.h>typedef int (pfunc)(int, int);int func_add(int a, int b){ return a+b;}int main(void){ pfunc func_ptr; (volatile uint32_t )0x20001000 = 0x01a23131; func_ptr = func_add; printf(\"大众%d\n\"大众, func_ptr(1, 2));}
这里解释下,volatile易变的,可变的,一样平常用于以下几种状况:
1)并行设备的硬件寄存器(如:状态寄存器)
2)一个中断做事子程序中会访问到的非自动变量(Non-automatic variables)
3)多线程运用中被几个任务共享的变量
volatile可以办理用户模式和非常中断访问同一个变量时,涌现的不同步问题,其余在访问硬件地址时,volatile也阻挡对地址访问的优化,从而确保访问的实际的地址,精通volatile的利用,在嵌入式底层中十分主要,也是嵌入式C从业者的基本哀求之一。函数指针在一样平常嵌入式软件的开拓中并不常见,但对许多主要的实现如异步回调,驱动模块,利用函数指针就可以利用大略的办法实现很多运用,当然我这里只能说是抛砖引玉,许多细节知识是值得详细去理解节制的。
5.构造类型和对齐
C措辞供应自定义数据类型来描述一类具有相同特色点的事务,紧张支持的有构造体,列举和联合体。个中列举通过别名限定数据的访问,可以让数据更直不雅观,易读,实现如下:
typedef enum {spring=1, summer, autumn, winter }season; season s1 = summer;
联合体的是能在同一个存储空间里存储不同类型数据的数据类型,对付联合体的占用空间,则因此个中占用空间最大的变量为准,如下:
typedef union{ char c; short s; int i; }UNION_VAL; UNION_VAL val; int main(void) { printf(\"大众addr:0x%x, 0x%x, 0x%x\n\"大众, (int)(&(val.c)), (int)(&(val.s)), (int)(&(val.i))); val.i = 0x12345678; if(val.s == 0x5678) printf(\"大众小端模式\n\公众); else printf(\"大众大端模式\n\"大众); } /addr:0x407970, 0x407970, 0x407970 小端模式/
联合体的用场紧张通过共享内存地址的办法,实现对数据内部段的访问,这在解析某些变量时,供应了更为简便的办法,此外测试芯片的大小端模式也是联合体的常见运用,当然利用指针逼迫转换,也能实现该目的,实现如下:
int data = 0x12345678; short pdata = (short )&data; if(pdata = 0x5678) printf(\"大众%s\n\"大众, \"大众小端模式\"大众); else printf(\"大众%s\n\"大众, \"大众大端模式\"大众);
可以看出利用联合体在某些情形下可以避免对指针的滥用。
构造体则是将具有共通特色的变量组成的凑集,比起C++的类来说,它没有安全访问的限定,不支持直接内部带函数,但通过自定义数据类型,函数指针,仍旧能够实现很多类似于类的操作,对付大部分嵌入式项目来说,构造化处理数据对付优化整体架构以及后期掩护大有便利,下面举例解释:
typedef int (pfunc)(int, int); typedef struct{ int num; int profit; pfunc get_total; }STRUCT_VAL; int GetTotalProfit(int a, int b){ return ab; } int main(void){ STRUCT_VAL Val; STRUCT_VAL pVal; Val.get_total = GetTotalProfit; Val.num = 1; Val.profit = 10; printf(\公众Total:%d\n\"大众, Val.get_total(Val.num, Val.profit)); //变量访问 pVal = &Val; printf(\公众Total:%d\n\公众, pVal->get_total(pVal->num, pVal->profit)); //指针访问 } / Total:10 Total:10 /
C措辞的构造体支持指针和变量的办法访问,通过转换可以解析任意内存的数据(如我们之条件到的通过指针逼迫转换解析协议),其余通过将数据和函数指针打包,在通过指针通报,是实现驱动层实接口切换的主要根本,有着重要的实践意义,其余基于位域,联合体,构造体,可以实现另一种位操作,这对付封装底层硬件寄存用具有主要意义,实践如下:
typedef unsigned char uint8_t; union reg{ struct{ uint8_t bit0:1; uint8_t bit1:1; uint8_t bit2_6:5; uint8_t bit7:1; }bit; uint8_t all; }; int main(void){ union reg RegData; RegData.all = 0; RegData.bit.bit0 = 1; RegData.bit.bit7 = 1; printf(\"大众0x%x\n\公众, RegData.all); RegData.bit.bit2_6 = 0x3; printf(\"大众0x%x\n\"大众, RegData.all); } / 0x81 0x8d/
通过联合体和位域操作,可以实现对数据内bit的访问,这在寄存器以及内存受限的平台,供应了简便且直不雅观的处理办法,其余对付构造体的另一个主要知识点便是对齐了,通过对齐访问,可以大幅度提高运行效率,但是由于对齐引入的存储长度问题,也是随意马虎出错的问题,对付对齐的理解,可以分类为如下解释。
根本数据类型:以默认的的长度对齐,如char以1字节对齐,short以2字节对齐等
数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
联合体 :按其包含的长度最大的数据类型对齐。
构造体: 构造体中每个数据类型都要对齐,构造体本身以内部最大数据类型长度对齐
union DATA{ int a; char b; }; struct BUFFER0{ union DATA data; char a; //reserved[3] int b; short s; //reserved[2] }; //16字节 struct BUFFER1{ char a; //reserved[0] short s; union DATA data; int b; };//12字节 int main(void) { struct BUFFER0 buf0; struct BUFFER1 buf1; printf(\"大众size:%d, %d\n\"大众, sizeof(buf0), sizeof(buf1)); printf(\"大众addr:0x%x, 0x%x, 0x%x, 0x%x\n\公众, (int)&(buf0.data), (int)&(buf0.a), (int)&(buf0.b), (int)&(buf0.s)); printf(\"大众addr:0x%x, 0x%x, 0x%x, 0x%x\n\"大众, (int)&(buf1.a), (int)&(buf1.s), (int)&(buf1.data), (int)&(buf1.b)); } / size:16, 12 addr:0x61fe10, 0x61fe14, 0x61fe18, 0x61fe1c addr:0x61fe04, 0x61fe06, 0x61fe08, 0x61fe0c /
个中union联合体的大小与内部最大的变量int同等,为4字节,根据读取的值,就知道实际内存布局和添补的位置是同等,事实上学会通过添补来理解C措辞的对齐机制,是有效且快捷的办法。
6. 预处理机制
C措辞供应了丰富的预处理机制,方便了跨平台的代码的实现,此外C措辞通过宏机制实现的数据和代码块更换,字符串格式化,代码段切换,对付工程运器具有主要意义,下面按照功能需求,描述在C措辞利用中的常用预处理机制。
#include 包含文件命令,在C措辞中,它实行的效果是将包含文件中的所有内容插入到当前位置,这不但包含头文件,一些参数文件,配置文件,也可以利用该文件插入到当前代码的指定位置。个中<>和\公众\公众分别表示从标准库路径还是用户自定义路径开始检索。
#define宏定义,常见的用法包含定义常量或者代码段别名,当然某些情形下合营##格式化字符串,可以实现接口的统一化处理,实例如下:
#define MAX_SIZE 10#define MODULE_ON 1#define ERROR_LOOP() do{\ printf(\公众error loop\n\"大众);\ }while(0);#define global(val) g_##valint global(v) = 10;int global(add)(int a, int b){ return a+b;}
#if..#elif...#else...#endif, #ifdef..#endif, #ifndef...#endif条件选择判断,条件选择紧张用于切换代码块,这种综合性项目和跨平台项目中为了知足多种情形下的需求每每会被利用。
#undef 取消定义的参数,避免重定义问题。
#error,#warning用于用户自定义的告警信息,合营#if,#ifdef利用,可以限定缺点的预定义配置。
#pragma 带参数的预定义处理,常见的#pragma pack(1), 不过利用后会导致后续的全体文件都以设置的字节对齐,合营push和pop可以办理这种问题,代码如下:
#pragma pack(push)#pragma pack(1)struct TestA{ char i; int b;}A;#pragma pack(pop); //把稳要调用pop,否则会导致后续文件都以pack定义值对齐,实行不符合预期等同于 struct _TestB{ char i; int b; }__attribute__((packed))A;
7.总结
如果你看到了这里,那么该当对C措辞有了比较清晰的认识,嵌入式C措辞在处理硬件物理地址,位操作,内存访问,都给予开拓者了充分的自由,通过数组,指针以及逼迫转换的技巧,可以有效减少数据处理中的复制过程,这对付底层是必要的,也方便了全体架构的开拓。但是由这种自由带来的造孽访问,溢出,越界,以及不同硬件平台对齐,数据宽度,大小端问题,在功能设计职员手里一样平常还能够处理,对付后续接手项目的人来说,如果本身的设计没有考虑清楚这些问题,每每代表着问题和麻烦,以是对付任何嵌入式C的从业者,清晰的节制这些根本的知识和必要的。
讲到这里,关于嵌入式C措辞的初步总结就到此为止,但C措辞在嵌入式利用的中的重点和难点并不仅仅只有这些,如嵌入式C措辞支持的内联汇编,通讯间的可靠性实现,存储数据校验和完全性担保,这些工程上的利用和技巧,都很难用大略的言语说清楚,其余有关非常触发后的查找和解决的技巧,也值得详细的解释,这里由于篇幅以及自己还未整理清晰,就先到此为止,后续如果有空闲,我也会进行干系的分享。其余对付本文章提到的知识点,由于篇幅缘故原由,都只进行了大略的描述,没有详细深究内部的事理和更多的利用,如果事情和或者学习中碰着,十分建议从其它资料中研读,其余由于本人的能力有限,以是在本文中理解可能有所疏忽,如果有不理解的地方或者漏掉的地方,也十分欢迎指出,我会客气受教。
学习C/C++的伙伴可以私信回答