1. Development Environment Setup
The 9Mod MCPBoard uses STM32F103CBT6 as the main MCU, communicating with the Ai-WV01-32S module via the emMCP library for MCP protocol interaction.
Required Tools
| Tool | Purpose | Download |
|---|---|---|
| VSCode | Code editing and compilation | Official |
| STM32CubeMX | STM32 peripheral configuration and project generation | Official |
| ARM GCC Toolchain | STM32 firmware cross-compilation | ARM Official |
| CMake | Build system (used by examples) | Official |
| ST-Link Debugger | Program flashing and debugging | - |
| BLDevCube | Ai-WV01-32S firmware flashing | Download |
Reference Video
1.1 Install ARM GCC Toolchain
ARM GCC (gcc-arm-none-eabi) is the cross-compiler for compiling STM32 firmware. It supports Cortex-M0 through Cortex-M7 series cores.
Installation methods (choose one):
| Method | Description | Command / Link |
|---|---|---|
| ARM Official (recommended) | Download archive, extract, configure PATH | Download |
| xPack (Windows) | Package manager install, auto environment setup | xpm install --global @xpack-dev-tools/arm-none-eabi-gcc@latest |
| MSYS2 (Windows) | Install via pacman | pacman -S mingw-w64-x86_64-arm-none-eabi-gcc |
| apt (Ubuntu/Debian) | Install via system package manager | sudo apt install gcc-arm-none-eabi |
Verify installation:
arm-none-eabi-gcc --versionExpected output:
arm-none-eabi-gcc (GNU Arm Embedded Toolchain 10.3-2021.10) 10.3.1 20210824 (release)
Copyright (C) 2020 Free Software Foundation, Inc.VSCode Extensions:
Search for and install the following extensions in the VSCode marketplace:
- Cortex-Debug — ST-Link debugging support: variable watch, breakpoints, register view
- CMake — CMake syntax highlighting and integrated build
- C/C++ — Code completion, navigation, syntax checking
1.2 STM32CubeMX Project Configuration
The 9Mod board example already includes a complete .ioc configuration file — no need to create a project from scratch. Open it directly to review or adjust peripheral settings.
Steps:
Launch STM32CubeMX
Click File > Open Project and select the
.iocfile in the example directory:emMCP/example/9Mod_MCPBoard/9Mod_MCPBoard.iocReview key chip configuration (pre-set, no modification needed):
Parameter Value Chip Model STM32F103CBT6 HSE (External Crystal) 8 MHz SYSCLK (System Clock) 72 MHz APB1 Clock 36 MHz APB2 Clock 72 MHz Peripheral configuration overview (pre-set in the example):
Peripheral Pins / Interface Communication Purpose USART1 PA9(TX) / PA10(RX) 115200-8N1 Debug log output USART2 PA2(RX) / PA3(TX) 115200-8N1 AI module MCP communication USART3 PB10(TX) / PB11(RX) 9600-8N1 IR module control I2C1 PB6(SCL) / PB7(SDA) 400kHz OLED, SHT30, PD decoy (shared bus) GPIO - PA8 PA8 (input) - Radar module status detection GPIO - PB4 PB4 (input) - User button 1 GPIO - PB8 PB8 (input) - User button 2 GPIO - PB5 PB5 (output) - Relay control GPIO - PA11 PA11 (output) - WS2812 LED strip GPIO - PC13 PC13 (output) - Onboard LED SWD PA13(SWDIO) / PA14(SWCLK) - Debug and flash interface To adjust peripheral parameters (e.g., baud rate), modify them in the Pinout & Configuration view, then click Project > Generate Code to regenerate. Keep the "Keep User Code" option enabled to avoid overwriting existing business logic.
Note: If you are only developing application layer logic (MCP tools), there is no need to modify the CubeMX configuration — skip ahead to §1.3 to compile.
1.3 CMake Build System
The example uses CMake as the build system (instead of traditional Makefiles). CMake generates build scripts from CMakeLists.txt, then invokes ARM GCC for cross-compilation.
Install CMake:
VSCode users: The CMake extension includes built-in CMake tools — no separate installation needed
Standalone install: Download from CMake official site or use a package manager:
bash# Ubuntu / Debian sudo apt install cmake # macOS (Homebrew) brew install cmake # Windows (MSYS2) pacman -S mingw-w64-x86_64-cmakeVerify installation:
bashcmake --version
Method 1: Terminal Command Build
# Navigate to example directory
cd emMCP/example/9Mod_MCPBoard
# Create build directory
mkdir -p build && cd build
# Configure CMake (specify ARM GCC toolchain)
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/gcc-arm-none-eabi.cmake
# Compile
make -j$(nproc)Method 2: VSCode CMake Plugin One-Click Build
- Open the example directory in VSCode: File > Open Folder >
emMCP/example/9Mod_MCPBoard - Press
F1orCtrl+Shift+P, enterCMake: Configureand press Enter - Select the compiler as ARM GCC (arm-none-eabi)
- Click the Build button on the bottom status bar (or press
F7)
CMakeLists.txt Key Configuration:
| Setting | Description |
|---|---|
project(9Mod_MCPBoard C ASM) | Project name, supports C and assembly source files |
CMAKE_TOOLCHAIN_FILE | Specifies ARM GCC cross-compilation toolchain file |
target_compile_definitions | Compile macro definitions (e.g., emMCP porting macros) |
add_executable | Adds target executable, links all source files |
TARGET_LINK_DIRECTORIES | Linker script directory (.ld files) |
TARGET_LINK_OPTIONS | Linker options (e.g., -T STM32F103CBTx_FLASH.ld) |
1.4 Build Verification
After completing the build as described in §1.3, verify the result:
Check build output:
[100%] Built target 9Mod_MCPBoard.elfConfirm generated files:
ls -la build/Should include the following key files:
| File | Description |
|---|---|
9Mod_MCPBoard.elf | ELF executable (with debug info) |
9Mod_MCPBoard.hex | Intel HEX format (for flashing) |
9Mod_MCPBoard.bin | Raw binary format (for flashing) |
9Mod_MCPBoard.map | Linker map (memory layout) |
Common error troubleshooting:
| Error | Cause | Solution |
|---|---|---|
arm-none-eabi-gcc: command not found | ARM GCC not installed or not in PATH | Check §1.1 installation steps |
CMake Error: Could not find CMAKE_TOOLCHAIN_FILE | Toolchain file path wrong | Ensure running cmake .. from example root |
undefined reference to HAL_xxx | STM32 HAL lib not properly included | Ensure code was regenerated with STM32CubeMX |
No such file or directory: STM32F103CBTx_FLASH.ld | Linker script path wrong | Check .ld file exists in cmake/ directory |
Expected result: A clean build should have no errors (0 errors, 0 warnings ideally) and generate
.hexand.binfirmware files inbuild/for flashing.
2. Get the Source Code
Clone the emMCP repository via Git:
git clone https://github.com/Ai-Thinker-Open/emMCP.gitDirectory Structure
emMCP
├── example/ # Example projects
│ ├── 9Mod_MCPBoard/ # 9Mod board dedicated example ⭐
│ └── STM32F40xRTOS_XiaoZhiAI/ # STM32F407 FreeRTOS XiaoZhiAI example
├── port/ # Porting interface layer
│ ├── uartPort.h # Porting header (with config system)
│ ├── uartPort.c # UART TX/RX (double buffer)
│ ├── emMCP_port_config_example.h # Config example (STM32 HAL + FreeRTOS)
│ ├── emMCP_port_config_template.h # Config template
│ └── README_PORT.md # Porting configuration guide
└── uart-mcp/ # emMCP core library
├── cJSON/
│ ├── cJSON.h
│ └── cJSON.c
├── emMCP.h # emMCP main header
├── emMCP.c # emMCP main source
└── emMCPLOG.h # Logging headerTIP
The dedicated 9Mod board example is located at example/9Mod_MCPBoard/, pre-configured with MCP tools for all onboard peripherals. Use it as your starting point.
3. Hardware Connections
AI Module to MCU (UART)
| STM32 Pin | Ai-WV01-32S Pin | Function |
|---|---|---|
| PA2 (USART2_RX) | TX | MCP protocol data receive |
| PA3 (USART2_TX) | RX | MCP protocol data transmit |
WARNING
Connected via jumper caps. Ensure USART2 TX/RX are correctly paired with the module. Default baud rate: 115200.
ST-Link Debug Connection
| ST-Link Pin | STM32 Pin |
|---|---|
| SWDIO | PA13 |
| SWCLK | PA14 |
| GND | GND |
| 3.3V | VDD |
Before flashing, ensure BOOT0 is pulled low (connected to GND) via jumper cap.
4. Porting emMCP
Refer to Porting to MCU for details. Key configurations for the 9Mod board:
v1.0.1 Config System Change
port/port.h has been removed. Platform-specific macros (delay, memory management, UART send) have been migrated to uartPort.h with a conditional compilation + user configuration design. A double-buffer RX mechanism has been added for improved UART data stability.
4.1 Configure Porting Macros
emMCP v1.0.1 provides three ways to configure platform-specific macros:
Method 1: Direct Macro Definition (Simple Projects) Define before including uartPort.h:
#define emMCP_printf log_printf // Print function
#define emMCP_malloc pvPortMalloc // Memory allocation
#define emMCP_free vPortFree // Memory free
#define emMCP_delay osDelay // Delay function
#define emMCP_uart_send HAL_UART_Transmit // UART send (NEW!)
#include "uartPort.h"Method 2: Create Config File (Recommended) Create emMCP_port_config.h in your project directory. uartPort.h auto-detects it via __has_include. Refer to port/emMCP_port_config_example.h.
Method 3: CMake Build Definition
target_compile_definitions(9Mod_MCPBoard PRIVATE
emMCP_printf=log_printf
emMCP_malloc=pvPortMalloc
emMCP_free=vPortFree
emMCP_delay=osDelay
emMCP_uart_send=HAL_UART_Transmit
)4.2 Implement UART TX/RX
uartPortSendData() now calls the emMCP_uart_send(data, len) macro internally. Just ensure the macro is defined correctly:
// Method A: Via macro (recommended)
#define emMCP_uart_send HAL_UART_Transmit
// Method B: Write the implementation directly (optional)
int uartPortSendData(char *data, int len)
{
if (data == NULL || len <= 0) return -1;
return HAL_UART_Transmit(&huart2, (uint8_t *)data, len, 100);
}In the DMA receive callback, call uartPortRecvData() — the new version automatically uses double buffering to avoid dynamic memory allocation in interrupts:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART2) {
HAL_UARTEx_ReceiveToIdle_DMA(huart, (uint8_t *)rxBuffer, sizeof(rxBuffer));
uartPortRecvData((char *)rxBuffer, Size); // Auto-stored in double buffer
__HAL_DMA_ENABLE_IT(&hdma_usart2_rx, DMA_IT_TC);
}
}Retrieve data in the main loop or task using these thread-safe APIs:
char *data = uartPortGetRxData(); // Get received data
if (data != NULL) {
// Process JSON/MCP data...
uartPortClearRxData(); // Mark as processed, allow new RX
}4.3 New Helper APIs
| API | Purpose |
|---|---|
uartPortGetRxData() | Get data from double buffer (thread-safe, NULL = no new data) |
uartPortClearRxData() | Mark data as processed, allow interrupt to receive new data |
emMCP_UpdateUartRecv(bool isRecv) | Update UART receive status |
emMCP_CheckUartSendStatus() | Check UART send completion status |
5. Initialization and Main Loop
static emMCP_t emMCP_dev;
static uint8_t uart_rx_buf[512]; // Receive buffer
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init(); // AI module UART
MX_USART1_UART_Init(); // Debug log UART
// Initialize peripherals
OLED_Init(); // 0.96" OLED
WS2812_Init(); // WS2812 LED strip
Relay_Init(); // Relay
SHT30_Init(); // Temp/Humidity sensor
// Initialize emMCP
emMCP_Init(&emMCP_dev);
// Set UART data buffer
uartPortSetDataBuf((char *)uart_rx_buf);
while (1)
{
emMCP_TickHandler(); // emMCP state machine
userTaskHandler(); // User tasks
}
}TIP
emMCP_TickHandler() must be called frequently in the main loop to maintain MCP protocol responsiveness.
6. Registering MCP Tools
MCP tools are the bridge between AI and hardware. Each tool includes a name, description, parameters, and callback function.
6.1 Tool Structure
typedef struct {
const char *name; // Tool name (used by AI)
const char *description; // Tool description
emMCP_param_t params[EMCP_MAX_TOOL_PARAMS]; // Parameter list
int param_count; // Parameter count
emMCP_tool_cb callback; // Callback function
} emMCP_tool_t;6.2 Callback Signature
char* tool_callback(mcp_server_tool_type_t type, void *param);type: Parameter type (usuallyMCP_TOOL_TYPE_STR)param: JSON parameter string- Returns: JSON string with execution result
6.3 Example: Relay Control
// 1. Parameter definition
emMCP_param_t relay_params[] = {
{"state", "Switch state: on/off"}
};
// 2. Callback function
char* relay_control_cb(mcp_server_tool_type_t type, void *param)
{
char *param_str = (char *)param;
cJSON *json = cJSON_Parse(param_str);
cJSON *state = cJSON_GetObjectItem(json, "state");
if (state && strcmp(state->valuestring, "on") == 0) {
HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET);
}
cJSON_Delete(json);
return "{\"result\": \"ok\"}";
}
// 3. Create and register tool
emMCP_tool_t relay_tool = {
.name = "relay_control",
.description = "Control the relay switch",
.params = relay_params,
.param_count = 1,
.callback = relay_control_cb
};
emMCP_RegTool(&emMCP_dev, &relay_tool);7. 9Mod Board Peripheral Quick Reference
| Peripheral | GPIO / Interface | Protocol | Recommended Tool Name |
|---|---|---|---|
| AI Module | PA2(RX) / PA3(TX) | USART2 | - (used internally by emMCP) |
| Relay | PB5 | GPIO Out | relay_control |
| WS2812 LED | PA11 | Single-wire | led_control |
| SHT30 Temp/Humid | PB6(SCL) / PB7(SDA) | I²C | get_temperature, get_humidity |
| OLED Display | PB6(SCL) / PB7(SDA) | I²C | oled_display |
| Radar Module | PA8 | GPIO In | get_radar_status |
| IR Control | PB10(TX) / PB11(RX) | USART3 | ir_send |
| User Buttons | PB4, PB8 | GPIO In | - (event trigger) |
| PD Decoy | PB6(SCL) / PB7(SDA) | I²C | pd_set_voltage |
| Debug UART | PA9(TX) / PA10(RX) | USART1 | - (log output) |
I²C Bus Sharing
OLED, SHT30, and PD decoy share PB6(SCL) / PB7(SDA) I²C bus. They are distinguished by device address (OLED: 0x3C, SHT30: 0x44, CH224K: 0x48).
8. Full Example: Temperature & Humidity Tool
emMCP_param_t temp_params[] = {
{"type", "Reading type: temperature / humidity / both"}
};
char* get_sensor_cb(mcp_server_tool_type_t type, void *param)
{
float temp = 0, humi = 0;
SHT30_ReadData(&temp, &humi);
char result[128];
snprintf(result, sizeof(result),
"{\"temperature\": %.1f, \"humidity\": %.1f, \"unit_temp\": \"°C\", \"unit_humi\": \"%%\"}",
temp, humi);
static char ret_buf[256];
strcpy(ret_buf, result);
return ret_buf;
}
emMCP_tool_t sensor_tool = {
.name = "get_environment",
.description = "Get ambient temperature and humidity",
.params = temp_params,
.param_count = 1,
.callback = get_sensor_cb
};
emMCP_RegTool(&emMCP_dev, &sensor_tool);Once registered, users can say "Check the temperature and humidity" via voice, and the AI will automatically call this tool and read the result aloud.
9. Event Callback Handling
emMCP provides rich event notifications. Redefine emMCP_EventCallback to listen:
void emMCP_EventCallback(emMCP_event_t event, mcp_server_tool_type_t type, void *param)
{
char *str = (char *)param;
switch (event) {
case emMCP_EVENT_AI_WAKE:
OLED_ShowString("Listening...");
break;
case emMCP_EVENT_AI_SLEEP:
OLED_ShowString("Standby");
break;
case emMCP_EVENT_AI_MCP_CMD:
printf("[MCP] %s\n", str);
break;
case emMCP_EVENT_AI_MCP_Text:
OLED_ShowString(str);
break;
case emMCP_EVENT_AI_WIFI_CONNNECT:
OLED_ShowString("WiFi OK");
break;
case emMCP_EVENT_AI_WIFI_DISCONNECT:
OLED_ShowString("WiFi Lost");
break;
default:
break;
}
}For the full event list, see emMCP Event List.
10. Debugging
10.1 TTL UART Logging
The 9Mod board exposes USART1 (PA9/PA10) via CH340C chip as a debug UART at 115200 baud. Connect via Type-C to any serial terminal (PuTTY, MobaXterm, etc.).
// Redirect printf to USART1
int _write(int fd, char *ptr, int len)
{
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 100);
return len;
}10.2 OLED Display Debugging
Call OLED display functions at key points to visualize program state:
OLED_ShowString(0, 0, "MCP Init OK");
OLED_ShowString(0, 2, "Tool: 5 reg");
OLED_ShowString(0, 4, "WiFi: connected");10.3 Common Issues
| Symptom | Possible Cause | Solution |
|---|---|---|
| emMCP receives no data | Wrong UART pins or baud rate | Verify PA2/PA3 jumper caps, baud rate 115200 |
| AI cannot call registered tool | Invalid tool name or not registered | Ensure name uses only [a-z0-9_], verify emMCP_RegTool called |
| MCP command timeout | Callback takes too long | Avoid blocking delays in callbacks; use tasks/queues for complex ops |
| Ai-WV01-32S unresponsive | Not provisioned or wrong firmware | Ensure Wi-Fi is set up, use V3.4 firmware |
| OLED blank | I²C address conflict | Verify OLED at 0x3C, SHT30 at 0x44 |
| Build error: undefined reference | Missing source files | Ensure all .c files in port/ and uart-mcp/ are included in the build |

