Skip to content

1. 开发环境搭建

九章 MCP 验证板使用 STM32F103CBT6 作为主控 MCU,通过 emMCP 库与 Ai-WV01-32S 模组进行 MCP 协议通信。

所需工具

工具用途下载
VSCode代码编辑与编译官网下载
STM32CubeMXSTM32 外设配置与工程生成官网下载
ARM GCC 工具链交叉编译 STM32 固件ARM 官方下载
CMake构建系统(例程使用)官网下载
ST-Link 调试器程序烧录与调试-
BLDevCubeAi-WV01-32S 固件烧录点击下载

参考视频


1.1 安装 ARM GCC 工具链

ARM GCC(gcc-arm-none-eabi)是编译 STM32 固件的交叉编译器,支持从 Cortex-M0 到 Cortex-M7 全系列内核。

安装方式(任选其一):

方式说明命令 / 链接
ARM 官方(推荐)下载压缩包解压后配置 PATH下载地址
xPack(Windows)包管理器安装,自动配置环境xpm install --global @xpack-dev-tools/arm-none-eabi-gcc@latest
MSYS2(Windows)通过 pacman 安装pacman -S mingw-w64-x86_64-arm-none-eabi-gcc
apt(Ubuntu/Debian)通过系统包管理器安装sudo apt install gcc-arm-none-eabi

验证安装:

bash
arm-none-eabi-gcc --version

预期输出示例:

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 插件:

在 VSCode 扩展市场中搜索并安装以下插件:

  • Cortex-Debug — 支持 ST-Link 调试,提供变量监视、断点、寄存器查看等功能
  • CMake — CMake 语法高亮与集成构建
  • C/C++ — 代码补全、跳转、语法检查

1.2 STM32CubeMX 工程配置

九章验证板例程已提供完整的 .ioc 配置文件,无需从头创建工程。直接打开即可查看或调整外设配置。

操作步骤:

  1. 启动 STM32CubeMX

  2. 点击 File > Open Project,选择例程目录下的 .ioc 文件:

    emMCP/example/9Mod_MCPBoard/9Mod_MCPBoard.ioc
  3. 查看关键芯片配置(已预设,无需修改):

    参数
    芯片型号STM32F103CBT6
    HSE(外部晶振)8 MHz
    SYSCLK(系统时钟)72 MHz
    APB1 时钟36 MHz
    APB2 时钟72 MHz
  4. 各外设配置总览(例程已预设):

    外设引脚 / 接口通信参数用途
    USART1PA9(TX) / PA10(RX)115200-8N1调试日志输出
    USART2PA2(RX) / PA3(TX)115200-8N1AI 模组 MCP 通信
    USART3PB10(TX) / PB11(RX)9600-8N1红外模块控制
    I2C1PB6(SCL) / PB7(SDA)400kHzOLED、SHT30、PD 诱骗(共享总线)
    GPIO - PA8PA8 (输入)-雷达模块状态检测
    GPIO - PB4PB4 (输入)-用户按键 1
    GPIO - PB8PB8 (输入)-用户按键 2
    GPIO - PB5PB5 (输出)-继电器控制
    GPIO - PA11PA11 (输出)-WS2812 灯条
    GPIO - PC13PC13 (输出)-板载 LED
    SWDPA13(SWDIO) / PA14(SWCLK)-调试与烧录接口
  5. 如需调整外设参数(如修改波特率),在 Pinout & Configuration 视图中修改后,点击 Project > Generate Code 重新生成代码。注意保持"保留用户代码段"(Keep User Code)选项开启,避免覆盖已有的业务逻辑。

提示: 如果只是开发应用层逻辑(MCP 工具),无需修改 CubeMX 配置,直接跳转到 §1.3 编译即可。


1.3 CMake 构建系统

例程使用 CMake 作为构建系统(而非传统 Makefile)。CMake 会根据 CMakeLists.txt 自动生成编译脚本,调用 ARM GCC 完成交叉编译。

