0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

基于串口环形队列的IAP实现

技术让梦想更伟大 来源:CSDN技术社区 2023-04-12 09:28 次阅读

我这里主要是记录一下我所使用的方法,调试也花了两天时间。 我所用的型号是STM32F103C8T6,这个IC有64KFlash和20K的RAM,也有小道说有后置隐藏的64K,也就是说其实是有128K,我一直也没有测试,有空测测,有大神这样说,估计是可以的。 这里重点记录一下我写的IAP思路和代码以及细节和遇到坑的地方。先大体的概述一下,最后贴上我认为重点的代码。 在概述之前先要解决一个问题,那就是sram空间和flash空间的问题,sram只有20K,flash有64k。解决的办法有很多: 1)最常见的就是自己写上位机软件,通过分包发送,期间还可以加入加密算法,校验等等。 2)使用环形队列,简单点说就是个环形数组,一边接收上位机数据,一边往flash里面写。 这里条件限制就采用第二种方法。所以即使是分给A和B的25K空间的flash空间,sram只有20K也是不能一次接收完所有的bin数据的,这里我只开辟了一个1K的BUF,使用尾插法写入,我的测试应用程序都在5-6K,用这样的方法可以在9600波特率下测试稳定,也试过57600的勉强可以的,115200就不行了。环形队列代码如下: C文件:

				#include"fy_looplist.h"  #include"fy_includes.h"   #ifndefNULL #defineNULL0 #endif  #ifndefmin #definemin(a,b)(a)<(b)?(a):(b) //< 获取最小值 #endif  #defineDEBUG_LOOP1  staticintCreate(_loopList_s*p,unsignedchar*buf,unsignedintlen); staticvoidDelete(_loopList_s*p); staticintGet_Capacity(_loopList_s*p); staticintGet_CanRead(_loopList_s*p); staticintGet_CanWrite(_loopList_s*p); staticintRead(_loopList_s*p,void*buf,unsignedintlen); staticintWrite(_loopList_s*p,constvoid*buf,unsignedintlen);  struct_typdef_LoopList_list= { Create, Delete, Get_Capacity, Get_CanRead, Get_CanWrite, Read, Write };   //初始化环形缓冲区 staticintCreate(_loopList_s*p,unsignedchar*buf,unsignedintlen) { if(NULL==p) { #ifDEBUG_LOOP printf("ERROR:inputlistisNULL "); #endif return0; } p->capacity=len; p->buf=buf; p->head=p->buf;//头指向数组首地址 p->tail=p->buf;//尾指向数组首地址  return1; }  //删除一个环形缓冲区 staticvoidDelete(_loopList_s*p) { if(NULL==p) { #ifDEBUG_LOOP printf("ERROR:inputlistisNULL "); #endif return; }  p->buf=NULL;//地址赋值为空 p->head=NULL;//头地址为空 p->tail=NULL;//尾地址尾空 p->capacity=0;//长度为空 }  //获取链表的长度 staticintGet_Capacity(_loopList_s*p) { if(NULL==p) { #ifDEBUG_LOOP printf("ERROR:inputlistisNULL "); #endif return-1; } returnp->capacity; }  //返回能读的空间 staticintGet_CanRead(_loopList_s*p) { if(NULL==p) { #ifDEBUG_LOOP printf("ERROR:inputlistisNULL "); #endif return-1; }  if(p->head==p->tail)//头与尾相遇 { return0; }  if(p->head< p->tail)//尾大于头 { returnp->tail-p->head; }  returnGet_Capacity(p)-(p->head-p->tail);//头大于尾 }  //返回能写入的空间 staticintGet_CanWrite(_loopList_s*p) { if(NULL==p) { #ifDEBUG_LOOP printf("ERROR:inputlistisNULL "); #endif return-1; }  returnGet_Capacity(p)-Get_CanRead(p);//总的减去已经写入的空间 }   //p--要读的环形链表 //buf--读出的数据 //count--读的个数 staticintRead(_loopList_s*p,void*buf,unsignedintlen) { intcopySz=0;  if(NULL==p) { #ifDEBUG_LOOP printf("ERROR:inputlistisNULL "); #endif return-1; }  if(NULL==buf) { #ifDEBUG_LOOP printf("ERROR:inputbufisNULL "); #endif return-2; }  if(p->head< p->tail)//尾大于头 { copySz=min(len,Get_CanRead(p));//比较能读的个数 memcpy(buf,p->head,copySz);//读出数据 p->head+=copySz;//头指针加上读取的个数 returncopySz;//返回读取的个数 } else//头大于等于了尾 { if(len< Get_Capacity(p)-(p->head-p->buf))//读的个数小于头上面的数据量 { copySz=len;//读出的个数 memcpy(buf,p->head,copySz); p->head+=copySz; returncopySz; } else//读的个数大于头上面的数据量 { copySz=Get_Capacity(p)-(p->head-p->buf);//先读出来头上面的数据 memcpy(buf,p->head,copySz); p->head=p->buf;//头指针指向数组的首地址 //还要读的个数 copySz+=Read(p,(char*)buf+copySz,len-copySz);//接着读剩余要读的个数 returncopySz; } } } //p--要写的环形链表 //buf--写出的数据 //len--写的个数 staticintWrite(_loopList_s*p,constvoid*buf,unsignedintlen) { inttailAvailSz=0;//尾部剩余空间  if(NULL==p) { #ifDEBUG_LOOP printf("ERROR:listisempty "); #endif return-1; }  if(NULL==buf) { #ifDEBUG_LOOP printf("ERROR:bufisempty "); #endif return-2; }  if(len>=Get_CanWrite(p))//如果剩余的空间不够 { #ifDEBUG_LOOP printf("ERROR:nomemory "); #endif return-3; }  if(p->head<= p->tail)//头小于等于尾 { tailAvailSz=Get_Capacity(p)-(p->tail-p->buf);//查看尾上面剩余的空间 if(len<= tailAvailSz)//个数小于等于尾上面剩余的空间 { memcpy(p->tail,buf,len);//拷贝数据到环形数组 p->tail+=len;//尾指针加上数据个数 if(p->tail==p->buf+Get_Capacity(p))//正好写到最后 { p->tail=p->buf;//尾指向数组的首地址 } returnlen;//返回写入的数据个数 } else { memcpy(p->tail,buf,tailAvailSz);//填入尾上面剩余的空间 p->tail=p->buf;//尾指针指向数组首地址 //剩余空间剩余数据的首地址剩余数据的个数 returntailAvailSz+Write(p,(char*)buf+tailAvailSz,len-tailAvailSz);//接着写剩余的数据 } } else//头大于尾 { memcpy(p->tail,buf,len); p->tail+=len; returnlen; } }   /*********************************************ENDOFFILE********************************************/

