audio HAL与kernel联动过程中几个关键的函数分析

75 篇文章 7 订阅
65 篇文章 31 订阅

audio HAL与kernel联动过程中几个关键的函数分析

2017年04月19日 23:38:49 Winston_Jory 阅读数:5105

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weijory/article/details/62422869

前言:

这篇文档主要是对audio HAL与kernel联动过程中几个关键的函数进行分析和总结:

1.select_device, 
2.enable_snd_device, 
3.enable_audio_route,
4.pcm_open,
5.pcm_write


播放音乐的时候AudioFlinger往hal层写数据时会调用到Hal层的out_write函数,我们以out_write函数为入口,层层分析上面所列的几个关键函数:

 
  1. static ssize_t out_write(struct audio_stream_out *stream, const void *buffer,size_t bytes)

  2. {

  3. ...

  4. if (out->standby) 

 
  1. {//第一次调用out_write时需要打开pcm stream设备

  2. out->standby = false;

  3. ...

  4. ret = start_output_stream(out);//打开pcm stream设备

  5. ...

  6. }

  7. ...

  8. ret = pcm_write(out->pcm, (void *)buffer, bytes);//调用alsalib函数,写入音频数据

  9. ...

  10. return bytes;//返回写入数据的字节数

  11. }

  12.  




在打开pcm stream设备的时候会调用到select_device和pcm_open函数

 
  1. int start_output_stream(struct stream_out *out)

  2. {

  3. ...

  4. out->pcm_device_id = platform_get_pcm_device_id(out->usecase, PCM_PLAYBACK);

  5. //根据out->usecase找到pcm逻辑设备编号

  6. ...

  7. select_devices(adev, out->usecase);//打开设备通路

  8. ...

  9. if (out->usecase != USECASE_AUDIO_PLAYBACK_OFFLOAD) 

 
  1. {

  2. out->pcm = pcm_open(adev->snd_card, out->pcm_device_id,

 
  1. PCM_OUT | PCM_MONOTONIC, &out->config);

  2. //打开/dev/snd/pcmC0D0p 设备节点

  3. ...

  4. }

  5. ...

  6. }





1、select_devices函数

 
  1. int select_devices(struct audio_device *adev, audio_usecase_t uc_id)

  2. {

  3. ...

  4. usecase = get_usecase_from_list(adev, uc_id);//根据uc_id获取当前的usecase

  5. ...

  6. if ((usecase->type == VOICE_CALL) || (usecase->type == VOIP_CALL) ||

  7. (usecase->type == PCM_HFP_CALL)) 

 
  1. {

  2. out_snd_device = platform_get_output_snd_device(adev->platform,

  3. usecase->stream.out->devices);

  4. in_snd_device = platform_get_input_snd_device(adev->platform, 

 
  1. usecase->stream.out->devices);//如果当前是通话的状态下的话,获取输入和输出设备

  2. usecase->devices = usecase->stream.out->devices;

  3. } else {

  4. ...

  5. if (usecase->type == PCM_PLAYBACK) {

  6. usecase->devices = usecase->stream.out->devices;

  7. in_snd_device = SND_DEVICE_NONE;

  8. out_snd_device = platform_get_output_snd_device(adev->platform,

  9. usecase->stream.out->devices);

  10. ...//如果当前是音乐播放的话只选择输出设备

  11. } else if (usecase->type == PCM_CAPTURE) {

  12. usecase->devices = usecase->stream.in->device;

  13. out_snd_device = SND_DEVICE_NONE;

  14. in_snd_device = platform_get_input_snd_device(adev->platform, AUDIO_DEVICE_NONE);

  15. ...//如果当时是录音的话只选择输入设备

  16. }

  17. }

  18. ...

  19. if (out_snd_device != SND_DEVICE_NONE) {

  20. ...

  21. enable_snd_device(adev, out_snd_device, false);//打开输出设备通路

  22. }

  23. if (in_snd_device != SND_DEVICE_NONE) {

  24. ....

  25. enable_snd_device(adev, in_snd_device, false);//打开输入设备通路

  26. }

  27. ...

  28. usecase->in_snd_device = in_snd_device;

  29. usecase->out_snd_device = out_snd_device;//更新当前的输入和输出设备

  30.  
  31.  
  32. enable_audio_route(adev, usecase, true);//使能audio route

  33. ...

  34. }





