IMX6ULL学习笔记(一) —— 寄存器组

IMX6ULL 学习笔记


version : v1.0 「2023.4.27」

author: Y.Z.T.


简介: 随记, 关于I.MX6ULL 系统SOC 的部分寄存器




⭐️ 目录





1️⃣ 裸机

简单了解原理

1.1 GUN汇编

1.1.1 与ARM汇编的伪操作差异

image-20230430150223464



1.1.2 常见GUN ARM 伪操作指令
image-20230430150409583

补充数据传输指令

image-20230428122910132



1.1.3

GUN ARM 汇编语言中 , 标号_start 是汇编程序的入口 , 如果希望这个标号被其他文件引用 , 只需要在定义的地方使用 .globa 伪操作声明即可 ( .globa 表示定义一个全局标号 )

.globa -start

...

.section 伪操作

可以通过 .section 伪操作自定义一个段

.section <section name> {,"<flags>"}

.section .mysection "awx"   @注释 : 定义一个可写、可执行的段
.align 2
  • 注意在使用伪操作.section定义一个段时,每个段以段名开始,以 下一个段名或文件结尾 作为结束标记。
  • 在定义段名时,注意不要和系统预留的段名冲突,(如.text.data.bss.rodata )

系统预留的段名

image-20230430151811342



1.1.4 基本数据格式

在定义数据的过程中需要注意 :

  • 二进制 数据通常以0B0b开头
  • 八进制 数据以0开头
  • 十六进制 数据以0x开头
  • 十进制 数据则以非0数字开头
  • 负数 前面加 -
  • 取补~
  • 不相等〈〉
  • 其他运算符号如+、-、*、%、<、<<、\>、>>、|、&、^、!、==、>=、&&与C语言语法相似
  • 字符串常量 要用双引号 "" 括起来
  • 使用.ascii定义字符串时要自行在结尾加 \0
  • .string 伪操作可以定义多个字符串
  • .asciz 伪操作可以定义一个以NULL字符结尾的字符串
  • .rept 伪操作可以重复定义数据
  • GNU ARM汇编程序中经常使用小圆点 . 表示 当前指令的地址
.ascii "hello\0"
.string "hello", "world!"
.asciz "hello"
.rept 3 .byte 0x10 .endr


1.1.5 .equ 伪操作

使用 伪操作 定义浮点数

@标签: 命令
f:
.float 3.14
.equ f,3.1415
  • 上面通过使用.float伪操作定义一个浮点数f,并初始化为3.14
  • 并通过.equ伪操作 将浮点数重新赋值成 3.1415

.equ伪操作除了给数据赋值,还可以把常量定义在代码段中,然后在代码中直接引用。类似C语言中的#define宏定义

.section .data
.equ DELAY,100
...

.section .text
...
MOV R0,$DELAY
...


1.1.6 补充

stmdbldmia

  • stmdb和ldmia指令一般配对使用
  • stmdb用于将寄存器压栈
  • ldmia用于将寄存器弹出栈
  • 它们的作用是保存使用到的寄存器

ARM指令的多数据传输(STM、LDM)中,提到:多寄存器的Load和Store指令分为2组:

  • 一组用于数据的存储与读取,对应于IA、IB、DA、DB,
  • 一组用于堆栈操作,对应于FD、ED、FA、EA,
  • STMIB(地址先增而后完成操作)、STMFA(满递增堆栈);LDMIBLDMED
  • STMIA(完成操作而后地址递增)、STMEA(空递增堆栈);LDMIALDMFD
  • STMDB(地址先减而后完成操作)、STMFD(满递减堆栈);LDMDBLDMEA
  • STMDA(完成操作而后地址递减)、STMED(空递减堆栈);LDMDALDMFA

  • IA模式表示:每次传送后地址+4;(After Increase)
  • DB模式表示:每次传送前地址-4;(Before Decrease)

1.2 IO复用表

在《imx6ull 中文参考手册》 199页

image-20230428162002702

image-20230428162114657

image-20230428162132536

image-20230428162148167

image-20230428162217127

image-20230428162232673


可以看见 :

  • GPIO1 有 32 个 IO
  • GPIO2 有 22 个 IO
  • GPIO3 有 29 个 IO
  • GPIO4 有 29 个 IO
  • GPIO5 有 12 个 IO

