2026寒假练 - 基于人工智能硬件实验套件平台制作的氛围灯
该项目使用了人工智能套件,实现了手势识别和语音识别两种模式控制LED的设计,它的主要功能为:使用人工智能硬件中的ESP32S3实现了,手势识别和语音命令字控制LED灯效果的设计。
标签
esp-idf
手势识别
语音识别
ESP32S3
LED特效
happy
更新2026-03-24
52

硬件介绍:

这是我参加2026年寒假一起练活动,使用的人工智能硬件实验套件平台。核心主控是一个Seeed XIAO ESP32-S3 Sensen。这个模块是一个集成了摄像头传感器、数字麦克风和支持 SD 卡的功能。处理器 ESP32-S3R8 SoC,支持 2.4GHz WiFi 和低功耗蓝牙® BLE 5.0 双模,适用于多种无线应用。电路板还配有数字麦克风,用于语音感应和音频识别。

任务选择:

我选择的是任务二:智能语音氛围灯带。使用语音、手势、按键等方式控制LED灯带显示模式

任务分解:

image.png

将任务分解为三大块。第一块是语音识别,负责监听环境中的语音命令字。第二块是手势识别,通过摄像头发现手势信息。最后一块是LED驱动,通过RMT方式驱动LED灯条,预先设定了几种灯光效果,通过消息队列或按键切换效果、亮度、颜色、速度等信息。任务使用Vscode+ESP-IDF5.5实现。

任务实现:

1、LED驱动。RGB灯带,是由WS2812构成的,一共有10颗,只需要一个管脚(GPIO8)即可驱动。这里使用RMT方式驱动。设定了5种灯光效果进行切换。预设了几个颜色,用来展示。

#define LED_STRIP_GPIO_PIN 8         // 默认 GPIO 引脚
#define LED_STRIP_LED_COUNT 10        // 默认 LED 数量
// 灯光效果枚举
typedef enum {
    EFFECT_STATIC,      // 静态颜色
    EFFECT_RAINBOW,     // 彩虹效果
    EFFECT_BREATHING,   // 呼吸灯效果
    EFFECT_BLINKING,    // 闪烁效果
    EFFECT_CHASING,     // 追逐效果
} LightEffect;
// 常用颜色表
const Color ColorTable[] = {
    {230, 0, 0},     // 中国红 (Red)
    {0, 140, 140},   // 马尔斯绿 (Green)
    {0, 47, 167},    // 克莱因蓝 (Blue)
    {0, 49, 83},     // 普鲁士蓝
    {129, 216, 208}, // 蒂芙尼蓝
    {251, 210, 206}, // 申布伦黄
    {10, 79, 87},    // 爱马仕橙
    {64, 224, 208},  // 只此青绿
};
WS2812::WS2812(uint16_t ledCount, uint8_t pin)
    : ledCount(ledCount), pin(pin), currentEffect(EFFECT_STATIC),
      brightness(20), speed(50), r(0), g(0), b(0),
      lastUpdate(0), currentColorIndex(0), ledStrip(nullptr) {}


void WS2812::begin()
{
    // LED 灯带通用配置(不使用指定初始化语法)
    led_strip_config_t strip_config = {};
    strip_config.strip_gpio_num = pin;
    strip_config.max_leds = ledCount;
    strip_config.led_model = LED_MODEL_WS2812;
    strip_config.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB;
    strip_config.flags.invert_out = false;


    // RMT 后端配置(同样避免指定初始化语法)
    led_strip_rmt_config_t rmt_config = {};
    rmt_config.clk_src = RMT_CLK_SRC_DEFAULT;
    rmt_config.resolution_hz = LED_STRIP_RMT_RES_HZ;
    rmt_config.mem_block_symbols = LED_STRIP_MEMORY_BLOCK_WORDS;
    rmt_config.flags.with_dma = LED_STRIP_USE_DMA;


    // 创建 LED 灯带对象
    ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &ledStrip));
    ESP_LOGI(TAGLED, "WS2812 initialized with %d LEDs on pin %d", ledCount, pin);
}

灯光效果可以变换的内容有:灯光效果、速度、亮度、颜色。所以设定了5个命令字,用来控制这些变量。1:切换灯光效果   2:修改亮度   3:修改速度   4:修改颜色   5:关闭灯光