安装 CMake:

  • VSCode 用户: CMake 插件已内置 CMake 工具,无需额外安装

  • 独立安装:CMake 官网 下载安装包,或使用包管理器:

    bash
    # Ubuntu / Debian
    sudo apt install cmake
    
    # macOS (Homebrew)
    brew install cmake
    
    # Windows (MSYS2)
    pacman -S mingw-w64-x86_64-cmake
  • 验证安装:

    bash
    cmake --version

方式一:终端命令编译

bash
# 进入例程目录
cd emMCP/example/9Mod_MCPBoard

# 创建构建目录
mkdir -p build && cd build

# 配置 CMake(指定 ARM GCC 工具链)
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/gcc-arm-none-eabi.cmake

# 编译
make -j$(nproc)

方式二:VSCode CMake 插件一键编译

  1. 在 VSCode 中打开例程目录:File > Open Folder > emMCP/example/9Mod_MCPBoard
  2. F1Ctrl+Shift+P,输入 CMake: Configure 并回车
  3. 选择编译器为 ARM GCC (arm-none-eabi)
  4. 点击底部状态栏的 Build 按钮(或按 F7

CMakeLists.txt 关键说明:

配置项说明
project(9Mod_MCPBoard C ASM)工程名称,支持 C 和汇编源文件
CMAKE_TOOLCHAIN_FILE指定 ARM GCC 交叉编译工具链文件
target_compile_definitions编译宏定义(如 emMCP 移植宏)
add_executable添加目标可执行文件,链接所有源文件
TARGET_LINK_DIRECTORIES链接脚本目录(.ld 文件)
TARGET_LINK_OPTIONS链接选项(如 -T STM32F103CBTx_FLASH.ld

1.4 编译验证

按照 §1.3 完成编译后,进行以下验证:

检查编译输出:

[100%] Built target 9Mod_MCPBoard.elf

确认生成文件:

bash
ls -la build/

应包含以下关键文件:

文件说明
9Mod_MCPBoard.elfELF 格式可执行文件(含调试信息)
9Mod_MCPBoard.hexIntel HEX 格式(用于烧录)
9Mod_MCPBoard.bin纯二进制格式(用于烧录)
9Mod_MCPBoard.map链接映射文件(查看内存分布)

常见错误排查:

错误原因解决方法
arm-none-eabi-gcc: command not foundARM GCC 未安装或未加入 PATH检查 §1.1 安装步骤
CMake Error: Could not find CMAKE_TOOLCHAIN_FILE工具链文件路径错误确认在例程根目录执行 cmake ..
undefined reference to HAL_xxxSTM32 HAL 库未正确包含确认已用 STM32CubeMX 重新生成代码
No such file or directory: STM32F103CBTx_FLASH.ld链接脚本路径错误检查 cmake/ 目录下是否存在 .ld 文件

预期结果: clean build 应无错误(0 errors, 0 warnings 为最佳),并在 build/ 目录下生成 .hex.bin 固件文件,可用于下一步烧录验证。


2. 获取源码

建议使用 Git 克隆 emMCP 仓库:

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

目录结构

emMCP
├── example/                          # 示例工程
│   ├── 9Mod_MCPBoard/                # 九章验证板专用例程 ⭐
│   └── STM32F40xRTOS_XiaoZhiAI/      # STM32F407 FreeRTOS 小智AI示例
├── port/                             # 移植接口层
│   ├── uartPort.h                    # 移植接口头文件(含配置系统)
│   ├── uartPort.c                    # 串口发送/接收(双缓冲)
│   ├── emMCP_port_config_example.h   # 配置示例(STM32 HAL + FreeRTOS)
│   ├── emMCP_port_config_template.h  # 配置模板
│   └── README_PORT.md                # 移植配置说明
└── uart-mcp/                         # emMCP 核心库
    ├── cJSON/
    │   ├── cJSON.h
    │   └── cJSON.c
    ├── emMCP.h                       # emMCP 主头文件
    ├── emMCP.c                       # emMCP 主源文件
    └── emMCPLOG.h                    # 日志头文件

提示

九章验证板专用例程位于 example/9Mod_MCPBoard/,配置了所有板载外设的 MCP 工具示例,推荐直接使用该例程作为开发起点。


3. 硬件连接

AI 模组与 MCU 通信

Ai-WV01-32S 模组通过 UART 与 STM32 通信,引脚连接如下:

STM32 引脚Ai-WV01-32S 引脚功能
PA2 (USART2_RX)TXMCP 协议数据接收
PA3 (USART2_TX)RXMCP 协议数据发送

注意

通过短路帽连接,确保 USART2 的 TX/RX 与模组正确对接。波特率默认 115200

ST-Link 引脚STM32 引脚
SWDIOPA13
SWCLKPA14
GNDGND
3.3VVDD

烧录前确认 BOOT0 用跳线帽拉低(接 GND)。


4. 移植 emMCP

详细步骤参考 移植到MCU。针对九章验证板的关键配置如下:

v1.0.1 配置系统变更

port/port.h 已移除。平台相关宏定义(延时、内存管理、串口发送)迁移到 uartPort.h,采用条件编译 + 用户配置的设计。同时新增双缓冲接收机制,提升串口数据稳定性。

4.1 配置移植宏

emMCP v1.0.1 提供三种方式配置平台相关宏:

方式 1:直接定义宏(简单项目) 在包含 uartPort.h 之前定义:

c
#define emMCP_printf    log_printf          // 打印函数
#define emMCP_malloc    pvPortMalloc        // 内存分配
#define emMCP_free      vPortFree           // 内存释放
#define emMCP_delay     osDelay             // 延时函数
#define emMCP_uart_send HAL_UART_Transmit   // 串口发送(新!)

#include "uartPort.h"

方式 2:创建配置文件(推荐) 在项目目录创建 emMCP_port_config.huartPort.h 会自动通过 __has_include 检测并包含。九章验证板可直接参考 port/emMCP_port_config_example.h

方式 3:CMake 编译定义

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 实现串口收发

uartPortSendData() 已内置 emMCP_uart_send(data, len) 宏调用。确保宏正确定义即可,不必修改函数体:

c
// 方式 A:通过宏配置(推荐)
#define emMCP_uart_send HAL_UART_Transmit

// 方式 B:直接写具体实现(可选)
int uartPortSendData(char *data, int len)
{
    if (data == NULL || len <= 0) return -1;
    return HAL_UART_Transmit(&huart2, (uint8_t *)data, len, 100);
}

在 DMA 接收回调中调用 uartPortRecvData()——新版会自动使用双缓冲存储数据,避免在中断中动态分配内存:

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);  // 自动存入双缓冲
        __HAL_DMA_ENABLE_IT(&hdma_usart2_rx, DMA_IT_TC);
    }
}

