The Indian product teams that ship reliably on schedule have one thing in common: every PR triggers a full firmware build and the result is in the developer’s review queue within ten minutes. Teams that ship-and-firefight typically don’t. Modern Cortex-M projects can have the first kind of engineering culture without the heavy infrastructure historical EDA toolchains required, the CMSIS-Toolbox CLI (csolution / cbuild / cpackget) and modern UBL licensing turn Cortex-M CI/CD into a normal-looking GitHub Actions / GitLab CI / Jenkins pipeline.
This is the working pattern GSAS Micro Systems gives Indian Cortex-M firmware teams in Bengaluru, Hyderabad, Chennai, Pune, Mumbai, and Delhi NCR when they ask “how do we get our firmware build into GitHub Actions cleanly?”
GSAS is Arm’s authorized partner in India for Arm Development Tools. The pipeline pattern below works on any of those tiers, and on ATfE Professional or the bundled Arm Hardware Success Kit (both sold direct by Arm).
What CMSIS-Toolbox actually gives you
The legacy Cortex-M build experience was µVision-on-Windows, a .uvprojx project file, and a click on the green “rebuild all” button. Everything about that flow assumed an interactive Windows host. That is not what a CI runner looks like.
The CMSIS-Toolbox CLI is the headless replacement:
| Tool | What it does |
|---|---|
csolution | Reads a declarative csolution.yml project file and resolves it into a build plan, which target(s), which compiler, which packs, which build types. |
cbuild | Executes the build plan that csolution generates. Produces final binaries and link maps. |
cpackget | Manages CMSIS-Pack installation, the dependency manager equivalent for Arm middleware, drivers, and example code. |
All three are open-source, cross-platform (Windows / macOS / Linux), and work with any of Arm Compiler 6, Arm Toolchain for Embedded, or arm-none-eabi-gcc. The same csolution.yml builds locally in Keil Studio (VS Code), in µVision on Windows, and headlessly in CI, there is no project-file divergence.
The minimal csolution.yml
For a simple Cortex-M4 product project, the project file looks like this:
solution:
cdefault:
compiler: AC6
packs:
- pack: ARM::CMSIS@^6.0.0
- pack: ARM::CMSIS-Compiler@^2.0.0
- pack: Keil::STM32F4xx_DFP@^3.0.0
target-types:
- type: STM32F411
device: STMicroelectronics::STM32F411RETx
build-types:
- type: Debug
optimize: debug
debug: on
- type: Release
optimize: speed
debug: off
projects:
- project: ./firmware/firmware.cproject.yml
The companion firmware.cproject.yml lists the source groups, the components, and the build options. Both files are version-controlled YAML, diff-friendly, merge-friendly, code-review-friendly. Compare with the binary-blob .uvprojx files of older Keil projects.

