嵌入式系统内存管理包:tzmalloc

13 篇文章 0 订阅

嵌入式系统内存管理包:tzmalloc

介绍

在嵌入式领域并没有好用的内存管理框架,一般是直接使用malloc进行内存分配。当内存泄漏时没有好的办法定位到问题,容易造成稳定性问题。所以一般不建议在单片机中使用malloc,就是出于这方面考虑。

tzmalloc是适合于嵌入式领域的内存管理包,可以追踪到每个模块的内存使用,内存使用次数等等。如果有内存泄漏问题,可以很方便的定位到出问题的模块。有了tzmalloc,则可放心的在单片机等嵌入式系统中进行内存分配。

开源

特性

  • 可以查看应用模块开辟空间,释放空间次数
  • 可以检测出内存分配错误
  • 支持多块独立的RAM

支持多块独立的RAM

有些单片机有多块RAM,比如单片机内部RAM,CCM以及外接RAM。tzmalloc支持多块独立的RAM,可以指定应用模块在某块RAM开辟空间。

默认最大支持3块RAM,bget.h中使用宏定义控制数量:

// suport ram num
#define BGET_RAM_NUM 3

bget

bget是一个有历史的内存分配包,创作于1972年,可替代标准库中的malloc。目前是一些RTOS内置的内存分配包。

tzmalloc使用bget作为内存分配的工具。

与malloc对比分配效率

实测bget比malloc分配效率高2倍左右:

测试1000万次内存分配和释放,每次分配1K字节。malloc测试需1006281us,bget需596616us:

在这里插入图片描述

bget的详细资料可参考:http://www.fourmilab.ch/bget/,可以从链接中下载bget源码。

单片机中使用assert

bget中大量使用了assert,如果芯片的开发环境使用标准库则已经支持。否则需要在MDK等环境中勾选使用MicroLib,并添加语句:

// 为mirco lib适配assert
void __aeabi_assert(uint8_t* file, uint32_t line) {
    while (1);
}

API

// user's tag max len
#define TZMALLOC_TAG_LEN_MAX 16
// support ram num
#define TZMALLOC_RAM_NUM 3
// max user nums each ram
#define TZMALLOC_SUPPORT_USER_NUM_MAX 10000

#pragma pack(1)

// user info
typedef struct {
    int RamIndex;
    char Tag[TZMALLOC_TAG_LEN_MAX + 1];
    uint32_t Total;
    uint32_t Used;
    uint32_t MallocNum;
    uint32_t FreeNum;
    uint32_t ExceptionNum;
} TZMallocUser;

// memory usage status information
typedef struct {
    long UsedSize;
    long FreeSize;
    long MaxFreeSize;
    long MallocNum;
    long FreeNum;
} TZMallocStatus;

#pragma pack()

// TZMallocLoad module load
bool TZMallocLoad(int ramIndex, int maxUserNum, int totalMemorySize, void* addr);

// TZMallocRegister register user
// total is user's max allowable size
// register success return mid,else return -1
int TZMallocRegister(int ramIndex, const char* tag, int total);

// TZMalloc allocate space.return NULL if failed
// allocated space value is 0
void* TZMalloc(int mid, int size);

// TZFree free space
void TZFree(void* data);

// TZMallocGetUser get user info
// return NULL if get failed
TZMallocUser* TZMallocGetUser(int mid);

// TZMallocGetUserNum get user num
int TZMallocGetUserNum(int ramIndex);

// TZMallocGetStatus get memory status
TZMallocStatus TZMallocGetStatus(int ramIndex);

使用方法

比如项目中有一个转发模块需要使用到内存分配,则需先定义模块名和最大分配的内存大小:

// tzmalloc标签和字节数
#define FORWARD_MALLOC_TAG "forward"
#define FORWARD_MALLOC_TOTAL 4096

然后获得一个句柄:

#define RAM_INTERNAL 0 
gMid = TZMallocRegister(RAM_INTERNAL, FORWARD_MALLOC_TAG, FORWARD_MALLOC_TOTAL);

后续在此模块内即可使用此句柄进行内存分配和释放:

// 分配
TZBufferDynamic* buffer = (TZBufferDynamic*)TZMalloc(gMid, sizeof(TZBufferDynamic) + NLV1_HEAD_LEN + frame->Size);
if (buffer == NULL) {
    return;
}

