OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 21|回复: 0

《STM32H562开发指南 V1.0 》第四十二章 内存管理实验

[复制链接]

1418

主题

1434

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
6049
金钱
6049
注册时间
2019-5-8
在线时间
1728 小时
发表于 半小时前 | 显示全部楼层 |阅读模式
第四十二章 内存管理实验

1)实验平台:正点原子Mini STM32H562开发板

2)章节摘自【正点原子】STM32H562开发指南 V1.0

3)购买链接: https://detail.tmall.com/item.htm?id=753606659612

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32h562_mini.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)正点原子STM32开发板技术交流群:414052738


2.jpg

3.png

如果我们所用的内存都是直接定义一个数组来使用,灵活性会比较差,很多时候不能满足实际使用需求。为了解决这些问题,我们来学习内存管理,实现对内存的动态管理。
本章分为如下几个小节:
42.1 内存管理简介
42.2 硬件设计
42.3 程序设计
42.4 下载验证


42.1 内存管理简介
内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效、快速的分配,并且在适当的时候释放和回收内存资源。内存管理的实现方法有很多种,其实最终都是要实现两个函数:malloc和free。malloc函数用来内存申请,free函数用于内存释放。
本章,我们介绍一种比较简单的办法来实现:分块式内存管理。下面我们介绍一下该方法的实现原理,如图42.1.1所示:


第四十二章 内存管理实验352.png
图42.1.1 分块式内存管理原理

从上图可以看出,分块式内存管理由内存池和内存管理表两部分组成。内存池被等分为了n块,对应的内存管理表,大小也为n,内存管理表的每一个项对应内存池的一块内存。
内存管理表的项值代表的意义为:当该项值为0的时候,代表对应的内存块未被占用,当该项值非零的时候,代表该项对应的内存块已经被占用,其数值则代表被连续占用的内存块数。比如某项值为10,那么说明包括本项对应的内存块在内,总共分配了10个内存块给外部的某个指针。
内存分配方向如上图所示,是从顶→底的分配方向。即首先从最末端开始找空内存。当内存管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。
分配原理:
当指针p调用malloc申请内存的时候,先判断p要分配的内存块数(m),然后从第n开始,向下查找,直到找到m块连续的空内存块(即对应内存管理表项为0),然后将这m个内存管理表项的值都设置为m(标记被占用),最后,把最后的这个空内存块的地址返回指针p,完成一次分配。注意:如果当内存不够的时候(找到最后也没有找到连续m块空闲内存),则返回NULL给p,表示分配失败。
释放原理:
当p申请的内存用完,需要释放的时候,调用free函数实现。free函数先判断p指向的内存地址所对应的内存块,然后找到对应的内存管理表项目,得到p所占用的内存块数目m(内存管理表项目的值就是所分配内存块的数目),将这m个内存管理表项目的值都清零,标记释放,完成一次内存释放。


42.2 硬件设计

1. 例程功能
本实验开机后,显示提示信息,等待外部输入。KEY0用于申请内存并写入内容,每次申请2K字节内存。KEY1用于写数据到申请到的内存里面并在LCD上显示。KEY_UP用于释放内存,只能释放上一次申请的内存,上上次以上申请的内存无法释放。还可以通过USMART调试,测试内存管理函数。
LED0闪烁用于提示程序正在运行。

2. 硬件资源
1)LED灯
       LED0 – PB4
2)独立按键  
       KEY0  - PA1
       KEY1  - PA15
       WK_UP - PA0
3)串口1 (PA9/PA10连接在板载USB转串口芯片CH340上面)
4)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
5)定时器4


42.3 程序设计

42.3.1 程序流程图

第四十二章 内存管理实验1384.png
图42.3.1.1 内存管理实验程序流程图

42.3.2 程序解析