image.png

2:按键控制。套件中提供的按键有四个按键,通过电阻分压的方式连接到GPIO1管脚上。但是按键模块是按照5V输入电压设计的,现在接的3.3V电压,导致无法使用全部的按键,这里参考别人的做法,按键模块的电源正负极对调,这样四个按键就都能使用了。这里还有个问题,控制LED灯,有5个命令字,这里只有4个按键,所以,按键只能发送4个命令字,无法关闭LED灯。

static const char *ButtonTAG = "Button";
// ADC校准参数
static esp_adc_cal_characteristics_t adc1_chars;
static const adc1_channel_t channel = ADC1_CHANNEL_0;
// 按键电压阈值定义 (根据实际电阻分压电路调整)
#define KEY_THRESHOLD_1 2400 // 第一个按键触发的ADC值
#define KEY_THRESHOLD_2 2000 // 第二个按键触发的ADC值
#define KEY_THRESHOLD_3 1500 // 第三个按键触发的ADC值
#define KEY_THRESHOLD_4 800  // 第四个按键触发的ADC值
#define KEY_DEBOUNCE_TIME 50 // 防抖时间(ms)
#define KEY_REPEAT_DELAY 400 // 按键重复触发间隔(ms)
// 按键状态枚举
typedef enum
{
    KEY_NONE = 0,
    KEY_1,
    KEY_2,
    KEY_3,
    KEY_4
} key_state_t;

3:语音控制。这里语音控制使用了乐鑫提供的语音识别库来进行语音识别。esp-sr组件添加到自己的项目中去。ESP-SR 是乐鑫科技推出的一个智能语音识别框架,专为 ESP32 ESP32-S3 芯片优化,支持高效的语音处理和命令识别功能。

image.png

参考着esp-sr中的例程修改代码。这里参考的是ESP32S3-EYE这块开发板的代码,需要参考着修改Seeed XIAO ESP32-S3 Sensen的I2S读取的部分硬件代码。重点修改这两个函数:

static esp_err_t bsp_i2s_init(i2s_port_t i2s_num, uint32_t sample_rate, int channel_format, int bits_per_chan)
{
    esp_err_t ret_val = ESP_OK;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
    i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(i2s_num, I2S_ROLE_MASTER);
    ret_val |= i2s_new_channel(&chan_cfg, NULL, &rx_handle);
    i2s_pdm_rx_config_t pdm_rx_cfg = {
        .clk_cfg = I2S_PDM_RX_CLK_DEFAULT_CONFIG(s_play_sample_rate),
        /* The data bit-width of PDM mode is fixed to 16 */
        .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
        .gpio_cfg = {
            .clk = GPIO_NUM_42,
            // Only ESP32-S3 can support 4-line PDM RX
            .dins = {
                GPIO_NUM_41,
            },
            .invert_flags = {
                .clk_inv = false,
            },
        },
    };
    ret_val |= i2s_channel_init_pdm_rx_mode(rx_handle, &pdm_rx_cfg);
    ret_val |= i2s_channel_enable(rx_handle);
#else
    // i2s_config_t i2s_config = I2S_CONFIG_DEFAULT(16000, I2S_CHANNEL_FMT_ONLY_LEFT, 32);
    i2s_config_t i2s_config = I2S_CONFIG_DEFAULT(sample_rate, I2S_CHANNEL_FMT_ONLY_LEFT, bits_per_chan);
    i2s_pin_config_t pin_config = {
        .bck_io_num = GPIO_I2S_SCLK,
        .ws_io_num = GPIO_I2S_LRCK,
        .data_out_num = GPIO_I2S_DOUT,
        .data_in_num = GPIO_I2S_SDIN,
        .mck_io_num = GPIO_I2S_MCLK,
    };
    ret_val |= i2s_driver_install(i2s_num, &i2s_config, 0, NULL);
    ret_val |= i2s_set_pin(i2s_num, &pin_config);
#endif
    return ret_val;
}

