AtomS3R-M12 开发板集成的 M12 摄像头模组采用 OmniVision OV3660 CMOS 图像传感器,最大分辨率 3 MP(2048 × 1536),配合 120° 广角 M12 镜头,可在 30 帧/秒的速率下输出多种格式的图像数据。
1.1 传感器概述
OV3660 是一款低功耗 CMOS 图像传感器,有效像素 3 MP,最大输出分辨率 2048 × 1536。传感器通过 8 位并行 DVP(Digital Video Port)接口输出图像数据,并通过 SCCB(兼容 I2C)总线完成寄存器配置与参数调节,可调节的项目包括输出分辨率、曝光、增益、白平衡、亮度、对比度与饱和度等。
1.2 光学参数
模组采用 M12 接口镜头,光圈值 F2.4,焦距 1.8 ± 5% mm,视场角(FOV)达 120°,属于大广角设计,在相同安装距离下能够覆盖更宽的画面范围。M12 镜头座支持更换不同焦距与视场角的镜头,便于根据应用需求灵活调整取景范围。
1.3 图像输出格式
传感器支持 RAW RGB、RGB565、RGB555、RGB444、CCIR656、YCbCr422 以及压缩格式等多种输出方式。在 Arduino 与 ESP-IDF 开发中,通常选择 JPEG 压缩输出,以降低总线带宽与内存占用,并可由 ESP32-S3 的硬件加速完成编码。
1.4 接口与引脚连接
摄像头通过 8 位并行数据总线与 ESP32-S3 主控连接。SCCB 配置总线使用 G12 作为数据线 SDA、G9 作为时钟线 SCL;像素时钟 PCLK 使用 G40,行同步信号 HREF 使用 G14,场同步信号 VSYNC 使用 G10,主时钟 XCLK 使用 G21,电源使能信号 POWER_N 使用 G18。
8 位图像数据线的连接关系为:Y2 对应 G3,Y3 对应 G42,Y4 对应 G46,Y5 对应 G48,Y6 对应 G4,Y7 对应 G17,Y8 对应 G11,Y9 对应 G13。开发时这些引脚需在程序中的 camera_config_t 结构体里逐一定义,否则将出现初始化失败或图像错位。
1.5 与主控的协同工作
OV3660 输出的大尺寸图像帧需要缓存在 ESP32-S3 的 8 MB PSRAM 中。配合双核 240 MHz 处理器与硬件 JPEG 编码器,开发板可以实现较高分辨率下的连续采集。这也是使用摄像头功能时必须正确配置 PSRAM 工作模式(OPI PSRAM)的根本原因;若 PSRAM 模式配置错误,摄像头初始化通常会直接失败。
第二章 产品特点
AtomS3R-M12 上的 OV3660 摄像头模组在同类小型开发板中具有较为均衡的性能与体积表现,主要特点如下。
2.1 3 MP 高分辨率成像
有效像素 3 MP,最高可输出 2048 × 1536 的图像,相比常见的 30 万像素摄像头,画面细节明显更丰富,可用于文字识别、目标轮廓提取等对清晰度要求较高的场景。
2.2 120° 大广角取景
120° 视场角带来更宽的覆盖范围,在近距离安装时即可拍摄较大区域,适合室内监控、桌面全幅采集等应用;同时广角镜头也存在一定的桶形畸变,实际使用中需结合畸变测评结果判断可用性。
2.3 30 帧/秒流畅输出
模组支持最高 30 帧/秒的输出帧率,能够满足实时预览、动态目标跟踪等应用的基本流畅度要求。实际帧率会随分辨率提升而下降,高分辨率下受总线带宽与内存拷贝速度限制更为明显。
2.4 多种输出格式与硬件加速
支持 RAW RGB、RGB565/555/444、CCIR656、YCbCr422 与压缩格式输出。配合 ESP32-S3 的硬件 JPEG 编码能力,可在保证画质的同时显著降低数据传输量,是嵌入式视觉项目的常用配置方式。
2.5 UVC 免驱即插即用
出厂固件已内置 UVC(USB Video Class)功能,通过 USB Type-C 连接电脑后即可作为标准摄像头使用,无需安装额外驱动,可用于快速验证成像效果,也可作为日常视频会议摄像头。
2.6 M12 可更换镜头
采用 M12 标准镜头接口,可根据需求更换不同焦距、不同视场角的镜头,例如更换长焦镜头以获得更远距离的细节,或更换更广视角镜头以获得更大的覆盖范围。
2.7 与九轴传感器融合
开发板同时板载 BMI270 六轴姿态传感器与 BMM150 三轴地磁传感器。摄像头可与九轴传感器数据同步采集,使图像信息与姿态、方向信息相互补充,适合运动感知、稳定补偿类应用。
第三章 应用场景
基于 OV3660 摄像头的成像能力与开发板整体配置,该摄像头模组可应用于以下场景。
3.1 家庭与商业视觉监控
通过 Wi-Fi 图传将摄像头画面推送到浏览器或移动端,可实现远程实时查看,适用于家庭安防、店铺监控、仓库与设备状态观察等场景。
3.2 机器视觉与图像识别
3 MP 分辨率与广角取景为 OCR 文本识别、目标检测、人脸检测等算法提供了较好的图像输入条件。图像可在端侧做初步处理,也可上传至云端完成识别任务。
3.3 教育与创客实验
结合 Arduino IDE 与图形化编程平台,可用于摄像头初始化、图像采集、分辨率切换、帧率统计等教学实验,是学习嵌入式视觉与物联网开发的合适载体。
3.4 智能家居集成
可将设备接入 Home Assistant 等智能家居平台,作为摄像头节点参与自动化联动,例如触发拍摄、画面抓拍存档与状态推送。
3.5 图像采集与数据记录
可用于定时抓拍、周期性图像存档、实验数据采集等用途,配合存储与网络能力可构建轻量级图像采集终端。
3.6 机器人视觉与运动感知
将摄像头与九轴传感器数据结合,可为小型移动平台提供前方画面感知与自身姿态判断,用于避障、循迹、稳定补偿等功能的原型验证。
第四章 Arduino 开发例程
本章给出在 Arduino IDE 下驱动 AtomS3R-M12 摄像头的完整例程,包括开发环境配置、引脚定义、初始化采集以及分辨率与图像参数调节。4.1 开发环境准备
首先在 Arduino IDE 中安装 ESP32 开发板支持包,并按 AtomS3R-M12 的硬件特性完成开发板选项配置。
安装 Arduino IDE 2.x,打开开发板选择 ESP32S3 Dev Module(如已安装 M5Stack 板级支持包,也可选择对应的 AtomS3 系列开发板)。
•Flash Size 选择 8MB,PSRAM 选择 OPI PSRAM,Partition Scheme 选择默认 8MB,USB CDC On Boot 设为 Enabled。
如需烧录固件,先长按侧面复位键约 2 秒,待内部绿色 LED 亮起后松开,设备进入下载模式,再点击上传。
若使用 PlatformIO,需在配置中设置 board_build.arduino.memory_type = qio_opi,并添加 -DBOARD_HAS_PSRAM 编译选项。
4.2 摄像头引脚定义
OV3660 采用 8 位并行接口,各信号线对应的 GPIO 如下:电源使能 PWDN 为 G18,主时钟 XCLK 为 G21,SCCB 数据 SDA 为 G12,SCCB 时钟 SCL 为 G9,像素时钟 PCLK 为 G40,行同步 HREF 为 G14,场同步 VSYNC 为 G10。图像数据线 D0 至 D7 依次对应 G3、G42、G46、G48、G4、G17、G11、G13。程序中需按此对应关系为 pin_d0 至 pin_d7 赋值,顺序错误会导致图像出现条纹或颜色异常。
4.3 软件代码如下所示:
/*
* SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/**
* @Hardwares: AtomS3R-CAM / AtomS3R-M12
* @Platform Version: Arduino M5Stack Board Manager v2.1.4
*/
#include "camera_pins.h"
#include <WiFi.h>
#include "esp_camera.h"
//#define USE_ATOMS3R_CAM
#define USE_ATOMS3R_M12
#define STA_MODE
//#define AP_MODE
const char* ssid = "航天科宇";
const char* password = "88888888";
WiFiServer server(80);
camera_fb_t* fb = NULL;
uint8_t* out_jpg = NULL;
size_t out_jpg_len = 0;
static void jpegStream(WiFiClient* client);
static camera_config_t camera_config = {
.pin_pwdn = PWDN_GPIO_NUM,
.pin_reset = RESET_GPIO_NUM,
.pin_xclk = XCLK_GPIO_NUM,
.pin_sscb_sda = SIOD_GPIO_NUM,
.pin_sscb_scl = SIOC_GPIO_NUM,
.pin_d7 = Y9_GPIO_NUM,
.pin_d6 = Y8_GPIO_NUM,
.pin_d5 = Y7_GPIO_NUM,
.pin_d4 = Y6_GPIO_NUM,
.pin_d3 = Y5_GPIO_NUM,
.pin_d2 = Y4_GPIO_NUM,
.pin_d1 = Y3_GPIO_NUM,
.pin_d0 = Y2_GPIO_NUM,
.pin_vsync = VSYNC_GPIO_NUM,
.pin_href = HREF_GPIO_NUM,
.pin_pclk = PCLK_GPIO_NUM,
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
#ifdef USE_ATOMS3R_CAM
.pixel_format = PIXFORMAT_RGB565,
.frame_size = FRAMESIZE_QVGA,
#endif
#ifdef USE_ATOMS3R_M12
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_UXGA,
#endif
.jpeg_quality = 12,
.fb_count = 2,
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_LATEST,
.sccb_i2c_port = 0,
};
void setup()
{
Serial.begin(115200);
pinMode(POWER_GPIO_NUM, OUTPUT);
digitalWrite(POWER_GPIO_NUM, LOW);
delay(500);
esp_err_t err = esp_camera_init(&camera_config);
if (err != ESP_OK) {
Serial.println("Camera Init Fail");
delay(1000);
esp_restart();
} else {
Serial.println("Camera Init Success");
}
delay(100);
#ifdef STA_MODE
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
WiFi.setSleep(false);
Serial.println("");
Serial.print("Connecting to ");
Serial.println(ssid);
// Wait for connection
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
#endif
#ifdef AP_MODE
if (!WiFi.softAP(ssid, password)) {
log_e("Soft AP creation failed.");
while (1);
}
Serial.println("AP SSID:");
Serial.println(ssid);
Serial.println("AP PASSWORD:");
Serial.println(password);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
#endif
server.begin();
}
void loop()
{
WiFiClient client = server.available(); // listen for incoming clients
if (client) { // if you get a client,
while (client.connected()) { // loop while the client's connected
if (client.available()) { // if there's bytes to read from the
jpegStream(&client);
}
}
// close the connection:
client.stop();
Serial.println("Client Disconnected.");
}
}
// used to image stream
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "--" PART_BOUNDARY "rn";
static const char* _STREAM_PART = "Content-Type: image/jpegrnContent-Length: %urnrn";
static void jpegStream(WiFiClient* client)
{
Serial.println("Image stream start");
client->println("HTTP/1.1 200 OK");
client->printf("Content-Type: %srn", _STREAM_CONTENT_TYPE);
client->println("Content-Disposition: inline; filename=capture.jpg");
client->println("Access-Control-Allow-Origin: *");
client->println();
static int64_t last_frame = 0;
if (!last_frame) {
last_frame = esp_timer_get_time();
}
for (;;) {
fb = esp_camera_fb_get();
if (fb) {
#ifdef USE_ATOMS3R_CAM
frame2jpg(fb, 255, &out_jpg, &out_jpg_len);
#endif
#ifdef USE_ATOMS3R_M12
out_jpg = fb->buf;
out_jpg_len = fb->len;
#endif
Serial.printf("pic size: %dn", out_jpg_len);
client->print(_STREAM_BOUNDARY);
client->printf(_STREAM_PART, out_jpg_len);
int32_t to_sends = out_jpg_len;
int32_t now_sends = 0;
uint8_t* out_buf = out_jpg;
uint32_t packet_len = 8 * 1024;
while (to_sends > 0) {
now_sends = to_sends > packet_len ? packet_len : to_sends;
if (client->write(out_buf, now_sends) == 0) {
goto client_exit;
}
out_buf += now_sends;
to_sends -= now_sends;
}
int64_t fr_end = esp_timer_get_time();
int64_t frame_time = fr_end - last_frame;
last_frame = fr_end;
frame_time /= 1000;
Serial.printf("MJPG: %luKB %lums (%.1ffps)rn", (long unsigned int)(out_jpg_len / 1024),
(long unsigned int)frame_time, 1000.0 / (long unsigned int)frame_time);
if (fb) {
esp_camera_fb_return(fb);
fb = NULL;
}
#ifdef USE_ATOMS3R_CAM
if (out_jpg) {
free(out_jpg);
out_jpg = NULL;
out_jpg_len = 0;
}
#endif
} else {
Serial.println("Camera capture failed");
}
}
client_exit:
if (fb) {
esp_camera_fb_return(fb);
fb = NULL;
}
#ifdef USE_ATOMS3R_CAM
if (out_jpg) {
free(out_jpg);
out_jpg = NULL;
out_jpg_len = 0;
}
#endif
client->stop();
Serial.printf("Image stream endrn");
}五:实物演示
5.1 软件代码仿真效果图

5.2 网页结果图如下所示

我要赚赏金
