2025交互标牌 - 基于人脸特征和语音识别的智能指路标牌
该项目使用了ESP32S3、摄像头和麦克风,实现了智能交互导航标牌的设计,它的主要功能为:语音识别用户目的地并动态显示方向,随后根据人脸特征自动更新方向。
标签
语音识别
ESP32S3
人脸识别
交互标牌
导航
小小洋洋
更新2025-08-19
同济大学
27

一、项目介绍

image.png image.png

image.png image.png

指路标牌通过在特定位置,将附近的地点标识并指示大概方向,为用户提供导航。这类指路标牌在商场、动物园、植物园 、博物馆、科技馆、展厅等场景均具有广泛应用。然而,传统标牌形式单一,提供的信息有限,需要用户自己多次寻找等缺点。

为此,本项目旨在设计一种可交互的智能指路标牌,通过人脸特征识别和语音识别技术,为用户提供无感、持续的导航服务。基于该标牌系统,用户无需反复输入指令,系统能自动识别人脸特征,在各个路口中提供实时的导航路线,确保流畅便捷的体验。


二、硬件介绍

  1. 整体组成

image.png

image.png

智能指路标牌主体由16x16显示阵列和顶部的核心主控模块(包含ESP32S3和传感器)组成,同一局域网的树莓派运行着Face Recognition人脸识别库和路标数据库,为智能指路标牌提供服务。相关介绍如下:

    1. XIAO ESP32S3主控 其具有双核、240MHz运行频率、丰富外设和完整WiFi通信。
    2. 数字麦克风 型号为MSM261D3526H1CPM,用于采集用户的声音指令,其在ESP32S3核心板上。
    3. 摄像头 型号为OV2640,用于拍摄照片,其在ESP32S3核心板上。
    4. 16x16显示阵列:型号为WS2812B,内部共串联着256个,显示丰富的RGB色彩。
    5. 树莓派:本项目使用了Raspberry Pi 5,由于仅用到了Python,服务端代码可快速部署到其他平台或设备。


  1. 电路原理图

Seeed XIAO ESP32-S3 Sense原理图(图)

image.png

扩展板原理图(图)

以上为核心主控模块中Seeed XIAO ESP32-S3 Sense扩展板的原理图

    1. Seeed XIAO ESP32-S3 Sense:包含摄像头和SD卡的接口,与一个板载数字麦克风。
    2. 扩展板:用于将ESP32 S3核心板与其他外设进行快速连接。


  1. 扩展板设计与接线

image.png

image.png

合理设计扩展板,并接上对应的传感器、模块和显示阵列,接线如图中所示

    1. 核心板接口:中间镂空并通过2*7P排针连接核心板,镂空设计有利于为核心板和核心板上的摄像头进行散热,保证稳定性。
    2. TYPE-C接口:提供5V电源输入,最大可支持3A电流。
    3. 喇叭驱动模块接口:MAX98357A模块提供直插排针,模块可驱动最大3W的喇叭输出。(本项目暂未使用上)
    4. 超声波模块接口:为HC-SR40超声波测距模块提供直插排针。
    5. 风扇接口:提供5V输出,驱动散热风扇。散热风扇可选择4010风扇,通过M3螺丝部署在背部。


  1. 装配

image.png image.png

连接好各个模块,通过M3螺丝和尼龙柱即可固定在3D外壳中。散热风扇通过M3螺丝固定在背部向内吹风,喇叭可粘贴至侧面。最后,将整个主控模块粘贴至显示阵列顶部。


三、方案选择

  1. 人脸识别方案:针对如下几个方案,选择Face Recognition,其MIT协议,且python库直接部署。

方案名称

特点描述

推荐场景

CompreFace

一站式服务,易于部署

快速集成,企业级应用

Face Recognition

简单易用,Python 库

快速原型开发

InsightFace

高性能,支持多模型

高精度要求,定制开发

DeepFace

多模型支持,统一接口

多模型对比与融合

  1. 语音识别方案:现有语音识别技术包括如下KaldiVoskESPnet等,然而,考虑到部署难度,决定参考官方XIAO的教程,利用百度云语音识别服务完成语音识别。


四、系统框图

image.png

  1. 智能路标:主控为ESP32S3,读取摄像头、麦克风和超声波测距模块的数据,并通过显示阵列和喇叭输出。多个路标独立运行。
  2. 树莓派:运行Face_recogntion服务,存储路标数据库,其与智能路标通过WebSocket通信。
  3. 百度云语音识别服务:申请了免费的语音识别API,通过Http通信。


五、软件流程

  1. 初始化

image.png

    1. 初始化WS2812显示阵列,并在后续初始化过程中显示图案,方便用户判断状态。
    2. 初始化WiFi,连接失败会显示错误图标。
    3. 初始化语音识别,依次初始化麦克风、百度云服务连接、16kHz定时器。
    4. 初始化摄像头,配置为jpeg采集模式。
    5. 初始化WebSocket客户端,连接到树莓派(需要提前在树莓派启动WebSocket服务端代码),连接失败会显示错误图标。
    6. 初始化超声波。


  1. 循环Task

