LVGL源码(2):LVGL的lv_task_handler()函数工作逻辑

    LVGL版本:8.3

    通过对LVGL的学习,我们知道在移植完LVGL之后,我们在对LVGL进行lvgl初始化lv_init(); 显示器初始化lv_port_disp_init();和输入设备初始化lv_port_indev_init();  后,每ms调用一次lv_tick_inc(1)给LVGL一个时钟后,再每隔一段时间(一般为5ms)调用一次lv_task_handler()函数即可将LVGL成功部署到我们的设备上,从而实现检测用户输入操作、调用我们编写的逻辑、在屏幕上显示对应的画面等一系列功能;那么我们可以知道整个LVGL工作的核心其实就在lv_task_handler()这个函数当中,因此本文就记录一下我个人对lv_task_handler()工作逻辑的看法,结合一下keypad输入设备来进行讲解,也算填一下上一篇文章中的坑LVGL Keypad键值介绍以及禁止长按连续触发方法-CSDN博客。最后欢迎大家一起交流,如有纰漏,望各位纠正。

首先我们来分析函数lv_task_handler()函数:

lv_api_map.h:



static inline LV_ATTRIBUTE_TIMER_HANDLER uint32_t lv_task_handler(void)
{
    return lv_timer_handler();
}



lv_timer.c:


/**
 * Call it periodically to handle lv_timers.
 * @return the time after which it must be called again
 */
uint32_t LV_ATTRIBUTE_TIMER_HANDLER lv_timer_handler(void)
{
    TIMER_TRACE("begin");
    /*判断本函数运行条件,如果本函数未再运行即already_running为false(防止本函数被并发调用)
    且LVGL使能标志位lv_timer_run为ture就往下执行(定时器系统被激活)
    */
    /*Avoid concurrent running of the timer handler*/
    static bool already_running = false;
    if(already_running) {
        TIMER_TRACE("already running, concurrent calls are not allow, returning");
        return 1;
    }
    already_running = true;

    if(lv_timer_run == false) {    
        already_running = false; /*Release mutex*/
        return 1;
    }

    static uint32_t idle_period_start = 0;
    static uint32_t busy_time         = 0;

    uint32_t handler_start = lv_tick_get();  //开始记录本函数开始时间

    if(handler_start == 0) {           //检测是否正确使用了lv_tick_inc()回调
        static uint32_t run_cnt = 0;
        run_cnt++;
        if(run_cnt > 100) {
            run_cnt = 0;
            LV_LOG_WARN("It seems lv_tick_inc() is not called.");
        }
    }

    /*Run all timer from the list*/
    /*本函数正式开始执行,循环判断_lv_timer_ll列表的所有定时器,如果剩余时间为0就运行,
    在这个过程中发生定时器创建或删除事件就提前退出循环,下次进入本函数再重新循环判断,保证数据完整性
    */
    lv_timer_t * next;               
    do { 
        timer_deleted             = false;
        timer_created             = false;
        LV_GC_ROOT(_lv_timer_act) = _lv_ll_get_head(&LV_GC_ROOT(_lv_timer_ll));
        while(LV_GC_ROOT(_lv_timer_act)) {
            /*The timer might be deleted if it runs only once ('repeat_count = 1')
             *So get next element until the current is surely valid*/
            next = _lv_ll_get_next(&LV_GC_ROOT(_lv_timer_ll), LV_GC_ROOT(_lv_timer_act));

            if(lv_timer_exec(LV_GC_ROOT(_lv_timer_act))) {
                /*If a timer was created or deleted then this or the next item might be corrupted*/
                if(timer_created || timer_deleted) {
                    TIMER_TRACE("Start from the first timer again because a timer was created or deleted");
                    break;
                }
            }

            LV_GC_ROOT(_lv_timer_act) = next; /*Load the next timer*/
        }
    } while(LV_GC_ROOT(_lv_timer_act));


    /*遍历_lv_timer_ll列表的所有定时器剩余时间,将time_till_next设置为
    所有定时器中最小的剩余时间
    */
    uint32_t time_till_next = LV_NO_TIMER_READY;   
    next = _lv_ll_get_head(&LV_GC_ROOT(_lv_timer_ll));
    while(next) {
        if(!next->paused) {
            uint32_t delay = lv_timer_time_remaining(next);
            if(delay < time_till_next)
                time_till_next = delay;
        }

        next = _lv_ll_get_next(&LV_GC_ROOT(_lv_timer_ll), next); /*Find the next timer*/
    }

    /*记录本函数的总共运行时间busy_time和本程序的总共运行时间idle_period_time,
    每500ms算一次空闲时间占本程序的总共运行时间百分比idle_last,算完之后重置busy_time和
    idle_period_time
    */
    busy_time += lv_tick_elaps(handler_start);   //
    uint32_t idle_period_time = lv_tick_elaps(idle_period_start);  
    if(idle_period_time >= IDLE_MEAS_PERIOD) {
        idle_last         = (busy_time * 100) / idle_period_time;  /*Calculate the busy percentage*/
        idle_last         = idle_last > 100 ? 0 : 100 - idle_last; /*But we need idle time*/
        busy_time         = 0;
        idle_period_start = lv_tick_get();
    }

    already_running = false; /*Release the mutex*/

    TIMER_TRACE("finished (%d ms until the next timer call)", time_till_next);
    return time_till_next;
}

    从上述代码中我们可以知道,lv_task_handler()函数首先调用了一个内联函数执行了lv_timer_handler()并返回了该函数的结果,我们看到lv_timer_handler()函数中,真正的核心执行部分其实是下面这段代码:

