高通平台smd分析及smem共享内存的创建笔记

高通平台smd分析及smem共享内存的创建笔记

tags : linux driver


最近研究高通平台的音频相关,由于实在受不了没头没尾的数据,所以就顺便研究了一下smd以及smem,这篇博客就是一个笔记,比较杂乱,主要用来备忘!

网上找到一些关于smd的文章,但是基本上都仅限于smd,只讲述了smd的channel相关,但是channel的内存到底哪里来的,smem到底是怎么回事好像都没提……专门写smem的文章里面好像对smem的描述也仅限于高通官方的一个txt,很多细节也没说清楚,尤其是channel与smem怎么对应上的,以及channel所使用smem到底哪里创建出来的,只字未提,所以就抽时间研究了一下,这里做下笔记,所以是想到哪写到哪的,可能也只有我自己看得懂……

这里重点更新一下关于smem的一些研究

smem里面的id就是一个item的编号,也是这个item的type,一个to_proc就是一个partition,to_proc就是partition的编号
关于smem怎么跟其他设备交互的,以audio相关的dsp为例,dsp在820平台上其设备号被定义为2

enum {
    SMD_APPS = SMEM_APPS,
    SMD_MODEM = SMEM_MODEM,
    SMD_Q6 = SMEM_Q6,//这个就是处理音频的dsp
    SMD_DSPS = SMEM_DSPS,
    SMD_TZ = SMEM_DSPS,
    SMD_WCNSS = SMEM_WCNSS,
    SMD_MODEM_Q6_FW = SMEM_MODEM_Q6_FW,
    SMD_RPM = SMEM_RPM,
    NUM_SMD_SUBSYSTEMS,
};

对于smem的使用,根据高通的说明文档,有security和普通模式,这里使用的是security模式。

  • smem的使用方式,应该跟双口ram是一个道理,两边都可以向同一个内存中写,写完后通过一个中断通知对端cpu来取数据,同时写的时候应该会有一个指定的帧格式,让对方知道这次写的是什么消息
  • 每个partition用来指定一对处理器交互空间,也就是指定这一段内存是给哪两个处理器使用,由平台设计者事先约定好,通过某一个处理器在linux系统启动前已经把partition的toc写在了dts配置的smem对应的位置了,至于这个toc是谁写的?我觉得其实哪个处理器都可以来实现这个操作,cpu可以做,dsp可以做,modem也可以做,因为这仅仅是个共享内存块,只要大家约定好一个人来写就行。从其他地方的资料来看好像是cpu在bootloader里面做的这件事,但是这个究竟是不是,无法验证,但是原理上就是这样,谁来写都行!!
  • 每个cup启动后,可以根据toc里面的规定,找到自己跟其他处理器通信到底该使用哪个partition,同样,每个cpu可以都可以向partition里面建立item,所以!!!不要天经地义的以为所有的item都该cpu建立,这里我之前就一直没转过弯来,总觉得所有partition都该cpu来创建,结果发现关于smd中channel使用的item SMEM_SMD_BASE_ID + ch_n,以及SMEM_CHANNEL_ALLOC_TBL等等编号的item,在linux中无论怎么跟踪,怎么加打印从任何地方都找不到其在哪里创建的,但是当smem的第一个中断到来,再去读dsp对应的partition时,发现该partition的header内容已经被修改了!!!在linux中完全跟踪不到!!所以,这里猜测,当dps启动后会去读toc相关信息,然后根据相关信息,创建其需要创建的item。这样的猜测我觉得是唯一能解释目前实验现象的,同时在系统设计上也是合理的!!比如说:如果channel的创建由外部处理器来创建,则cpu可以容易的知道当前平台上到底挂接了哪些可用的外部处理器。
  • 现在通过实验又多了一个佐证,也就是在dsp第一个中断来之前,读partition2的SMEM_CHANNEL_ALLOC_TBL类型item时,始终读不到,一旦第一个中中断到来,则读到了,所以可以从侧面证明在linux中找不到的item都是被其他处理器创建的,只不过目前没有根本的证据能证明这一点。高通,你敢把dsp的代码开源么?!!

言归正传,来研究smd

smd应该是高通共享内存设备,对下,通过操作smem来实现共享内存的物理操作,对上提供channel进行操作。

