ESP32-S3 ADF音频播放器player应用开发学习记录

ESP32-S3 ADF音频播放器player应用开发学习记录

使用的开发板是 ESP32-S3-Korvo-2 V3.0,项目主要应用 播放SD卡音频 功能,所以开发板上其他组件功能未使用,主要学习开发创建播放器player播放音频功能。

开发板上使用的音频解码芯片是 ES8311和 音频功率放大器 NS4150。

组件介绍
音频编解码芯片 (Audio Codec Chip)音频编解码器芯片 ES8311 是一种低功耗单声道音频编解码器,包含单通道 ADC、单通道 DAC、低噪声前置放大器、耳机驱动器、数字音效、模拟混音和增益功能。它通过 I2S 和 I2C 总线与 ESP32-S3-WROOM-1 模组连接,以提供独立于音频应用程序的硬件音频处理。
音频功率放大器 (Audio PA Chip)NS4150 是一款低 EMI、3 W 单声道 D 类音频功率放大器,用于放大来自音频编解码芯片的音频信号,以驱动扬声器。

硬件部分

开发板使用引脚如下

管脚名称ES8311NS4150
IO16I2S0_MCLK
IO17I2C_SDA
IO18I2C_CLK
IO8I2S0_DSDIN
IO9I2S0_SCLK
IO45I2S0_LRCK
IO48PA_CTRL

开发板硬件原理图如下

请添加图片描述
请添加图片描述

程序部分

ESP32提供了几个简单的高级 API。它旨在基于标准化音频元素的典型互连快速实现音频应用。所以此次开发使用了高等级的接口 API (esp_audio_) 来构建 player。

可以参考例程: /examples/advanced_examples/esp_dispatcher_dueros/main/audio_setup.c

创建播放器

创建播放器具体分为六步:
Ⅰ 初始化音频解码芯片
Ⅱ 创建esp_audio实例
Ⅲ 启动编解码驱动程序
Ⅳ 添加音频输入流到特定的esp_audio实例
Ⅴ 添加解码器和编码器到esp_audio实例
Ⅵ 添加音频输出流到特定的esp_audio实例

void Setup_player(void)
{	
    esp_audio_cfg_t cfg = DEFAULT_ESP_AUDIO_CONFIG();
    //硬件编解码器初始化
    board_handle = audio_board_init();
    cfg.vol_handle = board_handle->audio_hal;
    cfg.vol_set = (audio_volume_set)audio_hal_set_volume;
    cfg.vol_get = (audio_volume_get)audio_hal_get_volume;
    //esp_audio适用于sepcific类型
    //ESP_AUDIO_PREFER_MEM 在新管道启动之前停止了先前链接的元素,但out流元素除外
    cfg.prefer_type = ESP_AUDIO_PREFER_MEM;
    //目标采样率
    cfg.resample_rate = 48000;
    //根据 cfg 参数创建 esp_audio实例
	player = esp_audio_create(&cfg);
	audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START);//启动/停止编解码驱动程序

    //添加音频输入流到特定的esp_audio实例
	fatfs_stream_cfg_t fatfs_cfg = FATFS_STREAM_CFG_DEFAULT();
	fatfs_cfg.type = AUDIO_STREAM_READER;
	audio_element_handle_t fatfs_stream_reader = fatfs_stream_init(&fatfs_cfg);
	esp_audio_input_stream_add(player, fatfs_stream_reader);

    //添加一个新的编解码器库,可以解码或编码音乐
	mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG();
	audio_element_handle_t mp3_decoder = mp3_decoder_init(&mp3_cfg);
    esp_audio_codec_lib_add(player, AUDIO_CODEC_TYPE_DECODER, mp3_decoder);
	
    //添加音频输出流到特定的esp_audio实例
	i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
    i2s_cfg.i2s_config.sample_rate = 48000;//和编解码的采样率保持一致
	i2s_cfg.type = AUDIO_STREAM_WRITER;
	audio_element_handle_t i2s_stream_writer = i2s_stream_init(&i2s_cfg);
	esp_audio_output_stream_add(player, i2s_stream_writer);	
}
Ⅰ.初始化音频解码芯片
   audio_board_handle_t board_handle = audio_board_init();
具体初始化流程

\esp-adf\components\audio_board\esp32_s3_korvo2_v3\board.c

audio_board_init --> audio_board_codec_init(音频芯片初始化) --> es8311_codec_init
				 --> audio_board_adc_init(音频ADC初始化)