2、enable_snd_device和 enable_audio_route函数

打开设备通路的流程:

 

int enable_snd_device(struct audio_device *adev,
                             snd_device_t snd_device,
                             bool __unused update_mixer)
{
....
if(platform_get_snd_device_name_extn(adev->platform, snd_device, device_name) < 0 ) {
        ALOGE("%s: Invalid sound device returned", __func__);
        return -EINVAL;
}//根据snd_device名字在device_table上查找device_name,如[SND_DEVICE_OUT_HANDSET] = "handset"等
...
audio_route_apply_and_update_path(adev->audio_route, device_name);
//跟当前的device_name在 mixer_paths.xml上找到对应的设备通路并打开
...
}


Audio_route.c (\system\media\audio_route)

 
  1. int audio_route_apply_and_update_path(struct audio_route *ar, const char *name)

  2. {

  3. if (audio_route_apply_path(ar, name) < 0) {

  4. //更新声卡中所有控件的值

  5. return -1;

  6. }

  7. return audio_route_update_path(ar, name, false /*reverse*/);

  8. //将更新后声卡控件的值写入到“/dev/snd/controlC0”中

  9. }





对于一条设备通路对应一个mixer_path结构体

 
  1. struct mixer_path {

  2. char *name;

  3. unsigned int size;

  4. unsigned int length;//这条通路中控件的个数

  5. struct mixer_setting *setting;//通路中各个控件的值

  6. };

  7. struct mixer_setting {

  8. unsigned int ctl_index;//控件的编号

  9. unsigned int num_values;

  10. int *value;

  11. };

 

 
  1. struct audio_route {

  2. struct mixer *mixer;

  3. unsigned int num_mixer_ctls;

  4. struct mixer_state *mixer_state; // 这里面保存着整个声卡中每个控件的初始值

  5.  
  6.  
  7. unsigned int mixer_path_size;

  8. unsigned int num_mixer_paths;

  9. struct mixer_path *mixer_path;// 这里面保存mixer_paths.xml的所有设备通路值

  10. };





如:<path name="speaker">
        <ctl name="Spk HP Switch" value="Spk" />
        <ctl name="Ext Spk PA Mode" value="Mode_2" />
        <ctl name="MI2S_RX Channels" value="One" />
        <ctl name="RX1 MIX1 INP1" value="RX1" />
        <ctl name="RX1 MIX1 INP2" value="RX2" />
        <ctl name="HPHL" value="Switch" />