1、整体思路

把64K的flash空间分成了4个部分,第一部分是BootLoader,第二部分是程序A(APP1),第三部分是程序B(APP2),第四部分是用来存储一些变量和标记的。下面是空间的分配情况。BootLoader程序可以用来更新程序A,而程序A又更新程序B,程序B可以更新程序A。 最开始的时候想的是程序A、B都带更新了干嘛还多此一举,其实这个Bootloader还是需要的。如果之后程序A、B和FLAG三部分,假设一种情况,在程序B中更新程序A中遇到问题,复位后直接成砖,因为程序A在其实地址,上电直接运行程序A,而程序A现在出问题了,那就没招了。 所以加上BootLoader情况下,不管怎么样BootLoader的程序是不会错的,因为更新不会更新BootLoader,计时更新出错了,还可以进入BootLoader重新更新应用程序。我见也有另外一种设计方法的,就是应用程序只有一个程序A,把程序B区域的flash当作缓存用,重启的时候判断B区域有没有更新程序,有的话就把B拷贝到A,然后擦除B,我感觉这样其实也一样,反正不管怎么样这部分空间是必须要预留出来的。b8815812-d893-11ed-bfe3-dac502259ad0.png 这里在keil中配置的只有起始地址和大小,并没有结束地址,我这里也就不详细计算了。总体就是这样的。

2、Bootloader部分

