STM32 SD卡引导程序(BOOTLOADER)开发实战

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32 SD卡BOOTLOADER是微控制器在启动时加载应用程序的初始程序。它允许通过SD卡更新固件,通过IAP进行现场升级。其工作流程包括硬件初始化、SD卡检测、固件验证、加载固件到闪存、控制权转移以及安全性考虑。开发时需关注兼容性、错误处理、效率和可配置性,确保BOOTLOADER适应不同型号的STM32芯片并提供强大的错误恢复机制和优化的性能表现。源码和配置文件包的提供,有助于开发者根据项目需求定制优化。

1. STM32 SD卡BOOTLOADER概述

1.1 BOOTLOADER简介

在嵌入式系统中,BOOTLOADER是微控制器上执行的第一段代码,它负责初始化硬件并加载主应用程序。STM32系列微控制器具备灵活的启动模式,可以使得BOOTLOADER与应用程序共存于同一存储空间内,实现自升级和远程更新固件的IAP(In-Application Programming)功能。本文将深入探讨STM32 SD卡BOOTLOADER的实现过程,包括硬件初始化、SD卡通信、文件系统支持、固件验证、加载执行以及安全性等关键步骤。

1.2 系统需求与目标

对于开发者而言,一个健壮且高效的BOOTLOADER系统能够提高产品的可靠性与维护性。本文的目标是提供一个详细的框架,以引导开发人员理解并实现一个适用于STM32微控制器的SD卡BOOTLOADER。我们将通过分析每个组件的作用,操作方法和优化策略,帮助读者打造一个从设计到部署的完整流程。

2. 硬件初始化

2.1 STM32微控制器的启动模式

2.1.1 启动模式的配置

STM32微控制器的启动模式是引导程序执行的起点。它决定了微控制器在上电或复位后从何种存储介质读取并执行代码。通常,STM32支持以下几种启动模式:

  • 主闪存存储器(Main Flash Memory)
  • 系统存储器(System Memory)
  • 嵌入式SRAM(Embedded SRAM)
  • 用户闪存存储器(User Flash Memory)

要配置启动模式,通常需要设置STM32的Flash选项字节(Flash Option Bytes)。在STM32CubeMX工具或通过编程器如ST-Link,开发者可以配置这些选项。比如,可以设置 nBOOT0 引脚电平来改变启动模式。

2.1.2 启动模式对BOOTLOADER的影响

启动模式的选择对BOOTLOADER的运行有直接影响。例如,如果将启动模式设置为从系统存储器启动,那么微控制器将不会从主闪存执行代码,而是从系统存储器执行预设的引导程序。这对于执行芯片内的固件升级或者从其他非标准的存储器设备如SD卡启动都是很有用的。

2.2 硬件接口的初始化

2.2.1 GPIO端口初始化

通用输入输出(GPIO)端口的初始化是微控制器应用开发的基本步骤。STM32系列提供了丰富的GPIO引脚,需要根据电路设计和外设需要来配置它们。

在初始化GPIO时,通常要完成以下操作:

  • 指定GPIO引脚为输入还是输出
  • 设置输出模式下的推挽或开漏配置
  • 设置上拉、下拉电阻配置
  • 设置引脚速度(高速或低速)
  • 配置外部中断(如果需要)

初始化代码样例:

void GPIO_Init(void) {
    __HAL_RCC_GPIOA_CLK_ENABLE();  // 启动GPIOA时钟

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_0; // 配置GPIOA引脚0
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 设置为推挽输出模式
    GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上拉或下拉电阻
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 设置为低速
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化GPIOA引脚0

    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 将引脚设置为低电平
}
2.2.2 SPI接口初始化

串行外设接口(SPI)是一种常用的高速串行通信协议。在嵌入式开发中,SPI通常用于与传感器、显示屏和其他外设进行通信。