// 释放
TZFree(buffer);

在仿真时实时查看内存使用情况

如下图所示,在仿真调试可以查看某个软件模块当前使用内存大小,分配次数,释放次数,以及是否有分配异常。
在这里插入图片描述

支持多块RAM

比如某项目单片机有3块RAM:

  • 内部RAM
  • 内部CCM
  • 外部RAM
// 内存分为3片
// 单片机内部RAM
#define RAM_INTERNAL 0
// CCM RAM
#define RAM_CCM 1
// 外部RAM
#define RAM_EXTERNAL 2

// 给TZMALLOC使用的空间
// 使用的内部存储大小.单位:kbytes
#define RAM_INTERNAL_SIZE 70
// 使用的CCM大小
#define RAM_CCM_SIZE 64
// 使用的外部存储大小
#define RAM_EXTERNAL_SIZE 16384

则可使用以下代码载入:

__attribute__((section (".CCM_RAM"))) static uint8_t gCCMRam[RAM_CCM_SIZE * 1024];
__attribute__((section (".SDRAM")))  static uint8_t gExternalRam[RAM_EXTERNAL_SIZE * 1024];

void init(void) {
    void* addr = malloc((size_t)RAM_INTERNAL_SIZE * 1024);
    if (addr != NULL) {
        TZMallocLoad(RAM_INTERNAL, TZMALLOC_USER_NUM_MAX, RAM_INTERNAL_SIZE * 1024, addr);
    }

    TZMallocLoad(RAM_CCM, TZMALLOC_USER_NUM_MAX, RAM_CCM_SIZE * 1024, (void*)gCCMRam);
    TZMallocLoad(RAM_EXTERNAL, TZMALLOC_USER_NUM_MAX, RAM_EXTERNAL_SIZE * 1024, (void*)gExternalRam);
}

应用模块注册用户时设置RAM序号即可使用指定的RAM。

测试

测试框架使用的是适合于嵌入式系统的C语言单元测试框架:Scunit

使用cip可以安装依赖的包:cip:C/C++包管理工具

#include <stdio.h>
#include <string.h>
#include "tzmalloc.h"
#include "scunit.h"

#define RAM0 0
#define RAM1 1
#define RAM2 2

static int gMidRam0[20] = {0};
static int gMidRam1[20] = {0};
static int gMidRam2[20] = {0};

static void printAllUser(int ramIndex);
static void case0(void);
static void case1(void);
static void case2(void);
static void case3(void);
static void case4(void);

int main() {
    TZMallocLoad(RAM0, 20, 100 * 1000, malloc(100 * 1000));
    TZMallocLoad(RAM1, 20, 10 * 1000, malloc(10 * 1000));

    static uint8_t arr[5 * 1000] = {0};
    TZMallocLoad(RAM2, 20, 5 * 1000, arr);

    gMidRam0[0] = TZMallocRegister(RAM0, "RAM0-user0", 20);
    gMidRam0[1] = TZMallocRegister(RAM0, "RAM0-user1", 100);
    gMidRam0[2] = TZMallocRegister(RAM0, "RAM0-user2", 256);
    gMidRam1[0] = TZMallocRegister(RAM1, "RAM1-user0", 256);
    gMidRam2[0] = TZMallocRegister(RAM2, "RAM2-user0", 256);

    printAllUser(RAM0);
    printAllUser(RAM1);
    printAllUser(RAM2);


    ScunitLoad((ScunitPrintFunc)printf);
    ScunitAddSuite("test tzmalloc");
    ScunitAddTest("case0", case0);
    ScunitAddTest("case1", case1);
    ScunitAddTest("case2", case2);
    ScunitAddTest("case3", case3);
    ScunitAddTest("case4", case4);
    return 0;
}