BootLoader的任务有两个,一是在串口中断接收BIN的数据和主循环内判断以及更新APP1的程序,二是在在程序开始的时候判断有没有可用的用户程序进而跳转到用户程序(程序A或者程序B)。 简单介绍下执行流程: 系统上电首先肯定是执行BootLoader程序的,因为它的起始地址就是0x08000000,首先是初始化,然后判断按键是否手动升级程序,按键按下了就把FLAG部分的APP标记写成0xFFFF(这里用的宏定义方式),再执行执行App_Check(),否则就直接执行App_Check()。 App_Check函数是来判断程序A和程序B的,最开始BootLoader是用swd方式下载的,下载的时候全片擦除,所以会执行主循环的Update_Check函数。此时串口打印出“等待接收APP1的BIN”,这个时候发送APP1的BIN过去,等接受完了,会写在FLAG区域写个0xAAAA,代表程序A写入了,下次启动可以执行程序A。 主要代码部分

				#include"fy_includes.h"  /* 晶振使用的是16M其他频率在system_stm32f10x.c中修改 使用printf需要在fy_includes.h修改串口重定向为#definePRINTF_USARTUSART1 */   /* Bootloader程序  完成三个任务  步骤1.检查是否有程序更新,如果有就擦写flash进行更新,如果没有进入步骤2 步骤2.判断app1有没有可执行程序,如果有就执行,如果没有进入步骤3 步骤3.串口等待接收程序固件  */  #defineFLAG_UPDATE_APP10xBBAA #defineFLAG_UPDATE_APP20xAABB #defineFLAG_APP10xAAAA #defineFLAG_APP20xBBBB #defineFLAG_NONE0xFFFF  _loopList_slist1; u8rxbuf[1024]; u8temp8[2]; u16temp16; u32rxlen=0; u32applen=0; u32write_addr; u8overflow=0; u32now_tick=0; u8_cnt_10ms=0;  staticvoidApp_Check(void) { //获取程序标号 STMFLASH_Read(FLASH_PARAM_ADDR,&temp16,1);  if(temp16==FLAG_APP1)//执行程序A { if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//可执行? { printf("执行程序A... "); IAP_RunApp(FLASH_APP1_ADDR); } else { printf("程序A不可执行,擦除APP1程序所在空间... "); for(u8i=10;i<35;i++) { STMFLASH_Erase(FLASH_BASE+i*STM_SECTOR_SIZE,512); } printf("程序A所在空间擦除完成... "); printf("将执行程序B... "); if(((*(vu32*)(FLASH_APP2_ADDR+4))&0xFF000000)==0x08000000)//可执行? { printf("执行程序B... "); IAP_RunApp(FLASH_APP2_ADDR); } else { printf("程序B不可执行,擦除APP2程序所在空间... "); for(u8i=35;i<60;i++) { STMFLASH_Erase(FLASH_BASE+i*STM_SECTOR_SIZE,512); } printf("程序B所在空间擦除完成... "); } } }  if(temp16==FLAG_APP2)//执行程序B { if(((*(vu32*)(FLASH_APP2_ADDR+4))&0xFF000000)==0x08000000)//可执行? { printf("执行程序B... "); IAP_RunApp(FLASH_APP2_ADDR); } else { printf("程序B不可执行,擦除APP2程序所在空间... "); for(u8i=35;i<60;i++) { STMFLASH_Erase(FLASH_BASE+i*STM_SECTOR_SIZE,512); } printf("程序B所在空间擦除完成... "); printf("将执行程序A... "); if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//可执行? { printf("执行程序A... "); IAP_RunApp(FLASH_APP1_ADDR); } else { printf("程序A不可执行,擦除APP1程序所在空间... "); for(u8i=10;i<35;i++) { STMFLASH_Erase(FLASH_BASE+i*STM_SECTOR_SIZE,512); } printf("程序A所在空间擦除完成... "); } } }  if(temp16==FLAG_NONE) { printf("擦除App1程序所在空间... "); for(u8i=10;i<35;i++) { STMFLASH_Erase(FLASH_BASE+i*STM_SECTOR_SIZE,512); } printf("程序A所在空间擦除完成... "); } }   staticvoidUpdate_Check(void) { if(_list.Get_CanRead(&list1)>1) { _list.Read(&list1,&temp8,2);//读取两个数据  temp16=(u16)(temp8[1]<<8)|temp8[0];  STMFLASH_Write(write_addr,&temp16,1); write_addr+=2; }  if(GetSystick_ms()-now_tick>10)//10ms { now_tick=GetSystick_ms(); _cnt_10ms++; if(applen==rxlen&&rxlen)//接收完成 { if(overflow) { printf("接收溢出,无法更新,请重试 "); SoftReset();//软件复位 } else { printf(" 接收BIN文件完成,长度为%d ",applen);  temp16=FLAG_APP1; STMFLASH_Write(FLASH_PARAM_ADDR,&temp16,1);//写入标记 temp16=(u16)(applen>>16); STMFLASH_Write(FLASH_PARAM_ADDR+2,&temp16,1); temp16=(u16)(applen); STMFLASH_Write(FLASH_PARAM_ADDR+4,&temp16,1);  SoftReset();//软件复位 } }elseapplen=rxlen;//更新长度 } if(_cnt_10ms>=50) { _cnt_10ms=0; Led_Tog(); if(!rxlen) { printf("等待接收App1的BIN文件 "); } } } intmain(void) { NVIC_SetPriorityGrouping(NVIC_PriorityGroup_2); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//开启AFIO时钟 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);//禁止JTAG保留SWD  Systick_Configuration(); Led_Configuration(); Key_Configuration(); Usart1_Configuration(9600); USART_ITConfig(USART1,USART_IT_IDLE,DISABLE);//关闭串口空闲中断  printf("thisisbootloader! "); if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==SET) { Delay_ms(100); if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==SET)//开机按下keyup进行更新 { printf("主动更新,"); temp16=FLAG_NONE; STMFLASH_Write(FLASH_PARAM_ADDR,&temp16,1); } else {  } }  App_Check();  printf("执行BootLoader程序... "); _list.Create(&list1,rxbuf,sizeof(rxbuf));  write_addr=FLASH_APP1_ADDR;  while(1) { Update_Check(); } }  //USART1串口中断函数 voidUSART1_IRQHandler(void) { if(USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET) { u8temp=USART1->DR; if(_list.Write(&list1,&temp,1)<=0) { overflow=1; } rxlen++; } }
					其中的宏:

				//FLASH起始地址 #defineSTM32_FLASH_BASE0x08000000//STM32FLASH的起始地址 #defineFLASH_APP1_ADDRSTM32_FLASH_BASE+0x2800//偏移10K #defineFLASH_APP2_ADDRSTM32_FLASH_BASE+0x8c00//偏移35K #defineFLASH_PARAM_ADDRSTM32_FLASH_BASE+0xF000//偏移60K

