4. 系统联调基础

持续集成,说白了就是让代码每次提交都能自动编译、自动测试。我早年做嵌入式项目时,团队还在用手动编译、手动烧录的老办法。每次联调前光合并代码就要花半天,还经常漏掉依赖冲突。后来上了CI,才真正体会到什么叫「机器能干的活,别让人干」。

4.1 CI/CD 核心概念:为什么嵌入式项目更需要它

嵌入式开发和纯软件有个很大的不同——你离不开硬件。代码写得再好,烧到板子上跑不起来就是零。CI/CD 在嵌入式领域的价值,我总结为三点:

  • 尽早发现集成问题:硬件驱动、中间件、应用层代码,任何一层改了,其他层可能就崩了。CI 能在合并前就把问题暴露出来。
  • 自动化回归测试:每次提交都跑一遍单元测试、编译检查、静态分析。我见过太多「改了一行宏定义,整个工程编译不过」的惨案。
  • 可追溯的构建产物:每次 CI 构建都会生成带版本号的固件。出了问题,能精确回退到某个 commit 对应的二进制。

我的经验:在某个车载项目里,我们引入了 CI 后,集成测试周期从两周缩短到两天。关键不是工具多牛,而是「每次提交都验证」这个习惯。

4.2 Jenkins vs GitLab CI/CD:选型对比

这两个工具我都深度用过。Jenkins 是老牌选手,插件生态极其丰富;GitLab CI/CD 则胜在与代码仓库深度集成,配置更轻量。下面这张表是我在实际项目中的对比感受:

对比维度 Jenkins GitLab CI/CD
配置方式 Web界面 + Jenkinsfile .gitlab-ci.yml 纯代码
Pipeline即代码 Declarative / Scripted Pipeline YAML 语法,原生支持
触发策略 Webhook + 插件 内置 Push / MR / Tag 规则
嵌入式适配 需自行配置交叉编译环境 可用 Docker runner 封装工具链
维护成本 较高,需管理 Master/Slave 较低,SaaS 或自托管均可

我个人习惯:如果团队规模小、追求快速上手,GitLab CI/CD 是首选。如果项目复杂、需要高度定制(比如多架构并行构建),Jenkins 更灵活。

4.3 Pipeline即代码:把构建流程写成配置

Pipeline即代码,就是把整个 CI 流程——从拉代码、编译、测试到打包——全部写在一个文件里。这样做的好处很明显:版本可控、可审查、可复用。

4.3.1 GitLab CI/CD 的 Pipeline 结构

一个典型的 .gitlab-ci.yml 包含 stages、jobs 和 scripts。看个例子:

stages:
  - build
  - test
  - package

build_firmware:
  stage: build
  script:
    - make clean
    - make all
  artifacts:
    paths:
      - firmware.bin

run_unit_tests:
  stage: test
  script:
    - make test
  dependencies:
    - build_firmware

package_release:
  stage: package
  script:
    - cp firmware.bin release/
    - tar -czf release.tar.gz release/
  only:
    - tags

这里有个细节:artifacts 会把编译产物传递给后续 job。我在项目中遇到过一个问题——测试 job 拿不到编译产物,后来发现是 dependencies 没写对。嗯,这个坑踩过一次就不会忘了。

4.3.2 Jenkins Declarative Pipeline

Jenkins 的 Pipeline 写法类似,但用 Groovy 语法:

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'make clean'
                sh 'make all'
            }
        }
        stage('Test') {
            steps {
                sh 'make test'
            }
        }
        stage('Package') {
            when {
                tag "v*"
            }
            steps {
                sh 'tar -czf release.tar.gz firmware.bin'
            }
        }
    }
}

避坑指南:我曾经在 Jenkins 里用 Scripted Pipeline 写了一个超长的脚本,结果维护起来痛苦不堪。后来全部改成 Declarative Pipeline,结构清晰多了。建议新手从 Declarative 开始。

4.4 触发策略:Push / MR / Tag

触发策略决定了「什么时候跑 Pipeline」。选对了,效率翻倍;选错了,服务器天天空转。

4.4.1 Push 触发

最简单的策略:只要有人 push 代码,就触发 CI。适合开发分支的快速验证。但要注意——如果团队频繁 push,可能会造成资源浪费。我建议只在 develop 和 feature/* 分支上启用。

4.4.2 Merge Request 触发

这是我最推荐的策略。只有当你发起 MR(或 PR)时,才触发完整的 CI 流水线。这样做的好处是:

  • 代码审查和 CI 验证同步进行
  • MR 合并前必须通过所有测试
  • 避免「push 即触发」带来的碎片化构建

在 GitLab CI/CD 中配置 MR 触发很简单:

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

4.4.3 Tag 触发

Tag 触发通常用于发布流程。当你打了一个版本标签(如 v1.2.0),CI 自动执行打包、签名、生成 release notes 等操作。我在项目中是这样用的:

release_job:
  stage: release
  script:
    - ./sign_firmware.sh
    - ./upload_to_server.sh
  only:
    - tags
  except:
    - branches

注意:Tag 触发的 job 通常需要更高的权限(如访问密钥、签名证书)。务必在 CI 变量中安全存储这些敏感信息,不要硬编码在 yml 文件里。

4.5 嵌入式 CI 的典型触发流程

下面这张 SVG 图展示了我常用的嵌入式 CI 触发策略组合:

嵌入式 CI 触发策略流程图 嵌入式 CI 触发策略流程图 开发者提交代码 分支类型判断 Push 触发 MR 触发 Tag 触发 快速编译 + 单元测试 完整编译 + 集成测试 打包 + 签名 + 发布 不同触发策略对应不同验证深度,避免资源浪费

4.6 嵌入式 CI 的特殊考量

嵌入式 CI 和普通 Web 项目的 CI 有个本质区别——你没法在 CI 服务器上直接插一块开发板。怎么办?我分享几个实践:

  • 使用模拟器或 QEMU:对于 ARM Cortex-M 系列,QEMU 可以模拟大部分指令。至少能跑通编译和基础逻辑测试。
  • 硬件在环(HIL):如果预算允许,搭建一个硬件集群,通过 USB/串口与 CI 服务器通信。我在一个工业项目中就是这么做的——每次 CI 通过后,自动烧录到 5 块不同型号的板子上验证。
  • 静态分析先行:在真正烧录前,先用 MISRA 检查、代码规范扫描、内存泄漏检测把低级问题过滤掉。

我的建议:不要试图在 CI 里覆盖所有硬件测试。把 CI 定位为「快速反馈环」,硬件相关的深度测试留给 nightly build 或专门的系统联调阶段。

4.7 小结

持续集成的核心不是工具,而是「自动化验证」这个习惯。Jenkins 和 GitLab CI/CD 各有千秋,选哪个取决于你的团队规模和项目复杂度。Pipeline即代码让构建流程变得可追溯、可复用。触发策略则决定了 CI 的执行效率——Push 用于快速反馈,MR 用于质量门禁,Tag 用于发布管理。

你想想看,如果每次提交都能自动编译、自动跑测试、自动生成固件,集成测试还会是噩梦吗?