1.3 复用为GPIO时的寄存器

1.3.1 IO复用功能和IO电气属性
  • SW_MUX_CTL_PAD_* : 配置IO口的复用功能

    GPIO1_IO1举例:

    image-20230428164534841

    image-20230428164550307

    • 可以看到GPIO1_IO01可以复用为8种不同功能的IO

  • SW_PAD_CTL_PAD_* : 配置IO口的电气属性

    还是以GPIO1_IO00 举例:

    image-20230428165738791

    • 用于配置IO的 上下拉电阻、IO速度、IO驱动能力、压摆率等电气属性
    • 一般主要设置压摆率 (SRE)、驱动能力 (DSE)、速度 (SPEED )、上下拉 ( PUS)、 开漏 (ODE)


1.3.2 复用为GPIO时其他寄存器
  • DR寄存器: 数据寄存器
    • 寄存器中的每一位对应一个IO
    • 当 GPIO 被配置为输出功能 以后 , 向指定的位写入数据那么相应的 IO 就会输出相应的高低电平 ( 如 : GPIO1_IO00 输出高电平,那么就应该设置 GPIO1.DR=1)
    • 当 GPIO 被配置为输出功能 以后 , 此寄存器就保存着对应 IO 的电平值 (如 :GPIO1_IO00 这个引脚接地的话,那么 GPIO1.DRbit0 就是 0)

  • GDIR 寄存器 : 方向寄存器
    • 寄存器中的每一位对应一个IO , 用来设置某个IO的工作方向
    • 如果要设置一个GPIO为输入 , 则对应的位设置为0 (如 : 要设置 GPIO1_IO00 为输入,那么 GPIO1.GDIR=0)
    • 如果要设置一个GPIO为输出 , 则对应的位设置为1

  • PSR 寄存器 : 状态寄存器
    • 寄存器中的每一位对应一个IO , 获取对应GPIO的状态
    • 读取相应的位即可获取对应的 GPIO 的状态,也就是 GPIO 的高低电平值 ( 功能和输入状态下的 DR 寄存器一样。 )

  • ICR1 和 ICR2寄存器 : 中断控制寄存器

    • 寄存器中的每两位对应一个IO , ICR1用于配置低16个GPIO (IO0 ~ IO15) , ICR2 用于配置高 16 个 GPIO ( IO16~ IO31 )

    • 这两个位用来配置中断的触发方式

      举例: 设置GPIO1_IO15为上升沿触发中断 ( GPIO1.ICR1=2<<30 )

      image-20230428173120968


  • IMR 寄存器 : 中断使能寄存器
    • 寄存器中的每一位对应一个IO , 用来控制 GPIO 的中断禁止和使能
    • 如果使能某个 GPIO 的中断,那么设置相应的位为 1 ; ( : 使能 GPIO1_IO00 的中断,那么就可以设置 GPIO1.MIR=1 )
    • 禁止中断 , 则设置相应的位 0

  • ISR 寄存器 : 中断状态寄存器
    • 寄存器中的每一位对应一个IO , 用于判断GPIO 中断是否发生
    • 只要某个 GPIO 的中断发生 , 那么ISR中相应的位就会被置1
    • 在处理完中断之后 , 必须清除中断标志位

  • EDGE_SEL 寄存器 : 边沿选择寄存器
    • 寄存器中的每一位对应一个IO , 用来设置边沿中断
    • 这个寄存器会覆盖 ICR1ICR2 的设置
    • 如果相应的位被 置 1,那么就相当与设置了对应的 GPIO 是 上升沿和下降沿(双边沿) 触发
    • ( 例如 : 设置 GPIO1.EDGE_SEL=1,那么就表示 GPIO1_IO01 是双边沿触中断 )


1.4 CCM时钟控制寄存器 (CCM_CCGRx)

CCM_CCGRx 寄存器 用于使能各个外设模块的时钟 , 为每2位对应一个外设时钟

CGR值如下所示:

image-20230429161134535



1.4.1 CCM_CCGR0

地址 : 0x 20C_4068H

使能方式 :

以GPIO2的时钟为例 ( CCM_CCGR0 = 3<<30 )

image-20230429160800166

image-20230429160813754



