本文是一篇实战教程,旨在引导你通过自定义 MCP(Model Context Protocol)工具,结合语音指令控制硬件设备。我们将以 Arcs-mini 开发板为例实现一些 mcp 工具。
实操之前,请确保已根据文档《Arcs-Mini 开发环境搭建指南》 搭建开发环境。
本篇示例在 2.3.5 版本的 SDK 进行修改
可通过命令git log查看版本,如果版本不一致,请使用命令git checkout mini-v2.3.5切到指定SDK版本
参考文档《烧录工具安装教程》安装ADB工具
执行命令adb shell kv set int user.disable_app_update 1关闭云端OTA更新,防止云端更新把刚烧录的固件刷掉了。
如果您不想重新编译代码而希望直接体验本固件,可点击下载。
.hex 文件拖入 cskburn desktop 后选择 ADB 设备进行烧录即可。
如果您想直接查看所有代码,可点击下载。
源码下载:apps.zip
1.下载后,将其替换 arcs_mini 项目的
apps文件夹
2.参考文档《SDK编译教程》编译烧录即可
实现下面这个功能:
本文面向小白开发者,按步骤复制即可跑通。


MG90S-360°
MG90S 三根线:
+5VGND信号线连接建议:
舵机信号线接到 GPIOA06(本教程使用该引脚输出 PWM)。

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

重点:不共地时,PWM 信号参考电平会漂移,容易出现“停不住/乱转”。
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
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;
}
编辑:apps/arcs-mini/services/CMakeLists.txt
在 listenai_library_sources(...) 里增加:
service_mg90s.c
main.c 接入四击控制编辑:apps/arcs-mini/main.c
#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;
}
在工程根目录执行:
./build.sh -S ./apps/arcs-mini/
预期效果
fa1b10322c45ceaca7a64b3b91c39512.mp4
VOICE_MSG_BUTTON_ACTION_QUADRUPLE_CLICK。service_mg90s_init() 是否成功(日志里看 pwm0 device not ready 等错误)。1.5ms。speed_percent 或调整 SERVO_STOP_PULSE_NS(如 1480~1520us 微调)。实现一个云端可调用的 MCP 工具,用来控制 MG90S 连续旋转舵机:
direction(方向):顺时针 / 逆时针speed(速度)speed 不传时,默认按中速度控制本教程基于你已经完成的 service_mg90s_start() / service_mg90s_stop() 服务。
你需要改两个文件:
apps/arcs-mini/mcp-tools/mcp_tool_mg90s.capps/arcs-mini/mcp-tools/CMakeLists.txtmcp_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);
编辑 apps/arcs-mini/mcp-tools/CMakeLists.txt,在 listenai_library_sources(...) 里增加:
mcp_tool_mg90s.c
工具名:mg90s_control
参数:
direction(必填,字符串)
cw / clockwise / 顺时针ccw / counterclockwise / 逆时针speed(可选,字符串或整数,默认中速)
low / mid / high / stop0~100(0 表示停止)在工程根目录执行:
./build.sh -S ./apps/arcs-mini/
利用 cskburn desktop 将
./build/arcs-mini.bin 烧录到 0x600000
b310080db46cea28537192e9587af73b.mp4
service_mg90s_init() 已经在系统初始化里执行。舵机控制失败。direction 报错cw 或 ccw(也支持 顺时针/逆时针)。speed 会怎样50%)。