本次活动使用的板卡是了乐鑫的一款支持智能语音识别的AIoT应用开发板:ESP32-S3-BOX-Lite。
ESP32-S3-BOX-Lite搭载 ESP32-S3 AI SoC,在芯片内置的 512 KB SRAM 之外,还集成了16 MB QSPI flash 和 8 MB Octal PSRAM。它板载一块2.4 寸显示屏(分辨率 320 x 240),双麦克风,一个扬声器和两个用于硬件拓展的 Pmod™ 兼容接口;采用 Type-C USB 连接器,提供 5 V 电源输入和串口/JTAG 调试接口。
产品整体的功能框图如图所示。
本次活动选择任务为:使用ESP32的WiFi和TTS功能,实现一个语音播报系统,如联网获取粉丝数并播报或者获取天气并播报。
下面对任务的实现过程进行介绍。
1-官方例程
针对这款产品,乐鑫提供了完整的演示demo,在Github上可以找到其完整的的源码。
地址为:https://github.com/espressif/esp-box
拉取时使用以下命令克隆到本地
git clone --recursive https://github.com/espressif/esp-box.git
由于官方的主分支目前没有支持ESP32-S3-BOX-Lite,所以需要切换到分支上v0.3.0上
git checkout -b v0.3.0
同时运行以下指令对组件进行更新:
git submodule update –init
由于子模块嵌套的问题,有时编译会遇到组件缺失的问题,这是引文子模块的子模块没有更新造成的,在相应的模块仓库中运行上述指令就可以解决问题。
拉取完代码后,接下来就是安装官方的开发环境,在乐鑫科技的B站官方账号上有详细的安装过程和使用说明,大家可以去查看,这里就不在赘述了。
完成上述操作后,就可以进行官方例程的编译和下载。
由于官方提供了完整的硬件和代码,在实现本次任务时,就在官方提供的代码上进行修改来完成本次任务。本次任务涉及到的模块主要有ADC按键模块、ADC、ES8156,Wifi,使用的软件模块有Wifi协议栈、SNTP、TTS等。这些部分都有官方提供的源码,在其基础上稍加改动就可以实现我们的功能。
2-按键模块
在components\bsp\src\peripherals\bsp_btn.c中定义了按键相关的接口函数和结构体。其初始化的过程以及各个函数的功能如下图所示。
在调用初始化函数后,注册相应按键事件的回调函数即可,这里我们定义不同按键的单次点击事件的回调函数来实现语音播报功能。
// 注册按键事件的回调函数
bsp_btn_register_callback(BOARD_BTN_ID_PREV, BUTTON_SINGLE_CLICK, prev_click_cb, 0);
bsp_btn_register_callback(BOARD_BTN_ID_ENTER, BUTTON_SINGLE_CLICK, ok_click_cb, 0);
bsp_btn_register_callback(BOARD_BTN_ID_NEXT, BUTTON_SINGLE_CLICK, next_click_cb, 0);
3-Wifi模块
使用ESP32S3的Wifi功能,可以连接Wfi,从而实现联网的功能。在Wifi模块的事件处理函数event_handle中对wifi模块的事件进行相应的处理,从而可以让用户根据自己的设计相应的处理过程,比如设定状态标志位、初始化数据等,用于控制其他程序的运行,进而提高程序的运行效率。Wifi模块涉及到的代码如下所示。
wifi初始化
/* Set the SSID and Password via project configuration, or can set directly here */
#define DEFAULT_SSID "HOST_BWG"
#define DEFAULT_PWD "Xdy_China_Mobile"
// Wifi连接事件
static const int WIFI_CONNECTED_EVENT = BIT0;
static EventGroupHandle_t wifi_event_group;
/* Initialize Wi-Fi as sta and set scan method */
static void wifi_init(void)
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
wifi_event_group = xEventGroupCreate();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
// Initialize default station as network interface instance (esp-netif)
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
// Initialize and start WiFi
wifi_config_t wifi_config = {
.sta = {
.ssid = DEFAULT_SSID,
.password = DEFAULT_PWD,
.scan_method = WIFI_FAST_SCAN,
.sort_method = WIFI_CONNECT_AP_BY_SIGNAL,
.threshold.rssi = 127,
.threshold.authmode = WIFI_AUTH_OPEN,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
// 等待Wifi连接事件发生,同时更新本地时间
xEventGroupWaitBits(wifi_event_group, WIFI_CONNECTED_EVENT, false, true, portMAX_DELAY);
obtain_time();
}
wifi事件处理
static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
esp_wifi_connect();
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
esp_wifi_connect();
}
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
/* Signal main application to continue execution */
xEventGroupSetBits(wifi_event_group, WIFI_CONNECTED_EVENT);
}
}
4-Sntp模块
ESP的库函数中有sntp(Simple Network Time Protocol)相关的功能,配置相关设置的相关函数,就可以通过Wifi联网获取网络时间,用于更新本地时间。使用SNTP功能可以实现获取网络时间的功能。
初始化SNTP功能模块的代码如下
static void
initialize_sntp(void)
{
ESP_LOGI(TAG, "Initializing SNTP");
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, "ntp.aliyun.com");
sntp_setservername(1, "time.asia.apple.com");
sntp_setservername(2, "pool.ntp.org");
sntp_init();
}
访问SNTP服务器并获取相应的时间信息,并调用相关的时间设定函数,更新本地时间。代码如下所示
static void obtain_time(void)
{
char strftime_buf[64];
/**
* NTP server address could be aquired via DHCP,
* see LWIP_DHCP_GET_NTP_SRV menuconfig option
*/
#if LWIP_DHCP_GET_NTP_SRV
esp_sntp_servermode_dhcp(1); // accept NTP offers from DHCP server, if any
#endif
initialize_sntp();
// wait for time to be set
time_t now = 0;
struct tm timeinfo = {0};
int retry = 0;
const int retry_count = 10;
time(&now);
localtime_r(&now, &timeinfo);
// Set timezone to China Standard Time
setenv("TZ", "CST-8", 1);
tzset();
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count)
{
ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
time(&now);
localtime_r(&now, &timeinfo);
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "The current date/time in Shanghai is: %s", strftime_buf);
if (sntp_get_sync_mode() == SNTP_SYNC_MODE_SMOOTH)
{
struct timeval outdelta;
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_IN_PROGRESS)
{
adjtime(NULL, &outdelta);
ESP_LOGI(TAG, "Waiting for adjusting time ... outdelta = %li sec: %li ms: %li us",
(long)outdelta.tv_sec,
outdelta.tv_usec / 1000,
outdelta.tv_usec % 1000);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
}
// 生成包含本地事件信息的字符串
static void parse_currtime(char *tts_str)
{
time_t now_t;
struct tm local_time;
time(&now_t);
localtime_r(&now_t, &local_time);
strftime(tts_str, 100, "当地时间是%H点%M分", &local_time);
ESP_LOGI(TAG, "%s", tts_str);
}
5-天气信息和B站粉丝数量获取模块
天气信息的获取使用心知天气的API,来获取指定的城市信息。首先需要注册心知天气的账号,获得自己的API私钥,填入API示例中的指定位置,根据自己所处的城市,修改城市参数,这样通过http服务就可以获得包含城市天气信息的JSON字符串,然后从字符串中提取出相应的数据,得到相应的天气信息。相关的代码如下:
//心知天气访问API
char *weather_url = "https://api.seniverse.com/v3/weather/now.json?key=SG-nLPzA3pyLEy9Tw&location=wuxi&language=zh-Hans&unit=c";
// 向指令url的网站发起数据请求,获得其返回json字符串,存储到指定的区域中,返回数据区域的指针
char *webget_task(char *weburl)
{
int8_t return_res = 1;
char *data_buffer = NULL;
int content_length = 0;
esp_http_client_config_t config = {
.event_handler = _http_event_handler,
.url = weburl,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
if (client == NULL)
{
return NULL;
}
// Get Request
esp_http_client_set_method(client, HTTP_METHOD_GET);
esp_err_t err = esp_http_client_open(client, 0);
if (return_res && err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
return_res = 0;
}
else
{
content_length = esp_http_client_fetch_headers(client);
if (content_length < 0)
{
ESP_LOGE(TAG, "HTTP client fetch headers failed");
return_res = 0;
}
else
{
data_buffer = malloc(content_length + 1);
memset(data_buffer, 0, content_length + 1);
if (data_buffer == NULL)
{
return_res = 0;
}
else
{
int data_read = esp_http_client_read_response(client, data_buffer, content_length);
if (data_read >= 0)
{
ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %d, data_read = %d",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client),
data_read);
ESP_LOGI(TAG, "Data %s \r\n", data_buffer);
}
else
{
ESP_LOGI(TAG, "Failed tp read response");
return_res = 0;
}
}
}
}
esp_http_client_close(client);
if (!return_res)
{
free(data_buffer);
data_buffer = NULL;
}
return data_buffer;
}
// 将天气预报信息,构建位需要播报的语音字符串
static bool parse_weather_json(char *analysis_buf, char *tts_str)
{
char tts_str_temp[100];
if (analysis_buf == NULL)
return false;
cJSON *json_root = cJSON_Parse(analysis_buf);
if (json_root != NULL)
{
cJSON *cjson_arr = cJSON_GetObjectItem(json_root, "results");
cJSON *cjosn_location = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0), "location");
ESP_LOGI(TAG, "城市 -> %s", cJSON_GetObjectItem(cjosn_location, "name")->valuestring);
sprintf(tts_str_temp, "%s", cJSON_GetObjectItem(cjosn_location, "name")->valuestring);
strcat(tts_str, tts_str_temp);
// cJSON *cjson_days = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0), "daily");
cJSON *cjson_today = cJSON_GetObjectItem(cJSON_GetArrayItem(cjson_arr, 0), "now");
ESP_LOGI(TAG, "今天天气 -> %s", cJSON_GetObjectItem(cjson_today, "text")->valuestring);
ESP_LOGI(TAG, "温度 -> %s", cJSON_GetObjectItem(cjson_today, "temperature")->valuestring);
sprintf(tts_str_temp, "今天天气%s;温度%s摄氏度;",
cJSON_GetObjectItem(cjson_today, "text")->valuestring,
cJSON_GetObjectItem(cjson_today, "temperature")->valuestring);
strcat(tts_str, tts_str_temp);
ESP_LOGI(TAG, "%s", tts_str);
cJSON_Delete(json_root);
}
free(analysis_buf);
return true;
}
获取B站粉丝数的方法与上面的类似,也是通过访问网站的查询API,从而得到返回的JSON数据,从其中提取出相应的信息。代码如下所示。
char *bili_fans_url = "https://api.bilibili.com/x/relation/stat?vmid=130121646&jsonp=jsonp";
// 将bilibill的信息,构建需要播报的语音字符串
static bool parse_bilibili_json(char *analysis_buf, char *tts_str)
{
char tts_str_temp[100];
if (analysis_buf == NULL)
return false;
cJSON *json_root = cJSON_Parse(analysis_buf);
if (json_root != NULL)
{
cJSON *cjson_data = cJSON_GetObjectItem(json_root, "data");
ESP_LOGI(TAG, "粉丝数 -> %d", cJSON_GetObjectItem(cjson_data, "follower")->valueint);
sprintf(tts_str_temp, "账号当前粉丝数量%d位。", cJSON_GetObjectItem(cjson_data, "follower")->valueint);
strcat(tts_str, tts_str_temp);
ESP_LOGI(TAG, "%s", tts_str);
cJSON_Delete(json_root);
}
free(analysis_buf);
return true;
}
在本工程中由于在编译工程中会报出tls的错误,这里采用一种偷懒的办法,跳过了安全证书的检查。
6-tts模块
TTS(Text To Speech)功能模块目前仅支持中文语音合成,可以将字符串转换成语音信息,通过codec芯片输出到扬声器,从而实现语音播报的功能。整个模块的系统框图如图所示。
初始化tts模块的代码如下:
g_voice = (esp_tts_voice_t *)&esp_tts_voice_xiaole; // 配置tts的声音配置文件,来自libvoice_set_xiaole
g_tts_handle = esp_tts_create(g_voice); // 创建tts对象
对于不同的语音合成任务,调用相应的字符串获取函数。之后的处理都是一样的,语音合成任务的启动由不同的按键事件来驱动,以下为各个按键回调函数的具体定义
static void prev_click_cb(void *arg)
{
// button_dev_t *event = (button_dev_t *)arg;
size_t bytes_write = 0;
memset(tts_str, 0, 100);
parse_weather_json(webget_task(weather_url), tts_str);
ESP_LOGI(TAG, "获取天气信息");
if (esp_tts_parse_chinese(g_tts_handle, tts_str)) // 文字解析成拼音
{
int len1[1] = {0};
do
{
short *pcm_data1 = esp_tts_stream_play(g_tts_handle, len1, 0); // 拼音转换成pcm音频
i2s_write(I2S_NUM_0, (const char *)pcm_data1, len1[0] * 2, &bytes_write, portMAX_DELAY);
memset(pcm_data1, 0, len1[0] * 2);
// esp_audio_play(pcm_data, len[0] * 2, portMAX_DELAY); // 播放音频
} while (len1[0] > 0);
}
}
static void ok_click_cb(void *arg)
{
// button_dev_t *event = (button_dev_t *)arg;
size_t bytes_write = 0;
memset(tts_str, 0, 100);
parse_bilibili_json(webget_task(bili_fans_url), tts_str);
ESP_LOGI(TAG, "获取B站粉丝数量");
if (esp_tts_parse_chinese(g_tts_handle, tts_str)) // 文字解析成拼音
{
int len[1] = {0};
do
{
short *pcm_data1 = esp_tts_stream_play(g_tts_handle, len, 0); // 拼音转换成pcm音频
i2s_write(I2S_NUM_0, (const char *)pcm_data1, len[0] * 2, &bytes_write, portMAX_DELAY);
memset(pcm_data1, 0, len[0] * 2);
// esp_audio_play(pcm_data, len[0] * 2, portMAX_DELAY); // 播放音频
} while (len[0] > 0);
}
}
static void next_click_cb(void *arg)
{
// button_dev_t *event = (button_dev_t *)arg;
size_t bytes_write = 0;
memset(tts_str, 0, 100);
parse_currtime(tts_str);
ESP_LOGI(TAG, "获取当地时间信息");
if (esp_tts_parse_chinese(g_tts_handle, tts_str)) // 文字解析成拼音
{
int len1[1] = {0};
do
{
short *pcm_data1 = esp_tts_stream_play(g_tts_handle, len1, 0); // 拼音转换成pcm音频
i2s_write(I2S_NUM_0, (const char *)pcm_data1, len1[0] * 2, &bytes_write, portMAX_DELAY);
memset(pcm_data1, 0, len1[0] * 2);
// esp_audio_play(pcm_data, len[0] * 2, portMAX_DELAY); // 播放音频
} while (len1[0] > 0);
}
}
7-应用程序设计
完成上述模块的设计后,在应用程序的入口app_main()中完成外设、模块以及回调函数的初始化。之后,应用程序就开始等待相应的按键事件发生,进而调用相关的回调函数,实现对应的功能。应用程序入口的流程图如下。
相关的代码如下:
int app_main()
{
// Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(bsp_board_init());
ESP_ERROR_CHECK(bsp_board_power_ctrl(POWER_MODULE_AUDIO, true));
g_voice = (esp_tts_voice_t *)&esp_tts_voice_xiaole; // 配置tts的声音配置文件,来自libvoice_set_xiaole
g_tts_handle = esp_tts_create(g_voice); // 创建tts对象
wifi_init();
// 注册按键事件的回调函数
bsp_btn_register_callback(BOARD_BTN_ID_PREV, BUTTON_SINGLE_CLICK, prev_click_cb, 0);
bsp_btn_register_callback(BOARD_BTN_ID_ENTER, BUTTON_SINGLE_CLICK, ok_click_cb, 0);
bsp_btn_register_callback(BOARD_BTN_ID_NEXT, BUTTON_SINGLE_CLICK, next_click_cb, 0);
return 0;
}
8-总结
在学习发开过程中,乐鑫的相关资源是很丰富的,提供了许多可以直接运行的代码,在网上也可以找到相应的例程代码,学习起来还是很方便的。在这次活动中,学习了Wifi、语音合成、天气接口调用、JSON文件解析等基本工作原理,实现了简单的语音播报功能。
9-参考资料
TTS 语音合成模型 - ESP32 - — ESP-SR latest 文档 (espressif.com)
espressif/esp-skainet: Espressif intelligent voice assistant (github.com)