A working GitHub Actions pipeline
The CI pattern most Indian Cortex-M teams converge on, condensed:
name: Firmware CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
target: [STM32F411, STM32H743]
build_type: [Debug, Release]
fail-fast: false
steps:
- uses: actions/checkout@v4
# Set up CMSIS-Toolbox + supporting tools via Arm's vcpkg action.
# See https://github.com/ARM-software/cmsis-actions
- name: Set up CMSIS-Toolbox via vcpkg
uses: ARM-software/cmsis-actions/vcpkg@v1
with:
config: ./vcpkg-configuration.json
# Activate the UBL seat. The activation `code:` form takes precedence;
# provide it via a GitHub secret. For server-based activation, use
# `server:` + `product:` (with a real Arm product code such as
# KEMDK-COM0 is the documented default, see Arm's cmsis-actions README.
# See ARM-software/cmsis-actions README for the documented inputs.
- name: Activate Arm license
uses: ARM-software/cmsis-actions/armlm@v1
with:
code: ${{ secrets.ARM_UBL_ACTIVATION_CODE }}
- name: Install CMSIS packs
run: |
cpackget update-index
cpackget add -f packs.txt
- name: Build
run: |
csolution convert -s firmware.csolution.yml \
-c "${{ matrix.target }}+${{ matrix.build_type }}" \
-o build
cbuild build/firmware.${{ matrix.target }}+${{ matrix.build_type }}.cprj
- name: Run unit tests on FVP
if: matrix.build_type == 'Debug'
run: |
FVP_MPS2_Cortex-M4 -a build/firmware.axf \
--stat -C cpu.semihosting-enable=1
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: firmware-${{ matrix.target }}-${{ matrix.build_type }}
path: build/firmware.{axf,bin,map}
- name: Release UBL seat
if: always()
run: armlm deactivate
This pipeline runs four parallel jobs (2 targets × 2 build types) on every PR, optionally smoke-tests the Debug builds on a Fixed Virtual Platform, and uploads the build artefacts.
Four things this pipeline gets right
1. armlm activate at job start, armlm deactivate at job end
The most important habit. UBL seats are short-lived in CI, the bot claims a seat at the top of the job, releases it at the bottom (the if: always() ensures release happens even on build failure). Without explicit deactivate, you can leak seats during failed jobs and over time exhaust your concurrent-seat budget. With explicit deactivate, your seat budget tracks actual concurrent build activity, not historical job failures.
2. The matrix is bounded
Two architectures × two build types = four parallel jobs. Not “every architecture × every compiler × every test profile”, that’s how teams discover their UBL seat budget vanished mid-week. The matrix is sized intentionally; per-PR coverage is not the place to test every combination.
3. Pack installation is idempotent
cpackget update-index followed by cpackget add -f packs.txt is a no-op when packs are already installed. The CI runner caches ~/.cache/cmsis-toolbox/ between runs (configure via actions/cache); pack installation runs in seconds on warm caches. For new runners or post-cache-flush runs, expect 60-90 seconds for pack download.
4. The CMSIS-Toolbox is installed via the official action
ARM-software/cmsis-toolbox-action@v2 is Arm’s official GitHub Action, it handles version pinning, platform-specific binary download, and PATH setup. No bash-glue maintenance.
The four firewall + cache gotchas Indian teams hit
Gotcha 1: *.arm.com not reachable from the CI runner
UBL’s CLS (Cloud License Server) requires outbound HTTPS to *.arm.com. Indian corporate networks often have an outbound proxy that strips this on the runner host. Symptoms: armlm activate hangs or fails with a network error.
Fix: configure the proxy in the runner image (HTTPS_PROXY environment variable) OR move to Local License Server (LLS) if outbound *.arm.com is forbidden by network policy. See the Arm partner page UBL section for CLS-vs-LLS guidance.
Gotcha 2: UBL credentials in repo (NOT acceptable)
UBL activation codes are sensitive. They go in GitHub Actions secrets (secrets.ARM_UBL_ACTIVATION_CODE), never in the repo’s source files, never in commit history, never in logs. The pipeline above demonstrates the right pattern: secret → environment variable → tool. Audit your existing CI YAML; if your activation code is hardcoded in a workflow file, rotate the credential immediately and move it to secrets.
Gotcha 3: Pack cache invalidation across runner-image refreshes
GitHub-hosted runners get fresh images frequently; the ~/.cache/cmsis-toolbox/ cache disappears with the image. Use actions/cache@v4 keyed on packs.txt content hash to persist the pack cache across runner instances. Indian teams that skip this step pay 60-90 seconds of pack-download time on every CI run, which adds up.
Gotcha 4: Bashisms in csolution/cbuild invocation
The csolution and cbuild CLI binaries don’t expand shell globs themselves. If you write cbuild build/firmware*.cprj the shell expands the glob; if no file matches, you get a runtime error rather than a build failure. Use explicit filenames or wrap with find if you need glob behaviour.
Optional patterns Indian teams add later
Parallel matrix with selective skipping
Skip the full matrix on PRs that touch only documentation or non-firmware files. Use GitHub Actions’ paths filter:
on:
pull_request:
branches: [main]
paths-ignore:
- 'docs/**'
- '*.md'
FVP-based regression testing
For larger Indian product teams, the natural next step is regression testing on FVPs, see Fixed Virtual Platforms / Fast Models for the licensing model and the three-layer “FVP → physical → HIL” CI pattern Indian teams settle into.
Static analysis gates
Combine the build pipeline with Perforce Helix QAC or Perforce Klocwork for MISRA-conformance gates as a pre-merge check. The QAC analysis runs as a parallel CI job on the same matrix; failed analysis blocks the merge. Both Helix QAC and Klocwork have first-class GitHub Actions integration today.
Per-PR firmware preview deploys
For consumer-facing Indian products, deploy a built firmware to a hardware-in-the-loop test bench tied to a specific PR, the developer sees the firmware running on actual hardware as part of code review. This is a meaningful step up from FVP-based smoke tests; not every team needs it.
What about GitLab CI / Jenkins / Azure DevOps?
The pattern translates directly. The pipeline shape (CMSIS-Toolbox install → UBL activate → packs → build → test → deactivate) is the same; only the YAML / Groovy / pipeline-DSL syntax changes. GSAS has working examples for all four CI platforms; talk to us if you’re on something other than GitHub Actions.
When to talk to GSAS
- Your Indian team is moving from a click-driven µVision build to a CI-driven build for the first time.
- Your CI is timing out on
armlm activateand you suspect a firewall or proxy issue. - You’re sizing a UBL seat count for a new CI matrix, see the seat-calculation guide.
- You need air-gapped CI (LLS deployment) for a defence / aerospace programme.
- You want to layer Helix QAC or Klocwork static analysis into the pipeline.
Talk to GSAS about your Cortex-M CI/CD setup →
GSAS Micro Systems is Arm’s authorized engineering partner in India for the full Arm tools family, across Bengaluru, Hyderabad, Chennai, Pune, Mumbai, Delhi NCR, and Visakhapatnam.
Interested in Arm tools?
Talk to our application engineers for personalized tool recommendations.
More from Arm
View all →