【BSP开发经验】Linux与uboot控制台串口

背景

leadcore1861平台在到板后发现引出的串口控制台没有任何输出,询问厂家后了解到厂家代码指定串口4作为串口控制台,而我们使用了串口0,所以需要对uboot串口和linux串口控制台进行修改,指定到串口0输出。

uboot 串口驱动实现

相关文件

(1)common/serial.c

实现了U-Boot串口设备的通用接口。

(2)include/serial.h

定义了串口设备的数据结构及一些函数声明。

(3)board/开发板名字/serial.c、cpu/cpu名字/serial.c、drivers/serial/serial.c、drivers/serial/serial_xxx.c

各种开发板、控制器的串口操作的具体实现。

数据结构

在include/serial.h 中定义了表示串口设备的数据结构。

struct serial_device {
	char name[NAMESIZE];
	char ctlr[CTLRSIZE];

    int  (*init) (void);
	void (*setbrg) (void);
	int (*getc) (void);
	int (*tstc) (void);
	void (*putc) (const char c);
	void (*puts) (const char *s);

    struct serial_device *next;
};

name表示设备名称。

ctrl表示串口硬件的名称。

init表示串口初始化函数指针。

setbrg表示串口波特率设置函数指针。

getc表示从串口获取一个字符的函数指针。

tstc表示检查串口是否接收到数据的函数指针。

putc表示串口发送一个字符的函数指针。

puts表示串口发送字符串的函数指针。

next表示指向下一个串口设备的指针。各个串口设备通过next组成一个单向链表。

串口驱动的实现

common/serial.c实现了U-Boot串口设备的通用接口,外部调用这些接口函数来完成串口设备的操作。这些接口函数有:

(1)int serial_register (struct serial_device *dev)

注册串口设备。各个串口设备的操作在board/开发板名字/serial.c、cpu/cpu名字/serial.c、drivers/serial/serial.c、drivers/serial/serial_xxx.c文件里实现。

(2)int serial_assign (char *name)

分配串口设备。

(3)int serial_init (void)

串口初始化

(4)void serial_setbrg (void)

设置串口波特率

(5)int serial_getc (void)

从串口获取一个字符

(6)void serial_putc (const char c)

向串口发送一个字符。

(7)void serial_puts (const char *s)

向串口发送字符串。

(8)int serial_tstc (void)

检查串口是否接收到字符。

(9)void serial_initialize (void)

注册一些控制器、开发板的串口设备,并把控制台的默认串口设备分配为本文件操作的串口设备。

(10)void serial_reinit_all (void)

初始化所有串口。

uboot串口输出修改

leadcore1861使用了 serial_ns16550

__weak struct serial_device *default_serial_console(void)
{
#if CONFIG_CONS_INDEX == 1
	return &eserial1_device;
#elif CONFIG_CONS_INDEX == 2
	return &eserial2_device;
#elif CONFIG_CONS_INDEX == 3
	return &eserial3_device;
#elif CONFIG_CONS_INDEX == 4
	return &eserial4_device;
#elif CONFIG_CONS_INDEX == 5
	return &eserial5_device;
#elif CONFIG_CONS_INDEX == 6
	return &eserial6_device;
#else
#error "Bad CONFIG_CONS_INDEX."
#endif
}

可以看出其是通过CONFIG_CONS_INDEX 来控制默认串口控制台,全局搜索:CONFIG_CONS_INDEX

将include/configs/comip_lc1861evb.h

中的

#define CONFIG_CONS_INDEX 4

修改为

#define CONFIG_CONS_INDEX 1

Linux 串口驱动实现

Linux串口驱动框架

在这里插入图片描述

Linux系统提供了串口驱动框架,我们需要按照相应的串口框架编写驱动程序即可。串口驱动没有主机端和设备端之分,就只有一个串口驱动,我们真正要做的就是在设备数中添加所要使用的串口节点信息。当系统启动以后串口和设备匹配成功,相应的串口就会被驱动起来,生成/dev/ttySx文件。
struct  uart_driver{
      struct module  *owner;     //模块所属者

