【调研学习】I2S音频驱动

学习使用IIS芯片驱动音频。
I2S(Inter-IC Sound)总线–集成电路内置音频总线,是由飞利浦半导体公司针对数字音频设备之间的音频数据传输而定制的一种音频总线标准。采用独立的时钟线和数据线,在主设备和从设备之间能够实现同步传输。I2S驱动需要硬件支持,所以之前使用的STM32都没有I2S功能,而ESP32支持音频开发,大多都搭载了I2S接口。
在此记录I2S驱动学习时考虑到的一些问题,并通过调研尝试给出解答。

1.IIS各个引脚接口的作用是什么,如何使用IO口模拟IIS接口-初始化代码长啥样?

IIS总线,最简就是三条线:
BLCK-位时钟,bit clock,BCLK最低不低于采样频率*采样位数声道数
RLCK - 也叫WS,Word Select CLK ,左右声道选择时钟。0-左声道;1-右声道
SD serial data ,如果是MCU to I2Schip,在MCU是Dout,I2S芯片的Din

由MCU向I2S解码芯片传数据并播放,加上GND VCC一共五条线。
除此之外,一些I2S设备(ES8388)还需要主时钟,用于主从设备的同步,或者产生BCK;主时钟一般是MCU的主频,会高很多;如果是由声音采集的设备一般都需要主时钟和另一个Dout脚向MCU返回采集的PCM数据。
常用的数据传输标准由飞利浦标准、左对齐、短PCM格式。Philip FormatRLCK变化后第2个BCLK脉冲处变换声道数据,就是有一位延时。左对齐就是在WS电位变化时改变声道数据。PCM Short Format是每次变换WS都会有一个1周期的脉冲,动一次变换一次左右。
i2s-philipformat

I2S在STM32中不适用,不过在ESP32中用到比较多。ESP32无论是基于ESP-IDF架构的工程还是基于arduino的工程,使用的I2S驱动头文件都是<driver/i2s.h>
驱动的架构:
I2S_structure
主要使用的是APP层接口,移植的话可能需要基于driver层修改。主要还是使用STD模式。

  • i2s.h: The header file of legacy I2S APIs (for apps using legacy driver).
  • i2s_std.h: The header file that provides standard communication mode specific APIs (for apps using new driver with standard mode).
  • i2s_pdm.h: The header file that provides PDM communication mode specific APIs (for apps using new driver with PDM mode).
  • i2s_tdm.h: The header file that provides TDM communication mode specific APIs (for apps using new drivers with TDM mode).

i2s.h中使用的是最常见的总线协议初始化方式:封装好一个结构体,然后给出结构体参数配饰,再允许初始化函数;使用时会有类似于start者或write 函数运行。
引脚配置一般会在initial 结构体内。
在i2s.h 中,相关的配置结构体 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
int mck_io_num; /*!< MCK in out pin. Note that ESP32 supports setting MCK on GPIO0/GPIO1/GPIO3 only*/
int bck_io_num; /*!< BCK in out pin*/
int ws_io_num; /*!< WS in out pin*/
int data_out_num; /*!< DATA out pin*/
int data_in_num; /*!< DATA in pin*/
} i2s_pin_config_t; // 引脚配置

...
i2s_driver_config_t; // 驱动配置 包括I2S的工作模式、采样率、位深、dma配置、数据格式等等。

i2s_port_t;
// 端口号,也就是一个MCU有几个I2S端口;
//sco_caps.h里面的SOC_I2S_NUM 决定最多有几个I2S口 一般是1

一些最基础的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
esp_err_t i2s_set_pin(i2s_port_t i2s_num, const i2s_pin_config_t *pin);

esp_err_t i2s_driver_install(i2s_port_t i2s_num, const i2s_config_t *i2s_config, int queue_size, void *i2s_queue);
//初始化函数。 端口号一般是0;配置结构体先初始化好,然后给出I2S事件队列。

esp_err_t i2s_driver_uninstall(i2s_port_t i2s_num);

esp_err_t i2s_write(i2s_port_t i2s_num, const void *src, size_t size, size_t *bytes_written, TickType_t ticks_to_wait);
esp_err_t i2s_write_expand(i2s_port_t i2s_num, const void *src, size_t size, size_t src_bits, size_t aim_bits, size_t *bytes_written, TickType_t ticks_to_wait);
//主要用前者吧;expand增加数据量而不增加精度,主要是为了让数据格式适配总线配置

