让建站和SEO变得简单

让不懂建站的用户快速建站,让会建站的提高建站效率!

财经聚焦

财经聚焦

数媒在线课堂 镶嵌式C话语学问点

2024-12-16

本文发挥镶嵌式开辟中需要了解的C话语学问和重心,但愿每个读到这篇著述的东说念主都能有所成绩。

有东说念主说C话语瑕瑜常陋劣的,也有东说念主说学了十年照旧莫得学显然。事实上,编写优质镶嵌式C门径并非易事,需要了解相关硬件特色和颓势,还需要了解相应地编译旨趣。

一、要津字

险些每一门话语中都筹办键字,具有特殊功能,C话语也不例外,按照功能可分为:

▪ 数据类型(常用char, short, int, long, unsigned, float, double)

▪ 运算和抒发式( =, +, -, *, while, do-while, if, goto, switch-case)

▪ 数据存储(auto, static, extern,const, register,volatile,restricted)

▪ 结构(struct, enum, union,typedef)

▪ 位操作和逻辑运算(<<, >>, &, |, ~,^, &&

▪ 预处理(#define, #include, #error,#if...#elif...#else...#endif等)

▪ 平台推广要津字(__asm, __inline,__syscall)

这些要津字共同组成了镶嵌式平台的C话语语法,镶嵌式的应用从逻辑上不错抽象为以下三个部分:

▪ 数据的输入(如传感器,信号,接口输入)

▪ 数据的处理(如条约的解码和封包,AD采样值的调换等)

▪ 数据的输出(GUI的领悟,输出的引脚状态,DA的输出限定电压,PWM波的占空比等)

通顺在通盘镶嵌式应用开辟的经由中,对数据的管束包含以下几部分:

▪ 数据类型

▪ 存储空间

▪ 位和逻辑操作

▪ 数据结构

为了搪塞镶嵌式开辟中受限的资源环境,C话语从语法上复古上述功能的杀青,并提供相应的优化机制。

二、数据类型

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位宽相比陋劣的方式。

三、内存管束和存储架构

C话语允许门径变量在界说时就详情内存地址,通过作用域,以及要津字extern,static,杀青了详尽的处理机制,按照在硬件的区域不同,内存分拨有三种方式:

▪ 从静态存储区域分拨。内存在门径编译的时刻就仍是分拨好,这块内存在门径的通盘运行时代都存在。例如全局变量,static 变量。

▪ 在栈上创建。在履行函数时,函数内局部变量的存储单位都不错在栈上创建,函数履行末端时这些存储单位自动被开释。栈内存分拨运算内置于处理器的领导王人集 ,遵循很高,然而分拨的内存容量有限。

▪ 从堆上分拨,亦称动态内存分拨。门径在运行的时刻用 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>中对于存储相关的阐述。

四、指针和数组

数组和指针时常是引起门径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 0x0

pconst:hello

addr:6421994, 6421995

addr:6421968, 6421972

addr:6421936, 6421940

addr: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易变的,可变的,一般用于以下几种情状:

▪ 并行诞生的硬件寄存器(如:状态寄存器)

▪ 一个中断管事子门径中会造访到的非自动变量(Non-automatic variables)

▪ 多线程应用中被几个任务分享的变量

volatile不错管束用户模式和很是中断造访并吞个变量时,出现的不同步问题,另外皮造访硬件地址时,volatile也隔断对地址造访的优化,从而确保造访的推行的地址,闪耀volatile的哄骗,在镶嵌式底层中十分清苦,亦然镶嵌式C从业者的基本要求之一。函数指针在一般镶嵌式软件的开辟中并不常见,但对许多清苦的杀青如异步回调,驱动模块,使用函数指针就不错利用陋劣的方式杀青许多应用,固然我这里只可说是投砾引珠,许多细节学问是值得详备去了解掌捏的。

五、结构类型和对王人

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 a*b;

}

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话语的对王人机制,是灵验且快捷的方式。

六、预处理机制

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_##val

int 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;

返回

Powered by 钱江晚报浙江 RSS地图 HTML地图

Copyright Powered by365站群 © 2013-2024