【IOT开发】ESP-IDF&ADF 音频开发

ESP32 的音频应用开发框架。主要了解 ADF 的架构、使用、以及注意事项。

适用 MCU:ESP32, ESP32-S2, and ESP32-S3

ESP-ADF 是 IDF(Espressif IoT Development Framework)在音频应用方面的一系列扩展组件。所以得先搭好 IDF 环境,再搭建 ADF 环境。

ADF 框架:

几个重要的 element:streams,codecs,audio processing。这是在构建音频应用时主要考虑的功能元素。而一个功能应用需要一个 pipeline 把各个元素串起来,上图就是把 MP3decoder 和 I2S stream 结合,实现一个从 mp3 文件读数据并播放的功能。

The basic building block for the application programmer developing with ADF is the audio element object. 都被封装成了对象,对象会提供对应的 API。

元素的一般功能是接受一些数据的输入,对其进行处理,并输出到下一个程序。每个元素都能够单独运行。为了能够控制数据生命周期的特定阶段,从输入、处理到输出的过程中,element 对象提供了在每个阶段触发回调的接口。

例如一个蓝牙耳机的程序,会用到 bt_stream,从 A2DP 协议读数据;再使用 I2S_STREAM 把数据输给解码芯片:

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
...
audio_element_handle_t bt_stream_reader, i2s_stream_writer;
...
//配置a2dp_config 用以初始化 bt_stream_reader element
a2dp_stream_config_t a2dp_config = {
.type = AUDIO_STREAM_READER,
.user_callback = {0},
};
bt_stream_reader = a2dp_stream_init(&a2dp_config);

...//然后调用element接口,把从BT来的A2DP数据给element data
audio_pipeline_register(pipeline, bt_stream_reader, "bt");

//之后再把 bt stream 元素作为audioinfo输入
audio_element_getinfo(bt_stream_reader, &music_info);

//i2s_stream_writer 元素也是类似的流程
i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_DEFAULT();
i2s_cfg.type = AUDIO_STREAM_WRITER;
i2s_stream_writer = i2s_stream_init(&i2s_cfg);

audio_pipeline_register(pipeline, i2s_stream_writer, "i2s");
i2s_stream_set_clk(i2s_stream_writer, 48000, 16, 2);
audio_element_setinfo(i2s_stream_writer, &music_info);

过程中会有一个步骤: audio_pipeline_register,把元素绑定到 pipeline 上。pipeline 上的元素是动态的,例如上面的例程就没有 decoder 元素。

之所以叫做 pipeline,管道内流通的是 data,而把元素 register 上去,类似于提供 data 水流的出入口。

在注册好各种元素后,就可以 audio_pipeline_run(pipeline),让数据流起来,输出端也就能 work 了。此外,pipeline 还提供了 pause,resume,stop 等接口控制播放。

使用 ADF 最重要就是要有数据流的概念。

数据流支持的来源有:

只需要关注各个流的初始化方法和读写方法。

编解码器

编码器,把采集的 PCM 编码,ADF 只支持 AMR 和 WAV 编码。

支持 AAC AMR FLAC MP3 OGG OPUS WAV 解码。

这个也封装的很好,使用解码器时候只要调用相应的初始化函数,生成 element,再绑定到 pipeline 上就好了。

audio 操作

可以对音频流进行一些额外操作,例如混音、均衡器、更改采样通道、

例如配置混音,需要先建立两个 pipeline,然后作为 downmix 的输入

服务

服务 (service) 是具体产品功能在软件层面上的实现,如输入按键、网络配置管理、电池检测等功能。例如,想要在做的蓝牙耳机上加按键 调整音量、切歌的功能,就得使用蓝牙 service 配置。更具体的是 Bluetooth service 中的音视频远程控制规范 (Audio Video Remote Control Profile, AVRCP)。

在 ADF 框架中 AVRCP 是和 A2DP stream 封装在一起的。触发相应的服务控制,直接调用 api 即可。当然也可调用 bluetooth_service.h 中的 api 去实现 AVRCP 服务。

在实际应用中需要一定的外部事件来触发 bt service,例如 input_key_service.h 中关于按键输入的服务,其实就是把 GPIO 电位中断 ADC 时间中断等封装了一下,做成 periph 外设的功能服务。

工程设计:

使用 ESP-ADF 开发音频应用工程,大致的思路是:紧跟音频数据流。

数据输入可以从麦克风、本地存储、wifi、bt、I2S 输入、flash;数据输出也可以是以上通道。

