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 licence. The activation `code:` form takes precedence;
# provide it via a GitHub secret. For server-based activation, use
# `server:` + `product:` with the product code for the entitlement you
# hold. The action's `product:` input defaults to KEMDK-COM0, a
# non-commercial edition, so a production workflow must override it.
# 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}
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, and no armlm deactivate at job end
Licensing happens once, before anything expensive runs, using Arm’s own cmsis-actions/armlm action. There is deliberately no deactivate step at the end, because a per-job deactivate does not do what its name suggests. Arm’s User-based Licensing User Guide states that “Deactivating a license is an activity local to a single device” and that “Deactivation does not release the license so it can be used by another user.” For a licence server, a licence is released for another user only when it has not been renewed for a minimum of 7 days. Arm’s own ARM-software/cmsis-actions/armlm action has no deactivate input and no post-job step, and Arm’s CMSIS_6 CoreValidation workflow activates without ever deactivating.
The mechanics of what actually has to survive between jobs, and why per-job activation stops scaling, are in our Arm UBL in CI/CD guide.
2. The matrix is bounded
Two architectures × two build types = four parallel jobs. Not “every architecture × every compiler × every test profile”. The matrix is sized intentionally; per-PR coverage is not the place to test every combination. A wide matrix costs runner minutes, and if every job activates its own licence it also multiplies licence server requests.
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-actions/vcpkg@v1 is Arm’s own GitHub Action, and it installs the development environment described in vcpkg-configuration.json, handling 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 activation and renewal need outbound HTTPS from the runner. Per Arm’s User-based Licensing User Guide, activation-code licensing requires https://api.arm.com/p-software-licensing and https://arm.compliance.flexnetoperations.eu/instances, over HTTP with SSL on TCP port 443, so an allow-list built only around *.arm.com misses the second hostname. Indian corporate networks often put an outbound proxy in front of the runner host. Symptoms: armlm activate hangs or fails with a network error.
Fix: the documented routes are a Local License Server (LLS) on the reachable side, or proxy activation with a transfer file for runners that cannot meet the network requirements. Arm’s licensing documentation does not describe an HTTPS_PROXY setting for this, so treat that as a local workaround rather than the first thing to try. See the Arm partner page UBL section for CLS-vs-LLS guidance, and the Arm UBL in CI/CD guide for the exact firewall ask and the two curl reachability checks Arm publishes.
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) 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 →