Skip to content

概述

emMCP(Easy MCU MCP) 是安信可开源的适配库,用于快速对接小安 AI 模组(AiPi-PalChatV1)的 UART-MCP 协议。它封装了串口收发、JSON 解析、指令打包与状态机,开发者只需实现 3 个底层接口、调用 3 个 API 即可完成接入,最小占用仅 RAM 62 字节 / Flash 1708 字节。

官方基础工程已移植好 emMCP(配置、收发接口、心跳循环、接收回调均已就绪)。本章先理解官方工程中的移植实现,再升级配置(工具数量上限与注册模式),最后验证通信。

官方仓库:https://github.com/Ai-Thinker-Open/emMCP


🎯本页目标理解基础工程已移植的 emMCP 框架,升级配置并完成与 AI 模组的通信验证。
🧰前置条件① 软件与编译环境就绪(见 [软件安装](./software-setup) 与 [编译和下载工程](./vscode-setup))② 工程可正常编译 ③ 已准备 AI 模组(AiPi-PalChatV1)。
🔗相关章节移植完成后,进入 [OLED 显示](./oled-display) 点亮屏幕。
确认工程已包含 emMCP 框架

你的工程(~/DOCS_TEST1,复制自官方)已包含 emMCP 框架(仓库根的 emMCP/port/uart-mcp/ 目录),无需克隆或复制。确认目录存在:

cd ~/DOCS_TEST1
ls ../../../emMCP   # 应看到 emMCP/、port/、uart-mcp/ 三个目录

框架核心目录:

emMCP/
├── emMCP/            # CMake 工程入口(add_library(emMCP INTERFACE))
├── port/             # ★ 移植层:这里写 MCU 的适配代码
│   ├── uartPort.c    # 串口发送/接收接口
│   ├── uartPort.h
│   └── emMCP_port_config_example.h   # 配置模板(复制出去改名使用)
└── uart-mcp/         # 框架核心:协议解析、状态机、工具管理(无需修改)
    ├── emMCP.c
    ├── emMCP.h       # ★ 全部 API 声明
    └── cJSON/        # 轻量 JSON 库
升级 emMCP_config.h(工具上限 + 手动注册模式)

文件路径~/DOCS_TEST1/components/emMCP_config.h

官方默认配置已含基础宏:emMCP_printf/malloc/free/delayemMCP_uart_send 串口发送宏。要支持后续章节的多个工具,需要在 emMCP_uart_send 宏之后插入以下两段:

#define emMCP_uart_send(data, len)  HAL_UART_Transmit(&huart2, (uint8_t*)(data), (len), HAL_MAX_DELAY)  ← 现有代码
/* 工具数量上限:本项目 7 个工具 */
#ifdef MCP_SERVER_TOOL_NUMBLE_MAX
#undef MCP_SERVER_TOOL_NUMBLE_MAX
#endif
#define MCP_SERVER_TOOL_NUMBLE_MAX 7

/* 手动模式:工具用 mcp-tool JSON 字符串注册(节省 ~3KB 堆内存)*/
#define EMMCP_MANUAL_TOOLS_ONLY
#ifndef emMCP_uart_send
...

emMCP_config.h 文件内容

四个宏的分工(官方已配好):emMCP_delay(延时)、emMCP_malloc/emMCP_free(内存)、emMCP_uart_send(串口发送,指向 USART2)、emMCP_printf(日志)。

确认串口发送与接收接口(官方已实现,无需修改)

文件路径~/DOCS_TEST1/Core/Src/freertos.c

发送侧:emMCP_config.hemMCP_uart_send 宏已指向 HAL_UART_Transmit(&huart2,...)port/uartPort.cuartPortSendData() 无需修改。

接收侧:官方已在 HAL_UARTEx_RxEventCallback 回调中实现 USART2 空闲中断 + DMA 接收,调用 uartPortRecvData() 把整帧数据交给 emMCP,并重启下一次 DMA 接收:

/* freertos.c —— 官方已实现(简单版) */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
  if (huart == &huart2) {
    uartPortRecvData((char *)rxBuffer, Size);              // 交给 emMCP 解析
    HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rxBuffer, RXBUFFSER_MAX_SIZE);  // 重启 DMA 接收
    __HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
  }
}

⚠️ 升级为多消息拆包版(示例工程最终代码):官方简单版把整帧数据直接交给 emMCP,若 AI 一次发来多条消息(工具调用 + 字幕)会解析错乱。示例工程对接收做了大括号深度匹配拆包,支持一次处理多条 JSON。按以下三步升级:

第一步:Variables 区添加拆包变量USER CODE BEGIN Variables 内):

/* USER CODE BEGIN Variables */
static char rx_msg_buf[4][RXBUFFSER_MAX_SIZE];
static volatile uint8_t rx_msg_count = 0;
static uint8_t rx_msg_idx = 0;
static char rx_raw_buf[RXBUFFSER_MAX_SIZE];
/* USER CODE END Variables */

第二步:替换接收回调为拆包版

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
  if (huart == &huart2) {
uartPortRecvData((char *)rxBuffer, Size);               ← 官方简单版,删除
uint16_t sz = Size < RXBUFFSER_MAX_SIZE ? Size : RXBUFFSER_MAX_SIZE - 1;
    memcpy(rx_raw_buf, rxBuffer, sz);
    rx_raw_buf[sz] = '\0';
    HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rxBuffer, RXBUFFSER_MAX_SIZE);  ← 现有代码
    __HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);                       ← 现有代码
/* 拆包:大括号深度匹配,支持多条 JSON */
    char *p = (char *)rx_raw_buf;
    rx_msg_count = 0;
    while (rx_msg_count < 4) {
      char *start = memchr(p, '{', sz - (p - (char *)rx_raw_buf));
      if (!start) break;
      int depth = 0;
      char *end = start;
      while (end < (char *)rx_raw_buf + sz) {
        if (*end == '{') depth++;
        else if (*end == '}') { depth--; if (depth == 0) break; }
        end++;
      }
      if (depth != 0) break;
      int len = end - start + 1;
      if (len >= RXBUFFSER_MAX_SIZE) len = RXBUFFSER_MAX_SIZE - 1;
      memcpy(rx_msg_buf[rx_msg_count], start, len);
      rx_msg_buf[rx_msg_count][len] = '\0';
      rx_msg_count++;
      p = end + 1;
    }
    if (rx_msg_count > 0) {
      rx_msg_idx = 0;
    }
  }
}

第三步:主循环逐条处理拆包消息StartDefaultTask 的 for 循环内):

  for (;;) {                                                  ← 现有代码
    while (rx_msg_idx < rx_msg_count) {
      uartPortRecvData(rx_msg_buf[rx_msg_idx], strlen(rx_msg_buf[rx_msg_idx]));
      rx_msg_idx++;
      emMCP_TickHandle(100);
    }
    emMCP_TickHandle(100);                                  ← 现有代码
  }

DMA 接收的启动在 StartDefaultTask 任务开头,官方已实现,无需修改。

确认初始化与心跳循环(官方已实现,无需修改)

文件路径~/DOCS_TEST1/Core/Src/freertos.cStartDefaultTask 函数

官方已实现全部初始化与心跳,无需修改:

/* freertos.c —— 官方已实现 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, (uint8_t *)rxBuffer, sizeof(rxBuffer));  // 启动 DMA 接收
__HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
emMCP_Init(&emMCP);                                  // 初始化框架
for (;;) {
  emMCP_TickHandle(100);                             // 心跳循环,处理收发数据
}
编译烧录并验证(图形化,方法同"编译和下载工程"章节)

编译(图形化):点击 VSCode 左侧活动栏 CMake 图标 → 构建预设选 Debug → 点击 Build 按钮(具体操作与配图见 编译和下载工程 步骤①)。

编译 0 报错。若提示找不到头文件,检查 include 路径是否包含 components/port/uart-mcp/(检查 components/、port/、uart-mcp/ 是否在 include 路径中)。

烧录(图形化):ST-Link 接好(SWDIO→PA13、SWCLK→PA14、GND→GND)并用 Seahi-Serial 映射进 WSL 后,点击 VSCode 底部栏“烧录并调试 STM32F103 (OpenOCD)” 按钮烧录(具体操作见 编译和下载工程 步骤②)。

验证通信

  1. 板子上电,AI 模组连接 WiFi;

  2. Seahi-Serial 串口助手连接 USART1(PA9),波特率 1500000,应看到:

    [INFO] emMCP init done, MAX_TOOLS=7
    
  3. 对 AI 模组说 “你好小安”,唤醒后串口日志出现以下内容即表示移植成功:

    [DEBUG] emMCP_EventCallback: emMCP_EventCallback: event:8,type:4,param:2.WakeUP
    

没有 WakeUP 日志?① AI 模组未配网:参考 AiPi-PalChatV1 文档先用 App 配网;② 接线错误:AI 模组接 USART2(PA2→模组 RX、PA3→模组 TX),调试日志在 USART1;③ 波特率错误:USART2 必须 115200,USART1 为 1500000。


常见问题与踩坑提示

🔧 编译报 fatal error: emMCP.h: No such file or directory
原因:include 路径没配
解决:顶层 CMakeLists 确认 add_subdirectory 引到了 emMCP,且 components/CMakeLists 设置了 EMCP_USER_CONFIG_FILE;include 目录含 uart-mcp/

🔧 编译报 EMCP_USER_CONFIG_FILE 未定义
原因:配置文件路径没传进去
解决:顶层 CMakeLists 中 set(EMCP_USER_CONFIG_FILE ...) 必须在 add_subdirectory(emMCP) 之前

🔧 烧录后串口只有第一次能收指令,之后没反应
原因:接收回调里没重新启动 DMA 接收
解决HAL_UARTEx_ReceiveToIdle_DMA(&huart2, ...) 必须在回调里再次调用(本项目写法参考第③步)

🔧 说"你好小安"没任何日志
原因:① 模组没配网 ② 接线错 ③ 波特率错
解决:① 用 App 配网(参考 AiPi-PalChatV1)② AI 模组必须接 USART2(PA2/PA3)③ USART2=115200

🔧 日志乱码
原因:串口助手波特率不对
解决:USART1 调试日志是 1500000(不是 115200!)

🔧 只有 WakeUP 但 AI 不回答
原因:模组固件版本不对
解决:确认模组固件是 UART-MCP 版本(参考 AiPi-PalChatV1 文档刷固件)

🔧 emMCP_Init 后立刻 hardfault
原因:FreeRTOS 堆太小,内存分配失败
解决:检查 configTOTAL_HEAP_SIZE6656(本项目值,改为 4096 会因 WS2812 缓冲分配失败)

🔧 移植到自己工程后找不到 log.h
原因:emMCP_config.h 里 include 了 log.h
解决:要么把 log 组件一起移植,要么把 emMCP_printf 宏改成你自己的日志函数

🔧 换串口后没反应
原因:emMCP_uart_send 还指着旧串口
解决:emMCP_config.h 里 emMCP_uart_send 改成你的 AI 模组串口(本项目 huart2)

🔧 手动发 JSON 指令 MCU 不回应
原因:① 没勾"发送新行" ② 消息格式错
解决:① 串口助手勾选发送新行(\r\n)② 严格按 {"role":"AI","msgType":"MCP","data":{...}} 格式

移植成功的终极标志

对 AI 模组说"你好小安",串口日志出现 WakeUP,然后说"今天天气怎么样",AI 模组能正常回复(哪怕 MCU 不执行任何动作)——说明 emMCP 收发链路已经全通。

下一步

Released under the MIT License. Build Time 2026-08-07 22:59:19