# libopencm3: A From-Scratch Cortex-M Firmware Library Without the Vendor HAL

> libopencm3 is a GPL-3.0 firmware library for ARM Cortex-M parts, written from vendor datasheets rather than vendor code. Here is how it builds, how you use it, and where it stops being the right choice.

**libopencm3/libopencm3** — Open source ARM Cortex-M microcontroller library

- Repository: https://github.com/libopencm3/libopencm3
- Website: http://libopencm3.org/
- Stars: 3,661 · Forks: 1,133
- Language: C
- License: GPL-3.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/libopencm3-libopencm3

## What libopencm3 replaces, and who it is aimed at

Vendor HALs ship as large generated trees that you accept as a binary blob of sorts: you call the vendor's functions, and the vendor decides when the register-level behaviour changes. libopencm3 takes the opposite position. The README states the library is "written completely from scratch based on the vendor datasheets, programming manuals, and application notes", which means the headers and drivers are maintained by the project rather than generated from a chip vendor's release cycle. The supported list spans ST STM32 F0/F1/F2/F30x/F37x/F4/F7/H7, the STM32 G0/G4/L0/L1/L4 families, Atmel SAM3 and SAMD, NXP LPC1311/13/17/42/43, Stellaris LM3S, TI Tiva LM4F, EFM32 Gecko (core support only), Freescale Vybrid VF6xx, Qorvo PAC55XX, Synwit SWM050, and Nordic NRF51x/NRF52x. That is a wide net, and it is also the first warning: "at least partly supported" is the README's own phrasing. A chip appearing in that list does not mean every peripheral on it works. The audience is embedded engineers who prefer reading a reference manual over reading a HAL abstraction, and who want the same register-level code to move between an STM32F4 and an STM32L4 with modest edits.

## How the build system turns irq.json into headers and vector tables

The mechanism is visible in the top-level Makefile. A variable named IRQ_DEFN_FILES collects include/libopencm3/<target>/irq.json for every entry in TARGETS, and two derived lists are built from it: NVIC_H maps each irq.json to a matching nvic.h, and VECTOR_NVIC_C maps it to lib/<target>/vector_nvic.c. So interrupt numbers and vector table contents are generated from JSON descriptions rather than hand-written per chip. Python is a build requirement precisely because code is generated. This design has a practical consequence: adding a new part is partly a data exercise. You supply the target directory, an irq.json describing the interrupt layout, and the peripheral sources, and the build produces the headers and vector table. The default TARGETS list is long, and the README documents make TARGETS='stm32/f1 stm32/f4' as a way to build only the series you care about. That matters because a full default build compiles every supported family, which is wasted time on a machine that only ever targets one board.

## Installing libopencm3 and running a first build

There is no package manager step and no system-wide install. The README says installation means passing -I and -L flags to your own project, and it explicitly warns against installing the library into your toolchain path, because a multi-library linker can then pick the wrong version and produce hardfaults from branches into ARM code. The recommended layout is a Git submodule. Start by building the library itself from the repository root.

```bash
make
```

That compiles the default target set with the arm-none-eabi prefix. If you have an arm-elf toolchain instead, the README shows overriding the prefix, and a verbose build is available for diagnosing failures.

```bash
PREFIX=arm-elf make
make V=1
```

Before wiring the library into a project, confirm your part is actually a target. The Makefile defines TARGETS with entries such as stm32/f4, stm32/l4, sam/d and nrf/52, and the README documents a listing command.

```bash
make list-targets
```

For a first real use, the project points at two companion repositories rather than shipping an in-tree example. libopencm3-examples holds the community example collection, and libopencm3-miniblink covers a much larger set of boards but only demonstrates blinking LEDs. The README describes libopencm3-template as the template repository that uses the library as a Git submodule and calls it "the most popular method of use". If you only want to verify that your toolchain and build environment work, miniblink is the smaller starting point.

## Toolchain and floating-point settings you have to get right

The README is unusually direct about toolchains: the most heavily tested is gcc-arm-embedded, other toolchains should work but have not been nearly as well tested, and Linux-targeted toolchains such as gcc-arm-linux-gnu are "not appropriate". GCC 6 or later is required because the project uses attributes on enumerators to mark deprecations. On Windows the documented path is MSYS plus Python plus an arm-none-eabi toolchain, with an explicit PATH export that keeps Windows utilities such as find from interfering with the build. Floating-point behaviour is decided by the build rather than by you, unless you override it. M4F cores default to -mfloat-abi=hard -mfpu=fpv4-sp-d16, M7 cores default to double precision -mfloat-abi=hard -mfpu=fpv5-d16 when available and single precision -mfloat-abi=hard -mfpu=fpv5-sp-d16 otherwise, and other architectures get no FP flags, meaning traditional softfp. If you are linking against precompiled objects built with a different float ABI, that default will bite you, and the documented escape hatch is the FP_FLAGS environment variable.

```bash
FP_FLAGS="-mfloat-abi=soft" make
```

