从0学起的esp-idf之旅——i2s总线理解与运用

I2S总线基础概念

I2S概念

I2S(Inter-IC Sound)总线, 又称集成电路内置音频总线,最早是由现在的恩智浦半导体公司针对数字音频设备之间的音频数据传输而制定的总线标准。该总线专门用于音频设备间的传输,广泛用于各种多媒体系统。它传输的是PCM格式数据。

PCM音频数据

PCM (脉冲编码调制),由A.里弗斯于1937年提出的记录音频的格式。PCM是模拟信号经过采样、量化、编码转换成标准数字音频的原始数据格式。

声音量化过程

转换成PCM格式的三个参数

音频数据量=采样频率×量化位数×声道数/8(字节/秒)

采样频率(声音周期量化)

1秒同时对多个声道完成adc采样的次数。采样频率越高,声音质量越好,还原越真实,但同时它占的资源比较多。

采样位数(声音的幅度量化)

每个采样点用多少二进制位表示数据范围。量化位数越多,音质越好,数据量也越大

声道数(单声道,立体声)

使用声音通道的个数,有单声道和立体声之分,立体声比单声道数据量翻倍。

I2S总线通讯方式

  1. 支持全双工和半双工通信
  2. 支持主/从模式

I2S总线引脚

  1. SCK:(continuous serial clock) 串行时钟,I2S的心跳。
    串行时钟SCK,也叫位时钟BCLK,也有的称为BCK、SCLK等。对应数字音频的每一位数据,SCK都有1个脉冲。
    SCK的频率 = 声道数 * 采样频率 * 采样位数。
  2. WS: (word select) 字段(声道)选择
    字段选择信号WS,也叫LRCLK(LRCK),用于切换左右声道的数据。
    WS的频率 = 采样频率
    WS为0,表示正在传输的是左声道的数据;
    WS为1,表示正在传输的是右声道的数据。
  3. SDATA:串行数据,在全双工的模式下分为DATAin和DATAout,主从之间的in/out要交叉接线
  4. MCLK:主时钟频率。一般为采样频率的256倍

esp32从ES8311分析i2s驱动如何去写

原理图

┌─────────────────┐           ┌──────────────────────────┐
│       ESP       │           │          ES8311          │
│                 │           │                          │
│     MCLK-GPIO 0 ├──────────►│PIN2-MCLK                 │
│                 │           │                          │           ┌─────────┐
│     BCLK-GPIO 4 ├──────────►│PIN6-BCLK       PIN12-OUTP├───────────┤         │
│                 │           │                          │           │ EARPHONE│
│       WS-GPIO 5 ├──────────►│PIN8-LRCK       PIN13-OUTN├───────────┤         │
│                 │           │                          │           └─────────┘
│    SDOUT-GPIO 18├──────────►│PIN9-SDIN                 │
│                 │           │                          │
│     SDIN-GPIO 19│◄──────────┤PIN7-SDOUT                │
│                 │           │                          │           ┌─────────┐
│                 │           │               PIN18-MIC1P├───────────┤         │
│      SCL-GPIO 16├──────────►│PIN1 -CCLK                │           │  MIC    │
│         (GPIO 7)│           │               PIN17-MIC1N├───────────┤         │
│      SDA-GPIO 17│◄─────────►│PIN19-CDATA               │           └─────────┘
│         (GPIO 8)│           │                          │
│          VCC 3.3├───────────┤VCC                       │
│                 │           │                          │
│              GND├───────────┤GND                       │
└─────────────────┘           └──────────────────────────┘

注意:这里的SDOUT和SDIN要交叉接线。而且除了i2s给数据流之外,我们还需要一个i2c用来初始化codec芯片

例程代码分析

主函数

void app_main(void)
{
    /* 初始化i2s驱动 */
    if (i2s_driver_init() != ESP_OK) {
        ESP_LOGE(TAG, "i2s driver init failed");
        abort();
    }
    /* 初始化 i2c 外围设备并通过 i2c 配置 es8311 编解码器 */
    if (es8311_codec_init() != ESP_OK) {
        ESP_LOGE(TAG, "es8311 codec init failed");
        abort();
    }
#if CONFIG_EXAMPLE_MODE_MUSIC
    /* 在音乐模式下播放一段音乐 */
    xTaskCreate(i2s_music, "i2s_music", 4096, NULL, 5, NULL);
#else
    /* 以回声模式回声来自 MIC 的声音 */
    xTaskCreate(i2s_echo, "i2s_echo", 8192, NULL, 5, NULL);
#endif
}