esp_err_t bsp_get_feed_data(bool is_get_raw_channel, int16_t *buffer, int buffer_len)
{
    // afe入口是双声道,但是这里获取的声音是单声道的。
    esp_err_t ret = ESP_OK;
    size_t    bytes_read;
    int       audio_chunksize = buffer_len / (sizeof(int32_t));
    // ret = i2s_channel_read(rx_handle, buffer, audio_chunksize*sizeof(int16_t), &bytes_read, portMAX_DELAY);
    ret = i2s_channel_read(rx_handle, buffer, audio_chunksize*sizeof(int16_t), &bytes_read, portMAX_DELAY);
    //扩展到双声道
    for(int i = audio_chunksize-1; i >=0; i--){
        buffer[i*2] = buffer[i];
        buffer[i*2+1] = 0;
    }
    return ret;
}

然后选择唤醒词,可以在menuconfig中选择。可以选择两个唤醒词,我这里使用的是:“小爱同学”,“小鸭小鸭”。命令字使用中文命令字,可以直接使用拼音输入命令字。这里给了5个命令字,用来对应控制LED灯的5个命令。使用消息队列进行传递。

image.png

image.png

void detect_Task(void *arg)
{
    char command;
    esp_afe_sr_data_t *afe_data = (esp_afe_sr_data_t *)arg;
    int afe_chunksize = afe_handle->get_fetch_chunksize(afe_data);
    char *mn_name = esp_srmodel_filter(models, ESP_MN_PREFIX, ESP_MN_CHINESE);
    if (NULL == mn_name)
    {
        ESP_LOGE(VCTag, "No multinet model found");
        return;
    }
    esp_mn_iface_t *multinet = esp_mn_handle_from_name(mn_name);
    model_iface_data_t *model_data = multinet->create(mn_name, 6000); // 第二个参数6000 是指持续时间6秒
    esp_mn_commands_clear();
    esp_mn_commands_add(1, (char *)"qie huan xiao guo");   // 切换效果
    esp_mn_commands_add(2, (char *)"tiao zheng liang du"); // 调整亮度
    esp_mn_commands_add(2, (char *)"liang du tiao zheng");   // 亮度降低
    esp_mn_commands_add(3, (char *)"xiu gai su du");       // 修改速度
    esp_mn_commands_add(3, (char *)"jia kuai su du");      // 加快速度
    esp_mn_commands_add(4, (char *)"xiu gai yan se");      // 修改颜色
    esp_mn_commands_add(4, (char *)"gai bian yan se");     // 改变颜色
    esp_mn_commands_add(5, (char *)"guan bi deng guang");  // 关闭灯光
    esp_mn_commands_update();
    multinet->print_active_speech_commands(model_data); // 打印当前正在使用的所有命令词条
    int mu_chunksize = multinet->get_samp_chunksize(model_data);
    assert(mu_chunksize == afe_chunksize);


    while (task_flag)
    {
        afe_fetch_result_t *res = afe_handle->fetch(afe_data);
        if (!res || res->ret_value == ESP_FAIL)
        {
            printf("fetch error!\n");
            break;
        }
        if (res->wakeup_state == WAKENET_DETECTED)
        {
            ESP_LOGI(VCTag, "Wakeword detected ! model index:%d,word index:%d. Please speak:", res->wakenet_model_index, res->wake_word_index);
            multinet->clean(model_data); // clean all status of multinet
        }
        if (res->raw_data_channels == 1 && res->wakeup_state == WAKENET_DETECTED)
        {
            detect_flag = 1;
        }
        else if (res->raw_data_channels > 1 && res->wakeup_state == WAKENET_CHANNEL_VERIFIED)
        {
            // For a multi-channel AFE, it is necessary to wait for the channel to be verified.
            ESP_LOGI(VCTag, "AFE_FETCH_CHANNEL_VERIFIED, channel index: %d\n", res->trigger_channel_id);
            detect_flag = 1;
        }
        if (detect_flag == 1)
        {
            esp_mn_state_t mn_state = multinet->detect(model_data, res->data);
            if (mn_state == ESP_MN_STATE_DETECTING)
            {
                continue;
            }
            if (mn_state == ESP_MN_STATE_DETECTED)
            {
                esp_mn_results_t *mn_result = multinet->get_results(model_data);
                for (int i = 0; i < mn_result->num; i++)
                {
                    printf("TOP %d, command_id: %d, phrase_id: %d, string:%s prob: %f\n",
                           i + 1, mn_result->command_id[i], mn_result->phrase_id[i], mn_result->string, mn_result->prob[i]);
                    command = mn_result->command_id[i];
                    xQueueSend(xQueueCommand, &command, portMAX_DELAY);
                }
                printf("\n-----------listening-----------\n");
            }
            if (mn_state == ESP_MN_STATE_TIMEOUT)
            {
                esp_mn_results_t *mn_result = multinet->get_results(model_data);
                ESP_LOGI(VCTag, "timeout, string:%s", mn_result->string);
                afe_handle->enable_wakenet(afe_data);
                detect_flag = 0;
                printf("\n-----------awaits to be waken up-----------\n");
                continue;
            }
        }
    }
    if (model_data)
    {
        multinet->destroy(model_data);
        model_data = NULL;
    }
    vTaskDelete(NULL);
}

