这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【ATOMS3R-CAMAICHATBOT】使用ESP-IDF驱动摄像头和WiF

共1条 1/1 1 跳转至

【ATOMS3R-CAMAICHATBOT】使用ESP-IDF驱动摄像头和WiFi图传

高工
2026-08-23 19:41:15     打赏

       AtomS3R-CAM的最大特点是其有一颗显眼的摄像头,以及一个带有扬声器和麦克风的底座。摄像头可以用来进行无线图像传输。这个在Arduino工程中已经实现了。但是在ESP-IDF中还没有实现,所以本文主要介绍使用ESP-IDF实现AtomS3R-CAM的无线图像传输功能。

       首先是AtomS3R-CAM的硬件介绍。AtomS3R-CAM是一款非常小巧的物联网设备。其尺寸仅为24×24×24mm,但是其功能非常强大,主要用于AI视觉语音开发。设备由控制器与语音底座组成,两者用排针相互连接。控制器板载了摄像头、9轴IMU和IR发射等。语音底座板载了音频解码芯片,主要用于麦克风和扬声器。

一、硬件介绍

       首先是设备的照片

image.png

image.png

主控芯片:ESP32-S3-PICO-1-N8R8

摄像头:0.3MP GC0308

九轴传感器:六轴姿态传感器(BMI270)+三轴地磁式传感器(BMM150)

音频编解码器:ES8311(24位分辨率,采用I2S协议)

MEMS麦克风:MSM381A3729H9BPC

扬声器:2014型腔体喇叭:1W@8Ω

红外IR:180°发射角,无遮挡最远12.46m

WiFi:2.4 GHz

二、原理图

       摄像头对应的原理图

image.png

四、代码

       本代码使用AI生成,并在本地进行验证。程序的流程是驱动摄像头GC0308,以RGB565的格式获取视频数据流。在本地将其编码为JPEG格式,通过HTTP Server服务上传生成MJPEG动态图像画面,帧数大约在5fps,每帧数据大小约在5-10KB。函数主要在main.c文件中。具体代码如下

#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "driver/gpio.h"
#include "esp_camera.h"
#include "esp_check.h"
#include "esp_event.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/task.h"
#include "img_converters.h"
#include "nvs_flash.h"
#include "sdkconfig.h"

static const char *TAG = "atoms3r_cam";

/* M5Stack AtomS3R-CAM / GC0308 pin map. */
#define CAM_POWER_N_GPIO 18
#define CAM_PIN_PWDN     (-1)
#define CAM_PIN_RESET    (-1)
#define CAM_PIN_XCLK     21
#define CAM_PIN_SIOD     12
#define CAM_PIN_SIOC     9
#define CAM_PIN_VSYNC    10
#define CAM_PIN_HREF     14
#define CAM_PIN_PCLK     40
#define CAM_PIN_D0       3   /* Y2 */
#define CAM_PIN_D1       42  /* Y3 */
#define CAM_PIN_D2       46  /* Y4 */
#define CAM_PIN_D3       48  /* Y5 */
#define CAM_PIN_D4       4   /* Y6 */
#define CAM_PIN_D5       17  /* Y7 */
#define CAM_PIN_D6       11  /* Y8 */
#define CAM_PIN_D7       13  /* Y9 */

#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT      BIT1
#define WIFI_MAXIMUM_RETRY 10

#define PART_BOUNDARY "123456789000000000000987654321"

static const char *STREAM_CONTENT_TYPE =
    "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char *STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char *STREAM_PART =
    "Content-Type: image/jpeg\r\nContent-Length: %zu\r\n\r\n";

static EventGroupHandle_t s_wifi_event_group;
static int s_wifi_retry_count;