</path>
 

 
  1. int audio_route_apply_path(struct audio_route *ar, const char *name)

  2. {

  3. struct mixer_path *path;

  4. ...

  5. path = path_get_by_name(ar, name);//根据当前设备名字找到该设备通路

  6. ...

  7. path_apply(ar, path);//根据当前使能通路控件值更新声卡中控件的状态,即mixer_state

  8.  
  9.  
  10. return 0;

  11. }

  12.  
  13.  
  14. static int path_apply(struct audio_route *ar, struct mixer_path *path)

  15. {

  16. ...

  17. for (i = 0; i < path->length; i++) {

  18. ctl_index = path->setting[i].ctl_index;//通路控件的id

  19. ctl = index_to_ctl(ar, ctl_index);

  20. type = mixer_ctl_get_type(ctl);

  21. if (!is_supported_ctl_type(type))

  22. continue;

  23. ...

  24. memcpy(ar->mixer_state[ctl_index].new_value, path->setting[i].value,

  25. path->setting[i].num_values * sizeof(int));//更新声卡中该id对应的空间的值

  26. }

  27.  
  28.  
  29. return 0;

  30. }

  31.  
  32.  
  33. static int audio_route_update_path(struct audio_route *ar, const char *name, bool reverse)

  34. {

  35. ...

  36. path = path_get_by_name(ar, name);//找出当前设备通路

  37. ...

  38. i = reverse ? (path->length - 1) : 0;

  39. end = reverse ? -1 : (int32_t)path->length;

  40.  
  41.  
  42. while (i != end) {

  43. ...

  44. ctl_index = path->setting[i].ctl_index;//找出设备通路中控件id

  45. struct mixer_state * ms = &ar->mixer_state[ctl_index];//根据控件id找到当前控件的值

  46. ...

  47. /* if any value has changed, update the mixer */

  48. for (j = 0; j < ms->num_values; j++) {

  49. if (ms->old_value[j] != ms->new_value[j]) {

  50. if (type == MIXER_CTL_TYPE_ENUM)

  51. mixer_ctl_set_value(ms->ctl, 0, ms->new_value[0]);

  52. //将值写入到底层控件中

  53. else

  54. mixer_ctl_set_array(ms->ctl, ms->new_value, ms->num_values);

  55. memcpy(ms->old_value, ms->new_value, ms->num_values * sizeof(int));

  56. break;

  57. }

  58. }

  59. i = reverse ? (i - 1) : (i + 1);

  60. }

  61. return 0;

  62. }

 

 
  1. int mixer_ctl_set_value(struct mixer_ctl *ctl, unsigned int id, int value)

  2. {

  3. struct snd_ctl_elem_value ev;

  4. ...

  5. memset(&ev, 0, sizeof(ev));

  6. ev.id.numid = ctl->info->id.numid;

  7. ret = ioctl(ctl->mixer->fd, SNDRV_CTL_IOCTL_ELEM_READ, &ev);//读当前所有音频控件的值

  8. ...

  9. return ioctl(ctl->mixer->fd, SNDRV_CTL_IOCTL_ELEM_WRITE, &ev);//将新的控件的值写入到/dev/snd/controlC0中去

  10. }




使能audio route的流程:
 

 
  1. int enable_audio_route(struct audio_device *adev,

  2. struct audio_usecase *usecase,

  3. bool __unused update_mixer)

  4. {

  5. ...

  6. platform_add_backend_name(mixer_path, snd_device);//在通路名上添加设备名,用于定制不同的audio route

  7. ...

  8. audio_route_apply_and_update_path(adev->audio_route, mixer_path);//打开audio route,其流程与之前的打开设备通路的一样

  9. ...

  10. }




以上设备通路和audio route打开的过程中,最终都走到了mixer_ctl_set_value函数,这个函数里面利用底层提供给用户的空间的设备节点来控制底层的音频控件,其联动流程如下:


首先,我们在使用 snd_card_create函数创建声卡的时候,会调用snd_ctl_create函数创建control逻辑设备,

 
  1. int snd_ctl_create(struct snd_card *card)

  2. {

  3. static struct snd_device_ops ops = {

  4. .dev_free = snd_ctl_dev_free,

  5. .dev_register = snd_ctl_dev_register,

  6. .dev_disconnect = snd_ctl_dev_disconnect,

  7. };//这个ops很关键

  8.  
  9.  
  10. if (snd_BUG_ON(!card))

  11. return -ENXIO;

  12. return snd_device_new(card, SNDRV_DEV_CONTROL, card, &ops);//把control设备放入到card->devices中去

  13. }



上面创建control逻辑设备的时候,ops中的snd_ctl_dev_register接口,会调用snd_register_device传递snd_ctl_f_ops,这个osp就是实际使用到的control设备的操作函数。


static const struct file_operations snd_ctl_f_ops =
{
.owner =THIS_MODULE,
.read =snd_ctl_read,
.open =snd_ctl_open,
.release =snd_ctl_release,
.llseek =no_llseek,
.poll =snd_ctl_poll,
.unlocked_ioctl =snd_ctl_ioctl,
.compat_ioctl =snd_ctl_ioctl_compat,
.fasync =snd_ctl_fasync,
};


