PCM data flow - part 3: ASoC codec driver

http://blog.csdn.net/azloong/article/details/17252843


上一章提到codec_drv的几个组成部分,下面逐一介绍,基本是以内核文档Documentation/sound/alsa/soc/codec.txt中的内容为脉络来分析的。codec的作用,在概述中有说明,本章主要罗列下codec driver中重要的数据结构及注册流程。


Codec DAI and PCM configuration


codec_dai和pcm配置信息通过结构体snd_soc_dai_driver描述,包括dai的能力描述和操作接口,snd_soc_dai_driver最终会被注册到asoc-core中。

  1. /* 
  2.  * Digital Audio Interface Driver. 
  3.  * 
  4.  * Describes the Digital Audio Interface in terms of its ALSA, DAI and AC97 
  5.  * operations and capabilities. Codec and platform drivers will register this 
  6.  * structure for every DAI they have. 
  7.  * This structure covers the clocking, formating and ALSA operations for each 
  8.  * interface. 
  9.  */  
  10. struct snd_soc_dai_driver {  
  11.     /* DAI description */  
  12.     const char *name;  
  13.     unsigned int id;  
  14.     int ac97_control;  
  15.   
  16.     /* DAI driver callbacks */  
  17.     int (*probe)(struct snd_soc_dai *dai);  
  18.     int (*remove)(struct snd_soc_dai *dai);  
  19.     int (*suspend)(struct snd_soc_dai *dai);  
  20.     int (*resume)(struct snd_soc_dai *dai);  
  21.   
  22.     /* ops */  
  23.     const struct snd_soc_dai_ops *ops;  
  24.   
  25.     /* DAI capabilities */  
  26.     struct snd_soc_pcm_stream capture;  
  27.     struct snd_soc_pcm_stream playback;  
  28.     unsigned int symmetric_rates:1;  
  29.   
  30.     /* probe ordering - for components with runtime dependencies */  
  31.     int probe_order;  
  32.     int remove_order;  
  33. };  

·          name:codec_dai的名称标识,machine中的dai_link通过codec_dai_name来匹配codec_dai;

·          probe:codec_dai的probe函数,由snd_soc_instantiate_card回调;

·          playback:回放能力描述信息,如所支持的声道数、采样率、音频格式;

·          capture:录制能力描述信息,如所支持声道数、采样率、音频格式;

·          ops:指向codec_dai的操作函数集,这些函数集非常重要,它定义了dai的时钟配置、格式配置、硬件参数配置等回调。

