C++ code generator for embedded systems
This has been a fun project, but I will probably discontinue it, for the following reasons:
- I have not been using it myself in a debugging scenario, mostly because the debugging tool I have been using lately has a very good integration of SVD files.
- When it comes to peripheral device drivers, my current opinion is that they should be based on C++ templates, where the MCU and the peripheral instance are template parameters. The code currently generated by ecg does not really help with that.
The latter point probably means that I do want to generate C++ source code files from SVD-files, but they should be different from those currently generated by ecg. I will probably work on that in a separate repository: stay tuned!
At the moment, ecg
's only ability is to generate C++ header files from
CMSIS System View Description file. It may or may not grow
and at some point better reflect the possible interpretation of its name as a general acronym for "C++ code generator
for embedded systems".
Python 3 is required to run ecg
, as well as the package xmltodict
.
If you are lucky enough to run virtualenv
and virtualenvwrapper
, getting this could be as simple as:
$ mkvirtualenv ecg_venv -p /usr/bin/python3 && pip install xmltodict
ecg_venv
is of course just an arbitrary name. Just pick whatever you like.
Also, a compiler that supports C++17 is required to compile the generated code.
The syntax for ecg
is as illustrated below.
$ python ecg.py --help
usage: ecg.py [-h] -o OUTPUT [-n NAMESPACE] [-c COMPILER] svd_file
Generate a C++ header file from an ARM Cortex SVD file.
positional arguments:
svd_file SVD file
optional arguments:
-h, --help show this help message and exit
-o OUTPUT, --output OUTPUT
C++ header file name (default: None)
-n NAMESPACE, --namespace NAMESPACE
C++ namespace (default: mcu_support)
-c COMPILER, --compiler COMPILER
C++ header file name (default: None)
The compiler argument is optional. If provided, the generated header file will immediately be compiled, as a validation
test. Note: since the header file contains many offset static assertions, only a target compiler makes sense for the
compilation test. Also, it should be noted that the compiler option should only be used for validation purposes, not
for producing an object file that would be linked in a target application. For obvious reasons, ecg
has no way to
know what compilation flags should apply for a hypothetical target application, and it only uses the flags that are
necessary for code and offset validation. The source file (which is created on the fly and includes the header file to
allow compilation) and the object file are stored in a temporary directory provided by the operating system.
A test script is provided:
$ time python test.py
[...]
svd/STM32H7/STM32H743x.svd -> generated/STM32H7/STM32H743x/mcu.h
STM32H743x specified as little-endian
Generating code for 128 peripherals...
generated 57 files
real 0m12,473s
user 0m12,251s
sys 0m0,220s
You do not have to time
the command of course. You should however be aware of the fact the tests take a little
while to run. They generate C++ header files for pretty much all current STMicroelectronics' STM32 MCUs for which
SVD files are available. Note that this repository is in no way affiliated with STMicroelectronics. I just happen
to work quite a lot with STM32 MCUs these days. Also note that so far, no effort has been put into optimizing ecg
for performance.
The SVD files used by the tests can be found under svd, and the resulting header files are generated under
a generated
directory. All the generated files are called mcu.h
, and each file is stored in its own directory. You
are of course free to decide on your own structure and naming conventions when you use ecg
.
The test script can also be ordered to test-compile every generated file:
$ time python test.py -c /opt/gcc-arm-none-eabi/bin/arm-none-eabi-gcc
[...]
svd/STM32H7/STM32H743x.svd -> generated/STM32H7/STM32H743x/mcu.h
STM32H743x specified as little-endian
Generating code for 128 peripherals...
Running: /opt/gcc-arm-none-eabi/bin/arm-none-eabi-gcc -std=c++17 -o /tmp/test.o -c /tmp/test.cpp -I generated/STM32H7/STM32H743x
Compilation successful
generated 57 files
real 0m28,467s
user 0m27,964s
sys 0m0,493s
The compilation has many static_assert
-invocations that check the offset of every register in every peripheral. In
fact, these unit tests where able to automatically detect a number of errors in ... ST's SVD files! These were
corrected in commit 4c7f67e761,
and reported to ST.
ecg
has the ambition to handle any SVD -file as its
input. SVD implies CMSIS, which implies ARM Cortex.
However, I have so far only used ecg
for Cortex-M MCUs. Whether generating C++-header files for other Cortex
variants would even be interesting is unclear at the time of writing. Please
report any issue that you may encounter in your own use cases.
Also, ecg
chooses to generate bit-fields to conveniently map registers (see
Frequently asked questions). While this indeed is convenient, it unfortunately forces
the code to make an assumption about the processor's endianness. For Cortex-M, this
does not seem to be a problem.
The SVD files are in fact supposed to include endianness information.
But unfortunately, they sometimes do not.
To handle this issue, ecg
's strategy is the following:
- Only support for little-endian MCUs is provided until further notice.
- When the SVD file includes endianness information,
ecg
asserts that little-endian is specified. - When the SVD file does not include endianness information,
ecg
assumes that little-endian applies.ecg
also provides a compile time endianness assertion.
Also, at the time of writing, only one of the generated files has been tested in a debugger on target. If you use
ecg
, please do not blindly rely on the generated code, check it!
Finally, while ecg
seems to work on quite many STM32 SVD files, it has not been tested on other vendors' SVD files,
or other processor families. I am not sure how strict the SVD specification and its implementations are. I would not be
surprised if ecg
needed a few adjustments for it to work with other vendors or processor families. Again, please
report any issue that you may find.
Travis automatically runs the tests mentioned above for every commit, i.e.
successful code generation and successful compilation of the generated code, including many
static_assert
-invocations that check the offset of every register in every peripheral in the ARM ABI, as well as
compile time endianness check.
Also, the generated files can only be as good as the source SVD files. While ecg
has been seen to help detect
some errors in SVD files (offset error that break alignment, duplicated register names, invalid field names), many
potential SVD file errors can only be diagnosed by debugging on the target.
If we except the runtime checking function, which would typically not be linked in an application deployed in the
field, the only cost for the code generated by ecg
from an SVD file is at most one peripheral address per peripheral.
Typical numbers of peripherals in the test SVD files are 70 to 140. For 140, the maximum cost would be 560 bytes (a peripheral address is 4 byte-wide on an STM32 MCU), typically in flash memory. I say maximum cost, because in my experience, compilers are nowadays quite good at detecting multiple copies of a constant in the text segment, and removing redundancies. Under such an assumption, the cost for a peripheral that is used in the application would be zero.
We should be in the 0,1% range of available flash memory, but if that ever became a problem for a particular
application, my recommendation would be to make a copy of the header file for that particular application, and to use a
compile time flag to separate the used peripherals from the unused ones in the mcu
structure. You should probably keep
the used peripherals in alphabetic order, for your own convenience.
My customers and I mostly use gcc
for embedded development, which gives access to both C and C++. I believe other
compilers for currently available micro-controllers also give access to both C and C++. C++ being a superset of C for
all practical purposes, which adds some useful features (this is of course an understatement), I do not see any reason
to limit myself or my customers. Furthermore, I believe that C++, if used rationally, can help to vastly improve
embedded code bases.
For those reasons, I do not intend to spend time on making the code generated by ecg
compatible with the C-language.
Why would we need new header files? Isn't that redundant with CMSIS and the libraries provided by micro-controller vendors?
Yes, it is redundant.
But let me tell you a story. Ten years ago, I worked with a 16-bit micro-controller that had quite many peripherals.
They were memory-mapped. In the source code, we had a large C-struct
that matched the whole memory map. It had two
benefits:
- We could inspect and change the contents of all peripheral registers in a regular debugger variable view.
- Access to the peripheral registers from our device drivers was obvious and straightforward.
Today, when I work with one of ST's STM32 MCUs, if I want to inspect or update some peripheral registers (there are
MANY of them in a modern MCU), I typically use a peripheral view plugin in my IDE. While that mostly works (but
can for instance get broken by the latest IDE upgrade), it unnecessarily increases my dependency on my IDE. If that
solution somehow breaks, I have to switch to plan B: I look for a C-struct
in the vendor's library that corresponds
to the relevant peripheral, I look up the peripheral's base address in the micro-controller manual (routinely more
than 1500 pages) or in the code, and I use some gdb
-cast tricks in a variable view interface to display the
registers I need to inspect/update. This is of course not particularly efficient, but ST's support libraries, as far
as I know, do not provide a better solution. I do not know whether other vendors do, but the fact that part of the
library files are produced by ARM, while others are produced by MCU vendors is an obstacle for a centralized solution.
So while a new header file that maps all the MCU peripherals is redundant with the source code already provided by the vendor, it also fulfils a need that is not fulfilled today.
It should be noted that given the arguments above, fulfilment of the
CMSIS (Cortex Microcontroller Software Interface
Standard) is not a design goal for the code generated by ecg
.
For instance, the struct
that corresponds to the system control block peripheral starts in this way:
/**
* @brief System control block
*
* groupName: SCB
* baseAddress: 0xE000ED00
* addressBlock:
* offset: 0x0
* size: 0x41
* usage: registers
*/
struct SCB {
/**
* @brief CPUID base register
*
* displayName: CPUID
* addressOffset: 0x0
* size: 0x20
* access: read-only
* resetValue: 0x410FC241
*/
const struct CPUID {
uint32_t Revision: 4; /**< Revision number */
uint32_t PartNo: 12; /**< Part number of the processor */
uint32_t Constant: 4; /**< Reads as 0xF */
uint32_t Variant: 4; /**< Variant number */
uint32_t Implementer: 8; /**< Implementer code */
} cpuid_;
// ...
};
Of course read-only registers and fields are declared as const (write-only can unfortunately not be enforced in C++, or C, for that matter).
One design goal is to integrally transfer the information contained in the SVD files to the generated header files, either in the form of data fields, or in the form of comments in the generated code, i.e. no information should be lost in the transformation.
This is true. According to the C++-standard, packing and endianness are implementation defined.
However:
- Since we want to map hardware registers for a specific MCU, portability across multiple processors is irrelevant.
- While the C++ standard does not provide any guarantee, toolchains that generate code for ARM targets are expected to fulfill the Procedure Call Standard for the Arm Architecture (aka AAPCS), an ABI (Application Binary Interface).
In fact, as an example among others, the Application Program Status Register (APSR) is mapped in the following way in core_cm7.h, an official CMSIS core file from ARM:
/**
\brief Union type to access the Application Program Status Register (APSR).
*/
typedef union
{
struct
{
uint32_t _reserved0:16; /*!< bit: 0..15 Reserved */
uint32_t GE:4; /*!< bit: 16..19 Greater than or Equal flags */
uint32_t _reserved1:7; /*!< bit: 20..26 Reserved */
uint32_t Q:1; /*!< bit: 27 Saturation condition flag */
uint32_t V:1; /*!< bit: 28 Overflow condition code flag */
uint32_t C:1; /*!< bit: 29 Carry condition code flag */
uint32_t Z:1; /*!< bit: 30 Zero condition code flag */
uint32_t N:1; /*!< bit: 31 Negative condition code flag */
} b; /*!< Structure used for bit access */
uint32_t w; /*!< Type used for word access */
} APSR_Type;
Looks familiar?
It certainly looks like some core ARM files for Cortex-M assume that ARM toolchains do provide guarantees on how they lay out bit-fields!
So how can they be so sure?
The AAPCS has quite a long section on bit-fields. While that section feels like an unnecessarily dry read, with hardly a single example, it seems that for simple cases, all is well. If, for instance, all the bit-fields in a structure are declared with the same type, and if they all fit in that type, the AAPCS gives all the guarantess that we need. In particular:
- A sequence of bit-fields is laid out in the order declared. I.e. for little-endian, the first declared bit will be
the least significant if the memory occupied by a
struct
is interpreted as an integer. - There will be no padding between the bit-fields.
This means that if our toolchain fulfils the AAPCS, the two
struct
-examples seen so far map the corresponding hardware registers as expected.
But how does ARM know that all toolchains fulfil the AAPCS? While it is difficult to answer this question in general, let's have a look at two interesting cases:
- ARM's official compiler documentation explicitly describes a bit-field layout that fulfils our AAPCS interpretation.
- gcc has the option:
-mabi=name
: generate code for the specified ABI. Permissible values are: ‘apcs-gnu’, ‘atpcs’, ‘aapcs’, ‘aapcs-linux’ and ‘iwmmxt’.
The GNU Arm Embedded Toolchain has the following default option:
$ arm-none-eabi-gcc -Q --help=target
The following options are target specific:
-mabi= aapcs
[...]
This also looks as expected.
In summary, using bit-fields to map Cortex-M ARM registers seems reasonable.
While endianness is selectable in silicon for Cortex-M, it would seem that
core_cm7.h (and other CMSIS
Cortex-M header files) assumes a little endian-configuration (according to the
AAPCS, the bit-fields are laid out in the order declared, and the
first field in the struct
above is always assumed to map bits 0 to 15, i.e. the least significant bits). That
implicit assumption is quite confusing, seems a little dangerous, and
has been reported to ARM as an issue. The approach taken by ecg
is to assert endianness at compile time:
#ifdef __GNUC__
#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__
#error "Unsupported byte order"
#endif
#else
/*
* Byte order check is necessary because the bit-fields used in this file assume little-endianness. If you use a
* compiler for which the compile time check is not implemented, you could implement it and send a pull request to
* the ecg repository, or you could rely on the runtime check below.
*/
#warning "Endianness compile time check not implemented for your compiler! "
"Please implement it or use the runtime check!"
#endif
As can be seen, the only provided implementation at the time of writing is for gcc
. Supporting other compilers
should be trivial and can be added if necessary.
In fact, gcc
's ARM options include:
-mlittle-endian
Generate code for a processor running in little-endian mode. This is the default for all standard configurations.
-mbig-endian
Generate code for a processor running in big-endian mode; the default is to compile code for a little-endian processor.
So the precaution above could be redundant, but as the proverb goes, it is better to be safe than sorry.
In addition to this compile time assertion, ecg also provides a bit-field runtime checking function in the generated code:
inline void check_bit_field_mapping()
{
struct {
uint32_t flag1: 1;
uint32_t value1: 4;
uint32_t flag2: 1;
uint32_t flag3: 1;
uint32_t value2: 7;
uint32_t : 17;
uint32_t flag4: 1;
} bit_fields1{
.flag1 = 1u,
.value1 = 11u,
.flag2 = 1u,
.flag3 = 0,
.value2 = 53u,
.flag4 = 0,
};
if ((*reinterpret_cast<const uint32_t*>(&bit_fields1) & 0x80003fff) != (1u | (11u << 1u) | (1u << 5u) |
(53u << 7u)))
for (;;)
; // bit field mapping problem, halt
struct {
uint32_t flag1: 1;
uint32_t value1: 4;
uint32_t flag2: 1;
uint32_t flag3: 1;
uint32_t : 14;
uint32_t value2: 3;
uint32_t : 4;
uint32_t flag4: 1;
uint32_t flag5: 1;
uint32_t flag6: 1;
uint32_t flag7: 1;
} bit_fields2{
.flag1 = 0,
.value1 = 13u,
.flag2 = 1u,
.flag3 = 0,
.value2 = 5u,
.flag4 = 1u,
.flag5 = 1u,
.flag6 = 0,
.flag7 = 1u,
};
if ((*reinterpret_cast<const uint32_t*>(&bit_fields2) & 0xf0e0007f) != ((13u << 1u) | (1u << 5u) |
(5u << 21u) | (1u << 28u) |
(1u << 29u) | (1u << 31u)))
for (;;)
; // bit field mapping problem, halt
}
The paranoid (like me) is encouraged to run this function at least once per combination MCU/toolchain.
The generated code will force alignment checking at compile time for every single register of every single peripheral. For instance, this is how it is done for the watchdog peripheral of an arbitrary MCU:
static_assert(offsetof(WWDG, cr_) == 0x0, "padding error");
static_assert(offsetof(WWDG, cfr_) == 0x4, "padding error");
static_assert(offsetof(WWDG, sr_) == 0x8, "padding error");
It is straightforward. It lies in the namespace passed to ecg
, and typically looks
like this:
inline const Mcu mcu{
.adc1 = *reinterpret_cast<volatile ADC1*>(0x40012000),
.adc2 = *reinterpret_cast<volatile ADC2*>(0x40012100),
.adc3 = *reinterpret_cast<volatile ADC3*>(0x40012200),
.can1 = *reinterpret_cast<volatile CAN1*>(0x40006400),
.can2 = *reinterpret_cast<volatile CAN2*>(0x40006800),
.crc = *reinterpret_cast<volatile CRC*>(0x40023000),
.cryp = *reinterpret_cast<volatile CRYP*>(0x50060000),
.c_adc = *reinterpret_cast<volatile C_ADC*>(0x40012300),
.dac = *reinterpret_cast<volatile DAC*>(0x40007400),
.dbg = *reinterpret_cast<volatile DBG*>(0xE0042000),
.dcmi = *reinterpret_cast<volatile DCMI*>(0x50050000),
.dma1 = *reinterpret_cast<volatile DMA1*>(0x40026000),
.dma2 = *reinterpret_cast<volatile DMA2*>(0x40026400),
.exti = *reinterpret_cast<volatile EXTI*>(0x40013C00),
.ethernet_dma = *reinterpret_cast<volatile Ethernet_DMA*>(0x40029000),
.ethernet_mac = *reinterpret_cast<volatile Ethernet_MAC*>(0x40028000),
.ethernet_mmc = *reinterpret_cast<volatile Ethernet_MMC*>(0x40028100),
.ethernet_ptp = *reinterpret_cast<volatile Ethernet_PTP*>(0x40028700),
.flash = *reinterpret_cast<volatile FLASH*>(0x40023C00),
.fpu = *reinterpret_cast<volatile FPU*>(0xE000EF34),
.fpu_cpacr = *reinterpret_cast<volatile FPU_CPACR*>(0xE000ED88),
.fsmc = *reinterpret_cast<volatile FSMC*>(0xA0000000),
.gpioa = *reinterpret_cast<volatile GPIOA*>(0x40020000),
.gpiob = *reinterpret_cast<volatile GPIOB*>(0x40020400),
.gpioc = *reinterpret_cast<volatile GPIOC*>(0x40020800),
// ...
};
This is C++, so we use references instead of pointers for the peripheral members. Also, the mcu
variable is inline,
which is a new feature in C++17, and allows the header file to be included in multiple compilation units without
creating conflicting definitions of the variable (only one copy is created for the whole program).
For convenience, the peripherals are sorted in alphabetic order.
In a debugger session, the mcu
variable can be used in the following way:
(gdb) set print pretty on
(gdb) p/x mcu_support::mcu.scb
$4 = (volatile mcu_support::SCB &) @0xe000ed00: {
cpuid_ = {
Revision = 0x1,
PartNo = 0xc27,
Constant = 0xf,
Variant = 0x0,
Implementer = 0x41
},
icsr_ = {
VECTACTIVE = 0x0,
RETTOBASE = 0x0,
VECTPENDING = 0x0,
ISRPENDING = 0x0,
PENDSTCLR = 0x0,
PENDSTSET = 0x0,
PENDSVCLR = 0x0,
PENDSVSET = 0x0,
NMIPENDSET = 0x0
},
vtor_ = {
TBLOFF = 0x1000
},
aircr_ = {
VECTRESET = 0x0,
VECTCLRACTIVE = 0x0,
SYSRESETREQ = 0x0,
PRIGROUP = 0x0,
ENDIANESS = 0x0,
VECTKEYSTAT = 0xfa05
},
scr_ = {
SLEEPONEXIT = 0x0,
SLEEPDEEP = 0x0,
SEVEONPEND = 0x0
},
ccr_ = {
NONBASETHRDENA = 0x0,
USERSETMPEND = 0x0,
UNALIGN__TRP = 0x0,
DIV_0_TRP = 0x0,
BFHFNMIGN = 0x0,
STKALIGN = 0x1,
DC = 0x0,
IC = 0x0,
BP = 0x1
},
shpr1_ = {
PRI_4 = 0x0,
PRI_5 = 0x0,
PRI_6 = 0x0
},
shpr2_ = {
PRI_11 = 0x0
},
shpr3_ = {
PRI_14 = 0x0,
PRI_15 = 0x0
},
shcrs_ = {
MEMFAULTACT = 0x0,
BUSFAULTACT = 0x0,
USGFAULTACT = 0x0,
SVCALLACT = 0x0,
MONITORACT = 0x0,
PENDSVACT = 0x0,
SYSTICKACT = 0x0,
USGFAULTPENDED = 0x0,
MEMFAULTPENDED = 0x0,
BUSFAULTPENDED = 0x0,
SVCALLPENDED = 0x0,
MEMFAULTENA = 0x0,
BUSFAULTENA = 0x0,
USGFAULTENA = 0x0
},
cfsr_ufsr_bfsr_mmfsr_ = {
IACCVIOL = 0x0,
DACCVIOL = 0x0,
MUNSTKERR = 0x0,
MSTKERR = 0x0,
MLSPERR = 0x0,
MMARVALID = 0x0,
IBUSERR = 0x0,
PRECISERR = 0x0,
IMPRECISERR = 0x0,
UNSTKERR = 0x0,
STKERR = 0x0,
LSPERR = 0x0,
BFARVALID = 0x0,
UNDEFINSTR = 0x0,
INVSTATE = 0x0,
INVPC = 0x0,
NOCP = 0x0,
UNALIGNED = 0x0,
DIVBYZERO = 0x0
},
hfsr_ = {
VECTTBL = 0x0,
FORCED = 0x0,
DEBUG_VT = 0x0
},
padding_0 = {0xb, 0x0, 0x0, 0x0},
mmfar_ = {
ADDRESS = 0x0
},
bfar_ = {
ADDRESS = 0x0
},
padding_1 = {0x0, 0x0, 0x0, 0x0, 0x30}
}
One of the fields tells us that this is an ARM Cortex-M7. Can you find which one?
Use your favorite gdb
-IDE integration for a seamless read/write access to all your MCU peripheral registers.
Enjoy!
ecg
is licensed under the GNU General Public License v3.0, but this does not apply to the generated files.
No particular license applies to the generated files from ecg
's perspective, but since the generated files by design
reproduce large parts of the source SVD files, you should check the license agreement of your source SVD files. When
it comes to the SVD files included in this repository, they are distributed without a license by their original provider
and seem to be reproduced elsewhere without particular precaution. Please contact me if you think I am committing any
breach of a license agreement.
I use the branch dev
in this repository as my private backyard. Please do not make any assumption about that branch.
In particular, it may get rebased at any time.
On the other hand, I intend to keep the master
branch in a working state, and I will not rebase it.