uint32_t LV_ATTRIBUTE_TIMER_HANDLER lv_timer_handler(void)
{
    ......
    /*Run all timer from the list*/
    /*本函数正式开始执行,循环判断_lv_timer_ll列表的所有定时器,如果剩余时间为0就运行,
    在这个过程中发生定时器创建或删除事件就提前退出循环,下次进入本函数再重新循环判断,保证数据完整性
    */
    lv_timer_t * next;               
    do { 
        timer_deleted             = false;
        timer_created             = false;
        LV_GC_ROOT(_lv_timer_act) = _lv_ll_get_head(&LV_GC_ROOT(_lv_timer_ll));
        while(LV_GC_ROOT(_lv_timer_act)) {
            /*The timer might be deleted if it runs only once ('repeat_count = 1')
             *So get next element until the current is surely valid*/
            next = _lv_ll_get_next(&LV_GC_ROOT(_lv_timer_ll), LV_GC_ROOT(_lv_timer_act));

            if(lv_timer_exec(LV_GC_ROOT(_lv_timer_act))) {
                /*If a timer was created or deleted then this or the next item might be corrupted*/
                if(timer_created || timer_deleted) {
                    TIMER_TRACE("Start from the first timer again because a timer was created or deleted");
                    break;
                }
            }

            LV_GC_ROOT(_lv_timer_act) = next; /*Load the next timer*/
        }
    } while(LV_GC_ROOT(_lv_timer_act));
   ......
}

      这段代码的主要功能就是遍历_lv_timer_ll链表的所有定时器链表节点,如果节点存在就调用lv_timer_exec(lv_timer_t * timer)函数判断该定时器剩余时间,剩余时间为0就执行和该定时器绑定的回调函数。在遍历_lv_timer_ll链表期间如果发生定时器创建或删除事件就提前退出循环,下次进入本函数再重新遍历_lv_timer_ll链表,保证数据完整性。

    那我们就需要知道_lv_timer_ll链表中都有些啥,这里我们首先提出假设:关于输入设备的定时器就在该链表中,然后设备定时器回调函数lv_indev_read_timer_cb()由此得到执行,从而实现了对例如keypad输入设备的实时输入监测;但是现在出现了一个问题,我没有找到lv_timer_ll链表的定义在哪个文件里,全局搜索lv_timer_ll只在lv_gc.h头文件中发现有关于这个变量的描述,但是没看到这个变量的定义,lv_gc.c和lv_gc.h的主要代码如下:

lv_gc.c:


#if(!defined(LV_ENABLE_GC)) || LV_ENABLE_GC == 0
    LV_ROOTS
#endif /*LV_ENABLE_GC*/


void _lv_gc_clear_roots(void)
{
#define LV_CLEAR_ROOT(root_type, root_name) lv_memset_00(&LV_GC_ROOT(root_name), sizeof(LV_GC_ROOT(root_name)));
    LV_ITERATE_ROOTS(LV_CLEAR_ROOT)
}