audio_board_handle_t audio_board_init(void)
{
    if (board_handle) {
        ESP_LOGW(TAG, "The board has already been initialized!");
        return board_handle;
    }
    board_handle = (audio_board_handle_t) audio_calloc(1, sizeof(struct audio_board_handle));
    AUDIO_MEM_CHECK(TAG, board_handle, return NULL);
    board_handle->audio_hal = audio_board_codec_init();
    board_handle->adc_hal = audio_board_adc_init(); 
    return board_handle;
}
audio_hal_handle_t audio_board_codec_init(void)
{
    audio_hal_codec_config_t audio_codec_cfg = AUDIO_CODEC_DEFAULT_CONFIG();
    audio_hal_handle_t codec_hal = audio_hal_init(&audio_codec_cfg, &AUDIO_CODEC_ES8311_DEFAULT_HANDLE);
    AUDIO_NULL_CHECK(TAG, codec_hal, return NULL);
    return codec_hal;
}
//初始化音频解码芯片的结构体
struct {
audio_hal_adc_input_t adc_input;/ * !<设置adc通道*/
audio_hal_dac_output_t dac_output;/ * !<设置dac通道*/
audio_hal_codec_mode_t codec_mode;/ * !<选择编解码器模式:adc, dac或两者*/
audio_hal_codec_i2s_iface_t i2s_iface;/ * !< set I2S接口配置*/
} audio_hal_codec_config_t;
//编解码器E8311操作功能
AUDIO_CODEC_ES8311_DEFAULT_HANDLE = {
.audio_codec_initialize = es8311_codec_init //ES8311初始化
.audio_codec_deinitialize = es8311_codec_deinit,//去初始化
.audio_codec_ctrl = es8311_codec_ctrl_state,//控制状态
.audio_codec_config_iface = es8311_codec_config_i2s,//i2s配置
.audio_codec_set_mute = es8311_set_voice_mute,//设置静音
.audio_codec_set_volume = es8311_codec_set_voice_volume,//设置音量大小
.audio_codec_get_volume = es8311_codec_get_voice_volume,//获取音量大小
.audio_hal_lock = NULL
.handle = NULL;
}
ES8311初始化

\esp-adf\components\audio_hal\driver\es8311\es8311.c

