一、项目介绍
指路标牌通过在特定位置,将附近的地点标识并指示大概方向,为用户提供导航。这类指路标牌在商场、动物园、植物园 、博物馆、科技馆、展厅等场景均具有广泛应用。然而,传统标牌形式单一,提供的信息有限,需要用户自己多次寻找等缺点。
为此,本项目旨在设计一种可交互的智能指路标牌,通过人脸特征识别和语音识别技术,为用户提供无感、持续的导航服务。基于该标牌系统,用户无需反复输入指令,系统能自动识别人脸特征,在各个路口中提供实时的导航路线,确保流畅便捷的体验。
二、硬件介绍
- 整体组成
智能指路标牌主体由16x16显示阵列和顶部的核心主控模块(包含ESP32S3和传感器)组成,同一局域网的树莓派运行着Face Recognition人脸识别库和路标数据库,为智能指路标牌提供服务。相关介绍如下:
- XIAO ESP32S3主控 :其具有双核、240MHz运行频率、丰富外设和完整WiFi通信。
- 数字麦克风 :型号为MSM261D3526H1CPM,用于采集用户的声音指令,其在ESP32S3核心板上。
- 摄像头 :型号为OV2640,用于拍摄照片,其在ESP32S3核心板上。
- 16x16显示阵列:型号为WS2812B,内部共串联着256个,显示丰富的RGB色彩。
- 树莓派:本项目使用了Raspberry Pi 5,由于仅用到了Python,服务端代码可快速部署到其他平台或设备。
- 电路原理图
Seeed XIAO ESP32-S3 Sense原理图(图)
扩展板原理图(图)
以上为核心主控模块中Seeed XIAO ESP32-S3 Sense和扩展板的原理图。
- Seeed XIAO ESP32-S3 Sense:包含摄像头和SD卡的接口,与一个板载数字麦克风。
- 扩展板:用于将ESP32 S3核心板与其他外设进行快速连接。
- 扩展板设计与接线
合理设计扩展板,并接上对应的传感器、模块和显示阵列,接线如图中所示。
- 核心板接口:中间镂空并通过2*7P排针连接核心板,镂空设计有利于为核心板和核心板上的摄像头进行散热,保证稳定性。
- TYPE-C接口:提供5V电源输入,最大可支持3A电流。
- 喇叭驱动模块接口:为MAX98357A模块提供直插排针,模块可驱动最大3W的喇叭输出。(本项目暂未使用上)
- 超声波模块接口:为HC-SR40超声波测距模块提供直插排针。
- 风扇接口:提供5V输出,驱动散热风扇。散热风扇可选择4010风扇,通过M3螺丝部署在背部。
- 装配
连接好各个模块,通过M3螺丝和尼龙柱即可固定在3D外壳中。散热风扇通过M3螺丝固定在背部向内吹风,喇叭可粘贴至侧面。最后,将整个主控模块粘贴至显示阵列顶部。
三、方案选择
- 人脸识别方案:针对如下几个方案,选择Face Recognition,其MIT协议,且python库直接部署。
方案名称 | 特点描述 | 推荐场景 |
---|---|---|
CompreFace | 一站式服务,易于部署 | 快速集成,企业级应用 |
Face Recognition | 简单易用,Python 库 | 快速原型开发 |
InsightFace | 高性能,支持多模型 | 高精度要求,定制开发 |
DeepFace | 多模型支持,统一接口 | 多模型对比与融合 |
- 语音识别方案:现有语音识别技术包括如下Kaldi、Vosk、ESPnet等,然而,考虑到部署难度,决定参考官方XIAO的教程,利用百度云语音识别服务完成语音识别。
四、系统框图
- 智能路标:主控为ESP32S3,读取摄像头、麦克风和超声波测距模块的数据,并通过显示阵列和喇叭输出。多个路标独立运行。
- 树莓派:运行Face_recogntion服务,存储路标数据库,其与智能路标通过WebSocket通信。
- 百度云语音识别服务:申请了免费的语音识别API,通过Http通信。
五、软件流程
- 初始化
- 初始化WS2812显示阵列,并在后续初始化过程中显示图案,方便用户判断状态。
- 初始化WiFi,连接失败会显示错误图标。
- 初始化语音识别,依次初始化麦克风、百度云服务连接、16kHz定时器。
- 初始化摄像头,配置为jpeg采集模式。
- 初始化WebSocket客户端,连接到树莓派(需要提前在树莓派启动WebSocket服务端代码),连接失败会显示错误图标。
- 初始化超声波。
- 循环Task
- 由于摄像头拍摄并人脸识别的时延较长,这里以超声波测距结果触发后续操作,当检测距离小于1.2m时启动摄像头拍摄并识别。
- 识别到人脸并匹配数据库中的目的地,将会显示对应箭头。
- 未识别到人脸或者检测到声音,将会录音并进行语音识别。
- 语音识别结果如果匹配到数据库中的目的地,将会更新用户与目的地到数据库中。
- 代码树
ESP32S3的代码,将各个任务的函数封装到了对应的文件中,配置参数单独在一个文件夹中,方便用户修改。
- 关键代码说明
a. 语音识别初始化:代码在speech_recognition.cpp和microphone_IIS_PDM.cpp中,依次初始化百度云token,16kHz硬件定时器,I2S麦克风和内存分配。硬件定时器能够提供精准的周期回调,而PSRAM能够分配大内存存储语音数据。
// PDM 麦克风引脚配置
#define PDM_CLK_PIN GPIO_NUM_42 // PDM 时钟引脚
#define PDM_DATA_PIN GPIO_NUM_41 // PDM 数据引脚
uint8_t speech_recognition_init()
{
// 1. 获取百度云token
token = gainToken();
// 2. 创建硬件定时器,频率16k
timer = timerBegin(1200000);
timerAttachInterrupt(timer, &onTimer); // 设置回调函数
timerAlarm(timer, 75, true, 0); // 分频75,1200000/75=16k
timerStop(timer); // 先停止定时器
// 3. 初始化麦克风
i2s_init_pdm(); // 麦克风通过I2S接口读取
// 4. 动态分配PSRAM给麦克风ADC数据和发送的json数据
adc_data = (uint16_t *)ps_malloc(adc_data_len * sizeof(uint16_t));
data_json = (char *)ps_malloc(data_json_len * sizeof(char));
return pdTRUE;
}
I2SClass I2S;
void i2s_init_pdm()
{
// 设置 42 PDM 时钟和 41 PDM 数据引脚
I2S.setPinsPdmRx(PDM_CLK_PIN, PDM_DATA_PIN);
// 以 16 kHz 和 16 位每样本启动 I2S
if (!I2S.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO))
{
while (1)
{
Serial.println("初始化 I2S 失败!");
xTaskNotify(MatrixHelloTaskHandler, ERROR, eSetValueWithOverwrite); // 初始化失败将发送指令到LED Matrix警示
delay(200);
}
}
}
b. 摄像头初始化:代码在camera_ov2640.cpp中,初始化相机引脚和配置,其中图像格式为jpeg,分辨率为最大FRAMESIZE_UXGA(1600x1200)。
// 摄像头引脚定义
#define PWDN_GPIO_NUM -1 // 摄像头电源控制引脚
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10 // 摄像头时钟引脚
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
void camera_init()
{
// 相机配置初始化
camera_config_init();
if (esp_camera_init(&config) != ESP_OK)
{
Serial.println("Camera init failed!");
while (1)
{
Serial.println("Retrying camera init...");
esp_camera_deinit();
camera_config_init();
if (esp_camera_init(&config) == ESP_OK)
{
break; // 成功则跳出循环
}
}
xTaskNotify(MatrixHelloTaskHandler, ERROR, eSetValueWithOverwrite); // 初始化失败将发送指令到LED Matrix警示
delay(1000);
}
}
/**
* 相机配置初始化
*/
void camera_config_init()
{
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
// 如果触发:cam_hal: EV-VSYNC-OVF
// 分辨率变小或是 XCLK 变大,均可能导致传感器触发的帧同步信号过快。
config.xclk_freq_hz = 10000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 2;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_LATEST; // 图像捕获模式,设置为CAMERA_GRAB_LATEST,并在获取前丢弃一张,即可获取最新的图像
}
c. WebSocket连接:代码在websocket_camera.cpp中,初始化WebSocket客户端和回调函数。针对服务器发过来的数据,根据不同的ID进行相应,并通过任务通知传递给主任务处理。
// 前往config.h文件修改以下信息
// const char *websocket_host = "192.168.10.164"; // 树莓派IP
// const uint16_t websocket_port = 8765;
// const char *websocket_path = "/";
WebSocketsClient webSocket;
void websocket_connect()
{
webSocket.begin(websocket_host, websocket_port, websocket_path);
webSocket.onEvent([](WStype_t type, uint8_t *payload, size_t length)
{
if (type == WStype_TEXT) {
// 对于接收到的数据,以任务通知方式通知主任务进行处理
if(length > 0)
{
if(payload[0] == '1') // 没有人脸
{
xTaskNotify(disHCSR04TaskHandler, NO_FACE, eSetValueWithOverwrite); // 通知超声波任务没有人脸
}else if (payload[0] == '2') // 有人脸
{
xTaskNotify(disHCSR04TaskHandler, HAVE_FACE, eSetValueWithOverwrite); // 通知超声波任务有人脸
}else if (payload[0] == '3')
{
websocket_result_text = String((char*)payload); // 第一个字符是检测结果,第二个字符是方向
xTaskNotify(disHCSR04TaskHandler, MATCH_FACE, eSetValueWithOverwrite); // 通知超声波任务匹配到数据库人脸
}else if (payload[0] == '4')
{
xTaskNotify(disHCSR04TaskHandler, NEW_FACE, eSetValueWithOverwrite); // 通知超声波任务新人脸
}else if (payload[0] == '6')
{
websocket_result_text = String((char*)payload); // 第一个字符是检测结果,第二个字符是方向
xTaskNotify(disHCSR04TaskHandler, SAVE_OK, eSetValueWithOverwrite); // 通知超声波任务保存新人脸到数据库成功
}
else if (payload[0] == '8')
{
xTaskNotify(disHCSR04TaskHandler, ERROR_POS, eSetValueWithOverwrite); // 通知超声波任务匹配目的地失败
}
else
{
xTaskNotify(disHCSR04TaskHandler, ERROR_RES, eSetValueWithOverwrite); // 通知超声波任务失败
}
}
Serial.printf("📩 Message from server: %s\n", payload);
} });
webSocket.setReconnectInterval(5000);
}
d. LED Matrix显示状态和图像:代码在matrix_ws2812.cpp中,函数MatrixHelloTask()在初始化中依次显示状态,方便用户判断各个模块状态;动态图像的利用初始行列的变化来显示,减少存储占用。
// 用于在初始化过程中显示各种图标
xTaskHandle MatrixHelloTaskHandler;
void MatrixHelloTask(void *pvParameters)
{
uint32_t res = 0;
static uint8_t wifi_index = 0;
while (1)
{
// 接收任务通知
xTaskNotifyWait(0, ULONG_MAX, &res, portMAX_DELAY);
switch (res)
{
case WIFI_START:
// 显示 Wi-Fi 连接过程的图标
matrix_show_pic(wifi_frame_list[wifi_index]);
matrix_show();
wifi_index++;
wifi_index = wifi_index % 4;
break;
case WIFI_OK:
// 显示Wi-Fi 连接成功的图标
matrix_show_pic(wifi_frame_list[wifi_index]);
matrix_show();
break;
case WEBSOCKET_WAIT:
// 显示等待websocket连接的图标
matrix_show_pic(cloud_frame);
matrix_show();
break;
case NO_PIC:
// 清空显示
matrix_clear();
matrix_show();
break;
case ERROR:
// 显示初始化错误的图标
matrix_show_pic(error_frame);
matrix_show();
break;
default:
matrix_show_pic(error_frame);
matrix_show();
break;
}
}
}
// 显示16*16的图片,可以从选择的行和列进行显示,以显示动态图片
void matrix_show_pic(const uint8_t *pic, uint8_t row_s, uint8_t col_s)
{
for(uint16_t row = 0; row < 16; row++)
{
for(uint16_t col = 0; col < 16; col++)
{
uint16_t index = (row * 16 + col) * 3; // 计算该像素在数组中的起始位置
uint8_t r = pic[index]; // R
uint8_t g = pic[index + 1]; // G
uint8_t b = pic[index + 2]; // B
matrix_set_rgb_y_x((row+row_s)%16, (col+col_s)%16, r, g, b); // 设置矩阵上的 RGB 值
}
}
}
e. 循环任务:代码在main.cpp中的dis_HCSR04_task()中,循环任务以超声波测距为触发,以快速响应用户进入目标区域;随后进行相机拍摄和人脸识别;语音采集则会在检测到足够音量后才开始采集。(代码较多,裁剪了部分部分)
/**
* 超声波模块任务,主循环任务
*/
SemaphoreHandle_t disHCSR04Semaphore; // 声明信号量
xTaskHandle disHCSR04TaskHandler; // 声明全局句柄变量
const uint16_t distance_dec = 120; // 距离阈值,120cm
void dis_HCSR04_task(void *pvParameters)
{
while (1)
{
// 1.1 读取超声波距离,考虑到超声波数据的不稳定,需要加入判断和处理
dis_now = 0;
distance = 0;
distance_i = 0;
while (distance_i < 10)
{
dis_now = dis_HCSR04_get(); // 获取距离
if (dis_now > 20)
{
dis_list[distance_i] = (uint16_t)dis_now; // 存储距离
distance += dis_now; // 累加距离
distance_i++; // 统计有效距离
}
vTaskDelay(pdMS_TO_TICKS(10)); // 延时50ms,避免连续读取
}
if (distance_i > 0)
distance = distance / distance_i; // 取平均值
else
distance = 400; // 距离阈值
// 1.2 显示欢迎显示,循环改变亮度。可根据需要修改为其他图案。
hello_index += 5;
if (hello_index < 60)
{
matrix_set_light(hello_index); // 设置亮度
matrix_show_pic(earth_frame); // 设置显示图案
matrix_show();
}
else if (hello_index < 120)
{
matrix_set_light(120 - hello_index);
matrix_show_pic(earth_frame);
matrix_show();
}
else
{
hello_index = 0; // 重置索引
}
// 2.0 测距在阈值内,触发后续图像拍摄和识别等任务
if (distance < distance_dec)
{
// 2.1 显示相机图案提示用户
matrix_show_pic(camera_with_flash);
matrix_show();
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1s,等待摄像头稳定
// 2.2 启动拍照并进行人脸检测
// 模拟闪光灯进行拍照
matrix_set_light(200); // 闪关灯亮度设置为200
matrix_fill_rgb(255, 255, 255); // 设置白色背景
matrix_show();
vTaskDelay(pdMS_TO_TICKS(100)); // 延时200ms,等待显示
xTaskNotifyStateClear(NULL); // 清除任务通知状态,防止之前的任务通知影响结果
xTaskNotify(websocketTaskHandler, FACE_REG_TASK, eSetValueWithOverwrite); // 发送人脸检测任务通知
matrix_set_light(100);
matrix_show_pic(camera_with_flash);
matrix_show();
// 2.3 根据返回结果检测是否有人脸
uint32_t ulres = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(20 * 1000)); // 最多等待10秒
if (ulres == NO_FACE)
{
Serial.println("No face detected");
matrix_show_pic(cry);
matrix_show();
vTaskDelay(pdMS_TO_TICKS(200)); // 未检测到人脸,显示哭脸图片
continue;
}
else if (ulres == MATCH_FACE)
{
matrix_show_pic(grin);
matrix_show();
vTaskDelay(pdMS_TO_TICKS(200)); // 匹配到数据库人脸,显示笑脸图片
Serial.println("Match face detected");
}
else if (ulres == NEW_FACE)
{
matrix_show_pic(question);
matrix_show();
vTaskDelay(pdMS_TO_TICKS(200)); // 检测到新人脸,显示问号脸图片
Serial.println("New face detected");
}
else
{
Serial.println("Error result"); // 其他错误,重回超声波测距循环任务
continue;
}
// 2.4 如果是数据中的数据,进行方向标识
if (ulres == MATCH_FACE)
{
bool macrophone_state = false;
int tindex = 0;
// 2.4.1 先展示5s的方向,期间可语音触发语音采集,用于更改目的地
while (tindex < 50)
{
tindex++;
bool pdm_res2 = i2s_wait_pdm_start(300, 100); // 等待PDM数据准备,里面需要加上延时,不然可能会导致CPU全部占用而让系统崩溃
if (pdm_res2 == true)
{
ulres = NEW_FACE; // 如果检测到语音输入,则采用类似于检测到新人脸的流程进行后续操作
macrophone_state = true;
break; // 如果PDM数据准备失败,跳过本次循环
}
show_direction(); // 显示动态箭头方向
}
// 2.4.2 随后用超声波判断是否退出
if (macrophone_state == false)
while (distance < (distance_dec + 40))
{
distance = wait_people_leave(); // 使用超声波进行人判断,但是会影响声音
vTaskDelay(pdMS_TO_TICKS(40)); // 延时100ms,避免连续读取
}
}
// 2.5 如果是新的人脸或语音更新目的地,进行语音识别然后发送到WebSocket服务器
if (ulres == NEW_FACE)
{
// 2.5.1 显示麦克风图标
matrix_set_light(100);
matrix_show_pic(studio_microphone);
matrix_show();
bool pdm_res = i2s_wait_pdm_start(200, 10000); // 等待PDM数据准备,里面需要加上延时,不然可能会导致CPU全部占用而让系统崩溃
if (pdm_res == false)
{
continue; // 如果PDM数据准备失败,跳过本次循环
}
// 2.5.2 启动语音任务
xTaskNotifyStateClear(NULL); // 清除任务通知状态
xTaskCreatePinnedToCore(speechRecognitionTask, "SpeechRecognitionTask", 1024 * 10, NULL, 8, &speechRecognitionTaskHandler, 1);
// 2.5.3 等待事件结果,此结果由WebSocket客户端接收到的数据进行任务通知。
ulres = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10 * 1000)); // 最多等待10秒
while (ulres > 0)
{
if (ulres == SPEECH_START)
{
Serial.println("Main: Speech recognition started");
matrix_set_light(150);
matrix_show_pic(studio_microphone); // 麦克风图片,表示正在录音,提示用户
matrix_show();
ulres = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10 * 1000)); // 最多等待10秒
}
else if (ulres == SPEECH_END)
{
Serial.println("Main: Speech recognition end");
matrix_set_light(100);
matrix_show_pic(thinking_face); // 思考图片,表示正在语音识别,提示用户
matrix_show();
ulres = ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(30 * 1000)); // 最多等待10秒
}
else if (ulres == SPEECH_OK)
{
Serial.println("Main: Speech recognition OK");
matrix_set_light(100);
// 识别到语音结果之后,发送目标名字和拍照发送到服务器
{
// 省略了代码,进行拍照、图片上传和等待结果,并显示对应结果和动态方向
}
ulres = 0; // 重置结果,返回循环任务
}
else
{
ulres = 0;
}
}
}
Serial.println("Restart distance sensor task");
}
}
}
六、功能展示
- 初始化流程:正确配置WiFi参数、启动WebSocket服务器、配置百度云语音识别服务,连接Type-C(推荐至少5V2A电源)即可开机运行,初始化过程会显示相应图案,错误会显示错误图标,成功则会显示地球图标表示欢迎。
- 拍照但未识别到人脸:拍照过程会显示相机图标,并闪一下提供拍照闪光灯,识别失败会显示流泪图案。
- 新用户语音输入目的地,并匹配到数据库:用户语音输入时(格式:我想去XXX),会触发语音采集并持续采集6s,期间显示麦克风图标;识别并匹配到数据库目的地后,会再次拍照并存入数据库,随后显示方向。
- 用户再次进入查看标牌:无需语音输入,拍照并匹配到人脸后自动显示方向
- 用户更新目的地:用户在显示方向的5s内,说出新的目的地,将会重新触发录音、识别、存入数据库和显示方向这些流程。
- 用户更新目的地但匹配失败:会显示流泪图标表示匹配失败,如果用户还在前方,将继续循环Task。
七、复刻说明
- PCB打板:PCB选择厚度1.6mm 双层板。
- 3D外壳打印:WS2812阵列板显示最好使用双色打印,前面板为白色进行匀光,中间为黑色进行隔离。如果缺乏双色打印,也可使用全白。
- 物料清单:除了附件中的物料清单,还应准备4个M3*20的双通尼龙柱、4010 5V风扇、和一些M3螺丝。
八、遇到的问题
- 在本项目中,为了适应系统的复杂性并同时处理多个传感器和输出任务,我选择了FreeRTOS与Arduino框架的组合。在开发过程中,遇到了以下几项关键问题,并通过有效的解决方案得以解决:
- 在任务栈空间分配时,发现栈空间过小可能导致系统崩溃。通过增加栈空间,问题得到了有效解决,系统能够稳定运行。
- 为了避免某个任务长时间占用CPU资源而不释放,尤其是读取麦克风的任务,我们优化了任务的设计,确保每个任务在读取麦克风后能够释放CPU资源,避免了系统卡顿。
- 在任务间传递String类型的数据时,出现了资源释放后数据丢失的问题。该问题通过使用全局参数进行数据传递成功解决,从而确保数据的有效传递。
- 初始时,信号量被默认地“take”了,导致无法再次“take”影响程序运行。为此,我们在信号量的使用中添加了条件判断,确保在需要时通过“give”信号量后再进行“take”操作,从而避免了死锁问题。
- 数字麦克风读取与语音识别问题:
- 通过硬件定时器来实现标准的16KHz速率读取麦克风数据,但由于数据量巨大,初期栈空间不足,导致程序崩溃。通过增加任务栈空间,我们解决了这一问题,确保了数据的稳定读取。
- 录制的数据较长时,通过HTTP发送时会占用巨大的空间,导致栈空间不足。测试表明,当读取时间超过6秒时,系统可能崩溃。我们通过对传输数据进行分段处理,并调整栈空间配置,有效解决了这一问题。
- 在使用百度云语音识别API时,发现API在一段时间后可能会失效。为了解决这个问题,我们设置了定期检查并重新授权的机制,保证了系统的稳定性。
- 图像采集与发送问题:
- OV2640摄像头在长时间工作时会出现严重的发热问题,同时ESP32也会因为图像处理产生较大的热量。尽管添加了散热片,但依然无法满足散热需求。我们通过添加小风扇进行主动散热,解决了长时间稳定运行的挑战。
- 在图像读取过程中,发现使用全局变量存储图像帧(*fb)时,传递图像数据会出现失效的情况。通过改用局部变量或利用FreeRTOS传递指针的方式解决了此问题,确保了数据的准确传递。
- 超声波与麦克风问题:
- 即使在停止工作后的较长时间内,超声波传感器仍然会对附近的麦克风传感器产生影响,误导系统认为有声音输入。为解决这一问题,我们将麦克风的任务与超声波的任务进行了分离,确保两者不会互相干扰,保证了系统的稳定运行。
九、学习心得
通过本次的项目,对ESP32、FreeRTOS有了更深入的理解,也对摄像头和麦克风技术有了理解。通过整个项目的进行,对人机交互这个大需求也有了更深入的理解。