static void printAllUser(int ramIndex) {
    printf("print ram:%d user\n", ramIndex);
    TZMallocUser* user;
    int num = TZMallocGetUserNum(ramIndex);
    for (int i = 0; i < num; i++) {
        user = TZMallocGetUser(TZMALLOC_SUPPORT_USER_NUM_MAX * ramIndex + i);
        printf("mid:%d tag:%s total:%d used:%d mallocNum:%d freeNum:%d\n", i,
               user->Tag, user->Total, user->Used, user->MallocNum, user->FreeNum);
    }
    TZMallocStatus status = TZMallocGetStatus(ramIndex);
    printf("tzmalloc status:UsedSize=%ld FreeSize=%ld MaxFreeSize=%ld MallocNum=%ld FreeNum=%ld\n", status.UsedSize,
        status.FreeSize, status.MaxFreeSize, status.MallocNum, status.FreeNum);
}

static void case0(void) {
    uint8_t* data = TZMalloc(gMidRam0[0], 30);
    ScunitAssertMessage(data == NULL, "case0", "data is null");

    data = TZMalloc(gMidRam0[1], 30);
    ScunitAssertMessage(data != NULL, "case0", "data is not null");
    TZMallocUser* user;
    if (data != NULL) {
        user = TZMallocGetUser(gMidRam0[1]);
        ScunitAssert(user->Used == 30 && user->MallocNum == 1 && user->FreeNum == 0, "case0");

        TZFree(data);
        ScunitAssert(user->Used == 0 && user->MallocNum == 1 && user->FreeNum == 1, "case0");
    }
    printAllUser(RAM0);
}

static void case1(void) {
    uint8_t* data[100] = {0};
    for (int i = 0; i < 100; i++) {
        data[i] = TZMalloc(gMidRam0[2], i + 31);
    }
    printAllUser(RAM0);
    for (int i = 0; i < 100; i++) {
        TZFree(data[i]);
        data[i] = NULL;
    }
    printAllUser(RAM0);

    TZMallocUser* user = TZMallocGetUser(gMidRam0[2]);
    ScunitAssert(user->Used == 0, "case1");
}

static void case2(void) {
    uint8_t* data = NULL;
    for (int i = 0; i < 10000; i++) {
        data = TZMalloc(gMidRam0[2], 232);
        TZFree(data);
        data = NULL;
    }
    printAllUser(RAM0);

    TZMallocUser* user = TZMallocGetUser(gMidRam0[2]);
    ScunitAssert(user->Used == 0, "case2");
}

static void case3(void) {
    uint8_t* data = NULL;
    for (int i = 0; i < 10000; i++) {
        data = TZMalloc(gMidRam1[0], 232);
        TZFree(data);
        data = NULL;
    }
    printAllUser(RAM1);

    TZMallocUser* user = TZMallocGetUser(gMidRam1[0]);
    ScunitAssert(user->Used == 0, "case3");
}

static void case4(void) {
    uint8_t* data = NULL;
    for (int i = 0; i < 10000; i++) {
        data = TZMalloc(gMidRam2[0], 232);
        TZFree(data);
        data = NULL;
    }
    printAllUser(RAM2);

    TZMallocUser* user = TZMallocGetUser(gMidRam2[0]);
    ScunitAssert(user->Used == 0, "case3");
}

输出:

print ram:0 user
mid:0 tag:RAM0-user0 total:20 used:0 mallocNum:0 freeNum:0
mid:1 tag:RAM0-user1 total:100 used:0 mallocNum:0 freeNum:0
mid:2 tag:RAM0-user2 total:256 used:0 mallocNum:0 freeNum:0
tzmalloc status:UsedSize=828 FreeSize=99164 MaxFreeSize=99164 MallocNum=1 FreeNum=0
print ram:1 user
mid:0 tag:RAM1-user0 total:256 used:0 mallocNum:0 freeNum:0
tzmalloc status:UsedSize=828 FreeSize=9164 MaxFreeSize=9164 MallocNum=1 FreeNum=0
print ram:2 user
mid:0 tag:RAM2-user0 total:256 used:0 mallocNum:0 freeNum:0
tzmalloc status:UsedSize=828 FreeSize=4164 MaxFreeSize=4164 MallocNum=1 FreeNum=0

