This is a practical setup I use to build Keil uVision projects automatically with GitLab CI/CD.
The goal is simple: every commit should produce a reproducible firmware build, with logs and binaries attached as artifacts.
If you work on embedded systems, this gives you fast feedback on broken builds and avoids “works on my machine” problems.
Prerequisites#
Before creating the pipeline, ensure:
- A Windows GitLab Runner is available and tagged (for example
windows). - Keil MDK / uVision is installed on the runner.
- The runner service account can access
UV4.exe. - Your project can already build from uVision command line.
Keil command-line reference:
uVision command-line options
Why this pipeline layout#
The original approach works, but repeating the same PowerShell logic across Debug and Release jobs becomes hard to maintain.
This version uses:
- one reusable template job (
.keil_build_template) - one variable (
UVISION_TARGET) to switch target - explicit artifact collection
- readable mapping of Keil exit codes
Example .gitlab-ci.yml#
stages:
- build
variables:
GIT_SUBMODULE_STRATEGY: recursive
UV4_PATH: "C:\\Keil_v5\\UV4\\UV4.exe"
UV_PROJ: ".\\projectname.uvprojx"
BUILD_LOG: ".\\Objects\\build.log"
.keil_build_template:
stage: build
tags:
- windows
script:
- |
& "$env:UV4_PATH" -crb "$env:UV_PROJ" -t"$env:UVISION_TARGET" -o "$env:BUILD_LOG" -sg -j0 | Out-Null
$code = $LASTEXITCODE
switch ($code) {
0 { Write-Host "No errors or warnings"; exit 0 }
1 { Write-Host "Warnings only"; exit 0 }
2 { Write-Host "Build errors"; exit 2 }
3 { Write-Host "Fatal build errors"; exit 3 }
11 { Write-Host "Cannot open project file for writing"; exit 11 }
12 { Write-Host "Device not found in database"; exit 12 }
13 { Write-Host "Error writing project file"; exit 13 }
15 { Write-Host "Error reading import XML file"; exit 15 }
20 { Write-Host "Project conversion error"; exit 20 }
default {
Write-Host "Unknown uVision exit code: $code"
exit $code
}
}
after_script:
- if (Test-Path "$env:BUILD_LOG") { Get-Content "$env:BUILD_LOG" -Tail 20 }
artifacts:
when: always
expire_in: 14 days
paths:
- Objects/*.hex
- Objects/*.axf
- Objects/*.htm
- Objects/*.log
- Listings/*.map
build_debug:
extends: .keil_build_template
variables:
UVISION_TARGET: "Debug"
build_release:
extends: .keil_build_template
variables:
UVISION_TARGET: "Release"Notes on exit-code policy#
In this setup, exit code 1 (“warnings only”) is treated as success.
That is intentional when warnings are acceptable in your process.
If you want a stricter gate, change:
1 { Write-Host "Warnings only"; exit 0 }
to:
1 { Write-Host "Warnings only"; exit 1 }
and the job will fail on warnings.
Common issues on self-hosted Windows runners#
- Runner cannot find
UV4.exe: check installation path and permissions for the runner service account. - License or pack problems: verify the runner machine has a valid MDK setup and required device packs.
- Project path issues: use quoted paths and avoid assumptions about current working directory.
- Artifacts missing: confirm output folder names (
Objects,Listings) match your project settings.
Suggested next improvements#
Once the basic build is stable, useful extensions are:
- add a
teststage for host-side unit tests (if available) - publish firmware binaries with semantic version names
- add a release job triggered by tags
- archive build metadata (commit, toolchain version, timestamp)
This keeps the pipeline simple while making embedded firmware builds more traceable and repeatable.