    const char *drvier_name;   //驱动名字

    const char *dev_name;      //设备名字

    int  major;                //主设备号

    int  minor;                //此设备号

    int   nr;                  //设备树

    struct consle    *cons;    //控制台

    struct uart_state    *state;

    struct tty_driver   *tty_driver;
};

//串口驱动要定义uart_driver,向系统中注册这个uart_driver

int uart_register_driver(struct uart_driver *drv)

uart_ops的具体实现

uart_port中的ops成员变量很重要,因为ops包含了针对UART具体的驱动函数,Linux系统收发数据最终调用的都是ops中的函数。ops是uart_ops类型的结构体指针变量。
struct  uart_ops{
    unsigned int (*tx_empty)(struct uart_port  *);

    void (*stop_tx)(struct uart_port  *);

};

Linux console

uboot传参分析

linux启动时uboot传递进console=ttyS0,115200n8的参数

内核中用__setup()宏声明参数处理的方法:__setup("console=", console_setup); 

1.console_cmdline结构体

struct console_cmdline 

{ 
    char name[8];    //驱动名  
    int  index;      //次设备号  
    char *options;   //选项  
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE  
    char    *brl_options;  
#endif  
}; 


2.内核调用console_setup()函数处理uboot传进的console参数
static int __init console_setup(char *str) 
{ 

    char buf[sizeof(console_cmdline[0].name) + 4]; //分配驱动名+index的缓冲区,分配12个字节 
    char *s, *options, *brl_options = NULL; 
    int idx; 

#ifdef CONFIG_A11Y_BRAILLE_CONSOLE  
    if (!memcmp(str, "brl,", 4)) { 
        brl_options = ""; 
        str += 4; 
    } else if (!memcmp(str, "brl=", 4)) { 
        brl_options = str + 4; 
        str = strchr(brl_options, ','); 
        if (!str) { 
            printk(KERN_ERR "need port name after brl=\n"); 
            return 1; 
        } 
        *(str++) = 0; 
    } 
#endif  

 
    if (str[0] >= '0' && str[0] <= '9') { //第一个参数属于[0,9]  
        strcpy(buf, "ttyS");    //则将其驱动名设为ttyS  
        strncpy(buf + 4, str, sizeof(buf) - 5);//将次设备号放其后面  
    } else { 
        strncpy(buf, str, sizeof(buf) - 1); //否则直接将驱动名+设备号拷贝到buf中
    } 

    buf[sizeof(buf) - 1] = 0; 
    if ((options = strchr(str, ',')) != NULL) //获取options,即“115200n8”  
        *(options++) = 0; 

#ifdef __sparc__  

    if (!strcmp(str, "ttya")) 

        strcpy(buf, "ttyS0"); 

    if (!strcmp(str, "ttyb")) 

        strcpy(buf, "ttyS1"); 

#endif 
    for (s = buf; *s; s++) 
        if ((*s >= '0' && *s <= '9') || *s == ',')//移动指针s到次设备号处 
            break; 

    idx = simple_strtoul(s, NULL, 10); //获取次设备号,字符串转换成unsigend long long型数据,s表示字符串的开始,NULL表示字符串的结束,10表示进制

    //这里返回的是次设备号=0
    *s = 0; 
    __add_preferred_console(buf, idx, options, brl_options); 
    console_set_on_cmdline = 1; 
    return 1; 

} 

3.__add_preferred_console()函数
//整体的作用是根据uboot传递的参数设置全局console_cmdline数组
//该数组及全局selected_console,在register_console中会使用到
static int __add_preferred_console(char *name, int idx, char *options,char *brl_options) 

{ 
    struct console_cmdline *c; 
    int i; 
    for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++)//可以最多8个console  
        if (strcmp(console_cmdline[i].name, name) == 0 && console_cmdline[i].index == idx) { 
            //比较已注册的console_cmdline数组中的项的名字及次设备号,若console_cmdline已经存在  
                if (!brl_options) 
                    selected_console = i;//设置全局selected_console索引号  
                return 0;//则返回  
        } 
    if (i == MAX_CMDLINECONSOLES)//判断console_cmdline数组是否满了  
        return -E2BIG; 
    if (!brl_options) 
        selected_console = i; //设置全局selected_console索引号  

    c = &console_cmdline[i];//获取全局console_cmdline数组的第i项地址  
    strlcpy(c->name, name, sizeof(c->name));  //填充全局console_cmdline的驱动名“ttyS2”  
    c->options = options;    //填充配置选项115200n8  
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE  
    c->brl_options = brl_options; 
