概述
emMCP(Easy MCU MCP) 是安信可开源的适配库,用于快速对接小安 AI 模组(AiPi-PalChatV1)的 UART-MCP 协议。它封装了串口收发、JSON 解析、指令打包与状态机,开发者只需实现 3 个底层接口、调用 3 个 API 即可完成接入,最小占用仅 RAM 62 字节 / Flash 1708 字节。
官方基础工程已移植好 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 库
文件路径:~/DOCS_TEST1/components/emMCP_config.h
官方默认配置已含基础宏:emMCP_printf/malloc/free/delay、emMCP_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_delay(延时)、emMCP_malloc/emMCP_free(内存)、emMCP_uart_send(串口发送,指向 USART2)、emMCP_printf(日志)。
文件路径:~/DOCS_TEST1/Core/Src/freertos.c
发送侧:emMCP_config.h 的 emMCP_uart_send 宏已指向 HAL_UART_Transmit(&huart2,...),port/uartPort.c 的 uartPortSendData() 无需修改。
接收侧:官方已在 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.c,StartDefaultTask 函数
官方已实现全部初始化与心跳,无需修改:
/* 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)” 按钮烧录(具体操作见 编译和下载工程 步骤②)。
验证通信:
-
板子上电,AI 模组连接 WiFi;
-
用 Seahi-Serial 串口助手连接 USART1(PA9),波特率 1500000,应看到:
[INFO] emMCP init done, MAX_TOOLS=7 -
对 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_SIZE ≥ 6656(本项目值,改为 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 收发链路已经全通。