1. MALLOC代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。内存管理驱动源码包括两个文件:malloc.c和malloc.h。这两个文件放在Middlewares文件夹下面的MALLOC文件夹。
下面我们介绍malloc.h中比较重要的一些结构体和内存参数宏定义,其定义如下:

  1. /* 定义1个内存池 */
  2. #define SRAMIN     0         /* 内部SRAM,分为SRAM1/2/3,SRAM1+SRAM2+SRAM3,共640KB */

  3. #define SRAMBANK   1          /* 定义支持的SRAM块数 */

  4. /* 定义内存管理表类型,当外扩SDRAM的时候,必须使用uint32_t类型,
  5. 否则可以定义成uint16_t,以节省内存占用 */
  6. #define MT_TYPE     uint16_t

  7. /* 单块内存,内存管理所占用的全部空间大小计算公式如下:
  8. * size = MEM1_MAX_SIZE + (MEM1_MAX_SIZE / MEM1_BLOCK_SIZE) * sizeof(MT_TYPE)
  9. * 以SRAMIN为例,size = 520 * 1024 + (520 * 1024 / 32) * 2 = 565760 = 552.5KB

  10. * 已知总内存容量(size),最大内存池的计算公式如下:
  11. * MEM1_MAX_SIZE = (MEM1_BLOCK_SIZE * size) / (MEM1_BLOCK_SIZE+sizeof(MT_TYPE))
  12. * 以SRAMIN为例,MEM1_MAX_SIZE = (32 * 640) / (32 + 2) = 602.35KB ≈ 600KB
  13. * 但是我们为了给其他全局变量 / 数组等预留内存空间, 这里设置最大管理为 520KB
  14. */

  15. /* mem1内存参数设定.mem1是H563内部的SRAM. */
  16. #define MEM1_BLOCK_SIZE   32       /* 内存块大小为32字节 */
  17. #define MEM1_MAX_SIZE  520 * 1024 /* 最大管理内存 520K, H563VI内部SRAM总共640KB */
  18. #define MEM1_ALLOC_TABLE_SIZE   MEM1_MAX_SIZE/MEM1_BLOCK_SIZE   /* 内存表大小 */

  19. /* 如果没有定义NULL, 定义NULL */
  20. #ifndef NULL
  21. #define NULL 0
  22. #endif

  23. /* 内存管理控制器 */
  24. struct _m_mallco_dev
  25. {
  26.     void (*init)(uint8_t);                   /* 初始化 */
  27.     uint16_t (*perused)(uint8_t);           /* 内存使用率 */
  28.     uint8_t *membase[SRAMBANK];             /* 内存池 管理SRAMBANK个区域的内存 */
  29.     MT_TYPE *memmap[SRAMBANK];              /* 内存管理状态表 */
  30.     uint8_t  memrdy[SRAMBANK];              /* 内存管理是否就绪 */
  31. };
复制代码
我们可以定义几个不同的内存管理表,再分配相应的指针给到管理控制器即可。程序中我们用宏定义MEM1_BLOCK_SIZE来定义malloc可以管理的内部内存池总大小,实际上我们定义为一个大小为MEM1_BLOCK_SIZE的数组,这样编译后就能获得一块实际的连续内存区域,这里是520KB,MEM1_ALLOC_TABLE_SIZE 代表内存池的内存管理表大小。我们可以定义多个内存管理表,这样就可以同时管理多块内存。
从这里可以看出,如果内存分块越小,那么内存管理表就越大,当分块为2字节1个块的时候,内存管理表就和内存池一样大了(管理表的每项都是uint16_t类型),显然是不合适。我们这里取32字节,比例为1:16,内存管理表相对就比较小了。
通过这个内存管理控制器_m_malloc_dev 结构体,我们把分块式内存管理的相关信息,其初始化函数、获取使用率、内存池、内存管理表以及内存管理的状态保存下来,实现对内存池的管理控制。其中,内存池的定义为:

  1. static __ALIGNED(64) uint8_t mem1base[MEM1_MAX_SIZE];        /* 内部SRAM内存池 */

  2. /* 内存管理表 */
  3. static MT_TYPE mem1mapbase[MEM1_ALLOC_TABLE_SIZE];           /* 内部SRAM内存池MAP */


  4. /* 内存管理参数 */
  5. const uint32_t memtblsize[SRAMBANK] = {MEM1_ALLOC_TABLE_SIZE};/* 内存表大小 */
  6. const uint32_t memblksize[SRAMBANK] = {MEM1_BLOCK_SIZE};               /* 内存分块大小 */
  7. const uint32_t memsize[SRAMBANK] = {MEM1_MAX_SIZE};                     /* 内存总大小 */

  8. /* 内存管理控制器 */
  9. struct _m_mallco_dev mallco_dev =
  10. {
  11.     my_mem_init,             /* 内存初始化 */
  12.     my_mem_perused,          /* 内存使用率 */
  13.     mem1base,                    /* 内存池 */
  14.     mem1mapbase,             /* 内存管理状态表 */
  15.     0,                            /* 内存管理未就绪 */
  16. };