主函数整体比较简单初始化i2s后初始化i2c并且给i2c初始化,然后就是通过i2s输入音频数据。

i2s初始化

static esp_err_t i2s_driver_init(void)
{
    i2s_config_t i2s_cfg = {
        .mode = I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_RX, //设置i2s工作模式,根据需求设置
        .sample_rate = EXAMPLE_SAMPLE_RATE,//设置I2S 采样率,根据音频确定采样率
        .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,//设置采样位数
        .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,//设置I2S 通道格式(分离左右声道)
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,//设置I2S 通讯格式
        .tx_desc_auto_clear = true,//I2S 自动清除tx描述符
#if SOC_I2S_SUPPORTS_TDM
        .total_chan = 2,
        .chan_mask = I2S_TDM_ACTIVE_CH0 | I2S_TDM_ACTIVE_CH1,
        .left_align = false,
        .big_edin = false,
        .bit_order_msb = false,
        .skip_msk = false,
#endif
        .dma_desc_num = 8, // I2S DMA 用于接收/发送数据的描述符总数
        .dma_frame_num = 64, // 一次性采样的帧数。这里的 frame 表示一个 WS 周期内所有通道的总数据
        .use_apll = false, // I2S 使用 APLL 作为主要 I2S 时钟,使其能够获得准确的时钟
        .mclk_multiple = EXAMPLE_MCLK_MULTIPLE, // I2S 主时钟(MCLK)与采样率的倍数,有256(默认) 128 384倍
        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // 用于分配中断的标志
    };

    ESP_RETURN_ON_ERROR(i2s_driver_install(I2S_NUM, &i2s_cfg, 0, NULL), TAG, "install i2s failed");
    i2s_pin_config_t i2s_pin_cfg = {
        .mck_io_num = I2S_MCK_IO,
        .bck_io_num = I2S_BCK_IO,
        .ws_io_num = I2S_WS_IO,
        .data_out_num = I2S_DO_IO,
        .data_in_num = I2S_DI_IO
    };
    ESP_RETURN_ON_ERROR(i2s_set_pin(I2S_NUM, &i2s_pin_cfg), TAG, "set i2s pins failed");
    return ESP_OK;
}

关于TDM相关的这里有比较详细的说明,大致就是多声道输出。本人理解有限 ,这里不做过多解释。

es8311初始化

static esp_err_t es8311_codec_init(void)
{
    /* i2c初始化 */
    i2c_config_t es_i2c_cfg = {
        .sda_io_num = I2C_SDA_IO,
        .scl_io_num = I2C_SCL_IO,
        .mode = I2C_MODE_MASTER,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = 100000,
    };
    ESP_RETURN_ON_ERROR(i2c_param_config(I2C_NUM, &es_i2c_cfg), TAG, "config i2c failed");
    ESP_RETURN_ON_ERROR(i2c_driver_install(I2C_NUM, I2C_MODE_MASTER,  0, 0, 0), TAG, "install i2c driver failed");

    /* 初始化es8311芯片 */
    es8311_handle_t es_handle = es8311_create(I2C_NUM, ES8311_ADDRRES_0);
    ESP_RETURN_ON_FALSE(es_handle, ESP_FAIL, TAG, "es8311 create failed");
    es8311_clock_config_t es_clk = {
        .mclk_from_mclk_pin = true,
        .sample_frequency = EXAMPLE_SAMPLE_RATE
    };

    es8311_init(es_handle, &es_clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16);
    ESP_RETURN_ON_ERROR(es8311_sample_frequency_config(es_handle, EXAMPLE_SAMPLE_RATE * EXAMPLE_MCLK_MULTIPLE, EXAMPLE_SAMPLE_RATE), TAG, "set es8311 sample frequency failed");
    ESP_RETURN_ON_ERROR(es8311_voice_volume_set(es_handle, EXAMPLE_VOICE_VOLUME, NULL), TAG, "set es8311 volume failed");
    ESP_RETURN_ON_ERROR(es8311_microphone_config(es_handle, false), TAG, "set es8311 microphone failed");
#if CONFIG_EXAMPLE_MODE_ECHO
    ESP_RETURN_ON_ERROR(es8311_microphone_gain_set(es_handle, EXAMPLE_MIC_GAIN), TAG, "set es8311 microphone gain faield");
#endif
    return ESP_OK;
}