例子,wm8994有三个dai,这里只列其一:

  1. static const struct snd_soc_dai_ops wm8994_aif1_dai_ops = {  
  2.     .set_sysclk = wm8994_set_dai_sysclk,  
  3.     .set_fmt    = wm8994_set_dai_fmt,  
  4.     .hw_params  = wm8994_hw_params,  
  5.     .shutdown   = wm8994_aif_shutdown,  
  6.     .digital_mute   = wm8994_aif_mute,  
  7.     .set_pll    = wm8994_set_fll,  
  8.     .set_tristate   = wm8994_set_tristate,  
  9. };  
  10.   
  11. static struct snd_soc_dai_driver wm8994_dai[] = {  
  12.     {  
  13.         .name = "wm8994-aif1",  
  14.         .id = 1,  
  15.         .playback = {  
  16.             .stream_name = "AIF1 Playback",  
  17.             .channels_min = 1,  
  18.             .channels_max = 2,  
  19.             .rates = WM8994_RATES,  
  20.             .formats = WM8994_FORMATS,  
  21.             .sig_bits = 24,  
  22.         },  
  23.         .capture = {  
  24.             .stream_name = "AIF1 Capture",  
  25.             .channels_min = 1,  
  26.             .channels_max = 2,  
  27.             .rates = WM8994_RATES,  
  28.             .formats = WM8994_FORMATS,  
  29.             .sig_bits = 24,  
  30.          },  
  31.         .ops = &wm8994_aif1_dai_ops,  
  32.     },  
  33.     // ...省略...  


Codec control IO


移动设备的音频codec,其控制接口一般是I2C或SPI。codec的读写接口访问操作codec寄存器,当dapm触发或mixer控件发生改变时,需要调用到读写接口。

在snd_soc_codec_driver结构体中,有如下字段描述codec的控制接口:

  1. /* codec IO */  
  2. unsigned int (*read)(struct snd_soc_codec *, unsigned int);  
  3. int (*write)(struct snd_soc_codec *, unsigned int, unsigned int);  
  4. int (*display_register)(struct snd_soc_codec *, char *,  
  5.             size_t, unsigned int);  
  6. int (*volatile_register)(struct snd_soc_codec *, unsigned int);  
  7. int (*readable_register)(struct snd_soc_codec *, unsigned int);  
  8. int (*writable_register)(struct snd_soc_codec *, unsigned int);  
  9. unsigned int reg_cache_size;  
  10. short reg_cache_step;  
  11. short reg_word_size;  
  12. const void *reg_cache_default;  
  13. short reg_access_size;  
  14. const struct snd_soc_reg_access *reg_access_default;  
  15. enum snd_soc_compress_type compress_type;  

·          read:读取codec寄存器接口;

·          write:写入codec寄存器接口;

·          volatile_register:判断指定的寄存器是否是volatile属性;假如是,则asoc-core读取它的时候不会只是读cache,会直接通过控制接口访问硬件IO;

·          readable_register:判断指定的寄存器是否可读;

·          reg_cache_default:codec寄存器的缺省值;

·          reg_cache_size:codec缺省的寄存器值数组大小;

·          reg_word_size:codec寄存器值宽度。


在Linux-3.4.5中,这里面很多信息都改用regmap描述了。asoc-core中判断是否用的是regmap,如果是,则调用regmap接口。见如下函数:

  1. int snd_soc_update_bits(struct snd_soc_codec *codec, unsigned short reg,  
  2.                 unsigned int mask, unsigned int value)  
  3. {  
  4.     bool change;  
  5.     unsigned int old, new;  
  6.     int ret;  
  7.   
  8.     if (codec->using_regmap) {  
  9.         // 当前使用regmap,调用regmap接口,其中codec->control_data是regmap私有数据  
  10.         ret = regmap_update_bits_check(codec->control_data, reg,  
  11.                            mask, value, &change);  
  12.     } else {  
  13.         // 非regmap,调用snd_soc_codec_driver定义的read/write回调  
  14.         ret = snd_soc_read(codec, reg);  
  15.         if (ret < 0)  
  16.             return ret;  
  17.   
  18.         old = ret;  
  19.         new = (old & ~mask) | (value & mask);  
  20.         change = old != new;  
  21.         if (change)  
  22.             ret = snd_soc_write(codec, reg, new);  
  23.     }  
  24.   
  25.     if (ret < 0)  
  26.         return ret;  
  27.   
  28.     return change;  
  29. }  

使用regmap,使得控制接口抽象化,codec_drv不用操心当前控制方式是什么;

regmap在线调试目录是/sys/kernel/debug/regmap

关于wm8994的regmap描述,请自行查阅driver/mfd/wm8994-regmap.c。


Mixers and audio controls


音频控件多用于部件开关和音量的设定,音频控件可通过soc.h中的宏来定义。例如单一型控件:

  1. #define SOC_SINGLE(xname, reg, shift, max, invert) \  
  2. {   .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \  
  3.     .info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\  
  4.     .put = snd_soc_put_volsw, \  
  5.     .private_value =  SOC_SINGLE_VALUE(reg, shift, max, invert) }  

这种控件只有一个设置量,一般用于部件开关。宏定义的参数说明:

·          xname:控件的名称标识;

·          reg:控件对应的寄存器地址;

·          shift:控件控制位在寄存器中的偏移;

·          max:控件设置值范围,一般来说,如果控制位只有1位的话,那么max=1,因为设置值只有0和1;

·          invert:设定值是否取反。

其他类型控件类似,不一一介绍了。

上述只是宏定义,音频控件真正的结构是snd_kcontrol_new

  1. struct snd_kcontrol_new {  
  2.     snd_ctl_elem_iface_t iface; /* interface identifier */  
  3.     unsigned int device;        /* device/client number */  
  4.     unsigned int subdevice;     /* subdevice (substream) number */  
  5.     const unsigned char *name;  /* ASCII name of item */  
  6.     unsigned int index;     /* index of item */  
  7.     unsigned int access;        /* access rights */  
  8.     unsigned int count;     /* count of same elements */  
  9.     snd_kcontrol_info_t *info;  
  10.     snd_kcontrol_get_t *get;  
  11.     snd_kcontrol_put_t *put;  
  12.     union {  
  13.         snd_kcontrol_tlv_rw_t *c;  
  14.         const unsigned int *p;  
  15.     } tlv;  
  16.     unsigned long private_value;  
  17. };  

codec初始化时,通过snd_soc_add_codec_controls把所有定义好的音频控件注册到alsa-core中,上层可以通过tinymix、alsa_amixer等工具查看修改这些控件的设定。

音频控件的详细分析见dapm系列文章。


Codec audio operations


Codec的音频操作接口通过结构体snd_soc_dai_ops描述:

  1. struct snd_soc_dai_ops {  
  2.     /* 
  3.      * DAI clocking configuration, all optional. 
  4.      * Called by soc_card drivers, normally in their hw_params. 
  5.      */  
  6.     int (*set_sysclk)(struct snd_soc_dai *dai,  
  7.         int clk_id, unsigned int freq, int dir);  
  8.     int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,  
  9.         unsigned int freq_in, unsigned int freq_out);  
  10.     int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div);  
  11.   
  12.     /* 
  13.      * DAI format configuration 
  14.      * Called by soc_card drivers, normally in their hw_params. 
  15.      */  
  16.     int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt);  
  17.     int (*set_tdm_slot)(struct snd_soc_dai *dai,  
  18.         unsigned int tx_mask, unsigned int rx_mask,  
  19.         int slots, int slot_width);  
  20.     int (*set_channel_map)(struct snd_soc_dai *dai,  
  21.         unsigned int tx_num, unsigned int *tx_slot,  
  22.         unsigned int rx_num, unsigned int *rx_slot);  
  23.     int (*set_tristate)(struct snd_soc_dai *dai, int tristate);  
  24.   
  25.     /* 
  26.      * DAI digital mute - optional. 
  27.      * Called by soc-core to minimise any pops. 
  28.      */  
  29.     int (*digital_mute)(struct snd_soc_dai *dai, int mute);  
  30.   
  31.     /* 
  32.      * ALSA PCM audio operations - all optional. 
  33.      * Called by soc-core during audio PCM operations. 
  34.      */  
  35.     int (*startup)(struct snd_pcm_substream *,  
  36.         struct snd_soc_dai *);  
  37.     void (*shutdown)(struct snd_pcm_substream *,  
  38.         struct snd_soc_dai *);  
  39.     int (*hw_params)(struct snd_pcm_substream *,  
  40.         struct snd_pcm_hw_params *, struct snd_soc_dai *);  
  41.     int (*hw_free)(struct snd_pcm_substream *,  
  42.         struct snd_soc_dai *);  
  43.     int (*prepare)(struct snd_pcm_substream *,  
  44.         struct snd_soc_dai *);  
  45.     int (*trigger)(struct snd_pcm_substream *, int,  
  46.         struct snd_soc_dai *);  
  47.     /* 
  48.      * For hardware based FIFO caused delay reporting. 
  49.      * Optional. 
  50.      */  
  51.     snd_pcm_sframes_t (*delay)(struct snd_pcm_substream *,  
  52.         struct snd_soc_dai *);  
  53. };  

