Skip to content

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

ToolPurposeDownload
VSCodeCode editing and compilationOfficial
STM32CubeMXSTM32 peripheral configuration and project generationOfficial
ARM GCC ToolchainSTM32 firmware cross-compilationARM Official
CMakeBuild system (used by examples)Official
ST-Link DebuggerProgram flashing and debugging-
BLDevCubeAi-WV01-32S firmware flashingDownload

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):

MethodDescriptionCommand / Link
ARM Official (recommended)Download archive, extract, configure PATHDownload
xPack (Windows)Package manager install, auto environment setupxpm install --global @xpack-dev-tools/arm-none-eabi-gcc@latest
MSYS2 (Windows)Install via pacmanpacman -S mingw-w64-x86_64-arm-none-eabi-gcc
apt (Ubuntu/Debian)Install via system package managersudo apt install gcc-arm-none-eabi

Verify installation:

bash
arm-none-eabi-gcc --version

Expected 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:

  1. Launch STM32CubeMX

  2. Click File > Open Project and select the .ioc file in the example directory:

    emMCP/example/9Mod_MCPBoard/9Mod_MCPBoard.ioc
  3. Review key chip configuration (pre-set, no modification needed):

    ParameterValue
    Chip ModelSTM32F103CBT6
    HSE (External Crystal)8 MHz
    SYSCLK (System Clock)72 MHz
    APB1 Clock36 MHz
    APB2 Clock72 MHz
  4. Peripheral configuration overview (pre-set in the example):

    PeripheralPins / InterfaceCommunicationPurpose
    USART1PA9(TX) / PA10(RX)115200-8N1Debug log output
    USART2PA2(RX) / PA3(TX)115200-8N1AI module MCP communication
    USART3PB10(TX) / PB11(RX)9600-8N1IR module control
    I2C1PB6(SCL) / PB7(SDA)400kHzOLED, SHT30, PD decoy (shared bus)
    GPIO - PA8PA8 (input)-Radar module status detection
    GPIO - PB4PB4 (input)-User button 1
    GPIO - PB8PB8 (input)-User button 2
    GPIO - PB5PB5 (output)-Relay control
    GPIO - PA11PA11 (output)-WS2812 LED strip
    GPIO - PC13PC13 (output)-Onboard LED
    SWDPA13(SWDIO) / PA14(SWCLK)-Debug and flash interface
  5. 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-cmake
  • Verify installation:

    bash
    cmake --version

Method 1: Terminal Command Build

bash
# 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

  1. Open the example directory in VSCode: File > Open Folder > emMCP/example/9Mod_MCPBoard
  2. Press F1 or Ctrl+Shift+P, enter CMake: Configure and press Enter
  3. Select the compiler as ARM GCC (arm-none-eabi)
  4. Click the Build button on the bottom status bar (or press F7)

CMakeLists.txt Key Configuration:

SettingDescription
project(9Mod_MCPBoard C ASM)Project name, supports C and assembly source files
CMAKE_TOOLCHAIN_FILESpecifies ARM GCC cross-compilation toolchain file
target_compile_definitionsCompile macro definitions (e.g., emMCP porting macros)
add_executableAdds target executable, links all source files
TARGET_LINK_DIRECTORIESLinker script directory (.ld files)
TARGET_LINK_OPTIONSLinker 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.elf

Confirm generated files:

bash
ls -la build/

Should include the following key files:

FileDescription
9Mod_MCPBoard.elfELF executable (with debug info)
9Mod_MCPBoard.hexIntel HEX format (for flashing)
9Mod_MCPBoard.binRaw binary format (for flashing)
9Mod_MCPBoard.mapLinker map (memory layout)

Common error troubleshooting:

ErrorCauseSolution
arm-none-eabi-gcc: command not foundARM GCC not installed or not in PATHCheck §1.1 installation steps
CMake Error: Could not find CMAKE_TOOLCHAIN_FILEToolchain file path wrongEnsure running cmake .. from example root
undefined reference to HAL_xxxSTM32 HAL lib not properly includedEnsure code was regenerated with STM32CubeMX
No such file or directory: STM32F103CBTx_FLASH.ldLinker script path wrongCheck .ld file exists in cmake/ directory

Expected result: A clean build should have no errors (0 errors, 0 warnings ideally) and generate .hex and .bin firmware files in build/ for flashing.


2. Get the Source Code

Clone the emMCP repository via Git:

bash
git clone https://github.com/Ai-Thinker-Open/emMCP.git

Directory 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 header

TIP

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 PinAi-WV01-32S PinFunction
PA2 (USART2_RX)TXMCP protocol data receive
PA3 (USART2_TX)RXMCP protocol data transmit

WARNING

Connected via jumper caps. Ensure USART2 TX/RX are correctly paired with the module. Default baud rate: 115200.

ST-Link PinSTM32 Pin
SWDIOPA13
SWCLKPA14
GNDGND
3.3VVDD

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:

c
#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

cmake
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:

c
// 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:

c
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:

c
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

APIPurpose
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

c
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

c
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

c
char* tool_callback(mcp_server_tool_type_t type, void *param);
  • type: Parameter type (usually MCP_TOOL_TYPE_STR)
  • param: JSON parameter string
  • Returns: JSON string with execution result

6.3 Example: Relay Control

c
// 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

PeripheralGPIO / InterfaceProtocolRecommended Tool Name
AI ModulePA2(RX) / PA3(TX)USART2- (used internally by emMCP)
RelayPB5GPIO Outrelay_control
WS2812 LEDPA11Single-wireled_control
SHT30 Temp/HumidPB6(SCL) / PB7(SDA)I²Cget_temperature, get_humidity
OLED DisplayPB6(SCL) / PB7(SDA)I²Coled_display
Radar ModulePA8GPIO Inget_radar_status
IR ControlPB10(TX) / PB11(RX)USART3ir_send
User ButtonsPB4, PB8GPIO In- (event trigger)
PD DecoyPB6(SCL) / PB7(SDA)I²Cpd_set_voltage
Debug UARTPA9(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

c
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:

c
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.).

c
// 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:

c
OLED_ShowString(0, 0, "MCP Init OK");
OLED_ShowString(0, 2, "Tool: 5 reg");
OLED_ShowString(0, 4, "WiFi: connected");

10.3 Common Issues

SymptomPossible CauseSolution
emMCP receives no dataWrong UART pins or baud rateVerify PA2/PA3 jumper caps, baud rate 115200
AI cannot call registered toolInvalid tool name or not registeredEnsure name uses only [a-z0-9_], verify emMCP_RegTool called
MCP command timeoutCallback takes too longAvoid blocking delays in callbacks; use tasks/queues for complex ops
Ai-WV01-32S unresponsiveNot provisioned or wrong firmwareEnsure Wi-Fi is set up, use V3.4 firmware
OLED blankI²C address conflictVerify OLED at 0x3C, SHT30 at 0x44
Build error: undefined referenceMissing source filesEnsure all .c files in port/ and uart-mcp/ are included in the build

11. Further Reading

Released under the MIT License.