☰
【ESP32】ESP32-S3 + USB 摄像头 一体化远程监控与机械臂控制方案
2026/11/5 3:58:59 网站建设 项目流程

单板 ESP32-S3 + USB 摄像头 一体化远程监控与机械臂控制方案

目标:一块 ESP32-S3 同时做"USB 摄像头取图 + 舵机控制 + WiFi 远程",无现场 PC。
适用:个人偶用,远程看机械臂画面 + 控制舵机。

1. 与前方案的关系

前方案本方案
两块板:ESP32(舵机MQTT) + ESP32-CAM(DVP摄像头MQTT帧)一块 ESP32-S3 全包
摄像头:ESP32-CAM 自带 OV2640(DVP 并口)USB UVC 摄像头(要买)
普通ESP32无USB主机,不能接USB摄像头ESP32-S3 有 USB OTG,可做 USB Host 读 UVC

家里端 GUI 复用pc/servo_gui_cam.py(MQTT 帧模式),无需改动。

2. 采购清单

物料型号价格(¥)要点
ESP32-S3 开发板ESP32-S3-DevKitC-1(带原生 USB OTG,N16R8 带 PSRAM)35–55必须有原生 USB 口(非 USB-串口桥);带 PSRAM 缓存帧
USB 摄像头罗技 C270(首选,官方实测兼容)80–130UVC 免驱;C920 也能识别但限 VGA
USB-A 母转 Type-C/杜邦视板子 USB 口形式5–10S3 DevKit 多为 Type-C,需 OTG 转接
PCA968516路 PWM5–12多舵机用;单舵机可省,用 S3 LEDC 直驱
舵机MG996R ×N8–25/只按机械臂自由度数
5V/2A 电源独立给摄像头+舵机8–18摄像头峰值~500mA,舵机峰值大,必须独立供电
杜邦线/共地5ESP32-S3 与电源共地
合计(单舵机版)≈¥180–240含板+摄像头+电源
合计(4舵机臂版)≈¥220–300加 PCA9685 + 3 舵机

3. 接线

USB摄像头 ─USB─ ESP32-S3 原生USB OTG口 (D+↔GPIO20, D-↔GPIO19, VBUS←5V) PCA9685 ─I²C─ ESP32-S3: SDA↔GPIO8, SCL↔GPIO9, VCC←3.3V, V+←5V, GND共地 MG996R ─PWM─ PCA9685 CH0~CH3 5V/2A电源 ───── 摄像头 VBUS + PCA9685 V+ ESP32-S3 供电 ─ USB(电脑烧录时) 或 独立 5V 急停按钮 ─GPIO5─GND

ESP32-S3 的 I²C/USB 引脚以板厂文档为准,DevKitC-1 默认 USB OTG 在 GPIO19/20。

4. 软件栈(ESP-IDF,非 Arduino)

ESP32-S3 USB Host UVC必须用 ESP-IDF v5.1+(Arduino 下 TinyUSB UVC 不稳定)。

  • ESP-IDF v5.1+:USB Host + UVC 类驱动
  • ESP-ADF:usb_host/uvc/http_stream组件 + 官方usb_camera_mac_speaker示例
  • esp-mqtt:MQTT 收发(控制+帧)
  • I²C 驱动:控制 PCA9685

5. 固件结构(基于官方示例扩展)

起点:forkesp-adf/examples/usb_camera/usb_camera_mac_speaker(已实现 USB UVC→HTTP MJPEG 流)。在此基础上加三块:

main/ ├── app_main.c # 主任务:初始化 USB Host/UVC、WiFi、MQTT、I²C ├── uvc_camera.c # 官方示例自带:UVC 抓帧回调 ├── mqtt_task.c # 新增:MQTT 连接 + 订阅 cmd + 发布 frame ├── servo_pca9685.c # 新增:I²C 驱动 PCA9685,setServo(ch,angle) └── CMakeLists.txt

数据流:

UVC抓帧回调 ──缩放320x240──→ JPEG缓冲 ──base64──→ MQTT发布 frame主题 MQTT回调(cmd) ──解析角度──→ servo_pca9685.setServo()

6. 关键集成代码(骨架,见同目录 app_main.c)

  • UVC 帧回调里调mqtt_publish_frame(fb->buf, fb->len)(base64 编码后发布)
  • MQTT cmd 回调里解析90/home→servo_set(ch, angle)
  • PCA9685:setServo(ch, angle)写 PWM 寄存器(角度→脉宽→12bit tick,同前固件)

7. 构建步骤

# 装 ESP-IDF v5.1+gitclone--recursivehttps://github.com/espressif/esp-idf.gitcdesp-idf&&./install.sh esp32s3&&../export.sh# 拉 ESP-ADFgitclone--recursivehttps://github.com/espressif/esp-adf.gitexportADF_PATH=~/esp-adf# 复制官方 USB 摄像头示例为本项目基础cp-r$ADF_PATH/examples/usb_camera/usb_camera_mac_speaker ~/esp32s3_usb_cam_armcd~/esp32s3_usb_cam_arm# 加入本目录的 mqtt_task.c / servo_pca9685.c,修改 app_main.c 集成idf.py set-target esp32s3 idf.py menuconfig# 填 WiFi SSID/密码、MQTT host、MQTT_PREFIXidf.py build flash monitor

8. 远程使用(家里)

  1. 现场通电:ESP32-S3 + USB 摄像头 + 舵机 + 5V 电源;
  2. ESP32-S3 自动连现场 WiFi、连 MQTT broker、开始发帧;
  3. 家里电脑:
    pipinstallpaho-mqtt Pillow python servo_gui_cam.py# CAM_STREAM_URL 留空,走 MQTT 帧
  4. GUI 左边出画面、右边拖滑块控舵机——一块板搞定。

9. 性能与限制

项实测/预期
视频帧率~1–2fps(MQTT 传帧,QVGA base64 ~13KB)
控制延迟<500ms(MQTT)
USB 摄像头兼容C270 稳定;C920 限 VGA;非 UVC 不识别
内存需 PSRAM(N16R8 板),UVC 帧缓冲 + WiFi + MQTT
开发难度高——ESP-IDF C,UVC+MQTT+I²C 集成,需读懂官方示例

10. 难度评估与降级路径

本方案开发难度高(ESP-IDF + UVC + 三模块集成)。若卡壳,降级:

难度方案说明
高本方案(ESP32-S3 + USB UVC)一块板,但要写 IDF C
中ESP32-S3 +DVP 摄像头(OV2640 模组接 S3 并口)用 Arduino esp32 camera,比 UVC 简单
低回到ESP32-CAM(DVP) + ESP32(舵机) 两块板已有现成固件,今天就能跑

若你已有 USB 摄像头就上本方案;若还没买,强烈建议改买 ESP32-CAM 模组(¥20,自带 OV2640),用现成的 cam_mqtt.ino,省掉所有 UVC/IDF 折腾,体验一样。

代码