复制代码
我们通过内存管理控制器mallco_dev结构体,实现对内存池的管理控制。
内部SRAM内存池,定义为:

  1. static __align(64) uint8_t mem1base[MEM1_MAX_SIZE];  /* 内部SRAM内存池 */
复制代码
STM32H562内部有3个连续的内存块(SRAM1/2/3),因此这里直接把它们放在一个内存池进行管理。
其中,__align(64)定义内存池为64字节对齐,以适应各种不同场合的需求。
下面介绍其它的malloc代码,具体如下:

  1. /**
  2. * @brief             复制内存
  3. * [url=home.php?mod=space&uid=271674]@param[/url]              *des : 目的地址
  4. * @param             *src : 源地址
  5. * @param            n     : 需要复制的内存长度(字节为单位)
  6. * @retval            无
  7. */
  8. void my_mem_copy(void *des, void *src, uint32_t n)
  9. {
  10.     uint8_t *xdes = des;
  11.     uint8_t *xsrc = src;
  12.     while (n--) *xdes++ = *xsrc++;
  13. }

  14. /**
  15. * @brief             设置内存值
  16. * @param             *s     : 内存首地址
  17. * @param              c      : 要设置的值
  18. * @param             count : 需要设置的内存大小(字节为单位)
  19. * @retval             无
  20. */
  21. void my_mem_set(void *s, uint8_t c, uint32_t count)
  22. {
  23.     uint8_t *xs = s;
  24.     while (count--) *xs++ = c;
  25. }

  26. /**
  27. * @brief              内存管理初始化
  28. * @param              memx : 所属内存块
  29. * @retval             无
  30. */
  31. void my_mem_init(uint8_t memx)
  32. {
  33. /* 获取memmap数组的类型长度(uint16_t /uint32_t)*/
  34. uint8_t mttsize = sizeof(MT_TYPE);
  35. /* 内存状态表数据清零 */
  36. my_mem_set(mallco_dev.memmap[memx], 0, memtblsize[memx] * mttsize);
  37. /* 内存管理初始化OK */
  38.     mallco_dev.memrdy[memx] = 1;        
  39. }

  40. /**
  41. * @brief              获取内存使用率
  42. * @param             memx : 所属内存块
  43. * @retval             使用率(扩大了10倍,0~1000,代表0.0%~100.0%)
  44. */
  45. uint16_t my_mem_perused(uint8_t memx)
  46. {
  47.     uint32_t used = 0;
  48.     uint32_t i;

  49.     for (i = 0; i < memtblsize[memx]; i++)
  50.     {
  51.         if (mallco_dev.memmap[memx][i]) used++;
  52.     }

  53.     return (used * 1000) / (memtblsize[memx]);
  54. }

  55. /**
  56. * @brief              内存分配(内部调用)
  57. * @param             memx : 所属内存块
  58. * @param             size : 要分配的内存大小(字节)
  59. * @retval             内存偏移地址
  60. *   @arg             0 ~ 0XFFFFFFFE : 有效的内存偏移地址
  61. *   @arg             0XFFFFFFFF     : 无效的内存偏移地址
  62. */
  63. static uint32_t my_mem_malloc(uint8_t memx, uint32_t size)
  64. {
  65.     signed long offset = 0;
  66.     uint32_t nmemb;                     /* 需要的内存块数 */
  67.     uint32_t cmemb = 0;         /* 连续空内存块数 */
  68.     uint32_t i;

  69.     if (!mallco_dev.memrdy[memx])
  70.     {
  71.         mallco_dev.init(memx);            /* 未初始化,先执行初始化 */
  72.     }
  73.     if (size == 0) return 0XFFFFFFFF;   /* 不需要分配 */
  74.     nmemb = size / memblksize[memx];    /* 获取需要分配的连续内存块数 */

  75.     if (size % memblksize[memx]) nmemb++;
  76. /* 搜索整个内存控制区 */
  77.     for (offset = memtblsize[memx] - 1; offset >= 0; offset--)
  78.     {
  79.         if (!mallco_dev.memmap[memx][offset])
  80.         {
  81.             cmemb++;             /* 连续空内存块数增加 */
  82.         }
  83.         else
  84.         {
  85.             cmemb = 0;          /* 连续内存块清零 */
  86.         }
  87.         
  88.         if (cmemb == nmemb)   /* 找到了连续nmemb个空内存块 */
  89.         {
  90.             for (i = 0; i < nmemb; i++) /* 标注内存块非空 */
  91.             {
  92.                 mallco_dev.memmap[memx][offset + i] = nmemb;
  93.             }
  94.             return (offset * memblksize[memx]); /* 返回偏移地址 */
  95.         }
  96.     }
  97.     return 0XFFFFFFFF;  /* 未找到符合分配条件的内存块 */
  98. }

  99. /**
  100. * @brief             释放内存(内部调用)
  101. * @param              memx   : 所属内存块
  102. * @param             offset : 内存地址偏移
  103. * @retval             释放结果
  104. *   @arg             0, 释放成功;
  105. *   @arg               1, 释放失败;
  106. *   @arg               2, 超区域了(失败);
  107. */
  108. static uint8_t my_mem_free(uint8_t memx, uint32_t offset)
  109. {
  110.     int i;

  111.     if (!mallco_dev.memrdy[memx])   /* 未初始化,先执行初始化 */
  112.     {
  113.         mallco_dev.init(memx);
  114.         return 1;                       /* 未初始化 */
  115.     }

  116.     if (offset < memsize[memx])     /* 偏移在内存池内. */
  117.     {
  118.         int index = offset / memblksize[memx];              /* 偏移所在内存块号码 */
  119.         int nmemb = mallco_dev.memmap[memx][index];        /* 内存块数量 */

  120.         for (i = 0; i < nmemb; i++)                            /* 内存块清零 */
  121.         {
  122.             mallco_dev.memmap[memx][index + i] = 0;
  123.         }

  124.         return 0;
  125.     }
  126.     else
  127.     {
  128.         return 2;   /* 偏移超区了. */
  129.     }
  130. }

  131. /**
  132. * @brief             释放内存(外部调用)
  133. * @param               memx : 所属内存块
  134. * @param               ptr  : 内存首地址
  135. * @retval              无
  136. */
  137. void myfree(uint8_t memx, void *ptr)
  138. {
  139.     uint32_t offset;

  140.     if (ptr == NULL)return;     /* 地址为0. */

  141.     offset = (uint32_t)ptr - (uint32_t)mallco_dev.membase[memx];
  142.     my_mem_free(memx, offset); /* 释放内存 */
  143. }

  144. /**
  145. * @brief               分配内存(外部调用)
  146. * @param            memx : 所属内存块
  147. * @param               size : 要分配的内存大小(字节)
  148. * @retval              分配到的内存首地址.
  149. */
  150. void *mymalloc(uint8_t memx, uint32_t size)
  151. {
  152.     uint32_t offset;
  153.     offset = my_mem_malloc(memx, size);

  154.     if (offset == 0XFFFFFFFF)   /* 申请出错 */
  155.     {
  156.         return NULL;              /* 返回空(0) */
  157.     }
  158.     else    /* 申请没问题, 返回首地址 */
  159.     {
  160.         return (void *)((uint32_t)mallco_dev.membase[memx] + offset);
  161.     }
  162. }

  163. /**
  164. * @brief             重新分配内存(外部调用)
  165. * @param               memx : 所属内存块
  166. * @param               *ptr : 旧内存首地址
  167. * @param               size : 要分配的内存大小(字节)
  168. * @retval              新分配到的内存首地址.
  169. */
  170. void *myrealloc(uint8_t memx, void *ptr, uint32_t size)
  171. {
  172.     uint32_t offset;
  173.     offset = my_mem_malloc(memx, size);

  174.     if (offset == 0XFFFFFFFF)   /* 申请出错 */
  175.     {
  176.         return NULL;              /* 返回空(0) */
  177.     }
  178.     else    /* 申请没问题, 返回首地址 */
  179.     {
  180.         my_mem_copy((void *)((uint32_t)mallco_dev.membase[memx] + offset),
  181. ptr, size);                /* 拷贝旧内存内容到新内存 */
  182.         myfree(memx, ptr);                     /* 释放旧内存 */
  183.         return (void *)((uint32_t)mallco_dev.membase[memx]
  184. + offset);          /* 返回新内存首地址 */
  185.     }
  186. }