如果可接受 8 位的数据输出,我们可使用两个板载 DAC 实现;如果需要更好的音频质量和更多的接口选项,可使用外部 I2S 编解码器来完成所有模拟输入和输出信号的处理。

I2S 是音频编解码器芯片接口的行业标准,通常用于高速、连续传输音频数据。为了优化音频数据处理的性能,可能需要额外的内存。对于这种情况,请考虑使用 8 MB PSRAM 。开发板与软件之间的交互由音频 HAL 和驱动程序完成。

不同的编码格式-WAV, MP3 or FLAC 和采样率位深需要不同的内存需求。

可以使用 SPI RAM 作为扩展,需要在组件配置 >SPI RAM 配置下的 menucofig 中启用它。

同时:Bluetooth and Wi-Fi can not coexist without PSRAM because it will not leave enough memory for an audio application.

UART 数据流作为输入 pipeline element

参考 element 的结构体 构建自定义 PCM 数据输入流的 element
还在开发中。

esp-idf_component搭建

ESP-IDF搭建自己的工程组件,需要了解sdkconfig文件、cmakelist文件的配置,系统驱动的过程。
具体见官方文档

系统启动

本文将会介绍 ESP32 从上电到运行 app_main 函数中间所经历的步骤(即启动流程)。

宏观上,该启动流程可以分为如下 3 个步骤:

  1. 一级引导程序 被固化在了 ESP32 内部的 ROM 中,它会从 flash 的 0x1000 偏移地址处加载二级引导程序至 RAM (IRAM & DRAM) 中。
  2. 二级引导程序 从 flash 中加载分区表和主程序镜像至内存中,主程序中包含了 RAM 段和通过 flash 高速缓存映射的只读段。
  3. 应用程序启动阶段 运行,这时第二个 CPU 和 RTOS 的调度器启动。

硬件和基本 C 语言运行环境的端口初始化。

软件服务和 FreeRTOS 的系统初始化。

运行主任务并调用 app_main

与普通的 FreeRTOS 任务(或嵌入式 C 的 main 函数)不同,app_main 任务可以返回。如果 app_main 函数返回,那么主任务将会被删除。系统将继续运行其他的 RTOS 任务。因此可以将 app_main 实现为一个创建其他应用任务然后返回的函数,或主应用任务本身。

OTA:
OTA(空中升级)更新可以在现场烧录新的应用程序,但不能烧录一个新的引导加载程序。因此,引导加载程序支持引导从 ESP-IDF 新版本中构建的应用程序。之后会尝试OTA功能进行物联网应用升级。

component 搭建

ESP-IDF 可以显式地指定和配置每个组件。在构建项目的时候,构建系统会前往 ESP-IDF 目录、项目目录和用户自定义组件目录(可选)中查找所有组件,允许用户通过文本菜单系统配置 ESP-IDF 项目中用到的每个组件。在所有组件配置结束后,构建系统开始编译整个项目。

  • 项目 特指一个目录,其中包含了构建可执行应用程序所需的全部文件和配置,以及其他支持型文件,例如分区表、数据/文件系统分区和引导程序。
  • 项目配置 保存在项目根目录下名为 sdkconfig 的文件中,可以通过 idf.py menuconfig 进行修改,且一个项目只能包含一个项目配置。
  • 应用程序 是由 ESP-IDF 构建得到的可执行文件。一个项目通常会构建两个应用程序:项目应用程序(可执行的主文件,即用户自定义的固件)和引导程序(启动并初始化项目应用程序)。
  • 组件 是模块化且独立的代码,会被编译成静态库(.a 文件)并链接到应用程序。部分组件由 ESP-IDF 官方提供,其他组件则来源于其它开源项目。
  • 目标 特指运行构建后应用程序的硬件设备。运行 idf.py –list-targets 可以查看当前 ESP-IDF 版本中支持目标的完整列表。

!!!!!!一下午的 debug 新添加 component 后,要删除原有的 build rebuild,不然已有的 build 目录会找不到 cmakelist 文件。 。。 一整个无语。 component 以及 main 里面的 Kconfig.projbuild ESP 配置目录更改,也需要重编译才会出现在 configuration UI 里面。

