深入浅出ARM7在黄山派开发板的应用

AI助手已提取文章相关产品:

ARM7嵌入式开发实战:从零构建黄山派系统

你有没有想过,一块小小的开发板是如何在没有操作系统的情况下“活”起来的?它上电后第一件事做什么?为什么LED能按指定节奏闪烁?串口又是怎样把一个字符准确无误地传到电脑上的?

这些问题的答案,都藏在ARM7这颗经典内核的底层机制里。今天,我们就以 黄山派开发板 为实践平台,深入到芯片内部,亲手搭建一套完整的裸机系统——不依赖任何RTOS或Linux,完全由我们自己掌控每一个时钟周期、每一条指令跳转。

准备好了吗?让我们从最原始的地方开始:当电源接通那一刻,处理器究竟经历了什么?


处理器启动的秘密:模式、寄存器与状态控制

ARM7TDMI可不是个“听话”的小家伙。它有七种身份,随时切换;它用37个寄存器编织出一张精密的状态网络;它能在几纳秒内响应中断,也能在毫秒级完成函数调用。要驾驭它,我们必须先理解它的“性格”。

七种模式,七副面孔

想象一下,你的程序正在安静运行(User模式),突然来了个外部中断(IRQ)。这时候如果还用普通用户的权限去处理,岂不是太危险了?于是ARM设计了一套 特权模式机制 ——就像给系统开了几个VIP通道。

模式 CPSR值 典型用途
User (0x10) 正常应用执行环境
FIQ (0x11) 高速数据采集,比如音频流
IRQ (0x12) 普通外设中断,如定时器、UART
SVC (0x13) 系统调用入口,SWI指令触发
Abort (0x17) 内存访问失败时进入
Undefined (0x1B) 执行非法指令时捕获
System (0x1F) 特权级用户态,适合跑系统服务

其中最特别的是 FIQ(Fast Interrupt reQuest) 。它之所以“快”,是因为它拥有自己专属的一组寄存器副本:R8~R12、SP和LR都是独立物理单元!这意味着你在处理高速中断时,根本不需要压栈保存现场——直接开干就行 ⚡️

    MSR CPSR_c, #0xDB        ; 切换到FIQ模式(二进制11011011)
    MOV R8, #0x100           ; 直接使用专用寄存器
    MOV R9, #0x200
    STR R9, [R8]             ; 不会影响主程序中的R8/R9

这种设计简直是为实时控制量身定做的。试想你在做电机闭环控制,每次编码器脉冲到来都要快速响应——有了FIQ,延迟几乎可以忽略不计!

💡 小贴士:虽然System模式看起来像User,但它拥有所有特权。所以很多开发者喜欢把它当作“超级用户”来运行关键任务代码,既方便又安全。


寄存器地图:谁在什么时候可见?

ARM7共有 37个32位寄存器 ,但并不是任何时候都能看到全部。它们采用“分组映射”策略,在不同模式下看到的是不同的物理实体。

寄存器 是否共享 说明
R0–R7 ✅ 全局共享 所有模式共用同一份
R8–R12 ❌ FIQ独占 其他模式共用一组,FIQ另有副本
R13 (SP) ❌ 各自独立 每个模式有自己的堆栈指针
R14 (LR) ❌ 各自独立 返回地址隔离存储
R15 (PC) ✅ 共享 当前执行位置
CPSR ✅ 可读写 控制+状态一体
SPSR ❌ 每个异常模式各有一个 异常发生前的状态快照

举个例子:当你从User模式进入IRQ中断时:

  1. 硬件自动将当前CPSR保存到 SPSR_irq
  2. PC被强制指向 0x00000018 (IRQ向量地址)
  3. 处理器切换到IRQ模式
  4. 此时使用的R13和R14已经是 R13_irq 和 R14_irq

这就保证了中断处理不会污染主程序的堆栈和返回地址。等你从中断返回时,再通过特殊指令恢复原来的CPSR,一切就像没发生过一样。

🧠 工程师笔记:别忘了初始化各个模式下的堆栈!如果你只设置了User模式的SP,那一旦发生中断,系统就会因为堆栈未定义而崩溃。这是新手最常见的“神秘死机”原因之一。


