前言

本文是一篇实战教程,旨在引导你通过自定义 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

  1. 预期效果

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

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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