在嵌入式开发领域,很多工程师习惯使用现成的IDE(如Keil、IAR等)来开发STM32项目。这些商业IDE确实提供了开箱即用的体验,但它们存在几个关键问题:
我在实际项目中发现,使用手工编写的构建系统可以带来以下优势:
一个完整的STM32构建系统需要以下工具:
交叉编译器:arm-none-eabi-gcc(建议版本9-10)
sudo apt install gcc-arm-none-eabiarm-none-eabi-gcc --version调试工具:
构建系统:
辅助工具:
提示:建议使用Ubuntu 20.04/22.04 LTS作为开发环境,避免在新版本中出现工具链兼容性问题。
不同STM32系列需要对应的硬件支持包(HAL/LL库)。以STM32F4为例:
bash复制# 下载STM32CubeF4软件包
wget https://www.st.com/resource/en/firmware/stm32cubef4.zip
unzip stm32cubef4.zip -d ~/STM32Cube/Repository
关键目录结构说明:
code复制STM32Cube/Repository/
├── Drivers/
│ ├── CMSIS/ # Cortex核心支持
│ └── STM32F4xx_HAL_Driver/ # HAL库源码
└── Projects/
└── STM32CubeExamples/ # 官方示例代码
创建一个典型的STM32项目目录结构:
code复制stm32-project/
├── cmake/
│ ├── toolchain-arm-none-eabi.cmake # 交叉编译工具链配置
│ └── stm32f4.cmake # 芯片特定配置
├── src/
│ ├── main.cpp
│ ├── stm32f4xx_it.c
│ └── system_stm32f4xx.c
├── include/
│ └── main.h
└── CMakeLists.txt # 主构建文件
CMakeLists.txt基础内容:
cmake复制cmake_minimum_required(VERSION 3.15)
project(STM32F4_Project LANGUAGES C CXX ASM)
# 包含芯片配置
include(cmake/stm32f4.cmake)
# 添加可执行文件
add_executable(${PROJECT_NAME}.elf
src/main.cpp
src/system_stm32f4xx.c
src/stm32f4xx_it.c
)
# 链接标准库和启动文件
target_link_libraries(${PROJECT_NAME}.elf
-T${LINKER_SCRIPT}
-Wl,--start-group
-lc -lm -lnosys
-Wl,--end-group
)
# 生成hex和bin文件
add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -Obinary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin
COMMAND ${CMAKE_OBJCOPY} -Oihex ${PROJECT_NAME}.elf ${PROJECT_NAME}.hex
)
1. 内存布局配置(链接脚本)
典型的STM32F407VG链接脚本(STM32F407VGTx_FLASH.ld)关键部分:
code复制MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 1024K
}
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
/* 其他段... */
} >FLASH
}
2. 启动文件选择
根据芯片型号选择正确的启动文件(startup_stm32f407xx.s),关键差异:
3. 编译选项优化
推荐的基础编译选项:
cmake复制target_compile_options(${PROJECT_NAME}.elf PRIVATE
-mcpu=cortex-m4
-mthumb
-mfpu=fpv4-sp-d16
-mfloat-abi=hard
-ffunction-sections
-fdata-sections
-Wall
-Og # 优化级别
)
对于复杂项目,可以使用CMake的add_subdirectory()功能实现模块化:
code复制project-root/
├── CMakeLists.txt # 主配置
├── app/
│ ├── CMakeLists.txt # 应用层
│ └── src/
├── drivers/
│ ├── CMakeLists.txt # 驱动层
│ └── src/
└── middleware/
├── CMakeLists.txt # 中间件
└── src/
主CMakeLists.txt示例:
cmake复制# 添加子项目
add_subdirectory(drivers)
add_subdirectory(middleware)
add_subdirectory(app)
# 应用层链接其他模块
target_link_libraries(app.elf
PRIVATE
drivers_lib
middleware_lib
)
在CMake中集成单元测试(使用CppUTest框架示例):
cmake复制# 查找测试框架
find_package(CppUTest REQUIRED)
# 添加测试可执行文件
add_executable(test_suite
tests/test_main.cpp
tests/gpio_test.cpp
src/gpio_driver.c
)
target_link_libraries(test_suite
PRIVATE
CppUTest::CppUTest
CppUTest::CppUTestExt
)
# 添加测试用例
add_test(NAME gpio_test COMMAND test_suite)
通过CMake自动生成版本信息:
cmake复制# 获取Git提交哈希
execute_process(
COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# 生成版本头文件
configure_file(
${CMAKE_SOURCE_DIR}/cmake/version.h.in
${CMAKE_BINARY_DIR}/include/version.h
)
version.h.in模板:
c复制#pragma once
#define FIRMWARE_VERSION "1.0.0"
#define BUILD_TIMESTAMP "@TIMESTAMP@"
#define GIT_COMMIT_HASH "@GIT_HASH@"
问题1:未定义的引用(undefined reference)
-Wl,--print-map查看链接映射问题2:内存区域溢出
.map文件中的段大小-ffunction-sections -fdata-sections配合-Wl,--gc-sections现象:代码下载后不运行
bash复制openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
# 在另一个终端
telnet localhost 4444
> reset halt
> reg pc
关键函数定位:
bash复制arm-none-eabi-objdump -d ${PROJECT_NAME}.elf | less
RAM使用分析:
bash复制arm-none-eabi-size -A ${PROJECT_NAME}.elf
编译优化建议:
-O2或-O3__attribute__((section(".fast_code")))定位关键函数到RAM-flto.vscode/c_cpp_properties.json示例:
json复制{
"configurations": [
{
"name": "STM32",
"includePath": [
"${workspaceFolder}/include",
"${workspaceFolder}/Drivers/CMSIS/Include",
"${workspaceFolder}/Drivers/STM32F4xx_HAL_Driver/Inc"
],
"defines": [
"STM32F407xx",
"USE_HAL_DRIVER"
],
"compilerPath": "/usr/bin/arm-none-eabi-gcc",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-arm"
}
]
}
build.sh示例:
bash复制#!/bin/bash
BUILD_DIR="build"
PROJECT="STM32F4_Project"
# 清理构建目录
rm -rf ${BUILD_DIR}
mkdir -p ${BUILD_DIR}
cd ${BUILD_DIR}
# 运行CMake
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain-arm-none-eabi.cmake \
-DCMAKE_BUILD_TYPE=Debug \
..
# 编译并生成hex文件
make -j$(nproc) && \
arm-none-eabi-objcopy -O ihex ${PROJECT}.elf ${PROJECT}.hex
# 输出大小信息
arm-none-eabi-size ${PROJECT}.elf
.vscode/launch.json配置:
json复制{
"version": "0.2.0",
"configurations": [
{
"name": "Debug STM32",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/${PROJECT}.elf",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"miDebuggerPath": "arm-none-eabi-gdb",
"miDebuggerServerAddress": "localhost:3333",
"setupCommands": [
{
"text": "target extended-remote localhost:3333"
},
{
"text": "monitor reset halt"
},
{
"text": "load"
},
{
"text": "monitor reset init"
}
]
}
]
}
在资源受限环境中使用C++的推荐实践:
禁用异常和RTTI:
cmake复制target_compile_options(${PROJECT_NAME}.elf PRIVATE
-fno-exceptions
-fno-rtti
)
内存管理策略:
cpp复制void* operator new(size_t size) {
return mem_pool_alloc(size);
}
模板元编程应用:
cpp复制template<GPIO_TypeDef* Port, uint16_t Pin>
class Gpio {
public:
static void set() {
Port->BSRR = Pin;
}
// ...
};
using Led = Gpio<GPIOC, GPIO_PIN_13>;
中断处理优化:
__attribute__((interrupt))确保正确的中断上下文保存关键段保护:
cpp复制class CriticalSection {
public:
CriticalSection() { __disable_irq(); }
~CriticalSection() { __enable_irq(); }
};
内存屏障使用:
cpp复制#define barrier() __asm__ volatile("":::"memory")
以GPIO驱动为例展示模块化开发:
drivers/gpio.hpp:
cpp复制#pragma once
#include <cstdint>
class Gpio {
public:
enum class Mode { Input, Output, Alternate, Analog };
enum class Pull { None, Up, Down };
Gpio(GPIO_TypeDef* port, uint16_t pin);
void set_mode(Mode mode, Pull pull = Pull::None);
void write(bool state);
bool read() const;
private:
GPIO_TypeDef* port_;
uint16_t pin_;
};
drivers/gpio.cpp:
cpp复制#include "gpio.hpp"
#include "stm32f4xx_hal.h"
Gpio::Gpio(GPIO_TypeDef* port, uint16_t pin)
: port_(port), pin_(pin) {}
void Gpio::set_mode(Mode mode, Pull pull) {
GPIO_InitTypeDef init = {0};
init.Pin = pin_;
init.Speed = GPIO_SPEED_FREQ_HIGH;
switch(mode) {
case Mode::Input: init.Mode = GPIO_MODE_INPUT; break;
// 其他模式处理...
}
HAL_GPIO_Init(port_, &init);
}
src/main.cpp中的典型初始化序列:
cpp复制extern "C" void SystemInit(); // 来自启动文件
int main() {
// 1. 硬件初始化
HAL_Init();
SystemInit();
// 2. 时钟配置
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_USART2_CLK_ENABLE();
// 3. 外设初始化
Gpio led(GPIOA, GPIO_PIN_5);
led.set_mode(Gpio::Mode::Output);
// 4. 主循环
while(true) {
led.write(true);
HAL_Delay(500);
led.write(false);
HAL_Delay(500);
}
}
集成DFU(Device Firmware Update)支持:
code复制MEMORY {
BOOTLOADER (rx) : ORIGIN = 0x08000000, LENGTH = 16K
APP (rx) : ORIGIN = 0x08004000, LENGTH = 1008K
}
cpp复制void jump_to_bootloader() {
void (*bootloader)(void) = (void (*)(void))(*((uint32_t*)0x1FFF0000));
__disable_irq();
SysTick->CTRL = 0;
HAL_RCC_DeInit();
HAL_DeInit();
__set_MSP(*(__IO uint32_t*)0x08000000);
bootloader();
}
bash复制sudo apt install ccache
cmake -DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache ..
cmake复制target_precompile_headers(${PROJECT_NAME}.elf PRIVATE
<stdint.h>
<stm32f4xx.h>
"config.h"
)
集成clang-tidy进行代码质量检查:
cmake复制find_program(CLANG_TIDY_EXE "clang-tidy")
if(CLANG_TIDY_EXE)
set(CMAKE_CXX_CLANG_TIDY
${CLANG_TIDY_EXE}
-checks=*
-header-filter=.*
)
endif()
-ffunction-sections -fdata-sections配合链接器垃圾回收:cmake复制target_link_options(${PROJECT_NAME}.elf PRIVATE
-Wl,--gc-sections
)
bash复制arm-none-eabi-nm --print-size --size-sort --radix=d ${PROJECT_NAME}.elf
-Os:优化尺寸(默认推荐)-Oz:更激进的尺寸优化-O2/-O3:性能优化(可能增加体积).github/workflows/build.yml示例:
yaml复制name: STM32 CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y gcc-arm-none-eabi cmake make
- name: Configure and build
run: |
mkdir build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain-arm-none-eabi.cmake ..
make -j$(nproc)
- name: Check size
run: |
cd build
arm-none-eabi-size ${PROJECT_NAME}.elf
扩展CI配置添加测试阶段:
yaml复制- name: Run unit tests
run: |
cd build
ctest --output-on-failure
添加生成发布包步骤:
yaml复制- name: Create release artifact
run: |
cd build
zip ${GITHUB_SHA}.zip ${PROJECT_NAME}.bin ${PROJECT_NAME}.hex
echo "ARTIFACT_PATH=build/${GITHUB_SHA}.zip" >> $GITHUB_ENV
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: firmware
path: ${{ env.ARTIFACT_PATH }}
常用调试命令备忘:
bash复制# 启动OpenOCD
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
# GDB连接后常用命令:
monitor reset halt # 复位并暂停
monitor flash write_image erase firmware.bin 0x08000000
monitor reset run # 复位并运行
使用GDB的perf插件:
bash复制arm-none-eabi-gdb -ex "target remote localhost:3333" \
-ex "monitor reset halt" \
-ex "load" \
-ex "perf record" \
-ex "continue"
# 运行一段时间后中断
Ctrl+C
perf report
bash复制arm-none-eabi-objdump -t ${PROJECT_NAME}.elf | grep -E '__stack_size|__heap_size'
bash复制arm-none-eabi-size -A -x ${PROJECT_NAME}.elf | less
cpp复制void configure_mpu() {
MPU_Region_InitTypeDef mpu;
HAL_MPU_Disable();
// 配置Flash为只读
mpu.Enable = MPU_REGION_ENABLE;
mpu.BaseAddress = 0x08000000;
mpu.Size = MPU_REGION_SIZE_1MB;
mpu.AccessPermission = MPU_REGION_FULL_ACCESS;
mpu.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
mpu.IsCacheable = MPU_ACCESS_CACHEABLE;
mpu.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
mpu.Number = MPU_REGION_NUMBER0;
mpu.TypeExtField = MPU_TEX_LEVEL0;
mpu.SubRegionDisable = 0x00;
mpu.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&mpu);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
cpp复制bool verify_firmware() {
// 从固定地址读取签名
const uint8_t* signature = (uint8_t*)0x08004000;
// 使用硬件加密模块验证
if(HAL_CRYP_Verify(signature, PUBLIC_KEY)) {
return true;
}
return false;
}
cpp复制void jump_to_app() {
typedef void (*app_entry)(void);
uint32_t* app_vector = (uint32_t*)0x08004000;
if(verify_firmware()) {
__disable_irq();
SCB->VTOR = 0x08004000;
__set_MSP(app_vector[0]);
((app_entry)app_vector[1])();
}
}
hal/interface.hpp:
cpp复制#pragma once
class HardwareInterface {
public:
virtual ~HardwareInterface() = default;
virtual void gpio_write(int pin, bool state) = 0;
virtual bool gpio_read(int pin) = 0;
// 其他硬件抽象接口...
};
// 平台特定实现
#ifdef STM32F4
#include "hal/stm32f4_impl.hpp"
#elif defined(LINUX_TEST)
#include "hal/linux_mock.hpp"
#endif
hal/linux_mock.hpp:
cpp复制class LinuxMockHardware : public HardwareInterface {
std::map<int, bool> gpio_states;
public:
void gpio_write(int pin, bool state) override {
gpio_states[pin] = state;
}
bool gpio_read(int pin) override {
return gpio_states[pin];
}
};
cpp复制// 平台检测宏
#if defined(STM32F407xx)
#define PLATFORM_STM32F4
#elif defined(__linux__)
#define PLATFORM_LINUX
#endif
// 外设访问抽象
inline void set_led(bool state) {
#ifdef PLATFORM_STM32F4
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, state);
#elif defined(PLATFORM_LINUX)
std::cout << "LED: " << (state ? "ON" : "OFF") << std::endl;
#endif
}
CMakeLists.txt配置:
cmake复制find_package(Doxygen REQUIRED)
doxygen_add_docs(docs
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/include
COMMENT "Generate documentation"
)
使用git日志自动生成ChangeLog:
cmake复制add_custom_command(OUTPUT ChangeLog.md
COMMAND git log --pretty=format:"- %h %s (%ad)" --date=short > ChangeLog.md
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
在链接阶段注入构建信息:
cmake复制add_custom_command(TARGET ${PROJECT_NAME}.elf PRE_LINK
COMMAND ${CMAKE_COMMAND} -DFIRMWARE_VERSION=${PROJECT_VERSION}
-P ${CMAKE_SOURCE_DIR}/cmake/inject_metadata.cmake
)
inject_metadata.cmake:
cmake复制file(WRITE ${CMAKE_BINARY_DIR}/metadata.c
"const char METADATA[] = \"${FIRMWARE_VERSION}\";\n"
"__attribute__((section(\".metadata\"))) const char* ptr = METADATA;"
)
execute_process(COMMAND ${CMAKE_C_COMPILER}
-c ${CMAKE_BINARY_DIR}/metadata.c
-o ${CMAKE_BINARY_DIR}/metadata.o
)
cmake复制add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin
COMMAND ${CMAKE_SOURCE_DIR}/scripts/add_header.py
-i ${PROJECT_NAME}.bin
-o ${PROJECT_NAME}_v${PROJECT_VERSION}.bin
-v ${PROJECT_VERSION}
)
bash复制# 合并bootloader和应用程序
cat bootloader.bin app.bin > combined.bin
# 添加安全头
secure_header_tool -i combined.bin -o final_image.bin
集成无线更新功能:
cpp复制struct UpdateHeader {
uint32_t magic;
uint32_t version;
uint32_t crc;
uint32_t size;
uint8_t signature[64];
};
cpp复制void handle_ota_update() {
if(verify_update_signature()) {
erase_target_sector();
write_new_firmware();
verify_crc();
set_update_flag();
reset_system();
}
}
添加生产测试入口:
cmake复制option(ENABLE_FACTORY_TEST "Build with factory test mode" OFF)
if(ENABLE_FACTORY_TEST)
target_compile_definitions(${PROJECT_NAME}.elf PRIVATE
FACTORY_TEST=1
)
endif()
测试模式实现:
cpp复制#ifdef FACTORY_TEST
void factory_test_entry() {
test_gpio();
test_adc();
test_comms();
// ...
set_test_result_flag();
}
#endif
FetchContent管理第三方库:cmake复制include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.11.0
)
FetchContent_MakeAvailable(googletest)
cmake复制set(CMAKE_PREFIX_PATH
${CMAKE_SOURCE_DIR}/third_party
${CMAKE_PREFIX_PATH}
)
ccache加速重复构建:bash复制export CCACHE_DIR="${HOME}/.ccache"
export CCACHE_SLOPPINESS="time_macros"
cmake复制target_precompile_headers(${PROJECT_NAME}.elf PRIVATE
<vector>
<string>
"common_defs.h"
)
bash复制# 在项目根目录创建.toolversions文件
arm-none-eabi-gcc=10.3.1
cmake=3.22.1
bash复制#!/bin/bash
# check_environment.sh
ERROR=0
# 检查工具版本
check_tool() {
local tool=$1
local expected=$2
local actual=$(command -v $tool && $tool --version | head -n1)
[[ "$actual" == *"$expected"* ]] || {
echo "ERROR: $tool version mismatch (need $expected)"
ERROR=1
}
}
check_tool arm-none-eabi-gcc "10.3.1"
check_tool cmake "3.22.1"
exit $ERROR
定时器精确延时实现:
cpp复制void delay_us(uint32_t us) {
asm volatile (
"mov r0, %[us] \n"
"1: subs r0, #1 \n"
"bne 1b \n"
: : [us] "r" (us * 16) : "r0"
);
}
DMA传输配置技巧:
cpp复制void configure_dma() {
// 确保缓冲区对齐
alignas(32) uint8_t buffer[1024];
// 启用缓存预取
SCB_EnableDCache();
SCB_EnableICache();
// 配置DMA
hdma.Instance = DMA2_Stream0;
hdma.Init.Channel = DMA_CHANNEL_0;
hdma.Init.MemBurst = DMA_MBURST_INC4;
hdma.Init.PeriphBurst = DMA_PBURST_INC4;
HAL_DMA_Init(&hdma);
}
cpp复制__attribute__((optimize("O3")))
void time_critical_function() {
// ...
}
cmake复制target_compile_options(${PROJECT_NAME}.elf PRIVATE
-flto
)
target_link_options(${PROJECT_NAME}.elf PRIVATE
-flto
)
cpp复制void enter_stop_mode() {
// 配置所有GPIO为模拟输入
for(auto& pin : active_pins) {
pin.set_mode(Gpio::Mode::Analog);
}
// 进入STOP模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 唤醒后重新初始化时钟
SystemClock_Config();
}
动态时钟管理:
cpp复制void enable_peripheral_clock(Peripheral periph) {
static uint32_t ref_counts[PERIPH_COUNT] = {0};
if(ref_counts[periph]++ == 0) {
// 首次启用,打开时钟
switch(periph) {
case PERIPH_USART1: __HAL_RCC_USART1_CLK_ENABLE(); break;
// 其他外设...
}
}
}
void disable_peripheral_clock(Peripheral periph) {
if(--ref_counts[periph] == 0) {
// 最后一个用户,关闭时钟
switch(periph) {
case PERIPH_USART1: __HAL_RCC_USART1_CLK_DISABLE(); break;
// 其他外设...
}
}
}
cmake复制option(LOW_POWER_MODE "Build for low power operation" OFF)
if(LOW_POWER_MODE)
target_compile_definitions(${PROJECT_NAME}.elf PRIVATE
USE_LOW_POWER=1
)
target_compile_options(${PROJECT_NAME}.elf PRIVATE
-Os # 优化尺寸和功耗
)
endif()
CMake集成FreeRTOS:
cmake复制# 添加FreeRTOS源码
add_subdirectory(third_party/FreeRTOS)
# 链接到主项目
target_link_libraries(${PROJECT_NAME}.elf PRIVATE
FreeRTOS::Kernel
)
# 配置FreeRTOS参数
target_compile_definitions(${PROJECT_NAME}.elf PRIVATE
configTOTAL_HEAP_SIZE=4096
configUSE_PREEMPTION=1
)
静态分配任务示例:
cpp复制// 静态分配任务栈和TCB
StaticTask_t task_tcb;
StackType_t task_stack[configMINIMAL_STACK_SIZE];
void task_function(void* arg) {
while(true) {
// 任务逻辑...
}
}
void create_tasks() {
xTaskCreateStatic(
task_function,
"Task1",
configMINIMAL_STACK_SIZE,
nullptr,
tskIDLE_PRIORITY + 1,
task_stack,
&task_tcb
);
}
添加任务统计功能:
cpp复制void enable_task_stats() {
#if (configUSE_TRACE_FACILITY == 1)
// 重置基准计数器
ulTaskResetRunTimeStats();
// 定期打印统计信息
vTaskList(stats_buffer);
printf("Task Stats:\n%s", stats_buffer);
#endif
}
cmake复制# 旧式单文件项目
add