1.4.2 CCM_CCGR1

地址 : 0x20C_406C H

image-20230429161435561

image-20230429161400848

image-20230429161502069



1.4.3 CCM_CCGR2

地址 : 0x 20C_4070 H

image-20230429161625904

image-20230429161655183

image-20230429161704139



1.4.4 CCM_CCGR3

地址 0x20C_4074 H

image-20230429161831307

image-20230429161858939

image-20230429161908392



1.4.5 CCM_CCGR4

地址 : 0x20C_4078 H

image-20230429162005946

image-20230429162020687

image-20230429162029214



1.4.6 CCM_CCGR5

地址 : 0x20C_407C H

image-20230429162152246

image-20230429162212873

image-20230429162223164



1.4.7 CCM_CCGR6

地址 : 0x20C_4080 H

image-20230429162317538

image-20230429162341515

image-20230429162350677



1.5 IO复用为GPIO

一般步骤:

  • 使能 GPIO 对应的时钟 (CCM_CCGRx 寄存器)
  • 设置寄存器 IOMUXC_SW_MUX_CTL_PAD_XX_XX,设置 IO 的复用功能
  • 设置寄存器 IOMUXC_SW_PAD_CTL_PAD_XX_XX,设置 IO 的上下拉、速度等电气属性
  • 配置GPIO专属寄存器 , 配置GPIO的输入输出是否使用中断 等 (DRGDIRICR等寄存器)


1.6 可执行文件

将裸机的汇编点灯程序编译成可执行文件 .bin , 需要在.bin 文件前面添加 头部信息

包括 镜像向量表 ( IVT) 、Boot启动数据Boot data )、 配置信息DCD)等 , 后面才是用户可执行代码Application


如下所示:

image-20230430215216894

  • 不同引导设备的 偏移量与 加载区域大小 ( 即 头部信息)

    image-20230430220127202


    • IVT + Boot Data + DCD 的大小为 加载区域大小 减去 偏移量 ( 4K Byte - 1K Byte = 3K Byte )

  • IVTBoot Data 中存放着各个 部分的 链接地址

    如 :

    • entry 存放的是镜像第一行指令所在的位置 , 即 用户执行代码 的入口 , 可以看到箭头指向了 Application

    • self 存放的是IVT 复制到 DDR 中以后的首地址 , 可以看到箭头目的地址指向 IVT的开头



1.6.1 各部分起始地址

在裸机点灯的程序中, 链接的地址是 0x87800000

  • 即 用户执行代码 (Application) 的 起始地址 entry = 0x87800000

  • 镜像向量表 ( IVT) 的起始地址 self = 0x877FF400 [ 0x87800000 - 3k byte = 0x877FF400 ]

  • Boot 启动数据 (Boot Data) 的起始地址 boot data = 0x877FF420

    • [ 此处是IVT的地址 加上 IVT的长度 0x877FF400 + 32 Byte = 0x877FF420]

  • 配置信息 ( DCD ) 的起始地址 dcd = 0x877FF42C

    • [ 此处是 Boot Data 的地址 加上 Boot data 的长度 0x877FF420 + 12 Byte = 0x877FF42C ]

  • 整个头部信息的 起始地址 start = 0X877FF000

    • [ 此处是 IVT 的起始地址 减去 1K Byte 的偏移量 0x877FF400 - 1K Byte = 0X877FF000 ]



1.6.2 镜像向量表

镜像向量表 ( IVT ) 存放了 8条 信息 , 每条信息大小都是 32位


IVT 存放的内容:

image-20230501144049011

image-20230501144104357


header 存放格式:

image-20230501144355924

  • Tag 为一个字节长度,值固定为 0XD1
  • Length大端模式的两字节字段 , 存放着 IVT长度信息 , IVT 长度固定为32字节 ( 0x0020 ) , 则Length = 0x2000
  • Version 为单字节字段 , 值为 0x400x41


1.6.3 Boot Data

Boot Data 存放了 3条 信息 , 每条信息大小都是 32位


image-20230501145200799

  • start 存放的是整个头部信息的存放地址 , 包含偏移量
  • length 存放的是整个镜像 文件的大小
  • plugin 插件