CPSR:掌控全局的钥匙

如果说寄存器是士兵,那么 CPSR(Current Program Status Register) 就是将军。它不仅记录着最近一次运算的结果标志,还能决定整个系统的运行行为。

它的32位结构如下:

[31:28] N Z C V   ← 条件标志位
[27: 8] -         ← 保留(ARM7中不用)
[ 7 : 0] I F T M4M3M2M1M0
          │ │ │ └───┘
          │ │ └─────→ 处理器模式
          │ └───────→ Thumb状态
          └─────────→ FIQ屏蔽(F), IRQ屏蔽(I)
  • N (Negative) :结果为负则置1
  • Z (Zero) :结果为零则置1
  • C (Carry) :加法进位 / 减法借位
  • V (oVerflow) :有符号溢出检测

这些标志直接影响条件跳转指令的行为。比如:

CMP R0, R1        ; 比较R0和R1,影响Z标志
BEQ label         ; 如果相等(Z=1),跳转

更强大的是它的控制位。你可以动态开关中断:

MRS R0, CPSR       ; 读取当前状态
ORR R0, R0, #0x80  ; 设置I位 → 关闭IRQ
MSR CPSR_c, R0     ; 写回,仅更新控制域
; ... 临界区操作 ...
BIC R0, R0, #0x80  ; 清除I位 → 开启IRQ
MSR CPSR_c, R0

这段代码实现了标准的 中断屏蔽保护 ,常用于多任务调度、共享资源访问等场景。

⚠️ 警告:不要滥用全局关中断!长时间关闭IRQ会导致其他外设响应延迟甚至丢失事件。尽量缩短临界区长度,或者考虑使用原子操作替代。


混合编程的艺术:C与汇编如何无缝协作

你可能会问:“现在都2025年了,谁还写汇编?”
答案是:每一个追求极致性能的嵌入式工程师 😎

C语言写逻辑清晰,但有些事它做不到:
- 精确控制某条指令的执行时机
- 直接修改SPSR恢复异常上下文
- 实现超低延迟中断响应

这时候就得请出汇编了。但怎么让它和C代码和平共处呢?关键在于遵守规则——ATPCS(ARM-Thumb Procedure Call Standard)。

ATPCS调用规范:跨语言通信协议

ATPCS规定了函数调用时的寄存器职责分工,就像交通法规一样确保秩序井然:

寄存器 角色 使用规则
R0–R3 参数传递 前4个参数依次放入,调用者无需保存
R4–R11 局部变量 若被调用函数使用,必须先压栈保存
R12 (IP) 中间暂存 子程序调用链内部使用
R13 (SP) 堆栈指针 满递减模式(Full Descending)
R14 (LR) 返回地址 BL指令自动填入
R15 (PC) 下条指令地址 自动更新

来看一个典型例子:

// C代码
extern void asm_add(int a, int b, int *result);
int main() {
    int res;
    asm_add(5, 7, &res);  // a=5→R0, b=7→R1, &res→R2
    return 0;
}

对应的汇编实现:

    AREA AddFunc, CODE, READONLY
    EXPORT asm_add

asm_add:
    ADD R3, R0, R1      ; 加法运算
    STR R3, [R2]        ; 结果写回内存
    MOV PC, LR          ; 返回调用者

看到了吗?完全不需要额外压栈!参数已经在R0~R2里等着了,效率极高。

🔍 技巧提示:对于频繁调用的小函数(如GPIO置位),可以用 __attribute__((always_inline)) 强制内联,避免函数调用开销。


内联汇编:把汇编嵌进C函数

有时候你只想插入一小段汇编,比如读写CPSR。这时用独立汇编文件就太重了,直接上内联!

GCC风格如下:

#define disable_irq() \
    do { \
        __asm volatile ("mrs r0, cpsr\n\t" \
                        "orr r0, r0, #0x80\n\t" \
                        "msr cpsr_c, r0" \
                        : : : "r0", "memory"); \
    } while(0)

#define enable_irq() \
    do { \
        __asm volatile ("mrs r0, cpsr\n\t" \
                        "bic r0, r0, #0x80\n\t" \
                        "msr cpsr_c, r0" \
                        : : : "r0", "memory"); \
    } while(0)