/* * app_main.c — 单板 ESP32-S3:USB UVC 摄像头 + MQTT(帧发布/舵机订阅) + PCA9685 舵机 * * 骨架/参考代码。基于官方 esp-adf usb_camera_mac_speaker 示例扩展, * 集成 MQTT 帧发布与舵机控制。需配合 ESP-IDF v5.1+ + ESP-ADF 编译。 * * 集成点(这是本方案核心,官方示例不含这两块): * 1. UVC 抓帧回调 frame_cb() —— 把帧 base64 编码后用 MQTT 发布 * 2. MQTT 收到 cmd 主题 —— 解析角度,调 servo_set() * * 注意:UVC/USB Host 部分 API 取自 esp-adf usb_camera 示例,按其为准; * 本文件展示集成结构,部分函数签名简化标注为「示例占位」。 * * 本文件为逐行注释教学版:每一行都说明它在做什么、为什么这么做。 */ /* ===== 标准库头文件 ===== */ #include <string.h> // 提供 memcpy / strncmp / strchr / sscanf 等字符串操作函数 #include <stdlib.h> // 提供 atoi / malloc 等通用工具函数 /* ===== ESP-IDF 系统头文件 ===== */ #include "freertos/FreeRTOS.h" // FreeRTOS 实时操作系统基础类型(TickType_t 等) #include "freertos/task.h" // FreeRTOS 任务相关:xTaskGetTickCount / vTaskDelay / pdMS_TO_TICKS #include "esp_log.h" // ESP_LOGI/ESP_LOGE 日志宏,输出带 TAG 的彩色日志到串口 #include "esp_camera.h" // 摄像头驱动(DVP 路线用;USB UVC 路线改用 esp-adf 的 uvc 组件) #include "mqtt_client.h" // ESP-IDF 官方 MQTT 客户端:esp_mqtt_client_* 系列 API #include "servo_pca9685.h" // 本项目自有的 PCA9685 舵机驱动(见 servo_pca9685.c),提供 servo_init/servo_set /* ===== 日志标签 ===== */ static const char *TAG = "main"; // 本文件的日志前缀,串口输出形如 I(main) xxx,便于过滤 /* ===== 用户配置宏(烧录前必改) ===== */ #define WIFI_SSID "YourWiFiName" // 现场 WiFi 名,ESP32-S3 用它连路由器上公网 #define WIFI_PASS "YourWiFiPassword" // 现场 WiFi 密码 #define MQTT_URI "mqtt://broker.emqx.io:1883" // 公共免费 MQTT broker,无需账号;协议://地址:端口 #define MQTT_PREFIX "esp32servo/abc123" // 主题前缀,改成你的唯一标识,防公共 broker 上别人误触 /* ===== 视频帧参数 ===== */ #define FRAME_INTERVAL_MS 500 // 两次发帧最小间隔 500ms,即 ~2fps;降带宽可调大 #define MAX_FRAME_BYTES (32 * 1024) // 单帧 JPEG 最大字节数上限 32KB,超过则截断(实际应先缩放到 QVGA) /* ===== 全局变量 ===== */ static esp_mqtt_client_handle_t mqtt_cli = NULL; // MQTT 客户端句柄,初始化后保存于此供各回调使用 static char topic_cmd[40]; // 命令主题字符串缓冲,形如 "esp32servo/abc123/cmd" (家里→ESP32 控舵机) static char topic_frame[40]; // 帧主题字符串缓冲,形如 "esp32servo/abc123/frame" (ESP32→家里 发视频帧) static char topic_state[40]; // 状态主题字符串缓冲,形如 "esp32servo/abc123/state"(ESP32→家里 上报当前角度) // ---------- Base64 编码 ---------- // Base64 字符表:6 位一组(0~63)映射到这 64 个字符,编码后可安全走 MQTT 文本传输 static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; // 把任意字节流 in(长度 len)编码为 Base64 文本写入 out,返回写出字符数(不含结尾\0) static size_t b64enc(const uint8_t *in, size_t len, char *out) { size_t i = 0, o = 0; // i=输入游标,o=输出游标 // 主循环:每次吃 3 字节,编码成 4 个 Base64 字符(3×8bit = 4×6bit) for (; i + 2 < len; i += 3) { // 把 3 字节拼成 24 位整数 n(高位在前) uint32_t n = ((uint32_t)in[i] << 16) | ((uint32_t)in[i+1] << 8) | in[i+2]; // 从 24 位里取 4 个 6 位段,分别查表输出 4 个字符 out[o++] = B64[(n >> 18) & 63]; out[o++] = B64[(n >> 12) & 63]; // 高 12 位 out[o++] = B64[(n >> 6) & 63]; out[o++] = B64[n & 63]; // 低 12 位 } // 尾部处理:剩余 1 或 2 字节时补 '=' 凑成 4 字符 if (i < len) { uint32_t n = (uint32_t)in[i] << 16; // 剩余首字节放最高 8 位 if (i + 1 < len) n |= (uint32_t)in[i+1] << 8; // 若还有第二字节,放中 8 位 out[o++] = B64[(n >> 18) & 63]; // 第 1 个字符(来自首字节) out[o++] = B64[(n >> 12) & 63]; // 第 2 个字符(首字节低 4 位 + 次字节高 4 位) out[o++] = (i + 1 < len) ? B64[(n >> 6) & 63] : '='; // 有第 2 字节则输出第 3 字符,否则补 '=' out[o++] = '='; // 末尾恒补 '=' 凑齐 4 字符 } out[o] = 0; // 字符串结尾 '\0',便于当 C 字符串处理 return o; // 返回编码后字符数 } // ---------- 集成点1:UVC 帧回调 → MQTT 发布 ---------- // 官方 usb_camera 示例的 UVC 帧回调签名以示例为准;这里给出处理逻辑。 // 摄像头每产出一帧 JPEG,USB Host 驱动就调一次本函数,参数 data 指向帧数据、len 为字节数。 void frame_cb(const uint8_t *data, size_t len) { static TickType_t last = 0; // 上次发帧的 tick 计数(static 保留跨调用) TickType_t now = xTaskGetTickCount(); // 取当前 FreeRTOS tick // 限频:距上次发帧不足 FRAME_INTERVAL_MS 就直接返回,避免高频刷爆 MQTT if ((now - last) * portTICK_PERIOD_MS < FRAME_INTERVAL_MS) return; last = now; // 记录本次发帧时间 if (len > MAX_FRAME_BYTES) len = MAX_FRAME_BYTES; // 超大帧截断(实际应先缩放到 QVGA 再编码) static char *buf = NULL; // Base64 输出缓冲,static 复用避免每帧 malloc/free if (!buf) buf = malloc(MAX_FRAME_BYTES * 4 / 3 + 8); // 首次调用时分配;Base64 膨胀系数 4/3,+8 余量 size_t outlen = b64enc(data, len, buf); // 把 JPEG 编码成 Base64 文本,返回长度 // 仅在 MQTT 已连接时发布,避免断连期间丢数据报错 if (mqtt_cli && esp_mqtt_client_is_connected(mqtt_cli)) { // 发布到 frame 主题:参数=客户端、主题、数据、数据长度、QoS=0、retain=false esp_mqtt_client_publish(mqtt_cli, topic_frame, buf, outlen, 0, false); } } // ---------- 集成点2:MQTT cmd → 舵机 ---------- // 收到家里的命令字符串后解析并驱动舵机。msg/len 是 MQTT 投递来的原始字节。 static void parse_cmd(const char *msg, int len) { char buf[16]; // 本地小缓冲存命令文本(命令都很短:如 "90" "0 90" "home") if (len >= sizeof(buf)) len = sizeof(buf) - 1; // 防越界:超长则截断到缓冲容量-1 memcpy(buf, msg, len); buf[len] = 0; // 复制进 buf 并补 '\0' 结尾,变成 C 字符串 // 命令 "home":4 个舵机全部回 90° 中位 if (strncmp(buf, "home", 4) == 0) { for (int i = 0; i < 4; i++) servo_set(i, 90); // 逐路设 90° esp_mqtt_client_publish(mqtt_cli, topic_state, "90", 2, 0, true); // 上报状态,retain=true 让新订阅者立刻拿到 return; // 处理完毕返回 } // 其他命令格式:"0 90"=通道号+角度,或 "90"=仅角度(默认通道 0) int ch = 0, a = 90; // ch=舵机通道(0~3),a=目标角度(0~180),给默认值 if (strchr(buf, ' ')) { // 字符串里有空格 → "通道 角度" 两段格式 sscanf(buf, "%d %d", &ch, &a); // 用 sscanf 解析两个整数 } else { // 无空格 → 单数字,当作通道 0 的角度 a = atoi(buf); // 字符串转整数 } if (ch < 0 || ch > 3) ch = 0; // 通道越界则兜底为 0 if (a < 0) a = 0; if (a > 180) a = 180; // 角度裁剪到合法范围 0~180 servo_set(ch, a); // 调 PCA9685 驱动把该通道舵机转到 a 度 // 把实际角度转成字符串上报到 state 主题,让家里 GUI 进度条更新 char st[4]; int n = snprintf(st, sizeof(st), "%d", a); // 角度整数→字符串,n=写出长度 esp_mqtt_client_publish(mqtt_cli, topic_state, st, n, 0, true); // retain=true } // MQTT 事件回调:连接、收到消息等事件都进来这里。参数 args/base/id/data 由框架传入。 static void mqtt_event_handler(void *args, esp_event_base_t base, int32_t id, void *data) { esp_mqtt_event_handle_t ev = data; // 把 data 转成 MQTT 事件结构体指针,含 data/data_len/topic 等 switch (id) { // 按 MQTT 事件类型分支 case MQTT_EVENT_CONNECTED: // 已连上 broker ESP_LOGI(TAG, "MQTT connected"); // 日志 esp_mqtt_client_subscribe(mqtt_cli, topic_cmd, 0); // 订阅命令主题,QoS=0 break; case MQTT_EVENT_DATA: // 收到一条订阅消息(payload 在 ev->data,长度 ev->data_len) ESP_LOGI(TAG, "cmd: %.*s", ev->data_len, ev->data); // 打印命令(%.*s 按长度打印,避免无\0越界) parse_cmd(ev->data, ev->data_len); // 交给 parse_cmd 解析并驱动舵机 break; default: break; // 其他事件(断连/订阅成功等)此处忽略 } } // ---------- WiFi(简化,实际用 esp_wifi 走完整 STA 连接) ---------- // 真实实现需:esp_netif_init→event loop→wifi_init_config→wifi_set_config(STA)→wifi_start→等 GOT_IP 事件。 // 这里仅占位,逻辑请参考 ESP-IDF wifi/getting_started 示例。 static void wifi_init_sta(void) { // TODO: esp_netif_init / event loop / wifi_init_config / wifi_set_config(STA) / wifi_start / wait connected // 参考 ESP-IDF wifi/getting_started 示例 ESP_LOGI(TAG, "WiFi STA: %s (填实现在此)", WIFI_SSID); // 占位日志 } // ESP-IDF 程序入口(相当于 Arduino 的 setup+loop,但这里 setup 完后由事件/回调驱动) void app_main(void) { // 用 snprintf 把前缀拼成三个完整主题字符串,存进全局缓冲 snprintf(topic_cmd, sizeof(topic_cmd), "%s/cmd", MQTT_PREFIX); // 命令主题 snprintf(topic_frame, sizeof(topic_frame), "%s/frame", MQTT_PREFIX); // 视频帧主题 snprintf(topic_state, sizeof(topic_state), "%s/state", MQTT_PREFIX); // 状态主题 ESP_ERROR_CHECK(servo_init()); // 初始化 PCA9685 + 4 路舵机(失败则中止,ESP_ERROR_CHECK 会断言) wifi_init_sta(); // 连现场 WiFi(占位,需补完整 STA 流程) // MQTT 客户端配置:指定 broker、放大收发缓冲以容纳 Base64 帧 esp_mqtt_client_config_t cfg = { .broker.address.uri = MQTT_URI, // broker 地址 .buffer.size = MAX_FRAME_BYTES * 4 / 3 + 64, // 接收缓冲,放大容 base64 帧 .buffer.out_size = MAX_FRAME_BYTES * 4 / 3 + 64, // 发送缓冲,放大容 base64 帧 }; mqtt_cli = esp_mqtt_client_init(&cfg); // 创建客户端实例 esp_mqtt_client_register_event(mqtt_cli, MQTT_EVENT_ANY, mqtt_event_handler, NULL); // 注册事件回调(任意事件) esp_mqtt_client_start(mqtt_cli); // 启动客户端,开始连 broker // USB UVC Host:启动 esp-adf usb_camera 任务,注册 frame_cb 为帧回调 // 参考 esp-adf/examples/usb_camera/usb_camera_mac_speaker // uvc_host_config_t cfg = { .frame_cb = frame_cb, ... }; // uvc_host_init(&cfg); ESP_LOGI(TAG, "TODO: start UVC host from esp-adf usb_camera example, wire frame_cb"); // MQTT out buffer 放大后即可发布帧;主循环空转,工作全在回调 while (1) vTaskDelay(pdMS_TO_TICKS(1000)); // 主任务每秒让出 CPU 一次,实际工作由回调驱动 }
/* * servo_pca9685.c — ESP-IDF 下 PCA9685 驱动 4 路舵机(I²C) * * 适配 ESP-IDF v5.1+,I²C 用 legacy driver(v5 仍可用,仅弃用警告)。 * 角度→脉宽→12bit tick,逻辑同 Arduino 版固件 setServo()。 * * 接线:SDA↔GPIO8, SCL↔GPIO9, PCA9685 V+←5V, 共地。 */#include"servo_pca9685.h"#include"driver/i2c.h"#include"esp_log.h"#include<math.h>staticconstchar*TAG="servo";#defineI2C_PORTI2C_NUM_0#definePCA_ADDR0x40#defineSDA_PIN8#defineSCL_PIN9#defineFREQ_HZ100000#defineMODE1_REG0x00#defineLED0_ON_L_REG0x06#definePRESCALE_REG0xFE#defineSERVO_US_MIN500#defineSERVO_US_MAX2500#definePWM_FREQ_SERVO50// 50Hzstaticesp_err_ti2c_write(uint8_treg,uint8_tval){i2c_cmd_handle_tcmd=i2c_cmd_link_create();i2c_master_start(cmd);i2c_master_write_byte(cmd,(PCA_ADDR<<1)|I2C_MASTER_WRITE,true);i2c_master_write_byte(cmd,reg,true);i2c_master_write_byte(cmd,val,true);i2c_master_stop(cmd);esp_err_tret=i2c_master_cmd_begin(I2C_PORT,cmd,pdMS_TO_TICKS(100));i2c_cmd_link_delete(cmd);returnret;}staticesp_err_tset_pwm_freq(uint16_tfreq){floatprescaleval=25000000.0f/(4096*freq)-1;uint8_tprescale=(uint8_t)floor(prescaleval+0.5f);uint8_toldmode;// 读 MODE1(简化:直接写)i2c_write(MODE1_REG,0x10);// sleepi2c_write(PRESCALE_REG,prescale);i2c_write(MODE1_REG,0x80);// restart, auto-incrementreturnESP_OK;}esp_err_tservo_set(intch,intangle_deg){// 角度→脉宽→12bit tickintus=SERVO_US_MIN+(int)((angle_deg/180.0f)*(SERVO_US_MAX-SERVO_US_MIN));inttick=(int)((long)us*4096/20000);uint8_treg=LED0_ON_L_REG+4*ch;i2c_write(reg,0);// ON_Li2c_write(reg+1,0);// ON_Hi2c_write(reg+2,tick&0xFF);// OFF_Li2c_write(reg+3,(tick>>8)&0x0F);// OFF_HreturnESP_OK;}esp_err_tservo_init(void){i2c_config_tconf={.mode=I2C_MODE_MASTER,.sda_io_num=SDA_PIN,.scl_io_num=SCL_PIN,.sda_pullup_en=GPIO_PULLUP_ENABLE,.scl_pullup_en=GPIO_PULLUP_ENABLE,.master.clk_speed=FREQ_HZ,};ESP_ERROR_CHECK(i2c_param_config(I2C_PORT,&conf));ESP_ERROR_CHECK(i2c_driver_install(I2C_PORT,I2C_MODE_MASTER,0,0,0));set_pwm_freq(PWM_FREQ_SERVO);for(inti=0;i<4;i++)servo_set(i,90);// 初始居中ESP_LOGI(TAG,"PCA9685 ready, 4 servos at 90deg");returnESP_OK;}
/* * servo_pca9685.h */#pragmaonce#include"esp_err.h"esp_err_tservo_init(void);esp_err_tservo_set(intch,intangle_deg);// ch 0-3, angle 0-180