四:摄像头读取。做手势识别需要用到摄像头。这里参考着esp-who开源项目,进行读取摄像头。每读取一帧摄像头的图片,就通过消息队列传输给手势识别,进行识别。

image.png


五:手势识别。手势识别是使用乐鑫提供的esp-dl组件来实现的。esp-dl组件中提供了“hand_detect”和“hand_gesture_recognition”两个模型,前者用来侦测是否有手出现,后者用来识别手势。目前能识别的手势有“ok”,“one”,“two”,“three”,“four”,“five”。去掉“ok”手势,用剩下的5个手势对应控制LED灯的5个命令,使用消息队列传递。当摄像头通过消息队列传输来的每一帧图片,通过模型进行解析,识别到手势后,就产生命令字。

static void task_process_handler(void *arg)
{
    camera_fb_t *frame = NULL;
    char command;
    dl::image::img_t img;
    img.pix_type = dl::image::DL_IMAGE_PIX_TYPE_RGB565;
    HandDetect *hand_detect = new HandDetect();
    auto hand_gesture_recognizer = new HandGestureRecognizer(HandGestureCls::MOBILENETV2_0_5_S8_V1);
    while (true)
    {
        if (xQueueReceive(xQueueFrameI, &frame, portMAX_DELAY))
        {
            img.data = frame->buf;
            img.width = frame->width;
            img.height = frame->height;
            auto &hand_detect_results = hand_detect->run(img);
            auto current_time = std::chrono::steady_clock::now();
            if (std::chrono::duration_cast<std::chrono::seconds>(current_time - last_detection_time).count() >= 1)
            {
                if (hand_detect_results.size() == 1)
                {
                    std::vector<dl::cls::result_t> results = hand_gesture_recognizer->recognize(img, hand_detect_results); // 移除 & 符号
                    for (const auto &res : results)
                    {
                        ESP_LOGI(TAG, "category: %s, score: %f", res.cat_name, res.score);
                        // 判断 res.cat_name 是否为指定的手势名称
                        if (strcmp(res.cat_name, "one") == 0)
                        {
                            command = 1;
                            xQueueSend(xQueueCmdO, &command, portMAX_DELAY);
                        }else if (strcmp(res.cat_name, "two") == 0 ){
                            command = 2;
                            xQueueSend(xQueueCmdO, &command, portMAX_DELAY);
                        }else if (strcmp(res.cat_name, "three") == 0 ){
                            command = 3;
                            xQueueSend(xQueueCmdO, &command, portMAX_DELAY);
                        }else if (strcmp(res.cat_name, "four") == 0 ){
                            command = 4;
                            xQueueSend(xQueueCmdO, &command, portMAX_DELAY);
                        }else if (strcmp(res.cat_name, "five") == 0 ){
                            command = 5;
                            xQueueSend(xQueueCmdO, &command, portMAX_DELAY);
                        }
                    }
                }
            }
        }
        esp_camera_fb_return(frame);
    }
}


void register_Dl_detection(const QueueHandle_t frame_i,
                           const QueueHandle_t frame_o)
{
    xQueueFrameI = frame_i;
    xQueueCmdO = frame_o;
    xTaskCreatePinnedToCore(task_process_handler, "dl_process", 4 * 1024, NULL, 2, NULL, 0);
}

六:OLED显示。这里显示屏幕为一个128X64的单色OLED屏幕,连接到ESP32S3的I2C总线上,这里我使用lvgl组件来进行图像显示。将灯光效果、速度、亮度三个值分别做成三个标签,然后动态刷新屏幕。

image.png

