2、CI/CD基础回顾:持续集成与持续部署概念、嵌入式CI/CD的特殊性、Jenkins/GitLab CI基础配置

2.1 持续集成与持续部署:不只是“自动编译”那么简单

说到CI/CD,很多人第一反应就是“自动编译代码”。嗯,这个理解没错,但太浅了。我个人的理解是:持续集成(CI)是让团队每天都能把代码合到一起,并且确保它还能跑;持续部署(CD)是让这个能跑的代码,自动送到测试环境甚至用户手里。

你想想看,传统开发流程里,大家各写各的模块,等到项目后期才合并。结果呢?一合并就是“地狱模式”——冲突一堆,编译不过,功能全崩。我在一个车载项目里就吃过这个亏,三个工程师写了两个月,合并花了两周,最后还漏了一个时序bug,导致路测时屏幕闪屏。从那以后,我坚决要求团队必须每天至少合并一次代码。

持续集成的核心,其实就三件事:

  • 频繁提交:每个人每天至少提交一次代码到主干
  • 自动构建:提交后自动触发编译、链接、打包
  • 自动测试:跑单元测试、静态检查、甚至硬件在环测试

持续部署则更进一步。它不光把代码构建好,还自动部署到目标环境。对于嵌入式来说,这个“目标环境”可能是开发板、仿真器,甚至是产线设备。我曾经见过一个团队,CD流水线跑完,固件直接烧录到100块测试板上,然后自动跑回归测试——这才是真正的“一键部署”。

重要概念区分:

概念核心目标嵌入式场景举例
持续集成(CI)保证代码合并后不坏每次提交后编译固件,跑静态分析
持续交付保证代码随时可发布构建产物(.hex/.bin)自动归档到服务器
持续部署(CD)自动部署到目标环境固件自动烧录到测试板,并启动HIL测试

2.2 嵌入式CI/CD的特殊性:为什么不能照搬互联网那套?

很多从互联网转过来的工程师,一开始会把Web端的CI/CD流程直接套到嵌入式上。结果呢?跑不通。为什么?因为嵌入式有它自己的“脾气”。

第一,硬件依赖。 Web应用编译完就能跑在服务器上,但嵌入式固件必须烧到特定芯片上才能验证。你没法在Jenkins的Slave节点上插一块STM32开发板吧?所以嵌入式CI/CD必须解决“怎么在没有硬件的情况下做初步验证,以及怎么在真实硬件上做最终验证”的问题。

第二,交叉编译。 我们的代码不是在目标平台上编译的,而是在x86的服务器上,用交叉编译工具链生成ARM或RISC-V的机器码。这个工具链的版本、路径、依赖库,稍微不对就编译失败。我记得有一次,团队升级了GCC版本,结果某个外设驱动的内联汇编语法变了,编译直接报错——排查了半天。

第三,烧录与调试。 编译完的.bin文件怎么烧到板子上?用J-Link?ST-Link?还是串口?不同的烧录方式,流水线的配置完全不同。而且烧录过程中可能遇到板子没上电、驱动没装、端口被占用等问题——这些都是嵌入式CI/CD特有的“坑”。

第四,测试的实时性要求。 嵌入式系统往往有实时性约束。比如一个电机控制任务,必须在1ms内响应。这种测试不能靠软件模拟,必须接真实硬件,用示波器或逻辑分析仪来验证。我在做无人机飞控项目时,CI流水线里专门加了一个步骤:自动飞控HIL测试,用仿真环境模拟传感器输入,验证输出PWM波的时序是否达标。

我的建议: 嵌入式CI/CD不要追求“全自动”,而是分阶段自动化。第一阶段:编译+静态检查(100%自动化)。第二阶段:单元测试+软件在环(90%自动化)。第三阶段:硬件在环测试(自动化触发,但需要人工确认结果)。这样逐步推进,团队更容易接受。

2.3 Jenkins基础配置:老兵的选择

Jenkins是我用得最久的CI工具。说实话,它界面确实不够现代,但胜在稳定、插件生态丰富。对于嵌入式团队,我建议这样配置:

第一步:安装与初始化。 直接在服务器上装Jenkins LTS版本。我个人习惯用Docker部署,方便迁移和备份。命令很简单:

docker run -d --name jenkins \
  -p 8080:8080 -p 50000:50000 \
  -v jenkins_home:/var/jenkins_home \
  jenkins/jenkins:lts