esp_err_t i2s_set_sample_rates(i2s_port_t i2s_num, uint32_t rate);
esp_err_t i2s_set_clk(i2s_port_t i2s_num, uint32_t rate, uint32_t bits_cfg, i2s_channel_t ch);
// Similar to i2s_set_sample_rates(), but also sets bit width.

esp_err_t i2s_stop(i2s_port_t i2s_num);
esp_err_t i2s_start(i2s_port_t i2s_num);
/* It is not necessary to call this function after i2s_driver_install(),
* however it is necessary to call it after i2s_stop().
*/

虽然官方的说明文档给到的应用层是i2s.h 也就是包含上面函数的头文件,但是在一些高级的应用中都额外封装了一层,例如 esp-adf中 i2s_stream.h 把I2S和audio_common,audio_pipeline 进行整合,让I2S流适应更多的音频数据传输模式。
在peripherals/i2s 给出的最简化的I2S初始化例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "driver/i2s.h"
#include "freertos/queue.h"

static const int i2s_num = 0; // i2s port number

static const i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_TX,
.sample_rate = 44100,
.bits_per_sample = 16,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB,
.intr_alloc_flags = 0, // default interrupt priority
.dma_buf_count = 8,
.dma_buf_len = 64,
.use_apll = false
};

static const i2s_pin_config_t pin_config = {
.bck_io_num = 26,
.ws_io_num = 25,
.data_out_num = 22,
.data_in_num = I2S_PIN_NO_CHANGE
};

...

i2s_driver_install(i2s_num, &i2s_config, 0, NULL); //install and start i2s driver

i2s_set_pin(i2s_num, &pin_config);

i2s_set_sample_rates(i2s_num, 22050); //set sample rates

i2s_driver_uninstall(i2s_num); //stop & destroy i2s driver

2.IIS Data数据流如何写入,格式有要求吗?

调用上述的 i2s_write 接口。
写入数据:

1
2
3
uint8_t *data_wr = (uint8_t *)malloc(sizeof(uint8_t) * 400);
size_t i2s_bytes_write = 0;
i2s_write(I2S_NUM_0, data_wr, sizeof(uint8_t) * 400, &i2s_bytes_write, 100);

关于data格式,可以是 u8* 也可以是u16* u32* int,因为参数定义的是void
数据传入都是bit流的模式,只要注意与编码 .bits_per_sample 位数对应即可。
已经读过的数据会让&i2s_bytes_write累加。

在各个audio框架中,想要把音源数据换成自己的数据,找到write语句,更改即可。

3.典型的A2DP协议 驱动IIS的流程?

数据流:
[Bluetooth] ---> bt_stream_reader ---> i2s_stream_writer ---> codec_chip ---> speaker
程序:
[ 1 ] Create Bluetooth service 初始化A2DP蓝牙协议

1
2
3
4
5
6
7
8
9
10
#include "esp_hf_client_api.h"
#include "bluetooth_service.h"

bluetooth_service_cfg_t bt_cfg = {
.device_name = "ESP-ADF-AUDIO",
.mode = BLUETOOTH_A2DP_SINK,
};
bluetooth_service_start(&bt_cfg);
esp_hf_client_register_callback(bt_hf_client_cb);
esp_hf_client_init();

[ 2 ] Start codec chip 初始化I2S编码解码芯片

1
2
audio_board_handle_t board_handle = audio_board_init();
audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START);

[ 3 ] Create audio pipeline for playback 构建音频通道 并为pipeline绑定各种功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include "i2s_stream.h"
#include "board.h"
#include "filter_resample.h"
#include "raw_stream.h"

#include "audio_element.h"
#include "audio_pipeline.h"
#include "audio_event_iface.h"
#include "audio_mem.h"


static audio_element_handle_t raw_read, bt_stream_reader, i2s_stream_writer, i2s_stream_reader;
static audio_pipeline_handle_t pipeline_d, pipeline_e;

...
//先初始化俩通道 编码和解码通道
audio_pipeline_cfg_t pipeline_cfg = DEFAULT_AUDIO_PIPELINE_CONFIG();
pipeline_d = audio_pipeline_init(&pipeline_cfg);
pipeline_e = audio_pipeline_init(&pipeline_cfg);