#endif  
    c->index = idx;  //填充索引号2,即次设备号  
    return 0; 

} 

 

early_printk

//在调用console_init之前调用printk也能打印出信息,这是為什麼呢?在start_kernel函数中很早就调用了 parse_early_param函数,
//该函数会调用到链接脚本中.init.setup段的函数。其中就有 setup_early_serial8250_console函数。
//该函数通过 register_console(&early_serial8250_console);
//注册了一个比较简单的串口设备。可以用来打印内核启 动早期的信息。
//对于early printk的console注册往往通过内核的early_param完成。
early_param(“earlycon”,setup_early_serial8250_console);
//定义一个earlycon的内核参数,内核解析这个参数时调用setup_early_serial8250_console()函数
1.setup_early_serial8250_console()函数

//earlycon = uart8250-32bit,0xfa882000,115200n8

int __init setup_early_serial8250_console(char *cmdline)
{
    char *options;
    int err;
 
    options = strstr(cmdline, "uart8250,");//找到“uart8250,”字符串,返回此字符串的起始位置
    if (!options) {
        options = strstr(cmdline, "uart,");
        if (!options)
            return 0;
    }

    options = strchr(cmdline, ',') + 1;//options指针指向第一个逗号后边的字符串地址
    err = early_serial8250_setup(options);//进行配置
    if (err < 0)
        return err;

 
    /*

    static struct console early_serial8250_console __initdata = {
        .name   = "uart",
        .write  = early_serial8250_write,
        .flags  = CON_PRINTBUFFER | CON_BOOT,//所用具有CON_BOOT属性的console都会在内核初始化到late initcall阶段被注销,相互消他们的函数是
        .index  = -1,
    };
    */
    //注册一个早期的console,到真正的console_init时,此console会被注销,因为设置了CON_BOOT标志
    register_console(&early_serial8250_console);

    return 0;

}

 

static int __init early_serial8250_setup(char *options)
{
    struct early_serial8250_device *device = &early_device;
    int err;

    if (device->port.membase || device->port.iobase)//early_device设备的端口地址若配置过则返回

        return 0;
    err = parse_options(device, options);//解析参数并配置early_device设备对应的uart_port结构
    if (err < 0)
        return err;
    init_port(device);//early_device设备对应的初始化uart_port结构
    return 0;
}

 

static int __init parse_options(struct early_serial8250_device *device,char *options)