/*

  • servo_pca9685.c — ESP-IDF 下 PCA9685 驱动 4 路舵机(I²C)
  • 适配 ESP-IDF v5.1+,I²C 用 legacy driver(v5 仍可用,仅弃用警告)。
  • 角度→脉宽→12bit tick,逻辑同 Arduino 版固件 setServo()。
  • 接线:SDA↔GPIO8, SCL↔GPIO9, PCA9685 V+←5V, 共地。
    */

#include “servo_pca9685.h”
#include “driver/i2c.h”
#include “esp_log.h”
#include <math.h>

static const char *TAG = “servo”;
#define I2C_PORT I2C_NUM_0
#define PCA_ADDR 0x40
#define SDA_PIN 8
#define SCL_PIN 9
#define FREQ_HZ 100000

#define MODE1_REG 0x00
#define LED0_ON_L_REG 0x06
#define PRESCALE_REG 0xFE

#define SERVO_US_MIN 500
#define SERVO_US_MAX 2500
#define PWM_FREQ_SERVO 50 // 50Hz

static esp_err_t i2c_write(uint8_t reg, uint8_t val) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (PCA_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_write_byte(cmd, val, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_PORT, cmd, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd);
return ret;
}

static esp_err_t set_pwm_freq(uint16_t freq) {
float prescaleval = 25000000.0f / (4096 * freq) - 1;
uint8_t prescale = (uint8_t)floor(prescaleval + 0.5f);

uint8_t oldmode; // 读 MODE1(简化:直接写) i2c_write(MODE1_REG, 0x10); // sleep i2c_write(PRESCALE_REG, prescale); i2c_write(MODE1_REG, 0x80); // restart, auto-increment return ESP_OK;

}