Suite:test tzmalloc
-------------------->case:case0 begin
case0 tag:case0 SCUNIT_ASSERT pass
case0 tag:case0 SCUNIT_ASSERT pass
case0 tag:case0 SCUNIT_ASSERT pass
case0 tag:case0 SCUNIT_ASSERT pass
print ram:0 user
mid:0 tag:RAM0-user0 total:20 used:0 mallocNum:0 freeNum:0
mid:1 tag:RAM0-user1 total:100 used:0 mallocNum:1 freeNum:1
mid:2 tag:RAM0-user2 total:256 used:0 mallocNum:0 freeNum:0
tzmalloc status:UsedSize=828 FreeSize=99164 MaxFreeSize=99164 MallocNum=2 FreeNum=1
-------------------->case:case0 end

-------------------->case:case1 begin
print ram:0 user
mid:0 tag:RAM0-user0 total:20 used:0 mallocNum:0 freeNum:0
mid:1 tag:RAM0-user1 total:100 used:0 mallocNum:1 freeNum:1
mid:2 tag:RAM0-user2 total:256 used:238 mallocNum:7 freeNum:0
tzmalloc status:UsedSize=1216 FreeSize=98776 MaxFreeSize=98776 MallocNum=9 FreeNum=1
print ram:0 user
mid:0 tag:RAM0-user0 total:20 used:0 mallocNum:0 freeNum:0
mid:1 tag:RAM0-user1 total:100 used:0 mallocNum:1 freeNum:1
mid:2 tag:RAM0-user2 total:256 used:0 mallocNum:7 freeNum:7
tzmalloc status:UsedSize=828 FreeSize=99164 MaxFreeSize=99164 MallocNum=9 FreeNum=8
case1 tag:case1 SCUNIT_ASSERT pass
-------------------->case:case1 end

-------------------->case:case2 begin
print ram:0 user
mid:0 tag:RAM0-user0 total:20 used:0 mallocNum:0 freeNum:0
mid:1 tag:RAM0-user1 total:100 used:0 mallocNum:1 freeNum:1
mid:2 tag:RAM0-user2 total:256 used:0 mallocNum:10007 freeNum:10007
tzmalloc status:UsedSize=828 FreeSize=99164 MaxFreeSize=99164 MallocNum=10009 FreeNum=10008
case2 tag:case2 SCUNIT_ASSERT pass
-------------------->case:case2 end

-------------------->case:case3 begin
print ram:1 user
mid:0 tag:RAM1-user0 total:256 used:0 mallocNum:10000 freeNum:10000
tzmalloc status:UsedSize=828 FreeSize=9164 MaxFreeSize=9164 MallocNum=10001 FreeNum=10000
case3 tag:case3 SCUNIT_ASSERT pass
-------------------->case:case3 end

-------------------->case:case4 begin
print ram:2 user
mid:0 tag:RAM2-user0 total:256 used:0 mallocNum:10000 freeNum:10000
tzmalloc status:UsedSize=828 FreeSize=4164 MaxFreeSize=4164 MallocNum=10001 FreeNum=10000
case4 tag:case3 SCUNIT_ASSERT pass
-------------------->case:case4 end

源码

最新代码请查看github/gitee仓库。

tzmalloc.h

// Copyright 2019-2021 The jdh99 Authors. All rights reserved.
// Secure memory allocation library for C
// Authors: jdh99 <jdh821@163.com>

#ifndef TZMALLOC_H
#define TZMALLOC_H

#include <stdint.h>
#include <stdbool.h>

// user's tag max len
#define TZMALLOC_TAG_LEN_MAX 16
// support ram num
#define TZMALLOC_RAM_NUM 3
// max user nums each ram
#define TZMALLOC_SUPPORT_USER_NUM_MAX 10000

#pragma pack(1)

// user info
typedef struct {
    int RamIndex;
    char Tag[TZMALLOC_TAG_LEN_MAX + 1];
    uint32_t Total;
    uint32_t Used;
    uint32_t MallocNum;
    uint32_t FreeNum;
    uint32_t ExceptionNum;
} TZMallocUser;

// memory usage status information
typedef struct {
    long UsedSize;
    long FreeSize;
    long MaxFreeSize;
    long MallocNum;
    long FreeNum;
} TZMallocStatus;

#pragma pack()

// TZMallocLoad module load
bool TZMallocLoad(int ramIndex, int maxUserNum, int totalMemorySize, void* addr);

// TZMallocRegister register user
// total is user's max allowable size
// register success return mid,else return -1
int TZMallocRegister(int ramIndex, const char* tag, int total);

