项目结构
完整的 ESPClaw 固件项目结构如下:
espclaw-firmware/
├── CMakeLists.txt
├── main/
│ ├── CMakeLists.txt
│ ├── main.c # 入口,初始化各模块
│ ├── servo.c / servo.h # 舵机驱动
│ ├── wifi_ap.c / wifi_ap.h # WiFi AP 配置
│ ├── http_server.c / http_server.h # HTTP + WebSocket
│ ├── web_page.h # 内嵌网页(编译时转成字节数组)
│ └── smooth.c / smooth.h # 平滑过渡算法
├── sdkconfig # ESP-IDF 配置
└── partitions.csv # OTA 分区表
本文将逐步实现每个模块。完整的源码可以在仓库中找到。
第一步:LEDC PWM 驱动舵机
ESP32 的 LEDC 模块专为 LED 调光设计,但用来输出舵机 PWM 信号同样合适。它支持 16 路独立通道,分辨率最高 20 位。
配置 LEDC
我们在 servo.c 中封装舵机驱动:
// servo.h
#pragma once
#include "driver/ledc.h"
#define SERVO_MIN_PULSE_US 500 // 0 度对应脉宽 500µs
#define SERVO_MAX_PULSE_US 2400 // 180 度对应脉宽 2400µs
#define SERVO_MIN_ANGLE 0
#define SERVO_MAX_ANGLE 180
void servo_init(int gpio_num);
void servo_set_angle(int angle);
int servo_get_current_angle(void);
// servo.c
#include "servo.h"
#define LEDC_TIMER LEDC_TIMER_0
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_MODE LEDC_LOW_SPEED_MODE
#define LEDC_RESOLUTION LEDC_TIMER_10_BIT // 10 位 = 1024 级
#define SERVO_FREQ 50 // 50Hz
static int current_angle = 90;
void servo_init(int gpio_num)
{
// 配置定时器
ledc_timer_config_t timer = {
.speed_mode = LEDC_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_RESOLUTION,
.freq_hz = SERVO_FREQ,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer);
// 配置通道
ledc_channel_config_t channel = {
.speed_mode = LEDC_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.gpio_num = gpio_num,
.duty = 0
};
ledc_channel_config(&channel);
// 初始化到 90 度
servo_set_angle(90);
}
void servo_set_angle(int angle)
{
if (angle < SERVO_MIN_ANGLE) angle = SERVO_MIN_ANGLE;
if (angle > SERVO_MAX_ANGLE) angle = SERVO_MAX_ANGLE;
// 将角度映射为占空比
// 脉宽 = 500 + (angle / 180) * (2400 - 500) µs
// 占空比 = 脉宽 / 20000µs * 1024
int pulse_us = SERVO_MIN_PULSE_US +
(angle - SERVO_MIN_ANGLE) * (SERVO_MAX_PULSE_US - SERVO_MIN_PULSE_US)
/ (SERVO_MAX_ANGLE - SERVO_MIN_ANGLE);
int duty = pulse_us * (1 << LEDC_RESOLUTION) / 20000;
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
current_angle = angle;
}
int servo_get_current_angle(void)
{
return current_angle;
}
关键点:LEDC 的占空比单位不是微秒,而是分辨率下的计数。10 位分辨率意味着周期被分成 1024 份。50Hz 下每份约 19.5µs,因此 500µs 对应 500 / 19.5 ≈ 26。
第二步:WiFi AP 模式
ESPClaw 工作在 AP(Access Point)模式,自己充当 WiFi 热点,不需要路由器。
// wifi_ap.c
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#define WIFI_SSID "ESPClaw"
#define WIFI_PASS "12345678"
#define MAX_CONNECTIONS 4
void wifi_ap_init(void)
{
// 初始化 NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase();
nvs_flash_init();
}
// 初始化网络接口
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_ap();
// 配置 WiFi
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
wifi_config_t wifi_config = {
.ap = {
.ssid = WIFI_SSID,
.ssid_len = strlen(WIFI_SSID),
.password = WIFI_PASS,
.max_connection = MAX_CONNECTIONS,
.authmode = WIFI_AUTH_WPA2_PSK,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
printf("WiFi AP started: SSID=%s, IP=192.168.4.1\n", WIFI_SSID);
}
连接上 “ESPClaw” 热点后,手机或电脑的 IP 由 ESP32 的 DHCP 服务器分配,默认网关为 192.168.4.1。
第三步:HTTP + WebSocket 服务器
ESP-IDF 提供了 esp_http_server 库,支持 HTTP 和 WebSocket。
主入口 main.c
// main.c
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "servo.h"
#include "wifi_ap.h"
#include "http_server.h"
void app_main(void)
{
// 初始化舵机
servo_init(4); // GPIO 4
// 启动 WiFi AP
wifi_ap_init();
// 启动 HTTP + WebSocket 服务器
start_webserver();
}
HTTP 服务器实现
// http_server.c
#include "esp_http_server.h"
#include "web_page.h" // 内嵌的 HTML 页面
static httpd_handle_t server = NULL;
/* 提供网页文件 */
static esp_err_t root_get_handler(httpd_req_t *req)
{
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, (const char *)web_page_html,
web_page_html_len);
return ESP_OK;
}
/* WebSocket 回调:接收角度值 */
static esp_err_t ws_handler(httpd_req_t *req)
{
if (req->method == HTTP_GET) {
return ESP_OK; // WebSocket 握手阶段
}
char buf[64] = {0};
int len = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (len <= 0) return ESP_FAIL;
// 解析控制命令
// 协议:{"angle": 90} 或 {"cmd": "open"} / {"cmd": "close"}
cJSON *json = cJSON_Parse(buf);
if (json == NULL) return ESP_FAIL;
cJSON *angle = cJSON_GetObjectItem(json, "angle");
if (angle != NULL) {
int target = angle->valueint;
smooth_move_to(target); // 平滑过渡
}
cJSON *cmd = cJSON_GetObjectItem(json, "cmd");
if (cmd != NULL) {
if (strcmp(cmd->valuestring, "open") == 0) {
smooth_move_to(180);
} else if (strcmp(cmd->valuestring, "close") == 0) {
smooth_move_to(0);
}
}
cJSON_Delete(json);
// 回复当前角度
char response[32];
snprintf(response, sizeof(response),
"{\"angle\": %d}", servo_get_current_angle());
httpd_ws_frame_t ws_pkt = {
.payload = (uint8_t *)response,
.len = strlen(response),
.type = HTTPD_WS_TYPE_TEXT
};
httpd_ws_send_frame(req, &ws_pkt);
return ESP_OK;
}
/* 注册路由 */
void start_webserver(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 8;
config.lru_purge_enable = true;
if (httpd_start(&server, &config) == ESP_OK) {
// HTTP 根路径 → 返回内嵌网页
httpd_uri_t root = {
.uri = "/",
.method = HTTP_GET,
.handler = root_get_handler
};
httpd_register_uri_handler(server, &root);
// WebSocket 端点
httpd_uri_t ws = {
.uri = "/ws",
.method = HTTP_GET,
.handler = ws_handler,
.is_websocket = true
};
httpd_register_uri_handler(server, &ws);
printf("Server started: http://192.168.4.1/\n");
}
}
WebSocket 通信协议
前端和后端之间的消息格式:
| 方向 | 消息体 | 说明 |
|---|---|---|
| 前端 → ESP | {"angle": 90} | 设置目标角度 |
| 前端 → ESP | {"cmd": "open"} | 张开(角度 180) |
| 前端 → ESP | {"cmd": "close"} | 闭合(角度 0) |
| ESP → 前端 | {"angle": 90} | 当前角度回显 |
第四步:平滑过渡算法
直接设置角度会让舵机瞬间跳到目标位置,机械爪动作生硬,还可能因惯性损坏零件。我们需要一个平滑过渡算法。
// smooth.c
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "servo.h"
static TaskHandle_t smooth_task_handle = NULL;
/* 平滑过渡任务 */
static void smooth_task(void *arg)
{
int target = (int)(intptr_t)arg;
int current = servo_get_current_angle();
int step = (target > current) ? 1 : -1;
int delay_ms = 10; // 每步间隔 10ms
for (int a = current; a != target; a += step) {
servo_set_angle(a);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
// 确保到达精确目标
servo_set_angle(target);
smooth_task_handle = NULL;
vTaskDelete(NULL);
}
/* 启动平滑移动 */
void smooth_move_to(int target)
{
// 如果已有平滑任务,先取消
if (smooth_task_handle != NULL) {
vTaskDelete(smooth_task_handle);
smooth_task_handle = NULL;
}
xTaskCreate(smooth_task, "smooth", 2048,
(void *)(intptr_t)target,
10, &smooth_task_handle);
}
每 10ms 移动 1 度,从 0 度到 180 度大约需要 1.8 秒。你可以调整 delay_ms 和 step 来控制速度。
如果要更自然的动作,可以实现缓入缓出(ease-in-out)曲线:
int ease_in_out(int start, int end, float t)
{
// t 范围 0.0 ~ 1.0
// ease-in-out 公式:t² / (2*(t² - t) + 1)
float eased;
if (t < 0.5f) {
eased = 2.0f * t * t;
} else {
eased = 1.0f - (-2.0f * t + 2.0f) * (-2.0f * t + 2.0f) / 2.0f;
}
return start + (end - start) * eased;
}
编译与烧录
cd espclaw-firmware
idf.py set-target esp32s3
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor
烧录完成后,用手机连接 WiFi “ESPClaw”,打开浏览器访问 http://192.168.4.1/,就能看到控制界面。
下一步
掌握了基础固件之后,进入网页控制与进阶,了解如何制作控制界面和扩展更多功能。