sdm.c(drivers\soc\qcom)
arch_initcall(msm_smd_init);
msm_smd_init()->msm_smd_driver_register();完成设备驱动注册

pid是个什么东西

msm_smd_init()函数中对全局数组remote_info[]进行了初始化,该初始化最主要的就是把remote_pid给赋值了,这个赋值是根据NUM_SMD_SUBSYSTEMS个枚举类型里面的值来赋值的,NUM_SMD_SUBSYSTEMS枚举类型里面的值又是NUM_SMEM_SUBSYSTEMS这个枚举类型定义的,NUM_SMEM_SUBSYSTEMS枚举类型就是smem设备的实际定义,在高通820平台开发板下,smen采用安全模式,所以会分成若干个partition,NUM_SMEM_SUBSYSTEMS枚举就跟这若干个partition是对应的,并且每个partition的host0和host1实在bootloader时就定义好了的,至于bootloader是从哪里定义的,暂时没有深究,这部分可以通过smem_init_security()(drivers\soc\qcom\smem.c)这个函数中的打印来查看,这里的打印会把smem_toc信息打印出来。

struct smem_toc结构对应的应该是sevurity模式下的toc,被记录在整个smem的最后面,有4k的空间。
struct smem_shared结构对应的应该是普通模式下TOC信息 在security模式下,Legacy/Default SMEM Partition 存储的应该是普通模式下的TOC信息。

关于smem的详细介绍可以参考高通的自说明文档msm_smem.txt,里面说明了smem的定义,非安全模式、安全模式、cache和uncache的栈生长方向等,顺便记录一下,文档中所描述的Item x Header信息,对应到代码里面应该是 smem_partition_allocation_header结构,smem_partition_header应该是每个partition的头,这里仅仅是猜测,但是从部分实验现象来看大概率应该是这样。

ps1:smd中edge的值应该是和partition的块序号是对应的,即edge 0则对应partition 0。
ps2:partition中,smem_alloc()为创建partition的具体执行函数,该函数在多个地方被调用,本平台中,创建modem和adsp的partition是在glink_smem_native_probe()(drivers\soc\qcom\glink_smem_native_xprt.c)中进行的,glink 这个东西到底做了什么这里有待进一步分析,同时有个glink pkt不知道跟这有没有关系……
这里,glink是作为SEC_ALLOC_TBL,而音频实际使用的tbl应该是PRI_ALLOC_TBL,两个tab的toc号,也就是type不同,pri的定义:base_id = SMEM_SMD_BASE_ID;fifo_id = SMEM_SMD_FIFO_BASE_ID;,SEC_ALLOC_TBL的定义:base_id = SMEM_SMD_BASE_ID_2;fifo_id = SMEM_SMD_FIFO_BASE_ID_2;

上述这些主要是记录一下pid是哪里来的,怎么来的。

初始化及相关创建函数行为分析

下面分析smd初始化的核心,注册驱动,注册的驱动是如何被调用到probe的就不记录了,那是linux驱动模型里面该掌握的。

int msm_smd_driver_register(void)
{
    int rc;

    rc = platform_driver_register(&msm_smd_driver);
    if (rc) {
        pr_err("%s: smd_driver register failed %d\n",
            __func__, rc);
        return rc;
    }

    rc = platform_driver_register(&msm_smsm_driver);
    if (rc) {
        pr_err("%s: msm_smsm_driver register failed %d\n",
            __func__, rc);
        return rc;
    }

    return 0;
}

这个函数里面其实就是注册了smd驱动以及smsm驱动,结果发现我的开发板上面好像是没有smsm设备的,所以这里是做什么的也不知道……暂时只管smd驱动。

