F FisherHub Docs

03. 固件开发

项目结构

完整的 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_msstep 来控制速度。

如果要更自然的动作,可以实现缓入缓出(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/,就能看到控制界面。

下一步

掌握了基础固件之后,进入网页控制与进阶,了解如何制作控制界面和扩展更多功能。