ESP-IDF 在搜索所有待构建的组件时,会按照 COMPONENT_DIRS 指定的顺序依次进行,这意味着在默认情况下,首先搜索 ESP-IDF 内部组件(IDF_PATH/components),然后是 EXTRA_COMPONENT_DIRS 中的组件,最后是项目组件(PROJECT_DIR/components)。如果这些目录中的两个或者多个包含具有相同名字的组件,则使用搜索到的最后一个位置的组件。这就允许将组件复制到项目目录中再修改以覆盖 ESP-IDF 组件,如果使用这种方式,ESP-IDF 目录本身可以保持不变。

每个组件都可以包含一个 Kconfig 文件,和 CMakeLists.txt 放在同一目录下。Kconfig 文件中包含要添加到该组件配置菜单中的一些配置设置信息。

运行 menuconfig 时,可以在 Component Settings 菜单栏下找到这些设置。

创建一个组件的 Kconfig 文件,最简单的方法就是使用 ESP-IDF 中现有的 Kconfig 文件作为模板,在这基础上进行修改。

有关示例请参阅 添加条件配置

KConfig.projbuildproject_include.cmake 类似,也可以为组件定义一个 KConfig 文件以实现全局的 组件配置。如果要在 menuconfig 的顶层添加配置选项,而不是在 “Component Configuration” 子菜单中,则可以在 CMakeLists.txt 文件所在目录的 KConfig.projbuild 文件中定义这些选项。

经典的组件 kconfig 和 cmakelist 配置:

在设置 UI 中启用了组件,才会把相应的文件选入 cmake 编译 list

1
2
3
4
5
config FOO_ENABLE_BAR
bool "Enable the BAR feature."
help
This enables the BAR feature of the FOO component.

1
2
3
4
5
6
7
8
9
 set(srcs "foo.c" "more_foo.c")

if(CONFIG_FOO_ENABLE_BAR)
list(APPEND srcs "bar.c")
endif()

idf_component_register(SRCS "${srcs}"
...)

嵌入二进制数据

//嵌入二进制文件-PCM MP3

有时组件中希望使用一个二进制文件或者文本文件,但是您又不希望将它们重新格式化为 C 源文件。(例如提示音.mp3)

这时,可以在组件注册中指定 EMBED_FILES 参数,用空格分隔要嵌入的文件名称:

idf_component_register(… EMBED_FILES server_root_cert.der)

或者,如果文件是字符串,则可以使用 EMBED_TXTFILES 变量,把文件的内容转成以 null 结尾的字符串嵌入:

idf_component_register(… EMBED_TXTFILES server_root_cert.pem)

文件的内容会被添加到 Flash 的 .rodata 段,用户可以通过符号名来访问,如下所示:

extern const uint8_t server_root_cert_pem_start[] asm(“_binary_server_root_cert_pem_start”);extern const uint8_t server_root_cert_pem_end[] asm(“_binary_server_root_cert_pem_end”);

符号名会根据文件全名生成,如 EMBED_FILES 中所示,字符 /. 等都会被下划线替代。符号名称中的 _binary 前缀由 objcopy 命令添加,对文本文件和二进制文件都是如此。

A2DP 自动连接

ESP32官方例程,A2DP无法做到开机即连。这在蓝牙而集中是一个“理所当然”的功能。

解决方案

查找相关例程、issue,没有找到类似的问题。

查看官方文档,在 A2DP 协议中找到了相应的 API:

esp_err_t**esp_a2d_sink_connect(esp_bd_addr_t remote_bda)**

Connect to remote bluetooth A2DP source device. This API must be called after esp_a2d_sink_init() and before esp_a2d_sink_deinit().

Return

  • ESP_OK: connect request is sent to lower layer successfully
  • ESP_INVALID_STATE: if bluetooth stack is not yet enabled
  • ESP_FAIL: others

查看 PC 的地址:

f0:9e:4a:80:5c:db

在初始化时调用 API,可以在开机时实现自动连接。

修改 APP 功能,使其能自动连接设备。现在需要知道上一次连接设备的地址,并自动连接最近所连接的设备。

地址 structure 以队列存储,存储上限为 6 。自动连接 list[0] 的地址,如果连接错误会连 list[1]。以此类推。

地址记忆实现

第一步需要获取所连接设备的 MAC 地址

找到可以给出所连接设备地址的结构体:

A2DP 协议, esp_a2d_cb_param_t * a2d ,一个回调函数指针的成员:a2d->conn_stat.remote_bda