{
    struct uart_port *port = &device->port;//找到early_device设备对应的uart_port结构
    int mmio, mmio32, length;
    if (!options)
        return -ENODEV;
    port->uartclk = BASE_BAUD * 16;//串口时钟
    mmio = !strncmp(options, "mmio,", 5);//查找"mmio,"字符串,找到mmio=1
    mmio32 = !strncmp(options, "mmio32,", 7);//mmio32=0
    if (mmio || mmio32) {
        port->iotype = (mmio ? UPIO_MEM : UPIO_MEM32);//串口类型设为UPIO_MEM=2
        port->mapbase = simple_strtoul(options + (mmio ? 5 : 7),&options, 0);//获得串口的配置寄存器基础地址(物理地址),这里是得到0xff5e0000
        if (mmio32)
            port->regshift = 2;
#ifdef CONFIG_FIX_EARLYCON_MEM
        set_fixmap_nocache(FIX_EARLYCON_MEM_BASE,port->mapbase & PAGE_MASK);
        port->membase =(void __iomem *)__fix_to_virt(FIX_EARLYCON_MEM_BASE);
        port->membase += port->mapbase & ~PAGE_MASK;
#else
        port->membase = ioremap_nocache(port->mapbase, 64);//映射到内存的配置寄存器基础地址
        if (!port->membase) {
            printk(KERN_ERR "%s: Couldn't ioremap 0x%llx\n",    __func__,(unsigned long long) port->mapbase);
            return -ENOMEM;
        }
#endif
    } else if (!strncmp(options, "io,", 3)) {
        port->iotype = UPIO_PORT;
        port->iobase = simple_strtoul(options + 3, &options, 0);
        mmio = 0;
    } else
        return -EINVAL;
    options = strchr(options, ',');//指针移到“115200n8”字符串处
    if (options) {//存在
        options++;
        device->baud = simple_strtoul(options, NULL, 0);//取得波特率115200
        length = min(strcspn(options, " "), sizeof(device->options));
        strncpy(device->options, options, length);//将字符串115200n8拷贝到设备的device->options字段中
    } else {
        device->baud = probe_baud(port);
        snprintf(device->options, sizeof(device->options), "%u",device->baud);
    }
    if (mmio || mmio32)
        printk(KERN_INFO "Early serial console at MMIO%s 0x%llx (options '%s')\n",mmio32 ? "32" : "",(unsigned long long)port->mapbase,device->options);
    else
        printk(KERN_INFO
              "Early serial console at I/O port 0x%lx (options '%s')\n",port->iobase,device->options);


    return 0;

}

 

static void __init init_port(struct early_serial8250_device *device)
{
    struct uart_port *port = &device->port;
    unsigned int divisor;
    unsigned char c;

 

    serial_out(port, UART_LCR, 0x3);    /* 8n1 */

    serial_out(port, UART_IER, 0);      /* no interrupt */

    serial_out(port, UART_FCR, 0);      /* no fifo */

    serial_out(port, UART_MCR, 0x3);    /* DTR + RTS */

 

    divisor = port->uartclk / (16 * device->baud);//根据波特率设置分频

    c = serial_in(port, UART_LCR);

    serial_out(port, UART_LCR, c | UART_LCR_DLAB);

    serial_out(port, UART_DLL, divisor & 0xff);

    serial_out(port, UART_DLM, (divisor >> 8) & 0xff);

    serial_out(port, UART_LCR, c & ~UART_LCR_DLAB);

}

 