//构建i2sstream to write data 并read data
i2s_stream_cfg_t i2s_cfg1 = I2S_STREAM_CFG_DEFAULT();
i2s_cfg1.type = AUDIO_STREAM_WRITER;
i2s_stream_writer = i2s_stream_init(&i2s_cfg1);
i2s_stream_cfg_t i2s_cfg2 = I2S_STREAM_CFG_DEFAULT();
i2s_cfg2.type = AUDIO_STREAM_READER;
i2s_stream_reader = i2s_stream_init(&i2s_cfg2);
//audio 板使用的编解码芯片ES8388不仅有播放,还有从数字mic录音的功能 所以这里初始化了reader

//配置从蓝牙通道传过来的数据流
raw_stream_cfg_t raw_cfg = RAW_STREAM_CFG_DEFAULT();
raw_cfg.type = AUDIO_STREAM_READER;
raw_read = raw_stream_init(&raw_cfg);
bt_stream_reader = bluetooth_service_create_stream();

//注册信息 到音频通道pipeline 并建立链接
audio_pipeline_register(pipeline_d, bt_stream_reader, "bt");
audio_pipeline_register(pipeline_d, i2s_stream_writer, "i2s_w");
audio_pipeline_register(pipeline_e, i2s_stream_reader, "i2s_r");
audio_pipeline_register(pipeline_e, raw_read, "raw");

const char *link_d[2] = {"bt", "i2s_w"};
audio_pipeline_link(pipeline_d, &link_d[0], 2);
const char *link_e[2] = {"i2s_r", "raw"};
audio_pipeline_link(pipeline_e, &link_e[0], 2);

//初始化外设 包括audio板的按键外设 和 Bluetooth音频输入外设
esp_periph_config_t periph_cfg = DEFAULT_ESP_PERIPH_SET_CONFIG();
esp_periph_set_handle_t set = esp_periph_set_init(&periph_cfg);
audio_board_key_init(set);
esp_periph_handle_t bt_periph = bluetooth_service_create_periph();
esp_periph_start(set, bt_periph);

[ 4 ] 根据各种事件进行事件触发

1
2
3
4
5
6
7
8
9
//根据相应的指令 播放or暂停事件
if (---)
periph_bluetooth_play(bt_periph);
else if (---)
periph_bluetooth_pause(bt_periph);
else if (---)
periph_bluetooth_next(bt_periph);
else if (---)
periph_bluetooth_prev(bt_periph);

[ 5 ] 任务链最后就是释放内存
1
2
3
4
5
6
7
8
9
10
    //结束任务 释放内存
audio_pipeline_stop(pipeline_d);
... 各种stop
audio_pipeline_unregister(pipeline_d, bt_stream_reader);
... 各种unregister
audio_pipeline_remove_listener(pipeline_d);
...
audio_pipeline_deinit(pipeline_d);
...
bluetooth_service_destroy();

4.编码解码的控制?目前有 mp3 aac 的编解码。

可以找到相应的解码头文件。 aac、flac(Free Lossless Audio Codec -free无损压缩)、mp3。
在ESP32中配置相应的解码器,可以将收到的数据解码:

1
2
3
4
5
6
7
8
9
10
11
12
#include "mp3_decoder.h"

ESP_LOGI(TAG, "Create mp3 decoder to decode mp3 file.");
mp3_decoder_cfg_t mp3_cfg = DEFAULT_MP3_DECODER_CONFIG();
mp3_decoder = mp3_decoder_init(&mp3_cfg);

audio_element_set_read_cb(mp3_decoder, mp3_music_read_cb, NULL);
//mp3_music_read_cb 在解码完成后的回调函数 继续解码下一段数据
//这里的回调函数决定了从哪里读mp3原始二进制数据 一般是file

audio_pipeline_register(pipeline, mp3_decoder, "mp3");
//需要在音频通道中注册mp3解码器

传输时可以把数据压缩后,在ESP32中调用相应的解码器解码,再播放。
上面的例子封装程度比较高,在 配置完audio_pipeline 的解码器、I2S输出通道后,run时会自动写入I2S数据流,所以没有解码后的数据写入语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "audio_element.h"
#include "audio_pipeline.h"
#include "audio_event_iface.h"
#include "audio_mem.h"
#include "audio_common.h"