在用户空间中 mixer_ctl_set_value函数读写底层control节点,其实就是通过snd_ctl_ioctl函数来实现的:
static long snd_ctl_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
...
case SNDRV_CTL_IOCTL_ELEM_READ:
return snd_ctl_elem_read_user(card, argp);
case SNDRV_CTL_IOCTL_ELEM_WRITE:
return snd_ctl_elem_write_user(ctl, argp);
...
}
最终,snd_ctl_elem_read_user和snd_ctl_elem_write_user函数都分别调用了snd_kcontrol的get和put函数,实现了对dapm kcontrol控件的控制。


3、pcm_open


Pcm_open函数主要是在start_output_stream中会调用到。


Pcm中的参数,以deep buffer中的参数来说明:
struct pcm_config pcm_config_deep_buffer = {
    .channels = 2,
    .rate = DEFAULT_OUTPUT_SAMPLING_RATE,//48000
    .period_size = DEEP_BUFFER_OUTPUT_PERIOD_SIZE,//960,硬件一次消耗的数据
    .period_count = DEEP_BUFFER_OUTPUT_PERIOD_COUNT,//8
    .format = PCM_FORMAT_S16_LE,//16 bits = 2bytes(即2字节)
    .start_threshold = DEEP_BUFFER_OUTPUT_PERIOD_SIZE / 4,
    .stop_threshold = INT_MAX,
    .avail_min = DEEP_BUFFER_OUTPUT_PERIOD_SIZE / 4,
};
1 frame = channels * format = 2*2字节 = 4字节 (32bits)
Period_size = DEEP_BUFFER_OUTPUT_PERIOD_SIZE 即 960frames = 960*32字节


struct pcm *pcm_open(unsigned int card, unsigned int device,
                     unsigned int flags, struct pcm_config *config)
{
struct snd_pcm_hw_params params;//硬件参数
struct snd_pcm_sw_params sparams;//软件参数
...
pcm = calloc(1, sizeof(struct pcm));
pcm->config = *config;
snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
             flags & PCM_IN ? 'c' : 'p');//P即为底层对应的dai_link
pcm->flags = flags;
pcm->fd = open(fn, O_RDWR);//打开底层设备节点


param_init(&params);
param_set_mask(&params, SNDRV_PCM_HW_PARAM_FORMAT,
                   pcm_format_to_alsa(config->format));
param_set_mask(&params, SNDRV_PCM_HW_PARAM_SUBFORMAT,
                   SNDRV_PCM_SUBFORMAT_STD);
param_set_min(&params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE, config->period_size);
param_set_int(&params, SNDRV_PCM_HW_PARAM_SAMPLE_BITS,
                  pcm_format_to_bits(config->format));
param_set_int(&params, SNDRV_PCM_HW_PARAM_FRAME_BITS,
                  pcm_format_to_bits(config->format) * config->channels);
param_set_int(&params, SNDRV_PCM_HW_PARAM_CHANNELS,
                  config->channels);
param_set_int(&params, SNDRV_PCM_HW_PARAM_PERIODS, config->period_count);
param_set_int(&params, SNDRV_PCM_HW_PARAM_RATE, config->rate);


if (flags & PCM_MMAP)
param_set_mask(&params, SNDRV_PCM_HW_PARAM_ACCESS,
                   SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);
else
param_set_mask(&params, SNDRV_PCM_HW_PARAM_ACCESS,
                   SNDRV_PCM_ACCESS_RW_INTERLEAVED);
//设置硬件参数


if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_HW_PARAMS, &params)) {
        oops(pcm, errno, "cannot set hw params");
        goto fail_close;
}//将硬件参数参数写到设备节点中。