// TZMalloc allocate space.return NULL if failed
// allocated space value is 0
void* TZMalloc(int mid, int size);

// TZFree free space
void TZFree(void* data);

// TZMallocGetUser get user info
// return NULL if get failed
TZMallocUser* TZMallocGetUser(int mid);

// TZMallocGetUserNum get user num
int TZMallocGetUserNum(int ramIndex);

// TZMallocGetStatus get memory status
TZMallocStatus TZMallocGetStatus(int ramIndex);

#endif

tzmalloc.c

// Copyright 2019-2021 The jdh99 Authors. All rights reserved.
// Secure memory allocation library for C
// Authors: jdh99 <jdh821@163.com>

#include "tzmalloc.h"
#include "bget.h"
#include <string.h>

#define MAGIC_NUMBER 0x2020

#pragma pack(1)
// tUnit malloc unit header
typedef struct {
    uint16_t magicNumber;
    uint8_t ramIndex;
    uint8_t mid;
    uint32_t size;
    uint8_t reserved[3];
    // check must the last field
    uint8_t check;
} tUnit;

#pragma pack()

static int gMaxUserNum[TZMALLOC_RAM_NUM] = {0};
static TZMallocUser* gUsers[TZMALLOC_RAM_NUM] = {NULL};
static int gUsersNum[TZMALLOC_RAM_NUM] = {0};

static uint8_t calcCheck(tUnit* unit);

// TZMallocLoad module load
bool TZMallocLoad(int ramIndex, int maxUserNum, int totalMemorySize, void* addr) {
    if (gMaxUserNum[ramIndex] == maxUserNum) {
        return true;
    }
    if (gUsersNum[ramIndex] != 0) {
        return false;
    }

    bpool(ramIndex, addr, totalMemorySize);
    gUsers[ramIndex] = bget(ramIndex, (size_t)maxUserNum * sizeof(TZMallocUser));
    if (gUsers[ramIndex] == NULL) {
        return false;
    }
    gMaxUserNum[ramIndex] = maxUserNum;
    return true;
}

// TZMallocRegister register user
// total is user's max allowable size
// register success return mid,else return -1
int TZMallocRegister(int ramIndex, const char* tag, int total) {
    if (gUsersNum[ramIndex] >= gMaxUserNum[ramIndex]) {
        return -1;
    }

    int tagLen = (int)strlen(tag);
    if (tagLen == 0) {
        return -1;
    }
    if (tagLen > TZMALLOC_TAG_LEN_MAX) {
        tagLen = TZMALLOC_TAG_LEN_MAX;
    }

    memset(&gUsers[ramIndex][gUsersNum[ramIndex]], 0, sizeof(TZMallocUser));
    gUsers[ramIndex][gUsersNum[ramIndex]].RamIndex = ramIndex;
    memcpy(gUsers[ramIndex][gUsersNum[ramIndex]].Tag, tag, (size_t)tagLen);
    gUsers[ramIndex][gUsersNum[ramIndex]].Total = (uint32_t)total;
    gUsersNum[ramIndex]++;

    return (TZMALLOC_SUPPORT_USER_NUM_MAX * ramIndex + gUsersNum[ramIndex] - 1);
}

// TZMalloc allocate space.return NULL if failed
// allocated space value is 0
void* TZMalloc(int mid, int size) {
    if (mid < 0 || size <= 0) {
        return NULL;
    }

    int ramIndex = mid / TZMALLOC_SUPPORT_USER_NUM_MAX;
    mid %= TZMALLOC_SUPPORT_USER_NUM_MAX;

    if (mid >= gUsersNum[ramIndex]) {
        return NULL;
    }
    if (gUsers[ramIndex][mid].Total <= gUsers[ramIndex][mid].Used) {
        if (gUsers[ramIndex][mid].Total < gUsers[ramIndex][mid].Used) {
            gUsers[ramIndex][mid].ExceptionNum++;
        }
        return NULL;
    }
    if (size > (int)(gUsers[ramIndex][mid].Total - gUsers[ramIndex][mid].Used)) {
        return NULL;
    }

    uint8_t* data = bget(ramIndex, sizeof(tUnit) + (size_t)size);
    if (data == NULL) {
        return NULL;
    }
    memset(data, 0, sizeof(tUnit) + (size_t)size);

    tUnit* unit = (tUnit*)data;
    unit->magicNumber = MAGIC_NUMBER;
    unit->ramIndex = (uint8_t)ramIndex;
    unit->mid = (uint8_t)mid;
    unit->size = (uint32_t)size;
    unit->check = calcCheck(unit);

    gUsers[ramIndex][mid].Used += (uint32_t)size;
    gUsers[ramIndex][mid].MallocNum++;

    return data + sizeof(tUnit);
}