1
2
3
4
5
6
7
8
9
/**
* @brief ESP_A2D_CONNECTION_STATE_EVT
*/
struct a2d_conn_stat_param {
esp_a2d_connection_state_t state; /*!< one of values from esp_a2d_connection_state_t */
esp_bd_addr_t remote_bda; /*!< remote bluetooth device address */
esp_a2d_disc_rsn_t disc_rsn; /*!< reason of disconnection for "DISCONNECTED" */
} conn_stat;

尝试在 APP 层修改代码,不修改 firmware 代码,以保证程序的可移植性。但是能给出地址的指针都在驱动车封装,相应的对象都做了 static 修饰,无法在 c 文件外部调用。

实在没有找到调用某个接口得 remote_bda 的方法,最后选择修改官方的底层代码。

最后修改了 ADF 的 a2dp_stream 驱动代码:

…\esp\esp-adf\components\bluetooth_service\a2dp_stream.c

添加:

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
66
67
68
69
70
71
72
73
74
75
76
77
78

char my_remote_bd_addr[6];
static uint8_t remote_add_index = 0;
static const char* remote_key_id[6] = { "ra_key1", "ra_key2", "ra_key3", "ra_key4", "ra_key5", "ra_key6" };

static void nvs_write_add_to_flash( )
{
nvs_handle handle;
static const char *NVS_CUSTOMER = "a2dp_add_data";
static const char *KEY = "key";
char *INDEX_KEY = remote_key_id[remote_add_index] ;

ESP_ERROR_CHECK( nvs_open( NVS_CUSTOMER, NVS_READWRITE, &handle) );
ESP_ERROR_CHECK( nvs_set_blob( handle, INDEX_KEY, &my_remote_bd_addr, sizeof(my_remote_bd_addr)) );
ESP_ERROR_CHECK( nvs_set_i8( handle, KEY, remote_add_index) );
ESP_ERROR_CHECK( nvs_commit(handle) );
nvs_close(handle);

}

void nvs_read_add_from_flash(void)
{
nvs_handle handle;
static const char *NVS_CUSTOMER = "a2dp_add_data";
static const char *KEY = "key";
uint32_t str_length = 32;
int8_t value;

ESP_ERROR_CHECK( nvs_open(NVS_CUSTOMER, NVS_READWRITE, &handle) );
ESP_ERROR_CHECK ( nvs_get_i8(handle, KEY, &value) );
remote_add_index=value;

char *INDEX_KEY = remote_key_id[remote_add_index] ;
ESP_ERROR_CHECK ( nvs_get_blob(handle, INDEX_KEY, &my_remote_bd_addr, &str_length) );
char *bda = my_remote_bd_addr;

// ESP_LOGI(TAG, "[add_index:]%d\r\n" ,remote_add_index);
// ESP_LOGI(TAG, "[add:] [%02x:%02x:%02x:%02x:%02x:%02x]",
// bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);

nvs_close(handle);
}

static void my_a2d_sink_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t *param)
{
if(event == ESP_A2D_CONNECTION_STATE_EVT && param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED)
{
ESP_LOGI(TAG, "my_a2d_sink_cb is run!\n" );
nvs_read_add_from_flash(); //读取index值,然后再读取地址值

esp_a2d_cb_param_t *a2d = (esp_a2d_cb_param_t *)(param);
uint8_t * addr = a2d->conn_stat.remote_bda;
for(int i=0;i<6;i++) my_remote_bd_addr[i]=addr[i];
remote_add_index++; if(remote_add_index == 6) remote_add_index =0 ; //更新 index 状态,在0~6循环
nvs_write_add_to_flash(); // 把index值和 add值 写入对应的地址
}
}

esp_bd_addr_t* get_remote_ba_addr_last( void )
{
nvs_read_add_from_flash(); //读取index值,然后再读取地址值
return &my_remote_bd_addr;
}

esp_bd_addr_t* get_remote_ba_addr_before(uint8_t before_index)
{
uint8_t index = ( remote_add_index +6 - before_index ) %6;
char *INDEX_KEY = remote_key_id[index] ;

nvs_handle handle;
static const char *NVS_CUSTOMER = "a2dp_add_data";
uint32_t str_length = 32;
ESP_ERROR_CHECK( nvs_open(NVS_CUSTOMER, NVS_READWRITE, &handle) );
ESP_ERROR_CHECK ( nvs_get_blob(handle, INDEX_KEY, &my_remote_bd_addr, &str_length) );
nvs_close(handle);
return &my_remote_bd_addr;
}

修改回调函数:

1
2
3
4
5
6
7
8
9
10
static void bt_a2d_sink_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t *param)
{
...

switch (event) {
case ESP_A2D_CONNECTION_STATE_EVT:
my_a2d_sink_cb(event, param); // the callback added to change the bd_add.
...
}

对应的 a2dp_stream.h 文件添加以下接口的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief return the mac address of the last connected device.
*
*/
esp_bd_addr_t* get_remote_ba_addr_last( void );

/**
* @brief return the mac address of remembered devices. The maximum number of devices is 6.
*
* @param[in] before_index The before_index of the address.
* For example, if the last index is 5 and the before_index is 1, it will return the address of index-4.
*/
esp_bd_addr_t* get_remote_ba_addr_before(uint8_t before_index);

经测试,用多台手机和 PC 分别连接 ESP-HAP,可以正确记忆地址并存储。可以掉电记忆。

自动连接实现

在 APP 层的对应任务函数中添加 “读取之前所连接的地址,然后连接对应地址” 的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint8_t *bda = *get_remote_ba_addr_last();
uint8_t add_index_before = 0;
ESP_LOGI(TAG, "a2dp last-connected bd address:, [%02x:%02x:%02x:%02x:%02x:%02x]",
bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);

while(1)
{
for(int i=0;i<6;i++) my_device_bd_addr[i]=bda[i];
esp_err_t err = esp_a2d_sink_connect(my_device_bd_addr);

if(err == ESP_OK) break;
else {
add_index_before++;
bda = *get_remote_ba_addr_before(add_index_before);
ESP_LOGI(TAG, "a2dp reconnect failed");
vTaskDelay(200);
}

}

现在开机后会自动找以前连接过的设备,直到连接成功。

其他可能的方法

连接所记忆的地址,在蓝牙协议中应该是一个“理所当然的”事情,自己想方设法用 flash 存储地址,再主动调接口连接就显得很不优雅。连接是 GATT 协议层的事情,而手机和 PC 明显有记录之前连接过设备的地址和服务。

https://github.com/espressif/esp-idf/issues/10107

“The ESP32 HID device set the SDP attribute HIDReconnectInitiate to True, it indicates that the HID device will be primarily responsible for connection re-establishment.”

https://www.esp32.com/viewtopic.php?t=11468#p46410

“It seems this is not done in the ESP firmware, it is done in the phone application code instead. I’m not sure if this is the correct way to do it or if there are more optimal ways, but here’s what I did. In your Android phone application, in the line of code that starts the GATT connection from the Android side (Android Studio or whatever you’re using) set it to auto-connect.”

windows 是有蓝牙自动连接设置的,一些业余的解决蓝牙自动连接的答案也都是依靠 PC 端来做的:

而之前在 ESP32 跑的一些蓝牙例程,例如 SPP BLE_SPP HID THROUGHPUT 都是需要“手动点一下连接”

如果能修改类似 HIDReconnectInitiate 的参数,让出重建连接任务的控制权,是否可以不用改记忆地址、调用连接接口这些步骤,然 PC Windows 来负责 reconnect.

目前还没找到 A2DP 类似的宏定义。

BUG 记录:

falsh 读写报错:

ESP_ERROR_CHECK failed: esp_err_t 0x110c (ESP_ERR_NVS_INVALID_LENGTH) at 0x40091a80

0x40091a80: _esp_error_check_failed at D:/Users/chery/esp/Espressif/frameworks/esp-idf-v4.4.3/components/esp_system/esp_err.c:42

func: nvs_read_add_from_flash

expression: nvs_get_str(handle, INDEX_KEY, my_remote_bd_addr, &str_length)

ESP_ERR_NVS_INVALID_LENGTH 错误是读取的空间不够。很玄学,&str_length 照大了给,可能是类似于 str 要读取末尾的/0。https://www.esp32.com/viewtopic.php?p=36770

最后是用 nvs_get_blob 接口代替了 nvs_get_str

ES8388 驱动 &component

之前使用的 I2S 解码芯片功能比较单一,直接在 I2S 信号引脚输入相应的脉冲量即可工作,通过一些外接引脚的拉高拉低控制数据格式、功放类型等。但是 ES8388 数字化集成较好,有双声道输入输出,集成 I2C 通信模块以及内部控制寄存器,需要通过 I2C 更改内部寄存器的值来调整工作模式。

ES8388 需要 I2C 通信,command 初始化 DAC 状态,而之前用的外置解码器直接输入 I2S 信号就可以工作了。
数据手册

ES8388 支持 SPI 模式和 I2C 模式,通过 CE 引脚来控制。SPI 需要三线,片选信号下降沿有效;在原理图中直接把 CE 拉低,默认 I2C 模式。400 kbps

The first byte transferred is the slave address. It is a seven-bit chip address followed by a RW bit. The chip address must be 001000x, where x equals AD0.

设备地址 0010001/0 AD0,也就是 CE 模拟的。在画图时该引脚拉低,所以地址是 0010000;0x20

首个 bute,读写位 置 0 写,置 1 读,I2C 通信格式。

主要控制的寄存器:

Register 23 ~52 DAC Control,控制DAC输出开启、时钟配置等。

Register 0 ~1 Chip Control,总时钟、电源控制。

具体如何修改寄存器值来指定工作模式,得看 table1

音频输出连接错误

在某给版本的电路图中犯了一个致命的错误:差分输出直接当单端输出接了。

ES8388 的模块图:

这里的音频输出是 mix 差分输出,而在我的原理图中:

因为选用的功放是 NS4105,音频信号单端输入,做法是直接去 OUT1 输出接到功放,然后另一个 OUT2 直接接地。

当时这样想当然的原因是:同时在看功放芯片 NS4105 的数据手册,选择单端输出时直接把一个 OUT 接地,另一个当输出用即可。

现在想想确实有些想当然了,NS4105 是通过某个引脚拉高拉低来确认是单端还是差分输出了。而且也没有 ES8388 模块产品,没有使用开发板 + 模块进行原理验证。

差分信号转单端需要一个减法电路。之后注意qwq

初始化代码

ESP-ADF 中的驱动程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
*/**
* @brief Initialize audio board
*
* @return The audio board handle
*/
*audio_board_handle_t audio_board_init(void);
{
...
board_handle->audio_hal = audio_board_dac_init();
...

}

*/**
* @brief Start/stop codec driver
*
* @param audio_hal reference function pointer for selected audio codec
* @param mode select media hal codec mode either encode/decode/or both to start from audio_hal_codec_mode_t
* @param audio_hal_ctrl select start stop state for specific mode
*
* @return int, 0--success, others--fail
*/
*esp_err_t audio_hal_ctrl_codec(audio_hal_handle_t *audio_hal*, audio_hal_codec_mode_t *mode*, audio_hal_ctrl_t *audio_hal_ctrl*);

初始化 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
30
31
32
    dac_hal = audio_hal_init(&audio_codec_cfg, &AUDIO_CODEC_ES7148_DEFAULT_HANDLE);
i2s_mclk_gpio_select(I2S_NUM_0, GPIO_NUM_0);


*/*
* Operate fuction of PA
*/
*audio_hal_func_t AUDIO_CODEC_TAS5805M_DEFAULT_HANDLE = {
.audio_codec_initialize = tas5805m_init,
.audio_codec_deinitialize = tas5805m_deinit,
.audio_codec_ctrl = tas5805m_ctrl,
.audio_codec_config_iface = tas5805m_conig_iface,
.audio_codec_set_mute = tas5805m_set_mute,
.audio_codec_set_volume = tas5805m_set_volume,
.audio_codec_get_volume = tas5805m_get_volume,
.audio_hal_lock = NULL,
.handle = NULL,
};

#define AUDIO_CODEC_DEFAULT_CONFIG(){ \
.adc_input = AUDIO_HAL_ADC_INPUT_LINE1, \
.dac_output = AUDIO_HAL_DAC_OUTPUT_ALL, \
.codec_mode = AUDIO_HAL_CODEC_MODE_BOTH, \
.i2s_iface = { \
.mode = AUDIO_HAL_MODE_SLAVE, \
.fmt = AUDIO_HAL_I2S_NORMAL, \
.samples = AUDIO_HAL_48K_SAMPLES, \
.bits = AUDIO_HAL_BIT_LENGTH_16BITS, \
}, \
};


