欢迎光临
我们一直在努力

持续集成在嵌入式开发中的实践:GitLab CI与交叉编译——自动化构建、固件生成

文章目录

    • 每日一句正能量
    • 一、前言:嵌入式开发的"构建之痛"
    • 二、GitLab CI 嵌入式流水线全景架构
      • 2.1 流水线阶段(Stage)设计
      • 2.2 GitLab Runner 配置
      • 2.3 自托管 Runner vs 共享 Runner
    • 三、交叉编译工具链:从x86到ARM的桥梁
      • 3.1 ARM GCC 工具链选型
      • 3.2 工具链安装与版本管理
    • 四、Docker 容器化:构建环境的一致性保障
      • 4.1 分层镜像策略
      • 4.2 .gitlab-ci.yml 核心配置
    • 五、固件生成流程:从源码到可烧录文件
      • 5.1 编译与链接
      • 5.2 固件转换与签名脚本
      • 5.3 产物管理策略
    • 六、Pipeline 阶段编排与Job依赖关系
      • 6.1 并行与串行的艺术
      • 6.2 多目标平台并行构建矩阵
    • 七、缓存策略:构建加速的关键
      • 7.1 四级缓存体系
      • 7.2 ccache 深度集成
      • 7.3 Git 子模块加速
    • 八、CMake 交叉编译工程实战
      • 8.1 完整 CMakeLists.txt 示例
      • 8.2 链接器脚本(Linker Script)
    • 九、高级技巧:条件触发、保护分支与回滚
      • 9.1 条件触发规则
      • 9.2 保护分支与部署权限
      • 9.3 固件版本回滚
    • 十、鸿蒙生态(OpenHarmony)中的CI实践
    • 十一、总结与最佳实践清单

在这里插入图片描述

每日一句正能量

精力过度投放在别人身上时,就容易变得敏感、拧巴且内耗。 注意力在哪,能量就流向哪。过度在意别人的言行,就会不停地猜测、比较、担忧,内心反复拉扯。这种内耗比体力劳动更累人。

一、前言:嵌入式开发的"构建之痛"

