学习使用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在STM32中不适用,不过在ESP32中用到比较多。ESP32无论是基于ESP-IDF架构的工程还是基于arduino的工程,使用的I2S驱动头文件都是<driver/i2s.h> 驱动的架构: 主要使用的是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; int bck_io_num; int ws_io_num; int data_out_num; int data_in_num; } i2s_pin_config_t ; ...i2s_driver_config_t ; i2s_port_t ;
一些最基础的函数:
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) ;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) ;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) ;esp_err_t i2s_stop (i2s_port_t i2s_num) ;esp_err_t i2s_start (i2s_port_t i2s_num) ;
虽然官方的说明文档给到的应用层是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 ; 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 , .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 ); i2s_set_pin (i2s_num, &pin_config); i2s_set_sample_rates (i2s_num, 22050 ); i2s_driver_uninstall (i2s_num);
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); 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); 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 (); 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 ); 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 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 );audio_pipeline_register (pipeline, mp3_decoder, "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); 由于之前绑定了mp3解码器和i2s writer,run会触发绑定元素的事件:解码并写入。 ... audio_element_run (el_item->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" #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) #define FLASH_SECTOR_SIZE (0x1000) #define FLASH_ADDR (0x200000) void example_erase_flash (void ) ;void example_disp_buf (uint8_t * buf, int length) ;void example_flashuse_task (void *arg) { 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 ); } example_erase_flash (); int i2s_read_len = EXAMPLE_I2S_READ_LEN; int flash_wr_size = 0 ; size_t bytes_read, bytes_written; uint8_t * flash_write_buff = (uint8_t *) calloc (i2s_read_len, sizeof (char )); while (flash_wr_size < FLASH_RECORD_SIZE) { 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 ; 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) { esp_partition_read (data_partition, rd_offset, flash_read_buff, FLASH_SECTOR_SIZE); } }
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 ; 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 , }; i2s_driver_install (i2s_num, &i2s_config, 0 , NULL ); i2s_set_dac_mode (I2S_DAC_CHANNEL_BOTH_EN); 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);