CFLAGS is appended after the build system's own flags, so it can override defaults, and the README gives -fshort-wchar as an example. The ordering is the point: your flags win.

## The API stability problem, in the project's own words

This is the limitation that decides adoption more than any missing peripheral. The README states the project is "(and presumably, always will be) a work in progress", that not all subsystems of all microcontrollers are supported, and that some parts are more complete than others. On versioning, it says that prior to 0.8.0 the API was largely in flux, that from 0.8.0 to 1.0 the project will attempt to follow semver but with "EXPECT CHANGES" in bold as old APIs are cleaned up and deprecated functions removed, and that only from 1.0 should you expect semver with functions and defines deprecated for a release before removal. The 0.8.0 tag exists as the "old stable" point before the newer code landed. There is also a workflow detail worth knowing: preview code lands in wildwest-N branches that appear and disappear, and pull requests marked merged-dev sit there until they merge to master. If your product ships firmware for years, that policy is a real cost. Pinning to a commit via submodule is the mitigation the project itself suggests, and it is the reason the submodule approach is framed as ensuring users get the right version. A second limitation is EFM32, where the README lists core support only, so you should not assume the Gecko peripheral set is available. A third is that a discontinued part like Stellaris LM3S is listed as having no replacement, which tells you the support is historical rather than growing.

## libopencm3 against the STM32Cube HAL and CMSIS

The comparison people search for is libopencm3 versus the STM32 HAL, and the difference is architectural rather than cosmetic. The STM32Cube HAL is a vendor-maintained abstraction layer generated around a specific silicon family, with a handle-based API and a configuration tool feeding it. libopencm3 exposes the peripheral registers directly through headers and small driver functions, with the register definitions traced to datasheets and programming manuals. That means less code between you and the hardware, and it also means you read the reference manual to know what a function does. CMSIS sits at a different level: it is the ARM core abstraction (core registers, intrinsics, NVIC definitions), and libopencm3's generated nvic.h and vector_nvic.c files occupy similar ground for interrupt layout, but libopencm3 also carries the peripheral drivers CMSIS does not provide. The practical split is portability versus support. libopencm3 lets you write one style of code across STM32, SAM, LPC and nRF, which no single vendor HAL does. In exchange, you give up the vendor's guarantee that the HAL will still compile against next year's silicon revision, and you accept that a peripheral you need may simply not be implemented for your series yet.

## Licence and the cost of keeping up

The repository ships COPYING.GPL3 and COPYING.LGPL3 at the top level, and the Makefile header carries the GNU Lesser General Public License, version 3 or later, while the project is listed under GPL-3.0. That combination is worth reading carefully before you vendor the library into a closed product, because the two files imply different obligations depending on which parts you use. This is not something to resolve from a README; read both files and get your own advice. On maintenance, the last push to master was on 2026-07-20, and the repository is not archived, so development is ongoing. The upgrade cost is dominated by the pre-1.0 policy described above: deprecated functions and defines are removed rather than kept indefinitely, so a library bump can require source edits in your project. The submodule pin is the control mechanism. Moving the pin forward is a deliberate act, and the README's advice to include the repository as a submodule exists precisely so that the version your users compile against is the version you chose.

## Conclusion

Adopt libopencm3 if you want a small, source-level register library for a Cortex-M part it already covers and you are comfortable reading the vendor reference manual alongside the headers. Do not adopt it if you need a vendor-supported HAL with long-term API guarantees, or if your chip is absent from the target list in the Makefile. Before committing, run make list-targets against your exact part, confirm your toolchain is arm-none-eabi and GCC 6 or newer, and check whether the peripheral you depend on is implemented for that series rather than only the core.

## FAQ

### How do I use libopencm3 in my own project?

The README says installation means passing -I and -L flags to your own project, and that the most popular method is adding the repository as a Git submodule, as done in libopencm3-template. It also warns against installing the library inside your toolchain path, since the linker can then pick the wrong version and cause hardfaults. Build the library first with make from the repository root.

### What is the difference between libopencm3 and STM32Cube?

libopencm3 is written from scratch from vendor datasheets and programming manuals and exposes peripheral registers directly across many Cortex-M families, while STM32Cube is ST's own vendor-maintained layer for STM32 parts. The README frames libopencm3 as a work in progress covering many vendors, with the trade-off that not all subsystems of all microcontrollers are supported.

### Does libopencm3 work with VS Code?

The README does not describe any editor integration or VS Code setup. It documents the build through make with an arm-none-eabi or arm-elf GCC toolchain, and the repository does contain a locm3.sublime-project file at the top level, which is the only editor-specific file listed.

## Sources

- [Issues](https://github.com/libopencm3/libopencm3/issues)
- [libopencm3/libopencm3 on GitHub](https://github.com/libopencm3/libopencm3)
- [License: GPL-3.0](https://github.com/libopencm3/libopencm3/blob/master/LICENSE)
- [Project website](http://libopencm3.org/)
- [README](https://github.com/libopencm3/libopencm3/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/libopencm3-libopencm3