esp_err_t es8311_codec_init(audio_hal_codec_config_t *codec_cfg)
{
    uint8_t datmp, regv;
    int coeff;
    esp_err_t ret = ESP_OK;
    i2c_init(); // ESP32 in master mode ESP32主模式

    ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG01, 0x30);
    ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG02, 0x00);
    ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG03, 0x10);
    ret |= es8311_write_reg(ES8311_ADC_REG16, 0x24);
    ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG04, 0x10);
    ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG05, 0x00);
    ret |= es8311_write_reg(ES8311_SYSTEM_REG0B, 0x00);
    ret |= es8311_write_reg(ES8311_SYSTEM_REG0C, 0x00);
    ret |= es8311_write_reg(ES8311_SYSTEM_REG10, 0x1F);
    ret |= es8311_write_reg(ES8311_SYSTEM_REG11, 0x7F);
    ret |= es8311_write_reg(ES8311_RESET_REG00, 0x80);
    /*
     * Set Codec into Master or Slave mode 设置编解码器为主或从模式
     */
    regv = es8311_read_reg(ES8311_RESET_REG00);
    /*
     * Set master/slave audio interface 设置主\从音频接口
     */
    audio_hal_codec_i2s_iface_t *i2s_cfg = &(codec_cfg->i2s_iface);
    switch (i2s_cfg->mode) {
        case AUDIO_HAL_MODE_MASTER:    /* MASTER MODE 主模式*/
            ESP_LOGI(TAG, "ES8311 in Master mode");
            regv |= 0x40;
            break;
        case AUDIO_HAL_MODE_SLAVE:    /* SLAVE MODE 从模式*/
            ESP_LOGI(TAG, "ES8311 in Slave mode");
            regv &= 0xBF;
            break;
        default:
            regv &= 0xBF;
    }
    ret |= es8311_write_reg(ES8311_RESET_REG00, regv);
    ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG01, 0x3F);
    /*
     * Select clock source for internal mclk 为内部时钟选择时钟源
     */
    switch (get_es8311_mclk_src()) {
        case FROM_MCLK_PIN:
            regv = es8311_read_reg(ES8311_CLK_MANAGER_REG01);
            regv &= 0x7F;
            ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG01, regv);
            break;
        case FROM_SCLK_PIN:
            regv = es8311_read_reg(ES8311_CLK_MANAGER_REG01);
            regv |= 0x80;
            ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG01, regv);
            break;
        default:
            regv = es8311_read_reg(ES8311_CLK_MANAGER_REG01);
            regv &= 0x7F;
            ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG01, regv);
            break;
    }
    int sample_fre = 0;
    int mclk_fre = 0;
    switch (i2s_cfg->samples) {
        case AUDIO_HAL_08K_SAMPLES:
            sample_fre = 8000;
            break;
        case AUDIO_HAL_11K_SAMPLES:
            sample_fre = 11025;
            break;
        case AUDIO_HAL_16K_SAMPLES:
            sample_fre = 16000;
            break;
        case AUDIO_HAL_22K_SAMPLES:
            sample_fre = 22050;
            break;
        case AUDIO_HAL_24K_SAMPLES:
            sample_fre = 24000;
            break;
        case AUDIO_HAL_32K_SAMPLES:
            sample_fre = 32000;
            break;
        case AUDIO_HAL_44K_SAMPLES:
            sample_fre = 44100;
            break;
        case AUDIO_HAL_48K_SAMPLES:
            sample_fre = 48000;
            break;
        default:
            ESP_LOGE(TAG, "Unable to configure sample rate %dHz", sample_fre);
            break;
    }
    mclk_fre = sample_fre * MCLK_DIV_FRE;
    coeff = get_coeff(mclk_fre, sample_fre);
    if (coeff < 0) {
        ESP_LOGE(TAG, "Unable to configure sample rate %dHz with %dHz MCLK", sample_fre, mclk_fre);
        return ESP_FAIL;
    }
    /*
     * Set clock parammeters 设置时钟参数
     */
    if (coeff >= 0) {
        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG02) & 0x07;
        regv |= (coeff_div[coeff].pre_div - 1) << 5;
        datmp = 0;
        switch (coeff_div[coeff].pre_multi) {
            case 1:
                datmp = 0;
                break;
            case 2:
                datmp = 1;
                break;
            case 4:
                datmp = 2;
                break;
            case 8:
                datmp = 3;
                break;
            default:
                break;
        }

        if (get_es8311_mclk_src() == FROM_SCLK_PIN) {
            datmp = 3;     /* DIG_MCLK = LRCK * 256 = BCLK * 8 */
        }
        regv |= (datmp) << 3;
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG02, regv);

        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG05) & 0x00;
        regv |= (coeff_div[coeff].adc_div - 1) << 4;
        regv |= (coeff_div[coeff].dac_div - 1) << 0;
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG05, regv);

        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG03) & 0x80;
        regv |= coeff_div[coeff].fs_mode << 6;
        regv |= coeff_div[coeff].adc_osr << 0;
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG03, regv);

        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG04) & 0x80;
        regv |= coeff_div[coeff].dac_osr << 0;
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG04, regv);

        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG07) & 0xC0;
        regv |= coeff_div[coeff].lrck_h << 0;
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG07, regv);

        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG08) & 0x00;
        regv |= coeff_div[coeff].lrck_l << 0;
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG08, regv);

        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG06) & 0xE0;
        if (coeff_div[coeff].bclk_div < 19) {
            regv |= (coeff_div[coeff].bclk_div - 1) << 0;
        } else {
            regv |= (coeff_div[coeff].bclk_div) << 0;
        }
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG06, regv);
    }

    /* 
     * mclk inverted or not MCLK是否倒置
     */
    if (INVERT_MCLK) {
        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG01);
        regv |= 0x40;
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG01, regv);
    } else {
        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG01);
        regv &= ~(0x40);
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG01, regv);
    }
    /*
     * sclk inverted or not SCLK是否倒置
     */
    if (INVERT_SCLK) {
        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG06);
        regv |= 0x20;
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG06, regv);
    } else {
        regv = es8311_read_reg(ES8311_CLK_MANAGER_REG06);
        regv &= ~(0x20);
        ret |= es8311_write_reg(ES8311_CLK_MANAGER_REG06, regv);
    }

    ret |= es8311_write_reg(ES8311_SYSTEM_REG13, 0x10);
    ret |= es8311_write_reg(ES8311_ADC_REG1B, 0x0A);
    ret |= es8311_write_reg(ES8311_ADC_REG1C, 0x6A);

    /* pa power gpio init 初始化功放引脚*/
    gpio_config_t  io_conf;
    memset(&io_conf, 0, sizeof(io_conf));
    io_conf.mode = GPIO_MODE_OUTPUT;
    io_conf.pin_bit_mask = BIT64(get_pa_enable_gpio());
    io_conf.pull_down_en = 0;
    io_conf.pull_up_en = 0;
    gpio_config(&io_conf);
    /* enable pa power 使能功放电源*/
    es8311_pa_power(true);

    codec_dac_volume_config_t vol_cfg = ES8311_DAC_VOL_CFG_DEFAULT();
    dac_vol_handle = audio_codec_volume_init(&vol_cfg);
    return ESP_OK;
}
#define 	PA_ENABLE_GPIO              GPIO_NUM_48