static uint8_t calcCheck(tUnit* unit) {
    uint8_t check = 0;
    uint8_t* data = (uint8_t*)unit;
    int num = sizeof(tUnit) - 1;
    for (int i = 0; i < num; i++) {
        check ^= data[i];
    }
    return check;
}

// TZFree free space
void TZFree(void* data) {
    if (data == NULL) {
        return;
    }
    tUnit* unit = (tUnit*)((intptr_t)data - (intptr_t)sizeof(tUnit));
    if (unit->mid > gUsersNum[unit->ramIndex]) {
        gUsers[unit->ramIndex][unit->mid].ExceptionNum++;
        return;
    }
    if (unit->magicNumber != MAGIC_NUMBER || unit->check != calcCheck(unit)) {
        gUsers[unit->ramIndex][unit->mid].ExceptionNum++;
        return;
    }

    if (gUsers[unit->ramIndex][unit->mid].Used >= unit->size) {
        gUsers[unit->ramIndex][unit->mid].Used -= unit->size;
    } else {
        gUsers[unit->ramIndex][unit->mid].ExceptionNum++;
        return;
    }
    gUsers[unit->ramIndex][unit->mid].FreeNum++;
    brel(unit->ramIndex, unit);
}

// TZMallocGetUser get user info
// return NULL if get failed
TZMallocUser* TZMallocGetUser(int mid) {
    if (mid < 0) {
        return NULL;
    }

    int ramIndex = mid / TZMALLOC_SUPPORT_USER_NUM_MAX;
    mid %= TZMALLOC_SUPPORT_USER_NUM_MAX;

    if (mid >= gUsersNum[ramIndex]) {
        return NULL;
    }
    return &gUsers[ramIndex][mid];
}

// TZMallocGetUser get user info
// return NULL if get failed
int TZMallocGetUserNum(int ramIndex) {
    return gUsersNum[ramIndex];
}

// TZMallocGetStatus get memory status
TZMallocStatus TZMallocGetStatus(int ramIndex) {
    TZMallocStatus status;
    bstats(ramIndex, &status.UsedSize, &status.FreeSize, &status.MaxFreeSize, &status.MallocNum, &status.FreeNum);
    return status;
}
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《STM32嵌入系统开发实战指南:FreeRTOS与LwIP联合移植》是一本针对STM32嵌入系统开发的书籍,主要介绍了如何在STM32平台上实现FreeRTOS实时操作系统和LwIP网络协议栈的联合移植。该书籍通过理论与实践相结合的方,详细介绍了如何进行STM32芯片的初始化配置、FreeRTOS操作系统的移植及应用、LwIP网络协议栈的移植及应用等内容。 在《STM32嵌入系统开发实战指南:FreeRTOS与LwIP联合移植》中,作者通过实际的案例和项目演示,向读者展示了如何利用STM32CubeMX工具进行芯片初始化配置,如何移植FreeRTOS实时操作系统,并详细介绍了FreeRTOS任务管理、任务间通信、时间管理内存管理等内容。同时,书中还介绍了LwIP网络协议栈的基本原理、移植方法以及在实际项目中的应用。 该书籍的特点是系统性强,内容全面,操作实用,适合嵌入系统开发初学者和从业人员使用。通过学习本书,读者可以全面掌握STM32芯片的初始化配置方法,了解FreeRTOS实时操作系统的移植和使用技巧,掌握LwIP网络协议栈的移植和应用实践,从而在实际项目中快速应用于嵌入系统开发中。 总的来说,《STM32嵌入系统开发实战指南:FreeRTOS与LwIP联合移植》通过丰富的案例和实践,帮助读者快速掌握STM32嵌入系统开发的关键技术和方法,是一本值得推荐的实用性书籍。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值