3、程序A和程序B部分

这两个都是用户程序,这两个程序都带有更新程序功能,我这里用作测试的A和B程序大体都差不多,不同的地方就是程序A接收的BIN用来更新程序B,程序B接收的BIN用来更新A,还有就是中断向量表便宜不同以及打印输出不同。 应用程序部分没什么说的,程序A和B很类似,这里贴上A的代码

				#include"fy_includes.h"  /* 晶振使用的是16M其他频率在system_stm32f10x.c中修改 使用printf需要在fy_includes.h修改串口重定向为#definePRINTF_USARTUSART1 */   /* APP1程序  完成两个任务  1.执行本身的app任务,同时监听程序更新,监听到停止本身的任务进入到状态2 2.等待接收完成,完成后复位重启  */  #defineFLAG_UPDATE_APP10xBBAA #defineFLAG_UPDATE_APP20xAABB #defineFLAG_APP10xAAAA #defineFLAG_APP20xBBBB #defineFLAG_NONE0xFFFF  _loopList_slist1; u8rxbuf[1024]; u8temp8[2]; u16temp16; u32rxlen=0; u32applen=0; u32write_flsh_addr; u8update=0; u8overflow=0; u32now_tick; u8_cnt_10ms=0;  staticvoidUpdate_Check(void) { if(update)//监听到有更新程序 { write_flsh_addr=FLASH_APP2_ADDR;//App1更新App2的程序 overflow=0; rxlen=0; _list.Create(&list1,rxbuf,sizeof(rxbuf));  printf("擦除APP2程序所在空间... "); for(u8i=35;i<60;i++)//擦除APP2所在空间程序 { STMFLASH_Erase(FLASH_BASE+i*STM_SECTOR_SIZE,512); } printf("程序B所在空间擦除完成... ");  while(1) { if(_list.Get_CanRead(&list1)>1) { _list.Read(&list1,&temp8,2);//读取两个数据  temp16=(u16)(temp8[1]<<8)|temp8[0];  STMFLASH_Write(write_flsh_addr,&temp16,1); write_flsh_addr+=2; }  if(GetSystick_ms()-now_tick>10)//10ms { now_tick=GetSystick_ms(); _cnt_10ms++; if(applen==rxlen&&rxlen)//接收完成 { if(overflow) { printf(" 接收溢出,请重新尝试 "); SoftReset();//软件复位 }  printf(" 接收BIN文件完成,长度为%d ",applen);  temp16=FLAG_APP2; STMFLASH_Write(FLASH_PARAM_ADDR,&temp16,1);//写入标记 temp16=(u16)(applen>>16); STMFLASH_Write(FLASH_PARAM_ADDR+2,&temp16,1); temp16=(u16)(applen); STMFLASH_Write(FLASH_PARAM_ADDR+4,&temp16,1);  printf("系统将重启.... "); SoftReset();//软件复位 }elseapplen=rxlen;//更新长度 }  if(_cnt_10ms>=50) { _cnt_10ms=0; Led_Tog(); if(!rxlen) { printf("等待接收App2的BIN文件 "); } } }//while(1) } }   staticvoidApp_Task(void) { if(GetSystick_ms()-now_tick>500) { now_tick=GetSystick_ms(); printf("正在运行APP1 "); Led_Tog(); } }  intmain(void) { SCB->VTOR=FLASH_APP1_ADDR;  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//开启AFIO时钟 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);//禁止JTAG保留SWD  Systick_Configuration(); Led_Configuration(); Usart1_Configuration(9600); printf("thisisAPP1! ");  Delay_ms(500);  while(1) { Update_Check(); App_Task(); } }  //USART1串口中断函数 voidUSART1_IRQHandler(void) { if(USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET) { u8temp=USART1->DR; if(update) { if(_list.Write(&list1,&temp,1)<= 0) { overflow=1; } } else { rxbuf[rxlen]=temp; } rxlen++; } if(USART_GetITStatus(USART1,USART_IT_IDLE)!=RESET) { u8temp=USART1->DR; temp=USART1->SR;  if(strstr((char*)rxbuf,"AppUpdate")&&rxlen) { update=1; USART_ITConfig(USART1,USART_IT_IDLE,DISABLE);//关闭串口空闲中断 } else { Usart1_SendBuf(rxbuf,rxlen); } rxlen=0; }  }
					这里如果要移植需要注意的就是向量表的偏移以及更新擦写的区域。