static int msm_smd_probe(struct platform_device *pdev)
{
    uint32_t edge;
    char *key;
    int ret;
    uint32_t irq_offset;
    uint32_t irq_bitmask;
    uint32_t irq_line;
    const char *subsys_name;
    struct interrupt_config_item *private_irq;
    struct device_node *node;
    void *irq_out_base;
    resource_size_t irq_out_size;
    struct platform_device *parent_pdev;
    struct resource *r;
    struct interrupt_config *private_intr_config;
    uint32_t remote_pid;
    bool skip_pil;

    node = pdev->dev.of_node;

    if (!pdev->dev.parent) {
        pr_err("%s: missing link to parent device\n", __func__);
        return -ENODEV;
    }

    mutex_lock(&smd_probe_lock);
    if (!first_probe_done) {
        smd_reset_all_edge_subsys_name();//把bus名称清空
        first_probe_done = 1;
    }
    mutex_unlock(&smd_probe_lock);

    parent_pdev = to_platform_device(pdev->dev.parent);
//从这里开始,到后面一段都是在读取硬件资源,比如中断呀,edge值呀等等,
//硬件资源都是dts中定义的,dts的加载、device的复原等等,见另一篇笔记
    key = "irq-reg-base";
    r = platform_get_resource_byname(parent_pdev, IORESOURCE_MEM, key);
    if (!r)
        goto missing_key;
    irq_out_size = resource_size(r);
    irq_out_base = ioremap_nocache(r->start, irq_out_size);
    if (!irq_out_base) {
        pr_err("%s: ioremap_nocache() of irq_out_base addr:%pr size:%pr\n",
                __func__, &r->start, &irq_out_size);
        return -ENOMEM;
    }
    SMD_DBG("%s: %s = %p", __func__, key, irq_out_base);

    key = "qcom,smd-edge";
    ret = of_property_read_u32(node, key, &edge);
    if (ret)
        goto missing_key;
    SMD_DBG("%s: %s = %d", __func__, key, edge);

    key = "qcom,smd-irq-offset";
    ret = of_property_read_u32(node, key, &irq_offset);
    if (ret)
        goto missing_key;
    SMD_DBG("%s: %s = %x", __func__, key, irq_offset);

    key = "qcom,smd-irq-bitmask";
    ret = of_property_read_u32(node, key, &irq_bitmask);
    if (ret)
        goto missing_key;
    SMD_DBG("%s: %s = %x", __func__, key, irq_bitmask);

    key = "interrupts";
    irq_line = irq_of_parse_and_map(node, 0);
    if (!irq_line)
        goto missing_key;
    SMD_DBG("%s: %s = %d", __func__, key, irq_line);

    key = "label";
    subsys_name = of_get_property(node, key, NULL);
    SMD_DBG("%s: %s = %s", __func__, key, subsys_name);
    /*
     * Backwards compatibility.  Although label is required, some DTs may
     * still list the legacy pil-string.  Sanely handle pil-string.
     */
    if (!subsys_name) {
        pr_warn("msm_smd: Missing required property - label. Using legacy parsing\n");
        key = "qcom,pil-string";
        subsys_name = of_get_property(node, key, NULL);
        SMD_DBG("%s: %s = %s", __func__, key, subsys_name);
        if (subsys_name)
            skip_pil = false;
        else
            skip_pil = true;
    } else {
        key = "qcom,not-loadable";
        skip_pil = of_property_read_bool(node, key);
        SMD_DBG("%s: %s = %d\n", __func__, key, skip_pil);
    }
/* 差不多一直到这里,才把硬件资源读完 */

/* 这里是找到中断服务函数,这个服务函数的映射关系也是根据smem那边定义好的对应关系,
是一个根据事先约定写死了的数组,当其他处理器给smem写了东西之后,给本地ap的中断,
然后调用该函数,告诉使用该channel的应用程序smem中有数据可以用了,
中断函数里面ch->notify()就是创建通道时使用通道者注册进来的函数。*/
    private_intr_config = smd_get_intr_config(edge);
    if (!private_intr_config) {
        pr_err("%s: invalid edge\n", __func__);
        return -ENODEV;
    }
    private_irq = &private_intr_config->smd;
    private_irq->out_bit_pos = irq_bitmask;
    private_irq->out_offset = irq_offset;
    private_irq->out_base = irq_out_base;
    private_irq->irq_id = irq_line;/*这个东西怎么来的,过程太复杂了,
毕竟中断定义已经在dts中描述了,这里主要是做一些变换*/
    remote_pid = smd_edge_to_remote_pid(edge);
    interrupt_stats[remote_pid].smd_interrupt_id = irq_line;

/*看这个架势,这里估计是向系统注册一个中断……
linux中断的注册这块还没研究,等这些搞完了再来研究一下*/
    ret = request_irq(irq_line,
            private_irq->irq_handler,
/*这个东西是在private_intr_config = smd_get_intr_config(edge)
里面获取的,这里由于private_irq = &private_intr_config->smd;的存在,
所以private_irq就是private_intr_config里面的smd设备。*/
            IRQF_TRIGGER_RISING | IRQF_NO_SUSPEND | IRQF_SHARED,
            node->name,
            &pdev->dev);
    if (ret < 0) {
        pr_err("%s: request_irq() failed on %d\n", __func__, irq_line);
        return ret;
    } else {
    //这里是不是就是使能中断的意思?
        ret = enable_irq_wake(irq_line);
        if (ret < 0)
            pr_err("%s: enable_irq_wake() failed on %d\n", __func__,
                    irq_line);
    }
    //重新写bus的名称,开头一进来就给清空了的
    smd_set_edge_subsys_name(edge, subsys_name);
    //暂时不清楚意义
    smd_proc_set_skip_pil(smd_edge_to_remote_pid(edge), skip_pil);
    //设置已经初始化了的标识,后面使用smd时会先判断edge初始化标识的
    smd_set_edge_initialized(edge);
    //创建channel,下面具体展开分析。
    smd_post_init(remote_pid);
    return 0;

missing_key:
    pr_err("%s: missing key: %s", __func__, key);
    return -ENODEV;
}
void smd_post_init(unsigned remote_pid)
{
    smd_channel_probe_now(&remote_info[remote_pid]);
}