在主循环或任务中通过以下 API 获取数据(线程安全):

c
char *data = uartPortGetRxData();  // 获取收到的数据
if (data != NULL) {
    // 处理接收到的 JSON/MCP 数据...
    uartPortClearRxData();         // 标记已处理,允许中断写入新数据
}

4.3 新增辅助 API

API用途
uartPortGetRxData()从双缓冲获取数据(线程安全,NULL 表示无新数据)
uartPortClearRxData()标记数据已处理,允许中断接收新数据
emMCP_UpdateUartRecv(bool isRecv)更新串口接收状态
emMCP_CheckUartSendStatus()查询串口发送是否完成

5. 初始化与主循环

c
static emMCP_t emMCP_dev;
static uint8_t uart_rx_buf[512]; // 接收缓冲区

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART2_UART_Init();       // AI 模组通信串口
    MX_USART1_UART_Init();       // 调试日志串口

    // 初始化外设
    OLED_Init();                 // 0.96寸 OLED
    WS2812_Init();               // WS2812 灯条
    Relay_Init();                // 继电器
    SHT30_Init();                // 温湿度传感器

    // 初始化 emMCP
    emMCP_Init(&emMCP_dev);

    // 设置串口数据缓冲区
    uartPortSetDataBuf((char *)uart_rx_buf);

    while (1)
    {
        emMCP_TickHandle(10);    // emMCP 状态机处理
        userTaskHandler();       // 用户任务处理
    }
}