注释比较详细的了,codec音频操作接口分为5大部分:时钟配置、格式配置、数字静音、PCM音频接口、FIFO延迟报告。着重说下时钟配置及格式配置接口:

·          set_sysclk:codec_dai的系统时钟设置,当上层pcm_open时,需要回调该接口设置codec的系统时钟,codec才能正常工作;

·          set_pll:Codec FLL设置,codec一般有个MCLK的输入时钟,回调该接口基于MCLK来设定codec FLL的时钟频率;得到正确的FLL时钟频率后,则codec的sysclk、bclk、lrclk均可从FLL分频出来(假设codec作为master);

·          set_fmt:codec_dai的格式设置,相关格式见soc-dai.h定义;

·          SND_SOC_DAIFMT_I2S——音频数据是I2S格式;

·          SND_SOC_DAIFMT_DSP_A——音频数据是语音常用的PCM格式;

·          SND_SOC_DAIFMT_CBM_CFM——codec作为master,BCLK和LRCLK由codec提供;

·          SND_SOC_DAIFMT_CBS_CFS——codec作为 slave ,BCLK和LRCLK由soc platform提供;

·          hw_params:codec_dai的硬件参数设置,根据上层设定的声道数、采样率、数据格式,来设置codec_dai相关寄存器。


以上接口一般在Machine驱动中回调,我们看看Machine驱动goni_wm8994.c的goni_hifi_hw_params函数:
  1. static int goni_hifi_hw_params(struct snd_pcm_substream *substream,  
  2.         struct snd_pcm_hw_params *params)  
  3. {  
  4.     struct snd_soc_pcm_runtime *rtd = substream->private_data;  
  5.     struct snd_soc_dai *codec_dai = rtd->codec_dai;  
  6.     struct snd_soc_dai *cpu_dai = rtd->cpu_dai;  
  7.     unsigned int pll_out = 24000000; // 这是MCLK的时钟频率,codec的源时钟  
  8.     int ret = 0;  
  9.   
  10.     /* set the cpu DAI configuration */  
  11.     ret = snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S |  
  12.             SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM);  
  13.     if (ret < 0)  
  14.         return ret;  
  15.   
  16.     /* set codec DAI configuration */  
  17.     ret = snd_soc_dai_set_fmt(codec_dai, SND_SOC_DAIFMT_I2S |  
  18.             SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM);  
  19.     if (ret < 0)  
  20.         return ret;  
  21.   
  22.     /* set the codec FLL */  
  23.     ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, 0, pll_out,  
  24.             params_rate(params) * 256);  
  25.     if (ret < 0)  
  26.         return ret;  
  27.   
  28.     /* set the codec system clock */  
  29.     ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1,  
  30.             params_rate(params) * 256, SND_SOC_CLOCK_IN);  
  31.     if (ret < 0)  
  32.         return ret;  
  33.   
  34.     return 0;  
  35. }  

