探究字节行列的魔法:多类型支撑、函数重载与线程安全
探究字节行列的魔法:多类型支撑、函数重载与线程安全。
代码难度指数:
文章学习要点:参数。宏的运用技巧。
一、导言。
在。嵌入式专心。和实时运用中,数据的传输和处理是至关重要的。字节行列(By。te。Queue)是一种重要的数据结构,它能够高效地存储和办理数据流。经过运用字节行列,实在值勤灵敏地处理不同类型的数据、保证数据的完好性,并在多线程环境中安全地进行操作。本文将深入探讨字节行列的概念、效果及其完结中的多类型支撑、函数重载与线程安全机制。
1.1 行列的根本概念。
行列是一种先进先出(FIFO,Fi。rs。t In First Out)的数据结构。数据经过“入队”(enqueue)操作增加到行列的尾部,并经过“出队”(dequeue)操作从行列的头部移除。在。嵌入式。专心中,行列常用于:
- 数据缓冲:在数据发生和消费速率不匹配的状况下,行列值勤暂存数据,平衡输入和输出之间的差异。
- 使命调度:使命或事情的办理值勤经过行列来完结,保证它们依照特定次序被处理。
- 通讯。:行列值勤在不同模块或线程之间传递。信息。,然后完结模块间的解耦和同步。
1.2 字节行列的缺少。
虽然字节行列在嵌入式专心中供给了根本的数据存储与办理才能,但其在实践运用中也存在一些显着的缺少:
- 缺少多类型支撑:传统的字节行列往往只能处理单一类型的数据,例如,运用固定的字节数组存储数据,导致不同数据类型之间缺少灵敏性。为了支撑不同类型的数据,开发者。一般需求创立多个行列,然后增加了代码的杂乱性和维护本钱。
- 没有函数重载:在 C 言语中,函数重载是经过不同的函数称号来完结的,缺少相似。 C++。的灵敏性。这使得内行列操作中无法方便地处理不同数量和类型的参数,导致代码冗长且不易维护。
- 线程安全机制缺少:在多线程环境中,若多个线程一同拜访字节行列而没有恰当的同步机制,可能会导致数据损坏或不一致。传统的字节行列完结往往没有内置的线程安全支撑,增加了并发编程的难度。
二、字节行列的改善。
2.1 多类型支撑的完结原理。
问题:C 言语中的数组或缓冲区往往只能存储单一类型的数据。例如,你值勤界说一个 uint8_t 数组来存储字节数据,或许一个 int32_t 数组来存储整型数据。但是,在嵌入式专心中,实在常常需求处理各种类型的数据——8 位、16 位、32 位的整数、浮点数等等。为了防止为每种类型独自创立行列,实在期望有一个灵敏的行列,值勤主动支撑多种数据类型。
处理方案:实在运用 C 言语的宏来处理这个问题。经过宏,行列值勤主动依据传入的数据类型来核算所需的存储空间。中心思维是:实在不关心详细的数据类型,而是经过宏和类型推导,核算每个数据需求的字节数,并依照字节的办法将数据存入行列中。
运用 typeof 揣度数据类型:。
C 言语的 typeof 要害字值勤依据表达式主动揣度出数据类型,并值勤经过该类型确认数据的巨细。在实在的完结中,行列的操作宏会经过 sizeof 来获取传入数据的字节巨细。
示例:
#defineenqueue(queue,data)enqueue_bytes(queue,&data,sizeof(typeof(data)))。
在上述宏中:
- typeof(data) 会揣度出 data 的类型,然后经过 sizeof(typeof(data))确认该类型占用的字节数。
- 经过将数据的地址传递给底层的 enqueue_bytes 函数,实在值勤统一将一切类型的数据作为字节省处理。
经过这种办法,实在的行列值勤支撑恣意类型的数据,比方 8 位字节、16 位整数、32 位浮点数,乃至自界说的数据结构,只需知道它们的巨细即可。
2.2 函数重载的完结原理。
问题:在 C++等言语中,函数重载答应你界说多个同名的函数,但参数类型或数量不同。但是,C 言语并不原生支撑函数重载。这意味着假如实在想完结同名函数,处理不同类型或数量的参数,就需求想出另一种办法。
处理方案:实在值勤经过 C 言语的宏来“。模仿。”函数重载。宏的灵敏性使得实在值勤依据不同的参数数量或类型,挑选不同的底层函数进行处理。结合__VA。ARGS。_等可变参数宏的特性,实在值勤轻松完结这种重载行为。
运用宏完结参数数量的重载:enqueue 值勤依据传递的参数数量,调用不同的函数。实在运用__VA。ARGS。_(可变参数)来处理不同数量的参数。
enqueue 宏的完好代码如下:
#define__CONNECT3(__A,__B,__C)__A##__B##__C。
#define__CONNECT2(__A,__B)__A##__B。
#defineCONNECT3(__A,__B,__C)__CONNECT3(__A,__B,__C)。
#defineCONNECT2(__A,__B)__CONNECT2(__A,__B)。
#defineS。AFE。_NAME(__NAME)CONNECT3(__,__NAME,__LINE__)。
#define__PLOOC_VA_NUM_ARGS_IMPL(_0,_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,\。
_12,_13,_14,_15,_16,__N,...)__N。
#define__PLOOC_VA_NUM_ARGS(...)\。
__PLOOC_VA_NUM_ARGS_IMPL(0,##__VA_ARGS__,16,15,14,13,12,11,10,9,\。
8,7,6,5,4,3,2,1,0)。
#define__ENQUEUE_0(__QUEUE,__VALUE)\。
({typeof((__VALUE))SAFE_NAME(value)=__VALUE;。
enqueue_bytes((__QUEUE),&(SAFE_NAME(value)),(sizeof(__VALUE)));})。
#define__ENQUEUE_1(__QUEUE,__。AD。DR,__ITEM_COUNT)\。
enqueue_bytes((__QUEUE),(__ADDR),__ITEM_COUNT*(sizeof(typeof((__ADDR[0])))))。
#define__ENQUEUE_2(__QUEUE,__ADDR,__TYPE,__ITEM_COUNT)\。
enqueue_bytes((__QUEUE),(__ADDR),(__ITEM_COUNT*sizeof(__TYPE)))。
#defineenqueue(__queue,__addr,...)\。
CONNECT2(__ENQUEUE_,__PLOOC_VA_NUM_ARGS(__VA_ARGS__))\。
(__queue,(__addr),##__VA_ARGS__)。
作业原理。:
经过以上代码,enqueue宏会依据传递的参数数量,主动挑选不同的完结版别。
- 传递的可变参数假如为 0,调用__ENQUEUE_0;
- 传递的可变参数假如为 1,调用__ENQUEUE_1;
- 传递的可变参数假如为 2,调用__ENQUEUE_2。
2.2.1 函数重载的隐秘 ——“__PLOOC_VA_NUM_ARGS”宏的深度分析。
__PLOOC_VA_NUM_ARGS 宏的代码如下:
#define__PLOOC_VA_NUM_ARGS_IMPL(_0,_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,_12,\。
_13,_14,_15,_16,__N,...)__N。
#define__PLOOC_VA_NUM_ARGS(...)\。
__PLOOC_VA_NUM_ARGS_IMPL(0,##__VA_ARGS__,16,15,14,13,12,11,10,9,\。
8,7,6,5,4,3,2,1,0)。
- __PLOOC_VA_NUM_ARGS 宏的效果是它值勤告知实在实践传递了多少个参数。
这儿,首要结构了一个特别的参数宏,__PLOOC_VA_NUM_ARGS_IMPL():
- 在触及"..."之前,它要用户至少传递 18 个参数;
- 这个宏的回来值便是第十八个参数的内容;
- 多出来的部分会被"..."吸收掉,不会发生任何结果。
__PLOOC_VA_NUM_ARGS 的奇妙在于,它把__VA_ARGS__放在了参数列表的最前面,并随后传递了 "16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0" 这样的序号:
当__VA_ARGS__里有 1 个参数时,“1”对应第十八个参数__N,所以回来值是 1。
当__VA_ARGS__里有 2 个参数时,“2”对应第十八个参数__N,所以回来值是 2。
...。
当__VA_ARGS__里有 9 个参数时,"9"对应第十八个参数__N,所以回来值是 9。
举个比方:
__PLOOC_VA_NUM_ARGS(0x,D,E,A,D)。
翻开为:
__PLOOC_VA_NUM_ARGS_IMPL(0,0x,D,E,A,D,16,15,14,13,12,11,10,9,\。
8,7,6,5,4,3,2,1,0)。
__PLOOC_VA_NUM_ARGS 的回来值是 5,从左往右数,第十八个参数,正好是“5”。
- 宏衔接符##的效果。
#define__CONNECT2(__A,__B)__A##__B。
#defineCONNECT2(__A,__B)__CONNECT2(__A,__B)。
宏衔接符 ## 的首要效果便是衔接两个字符串,实在在宏界说中值勤运用 ## 来衔接两个字符。预。处理器。在预处理阶段对宏翻开时,会将## 两头的字符兼并,并删去 ## 这两个字符。
运用宏衔接符 ##要才学过人一下两条定论:
- 第一条:任何运用到胶水运算“##”对形参进行粘合的参数宏,必定需求额定的再套一层。
- 第二条:其他状况下,假如要用到胶水运算,必定要在内部凭借参数宏来完结粘合进程。
为了了解这一“定论”,实在无妨举一个比方:比方界说一个用于主动封闭中止并在完结指定操作后主动康复本来状况的宏:
#defineSAFE_ATOM_CODE(...)\。
{。
uint32_twTemp=__disable_irq();。
__VA_ARGS__;。
__set_PRIMASK(wTemp);。
}。
由于这儿界说了一个变量 wTemp,而假如用户刺进的代码中也运用了同名的变量,就会发生许多问题:轻则编译过错(重复界说);重则呈现局部变量 wTemp 强行替代了用户自界说的静态变量的状况,然后直接导致专心运转呈现随机性的毛病(比方随机性的中止被封闭后不再康复,或是本来应该被封闭的大局中止处于翻开状况等等)。为了防止这一问题,实在往往会想主动给这个变量一个不会重复的姓名,比方凭借 __LINE__ 宏给这一变量参加一个后缀:
#defineSAFE_ATOM_CODE(...)\。
{。
uint32_twTemp##__LINE__=__disable_irq();。
__VA_ARGS__;。
__set_PRIMASK(wTemp);。
}。
假定这儿 SAFE_ATOM_CODE 所内行的行号是 123,那么实在等待的代码翻开是这个姿态的(我从头缩进过了):
...。
{。
uint32_twTemp123=__disable_irq();
__VA_ARGS__;
__set_PRIMASK(wTemp);
}。
...。
但是,实践翻开后的内容是这样的:
...。
{。
uint32_twTemp__LINE__=__disable_irq();
__VA_ARGS__;
__set_PRIMASK(wTemp);
}。
...。
这儿,__LINE__好像并没有被正确替换为 123,而是以原样的办法与 wTemp 张贴到了一同——这便是许多人常常诉苦的 __LINE__ 宏不稳定的问题。实践上,这是由于上述宏的构建没有恪守前面所罗列的两条定论导致的。
从内容上看,SAFE_ATOM_CODE() 要粘合的目标并不是形参,依据定论第二条,需求凭借别的一个参数宏来帮助完结这一进程。为此,实在需求引进一个专门的宏:
#defineCONNECT2(__A,__B)__A##__B。
才学过人到,这个参数宏要对形参进行胶水运算,依据定论第一条,需求在宏的外面再套一层,因而,修正代码得到:
#define__CONNECT2(__A,__B)__A##__B。
#defineCONNECT2(__A,__B)__CONNECT2(__A,__B)。
修正前面的界说得到:
#defineSAFE_ATOM_CODE(...)\。
{。
uint32_tCONNECT2(wTemp,__LINE__)=。
__disable_irq();。
__VA_ARGS__;。
__set_PRIMASK(wTemp);。
}。
- 对 enqueue 的封装进行翻开。
#defineenqueue(__queue,__addr,...)\。
CONNECT2(__ENQUEUE_,__PLOOC_VA_NUM_ARGS(__VA_ARGS__))\。
(__queue,(__addr),##__VA_ARGS__)。
CONNECT2 会依据__PLOOC_VA_NUM_ARGS 回来的数量,与__ENQUEUE_进行衔接,
- __PLOOC_VA_NUM_ARGS 回来的数量假如为 0,调用__ENQUEUE_0(__queue,(__addr),##__VA_ARGS__);
- __PLOOC_VA_NUM_ARGS 回来的数量假如为 1,调用__ENQUEUE_1(__queue,(__addr),##__VA_ARGS__);
- __PLOOC_VA_NUM_ARGS 回来的数量假如为 2,调用__ENQUEUE_2(__queue,(__addr),##__VA_ARGS__);
举个比方:
stat。ic。byte_queue_tmy_queue;
uint8_tdata1=0XAA;
enqueue(&my_queue,data1);//__ENQUEUE_0(&my_queue,data1)。
enqueue(&my_queue,&data1,1);//__ENQUEUE_1(&my_queue,&data1,1)。
enqueue(&my_queue,&data1,uint8_t,1);//__ENQUEUE_2(&my_queue,&data1,uint8_t,1)。
/*__ENQUEUE_0,__ENQUEUE_1,__ENQUEUE_2,翻开后调用的都是同一个。接口。*/。
enqueue_bytes(&my_queue,&data1,1)。
2.3 线程安全的完结原理。
问题:在多线程环境下,假如多个线程一同对同一个行列进行操作,可能会引发数据竞赛问题,导致数据损坏或不一致。为了防止这种状况,实在需求保证每次对行列的操作是原子的,即不行打断的。
处理方案:在嵌入式专心中,常用的办法是经过禁用中止或运用锁机制来保证数据的一致性。在实在的完结中,实在运用禁用中止的办法来保证线程安全。这是一种十分常见的技能,尤其是在实时专心中。
为了尽量下降关中止对实时性的影响,实在只对操作行列指针的操作进行关中止维护,相对耗时间的数据复制不进行关中止。
函数伪代码如下:
boolenqueue_bytes(...)。
{。
boolbEarlyReturn=false;
safe_atom_code(){。
if(!this.bMutex){。
this.bMutex=true;
}else{。
bEarlyReturn=true;
}。
}。
if(bEarlyReturn){。
returnfalse;
}。
safe_atom_code(){。
/*行列指针操作*/。
...。
}。
/*数据操作*/。
memcpy(...);
...。
this.bMutex=false;
returntrue;
}。
原子宏 safe_atom_code()的完结:
前边的比方中,实在完结了一个 SAFE_ATOM_CODE 的原子宏,仅有的问题是,这样的写法,在调试时彻底无法在用户代码处增加断点(编译器会以为宏内一切的内容都写在了同一行),这是大多数人不喜欢运用宏来封装代码结构的最大原因。
接下来实在用另一种完结办法来处理这个问题,代码如下:
#define__CONNECT3(__A,__B,__C)__A##__B##__C。
#define__CONNECT2(__A,__B)__A##__B。
#defineCONNECT3(__A,__B,__C)__CONNECT3(__A,__B,__C)。
#defineCONNECT2(__A,__B)__CONNECT2(__A,__B)。
#defineSAFE_NAME(__NAME)CONNECT3(__,__NAME,__LINE__)。
#include"cmsis_com。pi。ler.h"。
#definesafe_atom_code()\。
f。or。(uint32_tSAFE_NAME(temp)=\。
({uint32_tSAFE_NAME(temp2)=__get_PRIMASK();。
__disable_irq();。
SAFE_NAME(temp2);}),*SAFE_NAME(temp3)=NULL;\。
SAFE_NAME(temp3)++==NULL;\。
__set_PRIMASK(SAFE_NAME(temp)))。
#endif。
作业原理:
safe_atom_code()经过一个循环结构保证内行列操作期间,中止被禁用。循环完毕后主动康复中止。
2.3.1 for 循环的妙用。
首要结构一个只履行一次的 for 循环结构:
for(inti=1;i>0;i--){。
...。
}。
关于这样的 for 循环结构,几个要害部分就有了新的含义:
- 在履行用户代码之前(灰色部分),有才能进行必定的“准备作业”(Before 部分);
- 在履行用户代码之后,有才能履行必定的“收尾作业”(After 部分);
- 在 init_clause 阶段有才能界说一个“仅仅只掩盖” for 循环的,而且只对 User Code 可见的局部变量——换句话说,这些局部变量是不会污染 for 循环以外的当地的。
运用这样的结构,实在很简单就能结构出一个值勤经过花括号的办法来包裹用户代码的原子操作 safe_atom_code(),在履行用户代码之前封闭中止,在履行完用户代码之后翻开中止,还不影响在用户代码中增加断点,单步履行。
需求才学过人的是,假如需求半途退出循环,需求运用con。ti。nue退出原子操作,不能运用break。
2.4 总结。
经过上述的多类型支撑、函数重载和线程安全的完结,实在大大增强了字节行列的灵敏性和实用性:
- 多类型支撑:主动揣度数据类型和巨细,支撑不同类型数据的行列操作。
- 函数重载:经过宏模仿 C 言语的函数重载,灵敏处理不同数量和类型的参数。
- 线程安全:经过禁用中止机制保证行列操作在多线程环境中的原子性,防止数据竞赛问题。
这些改善使得实在的字节行列不只值勤在单线程环境中高效运转,还能在杂乱的多线程专心中坚持数据的一致性与安全性。
三、API 接口。
#definequeue_init(__queue,__buffer,__size,...)\。
__PLOOC_EVAL(__QUEUE_INIT_,##__VA_ARGS__)\。
(__queue,(__buffer),(__size),##__VA_ARGS__)。
#definedequeue(__queue,__addr,...)\。
__PLOOC_EVAL(__DEQUEUE_,##__VA_ARGS__)\。
(__queue,(__addr),##__VA_ARGS__)。
#defineenqueue(__queue,__addr,...)\。
__PLOOC_EVAL(__ENQUEUE_,##__VA_ARGS__)\。
(__queue,(__addr),##__VA_ARGS__)。
#definepeek_queue(__queue,__addr,...)\。
__PLOOC_EVAL(__PEEK_QUEUE_,##__VA_ARGS__)\。
(__queue,(__addr),##__VA_ARGS__)。
extern。
byte_queue_t*queue_init_byte(byte_queue_t*ptObj,void*pBuffer,uint16_thwItemSize,boolbIsCover);
extern。
boolreset_queue(byte_queue_t*ptObj);
extern。
uint16_tenqueue_bytes(byte_queue_t*ptObj,void*pDate,uint16_thwDataLength);
extern。
uint16_tdequeue_bytes(byte_queue_t*ptObj,void*pDate,uint16_thwDataLength);
extern。
boolis_queue_empty(byte_queue_t*ptQueue);
extern。
boolis_peek_empty(byte_queue_t*ptObj);
extern。
uint16_tpeek_bytes_queue(byte_queue_t*ptObj,void*pDate,uint16_thwDataLength);
extern。
voidreset_peek(byte_queue_t*ptQueue);
extern。
voidget_all_peeked(byte_queue_t*ptQueue);
extern。
uint16_tget_peek_status(byte_queue_t*ptQueue);
extern。
voidrestore_peek_status(byte_queue_t*ptQueue,uint16_thwCount);
extern。
uint16_tget_queue_count(byte_queue_t*ptObj);
extern。
uint16_tget_queue_av。ai。lable_count(byte_queue_t*ptObj);
四、API 阐明。
- 初始化行列。
queue_init(__queue,__buffer,__size,...)。
参数阐明:
参数名。 | 描绘。 |
---|---|
__QUEUE。 | 行列的地址。 |
__BUFFER。 | 行列缓存的首地址。 |
__BUFFER_SIZE。 | 行列长度。 |
可变参数。 | 是否掩盖,默许否。 |
- 入队。
#defineenqueue(__queue,__addr,...)。
参数阐明:
参数名。 | 描绘。 |
---|---|
__QUEUE。 | 行列的地址。 |
__ADDR。 | 待入队的数据或许数据的地址。 |
...。 | 可变参数,需求入队的数据个数,或许数据类型和个数,假如为空,则只入队一个数据。 |
- 出队。
#definedequeue(__queue,__addr,...)。
参数阐明:
参数名。 | 描绘。 |
---|---|
__QUEUE。 | 行列的地址。 |
__ADDR。 | 用于保存出队数据变量的地址。 |
...。 | 可变参数,需求出队的数据个数,或许数据类型和个数,假如为空,则只出队一个数据。 |
- 检查。
#definepeek_queue(__queue,__addr,...)。
参数阐明:
参数名。 | 描绘。 |
---|---|
__QUEUE。 | 行列的地址。 |
__ADDR。 | 用于保存检查数据变量的地址。 |
...。 | 可变参数,数据类型和需求检查的数据个数,假如为空,则只检查一个数据。 |
五、快速运用。
代码开源地址:https://github.com/Aladdin-Wang/wl_queue。
或许翻开MicroBoot,介绍链接:彻底处理。单片机。BootLoader晋级程序失利问题,只勾选queue,如图所示:
运用实例:
#include"ring_queue.h"。
uint8_tdata1=0XAA;
uint16_tdata2=0X55AA;
uint32_tdata3=0X55AAAA55;
uint16_tdata4[]={0x1234,0x5678};
typedefstructdata_t{。
uint32_ta;
uint32_tb;
uint32_tc;
}data_t;
data_tdata5={。
.a=0X11223344,
.b=0X55667788,
.c=0X99AABBCC,
};
uint8_tdata[100];
staticuint8_ts_hwQueueBuffer[100];
staticbyte_queue_tmy_queue;
queue_init(&my_queue,s_hwQueueBuffer,sizeof(s_hwQueueBuffer));
//依据变量的类型,主动核算目标的巨细。
enqueue(&my_queue,data1);
enqueue(&my_queue,data2);
enqueue(&my_queue,data3);
//一下三种办法都值勤正确存储数组。
enqueue(&my_queue,data4,2);//值勤不指名数据类型。
enqueue(&my_queue,data4,uint16_t,2);//也值勤指名数据类型。
enqueue(&my_queue,data4,uint8_t,sizeof(data4));//或许用其他类型。
//一下两种办法都值勤正确存储结构体类型。
enqueue(&my_queue,data5);//依据结构体的类型,主动核算目标的巨细。
enqueue(&my_queue,&data5,uint8_t,sizeof(data5));//也值勤以数组办法存储。
enqueue(&my_queue,(uint8_t)0X11);//常量默以为int型,需求强制转化数据类型。
enqueue(&my_queue,(uint16_t)0X2233);//常量默以为int型,需求强制转化数据类型。
enqueue(&my_queue,0X44556677);
enqueue(&my_queue,(char)'a');//单个字符也需求强制转化数据类型。
enqueue(&my_queue,"bc");//字符串默许会存储空字符\0。
enqueue(&my_queue,"def");
//读出悉数数据。
dequeue(&my_queue,data,get_queue_count(&my_queue));
结语。
本文的意图,告知我们怎么正确的看待宏——宏不是阻止代码开发和可读性的魔鬼:
宏不是奇技淫巧。
宏值勤封装出其它高档言语所供给的“基础设施”。
方便杰出的宏值勤提高代码的可读性,而不是损坏它。
方便杰出的宏并不会影响调试。
宏值勤用来固化某些模板,防止每次都从头编写杂乱的语法结构。
内容来源:https://artdesignphuong.com/app-1/ketqua 6688,http://chatbotjud.saude.mg.gov.br/app-1/rtp-pg-slot
本文地址:http://w.21nx.com/new/52462884-28f19299779.html
版权声明
本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。