文章目录
-
- 每日一句正能量
- 一、前言:嵌入式开发的"构建之痛"
- [二、GitLab CI 嵌入式流水线全景架构](#二、GitLab CI 嵌入式流水线全景架构)
-
- [2.1 流水线阶段(Stage)设计](#2.1 流水线阶段(Stage)设计)
- [2.2 GitLab Runner 配置](#2.2 GitLab Runner 配置)
- [2.3 自托管 Runner vs 共享 Runner](#2.3 自托管 Runner vs 共享 Runner)
- 三、交叉编译工具链:从x86到ARM的桥梁
-
- [3.1 ARM GCC 工具链选型](#3.1 ARM GCC 工具链选型)
- [3.2 工具链安装与版本管理](#3.2 工具链安装与版本管理)
- [四、Docker 容器化:构建环境的一致性保障](#四、Docker 容器化:构建环境的一致性保障)
-
- [4.1 分层镜像策略](#4.1 分层镜像策略)
- [4.2 .gitlab-ci.yml 核心配置](#4.2 .gitlab-ci.yml 核心配置)
- 五、固件生成流程:从源码到可烧录文件
-
- [5.1 编译与链接](#5.1 编译与链接)
- [5.2 固件转换与签名脚本](#5.2 固件转换与签名脚本)
- [5.3 产物管理策略](#5.3 产物管理策略)
- [六、Pipeline 阶段编排与Job依赖关系](#六、Pipeline 阶段编排与Job依赖关系)
-
- [6.1 并行与串行的艺术](#6.1 并行与串行的艺术)
- [6.2 多目标平台并行构建矩阵](#6.2 多目标平台并行构建矩阵)
- 七、缓存策略:构建加速的关键
-
- [7.1 四级缓存体系](#7.1 四级缓存体系)
- [7.2 ccache 深度集成](#7.2 ccache 深度集成)
- [7.3 Git 子模块加速](#7.3 Git 子模块加速)
- [八、CMake 交叉编译工程实战](#八、CMake 交叉编译工程实战)
-
- [8.1 完整 CMakeLists.txt 示例](#8.1 完整 CMakeLists.txt 示例)
- [8.2 链接器脚本(Linker Script)](#8.2 链接器脚本(Linker Script))
- 九、高级技巧:条件触发、保护分支与回滚
-
- [9.1 条件触发规则](#9.1 条件触发规则)
- [9.2 保护分支与部署权限](#9.2 保护分支与部署权限)
- [9.3 固件版本回滚](#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,因为它提供了最佳的环境隔离和可复现性。
bash
# 在构建服务器上安装 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:
bash
# 注册自托管 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
# 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:
bash
# 登录 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 分层镜像策略
dockerfile
# === 第一层:基础镜像 (极少变动) ===
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 核心配置
yaml
# .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
- static-analysis
- package
- deploy
# =============================================================================
# 全局默认配置
# =============================================================================
default:
image: $DOCKER_IMAGE
tags:
- docker
- embedded
before_script:
# 配置 ccache
- mkdir -p $CCACHE_DIR
- ccache --set-config=cache_dir=$CCACHE_DIR
- ccache --set-config=max_size=5G
- ccache -z # 清零统计
# 显示工具链版本
- arm-none-eabi-gcc --version
- cmake --version
- ninja --version
# =============================================================================
# 缓存配置
# =============================================================================
# ccache 缓存:跨Pipeline持久化
.ccache_cache:
cache:
key: ${CI_JOB_NAME}
paths:
- .ccache/
policy: pull-push
# Git 子模块缓存
.submodule_cache:
cache:
key: submodules-${CI_COMMIT_REF_SLUG}
paths:
- .git/modules/
policy: pull-push
# =============================================================================
# Build Stage: 交叉编译
# =============================================================================
build:stm32f4:
stage: build
extends: .ccache_cache
variables:
TARGET_BOARD: "stm32f4"
CMAKE_ARGS: >-
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/arm-none-eabi.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/arm-none-eabi.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/embedded-toolchain: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 --xml-pretty --exclude-unreachable-branches --print-summary
-o coverage.xml --root ${CI_PROJECT_DIR}
- gcovr --html --html-details -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: static-analysis
needs: []
allow_failure: true # 不阻塞流水线,但生成报告
script:
- cppcheck --enable=all --error-exitcode=0
--xml --xml-version=2
--suppress=missingIncludeSystem
--inline-suppr
-I include/
-I third_party/
src/
2> cppcheck-report.xml
# 转换为 GitLab 可读的代码质量报告格式
- cppcheck-codequality cppcheck-report.xml > gl-codequality-report.json
artifacts:
reports:
codequality: gl-codequality-report.json
paths:
- cppcheck-report.xml
expire_in: 1 week
static-analysis:misra:
stage: static-analysis
needs: []
allow_failure: true
script:
# 使用 Cppcheck 的 MISRA 插件
- cppcheck --addon=misra.json
--enable=all
--error-exitcode=0
-I include/
src/
2> misra-report.xml
artifacts:
paths:
- misra-report.xml
expire_in: 1 week
# =============================================================================
# Package Stage: 固件生成与签名
# =============================================================================
package:firmware:
stage: package
needs: [build:stm32f4, build:stm32h7]
script:
# 从 ELF 生成二进制文件
- arm-none-eabi-objcopy -O binary ${BUILD_DIR}/firmware.elf ${BUILD_DIR}/firmware.bin
- arm-none-eabi-objcopy -O ihex ${BUILD_DIR}/firmware.elf ${BUILD_DIR}/firmware.hex
# 生成反汇编文件(用于调试)
- arm-none-eabi-objdump -d ${BUILD_DIR}/firmware.elf > ${BUILD_DIR}/firmware.dump
# 生成内存使用报告
- arm-none-eabi-size ${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}"
--upload-file ${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
# 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 固件转换与签名脚本
bash
#!/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 产物管理策略
yaml
# .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之间串行执行。这种设计天然适合嵌入式多平台构建场景。
yaml
# 使用 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 多目标平台并行构建矩阵

yaml
# 使用 parallel:matrix 实现多平台并行构建
build:all-targets:
stage: build
extends: .ccache_cache
parallel:
matrix:
- TARGET: stm32f4
MCU: STM32F407VG
FPU: fpv4-sp-d16
TOOLCHAIN: arm-none-eabi
- TARGET: stm32h7
MCU: STM32H743ZI
FPU: fpv5-d16
TOOLCHAIN: arm-none-eabi
- TARGET: esp32s3
MCU: ESP32S3
FPU: none
TOOLCHAIN: xtensa-esp32s3-elf
- TARGET: nrf52840
MCU: nRF52840
FPU: fpv4-sp-d16
TOOLCHAIN: arm-none-eabi
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 四级缓存体系
yaml
# .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: pull-push
.git_submodules:
cache:
key: "submodules-${CI_COMMIT_REF_SLUG}"
paths:
- .git/modules/
- third_party/**/ # 缓存已下载的第三方库
policy: pull-push
.docker_layers:
cache:
key: "docker-layers"
paths:
- /var/lib/docker/
policy: pull # Docker层缓存只拉取不推送
# 在Job中使用
build:stm32f4:
extends:
- .ccache
- .git_submodules
# ...
7.2 ccache 深度集成
bash
# 在 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()
yaml
# 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 子模块加速
yaml
# 使用 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 示例
cmake
# 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)
ld
/* 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 条件触发规则
yaml
# 只在特定条件下触发构建
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 保护分支与部署权限
yaml
# 受保护的部署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 固件版本回滚
yaml
# 回滚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 firmware-rollback.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 firmware-rollback.bin verify reset exit 0x08000000"
when: manual # 手动触发回滚
allow_failure: false
十、鸿蒙生态(OpenHarmony)中的CI实践
在鸿蒙生态开发中,上述CI技术同样适用。OpenHarmony的编译系统基于GN + Ninja,可以通过以下方式集成到GitLab CI:
yaml
# OpenHarmony 项目的 .gitlab-ci.yml
build:openharmony:
stage: build
image: $CI_REGISTRY_IMAGE/openharmony-build-env:v1.0 # 预装鸿蒙编译环境
variables:
OHOS_ROOT: "/opt/openharmony"
PRODUCT: "rk3568" # 瑞芯微RK3568开发板
script:
# 设置编译环境
- source ${OHOS_ROOT}/build.sh --product-name ${PRODUCT}
# 编译轻内核(LiteOS-M)
- hb set -root ${OHOS_ROOT}
- hb build -f
# 生成烧录镜像
- ./device/board/rk3568/build_image.sh
# 打包固件
- tar -czf openharmony-firmware-${CI_COMMIT_SHORT_SHA}.tar.gz out/
artifacts:
paths:
- openharmony-firmware-*.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
欢迎 👍点赞✍评论⭐收藏,欢迎指正