int8_t get_pa_enable_gpio(void)
{
    return PA_ENABLE_GPIO;
}

void es8311_pa_power(bool enable)
{
    if (enable) {
        gpio_set_level(get_pa_enable_gpio(), 1);
    } else {
        gpio_set_level(get_pa_enable_gpio(), 0);
    }
}
Ⅱ.创建esp_audio实例
// esp_audio配置参数
struct {
int                          in_stream_buf_size;/// * !<输入缓冲区大小*/
int                          out_stream_buf_size;// * !<输出缓冲区大小*/
//目标采样率,0:禁用重新采样;其他:支持44.1K、48K、32K、16K、8K
//它应该确保相同的I2S流' sample_rate '
int                          resample_rate;
QueueHandle_t                evt_que;// * !<对于收到的esp_audio事件(可选)*/
esp_audio_event_callback cb_func;// * !< esp_audio events callback(可选)*/
void  * cb_ctx;// * !< esp_audio回调上下文(可选)*/


// esp_audio适用于特定类型,首选默认内存。
//ESP_AUDIO_PREFER_MEM '模式在新管道启动之前停止先前链接的元素,除了out stream元素。
//ESP_AUDIO_PREFER_SPEED '模式在新管道启动之前保留先前链接的元素,除了out stream元素。

esp_audio_prefer_t           prefer_type;

void                        * vol_handle;// * !< 音量变化实例 */
audio_volume_set             vol_set;// * !<设置音量回调*/
audio_volume_get             vol_get;// * !<获取音量回调*/
int                          task_prio;// * !< esp_audio任务优先级*/
int                          task_stack;// * !< esp_audio任务栈大小*/
} esp_audio_cfg_t;
    cfg.vol_handle = board_handle->audio_hal;
    cfg.vol_set = (audio_volume_set)audio_hal_set_volume;
    cfg.vol_get = (audio_volume_get)audio_hal_get_volume;
    cfg.prefer_type = ESP_AUDIO_PREFER_MEM;
    cfg.resample_rate = 48000;
	esp_audio_handle_t player = esp_audio_create(&cfg);
Ⅲ.启动编解码驱动程序
//audio_hal_ctrl_codec()  Start/stop codec driver启动/停止编解码驱动程序
//audio_hal  所选音频编解码器的参考函数指针
//mode 选择编解码模式 编码/解码/或两者都
//audio_hal_ctrl 选择特定模式的启动和停止状态

audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START);//启动/停止编解码驱动程序
Ⅳ.添加音频输入流到特定的esp_audio实例

此次开发使用的是读取SD卡的音频文件,所以添加了用来处理来自 sdcard 文件操作系统中的 fatfs stream

fatfs_stream_cfg_t fatfs_cfg = FATFS_STREAM_CFG_DEFAULT();
fatfs_cfg.type = AUDIO_STREAM_READER;
audio_element_handle_t fatfs_stream_reader = fatfs_stream_init(&fatfs_cfg);
esp_audio_input_stream_add(player, fatfs_stream_reader);//添加音频输入流到特定的esp_audio实例

也可以添加多个音频输入流

    // Create readers and add to esp_audio
    fatfs_stream_cfg_t fs_reader = FATFS_STREAM_CFG_DEFAULT();
    fs_reader.type = AUDIO_STREAM_READER;
    esp_audio_input_stream_add(handle, fatfs_stream_init(&fs_reader));//faths stream

    http_stream_cfg_t http_cfg = HTTP_STREAM_CFG_DEFAULT();
    http_cfg.event_handle = _http_stream_event_handle;
    http_cfg.type = AUDIO_STREAM_READER;
    http_cfg.enable_playlist_parser = true;
    audio_element_handle_t http_stream_reader = http_stream_init(&http_cfg);
    esp_audio_input_stream_add(handle, http_stream_reader);//http stream
Ⅴ.添加解码器和编码器到esp_audio实例
mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG();
audio_element_handle_t mp3_decoder = mp3_decoder_init(&mp3_cfg);
esp_audio_codec_lib_add(player, AUDIO_CODEC_TYPE_DECODER, mp3_decoder);

