(2)BF飞控笔记


#六、基于FreeRTOS的BF移植到Keil

##(记)6.1随记

###6.1.1 C语言中的#与##

参考:https://www.jb51.net/article/282832.htm

在C语言中#与##是两个常用的预处理运算符。分别代表 记号串化(#)、记号黏结(##),其常用于宏定义中的形参字符串操作。如

####记号串化(#):

记号串化可以将函数式宏定义中的实参转换为字符串。在函数式宏定义中,如果替换列表中有“#”,则其后的预处理记号必须是当前宏的形参。在预处理期间,“#”连同它后面的形参一起被实参取代。例如

#include <stdio.h>
#define PSQR(x) printf("The square of " #x " is %d.\n",((x)*(x)))
int main(void)
{
    int y = 5;
   PSQR(y);
   PSQR(2 + 4);
   PSQR( 3   *   2  );
   return 0;
}

程序运行结果如下:

第1次调用宏时,用"y"替换#x。第2次调用宏时,用"2 + 4"替换#x。第3次调用宏时,用"3 * 2"替换#x。

ANSI C字符串的串联特性将这些字符串与printf()语句的其他字符串组合,生成最终的字符串。例如,第1次调用变成:

printf(“The square of " “y” " is %d.\n”,((y)*(y)));

然后,字符串串联功能将这3个相邻的字符串组合成一个字符串:

“The square of y is %d.\n”

如果传入的实参中间有空白,则不管有多少,都被转换为一个空格,参数开头和末尾的空白都被删除。例如第3次调用宏时,实参“3 * 2 ”转换为“3 * 2”。

####记号黏结(##)

与#运算符类似,##运算符可用于函数式宏的替换部分,它把两个记号组合成一个记号。例如,可以这样定义函数式宏:

#define XNAME(n) x ## n

然后,展开宏XNAME(4)为x4。

记号黏结的作用是将几个预处理记号合并为一个。在一个函数式宏定义中,如果一个预处理记号的前面或者后面有"##",则该记号将与它前面或者后面的记号合并,如果该预处理记号是宏的形参,则用实参执行合并。例如:

#define F(x, y, z)   x##y##r
char F(a, b, c);

第2行的宏调用,其扩展之后如下:

char abr;

##6.1 GPIO初始化
###6.1.1 IOInitGlobal()函数
该函数在初始化init()中初调用,定义在“src/main/drivers/io.c”中284行,函数原型为:

void IOInitGlobal(void)
{
    ioRec_t *ioRec = ioRecs;

    for (unsigned port = 0; port < ARRAYLEN(ioDefUsedMask); port++) {			//循环扫描引脚组(port: 端口)
        for (unsigned pin = 0; pin < sizeof(ioDefUsedMask[0]) * 8; pin++) {		//循环扫描每组各引脚
            if (ioDefUsedMask[port] & (1 << pin)) {								//如果位设置为1
                ioRec->gpio = (gpio_type *)(GPIOA_BASE + (port << 10));   		//“<< 10”代表*0x400,为各端口(A~E)偏移地址
                ioRec->pin = 1 << pin;
                ioRec++;
            }
        }
    }
}
(1) ioRec_t结构体数据类型

函数头使用该结构体体数据类型定义了变量。
Rec全称record,记录?。该数据类型主要包含了GPIO属性相关信息,定义在“src/main/drivers/io_impl.h” 32行处,原型为:

typedef struct ioRec_s {
    gpio_type *gpio;		//GPIO所有相关寄存器
    uint16_t pin;			//引脚号(0~15)
    resourceOwner_e owner;	//所属驱动,LED、Gyro等
    uint8_t index;			//引脚自定义序号
} ioRec_t;  
(2) ioDefUsedMask[]数组

ARRAYLEN是一个判断数组元素个数的宏定义。在for循环中使用了ARRAYLEN来判断ioDefUsedMask[]数组的元素个数。
ioDefUsedMask[]数组原型为:

static const uint16_t ioDefUsedMask[DEFIO_PORT_USED_COUNT] = { DEFIO_PORT_USED_LIST };		//IO使用遮掩数组,位置1表使用,0表不使用

ioDefUsedMask[]数组是一个“IO使用遮掩数组”,“DEFIO_PORT_USED_COUNT”的宏定义值为5,而DEFIO_PORT_USED_LIST宏定义为:

# define DEFIO_PORT_USED_LIST DEFIO_PORT_A_USED_MASK,DmEFIO_PORT_B_USED_MASK,DEFIO_PORT_C_USED_MASK,DEFIO_PORT_D_USED_MASK,DEFIO_PORT_E_USED_MASK  

这是一个“引脚底遮掩列表”,其有5个元素,对应A~E五组IO引脚,也是赋给ioDefUsedMask[]数组各元素的值,该数组是uint16_t 类型的,每个元素的每个位都对应一个引脚,置1表示使用,置0表示遮掩而不使用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

该值最终在 中定义:

#define TARGET_IO_PORTA         0xffff		//目标使用的端口?只用低16位?1表使用?
#define TARGET_IO_PORTB         0xffff
#define TARGET_IO_PORTC         0xffff
#define TARGET_IO_PORTD         0xffff
#define TARGET_IO_PORTE         0xffff  

可以看到,默认使用所有引脚。

##6.2 LED(GPIO)——LED初始化

inty中LED初始化的函数为:

ledInit(statusLedConfig());  

其中statusLedConfig()为一个函数指针。

###6.2.1 statusLedConfig()函数
该函数在“1_keil/src/main/drivers/light_led.h”声明:

typedef struct statusLedConfig_s {
    ioTag_t ioTags[STATUS_LED_NUMBER];			//Tag:标签
    uint8_t inversion;							//电平反转
} statusLedConfig_t;

PG_DECLARE(statusLedConfig_t, statusLedConfig);		//声明对应函数指针等

####(1)statusLedConfig_t LED状态设置结构体数据类型

与LED设置相关的结构体数据类型,STATUS_LED_NUMBER为使用的LED数量(Github源程序设置为3)。ioTag_t数据类型实际定义为uint8_t,而ioTags[]数组元素的作用为储存对应LED标签,之后程序会根据这个标签查询到对应的IO寄存器等?

####(2)PG_DECLARE PG声明宏定义
PG(parameter group) 参数组

该宏定义原型在“src/main/pg/pg.h”中:

// Declare system config
#define PG_DECLARE(_type, _name)                                        \
    extern _type _name ## _System;                                      \
    extern _type _name ## _Copy;                                        \
    static inline const _type* _name(void) { return &_name ## _System; }\
    static inline _type* _name ## Mutable(void) { return &_name ## _System; }\
    struct _dummy                                                       \
    /**/  

其主要作用是根据输入的_type数据类型与_name名称,声明一些变量,定义一些函数指针。这之间运用到了之前章节讲到的“##”记号黏结预处理运算符和“inline”内联函数关键字。

综上所述,statusLedConfig()函数的定义如下:

static inline const statusLedConfig_t* statusLedConfig(void)   
{   
    return &statusLedConfig_System;   
}    

即运行statusLedConfig()函数后返回一个statusLedConfig_System指针,该指针是statusLedConfig_t类型的

####(3)statusLedConfig_System变量的定义与初始化
但是statusLedConfig()函数只是返回这样一个指针,却并未对statusLedConfig_System变量其进行其它操作,其还有其他定义与初始化的地方。
在源码中,statusLedConfig_System变量在“src/main/drivers/light_led.c”中定义:

PG_REGISTER_WITH_RESET_FN(statusLedConfig_t, statusLedConfig, PG_STATUS_LED_CONFIG, 0);  

PG_REGISTER_WITH_RESET_FN宏在“src/main/pg/pg.h”中定义,原型为:

#define PG_REGISTER_WITH_RESET_FN(_type, _name, _pgn, _version)         \
    extern void pgResetFn_ ## _name(_type *);                           \
    PG_REGISTER_I(_type, _name, _pgn, _version, .reset = {.fn = (pgResetFunc*)&pgResetFn_ ## _name }) \
    /**/  

而宏PG_REGISTER_I的定义中涉及许多汇编代码段部分,暂时还未搞清楚具体原理。

猜测statusLedConfig_System变量在pgResetFn_statusLedConfig()函数中初始化,不知道是不是编译环境原因,这个函数在程序进行时并未被执行,这里先手动将它加入init()函数中。
pgResetFn_statusLedConfig()函数原型为:

void pgResetFn_statusLedConfig(statusLedConfig_t *statusLedConfig)
{
    statusLedConfig->ioTags[0] = IO_TAG(LED0_PIN);
    statusLedConfig->ioTags[1] = IO_TAG(LED1_PIN);
    statusLedConfig->ioTags[2] = IO_TAG(LED2_PIN);

    statusLedConfig->inversion = 0
#ifdef LED0_INVERTED
    | BIT(0)
#endif
#ifdef LED1_INVERTED
    | BIT(1)
#endif
#ifdef LED2_INVERTED
    | BIT(2)
#endif
    ;
}    

###6.2.2 ledInit函数

定义在“src/main/drivers/light_led.c”中,函数原型为:  

void ledInit(const statusLedConfig_t *statusLedConfig)
{
ledInversion = statusLedConfig->inversion; //电平反转标志

for (int i = 0; i < STATUS_LED_NUMBER; i++) {
    if (statusLedConfig->ioTags[i]) {
        leds[i] = IOGetByTag(statusLedConfig->ioTags[i]);
        IOInit(leds[i], OWNER_LED, RESOURCE_INDEX(i));
        IOConfigGPIO(leds[i], IOCFG_OUT_PP);
    } else {
        leds[i] = IO_NONE;
    }
}

LED0_OFF;
LED1_OFF;
LED2_OFF;

}

####(1)IO_TAG(LED1_PIN)

这段代码运用了许多宏定义。
首先LED0_PIN等都在“src/main/target/AT32F437DEV/target.h”中定义,该文件是用来总体设置每个引脚功能与连接设备的。LED1_PIN定义如下:

#define LED1_PIN                PD14 //confirm on LQFP64  

其次IO_TAG宏的作用是运用“##”字符黏结加入“DEFIO_TAG__”前缀(好几层调用啊,DEF:def,定义; TAG:标签)。所以IO_TAG(LED1_PIN)展开后为:

DEFIO_TAG__PD14  

这又是一个宏了,经过若干调用展开为:

((((gpioid) + 1) << 4) | (pin))  

其中gpioid代表寄存器组(A:0 …… D:3 ……),pin为引脚号(1~15)。DEFIO_TAG__PD14对应的就是:

((((3) + 1) << 4) | (13)) = 77  

####(2) IOGetByTag(statusLedConfig->ioTags[i])

for循环中的这段代码,根据之前定义的IO标签,从好久之前(6.1小节)中所提到的ioRecs[]数组中找到对应的元素,并反回其指针。ioRecs[]有80个元素(对应5*16个引脚),如若是定义为PD14引脚,则返回 “&ioRecs[ 4 * 16 + 14]”

####(3)IOInit()函数
这个函数只是用来设置输入ioRec_t类型变量指针的值,而没有真正初始化GPIO

####(4)IOConfigGPIO()函数
这个函数老师真正GPIO外设的初始化函数,输入ioRec_t类型变量指针与IO电气参数。
而这个函数主要调用了RCC_ClockCmd()函数用来使能GPIO外设时钟,调用了gpio_init()函数用来初始化GPIO其它参数。其中gpio_init()函数是AT32库中的一个函数。

####(5)LED外设宏操作

对LED操作的宏,以LED1为例来说,有LED1_TOGGLE、LED1_OFF、LED1_ON分别代表LED电平反转、关、开。对LED的GPIO的操作主要是通过对“6.3.7 GPIO设置/清除寄存器(GPIOx_SCR)”、“6.3.6 GPIO输出数据寄存器(GPIOx_ODT)”两个GPIO外设寄存器来实现的。

6.3 USART初始化

注:这里为了区分,将BF提供的init()函数改为BF_init()函数,之后Keil程序中将使用BF_init()作为BF源码相关的初始化。

(记)6.3.1 随记

(1)USART估计初始化函数:
259行 printfSerialInit();

527行 uartPinConfigure(serialPinConfig());

540行 serialInit(featureIsEnabled(FEATURE_SOFTSERIAL), SERIAL_PORT_NONE);

(2)PG(parameter group) 参数组

6.3.1 printf()函数初始化与重定义

在BF_init()函数中printf串口通过调用printfSerialInit()函数进行初始化。
而printf()函数的重定义,在不同编译环境下是不同,其在“src/main/common/printf_serial.c”中实现。这里参考AT32官方例程进行了移植。原型如下:

#pragma import(__use_no_semihosting)
struct __FILE
{
    int handle;
};

FILE __stdout;

void _sys_exit(int x)
{
    x = x;
}
/* __use_no_semihosting was requested, but _ttywrch was */
void _ttywrch(int ch)
{
    ch = ch;
}
    
int fputc(int ch, FILE *f)		//重定义printf
{
    while(usart_flag_get(PRINT_UART, USART_TDBE_FLAG) == RESET);
    usart_data_transmit(PRINT_UART, ch);
    return ch;
}  

具体原理参考:https://zhuanlan.zhihu.com/p/649806021

6.3.2 uartPinConfigure(serialPinConfig())

这句代码调用了uartPinConfigure()函数,其输入参数是serialPinConfig()执行函数返回的一个变量的指针。无独有偶,serialPinConfig()函数也是使用了之前“6.2.1 statusLedConfig()函数”小节提到PG_DECLARE 宏来定义的。 位于“src/main/drivers/serial.h”中

PG_DECLARE(serialPinConfig_t, serialPinConfig);  
(1)PG_DECLARE 宏定义

PG(parameter group) 参数组

该宏定义原型在“src/main/pg/pg.h”中:

// Declare system config
#define PG_DECLARE(_type, _name)                                        \
    extern _type _name ## _System;                                      \
    extern _type _name ## _Copy;                                        \
    static inline const _type* _name(void) { return &_name ## _System; }\
    static inline _type* _name ## Mutable(void) { return &_name ## _System; }\
    struct _dummy                                                       \
    /**/  

其主要作用是根据输入的_type数据类型与_name名称,声明一些变量,定义一些函数指针。运用到了之前章节讲到的“##”记号黏结预处理运算符和“inline”内联函数关键字。

所以

PG_DECLARE(serialPinConfig_t, serialPinConfig);  

展开后的主要内容为:

extern serialPinConfig_t serialPinConfig_System; 

static inline const serialPinConfig_t* serialPinConfig(void)   
{   
    return &serialPinConfig_System;   
}    

即声明serialPinConfig_System变量,并在运行serialPinConfig()函数后返serialPinConfig_System的指针,该指针是serialPinConfig_t类型的。

(2)PG_REGISTER_WITH_RESET_FN宏

又是无独有偶,与“6.2.1 statusLedConfig()函数”小节一样,serialPinConfig_System变量的初始化也依靠于PG_REGISTER_WITH_RESET_FN宏与其所调用的pgResetFn_serialPinConfig函数。引用源码为:

PG_REGISTER_WITH_RESET_FN(serialPinConfig_t, serialPinConfig, PG_SERIAL_PIN_CONFIG, 0);

该宏定义在“src/main/pg/pg.h”中,原型为:

//参数组注册表复位函数?
#define PG_REGISTER_WITH_RESET_FN(_type, _name, _pgn, _version)         \
    extern void pgResetFn_ ## _name(_type *);                           \
    PG_REGISTER_I(_type, _name, _pgn, _version, .reset = {.fn = (pgResetFunc*)&pgResetFn_ ## _name }) \
    /**/

该宏有四个参数,分别为:

  1. _type (结构体)数据类型
  2. _name 参数组名字?
  3. _pgn 参数组序号
  4. _version 版本号

其中第3行 “extern void pgResetFn_ ## _name(_type *);”为声明参数组复位函数,第4行则又调用了一个宏。程序展开为:

extern void pgResetFn_serialPinConfig(serialPinConfig_t *);                           \
PG_REGISTER_I(serialPinConfig_t, serialPinConfig, PG_SERIAL_PIN_CONFIG, 0, .reset = {.fn = (pgResetFunc*)&pgResetFn__serialPinConfig}) \
/**/

主要是声明“pgResetTemplate_serialPinConfig”函数,该函数在“src/main/drivers/serial_pinconfig.c”定义。

PG_REGISTER_I宏的原型为:

// Register system config		根据注册表设置系统?
#define PG_REGISTER_I(_type, _name, _pgn, _version, _reset)             \
    _type _name ## _System;                                             \
    _type _name ## _Copy;                                               \
    uint32_t _name ## _fnv_hash;                                        \
    /* Force external linkage for g++. Catch multi registration */      \
    extern const pgRegistry_t _name ## _Registry;                       \
    const pgRegistry_t _name ##_Registry PG_REGISTER_ATTRIBUTES = {     \
        .pgn = _pgn | (_version << 12),                                 \
        .length = 1,                                                    \
        .size = sizeof(_type) | PGR_SIZE_SYSTEM_FLAG,                   \
        .address = (uint8_t*)&_name ## _System,                         \
        .fnv_hash = &_name ## _fnv_hash,                                \
        .copy = (uint8_t*)&_name ## _Copy,                              \
        .ptr = 0,                                                       \
        _reset,                                                         \
    } 

该宏有五个参数,分别为:

  1. _type (结构体)数据类型
  2. _name 参数组名字?
  3. _pgn 参数组序号
  4. _version 版本号
  5. _reset ?

该宏中又嵌套有PG_REGISTER_ATTRIBUTES宏。

则宏一并展开为

extern void pgResetFn_serialPinConfig(serialPinConfig_t *); 
serialPinConfig_t serialPinConfig_System;                                             \
serialPinConfig_t serialPinConfig_Copy;                                               \
uint32_t serialPinConfig_fnv_hash;                                        \
/* Force external linkage for g++. Catch multi registration */      \
extern const pgRegistry_t serialPinConfig_Registry;                       \
const pgRegistry_t serialPinConfig_Registry PG_REGISTER_ATTRIBUTES = {     \
    .pgn = PG_SERIAL_PIN_CONFIG | (0 << 12),                                 \
    .length = 1,                                                    \
    .size = sizeof(serialPinConfig_t) | PGR_SIZE_SYSTEM_FLAG,                   \
    .address = (uint8_t*)&serialPinConfig_System,                         \
    .fnv_hash = &serialPinConfig_fnv_hash,                                \
    .copy = (uint8_t*)&serialPinConfig_Copy,                              \
    .ptr = 0,                                                       \
    .reset = {.fn = (pgResetFunc*)&pgResetFn__serialPinConfig},                                                         \
}     

总之宏一个套一个下来,最终实现了如下几个功能:

  1. 定义三个变量:serialPinConfig_System、serialPinConfig_Copy、serialPinConfig_fnv_hash
  2. 声明(定义前声明,有点奇怪)、定义一个pgRegistry_t结构体数据类型的变量serialPinConfig_Registry,并进行赋值。
    即为.pgn、.reset等子变量赋值。
  3. 使用之前提到的__attribute__将serialPinConfig_Registry放在.pg_registry段中,屏蔽优化,4字节对齐。

可见这里也只是声明,并示执行pgResetFn__serialPinConfig()函数。

不过经过述宏的学习,本月亮大概猜测出程序是先初始化EPPROM,读取参数并初始化一些变量(包括执行这些函数),然后才去初始化LED、USART等驱动的。果不其然,爷发现在“init.c”的388~392行,初始化了EPPROM,即:

initEEPROM();

ensureEEPROMStructureIsValid();

bool readSuccess = readEEPROM();

点奇怪)、定义一个pgRegistry_t结构体数据类型的变量serialPinConfig_Registry,并进行赋值。
即为.pgn、.reset等子变量赋值。
3. 使用之前提到的__attribute__将serialPinConfig_Registry放在.pg_registry段中,屏蔽优化,4字节对齐。

可见这里也只是声明,并示执行pgResetFn__serialPinConfig()函数。

不过经过述宏的学习,本月亮大概猜测出程序是先初始化EPPROM,读取参数并初始化一些变量(包括执行这些函数),然后才去初始化LED、USART等驱动的。果不其然,爷发现在“init.c”的388~392行,初始化了EPPROM,即:

initEEPROM();

ensureEEPROMStructureIsValid();

bool readSuccess = readEEPROM();

而这也的确是比LED、USART等驱动的初始化要更前的。

这里先在uartPinConfigure(serialPinConfig());前手动加上pgResetFn_serialPinConfig()函数的初始化。

6.3.3 pgResetFn_serialPinConfig()函数

定义在“src/main/drivers/serial_pinconfig.c”中,原型为:

void pgResetFn_serialPinConfig(serialPinConfig_t *serialPinConfig)
{
    for (size_t index = 0 ; index < ARRAYLEN(serialDefaultPin) ; index++) {
        const serialDefaultPin_t *defpin = &serialDefaultPin[index];
        serialPinConfig->ioTagRx[SERIAL_PORT_IDENTIFIER_TO_INDEX(defpin->ident)] = defpin->rxIO;
        serialPinConfig->ioTagTx[SERIAL_PORT_IDENTIFIER_TO_INDEX(defpin->ident)] = defpin->txIO;
        serialPinConfig->ioTagInverter[SERIAL_PORT_IDENTIFIER_TO_INDEX(defpin->ident)] = defpin->inverterIO;
    }
}    
(1)serialDefaultPin[]数组

ARRAYLEN是一个判断数组元素个数的宏,在for循环中判断了serialDefaultPin[]数组的元素个数。

serialDefaultPin[]数组是一个serialDefaultPin_t结构体数据类型的数组。serialDefaultPin_t的定义为:

typedef struct serialDefaultPin_s {
    serialPortIdentifier_e ident;		//串口ID号
    ioTag_t rxIO, txIO, inverterIO;		//IO标签(uint8)
} serialDefaultPin_t;

数组定义:

static const serialDefaultPin_t serialDefaultPin[] = {		//串口默认引脚数组
#ifdef USE_UART1
    { SERIAL_PORT_USART1, IO_TAG(UART1_RX_PIN), IO_TAG(UART1_TX_PIN), IO_TAG(INVERTER_PIN_UART1) },
#endif  
……  
}  

则每个数组元素的标签类变量与“target.h”中的宏定义一一对应。其中“IO_TAG”宏的作用为,加上“DEFIO_TAG__”前缀。 如:

IO_TAG(UART1_RX_PIN)  

展开为:

DEFIO_TAG__PA10    

再进一步展开

((ioTag_t)((((gpioid) + 1) << 4) | (pin)))  

其中gpioid代表寄存器组(A:0 …… D:3 ……),pin为引脚号(1~15)。 替换其中宏参数即得:

((uint8_t)((((0) + 1) << 4) | (10)))  
(2)其余部分

其余部分代码的作用就是将serialDefaultPin[]数组中每个元素的“Tag”标签变量赋值给serialPinConfig指针所对应变量(即serialPinConfig_System)子变量数组的每个标签元素。

6.3.4 uartPinConfigure()函数

展开宏,得函数原型为:


	void uartPinConfigure(const serialPinConfig_t *pSerialPinConfig)
	{
	    uartDevice_t *uartdev = uartDevice;
	
	    for (size_t hindex = 0; hindex < UARTDEV_COUNT; hindex++) {
	
	        const uartHardware_t *hardware = &uartHardware[hindex];
	        const UARTDevice_e device = hardware->device;
	
	        uartdev->pinSwap = false;
			
	        for (int pindex = 0 ; pindex < UARTHARDWARE_MAX_PINS ; pindex++) {
	            if (pSerialPinConfig->ioTagRx[device] && (pSerialPinConfig->ioTagRx[device] == hardware->rxPins[pindex].pin)) {
	                uartdev->rx = hardware->rxPins[pindex];
	            }
	
	            if (pSerialPinConfig->ioTagTx[device] && (pSerialPinConfig->ioTagTx[device] == hardware->txPins[pindex].pin)) {
	                uartdev->tx = hardware->txPins[pindex];
	            }
	
	            // Check for swapped pins
	            if (pSerialPinConfig->ioTagTx[device] && (pSerialPinConfig->ioTagTx[device] == hardware->rxPins[pindex].pin)) {
	                uartdev->tx = hardware->rxPins[pindex];
	                uartdev->pinSwap = true;
	            }
	
	            if (pSerialPinConfig->ioTagRx[device] && (pSerialPinConfig->ioTagRx[device] == hardware->txPins[pindex].pin)) {
	                uartdev->rx = hardware->txPins[pindex];
	                uartdev->pinSwap = true;
	            }
	        }
	
	        if (uartdev->rx.pin || uartdev->tx.pin) {
	            uartdev->hardware = hardware;
	            uartDevmap[device] = uartdev++;
	        }
	    }
	}     
(1)uartDevice_t 结构体数据类型
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值