f12 追溯不到关于 I2C 写入寄存器的函数。orz

从 ADF 源码修改 ES8388 驱动困难太大

github 上有关于 3S8388 初始化的开源库// https://github.com/maditnerd/es8388

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool ES8388::write_reg(uint8_t slave_add, uint8_t reg_add, uint8_t data)
{
Wire.beginTransmission(slave_add);
Wire.write(reg_add);
Wire.write(data);
return Wire.endTransmission() == 0;
}

bool ES8388::read_reg(uint8_t slave_add, uint8_t reg_add, uint8_t &data)
{
bool retval = false;
Wire.beginTransmission(slave_add);
Wire.write(reg_add);
Wire.endTransmission(false);
Wire.requestFrom((uint16_t)slave_add, (uint8_t)1, true);
if (Wire.available() >= 1)
{
data = Wire.read();
retval = true;
}
return retval;
}

ES8388 初始化:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
bool ES8388::begin(es8388_handle_t *dev*, const es8388_clock_config_t *const *clk_cfg*)
{
bool res = identify(sda, scl, frequency);
char ES8388_ADDR = *dev->addr;*

if (res == true)
{

/* mute DAC during setup, power up all systems, slave mode */
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL3, 0x04);
res &= write_reg(ES8388_ADDR, ES8388_CONTROL2, 0x50);
res &= write_reg(ES8388_ADDR, ES8388_CHIPPOWER, 0x00);
res &= write_reg(ES8388_ADDR, ES8388_MASTERMODE, 0x00);

/* power up DAC and enable LOUT1+2 / ROUT1+2, ADC sample rate = DAC sample rate */
res &= write_reg(ES8388_ADDR, ES8388_DACPOWER, 0x3e);
res &= write_reg(ES8388_ADDR, ES8388_CONTROL1, 0x12);

/* DAC I2S setup: 16 bit word length, I2S format; MCLK / Fs = 256*/
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL1, 0x18);
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL2, 0x02);