初始化SPI接口的步骤包括:

  • 启动SPI外设的时钟
  • 配置SPI模式(主/从模式)
  • 设置SPI时钟极性和相位
  • 选择数据帧格式(8位或16位)
  • 配置波特率
  • 启用SPI外设

初始化SPI接口的代码示例:

void SPI_Init(void) {
    __HAL_RCC_SPI1_CLK_ENABLE(); // 启动SPI1时钟

    SPI_HandleTypeDef hspi1;
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
    hspi1.Init.NSS = SPI_NSS_SOFT;
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi1.Init.CRCPolynomial = 10;

    HAL_SPI_Init(&hspi1); // 初始化SPI1

    HAL_SPI_Transmit(&hspi1, (uint8_t*)"\x00", 1, 1000); // 发送数据
}
2.2.3 其他外设接口初始化

STM32微控制器支持多种类型的外设接口,除了GPIO和SPI之外,还包括I2C、UART、CAN等。各种外设的初始化方法和SPI类似,核心步骤包括:

  • 启动外设时钟
  • 配置外设参数,如波特率、模式等
  • 启用外设

外设初始化的一般代码框架为:

void Peripheral_Init(void) {
    __HAL_RCC_PERIPHERAL_CLK_ENABLE(); // 启动外设时钟

    PERIPHERAL_HandleTypeDef hperipheral;
    hperipheral.Instance = PERIPHERAL;
    hperipheral.Init.Parameters = Parameter_Configuration; // 配置参数

    HAL_PERIPHERAL_Init(&hperipheral); // 初始化外设
}

外设初始化过程需要参考STM32的参考手册,确保每个参数都符合外设规范和应用需求。

3. SD卡检测和初始化

3.1 SD卡的硬件连接

3.1.1 SD卡引脚说明

SD卡作为一种广泛使用的数据存储设备,其接口简单而标准化。一个标准的SD卡拥有9个引脚,每个引脚都有其独特的作用。首先,我们需要了解这些引脚的功能,以便进行正确的硬件连接。

  • VDD (电源) : 这是供电引脚,通常连接到3.3V电源。
  • VSS (地) : 这是接地引脚,用于电源的负极连接。
  • DAT0 - DAT3 (数据线) : 这四根线用于数据传输。在SPI模式下,只使用DAT0。
  • CLK (时钟) : 这个引脚用于提供时钟信号,以同步数据传输。
  • CMD (命令) : 此引脚用于发送命令和接收响应。
  • CD/DAT3 (插入检测/数据线) : 在SD模式下,此引脚用作检测卡是否已经插入。在SPI模式下,它可以用作数据线DAT3。
  • WP (写保护) : 此引脚用于指示是否启用写保护。在某些设计中,可能被固定连接到VSS(未启用写保护)。

这些引脚的具体功能可能会根据SD卡的类型(比如SDSC、SDHC、SDXC)和操作模式(标准SD模式或SPI模式)有所不同。但在进行硬件连接时,以上引脚功能是非常基础且必须了解的。

3.1.2 SD卡与STM32的接口电路

在设计STM32与SD卡的接口电路时,必须遵循SD卡的电气特性,并确保硬件的兼容性。通常情况下,我们可以使用SPI模式进行通信,因为这种方式的接口简单,占用的STM32资源较少。以下是SPI接口电路的简要设计要点:

  • 电源管理 :确保SD卡的VDD连接到稳定的3.3V电源,而VSS接地。
  • 时钟信号 :通过STM32的某个引脚配置为输出,提供时钟信号到SD卡的CLK引脚。
  • 数据线 :最少使用一个数据引脚(DAT0),连接到STM32的SPI模块相应的数据引脚。
  • 命令/响应线 :通过STM32的一个GPIO引脚配置为输出,连接到SD卡的CMD引脚。
  • 写保护和卡检测 :写保护引脚WP连接到VSS,表示不启用写保护。而卡检测引脚CD可以连接到STM32的一个GPIO引脚,并配置为输入。

在设计电路时,务必注意电气特性匹配,包括电流驱动能力、上拉电阻、信号完整性等,以确保系统的稳定运行。