关键

emMCP_TickHandle() 必须在主循环中高频调用,以维持 MCP 协议通信的实时性。


6. 注册 MCP 工具

MCP 工具是 AI 控制硬件的核心。每个工具包含名称、描述、参数定义和回调函数。

6.1 工具结构体

c
/**
 * @brief MCP 服务器工具结构体
 *
 */
typedef struct emMCP_tool
{
    char *name;                          // 工具名称(AI 调用时使用)
    char *description;                   // 工具功能描述
    void (*setRequestHandler)(void *);   // 控制回调函数
    void (*checkRequestHandler)(void *); // 查询回调函数
    inputSchema_t inputSchema;           // 输入参数描述
    struct emMCP_tool *next;             // 下一个工具
} emMCP_tool_t;

其中 inputSchema_t 用于描述工具的属性和控制方法:

c
typedef struct
{
    properties_t properties[MCP_SERVER_TOOL_PROPERTIES_NUM]; // 属性
    methods_t methods[MCP_SERVER_TOOL_METHODS_NUM];          // 方法
} inputSchema_t;
字段说明
properties可查询的属性(AI 能获取这些属性的当前值)
methods可调用的控制方法(AI 能通过这些方法控制设备)

properties_t 描述单个属性:

c
typedef struct
{
    char *name;                         // 属性名称(如 "enable")
    char *description;                  // 属性描述
    mcp_server_tool_type_t type;        // 属性类型
} properties_t;

methods_t 描述单个控制方法:

c
typedef struct
{
    char *name;                                               // 方法名称
    char *description;                                        // 方法描述
    parameters_t parameters[MCP_SERVER_TOOL_METHODS_PARAMETERS_NUM]; // 方法参数
} methods_t;

parameters_t 描述方法的输入参数:

c
typedef struct
{
    char *name;                         // 参数名称
    char *description;                  // 参数描述
    mcp_server_tool_type_t type;        // 参数类型
} parameters_t;


### 6.2 工具回调函数

工具的回调函数是工具的核心,当 AI 下发 MCP 指令时,会调用对应的回调函数。回调函数接收一个 `void *arg` 参数,需要将其转换为 `cJSON *` 指针来解析指令内容。

**控制回调函数**:当 AI 发送控制指令(如"打开继电器")时调用:

```c
static void relay_set_handler(void *arg)
{
    cJSON *param = (cJSON *)arg;                          // 接收 cJSON 指针
    cJSON *state = cJSON_GetObjectItem(param, "enable");  // 获取指令参数

    if (state && cJSON_IsTrue(state)) {
        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);   // 返回控制成功
}

查询回调函数:当 AI 发送查询指令(如"继电器是否打开")时调用:

c
static void relay_get_handler(void *arg)
{
    // 构造查询结果
    uint8_t state = HAL_GPIO_ReadPin(RELAY_GPIO_Port, RELAY_Pin);
    cJSON *result = cJSON_CreateObject();
    cJSON_AddBoolToObject(result, "enable", state == GPIO_PIN_SET);
    emMCP_ResponseValue(result);          // 返回查询结果(自动释放)
}
要点说明
argcJSON * 指针,包含 AI 下发的指令参数
emMCP_ResponseValue(emMCP_CTRL_OK)返回控制成功
emMCP_ResponseValue(emMCP_CTRL_ERROR)返回控制失败
emMCP_ResponseValue(cJSON*)返回查询结果(JSON 对象)

6.3 完整注册示例:继电器控制

c
// 1. 创建工具变量
emMCP_tool_t relay_tool;

// 2. 控制回调函数
static void relay_set_handler(void *arg)
{
    cJSON *param = (cJSON *)arg;
    cJSON *state = cJSON_GetObjectItem(param, "enable");

    if (state && cJSON_IsTrue(state)) {
        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);
}

// 3. 查询回调函数
static void relay_get_handler(void *arg)
{
    uint8_t state = HAL_GPIO_ReadPin(RELAY_GPIO_Port, RELAY_Pin);
    cJSON *result = cJSON_CreateObject();
    cJSON_AddBoolToObject(result, "enable", state == GPIO_PIN_SET);
    emMCP_ResponseValue(result);
}

// 4. 在 main 中配置并注册
int main(void)
{
    // ... 初始化代码 ...

    emMCP_Init(&emMCP_dev);

    // 配置工具
    relay_tool.name = "继电器";
    relay_tool.description = "控制继电器的开关";
    relay_tool.inputSchema.properties[0].name = "enable";
    relay_tool.inputSchema.properties[0].description = "是否打开继电器,true表示打开,false表示关闭,查询时为null";
    relay_tool.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_BOOLEAN;
    relay_tool.setRequestHandler = relay_set_handler;
    relay_tool.checkRequestHandler = relay_get_handler;

    // 添加工具到工具列表
    emMCP_AddToolToToolList(&relay_tool);

    // 注册所有工具到小安 AI(确保所有工具已添加完毕)
    emMCP_RegistrationTools();

    while (1)
    {
        emMCP_TickHandle(10);
    }
}

注册流程

  1. 创建 emMCP_tool_t 变量并配置名称、描述、属性、回调
  2. 调用 emMCP_AddToolToToolList() 将每个工具加入工具列表
  3. 所有工具添加完毕后,调用 emMCP_RegistrationTools() 统一注册到小安 AI

7. 九章验证板外设映射速查

以下是各外设对应的 GPIO 和推荐的 MCP 工具名:

外设GPIO / 接口通信方式推荐 MCP 工具名
AI 模组PA2(RX) / PA3(TX)USART2- (emMCP 内部使用)
继电器PB5GPIO 输出relay_control
WS2812 灯条PA11单线串行led_control
SHT30 温湿度PB6(SCL) / PB7(SDA)I²Cget_temperature, get_humidity
OLED 显示屏PB6(SCL) / PB7(SDA)I²Coled_display
雷达模块PA8GPIO 输入get_radar_status
红外控制PB10(TX) / PB11(RX)USART3ir_send
用户按键PB4, PB8GPIO 输入- (事件触发)
PD 诱骗PB6(SCL) / PB7(SDA)I²Cpd_set_voltage
调试串口PA9(TX) / PA10(RX)USART1- (日志输出)

I²C 总线共享

OLED、SHT30、PD 诱骗三者共用 PB6(SCL) / PB7(SDA) I²C 总线,通过不同设备地址区分(OLED: 0x3C, SHT30: 0x44, CH224K: 0x48)。


8. 完整示例:温湿度读取工具

c
// 工具变量
emMCP_tool_t sensor_tool;

// 控制回调(温湿度以查询为主,这里做演示)
static void sensor_set_handler(void *arg)
{
    cJSON *param = (cJSON *)arg;
    cJSON *type = cJSON_GetObjectItem(param, "type");

    if (type != NULL) {
        // 根据 type 参数执行对应操作...
        emMCP_ResponseValue(emMCP_CTRL_OK);
    } else {
        emMCP_ResponseValue(emMCP_CTRL_ERROR);
    }
}

// 查询回调
static void sensor_get_handler(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", "%");
    emMCP_ResponseValue(result);
}

// 在 main 中注册
int main(void)
{
    // ... 初始化代码 ...

    emMCP_Init(&emMCP_dev);

    sensor_tool.name = "温湿度传感器";
    sensor_tool.description = "获取环境温湿度数据";
    sensor_tool.inputSchema.properties[0].name = "temperature";
    sensor_tool.inputSchema.properties[0].description = "当前温度值";
    sensor_tool.inputSchema.properties[0].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
    sensor_tool.inputSchema.properties[1].name = "humidity";
    sensor_tool.inputSchema.properties[1].description = "当前湿度值";
    sensor_tool.inputSchema.properties[1].type = MCP_SERVER_TOOL_TYPE_NUMBLE;
    sensor_tool.setRequestHandler = sensor_set_handler;
    sensor_tool.checkRequestHandler = sensor_get_handler;

    emMCP_AddToolToToolList(&sensor_tool);
    emMCP_RegistrationTools();

    while (1)
    {
        emMCP_TickHandle(10);
    }
}

注册后,用户可通过小安语音说「查一下现在的温湿度」即可触发 AI 调用此工具,AI 会自动语音播报结果。


9. 事件回调处理

emMCP 提供了丰富的事件通知,重新定义 emMCP_EventCallback 即可监听:

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:
        // 模组被唤醒,可以做 LED 闪烁等交互
        OLED_ShowString("聆听中...");
        break;
    case emMCP_EVENT_AI_SLEEP:
        OLED_ShowString("待机");
        break;
    case emMCP_EVENT_AI_MCP_CMD:
        // 收到 MCP 命令,显示调用日志
        printf("[MCP] %s\n", str);
        break;
    case emMCP_EVENT_AI_MCP_Text:
        // AI 回复的字幕文本,可显示在 OLED 上
        OLED_ShowString(str);
        break;
    case emMCP_EVENT_AI_WIFI_CONNECT:
        OLED_ShowString("WiFi OK");
        break;
    case emMCP_EVENT_AI_WIFI_DISCONNECT:
        OLED_ShowString("WiFi Lost");
        break;
    default:
        break;
    }
}

完整事件列表请参考 emMCP 事件列表


10. 调试方法

10.1 TTL 串口日志

九章验证板通过 CH340C 芯片引出 USART1(PA9/PA10)作为调试串口,波特率 115200

使用串口工具(如 PuTTY、MobaXterm、或 Ai-Thinker 串口助手)连接 Type-C 接口即可查看日志。

c
// 重定向 printf 到 USART1
int _write(int fd, char *ptr, int len)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 100);
    return len;
}

10.2 OLED 显示调试

在关键节点调用 OLED 显示函数,可视化程序状态:

c
OLED_ShowString(0, 0, "MCP Init OK");     // 初始化提示
OLED_ShowString(0, 2, "Tool: 5 reg");     // 已注册工具数
OLED_ShowString(0, 4, "WiFi: connected"); // 网络状态

10.3 常见问题排查

现象可能原因解决方法
emMCP 收不到数据串口引脚/波特率不匹配确认 PA2/PA3 短路帽连接正确,波特率 115200
工具注册后 AI 无法调用工具名含特殊字符或未完成注册检查工具名使用中文/字母,确认已调用 emMCP_AddToolToToolList() 并执行 emMCP_RegistrationTools()
MCP 命令超时回调函数执行过久避免在回调中使用阻塞延时,复杂操作用任务/队列异步处理
Ai-WV01-32S 无响应模组未配网或固件版本不对确认已配网成功,使用 V3.4 固件
OLED 不显示I²C 地址冲突确认 OLED 地址 0x3C,与 SHT30(0x44) 不冲突
编译报错 undefined reference缺少源文件确认 port/uart-mcp/ 下所有 .c 文件已加入编译

11. 更多资料

Released under the MIT License.