嵌入式开发中的极致轻量化AES实现:tiny-AES-c源码解析与实战

1. 项目概述:为什么我们需要极致轻量化的AES

在嵌入式开发领域,资源永远是第一位的考量。当你的MCU只有几十KB的Flash和几KB的RAM时,任何库的引入都必须慎之又慎。AES(高级加密标准)作为目前应用最广泛的对称加密算法,其官方参考实现往往动辄十几KB的代码量,这对于许多资源受限的嵌入式设备来说,几乎是不可承受之重。这就是 tiny-AES-c 这类项目存在的根本价值——它不是一个功能阉割版,而是一个为嵌入式环境量身定制的、经过极致优化的AES实现。

我最早接触 tiny-AES-c 是在一个基于STM32F030的项目上,那个芯片只有64KB Flash和8KB RAM,项目需要与云端进行加密通信。当时尝试了OpenSSL的移植,光是静态链接进去就几乎耗尽了所有空间。后来找到了 tiny-AES-c ,整个C文件编译后,代码体积仅增加了不到3KB,RAM占用几乎可以忽略不计,完美解决了问题。从那以后,它就成了我嵌入式加密方案的首选“瑞士军刀”。

简单来说, tiny-AES-c 就是一个用纯C语言编写的、高度可移植的AES-128、AES-192和AES-256加密/解密库。它的核心目标不是追求极致的运行速度(虽然也不慢),而是在保证算法正确性和安全性的前提下,将代码体积和内存占用压缩到极致。它没有依赖任何外部库,甚至对标准库的依赖都降到了最低,一个 .c 文件和一个 .h 文件就是全部,你可以轻松地把它丢进任何嵌入式项目的源码树里直接编译。

2. 核心设计思路与架构拆解

2.1 轻量化的哲学:牺牲什么,保留什么

要实现极致的轻量化,必然有所取舍。 tiny-AES-c 的设计哲学非常明确: 优先保证正确性、安全性和可移植性,在此前提下,极力压缩存储空间(ROM)占用,并谨慎优化运行效率(CPU和RAM)

它主要从以下几个层面进行“瘦身”:

  1. 算法层面 :完整实现了AES的标准流程,包括密钥扩展、字节替换、行移位、列混合和轮密钥加等所有步骤。没有使用任何会引入额外存储开销的“捷径”或简化算法,确保加密强度与标准AES完全一致。这是它的底线。
  2. 代码层面
    • 无动态内存分配 :所有操作都在栈或静态数组上完成,避免了 malloc/free 的开销和碎片化风险,这对没有成熟内存管理单元的MCU至关重要。
    • 精简的API :只提供最核心的 AES_ECB_encrypt/decrypt AES_CBC_encrypt/decrypt 等几个函数接口。像CTR、GCM等更复杂的模式需要用户基于基础块操作自行实现,这减少了代码量。
    • 编译时配置 :通过宏定义(如 AES128 AES192 AES256 )在编译时决定支持哪种密钥长度,避免将三种实现的代码全部链接进去。
    • 查表法的极致优化 :AES算法中的S盒(SubBytes)和逆S盒,以及列混合所需的预计算表,是代码体积的大头。 tiny-AES-c 对此做了精心设计。

2.2 核心数据结构与查表策略解析

这是理解其轻量化的关键。AES算法中, SubBytes (字节替换)操作通常通过一个256字节的查找表(S盒)来完成。加解密各需要一个S盒和逆S盒,这就是1KB的只读数据。此外,为了优化 MixColumns (列混合)操作,通常还会使用4个1KB的预计算表(T-table),这又会增加4KB的ROM占用。

tiny-AES-c 采用了更节省空间的策略:

  • 默认使用计算型S盒 :它没有将S盒作为静态常量数组存储,而是在代码中通过一个 getSBoxValue 函数动态计算。这个函数内部是一个紧凑的算法,用几十行代码替代了256字节的存储空间。这是其ROM占用极低的主要原因。
  • 提供可选的静态S盒 :同时,它也定义了 STATIC_TABLES 宏。如果用户定义此宏,则会使用静态的、预计算好的S盒数组。这会增加约0.5KB的ROM,但能显著提升运行速度(因为省去了每次查表时的计算开销)。这种设计给了开发者“空间换时间”的选择权。
  • 省略T-table tiny-AES-c 的列混合操作是通过公式直接计算实现的,而不是查T-table。这避免了4KB的巨大开销,代价是加密速度会慢一些。但对于很多嵌入式场景,每秒加密几十个数据块的速度已经足够,节省下的4KB Flash空间却可能是无价的。