static const char INDEX_HTML[] =
    "<!doctype html><html><head><meta charset='utf-8'>"
    "<meta name='viewport' content='width=device-width,initial-scale=1'>"
    "<title>AtomS3R-CAM</title>"
    "<style>body{margin:0;background:#111;color:#eee;font-family:sans-serif;"
    "text-align:center}h1{font-size:1.2rem}img{max-width:100%;height:auto;"
    "image-rendering:auto}</style></head>"
    "<body><h1>AtomS3R-CAM / GC0308</h1>"
    "<p><a href='/capture' style='color:#8cf'>Single JPEG</a></p>"
    "<img src='/stream' alt='MJPEG stream'></body></html>";

static framesize_t configured_frame_size(void)
{
#if defined(CONFIG_ATOMS3R_FRAME_QQVGA)
    return FRAMESIZE_QQVGA;
#elif defined(CONFIG_ATOMS3R_FRAME_VGA)
    return FRAMESIZE_VGA;
#else
    return FRAMESIZE_QVGA;
#endif
}

static esp_err_t camera_init(void)
{
    const gpio_config_t power_gpio = {
        .pin_bit_mask = 1ULL << CAM_POWER_N_GPIO,
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    ESP_RETURN_ON_ERROR(gpio_config(&power_gpio), TAG,
                        "configure camera power GPIO");

    /* POWER_N is active high for power-down: drive low to power the sensor. */
    ESP_RETURN_ON_ERROR(gpio_set_level(CAM_POWER_N_GPIO, 0), TAG,
                        "power on camera");
    vTaskDelay(pdMS_TO_TICKS(500));

    const camera_config_t config = {
        .pin_pwdn = CAM_PIN_PWDN,
        .pin_reset = CAM_PIN_RESET,
        .pin_xclk = CAM_PIN_XCLK,
        .pin_sccb_sda = CAM_PIN_SIOD,
        .pin_sccb_scl = CAM_PIN_SIOC,
        .pin_d7 = CAM_PIN_D7,
        .pin_d6 = CAM_PIN_D6,
        .pin_d5 = CAM_PIN_D5,
        .pin_d4 = CAM_PIN_D4,
        .pin_d3 = CAM_PIN_D3,
        .pin_d2 = CAM_PIN_D2,
        .pin_d1 = CAM_PIN_D1,
        .pin_d0 = CAM_PIN_D0,
        .pin_vsync = CAM_PIN_VSYNC,
        .pin_href = CAM_PIN_HREF,
        .pin_pclk = CAM_PIN_PCLK,
        .xclk_freq_hz = 20000000,
        .ledc_timer = LEDC_TIMER_0,
        .ledc_channel = LEDC_CHANNEL_0,

        /* GC0308 has no JPEG output; capture RGB565 then encode in software. */
        .pixel_format = PIXFORMAT_RGB565,
        .frame_size = configured_frame_size(),
        .jpeg_quality = 12, /* Not used for RGB565 capture. */
        .fb_count = CONFIG_ATOMS3R_FB_COUNT,
        .fb_location = CAMERA_FB_IN_PSRAM,
        .grab_mode = (CONFIG_ATOMS3R_FB_COUNT > 1)
                         ? CAMERA_GRAB_LATEST
                         : CAMERA_GRAB_WHEN_EMPTY,
        .sccb_i2c_port = 0,
    };

    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "esp_camera_init failed: %s (0x%x)",
                 esp_err_to_name(err), err);
        return err;
    }

    ESP_LOGI(TAG, "GC0308 ready: RGB565, frame size enum=%d, fb_count=%d",
             (int)config.frame_size, config.fb_count);
    return ESP_OK;
}

