Using OOP C++ for AVR microcontrollers
Times when you had to write your programs on embedded systems in assembler are gone. Microcontrollers are shipped with more and more memory, so we can sacrifice some bytes to achieve readability and easy setup of tests for our programs. Most of programs now are written in C, but you can also use C++ or Python.
Today I will show you some general OOP C++ advices based on my experience on (quite popular among hobbyists) AVR platform.
Embedded C++ rules
Actually, writing efficient OOP C++ isn’t that hard and resource consuming. You just need to be aware of few things:
“Premature optimization is the root of all evil”
Not really, don’t be afraid of using inline.
Everytime you create a new method you should consider how long the method is and how many times it will be propably called.
I tend to mark every 1-2 liners inline, as avr-gcc doesn’t always do good job at inlining automagically.
That’s really how most of my method signatures look like:
inline uint8_t get_8bit_conversion_result() const
const
Use it everywhere you can. Method variables? const. Class methods? const.
Objects? You guessed, const.
This is very helpful for compiler when it optimizes code. I saved multiple bytes by this, so now I mark everything as const unless I’m 100% sure I will modify a variable or object.
And you can mark methods which are setting registers directly as const, registers names are just addresses, so compiler allows that.
Use length-specified types
This is kinda universal thing, because you can use them in C as well. Use responsibly signed and unsigned variables and their types to improve readability. You propably want to keep text in char * and flags in bool. Don’t throw int’s everywhere.
You’ll save some data too by doing this :>
You can encapsulate almost everything
This is your advantage in testing
I came up with creating handy hardware abstraction layer to encapsulate most AVR things into one class. This gave me an opportunity to test some of AVR hardware code with C++ testing library (Catch2 if you’re curious, it’s great) because code was platform agnostic and I could provide appropriate mocks for platform calls. This is important step, as C++ testing libraries don’t really like AVR’s lack of full STL implementation and throws errors in your face when you include any of AVR specific headers.
Templateees!
Yeah, they’re compile time, use it when you want. And you want it most of the time.
It simplifies creation of some classes, especially containers.
And, when you use HAL you can pass it as argument, for example:
Usart<Avr> usart(4800); - that’s how I create USART communication manager
Usart<MockedAvr> usart(4800); - and that’s how I create USART communication manager for testing
6. C libraries
just use extern "C" block for those.
extern "C" {
#include <avr/interrupt.h>
#include <avr/io.h>
#include <util/delay.h>
}
That’s an example how code can look like:
extern "C" {
#include <avr/interrupt.h>
#include <avr/io.h>
#include <util/delay.h>
}
#include "measure.hpp"
#include "usart.hpp"
#include "util/avr.hpp"
Usart<Avr> usart(4800);
Measure<Avr, 800> measure;
int main() {
Avr::enable_interrupts();
measure.enable_measurements();
while (true) {
if (measure.is_buffer_full()) {
measure.flush_data_via_USART(usart);
}
}
}
ISR(USART_UDRE_vect) {
usart.send_data_via_interrupt();
}
ISR(USART_RXC_vect) {
usart.receive_data_via_interrupt();
}
ISR(ADC_vect) {
measure.store_measured_data(measure.get_measured_data());
}
(ISRs are interrupt handlers, in this case for USART data ready, USART data received and ADC conversion finish)
As you may see, almost everything is managed by abstractions, even turning on and off interrupts. It’s quite readable, so I won’t make any comment what this code is doing.
This approach allows to test every file that isn’t including avr.h with any of C++ testing libraries. I think profilers would work too, but I haven’t tested that.
ISRs are horrible though. This is how they internally look (in case of C++):
# define ISR(vector, ...) \
extern "C" void vector (void) __attribute__ ((signal,__INTR_ATTRS)) __VA_ARGS__; \
void vector (void)
So it just appends some compiler attributes before brackets with your handler’s code.
I’m not a C++ expert, maybe somebody know a way how this macro can be used with templates, I don’t know.
If you want to test code with ISR definitions in it, you’ll have to make some functions handling interrupts, call them in ISR macro and wrap ISR and AVR-specific includes in some preprocessor magic which executes only when TEST flag isn’t set. In tests you just override interrupt handlers.
How to compile with C++ using avr-g++?
Use -std=c++14 flag