[i=s] 本帖最后由 云烟 于 2026-1-8 14:42 编辑 [/i]
基于 Ai-WV01-32S+STM32移植 emMCP 实现 AI 语音控制点灯
前言
emMCP(Easy MCU MCP)是 AI-Thinker 推出的轻量级 UART-MCP 适配库,旨在降低 MCU 对接 AI 语音模组的开发门槛。
该库封装了 UART-MCP 协议的核心交互逻辑,提供简洁的 API 接口,开发者无需深入理解复杂的协议细节,
即可快速实现 MCU 与 AI 语音模组的通信,完成 AI 语音控制外设的开发。
帮助开发者快速使用 MCU 接入 AI 模型进行 MCP 交互。官方详细介绍
本文将以 STM32F103C8T6(HAL 库)为例,完整讲解 emMCP 库的移植、配置流程,并最终实现AI语音对话控制 LED 灯亮灭的功能,适合有一定基础 STM32 HAL 库开发经验的开发者参考。
一、准备工作
1.1 硬件清单
| 硬件名称 |
数量 |
备注 |
| STM32F103C8T6开发板 |
1 |
核心控制MCU |
| Ai-WV01-32S AI 语音模组 |
1 |
需刷入小安 AI V2.8 测试固件 |
| ST-Link 烧录器 |
1 |
烧录 STM32 程序 |
| 扬声器 |
1 |
连接 AI 模组,播放语音反馈 |
| 麦克风 |
1 |
连接 AI 模组,采集语音指令 |
| LED 灯 |
1 |
被控外设(示例用 PB5 引脚) |
| 杜邦线 |
若干 |
硬件连接 |
1.2 软件清单
前置要求
- 已完成 STM32 USART1(用于log打印调试) 和USART2 的 HAL 库配置(开启 DMA 接收、RxEvent 中断),确保串口收发功能正常;
- 已完成 Ai-WV01-32S 模组的固件烧录,并验证模组可正常唤醒(默认唤醒词:“你好小安”);
- 熟悉 STM32 HAL 库基础操作(GPIO、UART、DMA、中断)。
二、emMCP 源码获取与目录解析
- 建议直接通过 Git 克隆源码仓库,也可点击链接跳转直接下载
ZIP包:
#GitHub
git clone https://github.com/Ai-Thinker-Open/emMCP.git
#Gitee
git clone https://gitee.com/Ai-Thinker-Open/emMCP.git
emMCP
├── example ------> 示例代码文件加
│ ├── STM32F10xRTOS_MCP ------> STM32F103 的 CMake 示例
│ └── ...
├── port ------> emMCP 移植接口文件夹
│ ├── uartPort.h ------> emMCP 移植接口头文件
│ └── uartPort.c ------> emMCP 移植接口源文件
└── uart-mcp ------> emMCP 串口通讯文件夹
├──cJSON ------> cJSON 库
│ ├── cJSON.h ------> cJSON 头文件
│ └── cJSON.c ------> cJSON 源文件
├── emMCP.h ------> emMCP 头文件
└── emMCP.c ------> emMCP 源文件
三、emMCP 库移植到 STM32 工程
- 将
emMCP 源码中的 port、uart-mcp和 APP文件夹复制到你的 STM32 工程目录下(建议新建 emMCP文件夹统一存放),工程目录示例:

- 打开IDE编辑工程关联上述文件夹中的
.c和 .h文件,本章使用VScode,涉及插件安装以及配置可参考官方文章: 点击跳转

四、接口移植配置
- 本节提示 :记得在配置接口函数时
#include相应的头文件哦
- 打开
port/uartPort.c 文件,找到函数在函数 int uartPortSendData(char *data, int len)
当中调用你的串口发送函数,将 data和 len 作为参数传入。例如:
#include "uartPort.h"
#include "stm32f1xx_hal.h"
// 全局UART句柄(需与实际配置的串口一致,示例为USART2)
extern UART_HandleTypeDef huart2;
/**
* @brief 串口发送函数接口
*
* @param data
* @param len
* @return int
*/
int uartPortSendData(char *data, int len) {
if (data == NULL || len <= 0) {
return -1;
}
// 在此处实现串口发送函数
return HAL_UART_Transmit(&huart2, (uint8_t *)data, len, 100); // 返回发送状态
}
- 内存管理函数的实现,在
port/port.h 文件中,将内存管理函数在宏 #define emMCP_malloc 和 #define emMCP_free 中实现,例如:

- 注意:若工程启用了 RTOS,可替换为 RTOS 的内存管理函数(如pvPortMalloc/vPortFree)
- 延时函数的实现,在
port/port.h 文件中,将延时函数在宏 #define emMCP_delay 中实现,例如:

- 注意:若工程启用了 RTOS,可替换为 RTOS 的osDelay
- 在 MCU 的串口接收函数当中调用
int uartPortRecvData(char *data, int len)的接收函数,emMCP 的接收逻辑需要在 STM32 串口接收中断中触发,推荐使用 DMA+RxEvent 中断方式(避免数据丢失)例如:
#include "emMCP.h"
#include "uartPort.h"
#define UART_RXBUFF_MAX 256
emMCP_t emMCP;
uint8_t rxBuffer[UART_RXBUFF_MAX] = {0};
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart == &huart2) {
//调用emMCP接收函数
uartPortRecvData((char *)rxBuffer, Size);
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rxBuffer, UART_RXBUFF_MAX);
__HAL_DMA_DISABLE_IT(huart->hdmarx, DMA_IT_HT);
}
}
- 在
main.c的 main函数中,初始化串口 DMA接收:
// 其他初始化(时钟、GPIO、UART等)完成后
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rxBuffer, UART_RXBUFF_MAX);
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); // 禁用半传输中断
- 在
main.c中初始化emMCP,并在主循环 while中调用 emMCP_TickHandle()例如:
static emMCP_t emMCP_dev;
int main(void)
{
//其他初始化函数
emMCP_Init(&emMCP_dev);
while(1)
{
emMCP_TickHandle(10);
//其他任务
}
}
6.完成上述步骤后,编译项目,确保零错误零警告,按照以下步骤验证是否移植成功:
- 下载程序到MCU
- 等待小安初始化成功
- 对AI模组说:“你好小安”
- 得到AI模组回复后观察串口助手输出,得到以下类似输出,说明emMCP成功移植到MCU上:
[DEBUG] emMCP_EventCallback:99: emMCP_EventCallback: event:10,type:4,param:2.WakeUP
[DEBUG] emMCP_EventCallback:99: emMCP_EventCallback: event:11,type:4,param:3.Sleep
四、AI 语音控制点灯功能开发配置
- 需要在
main初始化配置emMCP结构体参数;详细的各函数参数配置描述可以参考 官方详细介绍 这里直接给出我的配置如下:
emMCP_tool_t relay;//创建工具
int main(void)
{
// 其他初始化代码
// 初始化MCP
emMCP_SetAiWakeUp(10);//唤醒10s
emMCP_Init(&emMCP);
relay.name = "继电器";//工具名称,保持唯一性
relay.description = "用来控制继电器的开关";//工具的功能描述
relay.inputSchema.properties[0].name = "enable";//属性指令,AI 通过这个指令发送命令
relay.inputSchema.properties[0].description = "控制继电器,打开:true,关闭为:false,查询为null"; //指令描述,AI 通过这个描述理解指令
relay.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;//指令类型,AI 通过这个类型发送相对应的数据
relay.setRequestHandler = emMCP_SetRelayHandler;//设置控制回调
relay.checkRequestHandler = emMCP_GetRelayHandler;//设置查询回调
emMCP_AddToolToToolList(&relay); // 添加工具到工具列表
emMCP_RegistrationTools(); // 注册工具到小安AI
while (1)
{
emMCP_TickHandle(10);
}
}
2.配置核心控制回调函数 emMCP_SetRelayHandler,emMCP_GetRelayHandler,该部分为控制效果的实际体现,注意你实际LED灯对应的引脚。我的配置如下:
//控制回调函数
static void emMCP_SetRelayHandler(void *arg)
{
// 接收到的数据
cJSON *param = (cJSON *)arg;
// 控制继电器
cJSON *enable = cJSON_GetObjectItem(param, "enable");//获取继电器命令
if (enable != NULL) {
if (enable->valueint == 1) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);
} else if (enable->valueint == 0) {
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
}
emMCP_ResponseValue(emMCP_CTRL_OK); //返回控制成功
}else {
emMCP_ResponseValue(emMCP_CTRL_ERROR); //返回控制失败
}
}
//查询回调函数
static void emMCP_GetRelayHandler(void *arg){}
五、功能测试
- 编译烧录程序到 STM32,确保硬件连接正常;
- 唤醒 AI 模组:“你好小安”(模组回应 “你好呀”);
- 发送语音指令:
“打开继电器” → PB5 引脚输出低电平,LED 点亮;
“关闭继电器 → PB5 引脚输出高电平,LED 熄灭;
- 模组会语音反馈 “已打开 LED 灯”/“已关闭 LED 灯”,同时串口输出指令日志。