3.2 SD卡初始化流程

3.2.1 SD卡的命令集介绍

SD卡的命令集是由一系列标准命令构成,这些命令用来控制SD卡的行为和状态。SD卡通信可以以两种模式进行:SD模式和SPI模式。在SPI模式下,只有少数命令是必须的,主要包括:

  • CMD0:软件复位命令,初始化通信。
  • CMD1:发送操作条件命令,用于设置SD卡的工作电压。
  • CMD8:发送接口条件命令,用于检查SD卡是否支持高容量规格。
  • CMD58:读OCR(操作条件寄存器),获取SD卡的详细信息。

SD卡初始化主要依赖于这些命令的正确执行,以及对返回响应的正确解析。

3.2.2 SD卡初始化命令序列

一旦硬件连接完成,我们就可以开始初始化SD卡。以下是使用SPI模式初始化SD卡的步骤,这是最基础且常见的方法:

  1. 初始化SPI接口 :首先,确保STM32的SPI接口已经初始化,并且准备好与SD卡进行通信。
  2. 软件复位 :发送CMD0,确保SD卡处于初始状态。
  3. 发送操作条件命令 :发送CMD1,设置工作电压范围,这一步是可选的,取决于具体SD卡的实现。
  4. 发送接口条件命令 :发送CMD8,检查SD卡是否支持高容量规格。
  5. 读OCR :发送CMD58,读取操作条件寄存器,确认SD卡的状态和电压范围。
  6. 初始化完成 :如果所有命令都成功执行,并且SD卡返回有效的响应,那么初始化过程完成,SD卡可以开始使用。

3.2.3 SD卡初始化的错误处理

错误处理是SD卡初始化过程中不可或缺的一部分。SD卡可能会因为多种原因无法完成初始化,例如硬件连接错误、卡损坏、不支持的操作模式等。因此,设计代码时必须对每个命令的响应进行检查,以及在遇到错误时执行相应的错误处理流程。

uint8_t SPI_SendCommand(uint8_t cmd, uint32_t arg, uint8_t crc) {
    // ...发送命令到SD卡的代码逻辑...
    uint8_t response = SPI_ReadResponse();
    if (response != EXPECTED_RESPONSE) {
        return ERROR;
    }
    return SUCCESS;
}

void SD_Initialize(void) {
    if (SPI_SendCommand(CMD0, 0x00000000, 0x95) != SUCCESS) {
        // 错误处理:CMD0命令失败
    }
    // ...其他初始化命令的调用和错误处理...
}

在实际代码中, SPI_SendCommand 函数负责发送命令到SD卡,并读取响应。根据返回的响应判断命令是否成功执行,并决定下一步操作。如果SD卡没有返回期望的响应,就需要进行错误处理,这可能包括重新发送命令、切换到不同的通信模式、或者报错并停止初始化流程。对于错误处理,通常建议记录错误信息,以便于后续的调试和问题定位。

4. 文件系统支持

4.1 文件系统的介绍

4.1.1 文件系统的作用和类型

文件系统是一个存储和组织数据的系统,它允许数据通过目录层次结构进行访问。在嵌入式系统如STM32上,文件系统主要用于管理SD卡或其他存储介质上的数据。其作用包括但不限于数据的持久化存储、高效的数据检索、文件的组织管理等。文件系统的类型多样,常见的有FAT、NTFS、ext4等。对于资源受限的嵌入式设备而言,FAT文件系统由于其简单性和广泛的兼容性,是较为常见和实用的选择。

4.1.2 文件系统的初始化和挂载

在使用文件系统之前,首先需要进行初始化,这包括设置文件系统的相关参数以及挂载文件系统。初始化阶段,系统会创建文件系统对象,并配置必要的参数,如块大小、卷标等。挂载则是在初始化后,将文件系统与存储介质结合,并使之对用户程序可见。挂载文件系统通常涉及到识别并建立文件系统与实际存储介质之间的映射关系。

