AtomS3R-CAM的最大特点是其有一颗显眼的摄像头,以及一个带有扬声器和麦克风的底座。摄像头可以用来进行无线图像传输。这个在Arduino工程中已经实现了。但是在ESP-IDF中还没有实现,所以本文主要介绍使用ESP-IDF实现AtomS3R-CAM的无线图像传输功能。
首先是AtomS3R-CAM的硬件介绍。AtomS3R-CAM是一款非常小巧的物联网设备。其尺寸仅为24×24×24mm,但是其功能非常强大,主要用于AI视觉语音开发。设备由控制器与语音底座组成,两者用排针相互连接。控制器板载了摄像头、9轴IMU和IR发射等。语音底座板载了音频解码芯片,主要用于麦克风和扬声器。
一、硬件介绍
首先是设备的照片


主控芯片: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
二、原理图
摄像头对应的原理图

四、代码
本代码使用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名称和密码等

也可以设置摄像头的画面效果。
3. 使用指令idf.py build进行代码编译
4. 使用指令idf.py -p COMx flash monitor烧录程序,其中的x代表实际的串口号。
六、效果展示
成功编译和烧写后,可以通过串口打印获取AtomS3R-CAM的IP地址。在浏览器中输入对应的IP地址即可打开AtomS3R-CAM上传的视频画面。需要注意的是AtomS3R-CAM只能连接2.4GHz的无线网络。画面显示效果如下

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

七、工程文件
工程文件:atoms3r_cam_mjpeg_idf.zip
下载后可以按照工程配置直接使用。
我要赚赏金