其中snd_soc_dai_set_fmt()实际上会调用cpu_dai或codec_dai的set_fmt回调,

snd_soc_dai_set_pll()、snd_soc_dai_set_sysclk()类似。

·          MCLK作为codec的源时钟,频率为24Mhz;

·          设置cpu_dai和codec_dai格式:I2S,BCLK和LRCLK由codec提供;

·          设置codec_dai的FLL1:时钟源是MCLK,时钟源频率是24Mhz,目的时钟频率是256fs;

·          设置codec_dai的系统时钟:时钟源是FLL1,系统时钟频率是256fs。


codec_dai的时钟设置非常重要,设置错误将会导致很多问题,典型如下:

·          无声-->> 检查codec的系统时钟、codec_dai的位时钟和帧时钟是否使能;

·          声音失真-->> 检查音频源数据的采样率是否和codec_dai的帧时钟一致;

·          声音断续 -->> 检查codec的系统时钟和位时钟、帧时钟是否同步,出现这种情况,可能是sysclk和BCLK/LRCLK不是由同一个时钟源分频出来导致的,取决于具体codec设计。


DAPM description


概念:Dynamic Audio Power Management,动态音频电源管理,为移动Linux设备设计,使得音频系统任何时候都工作在最低功耗状态。

目的:使能必要的最少的部件,令音频系统正常工作。