4、剩余的4Kflash空间部分

这里其实只是用来存储2个变量,一个是程序运行标记,一个是接收到的程序长度,程序标记还有点把子用,程序长度其实要不要都无所谓。

5、遇到的坑

最值得一说的就是更新部分,最开始程序没有加入擦除flash,遇到的情况就是下载完BootLoader后发送app1没问题,在app1中更新App2也没问题,然后app2再更新app1就出问题了。直观的结果就是循环队列溢出,原因就是app2在更新app1前没有去擦除app1所在的flash,所以在写的时候就要去擦除,这样就写的很慢,然而串口接收是不停的收,所以就是写不过来。
审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • FlaSh
    +关注

    关注

    10

    文章

    1551

    浏览量

    146717
  • 调试
    +关注

    关注

    7

    文章

    527

    浏览量

    33625
  • 串口
    +关注

    关注

    14

    文章

    1485

    浏览量

    74526
  • IAP
    IAP
    +关注

    关注

    2

    文章

    161

    浏览量

    23966
  • bootloader
    +关注

    关注

    2

    文章

    230

    浏览量

    45053

原文标题:基于串口环形队列的IAP实现!

文章出处:【微信号:技术让梦想更伟大,微信公众号:技术让梦想更伟大】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    基于STM32的串口环形队列IAP调试

    基于STM32的串口环形队列IAP调试心得
    的头像 发表于 09-18 15:33 863次阅读
    基于STM32的<b class='flag-5'>串口</b><b class='flag-5'>环形</b><b class='flag-5'>队列</b><b class='flag-5'>IAP</b>调试

    STM32进阶之串口环形缓冲区实现

    实现吧:从队列串口缓冲区的实现串口环形缓冲区收发:在很多入门级教程中,我们知道的
    发表于 06-08 14:03

    STM32串口环形缓冲区的实现

    试试用代码实现吧!从队列串口缓冲区的实现串口环形缓冲区收发:在很多入门级教程中,我们知道的
    发表于 10-16 11:40

    请问串口接受用环形队列,发送也能用吗?

    串口接受用环形队列,发送也可以用?发送用普通的中断也可以
    发表于 05-07 07:56

    环形队列串口数据接收中的使用

    前言  书接上回,前文主要介绍了环形队列实现原理以及C语言实现及测试过程,本文将回归到嵌入式平台的应用中,话不多说,淦,上干货!实验目的HAL库下
    发表于 12-06 06:27

    如何使用队列实现STM32串口环形缓冲?

    串口环形缓冲的好处是什么?如何使用队列实现STM32串口环形缓冲?
    发表于 12-07 07:13

    基于stm32串口环形缓冲队列处理机制是什么

    基于stm32串口环形缓冲队列处理机制是什么
    发表于 12-08 07:06

    实现队列环形缓冲的方法

    串口队列环形缓冲区队列串口环形缓冲的好处代码实现
    发表于 02-21 07:11

    环形队列的相关资料分享

    前言  当代码,不再是简单的完成需求,对代码进行堆砌,而是开始思考如何写出优美代码的时候,我们的代码水平必然会不断提升,今天,咱们来学习环形队列结构。环形队列的基本概念  相信对数据结
    发表于 02-23 06:10

    环形队列的操作如何去实现

    环形队列结构的定义是什么?环形队列的操作如何去实现呢?
    发表于 02-25 06:35

    STM32串口环形缓冲--使用队列实现(开放源码)

    串口队列环形缓冲区队列串口环形缓冲的好处代码实现
    发表于 12-24 19:04 24次下载
    STM32<b class='flag-5'>串口</b><b class='flag-5'>环形</b>缓冲--使用<b class='flag-5'>队列</b><b class='flag-5'>实现</b>(开放源码)

    基于STM32的串口环形队列IAP调试心得

    使用环形队列,简单点说就是个环形数组,一边接收上位机数据,一边往flash里面写。
    发表于 02-08 15:22 5次下载
    基于STM32的<b class='flag-5'>串口</b><b class='flag-5'>环形</b><b class='flag-5'>队列</b><b class='flag-5'>IAP</b>调试心得

    STM32进阶之串口环形缓冲区实现

    码代码的应该学数据结构都学过队列环形队列队列的一种特殊形式,应用挺广泛的。因为有太多文章关于这方面的内容,理论知识可以看别人的,下面写得挺好的:STM32进阶之
    发表于 12-06 10:00 2364次阅读

    嵌入式环形队列和消息队列实现

    嵌入式环形队列和消息队列实现数据缓存和通信的常见数据结构,广泛应用于嵌入式系统中的通信协议和领域。
    的头像 发表于 04-14 11:52 1063次阅读

    嵌入式环形队列和消息队列是如何去实现的?

    嵌入式环形队列和消息队列实现数据缓存和通信的常见数据结构,广泛应用于嵌入式系统中的通信协议和领域。
    发表于 05-20 14:55 671次阅读