/* DAC to output route mixer configuration: ADC MIX TO OUTPUT */
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL16, 0x1B);
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL17, 0x90);
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL20, 0x90);

/* DAC and ADC use same LRCK, enable MCLK input; output resistance setup */
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL21, 0x80);
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL23, 0x00);

/* DAC volume control: 0dB (maximum, unattenuated) */
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL5, 0x00);
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL4, 0x00);

/* power down ADC while configuring; volume: +9dB for both channels */
res &= write_reg(ES8388_ADDR, ES8388_ADCPOWER, 0xff);
res &= write_reg(ES8388_ADDR, ES8388_ADCCONTROL1, 0x88); // +24db

/* select LINPUT2 / RINPUT2 as ADC input; stereo; 16 bit word length, format right-justified, MCLK / Fs = 256 */
res &= write_reg(ES8388_ADDR, ES8388_ADCCONTROL2, 0xf0); // 50
res &= write_reg(ES8388_ADDR, ES8388_ADCCONTROL3, 0x80); // 00
res &= write_reg(ES8388_ADDR, ES8388_ADCCONTROL4, 0x0e);
res &= write_reg(ES8388_ADDR, ES8388_ADCCONTROL5, 0x02);

/* set ADC volume */
res &= write_reg(ES8388_ADDR, ES8388_ADCCONTROL8, 0x20);
res &= write_reg(ES8388_ADDR, ES8388_ADCCONTROL9, 0x20);

/* set LOUT1 / ROUT1 volume: 0dB (unattenuated) */
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL24, 0x1e);
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL25, 0x1e);

/* set LOUT2 / ROUT2 volume: 0dB (unattenuated) */
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL26, 0x1e);
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL27, 0x1e);

/* power up and enable DAC; power up ADC (no MIC bias) */
res &= write_reg(ES8388_ADDR, ES8388_DACPOWER, 0x3c);
res &= write_reg(ES8388_ADDR, ES8388_DACCONTROL3, 0x00);
res &= write_reg(ES8388_ADDR, ES8388_ADCPOWER, 0x00);

/* set up MCLK) */
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0_CLK_OUT1);
WRITE_PERI_REG(PIN_CTRL, 0xFFF0);
}


/**
* @brief Test if device with I2C address for ES8388 is connected to the I2C bus
*
* @param sda which pin to use for I2C SDA
* @param scl which pin to use for I2C SCL
* @param frequency which frequency to use as I2C bus frequency
* @return true device was found
* @return false device was not found
*/
bool ES8388::identify(int sda, int scl, uint32_t frequency)
{
Wire.begin(sda, scl, frequency);
Wire.beginTransmission(ES8388_ADDR);
return Wire.endTransmission() == 0;
}