解释几个关键点:

  • volatile :防止编译器优化掉这条“看似无用”的代码
  • \n\t :换行+缩进,让生成的汇编更易读
  • : : : "r0", "memory" :破坏列表,告诉编译器r0和内存可能被改写
  • do{...}while(0) :保证语法一致性,可用 ; 结尾

这样封装之后,你就可以像调用普通函数一样使用 disable_irq() 了,既安全又高效。

🛠 实战建议:这类宏最好放在头文件中,并加上 static inline 修饰,避免链接冲突。


启动代码:系统觉醒的第一步

如果说固件是一栋大楼,那启动代码就是地基。哪怕上面盖得再漂亮,地基塌了全完蛋。

异常向量表:CPU的导航图

ARM7要求异常向量表必须位于内存起始地址(0x00000000)或高位(0xFFFF0000)。每个异常占4字节,通常放一条跳转指令。

    AREA vectors, CODE, READONLY
    ENTRY

Reset_Handler:
    B Reset_Init            ; 复位
    B Undefined_Handler     ; 未定义指令
    B SWI_Handler           ; 软中断
    B Prefetch_Handler      ; 预取中止
    B DataAbort_Handler     ; 数据中止
    B .                     ; 保留
    B IRQ_Handler           ; 普通中断
    B FIQ_Handler           ; 快速中断

注意这里的 B 是相对跳转。但如果后期你想把向量表复制到RAM以便动态修改(比如打补丁),就需要启用高位映射:

    LDR R0, =0x40000000
    LDR R1, =vectors
    MOV R2, #32
copy_loop:
    LDR R3, [R1], #4
    STR R3, [R0], #4
    SUBS R2, R2, #4
    BNE copy_loop

    MRC p15, 0, R0, c1, c0, 0    ; 读CP15控制寄存器
    ORR R0, R0, #(1<<13)         ; 设置V位
    MCR p15, 0, R0, c1, c0, 0    ; 写回,启用高位向量

从此以后,异常会跳转到 0xFFFF0000 + offset ,而不再是低端地址。这个技巧在Bootloader升级时非常有用!


初始化堆栈:给每个模式安个家

很多人只设置了一个堆栈,结果一进中断就跑飞。记住: 每个特权模式都需要自己的堆栈空间!

Stack_Top       EQU     0x40004000

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
SVC_StackSpace  SPACE   0x100
IRQ_StackSpace  SPACE   0x100
ABT_StackSpace  SPACE   0x100
UND_StackSpace  SPACE   0x100
SYS_StackSpace  SPACE   0x100

                AREA    RESET, DATA, READONLY
SVC_StackTop    DCD     SVC_StackSpace + 0x100
IRQ_StackTop    DCD     IRQ_StackSpace + 0x100
ABT_StackTop    DCD     ABT_StackSpace + 0x100
UND_StackTop    DCD     UND_StackSpace + 0x100
USR_StackTop    DCD     SYS_StackSpace + 0x100

然后在复位处理中逐个设置:

Reset_Init:
    MSR CPSR_c, #0xD3         ; SVC模式
    LDR SP, =SVC_StackTop

    MSR CPSR_c, #0xD2         ; IRQ模式
    LDR SP, =IRQ_StackTop

    MSR CPSR_c, #0xDB         ; FIQ模式
    LDR SP, =0x40003000

    MSR CPSR_c, #0xDF         ; Abort模式
    LDR SP, =ABT_StackTop

    MSR CPSR_c, #0xDD         ; Undefined模式
    LDR SP, =UND_StackTop

    MSR CPSR_c, #0x1F         ; System模式
    LDR SP, =USR_StackTop

    B SetupMemory             ; 继续初始化

这套流程做完,才算真正准备好迎接C世界。


数据段搬运与清零: .data .bss 的归位

你写的全局变量,比如:

int g_counter = 100;
char g_buffer[256];

它们分别属于 .data (已初始化)和 .bss (未初始化)。但单片机Flash不能写,所以链接脚本会把 .data 放在Flash里,上电后再拷贝到SRAM。

Keil MDK使用Image$$符号定位:

SetupMemory:
    LDR R0, =|Image$$RW_IRAM1$$Base|        ; SRAM起始
    LDR R1, =|Image$$RO$$Limit|              ; Flash中.data起始
    LDR R2, =|Image$$RW_IRAM1$$Length|       ; 数据大小

copy_data:
    CMP R2, #0
    BEQ clear_bss
    LDRB R3, [R1], #1
    STRB R3, [R0], #1
    SUBS R2, R2, #1
    BNE copy_data

clear_bss:
    LDR R0, =|Image$$RW_IRAM1$$ZI$$Base|     ; .bss起始
    LDR R1, =|Image$$RW_IRAM1$$ZI$$Limit|   ; .bss结束
    MOV R2, #0
zero_loop:
    CMP R0, R1
    BGE init_done
    STR R2, [R0], #4
    B zero_loop

init_done:
    BL main                                   ; 终于可以进main了!

这一套下来,你的C程序才能正常运行。否则 g_counter 可能是个随机值, g_buffer 也可能全是垃圾数据。

📌 总结启动流程:

  1. 关中断
  2. 设置各模式堆栈
  3. 搬运 .data
  4. 清零 .bss
  5. 初始化时钟(PLL)
  6. 配置内存控制器(如有SDRAM)
  7. 跳转 main()

缺一不可!


GPIO驱动:点亮世界的第一个动作

终于进入外设世界了!GPIO是最基础也是最重要的接口。无论是点灯、读键,还是模拟I²C/SPI,全都靠它。

寄存器操控:直接对话硬件

以LPC2138为例,GPIO模块通过内存映射寄存器控制:

寄存器 偏移 功能
FIODIR 0x00 方向控制(1=输出,0=输入)
FIOPIN 0x14 当前电平读取
FIOSET 0x18 输出置高(写1有效)
FIOCLR 0x1C 输出置低(写1有效)

假设P0.10接LED,低电平点亮:

#define GPIO_BASE   0x3FFFC000
#define FIODIR      (*(volatile uint32_t*)(GPIO_BASE + 0x00))
#define FIOSET      (*(volatile uint32_t*)(GPIO_BASE + 0x18))
#define FIOCLR      (*(volatile uint32_t*)(GPIO_BASE + 0x1C))

// 初始化
FIODIR |= (1 << 10);        // P0.10设为输出

// 点亮
FIOCLR = (1 << 10);

// 熄灭
FIOSET = (1 << 10);

这里用了 volatile 关键字,防止编译器优化掉重复写操作。毕竟对硬件来说,哪怕写同样的值,也可能产生边沿信号。

💡 为什么不用 FIOPIN = 0 ?因为它不具备原子性!如果是多任务环境,读-改-写过程中可能被抢占,导致误操作。而 FIOSET/FIOCLR 是“写1生效”,天生线程安全。


按键消抖:不只是延时那么简单

机械按键按下时会有10~50ms的弹跳。如果直接检测,一次按下可能识别成好几次。

简单做法是延时重采样:

int key_pressed_debounced(void) {
    if ((FIOPIN >> 11) & 1) return 0;  // 未按下
    delay_ms(15);                      // 等待稳定
    return ((FIOPIN >> 11) & 1) == 0;  // 再次确认
}

但这会阻塞CPU。更好的方式是用定时器中断驱动状态机:

typedef enum {
    IDLE, PRESS_DETECTED, PRESSED, RELEASED
} KeyState;

KeyState state = IDLE;
uint32_t press_time;

void key_task(uint32_t now) {
    int level = ((FIOPIN >> 11) & 1) == 0;

    switch (state) {
        case IDLE:
            if (level) {
                press_time = now;
                state = PRESS_DETECTED;
            }
            break;
        case PRESS_DETECTED:
            if (!level) {
                state = IDLE;  // 抖动,忽略
            } else if (now - press_time > 20) {
                state = PRESSED;
                on_key_down();  // 真正触发
            }
            break;
        case PRESSED:
            if (!level) {
                state = RELEASED;
            }
            break;
        case RELEASED:
            state = IDLE;
            on_key_up();
            break;
    }
}

这个状态机能区分短按、长按、双击,而且是非阻塞的,完美融入实时系统。


LED多模式控制器:不只是闪