lv_gc.h:

/*********************
 *      DEFINES
 *********************/
#if LV_IMG_CACHE_DEF_SIZE
#    define LV_IMG_CACHE_DEF            1
#else
#    define LV_IMG_CACHE_DEF            0
#endif

#define LV_DISPATCH(f, t, n)            f(t, n)
#define LV_DISPATCH_COND(f, t, n, m, v) LV_CONCAT3(LV_DISPATCH, m, v)(f, t, n)

#define LV_DISPATCH00(f, t, n)          LV_DISPATCH(f, t, n)
#define LV_DISPATCH01(f, t, n)
#define LV_DISPATCH10(f, t, n)
#define LV_DISPATCH11(f, t, n)          LV_DISPATCH(f, t, n)

#define LV_ITERATE_ROOTS(f)                                                                            \
    LV_DISPATCH(f, lv_ll_t, _lv_timer_ll) /*Linked list to store the lv_timers*/                       \
    LV_DISPATCH(f, lv_ll_t, _lv_disp_ll)  /*Linked list of display device*/                            \
    LV_DISPATCH(f, lv_ll_t, _lv_indev_ll) /*Linked list of input device*/                              \
    LV_DISPATCH(f, lv_ll_t, _lv_fsdrv_ll)                                                              \
    LV_DISPATCH(f, lv_ll_t, _lv_anim_ll)                                                               \
    LV_DISPATCH(f, lv_ll_t, _lv_group_ll)                                                              \
    LV_DISPATCH(f, lv_ll_t, _lv_img_decoder_ll)                                                        \
    LV_DISPATCH(f, lv_ll_t, _lv_obj_style_trans_ll)                                                    \
    LV_DISPATCH(f, lv_layout_dsc_t *, _lv_layout_list)                                                 \
    LV_DISPATCH_COND(f, _lv_img_cache_entry_t*, _lv_img_cache_array, LV_IMG_CACHE_DEF, 1)              \
    LV_DISPATCH_COND(f, _lv_img_cache_entry_t, _lv_img_cache_single, LV_IMG_CACHE_DEF, 0)              \
    LV_DISPATCH(f, lv_timer_t*, _lv_timer_act)                                                         \
    LV_DISPATCH(f, lv_mem_buf_arr_t , lv_mem_buf)                                                      \
    LV_DISPATCH_COND(f, _lv_draw_mask_radius_circle_dsc_arr_t , _lv_circle_cache, LV_DRAW_COMPLEX, 1)  \
    LV_DISPATCH_COND(f, _lv_draw_mask_saved_arr_t , _lv_draw_mask_list, LV_DRAW_COMPLEX, 1)            \
    LV_DISPATCH(f, void * , _lv_theme_default_styles)                                                  \
    LV_DISPATCH(f, void * , _lv_theme_basic_styles)                                                  \
    LV_DISPATCH_COND(f, uint8_t *, _lv_font_decompr_buf, LV_USE_FONT_COMPRESSED, 1)                    \
    LV_DISPATCH(f, uint8_t * , _lv_grad_cache_mem)                                                     \
    LV_DISPATCH(f, uint8_t * , _lv_style_custom_prop_flag_lookup_table)

#define LV_DEFINE_ROOT(root_type, root_name) root_type root_name;
#define LV_ROOTS LV_ITERATE_ROOTS(LV_DEFINE_ROOT)

#if LV_ENABLE_GC == 1
#if LV_MEM_CUSTOM != 1
#error "GC requires CUSTOM_MEM"
#endif /*LV_MEM_CUSTOM*/
#include LV_GC_INCLUDE
#else  /*LV_ENABLE_GC*/
#define LV_GC_ROOT(x) x
#define LV_EXTERN_ROOT(root_type, root_name) extern root_type root_name;
LV_ITERATE_ROOTS(LV_EXTERN_ROOT)
#endif /*LV_ENABLE_GC*/

        而奥秘就藏在lv_gc.h头文件中的宏定义中,首先LV_ENABLE_GC这个宏定义是默认为0的,关于这个宏定义的描述如下:它是一个与 LVGL 垃圾回收机制相关的宏定义,设置为 0 时,LVGL 不会启用垃圾回收。如果 LV_ENABLE_GC 被启用,代码中需要通过 #include LV_GC_INCLUDE 引入与垃圾回收相关的头文件。LVGL 提供与一些脚本语言(如 Micropython)的绑定接口。在这些场景中,启用垃圾回收是必须的,因为这些语言的运行时需要垃圾回收机制来管理对象的生命周期。

        而当LV_ENABLE_GC = 0时,我们首先看lv_gc.c文件中就会调用LV_ROOTS这个宏定义,而LV_ROOTS宏定义相当于LV_ITERATE_ROOTS(LV_DEFINE_ROOT),那么我们就能推出如下关系链:

