本文是一篇实战教程,旨在引导你通过自定义 MCP(Model Context Protocol)工具,结合语音指令控制硬件设备,以LS26( Arcs-mini) 开发板二次开发为例实现一些 mcp 工具。
● 目标:
○ 当你说出“电机顺时针旋转时”,LS26(Arcs-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"
在 main() 的服务初始化区域加入:
service_mg90s_init();
找到:
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~100(0 表示停止)
6. 编译命令
在工程根目录执行:
./build.sh -S ./apps/arcs-mini/
利用 cskburn desktop 将
./build/arcs-mini.bin 烧录到 0x600000
8. 预期效果当你说出“电机顺时针旋转时”,舵机如文章前面的视频中一样旋转
9. 常见问题9.1 调用了工具但舵机不转● 确认 service_mg90s_init() 已经在系统初始化里执行。
● 确认舵机电源与主控 GND 共地。
● 查看日志是否出现 舵机控制失败。
9.2 direction 报错● 必须传可识别值:cw 或 ccw(也支持 顺时针/逆时针)。
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
我要赚赏金