4.2 FAT文件系统的操作

4.2.1 FAT文件系统的基本结构

FAT(File Allocation Table)文件系统有一个核心部分是FAT表,这个表记录了文件和目录在存储介质上的分布情况。在FAT32文件系统中,通常存在一个主FAT表和一个或多个副本FAT表以提高数据的可靠性。FAT文件系统将文件存储在数据区域,而文件的元数据,例如文件名、大小、属性和位置信息,则存储在目录区域。除此之外,FAT文件系统还包括根目录、子目录以及数据区域等多个部分。

4.2.2 文件和目录的读写操作

对文件和目录进行读写操作是文件系统最核心的功能。在FAT文件系统中,通常使用API函数来进行这些操作。例如,创建文件、读取文件内容、写入数据到文件以及删除文件等。操作时需要注意文件指针的位置,以确保数据正确地读取或写入。此外,FAT文件系统对文件的打开模式(只读、只写、读写)也有着严格的要求。

4.2.3 文件系统的异常处理

异常处理是文件系统中不可或缺的部分。异常情况可能包括但不限于文件打开失败、读写错误、存储介质损坏等。在这些情况下,文件系统需要能够妥善处理异常,并为上层应用提供错误信息。这可能涉及到异常的检测、恢复策略的实施以及日志的记录等步骤。

下面是一个使用FATFS库实现FAT文件系统在STM32上的初始化、读写操作和异常处理的代码示例:

#include "ff.h"

FATFS fs;
FIL fil; /* 文件对象 */
FRESULT fr; /* FRESULT枚举类型表示操作结果 */
UINT bw; /* 用于记录写入字节数 */

/* 挂载FAT文件系统 */
fr = f_mount(&fs, "", 0);
if (fr != FR_OK) {
    /* 挂载失败处理 */
    /* 可能的错误包括存储介质未插入、FAT表损坏等 */
    /* 此处应添加具体的错误处理代码 */
}

/* 在文件系统上打开一个文件 */
fr = f_open(&fil, "example.txt", FA_READ | FA_WRITE);
if (fr != FR_OK) {
    /* 文件打开失败处理 */
    /* 可能的错误包括文件不存在、无权限等 */
    /* 此处应添加具体的错误处理代码 */
} else {
    /* 文件成功打开,执行读写操作 */
    fr = f_write(&fil, "Hello, STM32!", 18, &bw);
    if (fr != FR_OK || bw != 18) {
        /* 写入失败处理 */
        /* 此处应添加具体的错误处理代码 */
    }
    f_close(&fil); /* 关闭文件 */
}

/* 卸载文件系统 */
f_mount(NULL, "", 0);

在上述代码中,我们首先尝试挂载文件系统,然后尝试打开一个文件并写入一段文本。在每一步操作后,我们检查 fr (FRESULT类型)的值,来判断操作是否成功,并进行相应的错误处理。如果操作成功,我们将继续执行。如果遇到错误,应具体分析错误原因并给出适当的错误处理措施。这个示例展示了文件系统操作的基本步骤和异常处理机制。

5. 固件验证过程

在固件更新和升级的过程中,验证固件的完整性和安全性是至关重要的环节。它确保了新固件没有被篡改,且具备正确的功能,避免了由于固件损坏或被恶意攻击而导致的系统崩溃或安全风险。本章将深入探讨固件的存储结构和固件完整性验证的实现机制。

5.1 固件的存储结构

5.1.1 固件的存储格式

固件的存储格式通常取决于使用的存储介质以及微控制器的架构。在STM32系列微控制器中,固件经常存储在如SD卡、NOR/NAND闪存、或其他外部存储设备上。固件可以被存储为二进制文件,也可以被压缩或打包成特定格式以节省空间并提供一定程度的保护。

固件文件通常包含两个主要部分:固件代码和固件头部。固件代码是实际执行的机器码指令,而固件头部则包含了用于描述和管理固件的信息,例如版本号、校验信息和加密标志等。