static void wifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data)
{
    (void)arg;

    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) {
        xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
        if (s_wifi_retry_count < WIFI_MAXIMUM_RETRY) {
            ++s_wifi_retry_count;
            ESP_LOGW(TAG, "Wi-Fi disconnected; retry %d/%d",
                     s_wifi_retry_count, WIFI_MAXIMUM_RETRY);
            esp_wifi_connect();
        } else {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        }
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        const ip_event_got_ip_t *event = (const ip_event_got_ip_t *)event_data;
        ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
        s_wifi_retry_count = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

static esp_err_t wifi_init(void)
{
    ESP_RETURN_ON_ERROR(esp_netif_init(), TAG, "esp_netif_init");
    ESP_RETURN_ON_ERROR(esp_event_loop_create_default(), TAG,
                        "create default event loop");

    const wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT();
    ESP_RETURN_ON_ERROR(esp_wifi_init(&init_config), TAG, "esp_wifi_init");
    ESP_RETURN_ON_ERROR(esp_wifi_set_storage(WIFI_STORAGE_RAM), TAG,
                        "set Wi-Fi storage");

#if defined(CONFIG_ATOMS3R_WIFI_STA)
    s_wifi_event_group = xEventGroupCreate();
    if (s_wifi_event_group == NULL) {
        return ESP_ERR_NO_MEM;
    }

    esp_netif_create_default_wifi_sta();
    ESP_RETURN_ON_ERROR(
        esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                   &wifi_event_handler, NULL),
        TAG, "register Wi-Fi event handler");
    ESP_RETURN_ON_ERROR(
        esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
                                   &wifi_event_handler, NULL),
        TAG, "register IP event handler");

    wifi_config_t wifi_config = {0};
    strlcpy((char *)wifi_config.sta.ssid, CONFIG_ATOMS3R_WIFI_SSID,
            sizeof(wifi_config.sta.ssid));
    strlcpy((char *)wifi_config.sta.password, CONFIG_ATOMS3R_WIFI_PASSWORD,
            sizeof(wifi_config.sta.password));
    wifi_config.sta.threshold.authmode = WIFI_AUTH_OPEN;
    wifi_config.sta.pmf_cfg.capable = true;
    wifi_config.sta.pmf_cfg.required = false;

    ESP_RETURN_ON_ERROR(esp_wifi_set_mode(WIFI_MODE_STA), TAG,
                        "set station mode");
    ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_STA, &wifi_config), TAG,
                        "set station config");
    ESP_RETURN_ON_ERROR(esp_wifi_start(), TAG, "start station");
    ESP_RETURN_ON_ERROR(esp_wifi_set_ps(WIFI_PS_NONE), TAG,
                        "disable Wi-Fi power saving");

    ESP_LOGI(TAG, "Connecting to SSID '%s'", CONFIG_ATOMS3R_WIFI_SSID);
    EventBits_t bits = xEventGroupWaitBits(
        s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE,
        pdFALSE, portMAX_DELAY);
    if ((bits & WIFI_CONNECTED_BIT) == 0) {
        ESP_LOGE(TAG, "Could not connect to SSID '%s'",
                 CONFIG_ATOMS3R_WIFI_SSID);
        return ESP_FAIL;
    }
#else
    esp_netif_t *netif = esp_netif_create_default_wifi_ap();
    if (netif == NULL) {
        return ESP_ERR_NO_MEM;
    }

    const size_t password_len = strlen(CONFIG_ATOMS3R_WIFI_PASSWORD);
    if (password_len > 0 && password_len < 8) {
        ESP_LOGE(TAG, "SoftAP password must be empty or at least 8 characters");
        return ESP_ERR_INVALID_ARG;
    }

    wifi_config_t wifi_config = {0};
    strlcpy((char *)wifi_config.ap.ssid, CONFIG_ATOMS3R_WIFI_SSID,
            sizeof(wifi_config.ap.ssid));
    strlcpy((char *)wifi_config.ap.password, CONFIG_ATOMS3R_WIFI_PASSWORD,
            sizeof(wifi_config.ap.password));
    wifi_config.ap.ssid_len = strlen(CONFIG_ATOMS3R_WIFI_SSID);
    wifi_config.ap.channel = CONFIG_ATOMS3R_WIFI_AP_CHANNEL;
    wifi_config.ap.max_connection = 2;
    wifi_config.ap.authmode = password_len == 0 ? WIFI_AUTH_OPEN
                                                : WIFI_AUTH_WPA2_PSK;

    ESP_RETURN_ON_ERROR(esp_wifi_set_mode(WIFI_MODE_AP), TAG, "set AP mode");
    ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_AP, &wifi_config), TAG,
                        "set AP config");
    ESP_RETURN_ON_ERROR(esp_wifi_start(), TAG, "start AP");
    ESP_RETURN_ON_ERROR(esp_wifi_set_ps(WIFI_PS_NONE), TAG,
                        "disable Wi-Fi power saving");

    esp_netif_ip_info_t ip_info;
    ESP_RETURN_ON_ERROR(esp_netif_get_ip_info(netif, &ip_info), TAG,
                        "read AP IP");
    ESP_LOGI(TAG, "SoftAP '%s' ready; open http://" IPSTR "/",
             CONFIG_ATOMS3R_WIFI_SSID, IP2STR(&ip_info.ip));
