This document provides example code and wiring instructions for each peripheral on the 9Mod MCP Verification Board. All examples are built on the emMCP v1.0.1 framework.
The general workflow is:
- Create an
emMCP_tool_tstruct and fill in its fields (name, description, callback handlers) - Call
emMCP_AddToolToToolList()for each tool - Call
emMCP_RegistrationTools()to sync all tool definitions to the AI platform
Once registered, the AI can directly control peripherals or read sensor data through voice commands.
TIP
For the complete project source code, refer to the example/9Mod_MCPBoard/ directory in the emMCP repository.
1. Relay Control
Wiring
| Relay Terminal | Description |
|---|---|
| NO (Normally Open) | Open by default, closes when GPIO is high |
| COM (Common) | Connect to the load line (e.g., light bulb, fan) |
| GND / VCC | Onboard, no external wiring needed |
STM32 control pin: PB5 (active high, optocoupler-isolated).
MCP Tool Registration
// Tool callback - control
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(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET);
}
emMCP_ResponseValue(emMCP_CTRL_OK);
} else {
emMCP_ResponseValue(emMCP_CTRL_ERROR);
}
}
// Tool callback - query
static void emMCP_GetRelayHandler(void *arg)
{
emMCP_ResponseValue(emMCP_CTRL_OK);
}
// Register in main()
emMCP_tool_t relay;
relay.name = "relay";
relay.description = "Control the relay switch";
relay.inputSchema.properties[0].name = "enable";
relay.inputSchema.properties[0].description = "Whether to turn on the relay, true=on, false=off, null for query";
relay.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;
relay.setRequestHandler = emMCP_SetRelayHandler;
relay.checkRequestHandler = emMCP_GetRelayHandler;
emMCP_AddToolToToolList(&relay);
emMCP_RegistrationTools();Voice Commands
"Turn on the relay" "Turn off the relay"
2. WS2812 LED Strip Control
Wiring
The WS2812 LED strip connects via the onboard 4-pin header:
| WS2812 Pin | Description |
|---|---|
| 5V | Power (onboard 5V output, external power recommended for long strips) |
| GND | Ground |
| DIN | Data input — PA11 (TIM1_CH4 PWM+DMA) |
| DOUT | Data output (cascade to next LED) |
Supports RGB color and brightness control. Each LED is individually addressable.
MCP Tool Registration
// Tool callback - control
static void emMCP_SetWS2812Handler(void *arg)
{
cJSON *param = (cJSON *)arg;
cJSON *red = cJSON_GetObjectItem(param, "red");
cJSON *green = cJSON_GetObjectItem(param, "green");
cJSON *blue = cJSON_GetObjectItem(param, "blue");
cJSON *brightness = cJSON_GetObjectItem(param, "brightness");
uint8_t r = red ? red->valueint : 0;
uint8_t g = green ? green->valueint : 0;
uint8_t b = blue ? blue->valueint : 0;
uint8_t bri = brightness ? brightness->valueint : 255;
WS2812_SetColor(r, g, b, bri);
emMCP_ResponseValue(emMCP_CTRL_OK);
}
// Tool callback - query
static void emMCP_GetWS2812Handler(void *arg)
{
emMCP_ResponseValue(emMCP_CTRL_OK);
}
// Register in main()
emMCP_tool_t ws2812;
ws2812.name = "ws2812";
ws2812.description = "Control WS2812 LED strip color and brightness";
ws2812.inputSchema.properties[0].name = "red";
ws2812.inputSchema.properties[0].description = "Red value 0-255, null for query";
ws2812.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
ws2812.inputSchema.properties[1].name = "green";
ws2812.inputSchema.properties[1].description = "Green value 0-255, null for query";
ws2812.inputSchema.properties[1].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
ws2812.inputSchema.properties[2].name = "blue";
ws2812.inputSchema.properties[2].description = "Blue value 0-255, null for query";
ws2812.inputSchema.properties[2].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
ws2812.inputSchema.properties[3].name = "brightness";
ws2812.inputSchema.properties[3].description = "Brightness 0-255, null for query";
ws2812.inputSchema.properties[3].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
ws2812.setRequestHandler = emMCP_SetWS2812Handler;
ws2812.checkRequestHandler = emMCP_GetWS2812Handler;
emMCP_AddToolToToolList(&ws2812);
emMCP_RegistrationTools();Voice Commands
"Set the LED strip to red" "Set brightness to 50%" "Turn off the LED strip"
3. SHT30 Temperature & Humidity Sensor
Wiring
SHT30 shares the GPIO bit-banged I²C bus (PB6(SDA) / PB7(SCL)) with the PD decoy, using separate device addresses:
| Device | I²C Address |
|---|---|
| SHT30 Temp/Humidity | 0x44 |
| CH224K PD Decoy | 0x40 |
OLED uses SPI (dedicated chip select) and does not share the I²C bus.
MCP Tool Registration
// Tool callback - control (read-only sensor, returns error on control attempt)
static void emMCP_SetSHT30Handler(void *arg)
{
emMCP_ResponseValue(emMCP_CTRL_ERROR);
}
// Tool callback - query
static void emMCP_GetSHT30Handler(void *arg)
{
float temp = 0, humi = 0;
SHT30_ReadData(&temp, &humi);
cJSON *result = cJSON_CreateObject();
cJSON_AddNumberToObject(result, "temperature", temp);
cJSON_AddNumberToObject(result, "humidity", humi);
cJSON_AddStringToObject(result, "unit_temp", "°C");
cJSON_AddStringToObject(result, "unit_humi", "%");
char *json_str = cJSON_PrintUnformatted(result);
emMCP_ResponseValue(json_str);
cJSON_Delete(result);
cJSON_free(json_str);
}
// Register in main()
emMCP_tool_t sht30;
sht30.name = "sht30";
sht30.description = "Get ambient temperature and humidity";
sht30.inputSchema.properties[0].name = "temperature";
sht30.inputSchema.properties[0].description = "Temperature in °C, null for control";
sht30.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
sht30.inputSchema.properties[1].name = "humidity";
sht30.inputSchema.properties[1].description = "Humidity in %, null for control";
sht30.inputSchema.properties[1].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
sht30.setRequestHandler = emMCP_SetSHT30Handler;
sht30.checkRequestHandler = emMCP_GetSHT30Handler;
emMCP_AddToolToToolList(&sht30);
emMCP_RegistrationTools();Voice Commands
"What's the temperature and humidity?" "Check the current temperature"
4. PD Decoy Power Control
Wiring
PD decoy uses a CH224K chip configured via I²C. Connect a PD charger to the dedicated Type-C port.
⚠️ High Voltage Warning: PD decoy output up to 20V/3A (60W). Verify the rated voltage of the external load before connecting. Do not short-circuit. Do not plug/unplug high-voltage loads while powered. For first use, start testing at 5V.
| PD Decoy | STM32 Pin |
|---|---|
| SDA | PB6 |
| SCL | PB7 |
| I²C Address | 0x40 |
MCP Tool Registration
// Tool callback - control
static void emMCP_SetPDHandler(void *arg)
{
cJSON *param = (cJSON *)arg;
cJSON *voltage = cJSON_GetObjectItem(param, "voltage");
if (voltage) {
CH224K_SetVoltage(voltage->valueint);
emMCP_ResponseValue(emMCP_CTRL_OK);
} else {
emMCP_ResponseValue(emMCP_CTRL_ERROR);
}
}
// Tool callback - query
static void emMCP_GetPDHandler(void *arg)
{
emMCP_ResponseValue(emMCP_CTRL_OK);
}
// Register in main()
emMCP_tool_t pd;
pd.name = "pd_decoy";
pd.description = "Set PD decoy output voltage";
pd.inputSchema.properties[0].name = "voltage";
pd.inputSchema.properties[0].description = "Target voltage options: 5/9/12/15/20, null for query";
pd.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
pd.setRequestHandler = emMCP_SetPDHandler;
pd.checkRequestHandler = emMCP_GetPDHandler;
emMCP_AddToolToToolList(&pd);
emMCP_RegistrationTools();Voice Commands
"Set PD output to 12V" "Boost PD to 20V"
5. OLED Display Control
Wiring
0.96" OLED (SSD1306, 128×64) uses SPI1. Onboard GT20L61S Chinese font chip shares the same SPI bus via separate chip select.
| OLED Pin | STM32 Pin | Description |
|---|---|---|
| OLED_DC | PA1 | Data/Command select |
| OLED_CS1 | PA4 | OLED chip select |
| CS2 | PB0 | GT20L61S font chip select |
| SCLK | PA5 (SPI1_SCK) | SPI clock |
| MISO | PA6 (SPI1_MISO) | SPI data input (font chip read) |
| MOSI | PA7 (SPI1_MOSI) | SPI data output |
MCP Tool Registration
// Tool callback - control
static void emMCP_SetOLEDHandler(void *arg)
{
cJSON *param = (cJSON *)arg;
cJSON *text = cJSON_GetObjectItem(param, "text");
if (text) {
OLED_Clear();
OLED_ShowString(0, 0, text->valuestring);
OLED_Refresh();
emMCP_ResponseValue(emMCP_CTRL_OK);
} else {
emMCP_ResponseValue(emMCP_CTRL_ERROR);
}
}
// Tool callback - query
static void emMCP_GetOLEDHandler(void *arg)
{
emMCP_ResponseValue(emMCP_CTRL_OK);
}
// Register in main()
emMCP_tool_t oled;
oled.name = "oled_display";
oled.description = "Display text on the OLED screen";
oled.inputSchema.properties[0].name = "text";
oled.inputSchema.properties[0].description = "Text to display, null for query";
oled.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_STRING;
oled.setRequestHandler = emMCP_SetOLEDHandler;
oled.checkRequestHandler = emMCP_GetOLEDHandler;
emMCP_AddToolToToolList(&oled);
emMCP_RegistrationTools();Voice Commands
"Display 'Hello' on the OLED" "Show the current temperature on the screen"
6. Full Example: Register All Peripherals
void RegisterAllTools(void)
{
// LED
emMCP_AddToolToToolList(&led);
// Relay
emMCP_AddToolToToolList(&relay);
// WS2812
emMCP_AddToolToToolList(&ws2812);
// Temperature/Humidity
emMCP_AddToolToToolList(&sht30);
// PD Decoy
emMCP_AddToolToToolList(&pd);
// OLED Display
emMCP_AddToolToToolList(&oled);
// Register all tools to XiaoZhi AI at once
emMCP_RegistrationTools();
}After registering all tools, the single call to emMCP_RegistrationTools() syncs all tool definitions to the AI platform. The AI can then recognize voice commands, match them to the appropriate tool, and send an mcp_set or mcp_check command over UART.
Verification
- Compile check: Ensure the project compiles without errors
- Serial log: After flashing, connect the debug serial port (USART1, 1500000 baud). Say "Hello XiaoZhi" to the module. You should see
[DEBUG] emMCP_EventCallback: event:8in the log - Tool sync: When
tool_listtransmission success appears in the serial log, the tools have been registered with the AI platform - Functional test: Speak the corresponding voice commands from the examples above and observe the peripheral response