typedef struct {
    uint32_t magic;        // 固件魔法值,用于验证固件文件格式
    uint32_t version;      // 固件版本号
    uint32_t size;         // 固件代码部分的大小
    uint32_t checksum;     // 校验和
    uint32_t signature;    // 数字签名(如果使用)
    uint8_t data[];        // 固件代码数据
} FirmwareHeader;

5.1.2 固件头部信息的解析

解析固件头部信息是验证固件完整性的重要步骤。开发者通常会编写一段代码来读取固件文件,解析头部信息,并执行相应的验证流程。以下是一个简单的示例代码块,用于解析固件头部信息:

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

// 假设固件头部数据已经通过某种方式读取到了内存
uint8_t *firmware_data;
size_t firmware_size;

// 解析固件头部信息
bool parse_firmware_header(uint8_t *data, size_t size, FirmwareHeader *header) {
    if (size < sizeof(FirmwareHeader)) {
        return false; // 固件头部信息不完整
    }
    header->magic = *(uint32_t *)(data);
    header->version = *(uint32_t *)(data + 4);
    header->size = *(uint32_t *)(data + 8);
    header->checksum = *(uint32_t *)(data + 12);
    header->signature = *(uint32_t *)(data + 16);
    // ... 其他头部信息
    return true;
}

// 使用示例
FirmwareHeader header;
if (parse_firmware_header(firmware_data, firmware_size, &header)) {
    // 固件头部解析成功,接下来进行固件验证
}

5.2 固件完整性验证

5.2.1 校验和的计算

在固件验证过程中,开发者会计算固件的校验和,并与头部信息中的校验和进行比较。如果不匹配,则表明固件可能损坏或被篡改。校验和的计算方法可以多种多样,例如简单的累加和、CRC校验,甚至是高级的哈希算法如MD5、SHA-1。

以下是一个使用CRC校验和的计算例子:

#include <stdio.h>
#include <stdint.h>

// CRC校验和的计算
uint32_t calculate_crc(uint8_t *data, size_t size) {
    uint32_t crc = 0xFFFFFFFF;
    for (size_t i = 0; i < size; ++i) {
        uint8_t byte = data[i];
        crc ^= (uint32_t)byte << 24;
        for (int i = 0; i < 8; ++i) {
            uint32_t mask = -(crc & 0x80000000);
            crc = (crc << 1) ^ (0x04C11DB7 & mask);
        }
    }
    return ~crc;
}

// 使用示例
uint32_t crc = calculate_crc(firmware_data, firmware_size);
if (crc == header.checksum) {
    // 校验和匹配,固件验证成功
}

5.2.2 签名和加密的实现

在更高级的安全性要求场景中,固件在存储前可能经过了数字签名和加密处理。数字签名可以确认固件的来源,而加密则保护固件内容不被非法读取。当固件被加载到微控制器时,固件完整性验证还包括对签名的验证和对加密内容的解密。

#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>

// 使用RSA公钥验证签名(示例)
bool verify_rsa_signature(uint8_t *data, size_t data_size, 
                          uint8_t *signature, size_t signature_size,
                          RSA *public_key) {
    bool result = false;
    // 初始化SSL库
    SSL_load_error_strings();
    ERR_load_crypto_strings();

    // 将数据、签名和公钥传入RSA函数以验证签名
    // 这里省略了具体的验证步骤,通常涉及到使用RSA_verify系列函数

    if (/* 验证成功 */) {
        result = true;
    } else {
        // 处理验证失败的情况
        // 可以通过ERR_print_errors_fp(stderr); 输出错误详情
    }

    // 清理SSL库资源
    EVP_cleanup();
    ERR_free_strings();

    return result;
}

// 使用示例
if (verify_rsa_signature(firmware_data, firmware_size,
                         firmware_signature, firmware_signature_size,
                         firmware_public_key)) {
    // 签名验证成功
}

