Practical Guide to Bare Metal C++
Posted by ibobev 4 days ago
Comments
Comment by myrmidon 15 hours ago
An interesting perspective. Could turn it around as "everything you can do in C++ you can do in C with a lot less language complexity".
My personal experience with low-level embedded code is that C++ is rarely all that helpful, tends to bait you into abstractions that don't really help, brings additional linker/compiler/toolchain complexity and often needs significant extra work because you can't really leverage it without building C++ abstractions over provided C-apis/register definitions.
Would not generally recommend.
Comment by jonathrg 15 hours ago
Comment by kevin_thibedeau 10 hours ago
Comment by kryptiskt 13 hours ago
No, you can't, C is lacking a lot that C++ brings to the table. C++ has abstraction capabilities with generic programming and, dare I say it, OO that C has no substitute for. C++ has compile-time computation facilities that C has no substitute for.
Comment by embeng4096 12 hours ago
The extent of my experience has been being able to replace functions like convert_uint32_to_float and convert_uint32_to_int32 by using templates to something like convert_uint32<float>(input_value), and I didn't feel like I really got much value out of that.
My team has also been using CRTP for static polymorphism, but I also feel like I haven't gotten much more value out of having e.g. a Thread base class and a derived class from that that implements a task function versus just writing a task function and passing it xTaskCreate (FreeRTOS) or tx_thread_create (ThreadX).
Typed compile-time computation is nice, though, good point. constexpr and such versus untyped #define macros.
Comment by csb6 11 hours ago
Comment by rkagerer 2 hours ago
I still use modern languages on a regular basis, but when I drop back to more basic languages there are only a few ergonomics that I truly miss (eg. generics).
Comment by slaymaker1907 12 hours ago
Comment by CyberDildonics 9 hours ago
Comment by myrmidon 12 hours ago
All those abstraction capabilities can be a big detriment to any project, because they always come with a cost, and runtime is far from the only concern.
Specifically in an embedded project, toolchain complications and memory use (both RAM and code) are potentially much bigger concerns than for Desktop applications, and your selection of programmers is more limited as well; might be much more feasible to lock your developers onto acceptable C coding standards than to make e.g. "template metaprogramming" a necessary prerequisite for your codebase and then having to teach your applicants electrical engineering.
Both object oriented programming and compile time computation is doable for a C codebase, just needs more boilerplate and maybe a code-generator step in your build, respectively. But that might well be an advantage, discouraging frivolous use of complexity that you don't actually need, and that introduces hidden costs (understanding, ease of change, compile time) elsewhere.
Comment by bigfishrunning 8 hours ago
The substitute for this is that C is insanely easy to generate. Do your compile time computation in your build system.
OO is also pretty trivial in C -- the Linux kernel is a great example of a program in C that is very Object Oriented.
Comment by bsoles 2 hours ago
Comment by myrmidon 1 hour ago
In practice: Is it worth wrapping all your vendor-provided hardware abstraction in classes that you write yourself? Designing something like that to be "complete" (i.e. allow full access to all hardware functionality like IRQ/DMA setup) and to still stay hardware-independent is often borderline impossible anyway, and any attempt is invariably going to make your codebase more difficult to understand.
Comment by Conscat 10 hours ago
C can't parameterize an optimal layout fixed-length bitset with zero overhead, nor can it pragmatically union error sets at scale.
Comment by chris_money202 3 hours ago
Comment by g947o 15 hours ago
Comment by myrmidon 15 hours ago
Generally, the more you deviate from your vendors "happy path", the more busy work/unexpected difficulties you will run into, and a solid grasp of how exactly architecture and toolchain work might become necessary (while staying on the "happy path" allows you to stay blissfully unaware).
Comment by technothrasher 15 hours ago
Comment by bsoles 2 hours ago
Comment by patchnull 13 hours ago
The LL API is basically thin inline wrappers around register writes, so you still get the CubeMX-generated init code but without the HAL abstraction tax at runtime.
Comment by embeng4096 14 hours ago
I've definitely learned a lot, and I like the portability of CMake for cross-platform use (our team uses all 3 of Windows, Mac, and Linux). My experience sounds much like yours: there've been a lot of times where using the vendor's flavor of Eclipse-based IDE (STM32CubeIDE, Renesas e2studio, etc) would have saved us a lot of discovered work, or extra work adapting the "happy path" to CMake and/or C++.
Using C++ and/or CMake is fine when it's part of the happy path and for simpler things e.g. STM32CubeMX-generated CMake project + simple usage of HAL. For more complex things like including MCUboot or SBSFU, etc, it's forced me to really dig in. Or even just including FreeRTOS/ThreadX, we've created abstractions like a Thread class on top -- sometimes it's nice and convenient, others it feels like unnecessary complexity, but maybe I'm just not used to C++ yet.
One clear, simple example is needing to figure out CMake and Ninja install. In an Eclipse-based IDE, you install the platform and everything Just Works(tm). I eventually landed on using scoop to install CMake and Ninja, which was an easy solution and didn't require editing my PATH, etc, but that wasn't the first thing I tried.
Comment by adrian_b 8 hours ago
Ninja is supposed to be faster for compiling very big software projects, but I have never seen an embedded software project that is well organized and which is not compiled in a few seconds on a powerful development computer with many cores, so I do not see the benefit of Ninja for such projects.
All Eclipse-based IDEs that I have ever seen are extremely slow for anything, both for editing and for building a project and they make the simplest project management operations extremely complicated. Even Visual Code Studio is much faster and more convenient than using Eclipse-based IDEs. Other solutions can be much faster.
While the example programs provided for STM32 MCUs are extremely useful for starting a project for them, I believe that using the methods of project building provided by the vendor results in a waste of time. I have always obtained better results and faster development by building a GNU toolchain (e.g. binutils,gcc,gdb, some libc) from scratch and by using universal GNU makefiles, which work for any CPU target and for any software project, with the customization of a few Make variables. I have written once a set of GNU Makefiles, according to its manual, around 1998, and I have never had to change them since then, regardless what platform I had as a target. For any new platform, there is just a small set of variables that must be changed by generating one per-platform included file, with things like the names of the compilers and other tools that must be invoked and their command-line options.
For new projects, there is one very small file that must be generated for each binary file that must be built, which must contain the type of the file (e.g. executable, static library, shared library) and a list with one or more directories where source files should be searched. No changes are needed when source files are created, deleted, moved or renamed, and dependencies are identified automatically. I am always astonished when I see how many totally unnecessary complications exist in the majority of the project building configurations that I have ever seen provided by the vendors or in most open-source projects.
Comment by bigfishrunning 8 hours ago
Honestly, I can't think of a single job for which C++ is the best tool.
Comment by embeng4096 14 hours ago
Comment by maldev 9 hours ago
Comment by nly 8 hours ago
Last time I did embedded work this was basically all that was required.
Comment by nulltrace 6 hours ago
Also -nostdlib means no global constructors run, so static objects with nontrivial ctors need you to call __libc_init_array yourself.
Comment by daemin 3 hours ago
Comment by chris_money202 3 hours ago
Comment by daemin 2 hours ago
Comment by pjmlp 16 hours ago
Comment by tialaramex 15 hours ago
Suppose Doodads can be constructed from a Foozle either with the Foozle Resigned or with the Foozle Submitted. Using tagged dispatch we make Resigned and Submitted types and the Doodad has two specialised constructors for the two types even though substantively we pass only the Foozle in both cases.
In a language like Rust all the constructors have names, it's obvious what Vec::with_capacity does while you will still see C++ programmers who thought constructing a std::vector with a single integer parameter does the same because it's just a constructor and you'd need to memorize what happens.
Comment by quuxplusone 15 hours ago
For more on disambiguation tags, see https://quuxplusone.github.io/blog/2025/12/03/tag-types/
and https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p39...
Comment by pjmlp 14 hours ago
Comment by randusername 14 hours ago
idk man, obviously I don't know much since I don't have my own online book, but templates would not be at the start of my list when selling C++ for bare-metal.
unit suffixes, namespaces, RAII, constexpr, OOP (interfaces mostly), and I like a lot of the STL in avoiding inscrutable "raw loops".
I like the idea of templates, but it feels like a different and specialized skillset. If you are considering C++ from C, why not ease into it?
Comment by bluGill 14 hours ago
Comment by lkjdsklf 13 hours ago
Just very basic type substitution is one of the most useful uses of templates and is useful in pretty much all software
They’re also useful when you can’t use virtual dispatch. Concepts help a lot in making that tolerable.
Sure they can get stupid complicated and ugly as hell, but you don’t have to do that. Even their basic form is very useful
That said, RAII is probably the must useful thing
Comment by saltmate 15 hours ago
Comment by birdsongs 15 hours ago
I've seen a lot of new people come into my team as juniors, or regular C/C++ engineers that convert to embedded systems. There is a real lack of good, concise resources for them, and the best result I've had is just mentoring them and teaching as we go.
You could look for an intro to embedded systems resource. Or just get a dev kit for something. Go different than the standard Pi or Arduino. Try and get something like a STM32G0 dev kit working and blinking its lights. It's less polished, but you'll have to touch more things and learn more.
If you want, core areas I would suggest to research are the very low level operations of a processor:
* How does the stack pointer work? (What happens to this during function calls?
* How do parameters get passed to functions by value, by reference? What happens when you pass a C++ class to a function by value? What is a deep vs shallow copy of a C++ object, and how does that work when you don't have an OS or MMU?
* Where is heap memory stored? Why do we have a heap and a stack? How do these work in the absence of an OS?
* The Program Counter? (PC register). What happens to this as program execution progresses?
* What happens when a processor boots, how does it start receiving instructions? (This is vague, you could read the datasheet for something like the STM32G0 microcontroller, or the general Arm Cortex M0 core.)
* How are data/instructions loaded from disk to memory to cache to register? What are these divisions and why do we have them?
* Basic assembly language, you should know how loads and stores work, basic arithmetic, checks/tests/comparisons, jump operations.
* How do interrupts work, what's an ISR, IRQ, IVT? How do peripherals like UART, I2C (also what are these?), handle incoming data when you have a main execution thread already running on a single core processor?
Some of this may be stuff you already know, or seem rudimentary and not exactly relevant, but they are things that have to be rock solid before you start thinking about how compilers for differently languages, like C++, create machine code that runs on a processor without an OS.
Assembly is often overlooked, but a critical skill here. It's really not that bad. Often when working with C++ (or Rust) on embedded systems, if I'm unsure of something, my first step is to decompile and dump the assembly to investigate, or step through the assembly with GDB via a JTAG probe on the target processor if the target was too small to hold all the debug symbols (very common).
Anyways, this may have been more than you were asking for. Just me typing out thoughts over my afternoon coffee.
Comment by randusername 14 hours ago
The former is probably more what you are looking for.
Comment by skydhash 15 hours ago
- Applied Embedded Electronics: Design Essentials for Robust Systems by J. Twomey. It goes over the whole process making a device and what knowledge would be required for each. Making Embedded Systems, 2nd Edition by E. White is a nice complement.
- Embedded System Interfacing by M. Wolf describes the signals and protocols behind everything. It's not necessary as a book, but can help understand the datasheets and standards
- But you want to start with something like Computer Architecture by C. Fox or Write Great Code - Volume 1 - Understanding the Machine, 2nd Edition by R. Hyde. There are better textbooks out there, but those are nice introductions to the binary world.
The gist is that you don't have a lot of memory and CPU power (If you do, adapting Linux is a more practical option unless it's not suited for another reason). So all the nice abstractions provided by an OS is a no go, so you need to take care of the hardware and those are really finicky,
Comment by mdocc 14 hours ago
Using C++ iterator interface to fix the main problem of a standard ring buffer of non-contiguous regions is a cute idea, but I like to use a "bip buffer"[1] instead which actually always gives you real contiguous regions so you can pass the pointers to things like a dma engine.
[1] https://ferrous-systems.com/blog/lock-free-ring-buffer/
The tradeoff is that you have in the worse case only half the buffer available - the ring buffer essentially becomes a kind of double buffer where you periodically switch between writing/reading at the end or the beginning of the storage.
Comment by VorpalWay 16 hours ago
Comment by NooneAtAll3 15 hours ago
Comment by m00dy 15 hours ago
Comment by soci 10 hours ago
Comment by bigstrat2003 10 hours ago
Comment by menaerus 15 hours ago
Comment by superxpro12 14 hours ago
Comment by menaerus 13 hours ago
Comment by bigstrat2003 10 hours ago
Comment by menaerus 9 hours ago
I didn't say that GCC version is relevant to bare metal definition, I said it's 15 years old. And when you try to draw conclusions, and derive strong opinion, based on the codegen output but you're using 15 year old toolchain, and btw pretty shitty CPU core, something isn't quite right. This article is just a good display of never ending cargo-cult programming myths.