【Funpack2-5】- 基于esp32s3的语音天气站
采用esp-box-lite实现的语音天气预报,使用知心天气的实况天气,配合esp-sr提供的tts将中文转换为pcm音频,通过扬声器播放。
标签
Funpack活动
WiFi
ESP32-S3-BOX-LITE
ESP-TTS
sll
更新2023-08-01
1341

1.硬件介绍

本设计使用ESP32-S3-BOX-Lite 轻量级开发套件,该开发板配备一块 2.4 寸 LCD 显示屏、双麦克风、一个扬声器、两个用于硬件拓展的 Pmod™ 兼容接口、结合三个独立按键,可构建多样的 HMI 人机交互应用。可实现离线语音识别与播报功能。

FjX5-P4hK9OWZW2v5uD_Bn3qa4jM

图1:硬件结构图

 

1.1按键

此开发套件有三个用户自定义按键,使用ADC方式采集按键信息;一个Boot和一个RST按键。仅需要一个IO就可以管理三个按键。

FoeMT__EMMhH0CcbGHTNEpiLbCG5

图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 操作

Fmq71ZgHb388S5cVDoir0pbQvX0v

图3:es8156接线图

FvjH3lmlX683mZ2-X2-I2ARrwsWA

图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程序流程图

FlJWUSH-cDfRJ2V1FSa6mIjel-X6

图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

团队介绍
个人
团队成员
sll
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号