/* get our refined hw_params */
config->period_size = param_get_int(&params, SNDRV_PCM_HW_PARAM_PERIOD_SIZE);
//硬件一次性消耗数据量
config->period_count = param_get_int(&params, SNDRV_PCM_HW_PARAM_PERIODS);
pcm->buffer_size = config->period_count * config->period_size;
//硬件整体缓冲大小


 if (flags & PCM_MMAP) {
     pcm->mmap_buffer = mmap(NULL, pcm_frames_to_bytes(pcm, pcm->buffer_size),
                                PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, pcm->fd, 0);
  //将pcm->buffer_size映射进内存
     ...
  }


  memset(&sparams, 0, sizeof(sparams));
  sparams.tstamp_mode = SNDRV_PCM_TSTAMP_ENABLE;
  sparams.period_step = 1;
  sparams.start_threshold = config->start_threshold;
  sparams.stop_threshold = config->stop_threshold;
  sparams.avail_min = config->avail_min;
  sparams.xfer_align = config->period_size / 2; /* needed for old kernels */
  sparams.silence_size = 0;
  sparams.silence_threshold = config->silence_threshold;
  pcm->boundary = sparams.boundary = pcm->buffer_size;


  if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_SW_PARAMS, &sparams)) {
        oops(pcm, errno, "cannot set sw params");
        goto fail;
  }//将软件参数写入到底层设备节点中


  rc = pcm_hw_mmap_status(pcm);
  //这个函数看不懂,请问这个函数是起什么作用的?
  ..
}


/dev/snd/pcmC%uD%u%c 节点底层相关的操作函数在:Pcm_native.c (\linux\android\kernel\sound\core):
const struct file_operations snd_pcm_f_ops[2] = {
{
.owner =THIS_MODULE,
.write =snd_pcm_write,
.aio_write =snd_pcm_aio_write,
.open =snd_pcm_playback_open,
.release =snd_pcm_release,
.llseek =no_llseek,
.poll =snd_pcm_playback_poll,
.unlocked_ioctl =snd_pcm_playback_ioctl,
.compat_ioctl =snd_pcm_ioctl_compat,
.mmap =snd_pcm_mmap,
.fasync =snd_pcm_fasync,
.get_unmapped_area =snd_pcm_get_unmapped_area,
},
{
.owner =THIS_MODULE,
.read =snd_pcm_read,
.aio_read =snd_pcm_aio_read,
.open =snd_pcm_capture_open,
.release =snd_pcm_release,
.llseek =no_llseek,
.poll =snd_pcm_capture_poll,
.unlocked_ioctl =snd_pcm_capture_ioctl,
.compat_ioctl =snd_pcm_ioctl_compat,
.mmap =snd_pcm_mmap,
.fasync =snd_pcm_fasync,
.get_unmapped_area =snd_pcm_get_unmapped_area,
}
};


硬件参数写入:
snd_pcm_playback_ioctl -> snd_pcm_playback_ioctl1 -> snd_pcm_common_ioctl1-> snd_pcm_hw_params_user(substream, arg);
其中 arg 对应的是 params 
static int snd_pcm_hw_params_user(struct snd_pcm_substream *substream,
 struct snd_pcm_hw_params __user * _params)
{
struct snd_pcm_hw_params *params;
int err;


params = memdup_user(_params, sizeof(*params)); //从用户向内核拷贝参数


err = snd_pcm_hw_params(substream, params);//pcm硬件设备参数设置


kfree(params);
return err;
}


软件参数写入,流程上硬件的类似,最终走到snd_pcm_sw_params函数。


4、pcm_write函数


int pcm_write(struct pcm *pcm, const void *data, unsigned int count)
{
struct snd_xferi x;
...
x.buf = (void*)data;
x.frames = count / (pcm->config.channels *
                        pcm_format_to_bits(pcm->config.format) / 8);
//计算出帧数


for (;;) {
   if (!pcm->running) {
      if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_PREPARE))
         return oops(pcm, errno, "cannot prepare channel");
         if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x))
            return oops(pcm, errno, "cannot write initial data");
         pcm->running = 1;
         return 0;
   }
   if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x)) {
         pcm->running = 0;
         return oops(pcm, errno, "cannot write stream data");
   }
   return 0;
}
}
底层相关操作函数:
snd_pcm_playback_ioctl1() -> SNDRV_PCM_IOCTL_WRITEI_FRAMES -> snd_pcm_lib_write() ->snd_pcm_lib_write1() -> snd_pcm_lib_write_transfer()