/*smd_channel_probe_now()这个函数就是在初始化和smem中断中被调用,
在初始化时被调用了没什么用,因为dsp这个时候还没起来,smem_get_entry()
直接啥都get不到。(这里面要get的item猜测极大概率是被dsp创建的,
所以cpu初始化时是找不到他所要的item的)

所以,这个函数主要是在smem中断中被调用,中断回调函数调用static void 
do_smd_probe(unsigned remote_pid)函数,
该函数完成schedule_work()的触发,而在初始化时,smd_channel_probe_worker()
函数被装入了work队列,smd_channel_probe_worker()又是smd_channel_probe_now()
的最终调用者,所以当中断smem中断到来,通过work机制最终调用此函数。
*/
static void smd_channel_probe_now(struct remote_proc_info *r_info)
{
    struct smd_alloc_elm *shared;
    unsigned tbl_size;

    shared = smem_get_entry(ID_CH_ALLOC_TBL, &tbl_size,
                            r_info->remote_pid, 0);

    if (!shared) {
        pr_err("%s: allocation table not initialized\n", __func__);
        return;
    }

    mutex_lock(&smd_probe_lock);

//扫描pri tbl的通道是否全部已经创建,若没有创建,则创建所有channel
    scan_alloc_table(shared, r_info->ch_allocated, PRI_ALLOC_TBL,
                        tbl_size / sizeof(*shared),
                        r_info);

    shared = smem_get_entry(SMEM_CHANNEL_ALLOC_TBL_2, &tbl_size,
                            r_info->remote_pid, 0);
    if (shared)
    //扫描sec tbl的通道是否全部已经创建,若没有创建,则创建所有channel,
    //但是在820平台上实际情况是这里根本没有执行,应为相应的item没有被创建
        scan_alloc_table(shared,
            &(r_info->ch_allocated[SMEM_NUM_SMD_STREAM_CHANNELS]),
            SEC_ALLOC_TBL,
            tbl_size / sizeof(*shared),
            r_info);

    mutex_unlock(&smd_probe_lock);
}
static void scan_alloc_table(struct smd_alloc_elm *shared,
                char *smd_ch_allocated,
                int table_id,
                unsigned num_entries,
                struct remote_proc_info *r_info)
{
    unsigned n;
    uint32_t type;