esp_err_t servo_set(int ch, int angle_deg) {
// 角度→脉宽→12bit tick
int us = SERVO_US_MIN + (int)((angle_deg / 180.0f) * (SERVO_US_MAX - SERVO_US_MIN));
int tick = (int)((long)us * 4096 / 20000);
uint8_t reg = LED0_ON_L_REG + 4 * ch;
i2c_write(reg, 0); // ON_L
i2c_write(reg + 1, 0); // ON_H
i2c_write(reg + 2, tick & 0xFF); // OFF_L
i2c_write(reg + 3, (tick >> 8) & 0x0F); // OFF_H
return ESP_OK;
}

esp_err_t servo_init(void) {
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = SDA_PIN,
.scl_io_num = SCL_PIN,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = FREQ_HZ,
};
ESP_ERROR_CHECK(i2c_param_config(I2C_PORT, &conf));
ESP_ERROR_CHECK(i2c_driver_install(I2C_PORT, I2C_MODE_MASTER, 0, 0, 0));

set_pwm_freq(PWM_FREQ_SERVO); for (int i = 0; i < 4; i++) servo_set(i, 90); // 初始居中 ESP_LOGI(TAG, "PCA9685 ready, 4 servos at 90deg"); return ESP_OK;

}

/*

  • servo_pca9685.h
    */
    #pragma once
    #include “esp_err.h”

esp_err_t servo_init(void);
esp_err_t servo_set(int ch, int angle_deg); // ch 0-3, angle 0-180

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询