请注意,示例代码是高度简化的,实际应用中需要考虑错误处理、资源管理等多方面的因素,并且要确保所使用的加密算法和库的安全性。

通过本章的介绍,我们了解了固件存储结构的基本概念,以及如何通过校验和、数字签名和加密技术来验证固件的完整性和安全性。这对于确保嵌入式设备固件的可靠性以及用户的数据安全至关重要。在下一章节,我们将探讨固件的加载和执行过程。

6. 固件加载和执行

6.1 固件的加载机制

在嵌入式系统中,固件加载机制至关重要,它是系统启动和功能实现的基石。理解固件加载的机制有助于优化启动速度和系统性能。

6.1.1 分页加载和连续加载的区别

分页加载通常涉及到将固件分割成多个固定大小的页面,每个页面可以独立地加载和执行。这种方法的优势在于减少了内存占用,因为不需要一次性将整个固件都加载到内存中。此外,分页加载还可以支持按需加载,这样能够加速启动过程,尤其在需要从存储介质中读取数据时。

连续加载,顾名思义,是将整个固件作为一个连续的内存块加载到内存中。这种方式比较简单,但是会占用更多的内存资源,且在加载过程中的延迟可能会较大。

6.1.2 固件加载过程中的异常处理

在固件加载过程中,可能会遇到多种异常情况,如存储介质损坏、读写错误或者内存不足等问题。为了确保系统的鲁棒性,必须在固件加载逻辑中加入异常处理机制。

例如,在读取存储介质时可以使用循环检测机制,如果连续几次读取都无法成功,则可以认为是介质问题,或者通过预留内存空间来防止内存不足导致的加载失败。

6.2 固件的执行环境

固件的执行环境需要保证操作的顺畅无误,确保系统的稳定运行。

6.2.1 环境准备和上下文切换

在固件开始执行之前,需要对运行环境进行准备,这包括堆栈初始化、系统时钟配置等。环境准备是固件加载到内存之后的第一步,它确保了固件能够在正确的上下文中运行。

上下文切换是指在不同的运行状态之间切换,它在多任务操作系统中尤为重要。在单任务环境中,上下文切换较少见,但当引入中断或者任务调度时,就显得不可或缺。它涉及到保存和恢复CPU寄存器、状态寄存器等关键信息,以保证切换后任务能够在离开前的状态继续运行。

6.2.2 固件执行的安全性考虑

固件执行不仅要关注性能,也要注重安全性。在执行过程中,必须考虑到代码的执行权限、数据的安全访问等问题。

在权限管理方面,可以利用微控制器的内存保护单元(MPU)来定义不同的执行区域,限制代码执行的权限。这能够有效防止执行非法指令和访问未授权内存区域,从而增强系统的安全性。

数据安全方面,可以实施数据加密和校验机制,确保数据在处理过程中的完整性和保密性。

// 示例代码:上下文切换的基本框架
void contextSwitch() {
    // 保存当前任务的上下文
    saveContext(&currentTask->context);
    // 选择下一个要执行的任务
    task_t* nextTask = getNextTask();
    // 恢复下一个任务的上下文
    restoreContext(&nextTask->context);
}

通过以上内容的深入分析,我们了解了固件加载和执行的机制,及其在嵌入式系统中的重要性。本章节内容的探讨,为我们后续深入学习IAP功能的实现,乃至整个嵌入式系统的设计打下了坚实的基础。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32 SD卡BOOTLOADER是微控制器在启动时加载应用程序的初始程序。它允许通过SD卡更新固件,通过IAP进行现场升级。其工作流程包括硬件初始化、SD卡检测、固件验证、加载固件到闪存、控制权转移以及安全性考虑。开发时需关注兼容性、错误处理、效率和可配置性,确保BOOTLOADER适应不同型号的STM32芯片并提供强大的错误恢复机制和优化的性能表现。源码和配置文件包的提供,有助于开发者根据项目需求定制优化。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值