In this article I will tell you how for five years I translated the enterprises I worked with from managing projects for microcontrollers in C to C ++ and what came of it (spoiler: everything is bad).
A little bit about yourself
I started writing under C microcontrollers, having only school experience with Pascal, then I studied assembler and spent about 3 years studying various microcontroller architectures and their peripherals. Then there was the experience of real work in C # and C ++ with their parallel study, which took several years. After this period, I again and for a long time returned to programming microcontrollers, already having the necessary theoretical basis for working on real projects.
First year
I had nothing against the procedural style of C, however, the enterprise that started my real practice on real projects used "C programming in an object-oriented style." It looked something like this.
typedef const struct _uart_init { USART_TypeDef *USARTx; uint32_t baudrate; ... } uart_cfg_t; int uart_init (uart_cfg_t *cfg); int uart_start_tx (int fd, void *d, uint16_t l); int uart_tx (int fd, void *d, uint16_t l, uint32_t timeout);
This approach had the following advantages:
')
- the code continued to be C code. The following advantages follow from this:
- it’s easier to control “objects”, because it is easy to track who and where what causes and in what sequence (with the exception of interruptions, but not in this article);
- to store the "pointer to the object" it is enough to remember the returned fd;
- if the “object” was deleted, then when you try to use it, you will receive the corresponding error in the return value of the function;
- the abstraction of such objects over the HAL used there made it possible to write objects customizable for the task from its own initialization structure (and in the case of a lack of HAL functionality, one could hide the access to the registers inside the "objects").
Minuses:
- if someone deleted the “object”, and then created a new one of a different type, it may happen that the new one gets the fd of the old one and further behavior will not be determined. This behavior could be easily changed at the expense of a small memory consumption for a linked list instead of using an array with a “key-value” (an array for each index fd stored a pointer to the structure of the object).
- it was impossible to statically mark memory under "global objects". Since in most applications “objects” were created once and were not deleted further, it looked like a “crutch”. Here, when creating an object, it would be possible to pass a pointer to its internal structure, which was allocated statically during layout, but this would confuse the initialization code even more and break encapsulation.
When asked about why C ++ was not selected when building the entire infrastructure, they answered something like the following: “Well, C ++ leads to strong additional costs, uncontrolled memory costs, as well as a bulky executable firmware file.” Perhaps they were right. Indeed, at the time of the beginning of designing, there was only GCC 3.0.5, which did not shine with special friendliness to C ++ (it is still necessary to work with it to write programs under QNX6). There was no constexpr and C ++ 11/14, allowing you to create global objects, which in essence were data in the .data area, calculated at the compilation stage and methods for them.
To the question, why not write on the registers - I got a clear answer that the use of "objects" allows you to configure the same type of application "in a day".
Realizing all this and realizing that now C ++ is not the same as it was with GCC 3.0.5, I began to rewrite the main part of the functionality in C ++. To get started, work with the hardware peripherals of the microcontroller, then the peripherals of external devices. In fact, this was only a more convenient shell over what was available at that time.
Year two and three
I rewrote everything I needed for my C ++ projects and continued to write new modules right away in C ++. However, these were just shells over C. Realizing that I was not using C ++ enough, I started using its strengths: templates, header-only classes, constexpr, and more. Everything was going well.
Fourth and Fifth Year
- all objects are global and include links to each other at the compilation stage (according to the architecture of the project);
- all objects are allocated memory at the stage of layout;
- by class object for each pin;
- an object that encapsulates all pins for their initialization in one method;
- an RCC control object that encapsulates all objects that are on the hardware buses;
- CAN <-> RS485 converter project according to customer protocol contains 60 objects;
- in case something is at that level at the HAL or class level of some object, you have to not just “fix the problem”, but also think how to fix it so that this fix works on all possible configurations of this module ;
- the used templates and constexpr cannot be calculated before viewing the map, asm and bin files of the final firmware (or starting debugging in the microcontroller);
- in case of an error in the template, a message with a length of one third of the project configuration from GCC is output. To read and understand something from it is a separate achievement.
Summary
Now I understand the following:
- the use of "universal module constructors" only complicates the program unnecessarily. It is much easier to adjust the configuration registers for a new project than to delve into the relationships between objects, and then also in the HAL library;
- don't be afraid to use C ++ for fear that it will “gobble up a lot of memory” or “be less optimized than C”. No, it is not. You need to be afraid that using objects and many layers of abstraction will make the code unreadable, and debugging it will be a heroic feat;
- if you don’t use anything “complicating” like templates, inheritance and other attractive charms of C ++, then why use C ++ at all? Just for the sake of objects? Is it worth it? And for the sake of static global objects without using new / delete prohibited on some projects?
Summing up, we can say that the apparent simplicity of using C ++ turned out to be just an excuse to repeatedly increase the complexity of the project without any gain in speed or memory.