// tiny-AES-c 中计算型S盒的示例(简化逻辑)
static uint8_t getSBoxValue(uint8_t num) {
    // 这里是一个复杂的仿射变换计算过程,而非直接查表
    // ... 计算代码 ...
    return rsbox[num]; // 或者 sbox[num], 取决于加密还是解密
}

// 如果定义了 STATIC_TABLES, 则直接使用预计算的数组
#if defined(STATIC_TABLES)
static const uint8_t sbox[256] = { 0x63, 0x7c, ... };
static const uint8_t rsbox[256] = { 0x52, 0x09, ... };
#endif

2.3 可移植性设计

tiny-AES-c 的整个代码库只使用了C99标准。它通过 typedef 自定义了 uint8_t 等类型,确保在任何平台上,只要包含 stdint.h (或提供类似定义)就能编译。它不依赖任何操作系统特性(如线程、文件IO),甚至对标准库的依赖也仅限于最基本的类型定义。你可以把它移植到Arduino、ESP32、STM32、甚至是8051内核的老古董MCU上。

注意 :这种极致的可移植性也意味着它不提供“开箱即用”的高级功能,如自动填充(PKCS#7)、加密模式初始化向量(IV)的自动生成与管理等。这些都需要应用层开发者自己处理,这既是灵活性,也增加了使用的复杂度。

3. 源码深度解析与关键函数剖析

要真正用好一个库,不能只停留在调用API,必须深入其实现,明白它每一步在做什么。我们以最常用的AES-128 CBC模式加密为例,拆解 tiny-AES-c 的核心流程。

3.1 密钥扩展(Key Expansion)

这是AES的第一步,也是安全的基础。它将一个短的初始密钥(16字节 for AES-128)扩展成一系列用于各轮加密的轮密钥(Round Keys)。

void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key) {
    KeyExpansion(ctx->RoundKey, key);
}

KeyExpansion 函数是核心。它遵循AES标准,使用Rijndael的密钥调度算法。对于AES-128,需要生成11个轮密钥(第0轮为初始密钥,1-10轮为扩展密钥)。 tiny-AES-c 的实现非常直接,逐字节计算,其中用到了 getSBoxValue 函数进行S盒替换,以及Rcon轮常量。这里的内存占用是固定的: ctx->RoundKey 是一个 uint8_t 数组,大小为 (Nr+1)*Nb * 4 字节。对于AES-128, Nr=10, Nb=4, 所以是 11*4*4 = 176 字节。这部分内存通常分配在栈上(如果ctx是局部变量)或静态区。

3.2 加密主循环:Cipher

这是加密的核心函数,执行一轮又一轮的变换。我们看 Cipher 函数(用于ECB模式的一个块加密)。

static void Cipher(state_t* state, const uint8_t* RoundKey) {
    uint8_t round = 0;
    // 第0轮:仅加轮密钥
    AddRoundKey(0, state, RoundKey);
    // 第1到Nr-1轮:四步变换
    for (round = 1; round < Nr; ++round) {
        SubBytes(state);
        ShiftRows(state);
        MixColumns(state);
        AddRoundKey(round, state, RoundKey);
    }
    // 最后一轮:省略列混合
    SubBytes(state);
    ShiftRows(state);
    AddRoundKey(Nr, state, RoundKey);
}

逻辑清晰明了:

  1. AddRoundKey :状态矩阵与轮密钥进行异或。
  2. SubBytes :通过 getSBoxValue 函数或静态S盒,对每个字节进行非线性替换。
  3. ShiftRows :对状态矩阵的行进行循环移位。
  4. MixColumns :对状态矩阵的列进行线性变换,这是通过公式计算实现的,而非查表。