具体流程如下:
在snd_pcm_playback_ioctl1函数中,根据SNDRV_PCM_IOCTL_WRITEI_FRAMES参数,将用户空间传下来的数据传递给snd_pcm_lib_write()函数:
if (copy_from_user(&xferi, _xferi, sizeof(xferi)))
return -EFAULT;
result = snd_pcm_lib_write(substream, xferi.buf, xferi.frames);
snd_pcm_lib_write()函数由通过snd_pcm_lib_write1()函数调用snd_pcm_lib_write_transfer()将size大小的数据buf传送给DSP:
return snd_pcm_lib_write1(substream, (unsigned long)buf, size, nonblock,
 snd_pcm_lib_write_transfer);


static snd_pcm_sframes_t snd_pcm_lib_write1(struct snd_pcm_substream *substream, 
   unsigned long data,
   snd_pcm_uframes_t size,
   int nonblock,
   transfer_f transfer)
{
...
while (size > 0) {
...
err = transfer(substream, appl_ofs, data, offset, frames);
//这个transfer即为snd_pcm_lib_write_transfer,在其中会调用substream->ops->copy(substream, -1, hwoff, buf, frames) 拷贝数据
...
if (runtime->status->state == SNDRV_PCM_STATE_PREPARED &&
snd_pcm_playback_hw_avail(runtime) >= (snd_pcm_sframes_t)runtime->start_threshold) {
err = snd_pcm_start(substream);
...
}
}
...
}
下面看一下snd_pcm_lib_write_transfer和snd_pcm_start两个函数。


static int snd_pcm_lib_write_transfer(struct snd_pcm_substream *substream,
     unsigned int hwoff,
     unsigned long data, unsigned int off,
     snd_pcm_uframes_t frames)
{
struct snd_pcm_runtime *runtime = substream->runtime;
...
char __user *buf = (char __user *) data + frames_to_bytes(runtime, off);
if (substream->ops->copy) {
if ((err = substream->ops->copy(substream, -1, hwoff, buf, frames)) < 0)
//调用substeam的copy函数拷贝数据到dai上去
return err;
} else {
char *hwbuf = runtime->dma_area + frames_to_bytes(runtime, hwoff);
if (copy_from_user(hwbuf, buf, frames_to_bytes(runtime, frames)))
    //直接将数据拷贝到硬件去
return -EFAULT;
}
return 0;
}


int snd_pcm_start(struct snd_pcm_substream *substream)
{
return snd_pcm_action(&snd_pcm_action_start, substream,
     SNDRV_PCM_STATE_RUNNING);
}
其中,
static struct action_ops snd_pcm_action_start = {
.pre_action = snd_pcm_pre_start,
.do_action = snd_pcm_do_start,
.undo_action = snd_pcm_undo_start,
.post_action = snd_pcm_post_start
};


在snd_pcm_start函数中有以下流程:
snd_pcm_start-> snd_pcm_action -> snd_pcm_action_single


在snd_pcm_action_single中会分别执行:
ops->pre_action(substream, state);
ops->do_action(substream, state);
即snd_pcm_pre_start() 和snd_pcm_do_start() 函数,在snd_pcm_do_start函数中会执行:
substream->ops->trigger(substream, SNDRV_PCM_TRIGGER_START); 即触发数据拷贝。


Substream的ops都是在platform中实现的,如在snd_soc_dai_link中,我们设置了platform_name  = "msm-pcm-dsp.0"
则可以找到其对应的ops函数在:
Msm-pcm-q6-v2.c (\kernel\sound\soc\msm\qdsp6v2)
static struct snd_pcm_ops msm_pcm_ops = {
.open           = msm_pcm_open,
.copy= msm_pcm_copy,
.hw_params= msm_pcm_hw_params,
.close          = msm_pcm_close,
.ioctl          = snd_pcm_lib_ioctl,
.prepare        = msm_pcm_prepare,
.trigger        = msm_pcm_trigger,
.pointer        = msm_pcm_pointer,
.mmap= msm_pcm_mmap,
};


结合上面的流程,用户空间pcm_write函数分别调用了底层的msm_pcm_copy和msm_pcm_trigger函数,在msm_pcm_copy函数中使用了q6asm_write函数向DSP中写入数据,q6asm_write和msm_pcm_trigger中最终都是通过apr_send_pkt来发送数据的。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值