【花雕动手做】ESP32-S3 + MimiClaw 实战:通过飞书自然语言控制 SG90 舵机

原标题
【花雕动手做】从零实现飞书 AI 控舵机:ESP32-S3 的 MimiClaw 嵌入式实践
——从“会发光”到“会动”,让你的嵌入式 AI Agent 拥有物理交互能力
引言
在上一篇文章中,我们已实现通过飞书发送“红”“绿”“蓝”等指令,控制 ESP32‑S3 板载的 WS2812 RGB LED 切换颜色,并完成了多色呼吸灯效果的开发。但静态灯效始终缺乏“灵性”,若能让 AI Agent 实现物理运动——比如控制舵机旋转,便能解锁自动开门、机械臂抓取、摄像头云台调控等更丰富的物理交互场景。
这里以 SG90 微型舵机为核心,详细讲解如何在 MimiClaw 框架下添加舵机控制工具,通过飞书自然语言指令(如“舵机0”“舵机90”“舵机180”)实现舵机远程精准控制。全文提供完整可运行代码,适配嵌入式 AI 爱好者、物联网开发者学习参考,手把手带你实现从“静态显示”到“动态交互”的突破。

一、舵机控制原理
1.1 SG90 舵机简介
SG90 是一款应用广泛的微型伺服电机,重量仅 9g,扭矩约 1.6kg·cm,凭借小巧的体积和稳定的性能,常被用于机器人关节、智能小车转向、门禁锁控制等场景。它采用三线制接口,各线路功能明确,便于接线操作:
-
红色:电源接口,必须接入 5V 电压(不可用 3.3V,否则供电不足)
-
棕色/黑色:接地接口(GND),需与开发板共地
-
橙色/黄色:PWM 信号接口,用于接收控制指令
1.2 控制信号原理
SG90 舵机通过 50Hz 的 PWM 信号(周期固定为 20ms)实现角度控制,脉宽(高电平持续时间)与舵机旋转角度存在明确的对应关系,具体如下:
-
脉宽 0.5ms → 舵机旋转至 0°(最小角度)
-
脉宽 1.5ms → 舵机旋转至 90°(中间角度)
-
脉宽 2.5ms → 舵机旋转至 180°(最大角度)
ESP32‑S3 内置 LEDC(LED PWM 控制器),可轻松生成 50Hz、14 位精度的 PWM 波形,我们只需将舵机角度换算为对应的占空比,即可实现精准角度控制。


二、硬件接线
SG90 舵机与 ESP32‑S3 开发板的接线方式简单易懂,具体对应关系如下表所示,可根据开发板空闲引脚灵活调整(推荐优先使用 GPIO 16):