LED不只是用来“亮”和“灭”。通过不同闪烁模式,它可以传达丰富信息:

  • 慢闪(1Hz):系统正常
  • 快闪(5Hz):正在处理
  • 常亮:严重错误
  • 双灯交替:通信握手

我们可以设计一个通用控制器:

typedef struct {
    const uint8_t *pattern;
    uint8_t len;
    uint8_t idx;
    uint16_t interval;
} LedMode;

const uint8_t pattern_normal[] = {1,0,0,0};     // 0.5s间隔
const uint8_t pattern_alert[]  = {1,1,0,0};     // 0.2s间隔

LedMode mode_normal = {pattern_normal, 4, 0, 500};
LedMode mode_alert  = {pattern_alert,  4, 0, 200};

volatile LedMode *current_mode = &mode_normal;

void timer1ms_isr(void) {
    static uint32_t counter = 0;
    counter++;

    if (counter >= current_mode->interval) {
        int state = current_mode->pattern[current_mode->idx];
        gpio_write(10, !state);  // 低电平点亮
        current_mode->idx = (current_mode->idx + 1) % current_mode->len;
        counter = 0;
    }
}

只要改变 current_mode 指针,就能平滑切换显示效果,毫无卡顿。


定时器与中断:时间的主人

没有定时器,嵌入式系统就是“瞎子”。所有的延时、调度、测量,都离不开它。

TIMER0配置:打造10ms心跳

以LPC2138为例,TIMER0是一个32位递增计数器:

void timer0_init(void) {
    T0PR  = 59999;                    // 分频 → 1MHz
    T0MR0 = 10000;                    // 匹配值 → 10ms
    T0MCR = (1<<0) | (1<<1);          // 匹配时中断+复位
    T0TCR = 1;                        // 启动计数
}

然后注册中断:

void setup_timer_irq(void) {
    VICIntSelect &= ~(1 << 4);       // 设为IRQ
    VICIntEnable |= (1 << 4);        // 使能中断
    VICVectCntl0 = (1 << 5) | 4;     // Slot0 → IRQ4
    VICVectAddr0 = (uint32_t)TIMER0_IRQHandler;
}

void __irq TIMER0_IRQHandler(void) {
    system_ms++;                     // 全局计数器+1
    T0IR = 1;                        // 清中断标志
    VICVectAddr = 0;                 // 中断结束
}

有了 system_ms ,你就可以实现非阻塞延时:

void delay_ms(uint32_t ms) {
    uint32_t start = system_ms;
    while (system_ms - start < ms);
}

CPU占用率接近零,还能并发执行其他任务!


任务调度器雏形

基于这个定时器,我们可以做一个简单的轮询调度器:

typedef struct {
    void (*func)(void);
    uint32_t interval;
    uint32_t last_run;
} Task;

Task tasks[] = {
    {led_task,   500, 0},
    {key_task,    10, 0},
    {sensor_task, 100, 0}
};

#define TASK_COUNT (sizeof(tasks)/sizeof(Task))

void scheduler_run(void) {
    uint32_t now = system_ms;
    for (int i = 0; i < TASK_COUNT; i++) {
        if (now - tasks[i].last_run >= tasks[i].interval) {
            tasks[i].func();
            tasks[i].last_run = now;
        }
    }
}

主循环只需调用 scheduler_run() ,就能实现类RTOS的任务管理。等将来想移植FreeRTOS时,你会发现底层思想一脉相承。


UART通信:与世界对话

调试靠它,升级靠它,远程监控也靠它。UART是嵌入式系统的“嘴巴”。

波特率计算:精度至关重要

公式:

$$
Divisor = \frac{PCLK}{16 \times BaudRate}
$$

例如 PCLK=60MHz,波特率115200:

$$
\frac{60000000}{16×115200} ≈ 32.55 → 取整32
$$

代码实现:

void uart0_init(uint32_t baud) {
    PINSEL0 |= (1<<4)|(1<<6);        // P0.0=TxD, P0.1=RxD
    U0LCR = (1<<7);                  // 使能DLL/DLM
    uint16_t div = PCLK / (16 * baud);
    U0DLL = div & 0xFF;
    U0DLM = (div >> 8) & 0xFF;
    U0LCR = 0x03;                   // 8N1格式
}