    for (n = 0; n < num_entries; n++) {
        if (smd_ch_allocated[n])
            continue;

        /*
         * channel should be allocated only if APPS processor is
         * involved
         */
         //这个type其实就是partition的编号
        type = SMD_CHANNEL_TYPE(shared[n].type);
        if (!pid_is_on_edge(type, SMD_APPS) ||
                !pid_is_on_edge(type, r_info->remote_pid))
            continue;
        if (!shared[n].ref_count)
            continue;
        if (!shared[n].name[0])
            continue;

        if (!smd_edge_inited(type)) {
            SMD_INFO(
                "Probe skipping proc %d, tbl %d, ch %d, edge not inited\n",
                r_info->remote_pid, table_id, n);
            continue;
        }
//实际创建通道函数
        if (!smd_alloc_channel(&shared[n], table_id, r_info))
            smd_ch_allocated[n] = 1;
        else
            SMD_INFO(
                "Probe skipping proc %d, tbl %d, ch %d, not allocated\n",
                r_info->remote_pid, table_id, n);
    }
}
static int smd_alloc_channel(struct smd_alloc_elm *alloc_elm, int table_id,
                struct remote_proc_info *r_info)
{
    struct smd_channel *ch;

    printk("gift_smem smd_alloc_channel table_id = %d \r\n", table_id);

    ch = kzalloc(sizeof(struct smd_channel), GFP_KERNEL);
    if (ch == 0) {
        pr_err("smd_alloc_channel() out of memory\n");
        return -ENOMEM;
    }
    ch->n = alloc_elm->cid;
    ch->type = SMD_CHANNEL_TYPE(alloc_elm->type);

    if (smd_alloc(ch, table_id, r_info)) {
        kfree(ch);
        return -ENODEV;
    }

    /* probe_worker guarentees ch->type will be a valid type */
    //这里的函数都是通知对端处理器的方法,也就是本地cpu写完了ram之后触发一下,
    //就可以通知对端cpu了
    if (ch->type == SMD_APPS_MODEM)
        ch->notify_other_cpu = notify_modem_smd;
    else if (ch->type == SMD_APPS_QDSP)
        ch->notify_other_cpu = notify_dsp_smd;
    else if (ch->type == SMD_APPS_DSPS)
        ch->notify_other_cpu = notify_dsps_smd;
    else if (ch->type == SMD_APPS_WCNSS)
        ch->notify_other_cpu = notify_wcnss_smd;
    else if (ch->type == SMD_APPS_Q6FW)
        ch->notify_other_cpu = notify_modemfw_smd;
    else if (ch->type == SMD_APPS_RPM)
        ch->notify_other_cpu = notify_rpm_smd;

    if (smd_is_packet(alloc_elm)) {
        ch->read = smd_packet_read;
        ch->write = smd_packet_write;
        ch->read_avail = smd_packet_read_avail;
        ch->write_avail = smd_packet_write_avail;
        ch->update_state = update_packet_state;
        ch->read_from_cb = smd_packet_read_from_cb;
        ch->is_pkt_ch = 1;
    } else {
        ch->read = smd_stream_read;
        ch->write = smd_stream_write;
        ch->read_avail = smd_stream_read_avail;
        ch->write_avail = smd_stream_write_avail;
        ch->update_state = update_stream_state;
        ch->read_from_cb = smd_stream_read;
    }

    if (is_word_access_ch(ch->type)) {
        ch->read_from_fifo = smd_memcpy32_from_fifo;
        ch->write_to_fifo = smd_memcpy32_to_fifo;
    } else {
        ch->read_from_fifo = smd_memcpy_from_fifo;
        ch->write_to_fifo = smd_memcpy_to_fifo;
    }

    smd_memcpy_from_fifo(ch->name, alloc_elm->name, SMD_MAX_CH_NAME_LEN);
    ch->name[SMD_MAX_CH_NAME_LEN-1] = 0;

    ch->pdev.name = ch->name;
    ch->pdev.id = ch->type;