#endif

    return ESP_OK;
}

static esp_err_t index_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html; charset=utf-8");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");
    return httpd_resp_send(req, INDEX_HTML, HTTPD_RESP_USE_STRLEN);
}

static esp_err_t capture_handler(httpd_req_t *req)
{
    camera_fb_t *fb = esp_camera_fb_get();
    if (fb == NULL) {
        ESP_LOGE(TAG, "Camera capture failed");
        httpd_resp_send_500(req);
        return ESP_FAIL;
    }

    uint8_t *jpeg_buf = NULL;
    size_t jpeg_len = 0;
    bool must_free = false;

    if (fb->format == PIXFORMAT_JPEG) {
        jpeg_buf = fb->buf;
        jpeg_len = fb->len;
    } else {
        must_free = frame2jpg(fb, CONFIG_ATOMS3R_JPEG_QUALITY,
                              &jpeg_buf, &jpeg_len);
        if (!must_free) {
            ESP_LOGE(TAG, "JPEG conversion failed");
            esp_camera_fb_return(fb);
            httpd_resp_send_500(req);
            return ESP_FAIL;
        }
    }

    httpd_resp_set_type(req, "image/jpeg");
    httpd_resp_set_hdr(req, "Content-Disposition",
                       "inline; filename=capture.jpg");
    httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");
    esp_err_t result = httpd_resp_send(req, (const char *)jpeg_buf, jpeg_len);

    if (must_free) {
        free(jpeg_buf);
    }
    esp_camera_fb_return(fb);
    return result;
}

static esp_err_t stream_handler(httpd_req_t *req)
{
    esp_err_t result = httpd_resp_set_type(req, STREAM_CONTENT_TYPE);
    if (result != ESP_OK) {
        return result;
    }
    httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
    httpd_resp_set_hdr(req, "Cache-Control", "no-store");

    int64_t last_frame_us = esp_timer_get_time();
    ESP_LOGI(TAG, "MJPEG client connected");

    while (result == ESP_OK) {
        camera_fb_t *fb = esp_camera_fb_get();
        if (fb == NULL) {
            ESP_LOGE(TAG, "Camera capture failed");
            result = ESP_FAIL;
            break;
        }

        uint8_t *jpeg_buf = NULL;
        size_t jpeg_len = 0;
        bool must_free = false;

        if (fb->format == PIXFORMAT_JPEG) {
            jpeg_buf = fb->buf;
            jpeg_len = fb->len;
        } else {
            must_free = frame2jpg(fb, CONFIG_ATOMS3R_JPEG_QUALITY,
                                  &jpeg_buf, &jpeg_len);
            if (!must_free) {
                ESP_LOGE(TAG, "JPEG conversion failed");
                esp_camera_fb_return(fb);
                result = ESP_FAIL;
                break;
            }
        }

        char part_header[96];
        int header_len = snprintf(part_header, sizeof(part_header),
                                  STREAM_PART, jpeg_len);
        if (header_len < 0 || (size_t)header_len >= sizeof(part_header)) {
            result = ESP_FAIL;
        }
        if (result == ESP_OK) {
            result = httpd_resp_send_chunk(req, STREAM_BOUNDARY,
                                           strlen(STREAM_BOUNDARY));
        }
        if (result == ESP_OK) {
            result = httpd_resp_send_chunk(req, part_header,
                                           (size_t)header_len);
        }
        if (result == ESP_OK) {
            result = httpd_resp_send_chunk(req, (const char *)jpeg_buf,
                                           jpeg_len);
        }

        if (must_free) {
            free(jpeg_buf);
        }
        esp_camera_fb_return(fb);

        const int64_t now_us = esp_timer_get_time();
        const int64_t frame_ms = (now_us - last_frame_us) / 1000;
        last_frame_us = now_us;
        const float fps = frame_ms > 0 ? 1000.0f / (float)frame_ms : 0.0f;
        ESP_LOGI(TAG, "MJPEG: %zu KB, %" PRId64 " ms (%.1f fps)",
                 jpeg_len / 1024, frame_ms, fps);
    }

    ESP_LOGI(TAG, "MJPEG client disconnected: %s",
             esp_err_to_name(result));
    return result;
}