重要提醒:SG90 舵机峰值电流约 200‑250mA,若开发板仅通过 USB 供电,极易出现供电不足,导致舵机抖动、无法转动等问题。建议使用外接 5V 电源(如手机充电器),并将外接电源的 GND 与开发板 GND 连接,确保供电稳定。
三、MimiClaw 中添加舵机控制工具
MimiClaw 框架项目结构清晰,添加舵机控制功能只需完成三步:创建舵机驱动文件、注册工具、添加自然语言指令映射。全程基于框架规范开发,无需修改核心代码,便于后续扩展和维护。
3.1 创建头文件 tool_servo.h
在项目 tools/ 目录下新建头文件 tool_servo.h,用于声明舵机初始化、角度控制相关函数,代码如下:
// tools/tool_servo.h
#ifndef TOOL_SERVO_H
#define TOOL_SERVO_H
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
// 舵机初始化(指定控制引脚)
esp_err_t tool_servo_init(int pin);
// 舵机角度设置(0-180°)
esp_err_t tool_servo_set_angle(int angle, char *output, size_t output_len);
// 工具执行函数(解析JSON输入,调用对应功能)
esp_err_t tool_servo_execute(const char *json_input, char *output, size_t output_len);
#ifdef __cplusplus
}
#endif
#endif // TOOL_SERVO_H
3.2 实现源文件 tool_servo.c
在 tools/ 目录下新建源文件 tool_servo.c,实现头文件中声明的函数,基于 LEDC 控制器完成 PWM 信号生成、角度与占空比换算等核心逻辑,代码如下(含详细注释):
// tools/tool_servo.c
#include “tool_servo.h”
#include “driver/ledc.h”
#include “esp_log.h”
#include “cJSON.h”
#include <string.h>
// 日志标签,便于调试
static const char *TAG = “TOOL_SERVO”;
// 舵机控制引脚(全局变量,记录当前初始化引脚)
static int s_servo_pin = -1;
// LEDC通道配置结构体
static ledc_channel_config_t s_ledc_channel = {0};
// 舵机初始化状态标志
static bool s_is_initialized = false;
/**
- @brief 角度转占空比(14位分辨率)
- @param angle 舵机目标角度(0-180°)
- @return 对应的PWM占空比
/
static uint32_t angle_to_duty(int angle) {
// 角度范围限制(防止超出舵机可控范围)
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;
// 14位分辨率最大占空比(2^14 - 1 = 16383)
const uint32_t duty_max = (1 << LEDC_TIMER_14_BIT) - 1;
// 脉宽计算:0.5ms(0°)~2.5ms(180°)线性映射
float pulse_width_ms = 0.5f + (angle / 180.0f) * 2.0f;
// 占空比 = (脉宽 / 周期) 最大占空比(周期20ms)
uint32_t duty = (uint32_t)((pulse_width_ms / 20.0f) * duty_max);
return duty;
}
/**
-
@brief 舵机初始化
-
@param pin 舵机控制引脚(GPIO)
-
@return esp_err_t 初始化结果(ESP_OK为成功)
*/
esp_err_t tool_servo_init(int pin) {
// 若已初始化且引脚相同,直接返回成功
if (s_is_initialized && s_servo_pin == pin) return ESP_OK;// 若已初始化但引脚不同,先停止当前LEDC通道
if (s_is_initialized) {
ledc_stop(LEDC_LOW_SPEED_MODE, s_ledc_channel.channel, 0);
}// LEDC定时器配置(50Hz,14位分辨率)
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE, // 低速模式
.timer_num = LEDC_TIMER_0, // 使用定时器0
.duty_resolution = LEDC_TIMER_14_BIT,// 14位分辨率
.freq_hz = 50, // 频率50Hz(周期20ms)
.clk_cfg = LEDC_AUTO_CLK // 自动时钟配置
};
esp_err_t err = ledc_timer_config(&ledc_timer);
if (err != ESP_OK) return err;// LEDC通道配置(绑定引脚、定时器)
s_ledc_channel = (ledc_channel_config_t){
.gpio_num = pin, // 舵机控制引脚
.speed_mode = LEDC_LOW_SPEED_MODE, // 与定时器模式一致
.channel = LEDC_CHANNEL_0, // 使用通道0
.timer_sel = LEDC_TIMER_0, // 绑定定时器0
.duty = 0, // 初始占空比0
.hpoint = 0 // 高电平起始点
};
err = ledc_channel_config(&s_ledc_channel);
if (err != ESP_OK) return err;// 更新初始化状态和引脚信息
s_servo_pin = pin;
s_is_initialized = true;
ESP_LOGI(TAG, “Servo initialized on GPIO %d”, pin);
return ESP_OK;
}
/**
- @brief 舵机角度设置
- @param angle 目标角度(0-180°)
- @param output 输出提示信息缓冲区
- @param output_len 缓冲区长度
- @return esp_err_t 执行结果
*/
esp_err_t tool_servo_set_angle(int angle, char *output, size_t output_len) {
// 检查舵机是否已初始化
if (!s_is_initialized) {
snprintf(output, output_len, “Error: Servo not initialized”);
return ESP_ERR_INVALID_STATE;
}
// 角度转占空比
uint32_t duty = angle_to_duty(angle);
// 设置占空比并更新
ledc_set_duty(s_ledc_channel.speed_mode, s_ledc_channel.channel, duty);
ledc_update_duty(s_ledc_channel.speed_mode, s_ledc_channel.channel);
// 输出执行结果
snprintf(output, output_len, “Servo moved to %d degree”, angle);
ESP_LOGI(TAG, “Angle %d → duty %lu”, angle, duty);
return ESP_OK;
}
/**
-
@brief 工具执行入口(解析JSON输入,调用初始化和角度设置)
-
@param json_input JSON格式输入(含pin和angle参数)
-
@param output 输出提示信息缓冲区
-
@param output_len 缓冲区长度
-
@return esp_err_t 执行结果
*/
esp_err_t tool_servo_execute(const char *json_input, char *output, size_t output_len) {
// 解析JSON输入
cJSON *root = cJSON_Parse(json_input);
if (!root) {
snprintf(output, output_len, “Error: invalid JSON”);
return ESP_ERR_INVALID_ARG;
}
// 获取pin和angle参数
cJSON *pin_obj = cJSON_GetObjectItem(root, “pin”);
cJSON *angle_obj = cJSON_GetObjectItem(root, “angle”);
// 检查参数合法性
if (!cJSON_IsNumber(pin_obj) || !cJSON_IsNumber(angle_obj)) {
snprintf(output, output_len, “Error: need pin and angle (0-180)”);
cJSON_Delete(root);
return ESP_ERR_INVALID_ARG;
}
// 转换参数类型
int pin = (int)pin_obj->valuedouble;
int angle = (int)angle_obj->valuedouble;
// 释放JSON内存
cJSON_Delete(root);// 初始化舵机
esp_err_t err = tool_servo_init(pin);
if (err != ESP_OK) {
snprintf(output, output_len, “Servo init failed on pin %d”, pin);
return err;
}
// 设置舵机角度
return tool_servo_set_angle(angle, output, output_len);
}
3.3 在 tool_registry.c 中注册工具
工具注册是 MimiClaw 框架调用自定义功能的核心步骤,需在 tool_registry.c 中引入舵机工具头文件,并注册舵机控制工具,具体操作如下:
- 在 tool_registry.c 顶部添加头文件引入:
#include "tool_servo.h"
- 在 tool_registry_init 函数中添加工具注册代码(与其他工具注册代码放在一起):
// 舵机控制工具注册
mimi_tool_t servo_tool = {
.name = "servo_set", // 工具名称(调用时需匹配)
.description = "Set servo angle. Input: {\"pin\":<GPIO>,\"angle\":0-180}", // 工具描述
.input_schema_json = "{\"type\":\"object\",\"properties\":{\"pin\":{\"type\":\"integer\"},\"angle\":{\"type\":\"integer\"}},\"required\":[\"pin\",\"angle\"]}", // 输入参数格式
.execute = tool_servo_execute, // 工具执行函数
};
register_tool(&servo_tool); // 注册工具到框架
注意:需确保 main/CMakeLists.txt 文件的 REQUIRES 字段中包含 esp_driver_ledc,否则编译时会出现“driver/ledc.h: No such file”错误,添加后即可正常编译。
3.4 在 agent_loop.c 中添加自然语言预处理
为实现飞书自然语言直接控制,需在 agent_loop.c 的 try_direct_command 函数中添加舵机指令匹配逻辑,让框架无需调用 LLM,即可快速响应固定指令。找到 try_direct_command 函数,在函数末尾添加以下代码:
// 舵机控制指令匹配(支持中英文指令,灵活匹配格式)
// 匹配“舵机0”“servo 0”
if (strstr(content, "舵机0") != NULL || strstr(content, "servo 0") != NULL) {
tool_registry_execute("servo_set", "{\"pin\":16,\"angle\":0}", output, output_size);
return true;
}
// 匹配“舵机90”“servo 90”
if (strstr(content, "舵机90") != NULL || strstr(content, "servo 90") != NULL) {
tool_registry_execute("servo_set", "{\"pin\":16,\"angle\":90}", output, output_size);
return true;
}
// 匹配“舵机180”“servo 180”
if (strstr(content, "舵机180") != NULL || strstr(content, "servo 180") != NULL) {
tool_registry_execute("servo_set", "{\"pin\":16,\"angle\":180}", output, output_size);
return true;
}
// 更灵活的匹配(支持“舵机 90 度”“舵机转到90”等格式)
if (strstr(content, "舵机") && (strstr(content, "0") || strstr(content, "90") || strstr(content, "180"))) {
if (strstr(content, "0")) {
tool_registry_execute("servo_set", "{\"pin\":16,\"angle\":0}", output, output_size);
return true;
} else if (strstr(content, "90")) {
tool_registry_execute("servo_set", "{\"pin\":16,\"angle\":90}", output, output_size);
return true;
} else if (strstr(content, "180")) {
tool_registry_execute("servo_set", "{\"pin\":16,\"angle\":180}", output, output_size);
return true;
}
}
说明:上述代码中固定使用 GPIO 16 作为舵机控制引脚,若实际接线时更换了引脚,只需将代码中的“16”替换为实际使用的引脚号即可。
四、编译与测试
完成代码开发后,通过编译烧录、串口测试、飞书测试三个步骤,验证舵机控制功能是否正常,确保每一步都符合预期效果。
4.1 编译烧录
打开终端,进入项目根目录,执行以下命令完成编译、烧录和串口监控(需将 COM12 替换为实际串口号):
# 清除之前的编译缓存
idf.py fullclean
# 编译项目
idf.py build
# 烧录固件并启动串口监控
idf.py -p COM12 flash monitor
4.2 串口手动测试
当串口监控启动后,等待 MimiClaw 框架初始化完成,出现“mimi> ”提示符时,输入以下指令,手动测试舵机控制功能:
# 控制舵机转到0°(GPIO 16)
tool_exec servo_set "{\"pin\":16,\"angle\":0}"
# 控制舵机转到90°(GPIO 16)
tool_exec servo_set "{\"pin\":16,\"angle\":90}"
# 控制舵机转到180°(GPIO 16)
tool_exec servo_set "{\"pin\":16,\"angle\":180}"
输入指令后,观察舵机是否能准确转动到对应角度。若舵机无反应,需优先检查 5V 供电、共地连接和信号线接线是否正确。
4.3 飞书自然语言测试
确保 MimiClaw 机器人已成功连接飞书,在飞书聊天框中向机器人发送以下指令,测试自然语言控制效果:
- 发送“舵机0” → 舵机旋转至 0°
- 发送“舵机90” → 舵机旋转至 90°
- 发送“舵机180” → 舵机旋转至 180°
测试成功后,串口日志会输出类似以下内容,说明指令被预处理直接捕获并执行,响应速度极快(延迟小于 0.5 秒):
I (132426) TOOL_SERVO: Servo initialized on GPIO 16
I (132426) TOOL_SERVO: Angle 0 → duty 409
I (132436) agent: Direct command matched, executing tool and responding
五、效果展示
飞书指令、舵机动作与串口输出的占空比对应关系如下表所示,可直观查看控制效果,验证舵机是否精准响应指令:

整个控制链路延迟小于 0.5 秒,且无需调用 LLM API,不消耗 API 额度,适合长期稳定使用。
六、常见问题与解决方法
在实操过程中,可能会遇到舵机不转、抖动、角度不准等问题,以下是常见问题的原因分析及解决方法,帮助快速排查故障:

七、通过飞书自然语言控制 SG90 舵机实验场景图与视频记录





【【花雕动手做】从零实现飞书 AI 控舵机:ESP32-S3 的 MimiClaw 嵌入式实践——让你的嵌入式 AI Agent 拥有物理交互能力#迷你小龙虾】
https://www.bilibili.com/video/BV15sDvBDEX7/?share_source=copy_web
从零实现飞书 AI 控舵机:ESP32-S3 的 MimiC
八、扩展思路
本文实现的舵机控制功能可灵活扩展,结合 MimiClaw 框架的特性,可实现更丰富的应用场景,以下是几个实用的扩展方向:
- 多角度预设
在 try_direct_command 函数中添加更多角度指令,如“舵机45”“舵机135”,同时调整 angle_to_duty 函数,支持 0-180° 任意角度的精准控制,满足更多场景需求。
- 多舵机控制
ESP32‑S3 拥有多组 LEDC 通道,可为每个舵机分配不同的 LEDC 通道和 GPIO 引脚,修改 tool_servo.c 代码,支持通过 JSON 参数指定舵机引脚,实现多舵机同步控制(如机器人多关节联动)。
- 传感器联动
结合超声波传感器、温湿度传感器等外设,实现智能化控制:例如通过超声波传感器检测距离,自动控制舵机旋转调整摄像头角度;通过温湿度传感器检测环境温度,自动控制舵机驱动开窗器。
- 定时任务
利用 MimiClaw 框架的 cron_add 工具,设置定时任务,让舵机在固定时间执行指定动作,例如每天固定时间转动舵机,实现自动宠物喂食器、定时开关门等功能。
- 平滑运动
在 tool_servo_set_angle 函数中添加步进循环逻辑,让舵机从当前角度逐步过渡到目标角度,实现缓动效果,避免角度突变导致的舵机抖动,提升控制体验。
八、总结
本文详细讲解了在 MimiClaw 嵌入式 AI Agent 框架中添加 SG90 舵机控制功能的完整流程,从舵机原理、硬件接线,到代码开发、工具注册、自然语言映射,再到测试与故障排查,全程贴合实战场景,提供可直接运行的代码和清晰的操作步骤。
通过本文的实践,你的 ESP32‑S3 开发板将不再局限于“智能灯泡”的角色,而是升级为能够执行物理动作的 AI 代理。核心实现逻辑可复用至步进电机、继电器、舵机阵列等其他外设,为机器人、智能家居、自动化设备开发提供了清晰的思路和基础。
项目源码基于 MimiClaw 二次开发,欢迎 fork 和贡献。若在实现过程中遇到任何问题,欢迎在评论区留言交流,共同探讨嵌入式 AI 与物联网的实践技巧。
本文为“花雕学编程”、“花雕动手做”系列博客之一,聚焦嵌入式 AI Agent 与物联网的交叉实践,后续将带来更多实战案例,敬请关注。

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)