1.6.4 DCD数据
  • DCD 存放 程序镜像中包含的各种配置信息 ( 即 IMX6ULL 寄存器地址 和对应配置信息的集合 )

  • 因为复位芯片后 , I.MX6U 片内的所有寄存器都会复位为默认值,但是这些默认值往往不是我们想要的值 , 通过在DCD中添加这部分寄存器的配置信息 ( 如系统时钟 ) , Boot ROM 会使用这些寄存器地址和配置集合来初始化相应的寄存器

  • DCD 区域不能超过 1768Byte


DCD格式 :

image-20230501151356227


Header格式

image-20230501151456739

  • Tag 是单字节,固定为 0XD2
  • Length大端模式的两字节字段 , 存放着 DCD长度信息
  • Version 为单字节 , 固定为0x41

CMD 格式:

CMD 包含 写数据命令检查数据命令NOP命令解锁命令


其中写数据命令格式如下:

image-20230501152014456

  • Tag 是单字节,固定为 0xCC
  • Length大端模式的两字节字段 , 存放着*** 写入数据长度信息***
  • CMD 是命令 , 包含header
  • Address 是地址 , 存放着写入数据的目标地址 ( 即寄存器地址 )
  • Value / Mask 值或掩码 , 存放着前一个 地址 ( Address) 的数据值 ( 即寄存器值)

Parameter 参数 的格式如下:

image-20230501152610550

  • Parameter 为单字节数据
  • bytes 表示是目标位置宽度,单位为 byte,可以选择 1、2、和 4 字节
  • flags 是命令控制标志位

例:

image-20230501152843932


1.7 点灯(GPIO复用测试)

1.7.1 启动文件(start.S)
// 定义全局入口符号
.global _start

_start:

mrs r0, cpsr      // 将CPSr寄存器的值读到 r0 中
bic r0, r0, #0x1f  // 0001 1111 将低5位清零
orr r0, r0, #0x13  // 0001 0011 将 [M0 ~ M4]设置为SVC模式
msr cpsr, r0      // 将r0 的值重新写回cpsr寄存器中

ldr sp, =0x80200000  // Cortex -A 是FD满递减栈 , DDR的起始地址是0x80000000 , 栈空间 2MB

b main             // 跳转main函数



1.7.2 源文件与头文件

main.c && led.c

/******************************** main.c ******************************/
#include "main.h"
#include "led.h"

/*
* @description : 短时间延时函数
* @param - n : 要延时循环次数(空操作循环次数,模式延时)
* @return : 无
*/
void delay_short(volatile unsigned int n)
{
    while(n--){}
}

/*
 * @description : 延时函数,在 396Mhz 的主频下延时时间大约为 1ms
 * @param - n : 要延时的 ms 数
 * @return : 无
 */
void delay(volatile unsigned int n)
{
    while(n--)
    {
        delay_short(0x7ff);
    }
}




int main(void)
{
    led_init();
    while (1)
    {
        set_led_off();
        delay(500);

        set_led_on();
        delay(500);
    }
    
    return 0; 
}



/******************************** led.c ******************************/
#include "led.h"
#include "main.h"

void led_init (void)
{
    /* 使能时钟 */
    CCM->CCGR1 = (3 << 26) | (3 << 18) ; //使能GPIO1的时钟 和 SIM时钟 , 因为是烧录到SD卡的

    /* IO复用为GPIO */
    SW_MUX_GPIO1_IO03 = 0x05; 

    /* 电气属性设置 */
    SW_PAD_GPIO1_IO03 = 0x00001058;

    /* GPIO 设置为输出 */
    GPIO1->GDIR  = 1 << 3;

    /* GPIO初始化为低电平 */
    GPIO1->DR = 0;
}

void set_led_on(void)
{
    GPIO1->DR &= ~(1 << 3);
}

void set_led_off(void)
{
    GPIO1->DR |= (1 << 3);
}

main.h $$ led.h

/******************************** led.h ******************************/
#ifndef __LED_H
#define __LED_H

void led_init (void);
void set_led_on(void);
void set_led_off(void);

#endif

/******************************** main.h ******************************/
#ifndef __MAIN_H
#define __MAIN_H


/* 
 * 外设寄存器组的基地址 
 */
#define CCM_BASE					(0X020C4000)
#define GPIO1_BASE                  (0x0209C000)

 /* 
  * IOMUX 相关寄存器地址
  */
