这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » 国产MCU » 【2026AI硬件开发学习必看系列】快速通过MCP让你的桌面宠物学会摇尾巴(语音

共1条 1/1 1 跳转至

【2026AI硬件开发学习必看系列】快速通过MCP让你的桌面宠物学会摇尾巴(语音控制舵机)

菜鸟
2026-04-17 18:38:51     打赏
【2026 AI硬件开发学习必看系列】快速通过MCP桌面宠物学会尾巴语音控制舵机前言

本文是一篇实战教程,旨在引导你通过自定义 MCP(Model Context Protocol)工具,结合语音指令控制硬件设备LS26( Arcs-mini) 开发板二次开发为例实现一些 mcp 工具。

 目标:

 当你说出“电机顺时针旋转时”,LS26Arcs-mini 开发板的可扩展接口 (PA06) 输出PWM方波,然后开始电机旋转

实操之前,请确保已根据文档开发环境搭建与烧录 | https://docs2.listenai.com/zz/11561.mp4?shortId=nfECTT98L 搭建开发环境。

固件下载

如果您不想重新编译代码而希望直接体验本固件,可点击下载。

固件下载链接:mcp_tool_mg90s.lpk(https://docs2.listenai.com/zz/11826.lpk?shortId=nfECTT98L)

下载后,可以按照文档恢复出厂固件&升级固件教程 | https://docs2.listenai.com/x/IMbN1kL5H 进行烧录

示例代码

如果您想直接查看所有代码,可点击下载。

源码下载:apps.zip(https://docs2.listenai.com/zz/11824.zip?shortId=nfECTT98L)

下载后,将其替换 arcs_mini 项目的 apps 文件夹

diff 文件下载链接:mcp_tool_mg90s.diff (https://docs2.listenai.com/zz/11825.diff?shortId=nfECTT98L)

下载后,将其放到 arcs_mini 项目根目录,然后执行可命令git apply ./mcp_tool_mg90s.diff 应用更改

一、初始化舵机1. 教程目标

实现下面这个功能:

 按键四击一次:舵机开始旋转。

 再按键四击一次:舵机停止旋转。

本文面向小白开发者,按步骤复制即可跑通。

 

2. 硬件接线

MG90S 三根线:

 红线:+5V

 棕线/黑线:GND

 橙线/黄线:信号线

连接建议:

 舵机信号线接到 GPIOA06(本教程使用该引脚输出 PWM)。

 舵机电源可以单独 5V,但必须和主控 GND 共地

重点:不共地时,PWM 信号参考电平会漂移,容易出现“停不住/乱转”。

 

3. 新增 service_mg90s.h

在路径:apps/arcs-mini/services/service_mg90s.h

复制以下内容:

 


#ifndef SERVICE_MG90S_H
#define SERVICE_MG90S_H
 
#include <stdbool.h>
#include <stdint.h>
 
typedef enum {
   SERVICE_MG90S_DIR_CW = 0,
   SERVICE_MG90S_DIR_CCW = 1,
} service_mg90s_direction_t;
 
int service_mg90s_init(void);
int service_mg90s_start(service_mg90s_direction_t direction, uint8_t speed_percent);
int service_mg90s_stop(void);
bool service_mg90s_toggle(void);
 
#endif


 

4. 新增 service_mg90s.c

在路径:apps/arcs-mini/services/service_mg90s.c

复制以下内容:

 


#include <stdbool.h>
#include <stdint.h>
 
#define TAG "service_mg90s"
 
#include "lisa_log.h"
#include "lisa_pwm.h"
 
#include "IOMuxManager.h"
 
#include "service_mg90s.h"
 
#define MG90S_PWM_PAD         CSK_IOMUX_PAD_A
#define MG90S_PWM_PIN         6U
#define MG90S_PWM_FUNC        CSK_IOMUX_FUNC_ALTER12
#define MG90S_PWM_CHANNEL     6U
#define SERVO_FREQUENCY_HZ         53U
#define SERVO_STOP_PULSE_NS        1500000U
#define SERVO_CW_MAX_PULSE_NS      1300000U
#define SERVO_CCW_MAX_PULSE_NS     1700000U
#define SERVO_PERIOD_NS            (1000000000U / SERVO_FREQUENCY_HZ)
 
static lisa_device_t *s_pwm_dev = NULL;
static bool s_running = false;
static bool s_inited = false;
static service_mg90s_direction_t s_direction = SERVICE_MG90S_DIR_CW;
static uint8_t s_speed_percent = 100U;
 
static uint8_t pulse_ns_to_duty(uint32_t pulse_ns)
{
uint32_t duty = (pulse_ns * 100U + (SERVO_PERIOD_NS / 2U)) / SERVO_PERIOD_NS;
 
if (duty > 100U) {
       duty = 100U;
}
 
return (uint8_t)duty;
}
 
static int mg90s_apply_duty(uint8_t duty)
{
int ret = lisa_pwm_set(s_pwm_dev, MG90S_PWM_CHANNEL, SERVO_FREQUENCY_HZ, duty);
if (ret != 0) {
LISA_LOGE(TAG, "lisa_pwm_set failed, ret=%d", ret);
return ret;
}
 
   ret = lisa_pwm_enable(s_pwm_dev, MG90S_PWM_CHANNEL);
if (ret != 0) {
LISA_LOGE(TAG, "lisa_pwm_enable failed, ret=%d", ret);
return ret;
}
 
return 0;
}
 
static int mg90s_apply_pulse_ns(uint32_t pulse_ns)
{
return mg90s_apply_duty(pulse_ns_to_duty(pulse_ns));
}
 
int service_mg90s_init(void)
{
if (s_inited) {
return 0;
}
 
   s_pwm_dev = lisa_device_get("pwm0");
if (!lisa_device_ready(s_pwm_dev)) {
LISA_LOGE(TAG, "pwm0 device not ready");
       s_pwm_dev = NULL;
return -1;
}
 
IOMuxManager_PinConfigure(MG90S_PWM_PAD, MG90S_PWM_PIN, MG90S_PWM_FUNC);
 
lisa_pwm_config_t config = {
.polarity = LISA_PWM_POLARITY_NORMAL,
};
 
int ret = lisa_pwm_configure(s_pwm_dev, MG90S_PWM_CHANNEL, &config);
if (ret != 0) {
LISA_LOGE(TAG, "lisa_pwm_configure failed, ret=%d", ret);
return ret;
}
 
   ret = mg90s_apply_pulse_ns(SERVO_STOP_PULSE_NS);
if (ret != 0) {
LISA_LOGE(TAG, "set stop level failed, ret=%d", ret);
return ret;
}
 
   s_inited = true;
   s_running = false;
LISA_LOGI(TAG, "MG90S initialized on GPIOA06 (PWM ch%u)", MG90S_PWM_CHANNEL);
 
return 0;
}
 
int service_mg90s_start(service_mg90s_direction_t direction, uint8_t speed_percent)
{
if (!s_inited) {
int ret = service_mg90s_init();
if (ret != 0) {
return ret;
}
}
 
if (speed_percent > 100U) {
       speed_percent = 100U;
}
 
uint32_t max_delta_ns = (direction == SERVICE_MG90S_DIR_CCW)
? (SERVO_CCW_MAX_PULSE_NS - SERVO_STOP_PULSE_NS)
: (SERVO_STOP_PULSE_NS - SERVO_CW_MAX_PULSE_NS);
uint32_t delta_ns = (max_delta_ns * speed_percent + 50U) / 100U;
uint32_t run_pulse_ns = SERVO_STOP_PULSE_NS;
uint8_t stop_duty = pulse_ns_to_duty(SERVO_STOP_PULSE_NS);
uint8_t run_duty = stop_duty;
 
if (direction == SERVICE_MG90S_DIR_CCW) {
       run_pulse_ns = SERVO_STOP_PULSE_NS + delta_ns;
} else {
       run_pulse_ns = SERVO_STOP_PULSE_NS - delta_ns;
}
 
   run_duty = pulse_ns_to_duty(run_pulse_ns);
if (speed_percent > 0U && run_duty == stop_duty) {
/* 避免量化后仍等于停转占空比(典型表现:始终约 1.5ms) */
if (direction == SERVICE_MG90S_DIR_CCW) {
           run_duty = (run_duty < 100U) ? (run_duty + 1U) : run_duty;
} else {
           run_duty = (run_duty > 0U) ? (run_duty - 1U) : run_duty;
}
}
 
int ret = mg90s_apply_duty(run_duty);
if (ret != 0) {
return ret;
}
 
   s_running = true;
   s_direction = direction;
   s_speed_percent = speed_percent;
LISA_LOGI(TAG, "MG90S start rotate (%s, speed=%u%%, pulse=%uns, duty=%u%%, stop_duty=%u%%)",
(direction == SERVICE_MG90S_DIR_CCW) ? "CCW" : "CW",
             speed_percent, run_pulse_ns, run_duty, stop_duty);
return 0;
}
 
int service_mg90s_stop(void)
{
if (!s_inited) {
return service_mg90s_init();
}
 
int ret = mg90s_apply_pulse_ns(SERVO_STOP_PULSE_NS);
if (ret != 0) {
LISA_LOGW(TAG, "set stop pulse failed, ret=%d", ret);
return ret;
}
 
   s_running = false;
LISA_LOGI(TAG, "MG90S stop rotate (pulse=1500000ns)");
return 0;
}
 
bool service_mg90s_toggle(void)
{
int ret = 0;
 
if (s_running) {
       ret = service_mg90s_stop();
} else {
       ret = service_mg90s_start(s_direction, s_speed_percent);
}
 
if (ret != 0) {
LISA_LOGE(TAG, "toggle failed, ret=%d", ret);
}
 
return s_running;
}


 

5. 把新文件加入编译

编辑:apps/arcs-mini/services/CMakeLists.txt

listenai_library_sources(...) 里增加:

 


service_mg90s.c


 

6. 修改 main.c 接入四击控制

编辑:apps/arcs-mini/main.c

6.1 增加头文件

 


#include "service_mg90s.h"


6.2 初始化时调用

main() 的服务初始化区域加入:

 


service_mg90s_init();


6.3 四击分支加入 toggle

找到:

 


case VOICE_MSG_BUTTON_ACTION_QUADRUPLE_CLICK:


替换为:

 


case VOICE_MSG_BUTTON_ACTION_QUADRUPLE_CLICK:
{
   bool running = service_mg90s_toggle();
   LISA_LOGI(TAG, "power button quadruple click, mg90s %s", running ? "start" : "stop");
   break;
}


 

7. 编译命令

在工程根目录执行:

 


./build.sh -S ./apps/arcs-mini/


 

8. 测试步骤

1.  烧录新固件。

2.  上电后,按键连续点击 4 次。

3.  观察舵机开始旋转。

4.  再连续点击 4 次。

5.  观察舵机停止旋转

预期效果

 

9. 常见问题9.1 按四下没反应

 检查是否命中 VOICE_MSG_BUTTON_ACTION_QUADRUPLE_CLICK

 检查 service_mg90s_init() 是否成功(日志里看 pwm0 device not ready 等错误)。

9.2 舵机停不住

 确认舵机电源与主控共地

 用逻辑分析仪查看停止脉宽是否接近 1.5ms

 轻微漂转可先减小 speed_percent 或调整 SERVO_STOP_PULSE_NS(如 1480~1520us 微调)。

9.3 抖动明显

 舵机电源要足够(建议独立稳压 5V,电流裕量足)。

 电源线和地线尽量短,信号线远离大电流线。

二、增加MCP工具1. 教程目标

实现一个云端可调用的 MCP 工具,用来控制 MG90S 连续旋转舵机:

 支持参数 direction(方向):顺时针 / 逆时针

 支持参数 speed(速度)

 speed 不传时,默认按中速度控制

本教程基于你已经完成的 service_mg90s_start() / service_mg90s_stop() 服务。

 

2. 最终文件改动

你需要改两个文件:

1.  新增:apps/arcs-mini/mcp-tools/mcp_tool_mg90s.c

2.  修改:apps/arcs-mini/mcp-tools/CMakeLists.txt

 

3. 新增 mcp_tool_mg90s.c

apps/arcs-mini/mcp-tools/ 目录下新建 mcp_tool_mg90s.c,复制下面完整代码:

 


#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
 
#define TAG "mcp_tool_mg90s"
 
#include "cJSON.h"
#include "lisa_log.h"
#include "mcp.h"
#include "service_mg90s.h"
 
#define MG90S_SPEED_DEFAULT_PERCENT 50U
#define MG90S_SPEED_LOW_PERCENT     30U
#define MG90S_SPEED_HIGH_PERCENT    80U
 
static bool str_equal_ignore_case(const char *a, const char *b)
{
   if (!a || !b) {
       return false;
   }
 
   while (*a && *b) {
       if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) {
           return false;
       }
       a++;
       b++;
   }
 
   return (*a == '\0' && *b == '\0');
}
 
static cJSON *mg90s_result(const char *name, const char *text, bool is_error)
{
   cJSON *result = mcp_tool_call_result_create(name);
   if (!result) {
       return NULL;
   }
 
   cJSON *content_array = cJSON_CreateArray();
   cJSON *content_item = cJSON_CreateObject();
   if (!content_array || !content_item) {
       if (content_array) {
           cJSON_Delete(content_array);
       }
       cJSON_Delete(content_item);
       cJSON_Delete(result);
       return NULL;
   }
 
   cJSON_AddStringToObject(content_item, "type", "text");
   cJSON_AddStringToObject(content_item, "text", text);
   cJSON_AddItemToArray(content_array, content_item);
   cJSON_AddItemToObject(result, "content", content_array);
   cJSON_AddBoolToObject(result, "isError", is_error);
 
   return result;
}
 
static bool mg90s_parse_direction(const cJSON *direction_json, service_mg90s_direction_t *direction)
{
   const char *direction_str = NULL;
 
   if (!direction_json || !cJSON_IsString(direction_json) || !direction_json->valuestring) {
       return false;
   }
 
   direction_str = direction_json->valuestring;
   if (str_equal_ignore_case(direction_str, "cw") || str_equal_ignore_case(direction_str, "clockwise")
       || strcmp(direction_str, "顺时针") == 0) {
       *direction = SERVICE_MG90S_DIR_CW;
       return true;
   }
 
   if (str_equal_ignore_case(direction_str, "ccw") || str_equal_ignore_case(direction_str, "counterclockwise")
       || str_equal_ignore_case(direction_str, "anticlockwise") || strcmp(direction_str, "逆时针") == 0) {
       *direction = SERVICE_MG90S_DIR_CCW;
       return true;
   }
 
   return false;
}
 
static bool mg90s_parse_speed(const cJSON *speed_json, uint8_t *speed_percent, bool *stop)
{
   if (!speed_percent || !stop) {
       return false;
   }
 
   *stop = false;
 
   if (!speed_json || cJSON_IsNull(speed_json)) {
       *speed_percent = MG90S_SPEED_DEFAULT_PERCENT;
       return true;
   }
 
   if (cJSON_IsNumber(speed_json)) {
       int value = speed_json->valueint;
       if (value == 0) {
           *speed_percent = 0U;
           *stop = true;
           return true;
       }
       if (value < 0 || value > 100) {
           return false;
       }
       *speed_percent = (uint8_t)value;
       return true;
   }
 
   if (cJSON_IsString(speed_json) && speed_json->valuestring) {
       const char *speed_str = speed_json->valuestring;
 
       if (speed_str[0] == '\0' || str_equal_ignore_case(speed_str, "mid")
           || str_equal_ignore_case(speed_str, "medium") || strcmp(speed_str, "中速") == 0) {
           *speed_percent = MG90S_SPEED_DEFAULT_PERCENT;
           return true;
       }
 
       if (str_equal_ignore_case(speed_str, "low") || strcmp(speed_str, "低速") == 0) {
           *speed_percent = MG90S_SPEED_LOW_PERCENT;
           return true;
       }
 
       if (str_equal_ignore_case(speed_str, "high") || strcmp(speed_str, "高速") == 0) {
           *speed_percent = MG90S_SPEED_HIGH_PERCENT;
           return true;
       }
 
       if (str_equal_ignore_case(speed_str, "stop") || str_equal_ignore_case(speed_str, "off")
           || strcmp(speed_str, "停止") == 0) {
           *speed_percent = 0U;
           *stop = true;
           return true;
       }
 
       {
           char *endptr = NULL;
           long parsed = strtol(speed_str, &endptr, 10);
           if (endptr && *endptr == '\0') {
               if (parsed == 0) {
                   *speed_percent = 0U;
                   *stop = true;
                   return true;
               }
               if (parsed < 0 || parsed > 100) {
                   return false;
               }
               *speed_percent = (uint8_t)parsed;
               return true;
           }
       }
   }
 
   return false;
}
 
static cJSON *mg90s_control_list(const char *name)
{
   cJSON *tool = mcp_tool_list_info_create_default(
       name,
       "控制 MG90S 连续旋转舵机。direction 必填(cw/ccw),speed 选填(默认 mid 中速)。");
   if (!tool) {
       return NULL;
   }
 
   cJSON *direction_property = cJSON_CreateObject();
   cJSON_AddStringToObject(direction_property, "type", "string");
   cJSON_AddStringToObject(direction_property, "description",
       "旋转方向。cw/clockwise/顺时针 表示顺时针;ccw/counterclockwise/逆时针 表示逆时针。");
   cJSON *direction_enum = cJSON_CreateArray();
   cJSON_AddItemToArray(direction_enum, cJSON_CreateString("cw"));
   cJSON_AddItemToArray(direction_enum, cJSON_CreateString("ccw"));
   cJSON_AddItemToObject(direction_property, "enum", direction_enum);
   mcp_tool_info_add_json_property(tool, "direction", direction_property, true);
 
   cJSON *speed_property = cJSON_CreateObject();
   cJSON_AddStringToObject(speed_property, "description",
       "旋转速度。可选,默认 mid(中速度)。支持 low/mid/high/stop 或 0~100 数值(0 表示停止)。");
   cJSON *speed_one_of = cJSON_CreateArray();
   cJSON *speed_string = cJSON_CreateObject();
   cJSON *speed_int = cJSON_CreateObject();
   cJSON *speed_enum = cJSON_CreateArray();
 
   cJSON_AddStringToObject(speed_string, "type", "string");
   cJSON_AddItemToArray(speed_enum, cJSON_CreateString("low"));
   cJSON_AddItemToArray(speed_enum, cJSON_CreateString("mid"));
   cJSON_AddItemToArray(speed_enum, cJSON_CreateString("high"));
   cJSON_AddItemToArray(speed_enum, cJSON_CreateString("stop"));
   cJSON_AddItemToObject(speed_string, "enum", speed_enum);
 
   cJSON_AddStringToObject(speed_int, "type", "integer");
   cJSON_AddNumberToObject(speed_int, "minimum", 0);
   cJSON_AddNumberToObject(speed_int, "maximum", 100);
 
   cJSON_AddItemToArray(speed_one_of, speed_string);
   cJSON_AddItemToArray(speed_one_of, speed_int);
   cJSON_AddItemToObject(speed_property, "oneOf", speed_one_of);
   cJSON_AddStringToObject(speed_property, "default", "mid");
   mcp_tool_info_add_json_property(tool, "speed", speed_property, false);
 
   return tool;
}
 
static cJSON *mg90s_control_call(const char *id, const char *name, cJSON *args)
{
   (void)id;
 
   const cJSON *direction_json = mcp_tool_call_args_get(args, "direction");
   const cJSON *speed_json = mcp_tool_call_args_get(args, "speed");
 
   service_mg90s_direction_t direction = SERVICE_MG90S_DIR_CCW;
   uint8_t speed_percent = MG90S_SPEED_DEFAULT_PERCENT;
   bool stop = false;
 
   if (!mg90s_parse_direction(direction_json, &direction)) {
       LOGE("direction parameter not found or invalid");
       return mg90s_result(name, "direction 参数缺失或格式错误,必须是 cw/ccw。", true);
   }
 
   if (!mg90s_parse_speed(speed_json, &speed_percent, &stop)) {
       LOGE("speed parameter invalid");
       return mg90s_result(name, "speed 参数格式错误,支持 low/mid/high/stop 或 0~100。", true);
   }
 
   int ret = 0;
   if (stop) {
       ret = service_mg90s_stop();
   } else {
       ret = service_mg90s_start(direction, speed_percent);
   }
 
   if (ret != 0) {
       LOGE("mg90s control failed, ret=%d", ret);
       return mg90s_result(name, "舵机控制失败。", true);
   }
 
   if (stop) {
       return mg90s_result(name, "舵机已停止。", false);
   }
 
   {
       char text[96] = {0};
       const char *direction_text = (direction == SERVICE_MG90S_DIR_CCW) ? "逆时针" : "顺时针";
       snprintf(text, sizeof(text), "舵机已开始旋转,方向=%s,速度=%u%%。", direction_text, speed_percent);
       return mg90s_result(name, text, false);
   }
}
 
MCP_TOOL_DEFINE(mg90s_control, mg90s_control_list, mg90s_control_call);


 

4. 加入编译

编辑 apps/arcs-mini/mcp-tools/CMakeLists.txt,在 listenai_library_sources(...) 里增加:

 


mcp_tool_mg90s.c


 

5. 参数说明(给云端)

工具名:mg90s_control

参数:

 direction(必填,字符串)

 cw / clockwise / 顺时针

 ccw / counterclockwise / 逆时针

 speed(可选,字符串或整数,默认中速)

 字符串:low / mid / high / stop

 整数:0~1000 表示停止)

 

6. 编译命令

在工程根目录执行:

 


./build.sh -S ./apps/arcs-mini/


7. 烧录

利用 cskburn desktop 将

./build/arcs-mini.bin 烧录到 0x600000

8.  预期效果

当你说出“电机顺时针旋转时”舵机文章前面视频一样旋转

9. 常见问题9.1 调用了工具但舵机不转

 确认 service_mg90s_init() 已经在系统初始化里执行。

 确认舵机电源与主控 GND 共地

 查看日志是否出现 舵机控制失败

9.2 direction 报错

 必须传可识别值:cwccw(也支持 顺时针/逆时针)。

9.3 不传 speed 会怎样

 会自动使用中速度(50%)。

 

总结和信息补充

MCP协议在拓展智能硬件功能时带来很大便利性,不仅可以让智能硬件可以快捷的调用互联网服务,也可以让外设和感应器等外设接入更简单。

更多智能硬件接MCP的方式和示例会陆续分享,有需求的朋友可以直接关注或在评论区留言,我们会持续分享相关操作示例。

 

本文操作示例中使用的硬件是LS26(Arcs-mini)大模型开发板,支持二次开发做更多个性化功能和DIY改造需要了解硬件详细信息可以参考:https://docs2.listenai.com/x/IPiXdnAJg

如果还想进阶学习更多离线AI示例和上手Zephyr 开发,可以选择CSK6大模型视觉语音开发套件,硬件详细信息可以参考:https://docs2.listenai.com/x/CNCwAs0Dv

 

 

 

 

 



共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]