实操心得 :在调试加密结果不对时,我常用的方法是 逐轮打印状态矩阵 。在 Cipher 函数的每个 AddRoundKey 之后,将 state 的16个字节以十六进制打印出来。然后与官方测试向量(例如NIST发布的已知答案测试)的中间结果进行比对。这样可以快速定位问题出在哪一轮,甚至是哪一个变换步骤。

3.3 加密模式:以CBC为例

tiny-AES-c 直接提供了CBC模式的封装函数 AES_CBC_encrypt_buffer 。CBC(密码分组链接)模式是常用的模式,它能有效隐藏明文的模式。

void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, uint32_t length) {
    uintptr_t i;
    uint8_t* Iv = ctx->Iv;
    for (i = 0; i < length; i += AES_BLOCKLEN) {
        XorWithIv(buf, Iv); // 第一步:与IV或前一个密文块异或
        Cipher((state_t*)buf, ctx->RoundKey); // 第二步:进行ECB加密
        Iv = buf; // 第三步:更新IV为当前密文块
        buf += AES_BLOCKLEN;
    }
    // 加密完成后,需要将最终的IV存回ctx,以便解密时使用
    memcpy(ctx->Iv, Iv, AES_BLOCKLEN);
}

这个实现非常经典:

  1. 将当前明文块与前一个密文块(或初始IV)进行异或。
  2. 对异或后的结果进行标准的AES块加密(ECB)。
  3. 输出的密文块作为下一个块的IV。

这里有一个至关重要的细节 tiny-AES-c AES_CBC_encrypt_buffer 函数会 修改传入的 ctx->Iv 。加密完成后, ctx->Iv 会被更新为最后一个密文块。这个设计意味着:

  • 加密连续数据流时是方便的 :你可以分多次调用该函数加密一段很长的数据,每次传入的 ctx 是同一个,其内部的 Iv 会自动衔接。
  • 解密时必须注意 :解密函数 AES_CBC_decrypt_buffer 期望的 ctx->Iv 是加密完成后最终的那个IV(即最后一个密文块)。如果你在加密后把 ctx 保存下来用于后续解密,那么它是正确的。但如果你是独立加密/解密,就必须手动保存并传递这个最终的IV。

4. 在嵌入式项目中的集成与实战

4.1 集成步骤

  1. 获取源码 :直接从GitHub仓库下载 aes.c aes.h
  2. 添加到项目 :将这两个文件放入你的项目源码目录。
  3. 配置宏 :在 aes.h 或你的编译器全局宏定义中,设置你需要的选项。最关键的几个:
    • AES128 AES192 AES256 必须且只能定义一个 ,决定密钥长度。
    • ECB CBC :定义你需要的加密模式,可以同时定义。
    • STATIC_TABLES :如果Flash空间充裕且追求速度,定义它。
    • AES_BLOCKLEN :通常为16,除非你修改了AES块大小(极不推荐)。
  4. 包含头文件 :在需要使用AES的文件中 #include “aes.h“
  5. 编译链接 :确保 aes.c 被加入编译列表。

4.2 实战示例:STM32 HAL库下的数据加密传输

假设我们有一个STM32F103,通过UART向上位机发送传感器数据,需要加密。

// aes_comm.c
#include “aes.h“
#include “string.h“

// 1. 定义密钥和初始化向量 (IV)
// 警告:实际项目中,密钥绝不能硬编码!应从安全存储区读取或通过安全协议协商。
static const uint8_t aes_key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
                                     0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c};
static uint8_t aes_iv[16] = {0}; // 初始化向量,通常需要是随机数