在嵌入式软件开发中,有一句广为流传的"至理名言":“在我的机器上能编译通过”。这句话背后折射出的是嵌入式构建环境的复杂性与脆弱性。与服务器端开发不同,嵌入式项目面临独特的构建挑战:

  • 交叉编译环境复杂:需要安装特定版本的ARM GCC、OpenOCD、J-Link工具,不同项目可能依赖不同版本的工具链
  • 依赖管理困难:第三方库(FreeRTOS、LwIP、mbedTLS)的源码集成、版本锁定、子模块管理耗时费力
  • 多目标平台并行:同一套业务代码需要编译到STM32F4、STM32H7、ESP32、nRF52等多个芯片平台
  • 固件生成链路长:从C源码到最终可烧录的.bin/.hex文件,需要经过编译、链接、转换、签名等多个步骤
  • 环境不一致导致"构建漂移":开发者的本地环境与CI环境、其他开发者的环境存在差异,导致"能跑"与"能构建"成为两回事
  • 持续集成(Continuous Integration, CI) 正是解决这些痛点的系统性方案。通过将构建流程自动化、环境容器化、产物版本化,团队可以实现"一次配置,处处构建"的目标。本文将深入讲解如何使用 GitLab CI 搭建嵌入式持续集成流水线,结合 Docker 容器化交叉编译环境,实现从代码提交到固件生成的全自动化。


    二、GitLab CI 嵌入式流水线全景架构

    在这里插入图片描述

    GitLab CI 的核心由三部分组成:GitLab 仓库(代码与配置)、GitLab Runner(执行构建任务的代理)、Docker Registry(容器镜像仓库)。三者协同工作,形成完整的嵌入式CI流水线。

    2.1 流水线阶段(Stage)设计

    一个典型的嵌入式CI流水线包含以下阶段:

    阶段目的典型Job执行环境
    build 交叉编译生成目标文件 build:stm32、build:esp32 Docker容器
    test 运行单元测试与静态分析 test:unit、test:coverage、test:static Docker容器
    package 生成可烧录固件并签名 package:bin、package:hex、package:sign Docker容器
    deploy 部署到OTA服务器或硬件 deploy:ota、deploy:hil 自托管Runner

    2.2 GitLab Runner 配置

    GitLab Runner 是执行CI任务的"工人"。对于嵌入式开发,推荐使用 Docker Executor,因为它提供了最佳的环境隔离和可复现性。

    # 在构建服务器上安装 GitLab Runner
    sudo curl -L –output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
    sudo chmod +x /usr/local/bin/gitlab-runner

    # 注册 Runner(需要 GitLab 的注册令牌)
    sudo gitlab-runner register \\
    –non-interactive \\
    –url "https://gitlab.com/" \\
    –registration-token "YOUR_REGISTRATION_TOKEN" \\
    –executor "docker" \\
    –docker-image "ubuntu:22.04" \\
    –description "embedded-docker-runner" \\
    –tag-list "docker,embedded,arm" \\
    –run-untagged="false" \\
    –locked="false" \\
    –access-level="not_protected"

    # 启动 Runner
    sudo gitlab-runner start

    关键配置说明:

    • –executor "docker":使用 Docker 容器执行每个 Job,确保环境隔离
    • –tag-list "docker,embedded,arm":为 Runner 打标签,.gitlab-ci.yml 中通过 tags 关键字匹配
    • –docker-image:默认基础镜像,可在 .gitlab-ci.yml 中覆盖

    2.3 自托管 Runner vs 共享 Runner

    类型优势适用场景
    共享 Runner 零运维成本,即开即用 通用编译、单元测试
    自托管 Runner 可连接真实硬件(HIL测试)、自定义工具链、数据安全 固件烧录、硬件在环测试、私有工具链

    对于需要连接J-Link、ST-Link等调试器的HIL测试阶段,必须使用自托管Runner:

    # 注册自托管 Shell Runner(用于硬件交互)
    sudo gitlab-runner register \\
    –non-interactive \\
    –url "https://gitlab.com/" \\
    –registration-token "YOUR_TOKEN" \\
    –executor "shell" \\
    –description "embedded-hil-runner" \\
    –tag-list "shell,hil,stm32" \\
    –run-untagged="false"


    三、交叉编译工具链:从x86到ARM的桥梁

    在这里插入图片描述

    交叉编译是嵌入式CI的核心。在x86_64服务器上生成ARM/RISC-V目标代码,需要完整的工具链支持。

    3.1 ARM GCC 工具链选型

    工具链适用场景特点
    arm-none-eabi-gcc 裸机MCU(Cortex-M/A) 无操作系统,Newlib C库
    arm-linux-gnueabihf-gcc Linux嵌入式(Cortex-A) 带Linux系统调用,glibc
    aarch64-none-elf-gcc 64位裸机 AArch64架构
    riscv64-unknown-elf-gcc RISC-V MCU 开源ISA,免授权费

    3.2 工具链安装与版本管理

    # Dockerfile.embedded-toolchain
    FROM ubuntu:22.04

    # 避免交互式配置提示
    ENV DEBIAN_FRONTEND=noninteractive

    # 安装基础依赖
    RUN apt-get update && apt-get install -y \\
    build-essential \\
    cmake \\
    ninja-build \\
    git \\
    wget \\
    curl \\
    python3 \\
    python3-pip \\
    libusb-1.0-0-dev \\
    libncurses5-dev \\
    && rm -rf /var/lib/apt/lists/*

    # 安装 ARM GCC 工具链 (13.2.Rel1)
    ARG ARM_GCC_VERSION=13.2.rel1
    ARG ARM_GCC_URL=https://developer.arm.com/-/media/Files/downloads/gnu/${ARM_GCC_VERSION}/binrel/arm-gnu-toolchain-${ARM_GCC_VERSION}-x86_64-arm-none-eabi.tar.xz

    RUN wget -q ${ARM_GCC_URL} -O /tmp/arm-toolchain.tar.xz \\
    && tar -xf /tmp/arm-toolchain.tar.xz -C /opt/ \\
    && rm /tmp/arm-toolchain.tar.xz

    # 设置环境变量
    ENV PATH=/opt/arm-gnu-toolchain-${ARM_GCC_VERSION}-x86_64-arm-none-eabi/bin:${PATH}

    # 验证安装
    RUN arm-none-eabi-gcc –version

    # 安装 OpenOCD (用于烧录和调试)
    RUN apt-get update && apt-get install -y openocd \\
    && rm -rf /var/lib/apt/lists/*

    # 安装 J-Link 工具 (需从Segger官网下载)
    # 注意:J-Link软件需遵守Segger许可协议
    COPY JLink_Linux_V796a_x86_64.deb /tmp/
    RUN dpkg -i /tmp/JLink_Linux_V796a_x86_64.deb || apt-get install -f -y \\
    && rm /tmp/JLink_Linux_V796a_x86_64.deb

    # 安装 ccache 加速编译
    RUN apt-get update && apt-get install -y ccache \\
    && rm -rf /var/lib/apt/lists/* \\
    && ccache –max-size=5G

    # 配置 ccache 作为编译器包装器
    ENV CCACHE_DIR=/cache/ccache
    RUN mkdir -p ${CCACHE_DIR}

    # 安装代码检查工具
    RUN apt-get update && apt-get install -y \\
    cppcheck \\
    clang-format \\
    clang-tidy \\
    && rm -rf /var/lib/apt/lists/*

    # 创建工作目录
    WORKDIR /workspace

    # 默认命令
    CMD ["/bin/bash"]

    构建并推送镜像到GitLab Registry:

    # 登录 GitLab Registry
    docker login registry.gitlab.com -u YOUR_USERNAME -p YOUR_TOKEN

    # 构建镜像
    docker build -f Dockerfile.embedded-toolchain \\
    -t registry.gitlab.com/your-group/your-project/embedded-toolchain:v1.0 .

    # 推送镜像
    docker push registry.gitlab.com/your-group/your-project/embedded-toolchain:v1.0


    四、Docker 容器化:构建环境的一致性保障

    在这里插入图片描述

    Docker 的分层缓存机制非常适合嵌入式构建场景。通过精心设计镜像层次,可以实现工具链层长期缓存、依赖层版本锁定、项目层快速构建。

    4.1 分层镜像策略

    # === 第一层:基础镜像 (极少变动) ===
    FROM ubuntu:22.04 AS base
    RUN apt-get update && apt-get install -y build-essential git cmake wget curl \\
    && rm -rf /var/lib/apt/lists/*

    # === 第二层:工具链镜像 (变动频率:月) ===
    FROM base AS toolchain
    ARG ARM_GCC_VERSION=13.2.rel1
    RUN wget -q https://developer.arm.com/-/media/Files/downloads/gnu/${ARM_GCC_VERSION}/binrel/arm-gnu-toolchain-${ARM_GCC_VERSION}-x86_64-arm-none-eabi.tar.xz \\
    && tar -xf /tmp/arm-toolchain.tar.xz -C /opt/ \\
    && rm /tmp/arm-toolchain.tar.xz
    ENV PATH=/opt/arm-gnu-toolchain-${ARM_GCC_VERSION}-x86_64-arm-none-eabi/bin:${PATH}

    # === 第三层:项目依赖镜像 (变动频率:周) ===
    FROM toolchain AS dependencies
    # 复制并安装项目特定依赖
    COPY third_party/ /workspace/third_party/
    COPY lib/ /workspace/lib/
    WORKDIR /workspace

    # === 第四层:项目构建镜像 (变动频率:每次提交) ===
    FROM dependencies AS builder
    COPY . /workspace/
    RUN make clean && make -j$(nproc)

    4.2 .gitlab-ci.yml 核心配置

    # .gitlab-ci.yml – 嵌入式CI流水线主配置
    # =============================================================================
    # 全局变量
    # =============================================================================
    variables:
    # 工具链版本
    ARM_GCC_VERSION: "13.2.rel1"
    # Docker镜像地址
    DOCKER_IMAGE: "$CI_REGISTRY_IMAGE/embedded-toolchain:v1.0"
    # 编译产物目录
    BUILD_DIR: "build"
    # ccache 目录
    CCACHE_DIR: "$CI_PROJECT_DIR/.ccache"
    # Git 子模块策略
    GIT_SUBMODULE_STRATEGY: recursive
    # 子模块深度(加速克隆)
    GIT_DEPTH: 10

    # =============================================================================
    # 阶段定义
    # =============================================================================
    stages:
    build
    test
    staticanalysis
    package
    deploy

    # =============================================================================
    # 全局默认配置
    # =============================================================================
    default:
    image: $DOCKER_IMAGE
    tags:
    docker
    embedded
    before_script:
    # 配置 ccache
    mkdir p $CCACHE_DIR
    ccache setconfig=cache_dir=$CCACHE_DIR
    ccache setconfig=max_size=5G
    ccache z # 清零统计
    # 显示工具链版本
    armnoneeabigcc version
    cmake version
    ninja version

    # =============================================================================
    # 缓存配置
    # =============================================================================
    # ccache 缓存:跨Pipeline持久化
    .ccache_cache:
    cache:
    key: ${CI_JOB_NAME}
    paths:
    .ccache/
    policy: pullpush

    # Git 子模块缓存
    .submodule_cache:
    cache:
    key: submodules${CI_COMMIT_REF_SLUG}
    paths:
    .git/modules/
    policy: pullpush

    # =============================================================================
    # Build Stage: 交叉编译
    # =============================================================================
    build:stm32f4:
    stage: build
    extends: .ccache_cache
    variables:
    TARGET_BOARD: "stm32f4"
    CMAKE_ARGS: >
    DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/armnoneeabi.cmake
    DMCU_FAMILY=STM32F4xx
    DMCU_MODEL=STM32F407VG
    DCMAKE_BUILD_TYPE=Release
    script:
    mkdir p ${BUILD_DIR}
    cmake B ${BUILD_DIR} G Ninja ${CMAKE_ARGS}
    cmake build ${BUILD_DIR} target all j$(nproc)
    # 显示编译统计
    ccache s
    artifacts:
    paths:
    ${BUILD_DIR}/*.elf
    ${BUILD_DIR}/*.map
    ${BUILD_DIR}/CMakeFiles/**/*.o
    expire_in: 1 week
    reports:
    dotenv: ${BUILD_DIR}/build.env

    build:stm32h7:
    stage: build
    extends: .ccache_cache
    variables:
    TARGET_BOARD: "stm32h7"
    CMAKE_ARGS: >
    DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/armnoneeabi.cmake
    DMCU_FAMILY=STM32H7xx
    DMCU_MODEL=STM32H743ZI
    DCMAKE_BUILD_TYPE=Release
    DENABLE_FPU=ON
    script:
    mkdir p ${BUILD_DIR}
    cmake B ${BUILD_DIR} G Ninja ${CMAKE_ARGS}
    cmake build ${BUILD_DIR} target all j$(nproc)
    ccache s
    artifacts:
    paths:
    ${BUILD_DIR}/*.elf
    ${BUILD_DIR}/*.map
    expire_in: 1 week

    # 使用矩阵构建多目标平台
    build:matrix:
    stage: build
    extends: .ccache_cache
    parallel:
    matrix:
    TARGET_BOARD: [stm32f4, stm32h7, esp32s3, nrf52840]
    script:
    mkdir p ${BUILD_DIR}
    cmake B ${BUILD_DIR} G Ninja
    DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/${TARGET_BOARD}.cmake
    cmake build ${BUILD_DIR} target all j$(nproc)
    artifacts:
    paths:
    ${BUILD_DIR}/*.elf
    expire_in: 1 week

    # =============================================================================
    # Test Stage: 单元测试 (在x86上使用模拟器或原生测试)
    # =============================================================================
    test:unit:
    stage: test
    needs: [build:stm32f4]
    image: $CI_REGISTRY_IMAGE/embeddedtoolchain:v1.0
    script:
    # 运行Unity单元测试(使用上一阶段的编译产物)
    cmake B ${BUILD_DIR}_test DENABLE_TESTING=ON
    cmake build ${BUILD_DIR}_test target test_runner
    ./${BUILD_DIR}_test/test_runner xml
    artifacts:
    reports:
    junit: ${BUILD_DIR}_test/test_results.xml
    paths:
    ${BUILD_DIR}_test/test_results.xml
    expire_in: 1 week

    test:coverage:
    stage: test
    needs: [build:stm32f4]
    script:
    cmake B ${BUILD_DIR}_cov DENABLE_COVERAGE=ON
    cmake build ${BUILD_DIR}_cov target coverage
    # 生成覆盖率报告
    gcovr xmlpretty excludeunreachablebranches printsummary
    o coverage.xml root ${CI_PROJECT_DIR}
    gcovr html htmldetails o coverage.html root ${CI_PROJECT_DIR}
    coverage: '/TOTAL.*\\s+(\\d+%)$/'
    artifacts:
    paths:
    coverage.xml
    coverage.html
    reports:
    coverage_report:
    coverage_format: cobertura
    path: coverage.xml
    expire_in: 1 week

    # =============================================================================
    # Static Analysis Stage: 静态代码分析
    # =============================================================================
    static-analysis:cppcheck:
    stage: staticanalysis
    needs: []
    allow_failure: true # 不阻塞流水线,但生成报告
    script:
    cppcheck enable=all errorexitcode=0
    xml xmlversion=2
    suppress=missingIncludeSystem
    inlinesuppr
    I include/
    I third_party/
    src/
    2> cppcheckreport.xml
    # 转换为 GitLab 可读的代码质量报告格式
    cppcheckcodequality cppcheckreport.xml > glcodequalityreport.json
    artifacts:
    reports:
    codequality: glcodequalityreport.json
    paths:
    cppcheckreport.xml
    expire_in: 1 week

    static-analysis:misra:
    stage: staticanalysis
    needs: []
    allow_failure: true
    script:
    # 使用 Cppcheck 的 MISRA 插件
    cppcheck addon=misra.json
    enable=all
    errorexitcode=0
    I include/
    src/
    2> misrareport.xml
    artifacts:
    paths:
    misrareport.xml
    expire_in: 1 week

    # =============================================================================
    # Package Stage: 固件生成与签名
    # =============================================================================
    package:firmware:
    stage: package
    needs: [build:stm32f4, build:stm32h7]
    script:
    # 从 ELF 生成二进制文件
    armnoneeabiobjcopy O binary ${BUILD_DIR}/firmware.elf ${BUILD_DIR}/firmware.bin
    armnoneeabiobjcopy O ihex ${BUILD_DIR}/firmware.elf ${BUILD_DIR}/firmware.hex
    # 生成反汇编文件(用于调试)
    armnoneeabiobjdump d ${BUILD_DIR}/firmware.elf > ${BUILD_DIR}/firmware.dump
    # 生成内存使用报告
    armnoneeabisize ${BUILD_DIR}/firmware.elf | tee ${BUILD_DIR}/memory_usage.txt
    # 计算校验和
    sha256sum ${BUILD_DIR}/firmware.bin | tee ${BUILD_DIR}/firmware.sha256
    # 版本信息注入
    echo "VERSION=${CI_COMMIT_TAG:${CI_COMMIT_SHORT_SHA}}" > ${BUILD_DIR}/version.txt
    echo "BUILD_TIME=$(date u +%Y%m%dT%H:%M:%SZ)" >> ${BUILD_DIR}/version.txt
    echo "GIT_COMMIT=${CI_COMMIT_SHA}" >> ${BUILD_DIR}/version.txt
    artifacts:
    name: "firmware-${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHORT_SHA}"
    paths:
    ${BUILD_DIR}/firmware.bin
    ${BUILD_DIR}/firmware.hex
    ${BUILD_DIR}/firmware.sha256
    ${BUILD_DIR}/memory_usage.txt
    ${BUILD_DIR}/version.txt
    ${BUILD_DIR}/firmware.dump
    expire_in: 1 month

    package:sign:
    stage: package
    needs: [package:firmware]
    # 签名需要访问私钥,使用受保护的Runner
    tags:
    protected
    signing
    script:
    # 使用 HSM 或安全密钥进行固件签名
    openssl dgst sha256 sign ${SIGNING_PRIVATE_KEY}
    out ${BUILD_DIR}/firmware.bin.sig
    ${BUILD_DIR}/firmware.bin
    # 验证签名
    openssl dgst sha256 verify ${SIGNING_PUBLIC_KEY}
    signature ${BUILD_DIR}/firmware.bin.sig
    ${BUILD_DIR}/firmware.bin
    # 打包为OTA更新包
    tar czf ${BUILD_DIR}/firmware${CI_COMMIT_TAG}.tar.gz
    C ${BUILD_DIR} firmware.bin firmware.bin.sig version.txt
    artifacts:
    name: "firmware-signed-${CI_COMMIT_TAG}"
    paths:
    ${BUILD_DIR}/firmware*.tar.gz
    expire_in: 3 months

    # =============================================================================
    # Deploy Stage: 部署
    # =============================================================================
    deploy:gitlab-registry:
    stage: deploy
    needs: [package:sign]
    image: curlimages/curl:latest
    script:
    # 将固件包上传到 GitLab Package Registry
    'curl -header "JOB-TOKEN: ${CI_JOB_TOKEN}"
    uploadfile ${BUILD_DIR}/firmware${CI_COMMIT_TAG}.tar.gz
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/firmware/${CI_COMMIT_TAG}/firmware${CI_COMMIT_TAG}.tar.gz"'
    only:
    tags

    deploy:hil-test:
    stage: deploy
    needs: [package:firmware]
    # HIL测试需要连接真实硬件,使用自托管Runner
    tags:
    shell
    hil
    stm32
    script:
    # 烧录固件到目标板
    openocd f interface/stlink.cfg f target/stm32f4x.cfg
    c "program ${BUILD_DIR}/firmware.bin verify reset exit 0x08000000"
    # 等待板子启动
    sleep 3
    # 运行 HIL 测试脚本
    python3 tests/hil/run_tests.py port /dev/ttyUSB0 baud 115200
    artifacts:
    reports:
    junit: tests/hil/results.xml
    paths:
    tests/hil/results.xml
    tests/hil/logs/
    expire_in: 1 week
    allow_failure: true # HIL测试失败不阻塞部署,但记录结果

    # =============================================================================
    # 触发规则
    # =============================================================================
    # 仅在 main 分支和 tag 上运行完整流水线
    .workflow:rules:
    rules:
    if: $CI_COMMIT_TAG
    when: always
    if: $CI_COMMIT_BRANCH == "main"
    when: always
    if: $CI_COMMIT_BRANCH == "develop"
    when: always
    if: $CI_PIPELINE_SOURCE == "merge_request_event"
    when: always
    when: never # 其他情况不触发

    # 应用到所有 Job
    default:
    rules:
    !reference [.workflow:rules, rules]


    五、固件生成流程:从源码到可烧录文件

    在这里插入图片描述

    嵌入式固件的生成是一个多步骤的精密流程,每一步都需要严格控制。

    5.1 编译与链接

    # cmake/toolchains/arm-none-eabi.cmake
    set(CMAKE_SYSTEM_NAME Generic)
    set(CMAKE_SYSTEM_PROCESSOR arm)

    # 指定交叉编译器
    set(CMAKE_C_COMPILER arm-none-eabi-gcc)
    set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
    set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
    set(CMAKE_AR arm-none-eabi-ar)
    set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
    set(CMAKE_OBJDUMP arm-none-eabi-objdump)
    set(CMAKE_SIZE arm-none-eabi-size)
    set(CMAKE_NM arm-none-eabi-nm)
    set(CMAKE_STRIP arm-none-eabi-strip)

    # 禁用默认的编译测试(交叉编译器无法在主机上运行)
    set(CMAKE_C_COMPILER_WORKS 1)
    set(CMAKE_CXX_COMPILER_WORKS 1)

    # 编译标志
    set(CMAKE_C_FLAGS_INIT "\\
    -mcpu=cortex-m4 \\
    -mthumb \\
    -mfpu=fpv4-sp-d16 \\
    -mfloat-abi=hard \\
    -O2 \\
    -g3 \\
    -Wall \\
    -Wextra \\
    -Werror \\
    -ffunction-sections \\
    -fdata-sections \\
    -fno-exceptions \\
    -fno-rtti \\
    -nostdlib \\
    -nostartfiles \\
    ")

    set(CMAKE_CXX_FLAGS_INIT "${CMAKE_C_FLAGS_INIT}")

    # 链接标志
    set(CMAKE_EXE_LINKER_FLAGS_INIT "\\
    -T${CMAKE_SOURCE_DIR}/linker/STM32F407VGTx_FLASH.ld \\
    -Wl,–gc-sections \\
    -Wl,-Map=output.map \\
    -specs=nano.specs \\
    -specs=nosys.specs \\
    -lc -lm -lnosys \\
    ")

    5.2 固件转换与签名脚本

    #!/bin/bash
    # scripts/generate_firmware.sh – 固件生成脚本

    set -euo pipefail

    BUILD_DIR="${1:-build}"
    OUTPUT_DIR="${2:-firmware_output}"
    VERSION="${3:-$(git describe –tags –always –dirty)}"
    BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
    GIT_COMMIT=$(git rev-parse HEAD)

    mkdir -p "${OUTPUT_DIR}"

    echo "=========================================="
    echo "固件生成开始"
    echo "版本: ${VERSION}"
    echo "构建时间: ${BUILD_TIME}"
    echo "Git Commit: ${GIT_COMMIT}"
    echo "=========================================="

    # 1. 从 ELF 生成各种格式
    echo "[1/6] 生成二进制格式 (.bin)…"
    arm-none-eabi-objcopy -O binary \\
    "${BUILD_DIR}/firmware.elf" \\
    "${OUTPUT_DIR}/firmware-${VERSION}.bin"

    echo "[2/6] 生成 Intel Hex 格式 (.hex)…"
    arm-none-eabi-objcopy -O ihex \\
    "${BUILD_DIR}/firmware.elf" \\
    "${OUTPUT_DIR}/firmware-${VERSION}.hex"

    echo "[3/6] 生成反汇编文件…"
    arm-none-eabi-objdump -d -S \\
    "${BUILD_DIR}/firmware.elf" > \\
    "${OUTPUT_DIR}/firmware-${VERSION}.dump"

    # 2. 内存使用分析
    echo "[4/6] 分析内存使用…"
    arm-none-eabi-size -A -x "${BUILD_DIR}/firmware.elf" | tee "${OUTPUT_DIR}/memory-${VERSION}.txt"

    # 3. 计算校验和
    echo "[5/6] 计算 SHA-256 校验和…"
    sha256sum "${OUTPUT_DIR}/firmware-${VERSION}.bin" | tee "${OUTPUT_DIR}/firmware-${VERSION}.sha256"

    # 4. 生成版本信息文件
    echo "[6/6] 生成版本信息…"
    cat > "${OUTPUT_DIR}/version-${VERSION}.json" <<EOF
    {
    "version": "${VERSION}",
    "build_time": "${BUILD_TIME}",
    "git_commit": "${GIT_COMMIT}",
    "git_branch": "$(git rev-parse –abbrev-ref HEAD)",
    "build_host": "$(hostname)",
    "compiler": "$(arm-none-eabi-gcc –version | head -n1)",
    "target": "STM32F407VG",
    "checksum_sha256": "$(sha256sum ${OUTPUT_DIR}/firmware-${VERSION}.bin | cut -d' ' -f1)"
    }
    EOF

    # 5. 固件签名(如果配置了私钥)
    if [ -n "${SIGNING_KEY:-}" ] && [ -f "${SIGNING_KEY}" ]; then
    echo "[7/6] 签名固件…"
    openssl dgst -sha256 -sign "${SIGNING_KEY}" \\
    -out "${OUTPUT_DIR}/firmware-${VERSION}.bin.sig" \\
    "${OUTPUT_DIR}/firmware-${VERSION}.bin"

    # 验证签名
    openssl dgst -sha256 -verify "${SIGNING_KEY}.pub" \\
    -signature "${OUTPUT_DIR}/firmware-${VERSION}.bin.sig" \\
    "${OUTPUT_DIR}/firmware-${VERSION}.bin"

    echo "签名验证通过"
    fi

    echo "=========================================="
    echo "固件生成完成"
    echo "输出目录: ${OUTPUT_DIR}"
    ls -lh "${OUTPUT_DIR}"
    echo "=========================================="

    5.3 产物管理策略

    # .gitlab-ci.yml 产物管理最佳实践
    variables:
    # 产物保留策略
    ARTIFACT_RETENTION_DAYS: "30"

    # 使用 GitLab Package Registry 长期存储固件
    upload:package-registry:
    stage: deploy
    image: curlimages/curl:latest
    needs: [package:sign]
    script:
    # 上传 .bin 到 Generic Package Registry
    |
    curl –request PUT \\
    –header "JOB-TOKEN: ${CI_JOB_TOKEN}" \\
    –upload-file firmware_output/firmware-${CI_COMMIT_TAG}.bin \\
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/firmware/${CI_COMMIT_TAG}/firmware.bin"

    # 上传 .hex
    |
    curl –request PUT \\
    –header "JOB-TOKEN: ${CI_JOB_TOKEN}" \\
    –upload-file firmware_output/firmware-${CI_COMMIT_TAG}.hex \\
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/firmware/${CI_COMMIT_TAG}/firmware.hex"

    # 上传版本信息
    |
    curl –request PUT \\
    –header "JOB-TOKEN: ${CI_JOB_TOKEN}" \\
    –upload-file firmware_output/version-${CI_COMMIT_TAG}.json \\
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/firmware/${CI_COMMIT_TAG}/version.json"

    only:
    tags


    六、Pipeline 阶段编排与Job依赖关系

    在这里插入图片描述

    6.1 并行与串行的艺术

    GitLab CI 的核心编排逻辑是:同一Stage内的Job并行执行,不同Stage之间串行执行。这种设计天然适合嵌入式多平台构建场景。

    # 使用 needs 关键字优化依赖关系
    build:stm32f4:
    stage: build
    # …

    build:stm32h7:
    stage: build
    # …

    # test:unit 不需要等待 build:stm32h7,只需要 build:stm32f4
    test:unit:
    stage: test
    needs: [build:stm32f4] # 仅依赖特定Job,而非整个Stage
    # …

    # package 需要等待所有 build 完成
    package:firmware:
    stage: package
    needs: [build:stm32f4, build:stm32h7]
    # …

    6.2 多目标平台并行构建矩阵

    在这里插入图片描述

    # 使用 parallel:matrix 实现多平台并行构建
    build:all-targets:
    stage: build
    extends: .ccache_cache
    parallel:
    matrix:
    TARGET: stm32f4
    MCU: STM32F407VG
    FPU: fpv4spd16
    TOOLCHAIN: armnoneeabi
    TARGET: stm32h7
    MCU: STM32H743ZI
    FPU: fpv5d16
    TOOLCHAIN: armnoneeabi
    TARGET: esp32s3
    MCU: ESP32S3
    FPU: none
    TOOLCHAIN: xtensaesp32s3elf
    TARGET: nrf52840
    MCU: nRF52840
    FPU: fpv4spd16
    TOOLCHAIN: armnoneeabi
    script:
    echo "Building for ${TARGET} (${MCU})"
    cmake B ${BUILD_DIR}
    DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/${TOOLCHAIN}.cmake
    DMCU_MODEL=${MCU}
    DMCU_FPU=${FPU}
    cmake build ${BUILD_DIR} j$(nproc)
    artifacts:
    paths:
    ${BUILD_DIR}/firmware.elf
    expire_in: 1 week


    七、缓存策略:构建加速的关键

    在这里插入图片描述

    嵌入式项目通常依赖大量第三方库和工具链,合理的缓存策略可以将构建时间从20分钟压缩到3分钟以内。

    7.1 四级缓存体系

    # .gitlab-ci.yml 完整缓存配置
    variables:
    # ccache 配置
    CCACHE_DIR: "${CI_PROJECT_DIR}/.ccache"
    CCACHE_MAXSIZE: "5G"
    CCACHE_CPP2: "true"
    CCACHE_COMPILERCHECK: "content"

    # 全局缓存模板
    .ccache:
    cache:
    key: "ccache-${CI_JOB_NAME}-${CI_COMMIT_REF_SLUG}"
    paths:
    .ccache/
    policy: pullpush

    .git_submodules:
    cache:
    key: "submodules-${CI_COMMIT_REF_SLUG}"
    paths:
    .git/modules/
    third_party/**/ # 缓存已下载的第三方库
    policy: pullpush

    .docker_layers:
    cache:
    key: "docker-layers"
    paths:
    /var/lib/docker/
    policy: pull # Docker层缓存只拉取不推送

    # 在Job中使用
    build:stm32f4:
    extends:
    .ccache
    .git_submodules
    # …

    7.2 ccache 深度集成

    # 在 CMake 中集成 ccache
    # cmake/ccache.cmake
    find_program(CCACHE_PROGRAM ccache)
    if(CCACHE_PROGRAM)
    message(STATUS "ccache found: ${CCACHE_PROGRAM}")
    set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE_PROGRAM})
    set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM})
    set(CMAKE_ASM_COMPILER_LAUNCHER ${CCACHE_PROGRAM})

    # 设置 ccache 配置
    execute_process(
    COMMAND ${CCACHE_PROGRAM} –set-config=sloppiness=pch_defines,time_macros,include_file_mtime
    )
    else()
    message(WARNING "ccache not found, builds will be slower")
    endif()

    # GitLab CI 中监控 ccache 命中率
    build:stm32f4:
    # …
    script:
    cmake build ${BUILD_DIR} j$(nproc)
    # 输出 ccache 统计
    |
    echo "========== ccache 统计 =========="
    ccache -s
    HIT_RATE=$(ccache -s | grep "cache hit rate" | grep -oP '\\d+\\.\\d+' | tail -1)
    echo "ccache 命中率: ${HIT_RATE}%"
    # 如果命中率低于50%,发出警告
    if (( $(echo "$HIT_RATE < 50" | bc -l) )); then
    echo "WARNING: ccache 命中率过低 (${HIT_RATE}%),请检查缓存配置"
    fi
    echo "=================================="

    7.3 Git 子模块加速

    # 使用 GIT_DEPTH 和 fetch 策略加速子模块克隆
    variables:
    GIT_DEPTH: 10 # 浅克隆,只拉取最近10个提交
    GIT_SUBMODULE_STRATEGY: recursive
    GIT_SUBMODULE_DEPTH: 1 # 子模块也浅克隆

    # 或者使用缓存的子模块
    build:with-submodule-cache:
    cache:
    key: "submodules-${CI_COMMIT_REF_SLUG}"
    paths:
    .git/modules/
    third_party/
    before_script:
    # 如果缓存存在,更新子模块;否则完整克隆
    |
    if [ -d ".git/modules" ]; then
    git submodule update –init –recursive –depth 1
    else
    git submodule sync –recursive
    git submodule update –init –recursive –depth 1 –jobs 4
    fi


    八、CMake 交叉编译工程实战

    8.1 完整 CMakeLists.txt 示例

    # CMakeLists.txt – 嵌入式交叉编译工程
    cmake_minimum_required(VERSION 3.20)
    project(EmbeddedFirmware VERSION 1.0.0 LANGUAGES C CXX ASM)

    # =============================================================================
    # 选项配置
    # =============================================================================
    option(ENABLE_TESTING "Enable unit testing" OFF)
    option(ENABLE_COVERAGE "Enable code coverage" OFF)
    option(ENABLE_FPU "Enable hardware FPU" ON)
    option(BUILD_EXAMPLES "Build example applications" OFF)

    # =============================================================================
    # 编译器配置
    # =============================================================================
    if(NOT CMAKE_CROSSCOMPILING)
    message(FATAL_ERROR "This project must be cross-compiled. Use -DCMAKE_TOOLCHAIN_FILE")
    endif()

    # 设置 C 标准
    set(CMAKE_C_STANDARD 11)
    set(CMAKE_C_STANDARD_REQUIRED ON)
    set(CMAKE_CXX_STANDARD 17)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)

    # 编译标志
    set(COMMON_FLAGS
    -mthumb
    -ffunction-sections
    -fdata-sections
    -fno-builtin
    -fno-exceptions
    -Wall
    -Wextra
    -Werror
    -Wshadow
    -Wdouble-promotion
    -Wformat=2
    -Wundef
    -Wconversion
    -Wsign-conversion
    )

    if(ENABLE_FPU)
    list(APPEND COMMON_FLAGS -mfpu=fpv4-sp-d16 -mfloat-abi=hard)
    endif()

    # 优化级别
    set(CMAKE_C_FLAGS_DEBUG "-O0 -g3 -DDEBUG")
    set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG -flto")
    set(CMAKE_C_FLAGS_RELWITHDEBINFO "-O2 -g -DNDEBUG")

    # 链接标志
    set(CMAKE_EXE_LINKER_FLAGS
    "-Wl,–gc-sections \\
    -Wl,–print-memory-usage \\
    -Wl,–no-warn-rwx-segments \\
    -specs=nano.specs \\
    -specs=nosys.specs"
    )

    # =============================================================================
    # 源文件配置
    # =============================================================================
    set(SOURCES
    src/main.c
    src/system_stm32f4xx.c
    src/startup_stm32f407xx.s
    src/hal/hal_gpio.c
    src/hal/hal_uart.c
    src/hal/hal_timer.c
    src/drivers/motor_driver.c
    src/drivers/sensor_driver.c
    src/app/control_loop.c
    src/app/state_machine.c
    src/utils/crc32.c
    src/utils/ring_buffer.c
    )

    set(INCLUDE_DIRS
    include
    include/hal
    include/drivers
    include/app
    include/utils
    third_party/CMSIS/Include
    third_party/STM32F4xx_HAL_Driver/Inc
    )

    # =============================================================================
    # 目标配置
    # =============================================================================
    add_executable(${PROJECT_NAME}.elf ${SOURCES})

    target_include_directories(${PROJECT_NAME}.elf PRIVATE ${INCLUDE_DIRS})

    target_compile_options(${PROJECT_NAME}.elf PRIVATE ${COMMON_FLAGS})

    target_link_options(${PROJECT_NAME}.elf PRIVATE
    -T${CMAKE_SOURCE_DIR}/linker/STM32F407VGTx_FLASH.ld
    ${CMAKE_EXE_LINKER_FLAGS}
    )

    # 链接库
    target_link_libraries(${PROJECT_NAME}.elf PRIVATE
    c
    m
    nosys
    )

    # =============================================================================
    # 固件生成规则
    # =============================================================================
    # 生成 .bin
    add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
    COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${PROJECT_NAME}.elf> ${PROJECT_NAME}.bin
    COMMENT "Generating binary file"
    )

    # 生成 .hex
    add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
    COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:${PROJECT_NAME}.elf> ${PROJECT_NAME}.hex
    COMMENT "Generating hex file"
    )

    # 生成反汇编
    add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
    COMMAND ${CMAKE_OBJDUMP} -d -S $<TARGET_FILE:${PROJECT_NAME}.elf> > ${PROJECT_NAME}.dump
    COMMENT "Generating disassembly"
    )

    # 内存使用报告
    add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
    COMMAND ${CMAKE_SIZE} -A -x $<TARGET_FILE:${PROJECT_NAME}.elf> > memory_usage.txt
    COMMENT "Analyzing memory usage"
    )

    # =============================================================================
    # 测试配置
    # =============================================================================
    if(ENABLE_TESTING)
    enable_testing()
    add_subdirectory(tests)
    endif()

    if(ENABLE_COVERAGE)
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} –coverage -fprofile-arcs -ftest-coverage")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} –coverage")
    endif()

    # =============================================================================
    # 安装规则
    # =============================================================================
    install(FILES
    ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.bin
    ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.hex
    ${CMAKE_BINARY_DIR}/memory_usage.txt
    DESTINATION firmware/${PROJECT_VERSION}
    )

    8.2 链接器脚本(Linker Script)

    /* linker/STM32F407VGTx_FLASH.ld */
    MEMORY
    {
    /* Flash 内存: 1MB */
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K

    /* SRAM: 128KB (0x20000000 – 0x2001FFFF) */
    RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K

    /* CCM RAM: 64KB (仅CPU访问) */
    CCM (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
    }

    /* 栈顶初始化值 */
    _estack = ORIGIN(RAM) + LENGTH(RAM);

    /* 最小栈大小 */
    _Min_Heap_Size = 0x200;
    _Min_Stack_Size = 0x400;

    SECTIONS
    {
    /* 中断向量表 */
    .isr_vector :
    {
    . = ALIGN(4);
    KEEP(*(.isr_vector))
    . = ALIGN(4);
    } >FLASH

    /* 代码段 */
    .text :
    {
    . = ALIGN(4);
    *(.text)
    *(.text*)
    *(.glue_7)
    *(.glue_7t)
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))
    . = ALIGN(4);
    _etext = .;
    } >FLASH

    /* 只读数据段 */
    .rodata :
    {
    . = ALIGN(4);
    *(.rodata)
    *(.rodata*)
    . = ALIGN(4);
    } >FLASH

    /* 初始化数据段的加载地址 (LMA) */
    _sidata = LOADADDR(.data);

    /* 初始化数据段 */
    .data :
    {
    . = ALIGN(4);
    _sdata = .;
    *(.data)
    *(.data*)
    . = ALIGN(4);
    _edata = .;
    } >RAM AT> FLASH

    /* 未初始化数据段 (BSS) */
    .bss :
    {
    . = ALIGN(4);
    _sbss = .;
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
    __bss_end__ = _ebss;
    } >RAM

    /* 用户堆栈初始化 */
    __end__ = .;
    end = __end__;
    }


    九、高级技巧:条件触发、保护分支与回滚

    9.1 条件触发规则

    # 只在特定条件下触发构建
    build:conditional:
    stage: build
    rules:
    # 只在 main 和 develop 分支触发
    if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"
    when: always
    # 只在 tags 上触发
    if: $CI_COMMIT_TAG
    when: always
    # Merge Request 时触发
    if: $CI_PIPELINE_SOURCE == "merge_request_event"
    when: always
    # 当源码文件变化时触发(使用 changes 关键字)
    if: $CI_COMMIT_BRANCH
    changes:
    src/**/*
    include/**/*
    CMakeLists.txt
    when: always
    # 其他情况不触发
    when: never

    9.2 保护分支与部署权限

    # 受保护的部署Job
    deploy:production:
    stage: deploy
    script:
    ./scripts/deploy.sh production
    environment:
    name: production
    url: https://ota.example.com
    only:
    tags # 只在打 tag 时部署到生产环境
    when: manual # 需要手动触发
    allow_failure: false # 失败时阻塞流水线

    9.3 固件版本回滚

    # 回滚Job
    rollback:firmware:
    stage: deploy
    script:
    |
    # 获取上一个成功的版本
    LAST_SUCCESSFUL=$(curl -s –header "PRIVATE-TOKEN: ${CI_API_TOKEN}" \\
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/pipelines?status=success&per_page=2" | \\
    jq -r '.[1].sha')

    echo "回滚到版本: ${LAST_SUCCESSFUL}"

    # 从 Package Registry 下载旧版本固件
    curl –header "JOB-TOKEN: ${CI_JOB_TOKEN}" \\
    o firmwarerollback.bin \\
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/firmware/${LAST_SUCCESSFUL}/firmware.bin"

    # 执行回滚烧录
    openocd f interface/stlink.cfg f target/stm32f4x.cfg \\
    c "program firmwarerollback.bin verify reset exit 0x08000000"
    when: manual # 手动触发回滚
    allow_failure: false


    十、鸿蒙生态(OpenHarmony)中的CI实践

    在鸿蒙生态开发中,上述CI技术同样适用。OpenHarmony的编译系统基于GN + Ninja,可以通过以下方式集成到GitLab CI:

    # OpenHarmony 项目的 .gitlab-ci.yml
    build:openharmony:
    stage: build
    image: $CI_REGISTRY_IMAGE/openharmonybuildenv:v1.0 # 预装鸿蒙编译环境
    variables:
    OHOS_ROOT: "/opt/openharmony"
    PRODUCT: "rk3568" # 瑞芯微RK3568开发板
    script:
    # 设置编译环境
    source ${OHOS_ROOT}/build.sh productname ${PRODUCT}

    # 编译轻内核(LiteOS-M)
    hb set root ${OHOS_ROOT}
    hb build f

    # 生成烧录镜像
    ./device/board/rk3568/build_image.sh

    # 打包固件
    tar czf openharmonyfirmware${CI_COMMIT_SHORT_SHA}.tar.gz out/
    artifacts:
    paths:
    openharmonyfirmware*.tar.gz
    expire_in: 1 month


    十一、总结与最佳实践清单

    实践项推荐方案效果
    环境一致性 Docker 容器化 + 私有 Registry 消除"在我机器上能跑"
    构建加速 ccache + 分层缓存 + 并行Job 构建时间从20min→3min
    多平台支持 parallel:matrix + 多工具链 一次提交验证全平台
    产物管理 Artifacts + Package Registry 版本可追溯、可回滚
    代码质量 Cppcheck + MISRA-C + 覆盖率门禁 缺陷早发现
    安全部署 固件签名 + 受保护Runner 防止恶意固件
    HIL集成 自托管Runner + OpenOCD 真实硬件自动化验证

    持续集成不是一次性配置,而是持续优化的过程。 建议团队从以下步骤开始:

  • 第一周:搭建基础Docker镜像,实现单次手动编译
  • 第二周:编写.gitlab-ci.yml,实现push自动触发
  • 第三周:引入ccache和子模块缓存,优化构建时间
  • 第四周:添加单元测试和静态分析,设置覆盖率门禁
  • 第一个月:集成HIL测试,实现从提交到部署的全自动化
  • 当每一次代码提交都能在15分钟内完成编译、测试、分析、打包的全流程验证时,团队就真正迈入了持续交付的门槛。


    转载自:https://blog.csdn.net/u014727709/article/details/162584640 欢迎 👍点赞✍评论⭐收藏,欢迎指正

    赞(0)
    未经允许不得转载:171主机测评 » 持续集成在嵌入式开发中的实践:GitLab CI与交叉编译——自动化构建、固件生成
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址