IDF 中有 ES8311 对应的驱动,根据 arduino 的文件,修改地址、 写入寄存器的地址及值等即可。

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
esp_err_t es8388_init(es8388_handle_t *dev*, const es8388_clock_config_t *const *clk_cfg*)
{
int res = 0;

res = i2c_init();* // ESP32 in master mode
*
char ES8388_ADDR = *dev->addr;*

res |= es_write_reg(ES8388_ADDR, ES8388_DACCONTROL3, 0x04);* // 0x04 mute/0x00 unmute&ramp;DAC unmute and disabled digital volume control soft ramp
/* Chip Control and Power Management */
* res |= es_write_reg(ES8388_ADDR, ES8388_CONTROL2, 0x50);
res |= es_write_reg(ES8388_ADDR, ES8388_CHIPPOWER, 0x00);* //normal all and power up all
*
*// Disable the internal DLL to improve 8K sample rate
* res |= es_write_reg(ES8388_ADDR, 0x35, 0xA0);
res |= es_write_reg(ES8388_ADDR, 0x37, 0xD0);
res |= es_write_reg(ES8388_ADDR, 0x39, 0xD0);

res |= es_write_reg(ES8388_ADDR, ES8388_MASTERMODE, cfg->i2s_iface.mode);* //CODEC IN I2S SLAVE MODE
*
* /* dac */
* res |= es_write_reg(ES8388_ADDR, ES8388_DACPOWER, 0xC0);* //disable DAC and disable Lout/Rout/1/2
* res |= es_write_reg(ES8388_ADDR, ES8388_CONTROL1, 0x12);* //Enfr=0,Play&Record Mode,(0x17-both of mic&paly)
// res |= es_write_reg(ES8388_ADDR, ES8388_CONTROL2, 0); //LPVrefBuf=0,Pdn_ana=0
* res |= es_write_reg(ES8388_ADDR, ES8388_DACCONTROL1, 0x18);*//1a 0x18:16bit iis , 0x00:24
* res |= es_write_reg(ES8388_ADDR, ES8388_DACCONTROL2, 0x02);* //DACFsMode,SINGLE SPEED; DACFsRatio,256
* res |= es_write_reg(ES8388_ADDR, ES8388_DACCONTROL16, 0x00);* // 0x00 audio on LIN1&RIN1, 0x09 LIN2&RIN2
* res |= es_write_reg(ES8388_ADDR, ES8388_DACCONTROL17, 0x90);* // only left DAC to left mixer enable 0db
* res |= es_write_reg(ES8388_ADDR, ES8388_DACCONTROL20, 0x90);* // only right DAC to right mixer enable 0db
* res |= es_write_reg(ES8388_ADDR, ES8388_DACCONTROL21, 0x80);* //set internal ADC and DAC use the same LRCK clock, ADC LRCK as internal LRCK
* res |= es_write_reg(ES8388_ADDR, ES8388_DACCONTROL23, 0x00);* //vroi=0
* res |= es8388_set_adc_dac_volume(ES_MODULE_DAC, 0, 0);* // 0db
*
res |= es_write_reg(ES8388_ADDR, ES8388_DACPOWER, tmp);* //0x3c Enable DAC and Enable Lout/Rout/1/2
/* adc */
* res |= es_write_reg(ES8388_ADDR, ES8388_ADCPOWER, 0xFF);
res |= es_write_reg(ES8388_ADDR, ES8388_ADCCONTROL1, 0xbb);* // MIC Left and Right channel PGA gain
*
res |= es_write_reg(ES8388_ADDR, ES8388_ADCCONTROL2, tmp);* //0x00 LINSEL & RINSEL, LIN1/RIN1 as ADC Input; DSSEL,use one DS Reg11; DSR, LINPUT1-RINPUT1
* res |= es_write_reg(ES8388_ADDR, ES8388_ADCCONTROL3, 0x02);
res |= es_write_reg(ES8388_ADDR, ES8388_ADCCONTROL4, 0x0c);* // 16 Bits length and I2S serial audio data format
* res |= es_write_reg(ES8388_ADDR, ES8388_ADCCONTROL5, 0x02);* //ADCFsMode,singel SPEED,RATIO=256
* *//ALC for Microphone
* res |= es8388_set_adc_dac_volume(ES_MODULE_ADC, 0, 0);* // 0db
* res |= es_write_reg(ES8388_ADDR, ES8388_ADCPOWER, 0x09);* //Power on ADC, Enable LIN&RIN, Power off MICBIAS, set int1lp to low power mode
*
return res;
}


【IOT开发】ESP-IDF&ADF 音频开发
http://example.com/2022/11/17/【IOT开发】ESP-IDF&ADF开发架构/
作者
Chery Young
发布于
2022年11月17日
许可协议