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的核心不是工具,而是流程和纪律。工具只是帮我们把流程自动化而已。