原理:当音频路径发生改变(比如上层使用tinymix工具设置音频通路)时,或发生数据流事件(比如启动或停止播放)时,都会触发dapm去遍历所有邻近的音频部件,检查是否存在完整的音频路径(complete path:该部件必须往前能到达输入端点如DAC/Mic/Linein,往后能到达输出端点如ADC/HP/SPK),如果存在完整的音频路径,则该路径上面的所有部件都是需要上电的,其他部件则下电。

对于此的完整分析,见另一个系列的文章。

说说DAPM部件中最典型的mixer widget:Mixes several analog signals into a single analog signal. 它可以把几路模拟信号混合到一路输出。如WM8994的SPKMIXL:


如图,SPKMIXL有5路输入,分别是:MIXINL、IN1LP、DAC1L、DAC2L、MIXEROUTL,因此这里可以构成5条路径。

·          1、如下5个控件控制SPKMIXL输入:

  1. static const struct snd_kcontrol_new left_speaker_mixer[] = {  
  2. SOC_DAPM_SINGLE("DAC2 Switch", WM8994_SPEAKER_MIXER, 9, 1, 0),  
  3. SOC_DAPM_SINGLE("Input Switch", WM8994_SPEAKER_MIXER, 7, 1, 0),  
  4. SOC_DAPM_SINGLE("IN1LP Switch", WM8994_SPEAKER_MIXER, 5, 1, 0),  
  5. SOC_DAPM_SINGLE("Output Switch", WM8994_SPEAKER_MIXER, 3, 1, 0),  
  6. SOC_DAPM_SINGLE("DAC1 Switch", WM8994_SPEAKER_MIXER, 1, 1, 0),  
  7. };  
·          2、定义SPKMIXL的dapm widget:
  1. SND_SOC_DAPM_MIXER_E("SPKL", WM8994_POWER_MANAGEMENT_3, 8, 0,  
  2.              left_speaker_mixer, ARRAY_SIZE(left_speaker_mixer),  
  3.              late_enable_ev, SND_SOC_DAPM_PRE_PMU),  
留意WM8994_POWER_MANAGEMENT_3寄存器的bit8正是控制SPKMIXL通断电的。