audio_pipeline_run(pipeline);
/**
* @brief Start Audio Pipeline.
*
* With this function audio_pipeline will create tasks for all elements,
* that have been linked using the linking functions.
*
* @param[in] pipeline The Audio Pipeline Handle
*
*/
由于之前绑定了mp3解码器和i2s writer,run会触发绑定元素的事件:解码并写入。
//---各种错误检查 前置判断 如果正确的话,就会执行:
...
audio_element_run(el_item->el);
...
//---其中el_item 是输入Pipeline绑定的元素,el是对应的事件

5.DMA IIS怎么使用,对于SRAM 是必需吗,不用SRAM会有何限制?流畅播放的典型缓存有多大?

不需要额外的命令,I2S驱动会自动调用DMA通道。write、read 命令在I2S总线的TX RX通道都有对应的DMA通道,两者独立。
I2S的数据传输:
I2S外设的所有数据传输都是通过DMA实现的。当发送或接收的数据达到一个 DMA 缓冲区的大小时,将触发 I2S_OUT_EOF 或 I2S_IN_SUC_EOF 中断。
具体的触发程序、回调函数没有说明的在哪一层,但是原理是收数据-到一定量-传到I2S数据缓存区。

SRAM不是必须的,可以用flash存。虽然flash的读写速度不如SRAM,但是也足够音频播放的数据读写了。
使用时,音频数据类似于一个环形链表,“环形”地读写新的数据。
注意程序设计时不要把数据链表读到内存,会放不下。之前写的程序把80k的缓存数据都放在内存,RAM基本就满了,不是合适的设计。可以放在flash内。某个例程中flash内数据表的大小约为60KB。
4MB ESP32的flash分区默认的是前1MB存运行程序,后3MB存数据,为了保护安全程序中不会修改固件分区的flash内容。

具体的flash操作API使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "esp_partition.h"

#define PARTITION_NAME "storage"
//flash record size, for recording 5 seconds' data
#define FLASH_RECORD_SIZE (EXAMPLE_I2S_CHANNEL_NUM * EXAMPLE_I2S_SAMPLE_RATE * EXAMPLE_I2S_SAMPLE_BITS / 8 * 5)
#define FLASH_ERASE_SIZE (FLASH_RECORD_SIZE % FLASH_SECTOR_SIZE == 0) ? FLASH_RECORD_SIZE : FLASH_RECORD_SIZE + (FLASH_SECTOR_SIZE - FLASH_RECORD_SIZE % FLASH_SECTOR_SIZE)
//sector size of flash
#define FLASH_SECTOR_SIZE (0x1000)
//flash read / write address
#define FLASH_ADDR (0x200000)


/*
* @brief erase flash for recording
*/
void example_erase_flash(void);

/**
* @brief debug buffer data
*/
void example_disp_buf(uint8_t* buf, int length);

//具体使用时的程序:
void example_flashuse_task(void*arg)
{
//0.验证flash空间
const esp_partition_t *data_partition = NULL;
data_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
ESP_PARTITION_SUBTYPE_DATA_FAT, PARTITION_NAME);
if (data_partition != NULL) {
printf("partiton addr: 0x%08x; size: %d; label: %s\n", data_partition->address, data_partition->size, data_partition->label);
} else {
ESP_LOGE(TAG, "Partition error: can't find partition name: %s\n", PARTITION_NAME);
vTaskDelete(NULL);
}
//1. Erase flash
example_erase_flash();
int i2s_read_len = EXAMPLE_I2S_READ_LEN;
int flash_wr_size = 0;
size_t bytes_read, bytes_written;

//2. write data to flash
uint8_t* flash_write_buff = (uint8_t*) calloc(i2s_read_len, sizeof(char));
while (flash_wr_size < FLASH_RECORD_SIZE) {

//每次写入的数据flash_write_buff 长度write_buff_len data_partition记录分区和flash_wr_size分区后多少bytes开始写入
//在此修改flash_write_buff的值

esp_partition_write(data_partition, flash_wr_size, flash_write_buff, write_buff_len);
flash_wr_size += write_buff_len;
}
free(flash_write_buff);
flash_write_buff = NULL;