image.png

    1. 由于摄像头拍摄并人脸识别的时延较长,这里以超声波测距结果触发后续操作,当检测距离小于1.2m时启动摄像头拍摄并识别。
    2. 识别到人脸并匹配数据库中的目的地,将会显示对应箭头。
    3. 未识别到人脸或者检测到声音,将会录音并进行语音识别。
    4. 语音识别结果如果匹配到数据库中的目的地,将会更新用户与目的地到数据库中。


  1. 代码树

image.png

ESP32S3的代码,将各个任务的函数封装到了对应的文件中,配置参数单独在一个文件夹中,方便用户修改。


  1. 关键代码说明

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");
    }
  }
}


、功能展示

  1. 初始化流程:正确配置WiFi参数、启动WebSocket服务器、配置百度云语音识别服务,连接Type-C(推荐至少5V2A电源)即可开机运行,初始化过程会显示相应图案,错误会显示错误图标成功则会显示地球表示欢迎

image.png

  1. 拍照但未识别到人脸:拍照过程会显示相机图标,并闪一下提供拍照闪光灯,识别失败会显示流泪图案。

image.png image.png

  1. 新用户语音输入目的地,并匹配到数据库:用户语音输入时(格式:我想去XXX),会触发语音采集并持续采集6s,期间显示麦克风图标;识别并匹配到数据库目的地后,会再次拍照并存入数据库,随后显示方向。

image.png image.png image.png

  1. 用户再次进入查看标牌:无需语音输入,拍照并匹配到人脸后自动显示方向
  2. 用户更新目的地:用户在显示方向的5s内,说出新的目的地,将会重新触发录音、识别、存入数据库和显示方向这些流程。

image.png image.png

  1. 用户更新目的地但匹配失败:会显示流泪图标表示匹配失败,如果用户还在前方,将继续循环Task。


七、复刻说明

  1. PCB打板:PCB选择厚度1.6mm 双层板。
  2. 3D外壳打印:WS2812阵列板显示最好使用双色打印,前面板为白色进行匀光,中间为黑色进行隔离。如果缺乏双色打印,也可使用全白。
  3. 物料清单:除了附件中的物料清单,还应准备4个M3*20的双通尼龙柱、4010 5V风扇、和一些M3螺丝。


八、遇到的问题

  1. 在本项目中,为了适应系统的复杂性并同时处理多个传感器和输出任务,我选择了FreeRTOS与Arduino框架的组合。在开发过程中,遇到了以下几项关键问题,并通过有效的解决方案得以解决:
    1. 在任务栈空间分配时,发现栈空间过小可能导致系统崩溃。通过增加栈空间,问题得到了有效解决,系统能够稳定运行。
    2. 为了避免某个任务长时间占用CPU资源而不释放,尤其是读取麦克风的任务,我们优化了任务的设计,确保每个任务在读取麦克风后能够释放CPU资源,避免了系统卡顿。
    3. 在任务间传递String类型的数据时,出现了资源释放后数据丢失的问题。该问题通过使用全局参数进行数据传递成功解决,从而确保数据的有效传递。
    4. 初始时,信号量被默认地“take”了,导致无法再次“take”影响程序运行。为此,我们在信号量的使用中添加了条件判断,确保在需要时通过“give”信号量后再进行“take”操作,从而避免了死锁问题。
  2. 数字麦克风读取与语音识别问题
    1. 通过硬件定时器来实现标准的16KHz速率读取麦克风数据,但由于数据量巨大,初期栈空间不足,导致程序崩溃。通过增加任务栈空间,我们解决了这一问题,确保了数据的稳定读取。
    2. 录制的数据较长时,通过HTTP发送时会占用巨大的空间,导致栈空间不足。测试表明,当读取时间超过6秒时,系统可能崩溃。我们通过对传输数据进行分段处理,并调整栈空间配置,有效解决了这一问题。
    3. 在使用百度云语音识别API时,发现API在一段时间后可能会失效。为了解决这个问题,我们设置了定期检查并重新授权的机制,保证了系统的稳定性。
  3. 图像采集与发送问题
    1. OV2640摄像头在长时间工作时会出现严重的发热问题,同时ESP32也会因为图像处理产生较大的热量。尽管添加了散热片,但依然无法满足散热需求。我们通过添加小风扇进行主动散热,解决了长时间稳定运行的挑战。
    2. 在图像读取过程中,发现使用全局变量存储图像帧(*fb)时,传递图像数据会出现失效的情况。通过改用局部变量或利用FreeRTOS传递指针的方式解决了此问题,确保了数据的准确传递。
  4. 超声波与麦克风问题
    1. 即使在停止工作后的较长时间内,超声波传感器仍然会对附近的麦克风传感器产生影响,误导系统认为有声音输入。为解决这一问题,我们将麦克风的任务与超声波的任务进行了分离,确保两者不会互相干扰,保证了系统的稳定运行。


九、学习心得

通过本次的项目,对ESP32、FreeRTOS有了更深入的理解,也对摄像头和麦克风技术有了理解。通过整个项目的进行,对人机交互这个大需求也有了更深入的理解。




附件下载
SIGNBOARD-ESP32S3-EETREE-UP.zip
ESP32-S3工程源码,使用VSCode+PIO打开
Gerber_PCB_交互标牌.zip
PCB Gerber制造文件
BOM_PCB_交互标牌.xlsx
PCB BOM器件清单
Face_Python_WebsocketServer.zip
树莓派Face recognition和Websocket服务器代码
3D_Model_SIGNBOARD.zip
外壳3D模型
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号