1.硬件介绍
本设计使用ESP32-S3-BOX-Lite 轻量级开发套件,该开发板配备一块 2.4 寸 LCD 显示屏、双麦克风、一个扬声器、两个用于硬件拓展的 Pmod™ 兼容接口、结合三个独立按键,可构建多样的 HMI 人机交互应用。可实现离线语音识别与播报功能。
图1:硬件结构图
1.1按键
此开发套件有三个用户自定义按键,使用ADC方式采集按键信息;一个Boot和一个RST按键。仅需要一个IO就可以管理三个按键。
图2:ADC按键原理图
1.2屏幕显示
搭载了一块2.4寸320*240的LCD屏幕,由spi总线控制。
1.3音频输出
此套件采用es8156方案对音频数据进行解码。
- ES8156 是一颗高性能、低功耗 delta-sigma 音频 DAC, 具有 7 段可调 EQ,集成了一颗耳放,可直接驱动耳机。
- 110 分贝信噪比
- 24 位,8 至 96 kHz 采样频率
- 内置耳机驱动程序
- 高信噪比和共模抑制比
- I2S/PCM 主或从串行数据端口 256/384Fs、USB 12/24 MHz 和其他非标准音频系统时钟
- I2C 接口 7 波段全可调均衡器动态范围压缩回放信号反馈弹出和点击噪声抑制 1.8V 至 3.3V 操作
图3:es8156接线图
图4:运放以及喇叭电路
通过i2s接收音频信号并解码,将dac信号送给运放驱动喇叭播放音频。
1.4主控制器
ESP32-S3 是一款集成 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE) 的 MCU 芯片,支持远距离模式 (Long Range)。ESP32-S3 搭载 Xtensa® 32 位 LX7 双核处理器,主频高达 240 MHz,内置 512 KB SRAM (TCM),具有 45 个可编程 GPIO 管脚和丰富的通信接口。ESP32-S3 支持更大容量的高速 Octal SPI flash 和片外 RAM,支持用户配置数据缓存与指令缓存。
- Xtensa® 32 位 LX7 双核处理器,主频高达 240 MHz
- 内置 512 KB SRAM、384 KB ROM 存储空间,并支持多个外部 SPI、Dual SPI、 Quad SPI、Octal SPI、QPI、OPI flash 和片外 RAM
- 额外增加用于加速神经网络计算和信号处理等工作的向量指令 (vector instructions)
- 45 个可编程 GPIO,支持常用外设接口如 SPI、I2S、I2C、PWM、RMT、ADC、UART、SD/MMC 主机控制器和 TWAITM 控制器等
- 基于 AES-XTS 算法的 Flash 加密和基于 RSA 算法的安全启动,数字签名和 HMAC 模块,“世界控制器 (World Controller)” 模块
2.程序设计
2.1设计思路
参考乐鑫官方提供的例程,使用wifi模组连接wifi后,发送http请求,获取实况天气,并使用cjson对返回的数据进行解析,将解析得到的日期、天气、温度信息经过简单的整理送给esp-tts模块进行播报。乐鑫提供了很完善的系统驱动,无需进行复杂的移植便可实现此流程。
2.2程序流程图
图5:程序流程图
2.3主要代码说明
2.3.1初始化代码
乐鑫官方提供了针对esp-box与esp-box-lite基础的bsp库,其中包含codec、btn、i2s、i2c、lcd、文件系统等等,无需复杂驱动的移植。
默认初始化i2s0,音频采样范围16K。
esp_err_t bsp_board_s3_box_lite_init(void)
{
bsp_btn_init_default();
/**
* @brief Initialize I2S and audio codec
*
* @note Actually the sampling rate can be reconfigured.
* `MP3GetLastFrameInfo` can fill the `MP3FrameInfo`, which includes `samprate`.
* So theoretically, the sampling rate can be dynamically changed according to the MP3 frame information.
*/
ESP_ERROR_CHECK(bsp_i2s_init(I2S_NUM_0, 16000));
ESP_ERROR_CHECK(bsp_codec_init(AUDIO_HAL_16K_SAMPLES));
return ESP_OK;
}
在初始化wifi配置前,需要先初始化nvs(非易失性存储)
nvs_flash_init_partition(NVS_DEFAULT_PART_NAME);
extern "C" esp_err_t nvs_flash_init_partition(const char *part_name)
{
esp_err_t lock_result = Lock::init();
if (lock_result != ESP_OK) {
return lock_result;
}
Lock lock;
return NVSPartitionManager::get_instance()->init_partition(part_name);
}
wifi的初始化,此处直接在结构体中写入wifi名称及密码,若wifi不定可以参考smartconfig。
static void initialise_wifi(void)
{
wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta(); // 初始化并注册事件至event loop中
assert(sta_netif);
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
wifi_config_t wifi_config = {
.sta = {
.ssid = WiFi_STA_SSID,
.password = WiFi_STA_PASSWORD,
.bssid_set = 0,
}};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(WiFi_TAG, "wifi_init_sta finished.");
}
创建tts实例
voice = (esp_tts_voice_t *)&esp_tts_voice_xiaole; // 配置tts的声音配置文件,来自libvoice_set_xiaole
tts_handle = esp_tts_create(voice); // 创建tts对象
2.3.2 http任务
http任务在wifi获取到ip后被创建
else if (event_base == IP_EVENT)
{
ip_event_got_ip_t *ip_event = (ip_event_got_ip_t *)event_data;
if (event_id == IP_EVENT_STA_GOT_IP)
{
ESP_LOGI(WiFi_TAG, "got ip:" IPSTR, IP2STR(&ip_event->ip_info.ip));
xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_BIT);
WiFi_retry_num = 0;
gl_got_ip = true;
// start http task
xTaskCreate(http_client_task, "http_client", 5120, NULL, 3, NULL);
}
}
/** HTTP functions **/
static void http_client_task(void *pvParameters)
{
char output_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0}; // Buffer to store response of http request
int content_length = 0;
static const char *URL = "http://" HOST "/v3/weather/daily.json?"
"key=" UserKey "&location=" Location
"&language=" Language
"&unit=c&start=" Strat "&days=" Days;
esp_http_client_config_t config = {
.url = URL,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
// GET Request 得到请求
esp_http_client_set_method(client, HTTP_METHOD_GET);
esp_err_t err = esp_http_client_open(client, 0);
if (err != ESP_OK)
{
ESP_LOGE(HTTP_TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
}
else
{
content_length = esp_http_client_fetch_headers(client);
if (content_length < 0)
{
ESP_LOGE(HTTP_TAG, "HTTP client fetch headers failed");
}
else
{
int data_read = esp_http_client_read_response(client, output_buffer, MAX_HTTP_OUTPUT_BUFFER);
if (data_read >= 0)
{
ESP_LOGI(HTTP_TAG, "HTTP GET Status = %d, content_length = %d",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client));
printf("data:%s", output_buffer);
cJSON_parse_task(output_buffer);
}
else
{
ESP_LOGE(HTTP_TAG, "Failed to read response");
}
}
}
esp_http_client_close(client);
vTaskDelete(NULL);
}
2.3.3 speaktext函数
中文字符串首先被转换为拼音,再调用esp_tts_stream_play函数得到拼音对应的pcm音频文件,最后使用i2s_write()发送给codec播放音频。
void speaktext(char *cmd)
{
if (esp_tts_parse_chinese(tts_handle, cmd))//将中文字符串转换为拼音
{
int len[1] = {0};
do
{
short *pcm_data = esp_tts_stream_play(tts_handle, len, 0); //将拼音转换为pcm音频
i2s_write(I2S_NUM_0, (const char *)pcm_data, len[0] * 2, &bytes_write, portMAX_DELAY);
//esp_audio_play(pcm_data, len[0] * 2, portMAX_DELAY);
} while (len[0] > 0);
}
}
2.3.4 cjson解析任务
if (sub_array_item->type == cJSON_Object)
{
JsonDate = cJSON_GetObjectItem(sub_array_item, "date");
if (cJSON_IsString(JsonDate))
{
char *parts[3]; // 存储日期部分的数组
char *buf = JsonDate->valuestring;
int index = 0;
char *date = strtok(buf, "-"); // 使用 "-" 分割字符串
while (date != NULL && index < 3)
{
parts[index] = date;
index++;
date = strtok(NULL, "-"); // 继续提取下一个日期部分
}
if (index == 3)
sprintf(ttsText, "%s年%s月%s日\n", parts[0], parts[1], parts[2]);
speaktext(ttsText);
freeBuffer(ttsText, 100);
}
以获取日期为例,json中返回的日期格式为“2023-06-30”若要播放音频x年x月x日,需要对字符串进行简单的处理。调用speaktext函数便可实现中文语音播报。
3.遇到的困难
3.1分区表大小的调整
在使用官方历程提供的模板开发时,默认使用的app内存空间非常小,需要更改默认使用的分区表,改为Custom partition table CSV,调整合适的分区大小。
默认的内置分区(Single factory app, no OTA):
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
flash 的 0x10000 (64 KB) 偏移地址处存放一个标记为 “factory” 的二进制应用程序,且启动加载器将默认加载这个应用程序。分区表中还定义了两个数据区域,分别用于存储 NVS 库专用分区和 PHY 初始化数据。
如果在 menuconfig
中选择了 “Custom partition table CSV”,则还需要输入该分区表的 CSV 文件在项目中的路径。CSV 文件可以根据需要,描述任意数量的分区信息。CSV 文件的格式与上面摘要中打印的格式相同,但是在 CSV 文件中并非所有字段都是必需的。下面是一个自定义的 OTA 分区表的 CSV 文件:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000
factory, app, factory, 0x10000, 4M
voice_data, data, fat, 0x410000, 3890K
其中包含nvs存储区域、app区域还有voice_data模型的空间。
3.2 模型文件的烧写
最初始参考mp3工程驱动扬声器时并未考虑到人声的模型文件,在观察chenese-tts历程后官方给出了详细的中文播报的流程
- Parser: 根据字典与语法规则,将输入文本转换为拼音列表,输入文本编码为 UTF-8。
- Synthesizer: 根据 Parser 输出的拼音列表,结合预定义的声音集,合成波形文件。默认输出格式为单声道, 16bit@16000Hz。
4 总结
- 此次官方要求的使用esptts必须使用idf开发,虽然已经不怎么使用idf,但是相比19年第一次使用的体验来说现在做的已经是相当好了,在安装环境时不再需要复杂的网络和命令,配合vs体验很好。
- esp-box工程的代码逻辑清晰,在完成任务地同时,也学习到了一个好工程的模板。
源码:【金山文档】 weather-station
https://kdocs.cn/l/cvTNcC7sW2lC