void register_console(struct console *newcon)
{
    int i;
    unsigned long flags;
    struct console *bcon = NULL;

    /*
    现在是注册一个early console,即
    static struct console early_serial8250_console __initdata = {
        .name   = "uart",
        .write  = early_serial8250_write,
        .flags  = CON_PRINTBUFFER | CON_BOOT,//所用具有CON_BOOT属性的console都会在内核初始化到late initcall阶段被注销,相互消他们的函数是
        .index  = -1,
    };

    */

    if (console_drivers && newcon->flags & CON_BOOT) {//注册的是否是引导控制台。early console的CON_BOOT置位,表示只是一个引导控制台,以后会被注销
        for_each_console(bcon) {遍历全局console_drivers数组  
            if (!(bcon->flags & CON_BOOT)) {//判断是否已经有引导控制台了,有了的话就直接退出
                printk(KERN_INFO "Too late to register bootconsole %s%d\n",newcon->name, newcon->index);
                return;
            }
        }
	}

    if (console_drivers && console_drivers->flags & CON_BOOT)//如果注册的是引导控制台 
        bcon = console_drivers;//让bcon指向全局console_drivers 

    if (preferred_console < 0 || bcon || !console_drivers)
        preferred_console = selected_console;//设置preferred_console为uboot命令选择的selected_console(即索引)  
    if (newcon->early_setup)//early console没有初始化early_setup字段,以下这个函数不执行
        newcon->early_setup();//调用serial8250_console_early_setup()
    if (preferred_console < 0) {
        if (newcon->index < 0)
            newcon->index = 0;
        if (newcon->setup == NULL ||newcon->setup(newcon, NULL) == 0) {
            newcon->flags |= CON_ENABLED;
            if (newcon->device) {
                newcon->flags |= CON_CONSDEV;
                preferred_console = 0;
            }
        }
    }
     //传给内核参数:
     //所以这里将根据传参console=ttyGS0,115200来配置作为console的ttyGS0串口

    for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0];i++) {//遍历全局console_cmdline找到匹配的
        if (strcmp(console_cmdline[i].name, newcon->name) != 0)//比较终端名称“ttyS”
            continue;
        if (newcon->index >= 0 &&newcon->index != console_cmdline[i].index)//console_cmdline[i].index=2。//比较次设备号 
            continue;
        if (newcon->index < 0)
            newcon->index = console_cmdline[i].index;//将终端号赋值给serial8250_console->index
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE//没有定义,下边不执行
        if (console_cmdline[i].brl_options) {
            newcon->flags |= CON_BRL;
braille_register_console(newcon,console_cmdline[i].index,console_cmdline[i].options,console_cmdline[i].brl_options);
            return;
        }
#endif
        //console_cmdline[i].options = "115200n8",对于early console而言setup字段未被初始化,故下边的函数不执行
        if (newcon->setup &&newcon->setup(newcon, console_cmdline[i].options) != 0)//调用serial8250_console_setup()对终端进行配置
            break;
        newcon->flags |= CON_ENABLED; //设置标志为CON_ENABLE(这个在printk调用中使用到)
        newcon->index = console_cmdline[i].index;//设置索引号  
        if (i == selected_console) { //索引号和uboot指定的console的一样
            newcon->flags |= CON_CONSDEV;//设置标志CON_CONSDEV(全局console_drivers链表中靠前)
            preferred_console = selected_console;
        }
        break;
    }//for循环作用大致是查看注册的console是否是uboot知道的引导console,是则设置相关标志和preferred_console

    if (!(newcon->flags & CON_ENABLED))
        return;
    if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV))//防止重复打印  
        newcon->flags &= ~CON_PRINTBUFFER;
    acquire_console_sem();
    if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) {//如果是preferred控制台
        newcon->next = console_drivers;
        console_drivers = newcon;//添加进全局console_drivers链表前面位置(printk中会遍历该表调用合适的console的write方法打印信息)
        if (newcon->next)
            newcon->next->flags &= ~CON_CONSDEV;
    } else {//如果不是preferred控制台
        newcon->next = console_drivers->next;
        console_drivers->next = newcon; //添加进全局console_drivers链表后面位置
    }
    //主册console主要是刷选preferred_console放置在全局console_drivers链表前面,剩下的console放置链表靠后的位置,并设置相应的flags,
    //console_drivers最终会在printk函数的层层调用中遍历到,并调用console的write方法将信息打印出来
    if (newcon->flags & CON_PRINTBUFFER) {
        spin_lock_irqsave(&logbuf_lock, flags);
        con_start = log_start;
        spin_unlock_irqrestore(&logbuf_lock, flags);
    }
    release_console_sem();
    if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) {
        printk(KERN_INFO "console [%s%d] enabled, bootconsole disabled\n",newcon->name, newcon->index);
        for_each_console(bcon)
            if (bcon->flags & CON_BOOT)
                unregister_console(bcon);
    } else {//调用这里
        printk(KERN_INFO "%sconsole [%s%d] enabled\n",(newcon->flags & CON_BOOT) ? "boot" : "" ,newcon->name, newcon->index);
    }
}

 

printk流程


  

//内核的打印函数

asmlinkage int printk(const char *fmt, ...)
{
    va_list args;   //可变参数链表
    int r;
    va_start(args, fmt);    //获取第一个可变参数
    r = vprintk(fmt, args); //调用vprintk函数
    va_end(args);   //释放可变参数链表指针
    return r;
}

