# CmBacktrace: Cortex-M fault diagnosis whose output has to survive the reset

> A C library for ARM Cortex-M microcontrollers that catches the five fault types, names the cause, prints a call stack and persists the record to flash, so a crash that happened while the device was unattended is still readable after it reboots. The porting instructions reference a source directory that is not in the tree.

**armink/CmBacktrace** — Advanced fault backtrace library for ARM Cortex-M series MCU | ARM Cortex-M 系列 MCU 错误追踪库

- Repository: https://github.com/armink/CmBacktrace
- Stars: 2,183 · Forks: 737
- Language: C
- License: MIT
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/armink-cmbacktrace

## Five fault types, and a handler you install by hand

The library covers the Cortex-M fault taxonomy rather than a single condition. The supported errors are an assert and five faults: Hard Fault, Memory Management Fault, Bus Fault, Usage Fault and Debug Fault. That matters because those five carry different information in the status registers, and a library that decodes them for you is doing the work that would otherwise be manual register reading. The installation is not automatic, and the porting instructions are explicit about the parts. You add the source files to your project and add that directory to the header search path. You call an initialisation function from your project's initialisation point. You call an assert hook from your project's assert function. And then there is the optional part, which is where the sharp edge is. There is an assembly file that can be added to the project, and the instruction is that if you add it you must comment out your original project's fault handler. Two handlers for the same exception is a link-time or runtime problem that you will not find until the device faults. The instructions also cover the case where you do not add the assembly file, which requires a further step, so both paths are described. The three identifiers that matter when you read the source are the assembly file, the initialisation function and the assert function, and the port is a matter of wiring those three into your startup and error paths.

## Writing the diagnosis to flash is the whole point, and it needs another library

Read the paragraph about how the output is used and you find the design decision that makes this project different from a serial-console backtrace. The error message can be output to a console, and it can also be saved using the log function of a separate flash storage library, and the point of that is stated plainly: the last error message can still be read after the device crashes and restarts. That constraint reorders the whole problem. A fault handler that prints to a serial port is useful in the lab and useless in a device in a field, because after a reset the evidence is gone and the fault may not recur for weeks. So the diagnostic record has to be written somewhere that survives power loss, which means the handler is doing flash work inside an exception context, which has its own constraints. This library does not implement a flash filesystem. It delegates to a separate project by the same author, and the README names it and links it. That division is worth understanding before you adopt either: the backtrace library produces the record, the storage library persists it, and you need both. What ends up in the record is a specific list, and it is a good one: the function call stack, the fault diagnosis result, the stack contents, the fault registers, and the product firmware information. That last item is what lets you reconstruct what was running, which is the difference between a stack trace and an incident report.

## The device gives you addresses, addr2line gives you source

The feature description is careful about a word, and the word is need. Outputting the function call stack of the error site is described as needing cooperation from a command line tool for precise positioning. What the device can produce is a set of return addresses on the stack, and on a microcontroller without symbols loaded anywhere, an address is just a number. Turning it into a file name, a line number and a function name requires resolving those addresses against the exact executable you flashed, which is what a symbolicisation tool does. The demonstration in the README lays the workflow out in five steps and the ordering is the point. The first step is deliberately manufacturing a fault, a division by zero, in one of the example projects. The next two steps are viewing the diagnosis output and the basic call stack information. The fourth step is entering the path to the executable file in the project. The fifth is running the address resolution command against it. So the workflow leaves the device twice: once to cause the fault and read the record, and once on your workstation to make the addresses mean something. That is a real constraint on an incident workflow, because you must keep the exact binary that produced the trace, and a firmware update between the crash and the investigation invalidates every address. The library mitigates this by including firmware information in the record, which is the right mitigation, and the rest is process: archive your build artefacts, not just your source.

## It also works when nothing has gone wrong, which is the least advertised feature

Buried in the middle of the feature list is a sentence that deserves its own paragraph. The library can also be used under normal conditions to get the current function call stack. That is a different tool from a fault handler. The same machinery that captures a stack at the moment of a fault can be invoked deliberately, at a point you choose, to see what the call stack looks like while the system is healthy. For a threaded system that is genuinely useful, because the alternative to knowing a thread's stack is guessing, and for an interrupt-driven system it tells you what interrupted what. The feature list also notes that the library outputs the corresponding thread stack or the C main stack according to the situation at the time of the error, which is the detail that makes the deliberate use possible: it knows whether it is on a thread or on the main stack, and it reports the right one. Taken with the operating system support, that turns the library from a crash reporter into a small introspection tool for free. Nothing in the documentation frames it that way, and the porting section says nothing about using it outside a fault, so if that is what you need, read the source rather than the documentation.

## Bare metal and three operating systems, one of which needs code changes