#define SW_MUX_GPIO1_IO03   *((volatile unsigned int *)0X020E0068)
#define SW_PAD_GPIO1_IO03   *((volatile unsigned int *)0X020E02F4)


/* 
 * CCM寄存器结构体定义
 */
typedef struct 
{
	volatile unsigned int CCR;
	volatile unsigned int CCDR;
	volatile unsigned int CSR;
	volatile unsigned int CCSR;
	volatile unsigned int CACRR;
	volatile unsigned int CBCDR;
	volatile unsigned int CBCMR;
	volatile unsigned int CSCMR1;
	volatile unsigned int CSCMR2;
	volatile unsigned int CSCDR1;
	volatile unsigned int CS1CDR;
	volatile unsigned int CS2CDR;
	volatile unsigned int CDCDR;
	volatile unsigned int CHSCCDR;
	volatile unsigned int CSCDR2;
	volatile unsigned int CSCDR3;	
	volatile unsigned int RESERVED_1[2];
	volatile unsigned int CDHIPR;  
	volatile unsigned int RESERVED_2[2];
	volatile unsigned int CLPCR;
	volatile unsigned int CISR;
	volatile unsigned int CIMR;
	volatile unsigned int CCOSR;
	volatile unsigned int CGPR;
	volatile unsigned int CCGR0;
	volatile unsigned int CCGR1;
	volatile unsigned int CCGR2;
	volatile unsigned int CCGR3;
	volatile unsigned int CCGR4;
	volatile unsigned int CCGR5;
	volatile unsigned int CCGR6;
	volatile unsigned int RESERVED_3[1];
	volatile unsigned int CMEOR;	
} CCM_Type; 

/* 
 * GPIO寄存器结构体
 */
typedef struct 
{
	volatile unsigned int DR;							
	volatile unsigned int GDIR; 							
	volatile unsigned int PSR;								
	volatile unsigned int ICR1; 							
	volatile unsigned int ICR2; 							 
	volatile unsigned int IMR;								 
	volatile unsigned int ISR;			
	volatile unsigned int EDGE_SEL;  
}GPIO_Type;

/* 
 * 外设指针 
 */
#define CCM					((CCM_Type *)CCM_BASE)
#define GPIO1				((GPIO_Type *)GPIO1_BASE)

#endif


1.7.3 Makefile
CC =arm-linux-gnueabihf
link :=-ld -Timx6ul.lds -o											 # 使用链接脚本
elf_bin :=-objcopy -O binary -S
obj :=$(subst .S,.o,$(wildcard *.S)) $(subst .c,.o,$(wildcard *.c))  #将c文件和s文件替换成 o 文件

ledc.bin: $(obj)
	$(CC)$(link) ledc.elf $^              # 链接
	$(CC)$(elf_bin) ledc.elf $@			  # 格式转换

%.o:%.c
	$(CC)-gcc -Wall -nostdlib -c -o $@ $<  # -Wall表示显示编译的时候的所有警告 

%.o:%.S
	$(CC)-gcc -Wall -nostdlib -c -o $@ $<  # -nostdilib表示不链接系统标准启动文件和库文件

.PHONY: clean
clean:
	rm *.o *.bin *.elf
	


1.7.4 链接脚本
SECTIONS{
    . = 0X87800000;
    .text :
    {
        start.o
        *(.text) 
    }
    .rodata ALIGN(4) : {*(.rodata*)}
    .data ALIGN(4)   : { *(.data) }
     __bss_start = .;
     .bss ALIGN(4)  : { *(.bss)  *(COMMON) }
     __bss_end = .;
}

  • 使用关键字 SECTIONS , 描述输出文件的内存布局
  • . 定位计数器 , 其值表示以其为地址开始 , 这里是以 0x87800000 为起始地址
  • .text .rodata .data . bss 分别是代码段只读数据段数据段bss段
  • 这几个段名冒号后面的 ALIGN(4) 表示以四字节对齐 , 大括号里面表示输入文件 ( 即将什么文件 放入哪一段)
  • __bss_start__bss_end 相当于两个变量 , 其值均为定位符 . , 分别报存了 bss段 的起始地址 和 结束地址 , 方便对bss段进行清零