// 2. 封装一个加密函数
int32_t encrypt_sensor_data(uint8_t* plaintext, uint32_t pt_len, uint8_t* ciphertext, uint32_t* ct_len) {
    struct AES_ctx ctx;
    uint32_t padded_len;
    uint8_t padding;

    // 2.1 计算填充长度 (PKCS#7)
    padding = AES_BLOCKLEN - (pt_len % AES_BLOCKLEN);
    padded_len = pt_len + padding;

    // 检查输出缓冲区是否足够大
    if (*ct_len < padded_len) {
        return -1; // 缓冲区不足
    }

    // 2.2 复制明文并添加填充
    memcpy(ciphertext, plaintext, pt_len);
    for (uint32_t i = 0; i < padding; i++) {
        ciphertext[pt_len + i] = padding;
    }

    // 2.3 初始化AES上下文 (设置密钥和IV)
    AES_init_ctx_iv(&ctx, aes_key, aes_iv);

    // 2.4 执行CBC加密
    AES_CBC_encrypt_buffer(&ctx, ciphertext, padded_len);

    // 2.5 更新输出长度和IV (加密后ctx.Iv已更新)
    *ct_len = padded_len;
    memcpy(aes_iv, ctx.Iv, AES_BLOCKLEN); // 保存新的IV供下次加密使用

    return 0; // 成功
}

// 在主循环中调用
void send_encrypted_data(float temperature, float humidity) {
    uint8_t tx_buffer[64]; // 明文缓冲区
    uint8_t enc_buffer[64]; // 密文缓冲区
    uint32_t enc_len = sizeof(enc_buffer);
    int32_t ret;

    // 构造明文数据 (例如: “T=25.6,H=60.2“)
    int len = snprintf((char*)tx_buffer, sizeof(tx_buffer), “T=%.1f,H=%.1f“, temperature, humidity);

    // 加密
    ret = encrypt_sensor_data(tx_buffer, len, enc_buffer, &enc_len);
    if (ret == 0) {
        // 通过UART发送密文
        HAL_UART_Transmit(&huart1, enc_buffer, enc_len, HAL_MAX_DELAY);
    }
}

4.3 内存与性能实测分析

为了量化其“轻量化”,我在STM32F103C8T6(64KB Flash, 20KB RAM)上做了一次实测,使用ARM GCC编译器,优化等级为 -Os (优化尺寸)。

配置 代码大小 (Flash) 增加量 RAM 占用 (全局变量) 备注
基础 (AES128 + CBC) ~2.8 KB ~200 字节 包含 AES_init_ctx_iv CBC_encrypt_buffer
+ STATIC_TABLES ~3.3 KB ~0.7 KB 增加了静态S盒和逆S盒,速度提升约30%
对比: mbedTLS AES ~12 KB+ ~1-2 KB 功能更全,但体积大得多

从数据可以看出, tiny-AES-c 在默认配置下,Flash占用控制在3KB以内,这对于很多只有32KB甚至16KB Flash的MCU来说是可行的。RAM占用主要是上下文结构体和一些栈上的临时变量,非常低。

性能方面,在72MHz的STM32F103上,加密一个16字节的数据块(AES-128 CBC)大约需要 70-100微秒 (取决于是否使用静态表)。这意味着每秒可以加密约1万到1.4万个数据块,对于大多数传感器数据上报、指令加密等场景,性能完全过剩。

5. 常见问题、安全陷阱与排查技巧

即使代码简单,在实际使用中也会遇到各种坑。以下是我和同事们踩过的一些典型问题。

5.1 数据对齐问题

AES操作通常假设数据是32位对齐的,这有助于提高访问速度。 tiny-AES-c 的内部 state_t 类型通常是 uint8_t 数组,本身没有对齐要求。但是,如果你传递给 AES_CBC_encrypt_buffer 的缓冲区指针 buf 指向一个非对齐地址,在某些架构(特别是ARM Cortex-M系列,当启用硬件对齐检查时)可能会导致硬件错误或性能下降。

排查技巧 :如果遇到神秘的硬件错误(HardFault),特别是在加密函数内部,首先检查传入缓冲区的地址。确保 buf 是4字节对齐的。在分配缓冲区时,可以使用编译器属性(如 __attribute__((aligned(4))) )或动态内存分配时使用对齐的分配函数。

5.2 填充(Padding)方案不匹配

tiny-AES-c 不处理填充。你必须自己实现填充和去填充。最常用的是PKCS#7。问题往往出现在 加密端和解密端使用了不同的填充方案 ,或者解密后去填充的逻辑有误。

症状 :解密出来的数据末尾有乱码,或者去填充时计算出错(例如,填充值大于16或等于0)。