前面部分是在初始化i2c,后面部分主要是在调用es8311库里的东西,主要是调用i2c去配置一些es8311的寄存器。

播放音频

static void i2s_music(void *args)
{
    esp_err_t ret = ESP_OK;
    size_t bytes_write = 0;
    while (1) {
        /* 将音频数据通过i2s写入es8311 */
        ret = i2s_write(I2S_NUM, music_pcm_start, music_pcm_end - music_pcm_start, &bytes_write, portMAX_DELAY);
        if (ret != ESP_OK) {
            /* 由于我们在 'i2s_write' 中将超时设置为 'portMAX_DELAY',
            所以除非设置其他超时值,否则您将无法到达此处,如果检测到超时,则表示写入操作失败。*/
            ESP_LOGE(TAG, "[music] i2s read failed, %s", err_reason[ret == ESP_ERR_TIMEOUT]);
            abort();
        }
        /* 清除 DMA 缓冲区以避免缓冲区中的旧数据产生噪声 */
        i2s_zero_dma_buffer(I2S_NUM);
        if (bytes_write > 0) {
            ESP_LOGI(TAG, "[music] i2s music played, %d bytes are written.", bytes_write);
        } else {
            ESP_LOGE(TAG, "[music] i2s music play falied.");
            abort();
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
    vTaskDelete(NULL);
}
ESP32-IDF开发环境下进行I2C总线设备地址扫描的实例,我们可以使用ESP-IDF提供的API函数来实现。 首先,我们需要在代码中包含头文件"driver/i2c.h"来获取I2C相关函数的声明。 接下来,我们需要初始化I2C总线。可以使用函数"i2c_config_t"来定义I2C总线的配置参数,包括总线号、SCL引脚、SDA引脚、时钟频率等。然后,我们可以调用函数"i2c_param_config"进行参数配置,并通过函数"i2c_driver_install"来安装I2C驱动程序。 一旦I2C总线初始化完成,我们就可以开始扫描I2C设备的地址了。我们可以使用函数"i2c_scan"来实现扫描。该函数接受一个包含所有扫描地址的数组作为参数。 下面是一个示例代码: ``` #include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/i2c.h" #define I2C_MASTER_NUM I2C_NUM_0 // I2C总线号 #define I2C_MASTER_SCL_IO 19 // SCL引脚 #define I2C_MASTER_SDA_IO 18 // SDA引脚 #define I2C_MASTER_FREQ_HZ 100000 // I2C总线时钟频率 void i2c_scan_task(void *arg) { i2c_config_t conf; conf.mode = I2C_MODE_MASTER; conf.sda_io_num = I2C_MASTER_SDA_IO; conf.sda_pullup_en = GPIO_PULLUP_ENABLE; conf.scl_io_num = I2C_MASTER_SCL_IO; conf.scl_pullup_en = GPIO_PULLUP_ENABLE; conf.master.clk_speed = I2C_MASTER_FREQ_HZ; i2c_param_config(I2C_MASTER_NUM, &conf); i2c_driver_install(I2C_MASTER_NUM, I2C_MODE_MASTER, 0, 0, 0); uint8_t scan_addr[128]; i2c_scan(I2C_MASTER_NUM, scan_addr); printf("I2C devices found:\n"); for (int i = 0; i < 128; i++) { if (scan_addr[i] != 0) { printf("- Address: 0x%.2X\n", scan_addr[i]); } } vTaskDelete(NULL); } void app_main() { xTaskCreate(i2c_scan_task, "i2c_scan_task", 2048, NULL, 10, NULL); } ``` 以上代码实现了一个名为"i2c_scan_task"的任务,它首先配置了I2C总线的参数,然后安装I2C驱动程序。接着,它创建了一个包含128个元素的数组,用于存储扫描到的I2C设备地址。最后,它遍历该数组并打印出非零的地址,即已扫描到的I2C设备地址。 通过运行以上代码,我们就可以在终端看到已连接到I2C总线上的设备地址列表。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值