中断驱动双缓冲队列

避免轮询浪费CPU,启用接收和发送中断:

#define RX_BUF_SIZE 128
uint8_t rx_buf[RX_BUF_SIZE];
uint32_t rx_head, rx_tail;

void __irq UART0_IRQHandler(void) {
    uint8_t status = U0IIR;

    if ((status & (1<<1)) == 0) {   // 接收就绪
        uint8_t data = U0RBR;
        rx_buf[rx_head] = data;
        rx_head = (rx_head + 1) % RX_BUF_SIZE;
    }

    if ((status & (1<<2)) == 0) {   // 发送空中断
        if (rx_tail != rx_head) {
            U0THR = rx_buf[rx_tail];
            rx_tail = (rx_tail + 1) % RX_BUF_SIZE;
        } else {
            U0IER &= ~(1<<1);       // 缓冲空,关TX中断
        }
    }
}

用户接口:

int uart_getchar(void) {
    while (rx_head == rx_tail);
    uint8_t data = rx_buf[rx_tail];
    rx_tail = (rx_tail + 1) % RX_BUF_SIZE;
    return data;
}

void uart_putchar(char c) {
    while (((rx_head - rx_tail) % RX_BUF_SIZE) == RX_BUF_SIZE - 1);
    rx_buf[rx_head++] = c;
    U0IER |= (1<<1);                // 开启TX中断
}

RTC实时时钟:让系统知道“现在几点”

工业设备需要时间戳记录事件。RTC能在掉电时靠电池维持运行。

BCD码读取与转换

RTC寄存器用BCD格式存储时间:

uint8_t rtc_read_sec(void) {
    uint8_t val = *(volatile uint8_t*)0x4000B000;
    return ((val >> 4) * 10) + (val & 0x0F);
}

功耗管理:进入掉电模式

空闲时可休眠:

void enter_powerdown(void) {
    SCB_PCON = 0x01;               // 进入掉电模式
    __asm("WFI");                  // 等待中断唤醒
}

通过RTC闹钟中断唤醒,实现定时采样。


FATFS文件系统:给单片机装上硬盘

SD卡 + FATFS = 嵌入式“U盘”。

移植步骤

  1. 下载FatFs源码
  2. 添加 ff.c , diskio.c
  3. 配置 ffconf.h
    c #define _FS_TINY 1 #define _FS_READONLY 0 #define _USE_MKFS 1

  4. 实现SPI驱动SD卡:
    c DSTATUS disk_initialize(BYTE drv) { spi_init(); send_cmd(CMD0, 0, 0x95); // 进入SPI模式 return RES_OK; }


日志自动记录

FRESULT log_data(float temp, float humi) {
    FIL file;
    FRESULT fr = f_open(&file, "LOG.TXT", FA_WRITE|FA_OPEN_APPEND);
    if (fr == FR_OK) {
        f_printf(&file, "[%02d:%02d] Temp:%.1f, Humi:%.1f\r\n",
                 get_hour(), get_minute(), temp, humi);
        f_close(&file);
    }
    return fr;
}

GPRS远程监控:把数据送到云端

AT指令封装

void gprs_send_at(const char* cmd) {
    uart_puts(cmd);
    uart_puts("\r\n");
    delay_ms(1000);
}

void gprs_connect_server(void) {
    gprs_send_at("AT+CIPSTART=\"TCP\",\"api.example.com\",\"80\"");
    delay_ms(5000);
    gprs_send_at("AT+CIPSEND");
}

JSON打包上传

{"dev":"HSP-001","temp":25.3,"humi":60.1,"time":"14:23"}

Python服务器接收:

from flask import Flask, request
app = Flask(__name__)

@app.route('/upload', methods=['POST'])
def upload():
    print(request.json)
    return "OK"

写在最后:从裸机到精通的路径

ARM7虽老,但其设计理念至今仍在A系列中延续。掌握它,不只是为了用一块开发板,更是为了建立 软硬协同的工程思维

当你能从复位向量一步步走到TCP上传,你会明白:
每一行代码背后,都有硬件在默默支撑;
每一次闪烁,都是数字世界的呼吸节拍。

而这,正是嵌入式开发的魅力所在 ❤️

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值