解决方案

  1. 双方明确约定使用同一种填充标准(强烈推荐PKCS#7)。
  2. 在解密后,严格验证填充字节。PKCS#7的规则是:最后一个字节的值 pad 表示填充的长度,且所有填充字节的值都必须等于 pad
    // 解密后,验证PKCS#7填充
    uint8_t pad = decrypted_data[data_len - 1];
    if (pad == 0 || pad > AES_BLOCKLEN) {
        // 无效填充
        return ERROR_INVALID_PADDING;
    }
    for (uint8_t i = 0; i < pad; i++) {
        if (decrypted_data[data_len - 1 - i] != pad) {
            // 无效填充
            return ERROR_INVALID_PADDING;
        }
    }
    // 去除填充
    *output_len = data_len - pad;
    

5.3 初始化向量(IV)管理混乱

这是CBC模式最常见的问题。IV必须是随机的、不可预测的,并且每次加密会话都应不同。但更重要的是, 加密端和解密端必须使用相同的IV

常见错误

  • 加密后IV未更新/保存 :如前所述, tiny-AES-c 的CBC加密函数会修改 ctx.Iv 。如果你加密一段数据后,用同一个 ctx (其IV已变)去加密下一段数据,这是正确的。但如果你需要将密文和IV分开传输,就必须在加密 保存原始的IV,并将这个原始IV发送给解密方。解密方用这个原始IV初始化 ctx
  • 固定IV :永远不要使用全零或固定的IV。这会使CBC模式的安全性大打折扣。对于嵌入式设备,可以从硬件随机数发生器(如果可用)、ADC采样噪声、RTC计时器等获取熵源来生成IV。

5.4 多任务/中断环境下的重入问题

tiny-AES-c 的函数本身是 可重入的 (因为它们不修改全局变量,所有状态都在传入的 ctx 结构体中)。但是,如果你在中断服务程序(ISR)和主循环中 共享同一个 struct AES_ctx 变量 ,就会发生数据竞争,导致加密结果错误或系统崩溃。

解决方案

  1. 为每个执行上下文提供独立的 ctx 实例 。例如,ISR用 ctx_isr ,主循环用 ctx_main
  2. 如果必须共享,则需要使用互斥锁(mutex)或关中断等同步机制来保护对 ctx 的访问。但在资源极度受限的系统中,方案1更简单可靠。

5.5 编译警告与优化

tiny-AES-c 的代码质量很高,但在高警告级别的编译器设置下,可能会看到关于“未使用的函数参数”或“指针类型转换”的警告。通常这些警告可以安全忽略。如果你希望代码完全干净,可以修改源码或调整编译器警告选项。

关于优化,务必使用 -Os (优化尺寸)选项进行编译。对于性能敏感的场景,可以尝试 -O2 ,但体积会增加。 切勿使用 -O3 ,它可能会进行过于激进的循环展开和内联,导致代码体积爆炸,违背了使用 tiny-AES-c 的初衷。

6. 进阶话题:与其他轻量化方案的对比与选型

tiny-AES-c 并非唯一选择。在嵌入式加密领域,还有其他一些优秀的轻量化实现。

方案 语言/依赖 代码体积 特点 适用场景
tiny-AES-c 纯C, 无依赖 极小 (~3KB) 极致轻量,可移植性最强,速度适中 资源极度受限的8/16/32位MCU, 对体积敏感
mbedTLS AES C, 依赖mbedTLS库 较大 (~12KB+) 功能完整,支持多种算法和模式,经过充分审计,生态好 资源相对宽松(>64KB Flash),需要TLS/DTLS等完整协议栈
wolfSSL AES C, 依赖wolfSSL库 中等 (~8KB+) 强调速度和安全性,有商业支持,可高度裁剪 对性能有要求,且有商业支持需求的物联网设备
硬件加密引擎 芯片内置 接近0 (驱动代码) 速度极快,功耗低,不占用CPU 芯片自带AES加速器(如STM32L4, ESP32, nRF52系列)

如何选择?

  1. 首先检查你的MCU是否有硬件AES加速器 。如果有,毫不犹豫地使用它。这是性能、功耗和代码体积的最优解。 tiny-AES-c 可以作为备用方案,当硬件引擎不可用时进行软件回退。
  2. 如果只有软件实现可选 ,评估你的Flash和RAM预算。如果空间捉襟见肘(比如总Flash<32KB), tiny-AES-c 几乎是唯一可行的选择。
  3. 如果你的项目未来可能扩展,需要TLS等更复杂的加密协议 ,那么从一开始就集成一个更完整的库如mbedTLS可能是更省事的,尽管初期体积大些。
  4. 考虑安全审计和长期维护 tiny-AES-c 代码简单,易于审计。而mbedTLS、wolfSSL等有专业团队维护,响应安全漏洞更快。

我个人在资源受限的裸机或RTOS项目中,90%的情况会首选 tiny-AES-c 。它的简洁、透明和可控性让我非常放心。只有当项目明确需要更复杂的密码学套件,或者MCU资源足够丰富时,我才会考虑更重量级的库。

内容概要:本文围绕“光伏-储能-数据中心”系统的容量优化配置展开研究,旨在通过构建数学模型并结合Matlab编程实现,对园区内光伏发电、储能设备数据中心算力负荷之间的多能互补关系进行协同优化。研究综合考虑可再生能源出力的波动性、储能系统的充放电特性以及数据中心的动态用电需求算力调度特性,建立以最小化综合成本、降低碳排放强度、提升能源自给率等为目标的多目标优化模型,并采用先进的智能优化算法(如粒子群、灰狼、鲸鱼等)进行求解,从而确定光伏装机容量储能系统配置的最优方案。文中提供了完整的Matlab代码实现流程,涵盖数据预处理、模型构建、算法求解结果可视化全过程,属于综合能源系统信息基础设施深度融合的前沿交叉课题。; 适合人群:具备电力系统、能源工程、自动化或计算机等相关专业背景,熟悉Matlab编程基本优化算法原理,从事新能源利用、绿色数据中心、综合能源系统规划低碳调度等方向的研究生、科研人员及工程技术人员。; 使用场景及目标:① 掌握光伏-储能-数据中心耦合系统的建模思路多能流协同机制;② 学习基于Matlab的多目标容量优化配置方法智能算法应用技巧;③ 为工业园区、数字经济园区及绿色数据中心的能源系统规划、节能减排可持续运行提供科学决策依据和技术解决方案。; 阅读建议:建议结合所提供的Matlab代码,深入理解目标函数设计、约束条设定及算法实现细节,可通过调整负荷参数、改变气候数据、引入新的优化目标或约束条等方式进行拓展性实验,以深化对系统特性的认知并提升实际科研工程应用能力。
内容概要:本文档聚焦于“考虑源网荷储协调的主动配电网优化调度方法”的Matlab代码复现研究,属于电力系统综合能源系统领域的高水平科研资源。文档系统阐述了融合电源侧、电网侧、负荷侧及储能系统(源---储)协同优化的主动配电网调度模型,旨在通过先进的数学优化方法提升电力系统的经济性、可靠性低碳化水平。研究内容涵盖多时间尺度调度架构、不确定性建模(如新能源出力波动)、鲁棒优化或分布鲁棒优化(DRO)等核心技术,并依托Matlab平台完成模型构建仿真验证。同时,文档列举了多个前沿研究方向,如光储充一体化、主从博弈、微电网双层优化、电氢耦合系统、电动汽车集群调度等,体现出其在现代智能电网综合能源系统优化调度中的代表性前瞻性。; 适合人群:具备电力系统基础理论知识和Matlab编程能力,从事能源、电气工程、自动化、可再生能源等方向研究的研究生、科研人员及工程技术人员,特别适合致力于优化建模、智能算法应用综合能源系统仿真的研究人员。; 使用场景及目标:① 复现高水平期刊论文中的主动配电网优化调度模型;② 掌握源网荷储协同优化的建模方法Matlab实现技术;③ 借助所提供的完整代码资源开展二次开发、算法改进或新场景拓展,服务于科研创新、项目申报学术论文撰写。; 阅读建议:建议结合文档中提及的相关文献案例,按照目录结构循序渐进地学习,重点理解模型构建的逻辑代码实现的关键细节,并利用附带的百度网盘资源获取全部代码数据文,通过动手实践加深对优化算法工程应用的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值