1.8 时钟树


  • 时钟树由三部分组成:时钟切换控制器,根时钟产生器,系统时钟

    • 时钟切换控制器: 用来将外部晶振进行倍频,以实现稳定且高频的时钟信号。

    • 根时钟产生器: 在时钟切换控制器配置完成之后的输出时钟就成了根时钟产生器的时钟源,在根时钟产生器中,经过寄存器配置之后,就成为了外设时钟的时钟源

  • 一个外设的时钟信号的产生途径:晶振向芯片输入时钟信号,信号进入时钟切换控制器,经过用户配置,产生PLL时钟信号,该信号进入根时钟产生步骤,经过分频或倍频,最终成为系统某个外设的时钟信号。


主时钟生成

image-20230505192304352
  • ARM_PLL(PLL1),此路 PLL 是供 ARM 内核使用的,ARM 内核时钟就是由此 PLL生成的,此 PLL 通过编程的方式最高可倍频到 1.3GHz

  • 528_PLL(PLL2),此路 PLL 也叫做 System_PLL,此路 PLL 是固定的 22 倍频 , 即528MHZ

    • 此PLL 有四路PFD : PLL2_PFD0PLL2_PFD1PLL2_PFD2PLL2_PFD3 , 这些PFDPLL为基础,经过不同的分频系数产生不同频率的时钟
    • 官方推荐配置参数image-20230505211259613
  • USB1_PLL(PLL3),此路 PLL 主要用于 USB1PHY, 此路 PLL 是固定的20倍频 , 即480MHZ

    • 此PLL 也有四路PFD : PLL3__PFD0PLL3__PFD1PLL3__PFD2PLL3__PFD3
    • 官方推荐配置参数image-20230505212211255
  • USB2_PLL(PLL7) 主要用于 USB2PHY ,此路PLL固定为20倍频,因此也是480MHz

  • ENET_PLL(PLL6) , 此路PLL 固定为 20+5/6 倍频 , 即480MHz

  • VIDEO_PLL(PLL5) 用于显示相关的外设 , PLL 的输出范围在 650MHz~1300MHz , 可选 1/2/4/8/16 分频

  • AUDIO_PLL(PLL4) 用于音频相关的外设 , PLL 的输出范围在 650MHz~1300MHz , 可选 1/2/4 分频




时钟树

image-20230505203255809
  • 橙色梯形的是 选择器 ,可以选择不同的PLL时钟作为 时钟源
  • 矩形就是 分频器(2位,3位,6位)
  • 黄色高亮的就是用来配置选择器的各种寄存器
  • 红色字是用来配置分频系数的 , 就是配置 寄存器的某个几位<>


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你想要学习IMX6ULL平台上裸机(即不依赖操作系统)编程控制LCD吗?这是一个不错的学习项目!在开始之前,你需要了解一些基本的知识,包括LCD的工作原理、接口协议以及IMX6ULL平台的GPIO控制等。以下是一些步骤供你参考: 1. 了解LCD的基本知识:学习LCD的工作原理、显示原理、分辨率、颜色深度等。 2. 确定LCD的接口类型:IMX6ULL平台可能支持不同的接口类型,如RGB、LVDS、MIPI等,你需要确定你使用的LCD的接口类型,并准备相应的连接线。 3. 配置IMX6ULL的GPIO:通过配置IMX6ULL的GPIO引脚,将其连接到LCD的控制和数据线上。你可以查阅IMX6ULL的技术文档或参考资料来了解如何配置GPIO。 4. 编写LCD控制代码:使用C语言或汇编语言编写裸机代码,通过设置GPIO引脚的电平和发送相应的命令和数据来控制LCD。你需要根据LCD的接口协议编写相应的代码。 5. 调试和测试:将代码下载到IMX6ULL开发板上,连接LCD,并进行调试和测试。你可以使用示波器或逻辑分析仪来检查信号是否正确发送到LCD,并观察LCD是否正确显示图像。 请注意,裸机编程需要一定的硬件和低级编程知识。确保在开始之前对相关的知识和技术有一定的了解。另外,IMX6ULL平台可能有其特定的文档和资料,你可以查阅相关文档以获取更详细的信息。祝你成功学习LCD控制!如果你有其他问题,欢迎继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值