    SMD_INFO("smd_alloc_channel() '%s' cid=%d\n",
         ch->name, ch->n);

/*把新创建的channel放入smd_ch_closed_list全局链表中,
这里面应该是保存所未被使用的ch的,从smd_get_channel()函数中可以佐证这一点,
还有两个跟他很像的全局链表是干嘛后面碰到了再研究*/
    mutex_lock(&smd_creation_mutex);
    list_add(&ch->ch_list, &smd_ch_closed_list);
    mutex_unlock(&smd_creation_mutex);
//把这个ch注册成一个设备,那么肯定会有ch对应的驱动的,后面再找
/*这里找到了,比如说audio,对应的ch驱动就是"apr_audio_svc",
其他的驱动也都是在别的文件里面有对应的,把ch->pdev.name打印出来,
然后搜一下就可以找到了,因为ch->pdev.name和driver的name一定是匹配的
(linux驱动模型通过名字匹配设备和驱动)*/
    platform_device_register(&ch->pdev);
    if (!strncmp(ch->name, "LOOPBACK", 8) && ch->type == SMD_APPS_MODEM) {
        /* create a platform driver to be used by smd_tty driver
         * so that it can access the loopback port
         */
        loopback_tty_pdev.id = ch->type;
        platform_device_register(&loopback_tty_pdev);
    }
    return 0;
}
static int smd_alloc(struct smd_channel *ch, int table_id,
                        struct remote_proc_info *r_info)
{
    void *buffer;
    unsigned buffer_sz;
    unsigned base_id;
    unsigned fifo_id;

    printk("gift_smem %s ch=%d\n", __func__, ch->n);

    switch (table_id) {
    case PRI_ALLOC_TBL:
        base_id = SMEM_SMD_BASE_ID;
        fifo_id = SMEM_SMD_FIFO_BASE_ID;
        break;
    case SEC_ALLOC_TBL:
        base_id = SMEM_SMD_BASE_ID_2;
        fifo_id = SMEM_SMD_FIFO_BASE_ID_2;
        break;
    default:
        SMD_INFO("Invalid table_id:%d passed to smd_alloc\n", table_id);
        return -EINVAL;
    }

    if (is_word_access_ch(ch->type)) {
        struct smd_shared_word_access *shared2;
        //根据item的id和partition的id来查找其对应的内存入口地址
        shared2 = smem_find(base_id + ch->n, sizeof(*shared2),
                            r_info->remote_pid, 0);
        if (!shared2) {
            printk("gift_smem word find failed ch=%d\n", ch->n);
            SMD_INFO("smem_find failed ch=%d\n", ch->n);
            return -EINVAL;
        }
        //如果找到了就把接收缓冲区和发送缓冲区的内存地址保存下来
        //这里应该只是channel的信息,并不是实际的数据内存,
        //可能只是传输一些此次对fifo操作的控制信息
        ch->send = &shared2->ch0;
        ch->recv = &shared2->ch1;
    } else {
        struct smd_shared *shared2;
        shared2 = smem_find(base_id + ch->n, sizeof(*shared2),
                            r_info->remote_pid, 0);
        if (!shared2) {
            printk("gift_smem find failed ch=%d\n", ch->n);
            SMD_INFO("smem_find failed ch=%d\n", ch->n);
            return -EINVAL;
        }
        ch->send = &shared2->ch0;
        ch->recv = &shared2->ch1;
    }
    /*这里是把通道的实际操作方法保存到half_ch里面,其实这里感觉更像是一个内存映射,
到时候需要从内存中取什么数据的时候,估计就是用类似ch->half_ch->xxx(ch->send);
的方式。具体这个结构体static struct smd_half_channel_access这样定义的原因,
应该就是各cpu之间的通信格式约定了。至于这个通信格式具体定义,高通也没说呀,
要我去哪里找?只能猜么?*/
    ch->half_ch = get_half_ch_funcs(ch->type);

//这里就是取实际的数据内存地址,估计就是用来保存需要交互的实际数据的
    buffer = smem_get_entry(fifo_id + ch->n, &buffer_sz,
                            r_info->remote_pid, 0);
    if (!buffer) {
        SMD_INFO("smem_get_entry failed\n");
        return -EINVAL;
    }

    /* buffer must be a multiple of 32 size */
    if ((buffer_sz & (SZ_32 - 1)) != 0) {
        SMD_INFO("Buffer size: %u not multiple of 32\n", buffer_sz);
        return -EINVAL;
    }
    buffer_sz /= 2;
    ch->send_data = buffer;
    /*这一句话写的很风骚啊,他确信内存的连续性,以及协议的正确性,所以直接这样操作,
    如果ch->send_data后面那一坨内存可以被其他地方先占用,那这里就不行了……*/
    ch->recv_data = buffer + buffer_sz;
    ch->fifo_size = buffer_sz;

    return 0;
}
  • 2
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值