static esp_err_t http_server_start(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.server_port = CONFIG_ATOMS3R_HTTP_PORT;
    config.stack_size = 10 * 1024; /* JPEG conversion runs in this task. */
    config.max_open_sockets = 4;
    config.lru_purge_enable = true;
    config.recv_wait_timeout = 10;
    config.send_wait_timeout = 10;

    httpd_handle_t server = NULL;
    ESP_RETURN_ON_ERROR(httpd_start(&server, &config), TAG,
                        "start HTTP server");

    const httpd_uri_t index_uri = {
        .uri = "/",
        .method = HTTP_GET,
        .handler = index_handler,
        .user_ctx = NULL,
    };
    const httpd_uri_t capture_uri = {
        .uri = "/capture",
        .method = HTTP_GET,
        .handler = capture_handler,
        .user_ctx = NULL,
    };
    const httpd_uri_t stream_uri = {
        .uri = "/stream",
        .method = HTTP_GET,
        .handler = stream_handler,
        .user_ctx = NULL,
    };

    ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &index_uri), TAG,
                        "register / handler");
    ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &capture_uri), TAG,
                        "register /capture handler");
    ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &stream_uri), TAG,
                        "register /stream handler");

    ESP_LOGI(TAG, "HTTP camera server listening on port %d",
             CONFIG_ATOMS3R_HTTP_PORT);
    return ESP_OK;
}

void app_main(void)
{
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES ||
        err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);

    ESP_ERROR_CHECK(camera_init());
    ESP_ERROR_CHECK(wifi_init());
    ESP_ERROR_CHECK(http_server_start());
}

五、工程配置

       使用的ESP-IDF版本为5.5.5,也可以使用其他的版本,但是版本号跨度不要太大。我使用的版本是5.5.4。下载好工程文件后,需要按照如下步骤配置工程。

1. 首先使用IDF_v5.5.4_Powershell的cd命令定位到工程文件根目录下。使用idf.py set-target esp32s3指令设置目标芯片型号,在此过程中也会按照idf_component.yml文件内容下载对应的组件。组件代码会放在根目录managed_components文件夹中。不需要自己将这些组件源码导入到工程中,它会自己导入的。

2. 使用idf.py menuconfig配置WiFi名称和密码等

image.png

        也可以设置摄像头的画面效果。

3. 使用指令idf.py build进行代码编译

4. 使用指令idf.py -p COMx flash monitor烧录程序,其中的x代表实际的串口号。

六、效果展示

       成功编译和烧写后,可以通过串口打印获取AtomS3R-CAM的IP地址。在浏览器中输入对应的IP地址即可打开AtomS3R-CAM上传的视频画面。需要注意的是AtomS3R-CAM只能连接2.4GHz的无线网络。画面显示效果如下

image.png

       串口会实时打印数据流的大小和帧数的,打印内容如下

image.png

七、工程文件

       工程文件:atoms3r_cam_mjpeg_idf.zip

       下载后可以按照工程配置直接使用。





关键词: ATOMS3R-CAM    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]