//vprintk函数
asmlinkage int vprintk(const char *fmt, va_list args)
{
    int printed_len = 0;
    int current_log_level = default_message_loglevel;
    unsigned long flags;
    int this_cpu;
    char *p;
    boot_delay_msec();
    printk_delay();
    preempt_disable();
    raw_local_irq_save(flags);
    this_cpu = smp_processor_id();
    if (unlikely(printk_cpu == this_cpu)) {
        if (!oops_in_progress) {
            recursion_bug = 1;
            goto out_restore_irqs;
        }
        zap_locks();
    }
 
    lockdep_off();
    spin_lock(&logbuf_lock);
    printk_cpu = this_cpu;

    if (recursion_bug) {
        recursion_bug = 0;
        strcpy(printk_buf, recursion_bug_msg);
        printed_len = strlen(recursion_bug_msg);
    }
    printed_len += vscnprintf(printk_buf + printed_len,sizeof(printk_buf) - printed_len, fmt, args);
    p = printk_buf;

    if (p[0] == '<') {//处理打印级别字段
        unsigned char c = p[1];
        if (c && p[2] == '>') {
            switch (c) {
            case '0' ... '7': /* loglevel */
                current_log_level = c - '0';
            case 'd': /* KERN_DEFAULT */
                if (!new_text_line) {
                    emit_log_char('\n');
                    new_text_line = 1;
                }
            case 'c': /* KERN_CONT */
                p += 3;
                break;
            }
        }
    }
    for ( ; *p; p++) {
        if (new_text_line) {
            /* Always output the token */
            emit_log_char('<');
            emit_log_char(current_log_level + '0');
            emit_log_char('>');
            printed_len += 3;
            new_text_line = 0;

            if (printk_time) {      //打印时间信息
                /* Follow the token with the time */
                char tbuf[50], *tp;
                unsigned tlen;
                unsigned long long t;
                unsigned long nanosec_rem;

                t = cpu_clock(printk_cpu);
                nanosec_rem = do_div(t, 1000000000);
                tlen = sprintf(tbuf, "[%5lu.%06lu] ",(unsigned long) t,nanosec_rem / 1000);

                for (tp = tbuf; tp < tbuf + tlen; tp++)
                    emit_log_char(*tp);
                printed_len += tlen;

            }


            if (!*p)
                break;
        }

 

        emit_log_char(*p);
        if (*p == '\n')
            new_text_line = 1;
    }

    if (acquire_console_semaphore_for_printk(this_cpu))
        release_console_sem();
 
	...

}

//接着调用release_console_sem函数
void release_console_sem(void)
{
    ...
    console_may_schedule = 0;
    for ( ; ; ) {
        spin_lock_irqsave(&logbuf_lock, flags);
        wake_klogd |= log_start - log_end;
        if (con_start == log_end)
            break;          /* Nothing to print */

        _con_start = con_start;
        _log_end = log_end;
        con_start = log_end;        /* Flush */
        spin_unlock(&logbuf_lock);
        stop_critical_timings();    /* don't trace print latency */
        call_console_drivers(_con_start, _log_end);
        ...

    }
	...
}
EXPORT_SYMBOL(release_console_sem);

//调用call_console_drivers函数
static void call_console_drivers(unsigned start, unsigned end)
{
    ...
    _call_console_drivers(start_print, end, msg_level);
}_call_console_drivers函数

 
//调用console的写方法
static void __call_console_drivers(unsigned start, unsigned end) 

{ 
    struct console *con; 
    for_each_console(con) {//遍历console_drivers数组 #define for_each_console(con) for (con = console_drivers; con != NULL; con = con->next)  
        if ((con->flags & CON_ENABLED) && con->write &&(cpu_online(smp_processor_id()) ||(con->flags & CON_ANYTIME))) 
            con->write(con, &LOG_BUF(start), end - start);   //调用console的写方法  
    } 
} 

 

//由于已经注册的终端是serial_comip_console,这个终端的写方法是调用serial_comip_console_write()函数--->serial_comip_console_write()--->serial_out()
//--->serial_out()最终打印在串口终端上
/*
static struct console serial_comip_console = {
	.name = "ttyS",
	.write = serial_comip_console_write,
	.device = uart_console_device,
	.setup = serial_comip_console_setup,
	.flags = CON_PRINTBUFFER,
	.index = -1,
	.data = &serial_comip_reg,
};
    */

console_drivers链表在register_console中会设置

Linux 修改串口控制台

修改earlyprintk:

在这里插入图片描述

修改 printk:

在这里插入图片描述

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

与光同程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值