·          3、定义SPKMIXL相关路由:

  1. static const struct snd_soc_dapm_route intercon[] = {  
  2.     // ...  
  3.     { "SPKL""DAC1 Switch""DAC1L" },  
  4.     { "SPKL""DAC2 Switch""DAC2L" },  

最终上层会看到两个控件:"SPKL DAC1 Switch","SPKL DAC2 Switch";前者用于SPKL选中DAC1L作为输入,后者用于SPKL选中DAC2L作为输入。

但控件"SPKL DAC1 Switch"或"SPKL DAC2 Switch"的打开,不代表能使得"SPKL"上电。只有当SPKL位于完整的音频路径中时,SPKL才会上电,WM8994_POWER_MANAGEMENT_3寄存器的bit8会被置为1。


Codec register


当platform_driver:

  1. static struct platform_driver wm8994_codec_driver = {  
  2.     .driver = {  
  3.         .name = "wm8994-codec",  
  4.         .owner = THIS_MODULE,  
  5.         .pm = &wm8994_pm_ops,  
  6.     },  
  7.     .probe = wm8994_probe,  
  8.     .remove = __devexit_p(wm8994_remove),  
  9. };  

与name ="wm8994-codec"的platform_device(该platform_device在driver/mfd/wm8994-core.c中创建并初始化完成)匹配后,立即回调wm8994_probe注册codec:

  1. static int __devinit wm8994_probe(struct platform_device *pdev)  
  2. {  
  3.     struct wm8994_priv *wm8994;  
  4.   
  5.     wm8994 = devm_kzalloc(&pdev->dev, sizeof(struct wm8994_priv),  
  6.                   GFP_KERNEL);  
  7.     if (wm8994 == NULL)  
  8.         return -ENOMEM;  
  9.     platform_set_drvdata(pdev, wm8994);  
  10.   
  11.     wm8994->wm8994 = dev_get_drvdata(pdev->dev.parent);  
  12.     wm8994->pdata = dev_get_platdata(pdev->dev.parent);  
  13.   
  14.     return snd_soc_register_codec(&pdev->dev, &soc_codec_dev_wm8994,  
  15.             wm8994_dai, ARRAY_SIZE(wm8994_dai));  
  16. }  


snd_soc_register_codec:将codec_driver和codec_dai_driver注册到asoc-core。

  1. /** 
  2.  * snd_soc_register_codec - Register a codec with the ASoC core 
  3.  * 
  4.  * @codec: codec to register 
  5.  */  
  6. int snd_soc_register_codec(struct device *dev,  
  7.                const struct snd_soc_codec_driver *codec_drv,  
  8.                struct snd_soc_dai_driver *dai_drv,  
  9.                int num_dai)  
创建一个snd_soc_codec实例,包含codec_drv(snd_soc_dai_driver)的相关信息,封装给asoc-core使用,相关代码段如下:
  1. struct snd_soc_codec *codec;  
  2.   
  3. dev_dbg(dev, "codec register %s\n", dev_name(dev));  
  4.   
  5. codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);  
  6. if (codec == NULL)  
  7.     return -ENOMEM;  
  8.   
  9. /* create CODEC component name */  
  10. codec->name = fmt_single_name(dev, &codec->id);  
  11. if (codec->name == NULL) {  
  12.     kfree(codec);  
  13.     return -ENOMEM;  
  14. }  
  15.   
  16. // 初始化codec的寄存器缓存配置及读写接口  
  17. codec->write = codec_drv->write;  
  18. codec->read = codec_drv->read;  
  19. codec->volatile_register = codec_drv->volatile_register;  
  20. codec->readable_register = codec_drv->readable_register;  
  21. codec->writable_register = codec_drv->writable_register;  
  22. codec->ignore_pmdown_time = codec_drv->ignore_pmdown_time;  
  23. codec->dapm.bias_level = SND_SOC_BIAS_OFF;  
  24. codec->dapm.dev = dev;  
  25. codec->dapm.codec = codec;  
  26. codec->dapm.seq_notifier = codec_drv->seq_notifier;  
  27. codec->dapm.stream_event = codec_drv->stream_event;  
  28. codec->dev = dev;  
  29. codec->driver = codec_drv;  
  30. codec->num_dai = num_dai;  
  31. mutex_init(&codec->mutex);  

把以上创建并初始化好的codec插入到codec_list链表上,Machine驱动初始化时会遍历该链表,以匹配并绑定dai_link上的codec:

  1. list_add(&codec->list, &codec_list);  

把codec_drv中的snd_soc_dai_driver(wm8994有3个dai,分别是aif1、aif2和aif3)注册到asoc-core:

  1. /* register any DAIs */  
  2. if (num_dai) {  
  3.     ret = snd_soc_register_dais(dev, dai_drv, num_dai);  
  4.     if (ret < 0)  
  5.         goto fail;  
  6. }  

snd_soc_register_dais会把dai插入到dai_list链表中:list_add(&dai->list,&dai_list); Machine驱动初始化时会遍历该链表,以匹配并绑定dai_link上的codec_dai。


最后顺便提下dai_link中的codec和codec_dai的区别:codec指音频codec共有的部分,包括codec初始化函数、控制接口、寄存器缓存、控件列表、dapm部件列表、音频路由列表、偏置电压设置函数等描述信息;而codec_dai指codec上的音频接口驱动描述,各个接口的描述信息不一定都是一致的,所以各个音频接口都有它自身的驱动描述,包括音频接口的初始化函数、操作函数集、能力描述等。

我开始时认为:codec_dai从属于codec,dai_link没有必要同时声明codec和codec_dai,应该可以实现codec_dai就能找到它对应的父设备codec的方法。后来想到系统上如果有两个以上的codec,而恰好不同codec上的codec_dai有重名的话,此时就必须同时声明codec和codec_dai才能找到正确的音频接口了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值