Platform support is four entries, and the fourth has a condition attached. The library supports bare metal, and it supports three operating systems: an embedded real-time OS, a small legacy real-time kernel, and FreeRTOS. The FreeRTOS entry is annotated to say the source code needs to be modified, which is the only caveat in the list and deserves more attention than it gets. The others work as supplied; the fourth does not. That is a real difference in adoption cost, because a patch to a library means you now maintain a fork, and every upstream improvement arrives as a merge rather than a version bump. For an organisation standardising on FreeRTOS, the sensible approach is to keep the patch as a small as possible and to track upstream, which means budgeting for that maintenance rather than treating the library as a dependency you never look at again. The compiler story is better news: IAR, Keil and GCC are all named, so the library is not tied to a proprietary toolchain, though the documentation is written as though the proprietary ones are primary. The example projects follow the same split, with a bare-metal example, and one each for the three kernels, and the OS examples are on Cortex-M3 and M4 targets while bare metal is on M3. The core is stated to adapt to M0, M3, M4 and M7.

## Windows paths in the instructions, and a source directory that is not in the tree

The porting section is written for a particular reader, and the paths give it away. The example table lists directories with backslashes as separators, and the instructions refer to a source directory the same way. That is consistent with a user base working in an IDE on Windows, which is also consistent with the compiler support list, but it means the instructions do not read naturally to someone on a POSIX build system, and copying a backslash path into a makefile is an afternoon. There is a more substantive problem in the same section. The first porting step tells you to add all the source files under a directory named src to your project. There is no directory with that name at the top level. The listing has a directory named for the library, a directory of examples, a documentation directory and a tools directory, and the source lives under the library directory, which is also where the assembly file is. So a reader following the first step literally will look for a directory that does not exist. The steps that follow also name the assembly file, and its link points into the library directory, so the correct location is discoverable from that link, but the instruction text and the tree disagree. For a library whose adoption depends on someone spending an afternoon wiring it into a project, that is a poor first impression, and it is the kind of defect that a screenshot of the tree or a corrected path would fix entirely.

## A minor release after four years, and a badge that disagrees with itself

The release history tells you how this project is maintained, and the answer is: carefully, by one person, infrequently. The recent releases are 1.4.0 in March 2020, 1.4.1 in August 2022, and 1.5.0 in May 2026. So there was a three-year gap, then a four-year gap, and the repository is not archived with the last push on 2026-05-21, the same day as the newest tag. For a fault-handler library this cadence is defensible, because the library's job is to decode a hardware fault taxonomy that changes on the silicon's schedule rather than on anyone's roadmap, and because every release requires testing on real hardware across three compiler chains and four platforms, which is not something you do in an afternoon. The gap still has a cost for an adopter: you are integrating against a version that may be three years old, and if you also need a fix, you are writing it yourself. One small inconsistency in the header is worth a mention because it is the kind of thing that tells you the badges are hand-maintained. There is a badge labelled as counting commits since a particular version, and its underlying comparison starts from an earlier version and runs to the branch head. The label and the query disagree, so the number it shows is not the number it claims. Small, but it is a reminder that a badge in a readme is a claim about a link, not a measurement you should rely on.

## Conclusion

Adopt CmBacktrace if you ship firmware to devices you cannot attach a debugger to, because the two scenarios it is built around are exactly yours: many products in the field with no emulator attached, and faults that are real but hard to reproduce. The persisted log is the feature that justifies it, since a console-only backtrace is worth nothing on a device that rebooted an hour ago. Two things to check before you port it. Read the porting steps carefully around the optional assembly file, because adding it means commenting out the existing fault handler, and leaving both in place is the kind of mistake that only shows up on hardware. And note that resolving a call stack to a file and line is a two-stage job, since the device gives you addresses and a separate tool on your workstation turns them into source lines. If you are on FreeRTOS, budget for the source modification the README says is needed, and read the licence file yourself before shipping it in a product.

## FAQ

### What errors does CmBacktrace catch on Cortex-M?

Asserts and the five Cortex-M fault types: Hard Fault, Memory Management Fault, Bus Fault, Usage Fault and Debug Fault. The library decodes the fault status registers and reports the cause and the code location rather than leaving you to read the registers yourself.

### How do I get a Cortex-M call stack to resolve to a file and line?

The device gives you return addresses, and the README says precise positioning needs the addr2line command. The workflow is: cause the fault, read the call stack output, then run addr2line against the exact executable that was flashed. Firmware information is included in the record, which helps identify the build.

### Does CmBacktrace work on FreeRTOS without changes?

No. Of the four supported platforms, bare metal and the other two kernels work as supplied, and the FreeRTOS entry is annotated to say the source code needs to be modified, which means adopting it there means maintaining a patch.

### How is the CmBacktrace error record kept after a device resets?

The record can be printed to a console or written using the log function of a separate flash storage library by the same author, and the README states the point is that the last error message can still be read after the device crashes and restarts. You need both libraries for a persisted record.

### What is the latest CmBacktrace release?

1.5.0, published on 2026-05-21, the same day as the last push. The two before it are 1.4.1 from 2022-08-12 and 1.4.0 from 2020-03-12, so releases are infrequent and the project is active but moves slowly.

## Sources

- [armink/CmBacktrace on GitHub](https://github.com/armink/CmBacktrace)
- [Issues](https://github.com/armink/CmBacktrace/issues)
- [License: MIT](https://github.com/armink/CmBacktrace/blob/master/LICENSE)
- [README](https://github.com/armink/CmBacktrace/blob/master/README.md)
- [Releases](https://github.com/armink/CmBacktrace/releases)

---

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