#define LV_DISPATCH(f, t, n)            f(t, n)


#define LV_ITERATE_ROOTS(f)                                                                            \
    LV_DISPATCH(f, lv_ll_t, _lv_timer_ll) /*Linked list to store the lv_timers*/                       
......

#define LV_DEFINE_ROOT(root_type, root_name) root_type root_name;

#define LV_ROOTS LV_ITERATE_ROOTS(LV_DEFINE_ROOT)

LV_ROOTS = LV_ITERATE_ROOTS(LV_DEFINE_ROOT) 
         = LV_DISPATCH(LV_DEFINE_ROOT, lv_ll_t, _lv_timer_ll)......
         = LV_DEFINE_ROOT(lv_ll_t, _lv_timer_ll)......
         = lv_ll_t _lv_timer_ll;......

    即LV_ROOTS就是定义各种全局变量,包括上述代码中的_lv_timer_ll链表和_lv_disp_ll链表、_lv_indev_ll链表等等;然后lv_gc.h头文件中还调用了LV_ITERATE_ROOTS(LV_EXTERN_ROOT)这个宏定义,根据上面这个关系链我们很容易就能知道LV_ITERATE_ROOTS(LV_EXTERN_ROOT)是extern在lv_gc.c中定义的各种全局变量。

    现在我们知道了_lv_timer_ll链表的定义和声明了,由于LVGL的链表定义后都需要初始化,关于LVGL链表的形式可以参考这篇文章LVGL源码分析(1):lv_ll链表的实现-CSDN博客。我们继续看该链表的初始化,在lvgl初始化函数lv_init();  中就有关于该链表的初始化,这里放出部分代码:

lv_obj.c:
void lv_init(void)
{
   ...

    _lv_timer_core_init();

    _lv_fs_init();

    _lv_anim_core_init();

    _lv_group_init();

   ...

    _lv_ll_init(&LV_GC_ROOT(_lv_disp_ll), sizeof(lv_disp_t));
    _lv_ll_init(&LV_GC_ROOT(_lv_indev_ll), sizeof(lv_indev_t));

   ...
}


lv_timer.c:
void _lv_timer_core_init(void)
{
    _lv_ll_init(&LV_GC_ROOT(_lv_timer_ll), sizeof(lv_timer_t));

    /*Initially enable the lv_timer handling*/
    lv_timer_enable(true);
}


lv_group.c:
void _lv_group_init(void)
{
    _lv_ll_init(&LV_GC_ROOT(_lv_group_ll), sizeof(lv_group_t));
}

     可以看到这里有初始化在lv_gc.c中定义的一些链表,其实我们现在已经大概能知道lv_task_handler()工作逻辑了,就是定义并初始化_lv_timer_ll这个链表,然后把LVGL要实现的功能写入定时器的回调函数中,然后创建定时器并加入到_lv_timer_ll链表,在lv_task_handler()遍历判断是否要调用该定时器对应的回调函数,我们只需要每隔一段时间调用一次lv_task_handler()函数即可。那么接下来我结合keypad输入设备来实际讲解一下,这样理解更清晰。

/*********************结合keypad输入设备讲解lv_task_handler()函数***************************/

    首先我们从另外两个LVGL初始化函数中的输入设备初始化函数lv_port_indev_init(); 开始讲起,

该函数的主要代码如下:

lv_port_indev.c:
void lv_port_indev_init(void)
{
	  static lv_indev_drv_t indev_keypad_drv;   //每一个输入设备需要有独立的输入设备驱动结构体

    /*------------------
     * Keypad
     * -----------------*/

    /*Initialize your keypad or keyboard if you have*/
    keypad_init();

    /*Register a keypad input device*/
    lv_indev_drv_init(&indev_keypad_drv);  //初始化输入设备结构体
    indev_keypad_drv.type = LV_INDEV_TYPE_KEYPAD;
    indev_keypad_drv.read_cb = keypad_read;

    indev_keypad_drv.long_press_time = 10;   //自定义
    indev_keypad_drv.long_press_repeat_time = 600;
    indev_keypad = lv_indev_drv_register(&indev_keypad_drv);

}


lv_hal_indev.c:
/**
 * Register an initialized input device driver.
 * @param driver pointer to an initialized 'lv_indev_drv_t' variable.
 * Only pointer is saved, so the driver should be static or dynamically allocated.
 * @return pointer to the new input device or NULL on error
 */
lv_indev_t * lv_indev_drv_register(lv_indev_drv_t * driver)
{

    if(driver->disp == NULL) driver->disp = lv_disp_get_default();

    if(driver->disp == NULL) {
        LV_LOG_WARN("lv_indev_drv_register: no display registered hence can't attach the indev to "
                    "a display");
        return NULL;
    }

    lv_indev_t * indev = _lv_ll_ins_head(&LV_GC_ROOT(_lv_indev_ll));
    if(!indev) {
        LV_ASSERT_MALLOC(indev);
        return NULL;
    }

    lv_memset_00(indev, sizeof(lv_indev_t));
    indev->driver = driver;

    indev->proc.reset_query  = 1;
    indev->driver->read_timer = lv_timer_create(lv_indev_read_timer_cb, LV_INDEV_DEF_READ_PERIOD, indev);

    return indev;
}


lv_hal_indev.h:
/** Initialized by the user and registered by 'lv_indev_add()'*/
typedef struct _lv_indev_drv_t {

    /**< Input device type*/
    lv_indev_type_t type;

    /**< Function pointer to read input device data.*/
    void (*read_cb)(struct _lv_indev_drv_t * indev_drv, lv_indev_data_t * data);

    /** Called when an action happened on the input device.
     * The second parameter is the event from `lv_event_t`*/
    void (*feedback_cb)(struct _lv_indev_drv_t *, uint8_t);

#if LV_USE_USER_DATA
    void * user_data;
#endif

    /**< Pointer to the assigned display*/
    struct _lv_disp_t * disp;

    /**< Timer to periodically read the input device*/
    lv_timer_t * read_timer;

    /**< Number of pixels to slide before actually drag the object*/
    uint8_t scroll_limit;

    /**< Drag throw slow-down in [%]. Greater value means faster slow-down*/
    uint8_t scroll_throw;

    /**< At least this difference should be between two points to evaluate as gesture*/
    uint8_t gesture_min_velocity;

    /**< At least this difference should be to send a gesture*/
    uint8_t gesture_limit;

    /**< Long press time in milliseconds*/
    uint16_t long_press_time;

    /**< Repeated trigger period in long press [ms]*/
    uint16_t long_press_repeat_time;
} lv_indev_drv_t;

/** The main input device descriptor with driver, runtime data ('proc') and some additional
 * information*/
typedef struct _lv_indev_t {
    struct _lv_indev_drv_t * driver;
    _lv_indev_proc_t proc;
    struct _lv_obj_t * cursor;     /**< Cursor for LV_INPUT_TYPE_POINTER*/
    struct _lv_group_t * group;    /**< Keypad destination group*/
    const lv_point_t * btn_points; /**< Array points assigned to the button ()screen will be pressed
                                      here by the buttons*/
} lv_indev_t;

   首先我们初始化了我们定义的输入设备lv_indev_drv_t indev_keypad_drv;然后调用lv_indev_drv_register(&indev_keypad_drv);函数将我们的输入设备进行注册;我们看到lv_indev_drv_register()函数的内部实现逻辑为:先判断该设备是否绑定了显示屏lv_disp_t * disp,如果没有的话就把默认显示屏lv_disp_t * disp_def和该输入设备进行绑定,如果默认显示屏存在的话就继续往下执行。后面就是在_lv_indev_ll链表头部新插入一个节点代表该输入设备,该节点通过数据区lv_indev_t绑定了新设备的驱动_lv_indev_drv_t,并通过_lv_indev_drv_t绑定了一个定时器,该定时器回调函数为lv_indev_read_timer_cb(),定时时间为LV_INDEV_DEF_READ_PERIOD,该宏定义在lv_conf.h中,表示轮询输入设备外部输入的间隔时间。由于定时器创建函数lv_timer_create()都会把创建的定时器加入到_lv_timer_ll链表中,因此前面提出的假设:输入设备的定时器就在_lv_timer_ll链表中,然后输入设备定时器回调函数lv_indev_read_timer_cb()由此得到执行,从而实现了对例如keypad等输入设备的实时输入监测;由此得到证明。我们从上面对输入设备注册的描述中也能知道每一个输入设备都会在_lv_indev_ll有一个节点,该节点绑定一个独立的定时器并通过定时器绑定一个lv_indev_read_timer_cb回调函数,因此每个输入设备的输入检测都是独立的。