复制代码
整个malloc代码的核心函数:my_mem_malloc和my_mem_free,分别用于内存申请和内存释放。思路就是前面42.1所介绍的分配内存和释放内存,不过在这里,这两个函数知识内部调用,外部调用我们另外定义了mymalloc和myfree两个函数,其它函数我们就不多介绍了。

2. main.c代码
在main.c下编写如下代码:

  1. int main(void)
  2. {
  3.     uint8_t paddr[20];                              /* 存放P Addr:+p地址的ASCII值 */
  4.     uint16_t memused = 0;
  5.     uint8_t key;
  6.     uint8_t i = 0;
  7.     uint8_t *p = 0;
  8.     uint8_t *tp = 0;

  9.     HAL_Init();                                      /* HAL库初始化 */
  10.     HAL_ICACHE_Enable();                              /* 使能指令缓存 */
  11.     sys_stm32_clock_init(250, 4, 2, 2);           /* 系统时钟初始化, 250Mhz */
  12.     delay_init(250);                                   /* 延时初始化 */
  13.     usart_init(115200);                                /* 串口初始化 */
  14.     usmart_dev.init(250);                             /* USMART初始化 */
  15.     led_init();                                         /* LED初始化 */
  16.     key_init();                                         /* 按键初始化 */
  17.     lcd_init();                                        /* LCD初始化 */

  18.     my_mem_init(SRAMIN);                             /* 内部SRAM内存池初始化 */

  19.     lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
  20.     lcd_show_string(30, 70, 200, 16, 16, "MALLOC TEST", RED);
  21.     lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
  22.     lcd_show_string(30, 110, 200, 16, 16, "KEY0: Malloc", RED);
  23.     lcd_show_string(30, 130, 200, 16, 16, "KEY1: Write Data", RED);
  24.     lcd_show_string(30, 150, 200, 16, 16, "KEY_UP: Free", RED);

  25.     lcd_show_string(30, 170, 200, 16, 16, "SRAMIN   USED:", BLUE);

  26.     while (1)
  27.     {
  28.         key = key_scan(0);                            /* 不支持连按 */

  29.         switch (key)
  30.         {
  31.             case KEY0_PRES:                            /* KEY0按下 */
  32.                 p = mymalloc(SRAMIN, 2048);        /* 申请2K字节 */

  33.                 if (p != NULL)
  34.                 {
  35. /* 向p写入一些内容 */
  36.                     sprintf((char *)p, "Memory Malloc Test%03d", i);
  37. /* 显示P的内容 */
  38.                     lcd_show_string(30, 230, 209, 16, 16, (char *)p, BLUE);
  39.                 }

  40.                 break;

  41.             case KEY1_PRES:                           /* KEY1按下 */
  42.                 if (p != NULL)
  43.                 {
  44.                     /* 更新显示内容 */
  45.                     sprintf((char *)p, "Memory Malloc Test%03d", i);
  46. /* 显示P的内容 */
  47.                     lcd_show_string(30, 230, 209, 16, 16, (char *)p, BLUE);
  48.                 }

  49.                 break;

  50.             case WKUP_PRES:                 /* KEY UP按下 */
  51.                 myfree(SRAMIN, p);           /* 释放内存 */
  52.                 p = 0;                        /* 指向空地址 */
  53.                 break;
  54.         }

  55.         if (tp != p )
  56.         {
  57.             tp = p;
  58.             sprintf((char *)paddr, "P Addr:0X%08X", (uint32_t)tp);
  59.             /* 显示p的地址 */
  60.             lcd_show_string(30, 210, 209, 16, 16, (char *)paddr, BLUE);

  61.             if (p)
  62.             {
  63.                 /* 显示P的内容 */
  64.                 lcd_show_string(30, 230, 280, 16, 16, (char *)p, BLUE);
  65.             }
  66.             else
  67.             {
  68.                 lcd_fill(30, 230, 209, 296, WHITE);  /* p=0,清除显示 */
  69.             }
  70.         }

  71.         delay_ms(10);
  72.         i++;

  73.         if ((i % 20) == 0)
  74.         {
  75.             memused = my_mem_perused(SRAMIN);
  76.             sprintf((char *)paddr, "%d.%01d%%", memused / 10, memused % 10);
  77. /* 显示内部内存使用率 */
  78.             lcd_show_string(30 + 112, 170, 200, 16, 16, (char *)paddr, BLUE);

  79.             LED0_TOGGLE();    /* LED0闪烁 */
  80.         }
  81.     }
  82. }