也可以同时添加其他的编解码器

    // Add decoders and encoders to esp_audio
    wav_decoder_cfg_t wav_dec_cfg = DEFAULT_WAV_DECODER_CONFIG();
    mp3_decoder_cfg_t mp3_dec_cfg = DEFAULT_MP3_DECODER_CONFIG();
    mp3_dec_cfg.task_core = 1;
    aac_decoder_cfg_t aac_cfg = DEFAULT_AAC_DECODER_CONFIG();
    aac_cfg.task_core = 1;
    esp_audio_codec_lib_add(handle, AUDIO_CODEC_TYPE_DECODER, aac_decoder_init(&aac_cfg));
    esp_audio_codec_lib_add(handle, AUDIO_CODEC_TYPE_DECODER, wav_decoder_init(&wav_dec_cfg));
    esp_audio_codec_lib_add(handle, AUDIO_CODEC_TYPE_DECODER, mp3_decoder_init(&mp3_dec_cfg));
Ⅵ.添加音频输出流到特定的esp_audio实例
	i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
    i2s_cfg.i2s_config.sample_rate = 48000;//和编解码的采样率保持一致
	i2s_cfg.type = AUDIO_STREAM_WRITER;
	audio_element_handle_t i2s_stream_writer = i2s_stream_init(&i2s_cfg);
	esp_audio_output_stream_add(player, i2s_stream_writer);	

参考

ESP Audio — ADF 音频应用开发框架 文档 (readthedocs-hosted.com)

音频播放

//播放给定的uri
//esp_audio_play有跟随活动,设置输入流,输出流和编解码器的uri,启动它们。
//esp_audio将根据URI字段选择输入流,编解码器和输出流。

//@paramhandle esp_audio_handle_t实例
//@param uri   播放音频文件的uri,如果设置为NULL,将使用esp_audio_setup设置的uri
//@param type   特定句柄类型的解码器或编码器
//@param pos    指定的起始位置(以字节为单位)
 *
//@return
// - ESP_ERR_AUDIO_NO_ERROR: on success
// - ESP_ERR_AUDIO_TIMEOUT:暂停播放活动
// - ESP_ERR_AUDIO_NOT_SUPPORT:当前状态为AUDIO_STATUS_RUNNING
// - ESP_ERR_AUDIO_INVALID_URI: URI不合法
// - ESP_ERR_AUDIO_INVALID_PARAMETER:无效参数
// - ESP_ERR_AUDIO_STOP_BY_USER:由于esp_audio_stop已被调用而退出。

audio_err_t esp_audio_play(esp_audio_handle_t handle, audio_codec_type_t type, const char *uri, int pos);

// @brief 播放给定的uri,直到音乐结束或发生错误

//@param handleesp_audio_handle_t实例
//@param uri
//@param pos指定的起始位置(以字节为单位)
*
//@return
//- ESP_ERR_AUDIO_NO_ERROR: on success
//- ESP_ERR_AUDIO_TIMEOUT:暂停播放活动
//- ESP_ERR_AUDIO_NOT_SUPPORT:当前状态为AUDIO_STATUS_RUNNING
//- ESP_ERR_AUDIO_INVALID_URI: URI不合法
//- ESP_ERR_AUDIO_INVALID_PARAMETER:无效参数

audio_err_t esp_audio_sync_play(esp_audio_handle_t handle, const char *uri, int pos);

开发中实例问题

  • 自己画的板子和开发板中使用的引脚不同,更改ES8311 I2S 引脚

esp-adf\esp-adf\components\audio_board\esp32_s3_korvo2_v3\board_pins_config.c 更改引脚

esp_err_t get_i2s_pins(i2s_port_t port, i2s_pin_config_t *i2s_config)
{
    AUDIO_NULL_CHECK(TAG, i2s_config, return ESP_FAIL);
    if (port == I2S_NUM_0) {
        i2s_config->bck_io_num = GPIO_NUM_9;
        i2s_config->ws_io_num = GPIO_NUM_45;
        i2s_config->data_out_num = GPIO_NUM_8;
        i2s_config->data_in_num = GPIO_NUM_10;
//        i2s_config->mck_io_num = GPIO_NUM_16;
		i2s_config->mck_io_num = GPIO_NUM_21;
    } else if (port == I2S_NUM_1) {
        i2s_config->bck_io_num = -1;
        i2s_config->ws_io_num = -1;
        i2s_config->data_out_num = -1;
        i2s_config->data_in_num = -1;
        i2s_config->mck_io_num = -1;
    } else {
        memset(i2s_config, -1, sizeof(i2s_pin_config_t));
        ESP_LOGE(TAG, "i2s port %d is not supported", port);
        return ESP_FAIL;
    }

    return ESP_OK;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值