lv_indev.c:
void lv_indev_read_timer_cb(lv_timer_t * timer)
{
    INDEV_TRACE("begin");
 
    lv_indev_data_t data;
 
    indev_act = timer->user_data;
 
    /*Read and process all indevs*/
    if(indev_act->driver->disp == NULL) return; /*Not assigned to any displays*/
 
    /*Handle reset query before processing the point*/
    indev_proc_reset_query_handler(indev_act);
 
    if(indev_act->proc.disabled ||
       indev_act->driver->disp->prev_scr != NULL) return; /*Input disabled or screen animation active*/
    bool continue_reading;
    do {
        /*Read the data*/
        _lv_indev_read(indev_act, &data);
        continue_reading = data.continue_reading;
 
        /*The active object might be deleted even in the read function*/
        indev_proc_reset_query_handler(indev_act);
        indev_obj_act = NULL;
 
        indev_act->proc.state = data.state;
 
        /*Save the last activity time*/
        if(indev_act->proc.state == LV_INDEV_STATE_PRESSED) {
            indev_act->driver->disp->last_activity_time = lv_tick_get();
        }
        else if(indev_act->driver->type == LV_INDEV_TYPE_ENCODER && data.enc_diff) {
            indev_act->driver->disp->last_activity_time = lv_tick_get();
        }
 
        if(indev_act->driver->type == LV_INDEV_TYPE_POINTER) {
            indev_pointer_proc(indev_act, &data);
        }
        else if(indev_act->driver->type == LV_INDEV_TYPE_KEYPAD) {
            indev_keypad_proc(indev_act, &data);
        }
        else if(indev_act->driver->type == LV_INDEV_TYPE_ENCODER) {
            indev_encoder_proc(indev_act, &data);
        }
        else if(indev_act->driver->type == LV_INDEV_TYPE_BUTTON) {
            indev_button_proc(indev_act, &data);
        }
        /*Handle reset query if it happened in during processing*/
        indev_proc_reset_query_handler(indev_act);
    } while(continue_reading);
 
    /*End of indev processing, so no act indev*/
    indev_act     = NULL;
    indev_obj_act = NULL;
 
    INDEV_TRACE("finished");
}

     lv_indev_read_timer_cb回调函数执行逻辑主要为:先获取和本定时器绑定的输入设备indev_act = timer->user_data;通过_lv_indev_read(indev_act, &data);调用绑定输入设备的输入检测函数,这个输入监测函数就是我们在lv_port_indev.c中自行定义的;然后根据输入设备的设备类型去执行对应设备的处理函数,这里由于我们是keypad设备,就执行 indev_keypad_proc(indev_act, &data);

      结合keypad输入设备后我们对lv_task_handler()函数的工作逻辑应该可以说理解的更为深刻了,大致逻辑是:输入设备绑定_lv_indev_ll链表节点,_lv_indev_ll链表节点绑定定时器,定时器绑定回调函数和_lv_timer_ll链表节点,lv_task_handler()循环调用每一个定时器的回调函数,然后根据剩余时间是否为0决定是否要调用,由此实现了实时监测不同输入设备的输入。举一反三,我们是不是可以认为LVGL其他功能例如显示屏刷新也是类似输入设备一样的逻辑,通过定时器回调然后由lv_task_handler()进行控制刷新呢?下一篇文章我们就来大概讲一下关于显示屏刷新的逻辑;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值