复制代码
该部分代码比较简单,主要是对mymalloc和myfree的应用。不过这里提醒大家,如果对一个指针进行多次内存申请,而之前的申请又没释放,那么将造成“内存泄露”,这是内存管理所不希望发生的,久而久之,可能导致无内存可用的情况!所以,在使用的时候,请大家一定记得,申请的内存在用完以后,一定要释放。
另外,本章希望利用USMART调试内存管理,所以在USMART里面添加了mymalloc和myfree两个函数,用于测试内存分配和内存释放。大家可以通过USMART自行测试。


42.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如42.4.1所示:

第四十二章 内存管理实验12506.png
图42.4.1 内存管理实验测试图

可以看到,内存的使用率均为0%,说明还没有任何内存被使用。此时我们按下KEY0按键申请内存并写入数据,就可以看到LCD屏上显示申请了0.3%的一个内存块,同时看到下面提示了指针p所指向的地址(其实就是被分配到的内存地址)和内容,如图48.4.2所示:

第四十二章 内存管理实验12652.png
图42.4.2 按下KEY0申请了部分内存

KEY0键用来更新p的内容,更新后的内容将重新显示在LCD模块上。多按几次KEY0,可以看到内存使用率持续上升(注意比对p的值,可以发现是递减的,说明是从顶部开始分配内存!)。每次申请一个内存块后,可以通过按下KEY0释放本次申请的内存,如果我们每次申请完内存不再使用却不及时释放掉, 再按 KEY1将无法释放之前的内存了,当这样的情况重复了多次,就会造成“内存泄漏”。我们程序就是模拟这样一个情况,告诉请大家在实际使用的时候去注意到这种做法的危险性,必须在编程时严格避免内存泄漏的情况发生。
本章,我们还可以借助 USMART,测试内存的分配和释放,有兴趣的朋友可以动手试试。如图42.4.3所示:


第四十二章 内存管理实验12977.png
图42.4.3 USMART测试内存管理图

图中,我们先申请了4660字节的内存,然后得到申请到的内存首地址为:0x2007DE20,说明我们申请内存成功(如果不成功,则会收到0),然后释放内存的时候,参数是指针的地址,即执行:myfree(0, 0x2007DE20),就可以释放我们申请到的内存。其它情况,大家可以自行测试并分析。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /1 下一条

正点原子公众号

如发现本坛存在违规或侵权内容, 请点击这里发送邮件举报 (或致电020-38271790)。请提供侵权说明和联系方式。我们将及时审核依法处理,感谢配合。

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2026-10-10 10:18

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表