第二步:安装必要插件。 嵌入式CI/CD最少需要这几个插件:

  • Git Plugin:拉取代码,不用多说
  • Pipeline Plugin:用流水线脚本定义构建步骤
  • Credentials Plugin:管理SSH密钥、烧录工具密码等敏感信息
  • Email Extension Plugin:构建失败时自动发邮件通知

第三步:创建Pipeline任务。 我推荐用Jenkinsfile来定义流水线,而不是在UI里点点点。为什么?因为Jenkinsfile可以跟代码一起版本管理,改流水线就像改代码一样方便。一个简单的嵌入式编译流水线长这样:

pipeline {
    agent any
    stages {
        stage('Checkout') {
            steps {
                git 'https://github.com/your-embedded-project.git'
            }
        }
        stage('Build') {
            steps {
                sh 'make clean && make all'
            }
        }
        stage('Archive') {
            steps {
                archiveArtifacts artifacts: 'build/*.bin', fingerprint: true
            }
        }
    }
    post {
        failure {
            emailext(
                subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: "请检查构建日志: ${env.BUILD_URL}",
                to: 'team@example.com'
            )
        }
    }
}

注意: Jenkins默认的工作空间路径可能包含空格或特殊字符,有些交叉编译工具链会因此报错。我曾经被这个问题坑了一下午。解决办法:在Jenkins系统配置里,把“工作空间根目录”设为一个无空格的路径,比如 /data/jenkins/workspace

2.4 GitLab CI基础配置:云原生时代的利器

如果你团队用的是GitLab,那直接用GitLab CI会更省事。它跟代码仓库深度集成,配置起来比Jenkins轻量很多。

核心概念:.gitlab-ci.yml 这个文件放在项目根目录,定义了流水线的所有步骤。GitLab会为每个分支自动运行流水线。我习惯这样组织:

stages:
  - build
  - test
  - deploy

build-job:
  stage: build
  script:
    - make clean
    - make all
  artifacts:
    paths:
      - build/*.bin
    expire_in: 1 week

test-job:
  stage: test
  script:
    - echo "运行单元测试..."
    - make test
  dependencies:
    - build-job

deploy-job:
  stage: deploy
  script:
    - echo "烧录固件到测试板..."
    - ./flash.sh build/firmware.bin
  only:
    - main
  when: manual

这里有几个要点:

  • artifacts:把构建产物(.bin文件)传递给后续阶段,避免重复编译
  • dependencies:明确指定依赖哪个job的产物
  • only: main + when: manual:只有main分支的部署步骤需要手动触发——这是为了防止不小心把未验证的固件烧到产线板上

Runner配置。 GitLab CI需要Runner来执行任务。对于嵌入式项目,我建议准备两种Runner:

Runner类型用途配置建议
共享Runner编译、静态检查、单元测试用Docker执行器,镜像里装好交叉编译工具链
特定Runner硬件在环测试、烧录用Shell执行器,直接跑在连接开发板的物理机上

一个小技巧: 在GitLab CI里,可以用 tags 关键字来指定任务跑在哪个Runner上。比如烧录任务加上 tags: [physical-runner],这样就不会误跑到共享Runner上(共享Runner一般没有物理硬件)。

2.5 嵌入式CI/CD的“避坑指南”

最后,分享几个我踩过的坑,希望能帮你少走弯路:

  • 工具链版本不一致:开发人员用GCC 10.3,CI服务器用GCC 9.2,编译出来的固件行为不一样。解决办法:用Docker镜像锁定工具链版本,所有人统一。
  • 烧录工具依赖本地环境:J-Link的驱动、USB权限、端口号,每台机器都不一样。我曾经在CI节点上折腾了两天才把J-Link驱动装好。建议用udev规则固定设备名,并在流水线脚本里加一个“检查硬件连接”的步骤。
  • 测试板资源竞争:多个流水线同时跑,都要用同一块开发板,结果互相干扰。解决办法:给每块板子分配一个唯一的标签,在流水线里用锁机制(比如GitLab CI的resource_group)来避免冲突。
  • 忽略静态分析:很多嵌入式团队只关注编译是否通过,不跑静态代码检查。结果呢?内存泄漏、空指针解引用这些bug,到了硬件测试阶段才暴露。我建议在CI流水线里强制加入MISRA C检查或Coverity扫描。

嗯,CI/CD的基础回顾就到这里。下一章我们会深入硬件在环测试的具体集成方法,到时候会用到今天讲的这些配置知识。记住:嵌入式CI/CD的核心不是工具,而是流程和纪律。工具只是帮我们把流程自动化而已。