//3. Read flash
uint8_t* flash_read_buff = (uint8_t*) calloc(i2s_read_len, sizeof(char));

for (int rd_offset = 0; rd_offset < flash_wr_size; rd_offset += FLASH_SECTOR_SIZE) {
//read I2S(ADC) original data from flash
esp_partition_read(data_partition, rd_offset, flash_read_buff, FLASH_SECTOR_SIZE);

//在此对读出的FLASH_SECTOR_SIZE 长度的数据flash_read_buff进行操作
}

}

6.比起IIC-DAC 或者直接DAC的方案,IIS的优势在哪?位深 频率 or数据处理逻辑?

目前所接触到的音频输出方式有三种:
1.MCU内置DAC通道输出
2.外置I2C-DAC 模块输出
3.外置I2S 解码 模块输出

内置DAC I2C-DAC I2S
外接芯片 N Y Y
频率 >48K 高速模式可勉强>48K >48K
位深 8~12bit 8~24bit
高位DAC芯片很贵
16~32bit
24bit标配
数据格式 PCM-DAC PCM-DAC PCM & PDM &DAC
总线通信带宽 ∞ 无片外通信 I2C总线带宽
高速约3.4Mbit/s
32bit-96K
音频够用

相较于I2C协议,I2S协议同样是同步时钟,无需应答,使用两条数据线实现双工通信,无抢占,可以实现更高频的通信。
I2S是专门为数字音频传输设计的,在音频输出时使用更合理。但是其他类型的DAC输出控制可能就不太适用。高质量音频输出还是得I2S解码芯片。
此外,手动写DAC,需要自己配置timer, 周期=1s/fs 。高频时每次都只进行一次DAC操作,占用CPU资源较多。而I2S协议,在ESP32的架构中,配有FIFO寄存器搭配MCLK分频的预设时钟进行通信,时序设计会简单很多。

7.使用ESP32内部ADC/DAC Mode可行吗?如何使用,有无接口?

I2S 输出可以直接路由DAC通道输出(GPIO 25 和 GPIO 26),而不通过外部 I2S 编解码器.
ADC/DAC Mode
ADC and DAC modes only exist on ESP32 and are only supported on I2S0. Actually, they are two sub-modes of LCD/Camera mode. I2S0 can be routed directly to the internal analog-to-digital converter(ADC) and digital-to-analog converter(DAC). In other words, ADC and DAC peripherals can read or write continuously via I2S0 DMA. As they are not an actual communication mode, the I2S driver does not implement them.

使用内部DAC进行模拟输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "driver/i2s.h"
#include "freertos/queue.h"

static const int i2s_num = 0; // i2s port number

/**
* @brief I2S ADC/DAC mode init.
*/
void example_i2s_init(void)
{
int i2s_num = EXAMPLE_I2S_NUM;
i2s_config_t i2s_config = {
.mode = I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN | I2S_MODE_ADC_BUILT_IN,
.sample_rate = EXAMPLE_I2S_SAMPLE_RATE,
.bits_per_sample = EXAMPLE_I2S_SAMPLE_BITS,
.communication_format = I2S_COMM_FORMAT_STAND_MSB,
.channel_format = EXAMPLE_I2S_FORMAT,
.intr_alloc_flags = 0,
.dma_buf_count = 2,
.dma_buf_len = 1024,
.use_apll = 1,
};
//install and start i2s driver
i2s_driver_install(i2s_num, &i2s_config, 0, NULL);
//init DAC pad
i2s_set_dac_mode(I2S_DAC_CHANNEL_BOTH_EN);
//init ADC pad
i2s_set_adc_mode(I2S_ADC_UNIT, I2S_ADC_CHANNEL);
}

设置initial后,不用设置I2Spin,配置DAC通道使能即可。
写入依然是:

1
2
3
uint8_t* i2s_write_buff = (uint8_t*) calloc(i2s_read_len, sizeof(char));

i2s_write(EXAMPLE_I2S_NUM, i2s_write_buff, FLASH_SECTOR_SIZE, &bytes_written, portMAX_DELAY);

【调研学习】I2S音频驱动
http://example.com/2022/09/15/【调研学习】I2S音频驱动/
作者
Chery Young
发布于
2022年9月15日
许可协议