七:灯光逻辑控制。因为灯光由多从因素控制,所以这里做了一下优先级排序。按键控制优先级最低,所以按键命令最先接收。然后是消息队列来的控制命令。这里的命令优先级最高。这样就可以解决命令字冲突问题。速度、亮度两个控制变量都采用单向循环控制,即只能减少,不能增加。当减少到0时,重新回到最大值,再次循环。颜色控制对彩虹灯无效。关闭灯光仅仅只是将亮度设置为0。每种灯光效果,让AI帮忙想出了牛逼点的描述,因为这里的lvgl使用的标签当字符多了的时候可以通过左右滚动来展示。

 while (true)
    {
        command = ButtonCtl::GetInstance().key_scan();          // 按键处理
        if(xQueueReceive(xQueueCmd, &command, 0)){
                    ESP_LOGI(TAG, "接收到命令字%d ", command);
        }
        switch (command)
        {
        case 1: // 按键1 切换效果 同时修改亮度到最大
        {
            LightEffect currEfct = strip.getEffect();
            LightEffect nextEfct = (LightEffect)((currEfct + 1) % 5);
            DisplayCtl::GetInstance().GetDisplay()->SetName(LightEffectName[nextEfct]);
            strip.setEffect(nextEfct);
            DisplayCtl::GetInstance().GetDisplay()->SetBright(strip.getBrightness());
            break;
        }
        case 2: // 按键2 修改速度 速度增加5  速度不能小于0
        {
            int newSpeed=0;
            if(strip.getSpeed() ==0 ) newSpeed = 50;
            else newSpeed = (strip.getSpeed() > 5) ? (strip.getSpeed() - 5) : 0;
            DisplayCtl::GetInstance().GetDisplay()->SetSpeed(newSpeed);
            strip.setSpeed(newSpeed); // 设置新速度
            break;
        }
        case 3:    // 按键3 亮度
        {
            int newBright = (strip.getBrightness() >5) ? (strip.getBrightness() - 5) : 0;
            DisplayCtl::GetInstance().GetDisplay()->SetBright(newBright);
            strip.setBrightness(newBright); // 亮度
            break;
        }
        case 4:    // 按键4 修改颜色 同时亮度会调整到最大
        {
            strip.setColor();
            DisplayCtl::GetInstance().GetDisplay()->SetBright(strip.getBrightness());
            break;
        }
        case 5:    // 按键5 关闭灯光
        {
            strip.setBrightness(0);
            DisplayCtl::GetInstance().GetDisplay()->SetBright(0);
            break;
        }


        default:
            // 忽略无效按键或添加默认处理逻辑
            break;
        }
        strip.update();
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }

效果展示:

9fa6c4c3e3a1013aa19884ff20a845ec.jpg

5d4d8d7511c314e1a5dce9cd222c5572.jpg

eab65b432984804fe80191e2473457e2.jpg

9863f5c012dd9111546d57d85d3489c2.jpg

d90ce96432c58eaae4261d934157ce6a.png

存在的问题:

问题1:ADC按键感觉很奇怪,似乎会漂移。每个按键设置了400的宽度,依然会存在启动后,按键ADC获取值不在设定范围内,不明白为啥。尤其按键1 从2000~2400都遇到过。

问题2:使用乐鑫官方提供的AI模型,很好用,效果很好。但是不知道为啥,手势识别和语音识别一起启动,手势识别就正常;语音识别中就只能唤醒,无法命令字识别。

问题3:LED灯的颜色问题。LED灯使用RGB构成具体的颜色,但是需要修改亮度,于是就将RGB转换为HSV模式,然后降低V的值,再转换回RGB模式,对纯色(R、G、B)还好,其它颜色,亮度一改变,颜色就跟着变化了。

问题4:速度问题。当开启了手势识别和语音识别。灯光效果控制速度就会被拖慢。但是语音识别因为有实时性,所以两个任务(监听任务、语音识别任务)优先级为最高,且分布在两个不同的核上。摄像头进程优先级次之,这样LED灯光的优先级就最低了,导致灯光跳动会卡顿。

心得体会

感谢电子森林提供的这套“人工智能硬件实验套件平台”。既能体验AI编程的快了,又有小车玩。期待各位老师的作品,好期望实现平衡小车的功能。


附件下载
camled-0210.zip
团队介绍
单片机爱好者
团队成员
happy
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号