FastArduino v1.10
C++ library to build fast but small Arduino/AVR projects
Loading...
Searching...
No Matches
FastArduino API Tutorial

This is FastArduino API step-by-step tutorial.

Only the API is covered here: creating and building a project is not described here, you are supposed to know how to do it already.

Using FastArduino API can be learnt step by step in the preferred following order:

Basics:

  1. gpio & time
  2. UART & flash
  3. analog input
  4. timer
  5. real-time timer
  6. PWM
  7. utilities

Advanced:

  1. watchdog
  2. interrupts
  3. events, scheduler
  4. power
  5. EEPROM
  6. SPI devices example
  7. I2C devices example
  8. software UART

Supported devices (not yet documented):

  1. SPI: NRF24L01P (RF TX/RX), WinBond Flash, MCP3XXX (ADC)
  2. I2C: DS1307 (RTC), HMC5883L (Compass), MPU6050 (Accelerometer), MCP230XX (IO Mux), VL53L0X (ToF laser)
  3. Other devices: sonar, servo, SIPO, tones generator, RFID reader

Basics: gpio & time

Blink example

Here is a first example of a FastArduino based program:

1#include <fastarduino/gpio.h>
2#include <fastarduino/time.h>
3
4int main()
5{
7 sei();
8
10 while (true)
11 {
12 led.toggle();
13 time::delay_ms(500);
14 }
15 return 0;
16}
General Purpose (digital) Input Output API.
static void init()
Performs special initialization for the target MCU.
Definition: empty.h:43
typename FastPinType< DPIN_ >::TYPE FAST_PIN
Useful alias type to the FastPin type matching a given board::DigitalPin.
Definition: gpio.h:694
@ OUTPUT
Digital pin is configured as output.
void delay_ms(uint16_t ms) INLINE
Delay program execution for the given amount of milliseconds.
Definition: time.h:346
Simple time utilities.

This example can be broken down into several parts:

This includes the necessary API from FastArduino; in this example, we just use gpio.h (all API for digital input/output) and time.h (API for busy loop delays).

int main()
{
sei();

The next part defines the standard main() function as the entry point of the program; first actions in the main() should always be to call board::init() (important initialization for some specific boards), then sooner or later call sei() to enable interrupts again, as interrupts are disabled when main() is initially called.

This line declares and initializes a digital pin variable named led as output for the board's LED (i.e. D13 on Arduino boards).

board::DigitalPin is a strong enum class that defines all digital pins for the current target, that target must be defined in the compiler command-line.

The actual type of led is gpio::FastPin<board::Port:PORT_B, 5> which means "the pin number 5 within port B"; since this type is not easy to declare when you only know the number of the pin you need, gpio::FAST_PIN<board::DigitalPin::LED> is used instead, as this useful template alias maps directly to the right type.

led is initialized as an output pin, its initial level is false (i.e. GND) by default.

while (true)
{
led.toggle();
}

Then the program enters an endless loop in which, at every iteration:

  1. It toggles the level of led pin (D13 on Arduino) from GND to Vcc or from Vcc to GND
  2. It delays execution (i.e. it "waits") for 500 milliseconds

This part of the program simply makes your Arduino LED blink at 1Hz frequency!

The last part below is never executed (because of the endless loop above) but is necessary to make the compiler happy, as main() shall return a value:

return 0;
}

Congratulation! We have just studied the typical "blink" program.

At this point, it is interesting to compare our first program with the equivalent with standard Arduino API:

1void setup()
2{
3 pinMode(LED_BUILTIN, OUTPUT);
4}
5
6void loop()
7{
8 digitalWrite(LED_BUILTIN, HIGH);
9 delay(500);
10 digitalWrite(LED_BUILTIN, LOW);
11 delay(500);
12}

Granted that the latter code seems simpler to write! However, it is also simpler to write it wrong, build and upload it to Arduino and only see it not working:

1#define LED 53
2
3void setup()
4{
5 pinMode(LED, OUTPUT);
6}
7
8void loop()
9{
10 digitalWrite(LED, HIGH);
11 delay(500);
12 digitalWrite(LED, LOW);
13 delay(500);
14}

The problem here is that Arduino API accept a simple number when they need a pin, hence it is perfectly possible to pass them the number of a pin that does not exist, as in the faulty code above: this code will compile and upload properly to an Arduino UNO, however it will not work, because pin 53 does not exist!

This problem cannot occur with FastArduino as the available pins are stored in a strong enum and it becomes impossible to select a pin that does not exist for the board we target!

Now, what is really interesting in comparing both working code examples is the size of the built program (measured with UNO as a target, FastArduino project built with AVR GCC 11.1.0, Arduino API project built with Arduino CLI 0.7.0):

Arduino API FastArduino
code size 928 bytes 156 bytes
data size 9 bytes 0 byte

As you probably know, Atmel AVR MCU (and Arduino boards that embed them) are much constrained in code and data size, hence we could say that "every byte counts". In the table ablove, one easily sees that Arduino API comes cluttered with lots of code and data, even if you don't need it; on the other hand, FastArduino is highly optimized and will produce code only for what you do use.

LED Chaser example

Now gpio.h has more API than just gpio::FastPin and gpio::FastPinType; it also includes gpio::FastPort and gpio::FastMaskedPort that allow to manipulate several pins at a time, as long as these pis belong to the same Port of the MCU. This allows size and speed optimizations when having to deal with a group of related pins, e.g. if you want to implement a LED chaser project.

With FastArduino, here is a program showing how you could implement a simple 8 LED chaser on UNO:

1#include <fastarduino/gpio.h>
2#include <fastarduino/time.h>
3
4int main()
5{
7 sei();
8
10 uint8_t pattern = 0x01;
11 while (true)
12 {
13 leds.set_PORT(pattern);
14 time::delay_ms(250);
15 pattern <<= 1;
16 if (!pattern) pattern = 0x01;
17 }
18 return 0;
19}
API that manipulates a whole digital IO port.
Definition: gpio.h:204
void set_PORT(uint8_t port) INLINE
Set the 8-bits value for port PORT register.
Definition: gpio.h:291

In this example, we selected all pins of the same port to connect the 8 LEDs of our chaser. Concretely on UNO, this is port D, which pins are D0-D7.

We thus declare and initialize leds as a gpio::FastPort<board::Port::PORT_D> port, with all pins as output (0xFF), with initial level to GND (0x00, all LEDs off).

Then, we will keep track of the current lit LED through pattern byte which each bit represents actually one LED; pattern is initialized with 0x01 i.e. D0 should be the first LED to be ON.

In the endless loop that follows, we perform the following actions:

  1. Set all pins values at once to the current value of pattern
  2. Delay execution for 250ms
  3. Shift the only 1 bit of pattern left; note that after 8 shifts, pattern will become 0, hence we need to check against this condition to reset pattern to its initial state.

This should be rather straightforward to understand if you know C or C++.

Here is an equivalent example with Arduino API:

1const byte LED_PINS[] = {0, 1, 2, 3, 4, 5, 6, 7};
2const byte NUM_LEDS = sizeof(LED_PINS) / sizeof(LED_PINS[0]);
3
4void setup()
5{
6 for(byte i = 0; i < NUM_LEDS; i++)
7 pinMode(LED_PINS[i], OUTPUT);
8}
9
10void loop()
11{
12 for(byte i = 0; i < NUM_LEDS; i++)
13 {
14 digitalWrite(LED_PINS[i], HIGH);
15 delay(250);
16 digitalWrite(LED_PINS[i], LOW);
17 }
18}

We see, with Arduino API, that we have to deal with each pin individually, which makes the program source code longer and not necessarily easier to understand.

Here is a quick comparison of the sizes for both programs:

Arduino API FastArduino
code size 968 bytes 172 bytes
data size 17 bytes 0 byte

Basics: UART & flash

Simple Serial Output example

Although not often necessary in many finished programs, UART (for serial communication interface) is often very useful for debugging a program while it is being developed; this is why UART is presented now.

Here is a first simple program showing how to display, with FastArduino API, a simple string to the serial output (for UNO, this is connected to USB):

1#include <fastarduino/uart.h>
2
3static constexpr const uint8_t OUTPUT_BUFFER_SIZE = 64;
4static char output_buffer[OUTPUT_BUFFER_SIZE];
5
7REGISTER_OSTREAMBUF_LISTENERS(serial::hard::UATX<board::USART::USART0>)
8
9int main() __attribute__((OS_main));
10int main()
11{
13 sei();
14
16 uart.begin(115200);
17
18 streams::ostream out = uart.out();
19 out.write("Hello, World!\n");
20 out.flush();
21 return 0;
22}
Hardware serial transmitter API.
Definition: uart.h:241
void begin(uint32_t rate, Parity parity=Parity::NONE, StopBits stop_bits=StopBits::ONE)
Enable the transmitter.
Definition: uart.h:269
Output stream wrapper to provide formatted output API, a la C++.
Definition: streams.h:61
void write(const char *content, size_t size)
Write a block of data to this stream.
Definition: streams.h:116
void flush()
Flush this ostream and blocks until all its buffer has been written to the underlying device.
Definition: streams.h:89
Defines all types and constants specific to support a specific MCU target.
Definition: empty.h:38
Defines all API for UART features.
Definition: soft_uart.h:84
#define REGISTER_OSTREAMBUF_LISTENERS(HANDLER1,...)
Register the necessary callbacks that will be notified when a streams::ostreambuf is put new content ...
Definition: streambuf.h:49
Hardware serial API.
#define REGISTER_UATX_ISR(UART_NUM)
Register the necessary ISR (Interrupt Service Routine) for an serial::hard::UATX to work correctly.
Definition: uart.h:37

As usual, at first we need to include the proper header (uart.h) to use its API.

Then, we define a buffer that will be used by the UART API to transmit characters to your PC through USB. You may find it cumbersome to do it yourself but it brings a huge advantage: you are the one to decide of the buffer size, whereas in Arduino API, you have no choice. Here, we consider 64 bits to be big enough to store characters that will be transmitted to the PC. How UART is using this buffer is not important to you though.

Then we register an ISR necessary for transmissions to take place; this is done by the REGISTER_UATX_ISR(0) macro. Explicit ISR registration is one important design choice of FastArduino: you decide which ISR should be registered to do what. This may again seem cumbersome but once again this gives you the benefit to decide what you need, hence build your application the way you want it.

In addition, we register UATX<board::USART::USART0> class, which we instantiate at the beginning of main(), to be notified whenever some content is inserted into uart.out(); this is done with the REGISTER_OSTREAMBUF_LISTENERS() macro. If you forget it, building your program will fail at link time.

The code that follows instantiates a serial::hard::UATX object that is using board::USART::USART0 (the only one available on UNO) and based on the previously created buffer. Note that UATX class is in charge of only transmitting characters, not receiving. Other classes exist for only receiving (UARX), or for doing both (UART).

Once created, we can set uart ready for transmission, at serial speed of 115200 bps.

Next step consists in extracting, from uart, a streams::ostream that will allow us to send characters or strings to USB:

out.write("Hello, World!\n");

The last important instruction, out.flush(), waits for all characters to be transmitted before leaving the program.

Do note the specific main declaration line before its definition: int main() __attribute__((OS_main));. This helps the compiler perform some optimization on this function, and may avoid generating several dozens code instructions in some circumstances. In some situations though, this may increase code size by a few bytes; for your own programs, you would have to compile with and without this line if you want to find what is the best for you.

Here is the equivalent code with Arduino API:

1void setup()
2{
3 Serial.begin(115200);
4 Serial.println("Hello, World!");
5}
6
7void loop()
8{
9}

Of course, we can see here that the code looks simpler, although one may wonder why we need to define a loop() function that does nothing.

Now let's compare the size of both:

Arduino API FastArduino
code size 1480 bytes 622 bytes
data size 202 bytes 82 bytes

The data size is big because the buffer used by Serial has a hard-coded size (you cannot change it without modifying and recompiling Arduino API). Moreover, when using Serial, 2 buffers are created, one for input and one for output, even though you may only need the latter one!

Now let's take a look at the 82 bytes of data used in the FastArduino version of this example, how are they broken down?

Source data size
output_buffer 64 bytes
UATX ISR 2 bytes
"Hello, World!\n" 16 bytes
TOTAL 82 bytes

As you can see in the table above, the constant string "Hello, World!\n" occupies 16 bytes of data (i.e. AVR SRAM) in addition to 16 bytes of Flash (as it is part of the program and must be stored permanently). If your program deals with a lot of constant strings like this, you may quickly meet a memory problem with SRAM usage. This is why it is more effective to keep these strings exclusively in Flash (you have no choice) but load them to SRAM only when they are needed, i.e. when they get printed to UATX as in the sample code.

How do we change our program so that this string is only stored in Flash? We can use FastArduino flash API for that, by changing only one line of code:

out.write(F("Hello, World!\n"));

Note the use of F() macro here: this makes the string reside in Flash only, and then it is being read from Flash "on the fly" by out.write() method; the latter method is overloaded for usual C-strings (initial example) and for C-strings stored in Flash only.

We can compare the impact on sizes:

without F() with F()
code size 622 bytes 600 bytes
data size 82 bytes 66 bytes

We can see here that 16 bytes have been removed from data, this is the size of the string constant.

You may wonder why "Hello, World!\n" occupies 16 bytes, although it should use only 15 bytes (if we account for the terminating ‘’\0'` character); this is because the string is stored in Flash and Flash is word-addressable, not byte-addressable on AVR.

Note that Flash can also be used to store other read-only data that you may want to access at runtime at specific times, i.e. data you do not want to be stored permanently on SRAM during all execution of your program.

The following example shows how to:

  • define, in your source code, read-only data that shall be stored in Flash memory
  • read that data when you need it
1#include <fastarduino/flash.h>
2
3// This is the type of data we want to store in flash
4struct Dummy
5{
6 uint16_t a;
7 uint8_t b;
8 bool c;
9 int16_t d;
10 char e;
11};
12
13// Define 2 variables of that type, which will be stored in flash
14// Note the PROGMEM keyword that says the compiler and linker to put this data to flash
15const Dummy sample1 PROGMEM = {54321, 123, true, -22222, 'z'};
16const Dummy sample2 PROGMEM = {12345, 231, false, -11111, 'A'};
17
18// The following function needs value of sample1 to be read from flash
19void read_and_use_sample1()
20{
21 // value will get copied with sample1 read-only content
22 Dummy value;
23 // request reading sample1 from flash into local variable value
24 flash::read_flash(&sample1, value);
25 // Here we can use value which is {54321, 123, true, -22222, 'z'}
26
27}
Flash memory utilities.
T * read_flash(uint16_t address, T *buffer, uint8_t size)
Read flash memory content at given address into buffer.
Definition: flash.h:50

Formatted Output example

Compared to Arduino API, FastArduino brings formatted streams as can be found in standard C++; although more verbose than usual C printf() function, formatted streams allow compile-time safety.

Here is an example that prints formatted data to USB:

1#include <fastarduino/uart.h>
2
3static constexpr const uint8_t OUTPUT_BUFFER_SIZE = 64;
4static char output_buffer[OUTPUT_BUFFER_SIZE];
5
7REGISTER_OSTREAMBUF_LISTENERS(serial::hard::UATX<board::USART::USART0>)
8using namespace streams;
9
10int main()
11{
13 sei();
14
16 uart.begin(115200);
17
18 ostream out = uart.out();
19 uint16_t value = 0x8000;
20 out << F("value = 0x") << hex << value
21 << F(", ") << dec << value
22 << F(", 0") << oct << value
23 << F(", B") << bin << value << endl;
24 return 0;
25}
#define F(ptr)
Force string constant to be stored as flash storage.
Definition: flash.h:150
Defines C++-like streams API, based on circular buffers for input or output.
Definition: empty_streams.h:34

Here, we still use uart.out(), but here we use its "insertion operator" <<.

If you are used to programming with C++ for more usual systems (e.g. Linux), then you will immediately recognize std::ostream API which FastArduino library tries to implement with some level of fidelity.

You can also find more details in streams namespace documentation.

Here is the equivalent code with Arduino API:

1void setup()
2{
3 Serial.begin(115200);
4 unsigned int value = 0x8000;
5 Serial.print(F("value = 0x"));
6 Serial.print(value, 16);
7 Serial.print(F(", "));
8 Serial.print(value);
9 Serial.print(F(", 0"));
10 Serial.print(value, 8);
11 Serial.print(F(", B"));
12 Serial.println(value, 2);
13}
14
15void loop()
16{
17}

Once again, we can compare the size of both:

Arduino API FastArduino
code size 1856 bytes 1696 bytes
data size 188 bytes 74 bytes

Serial Input example

FastArduino also implements input streams connected to serial output; here is a simple example:

1#include <fastarduino/uart.h>
2
3static constexpr const uint8_t INPUT_BUFFER_SIZE = 64;
4static char input_buffer[INPUT_BUFFER_SIZE];
5
7
8int main()
9{
11 sei();
12
14 uart.begin(115200);
15
16 streams::istream in = uart.in();
17
18 // Wait until a character is ready and get it
19 char value;
20 in.get(value);
21
22 // Wait until a complete string is ready and get it
23 char str[64+1];
24 in.get(str, 64+1);
25
26 return 0;
27}
Hardware serial receiver API.
Definition: uart.h:352
void begin(uint32_t rate, Parity parity=Parity::NONE, StopBits stop_bits=StopBits::ONE)
Enable the receiver.
Definition: uart.h:381
Input stream wrapper to provide formatted input API, a la C++.
Definition: streams.h:360
int get()
Extract a single character from this input stream.
Definition: streams.h:397
#define REGISTER_UARX_ISR(UART_NUM)
Register the necessary ISR (Interrupt Service Routine) for an serial::hard::UARX to work correctly.
Definition: uart.h:48

Note the similarities between this example and UATX example above for all the setup parts. The main differences are:

Then UARX mainly offers one method, get(), which returns the next character serially received and buffered; if the input buffer is currently empty, then get() will block.

The example uses 2 flavors of this istream method:

  • get(char&): this method blocks until one character is available on serial input.
  • get(char*, size_t, char): this blocks until a complete string (terminated by a specified delimiter character, '\n' by default) gets read on serial input and fills the given buffer parameter with that string content.

Note that these 2 methods use time::yield() while waiting; this may be linked to power management. Please take a look at the documentation for this API for further details.

Formatted Input example

Similar to output, input streams supports formatted input, as can be found in standard C++; once again, formatted input streams allow compile-time safety.

The following example uses formatted input to read values from USB:

1#include <fastarduino/uart.h>
2
3// Define vectors we need in the example
5
6// Buffers for UARX
7static const uint8_t INPUT_BUFFER_SIZE = 64;
8static char input_buffer[INPUT_BUFFER_SIZE];
9
10using INPUT = streams::istream;
11
12int main()
13{
15 sei();
16
17 // Start UART
19 uarx.begin(115200);
20 INPUT in = uarx.in();
21
22 // Wait for a char
23 char value1;
24 in >> streams::skipws >> value1;
25
26 // Wait for an uint16_t
27 uint16_t value2;
28 in >> streams::skipws >> value2;
29
30 return 0;
31}
void skipws(FSTREAM &stream)
Manipulator for an input stream, which will activate whitespace discarding before formatted input ope...
Definition: ios.h:729

Here, we still use uart.in() but we use its "extraction operator" >>. All extractions are blocking and will not return until the required type can be read from the buffer.

If you are used to programming with C++ for more usual systems (e.g. Linux), then you will immediately recognize std::istream API which FastArduino library tries to implement with some level of fidelity.

You can also find more details in streams namespace documentation.

We have already seen UATX and UARX as classes for sending, resp. receiving, data through serial. There is also UARX which combines both.

As you know, the number of physical (hardware) UART available on an MCU target is limited, some targets (ATtiny) don't even have any hardware UART at all. For this reason, if you need extra UART featurs to connect to some devices, you can use software UART API, documented in namespace serial::soft. As this is more complicated to use, it is not part of this basic tutorial, but will be addressed later on.

Basics: analog input

Here is a simple example using analog input API to read a value from some sensor (thermistor, potentiometer, whatever you like) and lights a LED if the read value is above some threshold:

2#include <fastarduino/gpio.h>
3#include <fastarduino/time.h>
4
5const uint16_t THRESHOLD = 500;
6
7int main()
8{
10 sei();
11
14 while (true)
15 {
16 if (sensor.sample() > THRESHOLD)
17 led.set();
18 else
19 led.clear();
20 time::delay_ms(100);
21 }
22 return 0;
23}
Analog Input API.
API that handles a given analog input pin of the target MCU.
Definition: analog_input.h:52
SAMPLE_TYPE sample()
Start an analog-digital conversion for this analog input pin and return sample value.
Definition: analog_input.h:104

This example is an adaptation of the first GPIO example of this tutorial.

The first change consists in including the necessary header:

Then we have the definition of the sensor variable:

Here we instantiate AnalogInput for analog pin A0 (on Arduino UNO).

In the infinite loop, we then get the current analog value of sensor and compare it to THRESHOLD constant:

if (sensor.sample() > THRESHOLD)

By default, sample values are on 10 bits (0..1023) represented as uint16_t.

If you don't need such precision, you can define sensor differently:

Note the two additional template arguments provided to AnalogInput<...>:

  • the first added argument board::AnalogReference::AVCC, although seldom changed, may become important when you create your own boards from MCU chips; you can further read API documentation if you need more information about it
  • the second added argument is the type of returned samples, either uint16_t (default value) or uint8_t. The type determines the samples precision:
    • uint8_t: 8 bits (samples between 0 and 255)
    • uint16_t: 10 bits (samples between 0 and 1023)

Now let's compare the first example with the equivalent Arduino core API program:

1void setup()
2{
3 pinMode(LED_BUILTIN, OUTPUT);
4}
5
6const uint16_t THRESHOLD = 500;
7
8void loop()
9{
10 if (analogRead(A0) > THRESHOLD)
11 digitalWrite(LED_BUILTIN, HIGH);
12 else
13 digitalWrite(LED_BUILTIN, LOW);
14 delay(100);
15}

As usual, we compare the size of both:

Arduino API FastArduino
code size 926 bytes 206 bytes
data size 9 bytes 0 byte

Note that Arduino core API does not allow you any other precision than 10 bits.

Basics: timer

A timer (it should actually be named "timer/counter") is a logic chip or part of an MCU that just "counts" pulses of a clock at a given frequency. It can have several modes. It is used in many occasions such as:

  • real time counting
  • asynchronous tasks (one-shot or periodic) scheduling
  • PWM signal generation (see PWM for further details)

A timer generally counts up, but it may also, on some occasions, count down; it may trigger interrupts on several events (i.e. when counter reaches some specific limits), it may drive some specific digital output pins, and sometimes it may also be driven by digital input pins (to capture counter value).

There are typically several independent timers on an MCU, but they are not all the same. Timers may differ in:

  • counter size (8 or 16 bits for AVR timers)
  • list of settable frequencies (timer frequencies are derived from the MCU clock by prescaler devices)
  • the timer modes supported
  • the pins they are connected to
  • specific capabilities they may have

Rather than explaining the theory further, we will start studying a simple example that uses a timer for blinking a LED, a little bit like the first example in this tutorial, but totally driven asynchronously:

1#include <fastarduino/gpio.h>
2#include <fastarduino/timer.h>
3
4constexpr const board::Timer NTIMER = board::Timer::TIMER1;
5using CALCULATOR = timer::Calculator<NTIMER>;
6using TIMER = timer::Timer<NTIMER>;
7constexpr const uint32_t PERIOD_US = 1000000;
8
9constexpr const TIMER::PRESCALER PRESCALER = CALCULATOR::CTC_prescaler(PERIOD_US);
10constexpr const TIMER::TYPE COUNTER = CALCULATOR::CTC_counter(PRESCALER, PERIOD_US);
11
12class Handler
13{
14public:
15 Handler(): _led{gpio::PinMode::OUTPUT, false} {}
16
17 void on_timer()
18 {
19 _led.toggle();
20 }
21
22private:
24};
25
26// Define vectors we need in the example
27REGISTER_TIMER_COMPARE_ISR_METHOD(1, Handler, &Handler::on_timer)
28
29int main() __attribute__((OS_main));
30int main()
31{
33 sei();
34 Handler handler;
37 timer.begin(COUNTER);
38
39 while (true) ;
40}
General API to handle an AVR timer.
Definition: timer.h:705
Timer
Defines all timers available for target MCU.
Definition: empty.h:112
Defines all API to manipulate general-purpose digital input/output pins.
Definition: gpio.h:55
void register_handler(Handler &handler)
Register a class instance containing methods that shall be called back by an ISR.
Definition: interrupts.h:185
Defines all API to manipulate AVR Timers.
Definition: pulse_timer.h:116
@ CTC
Timer "Clear Timer on Compare match" mode: counter is incremented until it reaches "TOP" (OCRxA regis...
@ OUTPUT_COMPARE_A
This interrupt occurs when the counter reached OCRA.
Defines a set of calculation methods for the given NTIMER_ The behavior of these methods is specific ...
Definition: timer.h:302
Timer API.
#define REGISTER_TIMER_COMPARE_ISR_METHOD(TIMER_NUM, HANDLER, CALLBACK)
Register the necessary ISR (Interrupt Service Routine) for a timer::Timer with a callback method in C...
Definition: timer.h:40

This example looks much more complex than all previous examples but it is straightforward to understand once explained part after part.

In addition to GPIO, we include the header containing all Timer API.

For this example, we use Arduino UNO, which MCU (ATmega328P) includes 3 timers (named respectively Timer0, Timer1, Timer2 in its datasheet), we use Timer1 which is 16-bits in size:

constexpr const board::Timer NTIMER = board::Timer::TIMER1;
using CALCULATOR = timer::Calculator<NTIMER>;
using TIMER = timer::Timer<NTIMER>;
constexpr const uint32_t PERIOD_US = 1000000;

Although not needed, it is a good practice to define a const, named NTIMER in this snippet, valued with the real timer we intend to use. Then we define 2 new type aliases, CALCULATOR and TIMER that will help us type less code (this is common recommended practice when using C++ templates heavily in programs):

  • CALCULATOR is the type of a class which provides static utility methods that will help us configure the timer we have selected; do note that, since all timers are different, CALCULATOR is specific to one timer only; hence if our program was using 2 distinct timers, we would have to define two distinct calculator type aliases, one for each timer.
  • TIMER is the type of the class that embed all timer API for the specific timer we have selected.

Finally we define PERIOD_US the period, in microseconds, at which we want the LED to blink. Please note that this is in fact half the actual period, because this is the time at which we will toggle the LED light.

constexpr const TIMER::PRESCALER PRESCALER = CALCULATOR::CTC_prescaler(PERIOD_US);
constexpr const TIMER::TYPE COUNTER = CALCULATOR::CTC_counter(PRESCALER, PERIOD_US);

The above snippet defines constant settings, computed by CALCULATOR utility class, that we will later use to initialize our timer:

  • PRESCALER is the optimum prescaler value that we can use for our timer in order to be able to count up to the requested period, i.e. 1 second; the type of prescaler is an enum that depends on each timer (because the list of available prescaler values differ from one timer to another). The prescaler defines the number by which the MCU clock frequency will be divided to provide the pulses used to increment the timer. We don't need to know this value or fix it ourselves because CALCULATOR::CTC_prescaler calculates the best choice for us.
  • COUNTER is the maximum counter value that the timer can reach until 1 second has ellapsed; its type is based on the timer we have selected (i.e. Timer1 => 16 bits => uint16_t), but we don't need to fix this type ourselves because it depends on the timer we have selected.

Note that, although we know in advance which timer we use, we always avoid declaring direct types (such as uint16_t) in order to facilate a potential change to another timer in the future, without having to change several code locations.

class Handler
{
public:
Handler(): _led{gpio::PinMode::OUTPUT, false} {}
void on_timer()
{
_led.toggle();
}
private:
};

Here we define the class which implements the code in charge of blinking the LED every time the timer has reached its maximum value, i.e. every second. There is nothing special to explain here, except that the method on_timer() is a callback function which will get called asynchronously (from interrupt handlers) when the timer reaches its max.

Since timers generate interruptions, we need to "attach" our handler code above to the suitable interruption, this is done through the following line of code:

REGISTER_TIMER_COMPARE_ISR_METHOD(1, Handler, &Handler::on_timer)

REGISTER_TIMER_COMPARE_ISR_METHOD is a macro that will generate extra code (code you do not need, nor want, to see) to declare the Interrupt Service Routine (ISR) attached to the proper interruption of our selected timer; it takes 3 arguments:

  • 1 is the timer number (0, 1 or 2 on UNO)
  • Handler is the class that contains the code to be called when the interrupt occurs
  • &Handler::on_timer is the Pointer to Member Function (often abbreviated PTMF by usual C++ developers) telling which method from Handler shall be called back when the interrupt occurs In FastArduino, interrupt handling follows some patterns that are further described here and won't be developed in detail now.

Now we can finally start writing the code of the main() function:

int main() __attribute__((OS_main));
int main()
{
sei();
Handler handler;

Past the usual initialization stuff, this code performs an important task regarding interrupt handling: it creates handler, an instance of the Handler class that has been defined before as the class to handle interrupts for the selected timer, and then it registers this handler instance with FastArduino. Now we are sure that interrupts for our timer will call handler.on_timer().

The last part of the code creates and starts the timer we need in our program:

timer.begin(COUNTER);
while (true) ;
}

timer is the instance of timer::Timer API for board::Timer::TIMER1; upon instantiation, it is passed the timer mode to use, the previously calculated clock prescaler, and the interrupt we want to enable.

Here we use CTC mode (Clear Timer on Compare); in this mode the counter is incremented until it reaches a maximum value, then it triggers an interrupt and it clears the counter value back to zero and starts counting up again.

To ensure that our handler to get called back when the timer reaches 1 second, we set timer::TimerInterrupt::OUTPUT_COMPARE_A, which enables the proper interrupt on this timer: when the counter is reached, an interrupt will occur, the properly registered ISR will be called, and in turn it will call our handler.

Then timer.begin() activates the timer with the maximum counter value, that was calculated initially in the program. This value, along with PRESCALER, has been calculated in order for timer to generate an interrupt (i.e. call handler.on_timer()) every second.

Note the infinite loop while (true); at the end of main(): without it the program would terminate immediately, giving no chance to our timer and handler to operate as expected. What is interesting to see here is that the main code does not do anything besides looping forever: all actual stuff happens asynchronously behind the scenes!

I would have liked to perform a size comparison with Arduino API, but unfortunately, the basic Arduino API does not provide an equivalent way to directly access a timer, hence we cannot produce the equivalent code here. Anyway, here is the size for the example above:

FastArduino
code size 234 bytes
data size 2 bytes

Basics: real-time timer

A real-time timer is primarily a device that tracks time in standard measurements (ms, us).

It may be used in various situations such as:

  • delay program execution for some us or ms
  • capture the duration of some event with good accuracy
  • implement timeouts in programs waiting for an event to occur
  • generate periodic events

The simple example that follows illustrates the first use case:

1#include <fastarduino/gpio.h>
3
5
6const constexpr uint32_t BLINK_DELAY_MS = 500;
7
8int main()
9{
11 sei();
12
14 rtt.begin();
15
17 while (true)
18 {
19 led.toggle();
20 rtt.delay(BLINK_DELAY_MS);
21 }
22}
API to handle a real-time timer.
void delay(uint32_t ms) const
Delay program execution for the given amount of milliseconds.
void begin()
Start this real-time timer, hence elapsed time starts getting counted from then.
Real-time Timer API.
#define REGISTER_RTT_ISR(TIMER_NUM)
Register the necessary ISR (Interrupt Service Routine) for a timer::RTT to work properly.

This example looks much like the first blinking example in this tutorial, with a few changes.

First off, as usual the neceaary header file is included:

Then we need to register an ISR for the RTT feature to work properly:

Then, in main(), after the usual initialization stuff, we create a real-time timer instance, based on AVR UNO Timer0 (8-bits timer), and start it counting time.

Finally, we have the usual loop, toogling the LED, and then delay for 10s, using the RTT API:

rtt.delay(BLINK_DELAY_MS);

Let's examine the size of this program and compare it with the first example of this tutorial, which used time::delay_ms():

delay_ms RTT::delay
code size 156 bytes 372 bytes
data size 0 byte 2 bytes

As you can see, code and data size is higher here, so what is the point of using RTT::delay() instead of time::delay_ms()? The answer is power consumption:

  • time::delay_ms is a busy loop which requires the MCU to be running during the whole delay, hence consuming "active supply current" (about 15mA for an ATmega328P at 16MHz)
  • RTT::delay() will set the MCU to pre-defined sleep mode and will still continue to operate well under most available sleep modes (this depends on which timer gets used, refer to AVR datasheet for further details); this will alow reduction of supply current, hence power consumption. Current supply will be reduced more or less dramatically according to the selected sleep mode.

Another practical use of RTT is to measure the elapsed time between two events. For instance it can be used with an ultrasonic ranging device to measure the duration of an ultrasound wave to do a roundript from the device to an obstacle, then calculate the actual distance in mm. The following snippet shows how it could look like for an HC-SR04 sensor:

// Declare 2 pins connected to HC-SR04
// Declare RTT (note: don't forget to call REGISTER_RTT_ISR(1) macro in your program)
// Send a 10us pulse to the trigger pin
trigger.set();
trigger.clear();
// Wait for echo signal start
while (!echo.value()) ;
// Reset RTT time
rtt.millis(0);
// Wait for echo signal end
while (echo.value()) ;
// Read current time
time::RTTTime end = rtt.time();
// Calculate the echo duration in microseconds
uint16_t echo_us = uint16_t(end.millis() * 1000UL + end.micros());
Structure used to hold a time value with microsecond precision.
Definition: time.h:50
uint16_t micros() const
Number of elapsed microseconds (0..999).
Definition: time.h:131
uint32_t millis() const
Number of elapsed milliseconds.
Definition: time.h:125
uint32_t millis() const
Elapsed time, in milliseconds, since this timer has started.
time::RTTTime time() const
Elapsed time, in milliseconds and microseconds, since this timer has started.
@ INPUT
Digital pin is configured as high-impedance (open drain) input.
void delay_us(uint16_t us) INLINE
Delay program execution for the given amount of microseconds.
Definition: time.h:334

Note that this snippet is just an example and is not usable as is: it does not include a timeout mechanism to avoid waiting the echo signal forever (which can happen if the ultrasonic wave does not encounter an obstacle within its possible range, i.e. 4 meters). Also, this approach could be improved by making it interrupt-driven (i.e. having interrupts generated when the echo_pin changes state).

Actually, if you want a complete implementation of HC-SR04 ultrasonic ranging device, then you should take a look at FastArduino provided API in namespace devices::sonar.

Another interesting use of RTT is to perform some periodic actions. FastArduino implements an events handling mechanism that can be connected to an RTT in order to deliver periodic events. This mechanism is described later in this tutorial.

Basics: PWM

PWM (Pulse Width modulation) is a technique that can be used to simulate generation of an analog voltage level through a purely digital output. This is done by varying the duty cycle of a rectangular pulse wave, i.e. the ratio of "on" time over the wave period.

PWM is implemented by MCU through timers.

FastArduino includes special support for PWM. The following example demonstrates PWM to increase then decrease the light emitted by a LED:

1#include <fastarduino/time.h>
2#include <fastarduino/pwm.h>
3
4static constexpr const board::Timer NTIMER = board::Timer::TIMER0;
5using TIMER = timer::Timer<NTIMER>;
7static constexpr const uint16_t PWM_FREQUENCY = 450;
8static constexpr const TIMER::PRESCALER PRESCALER = CALC::FastPWM_prescaler(PWM_FREQUENCY);
9
10static constexpr const board::PWMPin LED = board::PWMPin::D6_PD6_OC0A;
11using LED_PWM = analog::PWMOutput<LED>;
12
13int main()
14{
16 sei();
17
18 // Initialize timer
19 TIMER timer{timer::TimerMode::FAST_PWM, PRESCALER};
20 timer.begin();
21
22 LED_PWM led{timer};
23 // Loop of samplings
24 while (true)
25 {
26 for (LED_PWM::TYPE duty = 0; duty < LED_PWM::MAX; ++duty)
27 {
28 led.set_duty(duty);
30 }
31 for (LED_PWM::TYPE duty = LED_PWM::MAX; duty > 0; --duty)
32 {
33 led.set_duty(duty);
35 }
36 }
37 return 0;
38}
Construct a new handler for a PWM output pin.
Definition: pwm.h:54
PWMPin
Defines all digital output pins of target MCU, capable of PWM output.
Definition: empty.h:84
@ FAST_PWM
Timer "Fast Phase Width Modulation" mode: counter is incremented until it reaches MAX value (0xFF for...
PWM API.

The program starts by including the header for PWM API; this will automatically include the timer API header too.

Then a timer is selected for PWM (note that the choice of a timer imposes the choice of possible pins) and a prescaler value computed for it, based on the PWM frequency we want to use, 450Hz, which is generally good enough for most use cases (dimming a LED, rotating a DC motor...):

static constexpr const board::Timer NTIMER = board::Timer::TIMER0;
using TIMER = timer::Timer<NTIMER>;
static constexpr const uint16_t PWM_FREQUENCY = 450;
static constexpr const TIMER::PRESCALER PRESCALER = CALC::FastPWM_prescaler(PWM_FREQUENCY);

Then we define the pin that will be connected to the LED and the PWMOutput type for this pin:

static constexpr const board::PWMPin LED = board::PWMPin::D6_PD6_OC0A;
using LED_PWM = analog::PWMOutput<LED>;

Note that board::PWMPin enum limits the pins to PWM-enabled pins; also note the pin name D6_PD6_OC0A includes useful information:

  • this is pin D6 on Arduino UNO
  • this pin is on PD6 i.e. Port D bit #6
  • this pin is connectable to OC0A i.e. Timer 0 COM A

Then, in main(), after the usual initialization code, we initialize and start the timer:

// Initialize timer
TIMER timer{timer::TimerMode::FAST_PWM, PRESCALER};
timer.begin();

Notice we use the Fast PWM mode here, but we might as well use Phase Correct PWM mode.

Next we connect the LED pin to the timer:

LED_PWM led{timer};

In the main loop, we have 2 consecutive loops, the first increases the light, the second decreases it. Both loops vary the duty cycle between its limits:

for (LED_PWM::TYPE duty = 0; duty < LED_PWM::MAX; ++duty)
{
led.set_duty(duty);
}

Note the use of LED_PWM::TYPE, which depends on the timer selected (8 or 16 bits), and LED_PWM::MAX which provides the maximum value usable for the duty cycle, i.e. the value mapping to 100% duty cycle. Pay attention to the fact that LED_PWM::TYPE is unsigned, this explains the 2nd loop:

for (LED_PWM::TYPE duty = LED_PWM::MAX; duty > 0; --duty)
{
led.set_duty(duty);
}

Here, we shall not use duty >= 0 as the for condition, because that condition would be always true, hence the loop would be infinite.

Now let's compare this example with the Arduino API equivalent:

1#define LED 6
2
3void setup() {
4}
5
6void loop() {
7 for (int duty = 0; duty < 255; ++duty)
8 {
9 analogWrite(LED, duty);
10 delay(50);
11 }
12 for (int duty = 255; duty > 0; --duty)
13 {
14 analogWrite(LED, duty);
15 delay(50);
16 }
17}

Nothing special to comment here, except:

  • duty values are always limited to 255 even though some PWM pins are attached to a 16-bits timer
  • you cannot choose the PWM mode you want to use, it is fixed by Arduino API and varies depending on which timer is used (e.g. for Arduino UNO, Timer0 uses Fast PWM, whereas Time1 and Timer2 use Phase Correct PWM mode)
  • you cannot choose the PWM frequency, this is imposed to you by Arduino API and varies depending on which timer is used
  • you may pass any pin value to analogWrite() and the sketch will still compile and upload but the sketch will not work

Comparing sizes once again shows big differences:

Arduino API FastArduino
code size 1102 bytes 298 bytes
data size 9 bytes 0 byte

Basics: utilities

FastArduino provides several general utilities, gathered inside one namespace utils.

We will not demonstrate each of these utilities here but just show a few of them in action. In your own programs, if you find yourself in need of some helper stuff that you think deserves to be provided as a general utility, then first take a look at FastArduino utilities API documentation and check if you don't find it there, or something similar that you could use in your situation.

FastArduino utilities are made of different kinds:

  • low-level utilities: mostly functions to handle bytes and bits
  • value conversion utilities: functions to help convert a value from one referential to another, very useful when dealing with sensors of all sorts

Low-level utilities examples

The few examples in this section will introduce you to a few functions that may prove useful if you need to handle devices that are not natively supported by FastArduino.

  1. utils::swap_bytes: this function is useful whenever you use a sensor device that provides you with integer values, coded on 2 bytes, with high byte first and low byte second; since AVR MCU are "little-endian" processors, they expect words in the opposite order: low byte first, high byte second, hence in order to interpret values provided by that device, you need to first swap their bytes. Bytes swap is performed "in-place", i.e. the original value is replace with the converted value. The following example is an excerpt of hmc5883l.h provided by FastArduino, where magnetic fields in 3 axes have to be converted from big endian (as provided by the HMC5883L) to little endian (as expected by the AVR MCU):
    class MagneticFieldsFuture : public ReadRegisterFuture<Sensor3D>
    {
    public:
    bool get(Sensor3D& fields)
    {
    if (!PARENT::get(fields)) return false;
    utils::swap_bytes(fields.x);
    utils::swap_bytes(fields.y);
    utils::swap_bytes(fields.z);
    return true;
    }
    };
    void swap_bytes(uint16_t &value)
    Swap 2 bytes of a 2-bytes integer.
    Definition: utilities.h:405
  2. utils::bcd_to_binary: this function is useful when you use a sensor device that provides values coded as BCD (binary-coded decimal), i.e. where each half-byte (nibble) contains the value of one digit (i.e. 0 to 9), thus holding a range of values from 0 to 99. Many RTC devices use BCD representation for time. In order to perform a calculation on BCD values, you need to first convert them to binary. The opposite function is also provided as utils::binary_to_bcd. The following example is an excerpt of ds1307.h provided by FastArduino, where each datetime field (seconds, minutes, hours...) have to be converted from BCD to binary:
    struct set_tm
    {
    set_tm(const tm& datetime)
    {
    tm_.tm_sec = utils::binary_to_bcd(datetime.tm_sec);
    tm_.tm_min = utils::binary_to_bcd(datetime.tm_min);
    tm_.tm_hour = utils::binary_to_bcd(datetime.tm_hour);
    tm_.tm_mday = utils::binary_to_bcd(datetime.tm_mday);
    tm_.tm_mon = utils::binary_to_bcd(datetime.tm_mon);
    tm_.tm_year = utils::binary_to_bcd(datetime.tm_year);
    }
    uint8_t address_ = TIME_ADDRESS;
    tm tm_;
    };
    uint8_t binary_to_bcd(uint8_t binary)
    Convert a natural integers to a BCD byte (2 digits).
    Definition: utilities.h:389

Conversion utilities examples

Device sensors measure some physical quantity and generally provide you with some integer value that somehow maps to the physical value. hence to make use of the raw value provided by a sensor, you need to convert it to some more meaningful value that you can understand and operate upon.

Or conversely, you may just need to compare the physical value agains some thresholds (e.g. check the gyroscopic speed according to some axis is less than 10°/s), and perform some action when this is not true. In this situation, you don't really need to convert the raw sensor value into a physical quantity to compare to the physical threshold, but rather convert (once only) the physical threshold into the corresponding raw value (a constant in your program) and then only compare raw values, which is:

  • more performant (no conversion needed before comparison)
  • more size efficient (conversion of threshold can be done at compile time, hence no code is generated for it)

FastArduino utilities provide several conversion methods between raw and physical quantities, according to raw and physical ranges (known for each sensor), and unit prefix (e.g. kilo, mega, giga, centi, milli...). These methods are constexpr, which means that, when provided with constant arguments, they will be evaluated at compile-time and return a value that is itself stored as a constant.

  1. utils::map_physical_to_raw: although it may seem complicated by its list of arguments, this function is actually pretty simple, as demonstrated in the snippet hereafter:
    static constexpr const int16_t ACCEL_1 = map_physical_to_raw(500, UnitPrefix::MILLI, 2, 15);
    In this example, we convert the acceleration value 500mg (g is 9.81 m/s/s) to the equivalent raw value as produced by an MPU-6050 accelerometer, using +/-2g range (2 is the max physical value we can get with this device using this range) where this raw value is stored on 15 bits (+1 bit for the sign), i.e. 32767 is the raw value returned by the device when the measured acceleration is +2g.
  2. utils::map_raw_to_physical: this method does the exact opposite of utils::map_physical_to_raw with the same parameters, reversed:
    int16_t rotation = map_raw_to_physical(raw, UnitPrefix::CENTI, 250, 15);
    In this example, we convert raw which is returned by the MPU-6050 gyroscope, using range +/-250°/s with 15 bits precision (+1 bit for the sign), i.e. 32767 is the raw value returned by the device when the measured rotation speed is +250°/s. The calculated value is returned in c°/s (centi-degrees per second).

In addition to these functions, FastArduino utilities also include the more common utils::map and utils::constrain which work like their Arduino API equivalent map() and constrain().

Advanced: Watchdog

In general, a watchdog is a device (or part of a device) that is used to frequently check that a system is not hanging. AVR MCU include such a device and this can be programmed to other purposes than checking the system is alive, e.g. as a simple timer with low-power consumption. This is in that purpose that FastArduino defines a specific Watchdog API.

FastArduino defines 2 watchdog classes. The first one, WatchdogSignal allows you to generate watchdog timer interrupts at a given period. The following example is yet another way to blink a LED:

1#include <fastarduino/gpio.h>
2#include <fastarduino/power.h>
4
5// Define vectors we need in the example
7
8int main() __attribute__((OS_main));
9int main()
10{
12 sei();
13
15
18
19 while (true)
20 {
21 led.toggle();
22 power::Power::sleep(board::SleepMode::POWER_DOWN);
23 }
24}
static void sleep()
Enter power sleep mode as defined by Power::set_default_mode().
Definition: power.h:69
Simple API to handle watchdog signals.
Definition: watchdog.h:145
Defines the simple API for Watchdog timer management.
Definition: watchdog.h:101
@ TO_500ms
Watchdog timeout 500 ms.
Simple power support for AVR MCU.
Watchdog API.
#define REGISTER_WATCHDOG_ISR_EMPTY()
Register an empty ISR (Interrupt Service Routine) for a watchdog::WatchdogSignal.
Definition: watchdog.h:85

In this example, we use watchdog API but also power API in order to reduce power-consumption.

As we use WatchdogSignal, we need to register an ISR, however we do not need any callback, hence we just register an empty ISR.

Then we have the usual main() function which, after defining led output pin, starts the watchdog timer:

Here we use a 500ms timeout period, which means our sleeping code will be awakened every 500ms.

The infinite loop just toggles led pin level and goes to sleep:

while (true)
{
led.toggle();
power::Power::sleep(board::SleepMode::POWER_DOWN);
}

As explained later, power::Power::sleep(board::SleepMode::POWER_DOWN) just puts the MCU to sleep until some external interrupts wake it up; in our example, that interrupt is the watchdog timeout interrupt.

The size of this example is not much bigger than the first example of this tutorial:

FastArduino
code size 190 bytes
data size 0 byte

FastArduino defines another class, Watchdog. This class allows to use the AVR watchdog timer as a clock for events generation. This will be demonstrated when events scheduling is described.

Advanced: Interrupts

Introduction to AVR interrupts

Many AVR MCU features are based upon interrupts, so that the MCU can efficiently react to events such as timer overflow, pin level change, without having to poll for these conditions. Using AVR interrupts makes your program responsive to events relevant to you. This also allows taking advantage of power sleep modes thus reducing energy consumption while the MCU has nothing to do other than wait for events to occur.

Each AVR MCU has a predefined list of interrupts sources, each individually activable, and each with an associated vector which is the address of an Interrupt Service Routine (ISR), executed whenever the matching interrupt occurs. Each ISR is imposed a specific name.

Among common interrupt vectors, you will find for example:

  • TIMERn_OVF_vect: triggered when Timer n overflows
  • INTn_vect: triggered when input pin INTn changes level
  • UARTn_RX_vect: triggered when a new character has been received on serial receiver UARTn
  • ...

AVR interrupts handling in FastArduino

In FastArduino, ISR are created upon your explicit request, FastArduino will never add an ISR without your consent. FastArduino calls this ISR creation a registration.

ISR registration is performed through macros provided by FastArduino. There are essentially 4 flavours of registration macros:

  1. API-specific registration: in this flavour, a FastArduino feature requires to directly be linked to an ISR, through a dedicated macro and a specific registration method (either implicitly called by constructor or explicitly through a specific method). In the previous examples of this tutorial, you have already encountered REGISTER_UATX_ISR(), REGISTER_UARX_ISR(), REGISTER_UART_ISR() and REGISTER_RTT_ISR(). All macros in this flavour follow the same naming scheme: REGISTER_XXX_ISR, where XXX is the feature handled.
  2. Empty ISR registration: in this flavour, you activate an interrupt but do not want any callback for it, but then you have to define an empty ISR for it; this empty ISR will not increase code size of your program. You may wonder why you would want to enable an interrupt but do nothing when it occurs, in fact this is often used to make the MCU sleep (low power consumption) and wake it once an interrupt occurs. You have already seen such usage in a previous example, where REGISTER_WATCHDOG_ISR_EMPTY() was used.
  3. Method callback registration: with this flavour, you activate an interrupt and want a specific method of a given class to be called back when the interrupt occurs; in this flavour, a second step is required inside your code: you need to register an instance of the class that was registered. In this tutorial, previous examples used this approach with REGISTER_TIMER_COMPARE_ISR_METHOD() macro and interrupt::register_handler(handler); instance registration in main(). This is probably the most useful approach as it allows to pass an implicit context (this class instance) to the callback.
  4. Function callback registration: with this flavour, you can register one of your functions (global or static) as a callback of an ISR. This approach does not require an extra registration step. This is not used as often as the Method callback registration flavour above.

Whenever method callback is needed (typically for flavours 1 and 3 above), then a second registration step is needed.

All FastArduino API respects some guidelines for naming ISR registration macros. All macros are in one of the following formats:

  • REGISTER_XXX_ISR() for API-specific registration
  • REGISTER_XXX_ISR_EMPTY() for empty ISR
  • REGISTER_XXX_ISR_CALLBACK() for method callback
  • REGISTER_XXX_ISR_FUNCTION() for function callback

Here is a table showing all FastArduino macros to register ISR (Name is to be replaced in macro name REGISTER_NAME_ISR, REGISTER_NAME_ISR_EMPTY, REGISTER_NAME_ISR_CALLBACK or REGISTER_NAME_ISR_FUNCTION):

Header Name Flavours Comments
analog_comparator.h ANALOG_COMPARE 2,3,4 Called upon Analog Comparator interrupt.
eeprom.h EEPROM 1,3,4 Called when asynchronous EEPROM write is finished.
int.h INT 2,3,4 Called when an INT pin changes level.
pci.h PCI 2,3,4 Called when a PCINT pin changes level.
pulse_timer.h PULSE_TIMER8_A 1 Called when a PulseTimer8 overflows or equals OCRA.
pulse_timer.h PULSE_TIMER8_B 1 Called when a PulseTimer8 overflows or equals OCRB.
pulse_timer.h PULSE_TIMER8_AB 1 Called when a PulseTimer8 overflows or equals OCRA or OCRB.
realtime_timer.h RTT 1,3,4 Called when RTT timer has one more millisecond elapsed.
realtime_timer.h RTT_EVENT 1 Same as above, and trigger RTTEventCallback.
soft_uart.h UARX_PCI 1 Called when a start bit is received on a PCINT pin linked to UARX.
soft_uart.h UARX_INT 1 Called when a start bit is received on an INT pin linked to UARX.
soft_uart.h UART_PCI 1 Called when a start bit is received on a PCINT pin linked to UATX.
soft_uart.h UART_INT 1 Called when a start bit is received on an INT pin linked to UATX.
timer.h COMPARE 2,3,4 Called when a Timer counter reaches OCRA.
timer.h OVERFLOW 2,3,4 Called when a Timer counter overflows.
timer.h CAPTURE 2,3,4 Called when a Timer counter gets captured (when ICP level changes).
uart.h UATX 1 Called when one character is finished transmitted on UATX.
uart.h UARX 1 Called when one character is finished received on UARX.
uart.h UART 1 Called when one character is finished transmitted/received on UART.
watchdog.h WATCHDOG_CLOCK 1 Called when Watchdog timeout occurs, and clock must be updated.
watchdog.h WATCHDOG_RTT 1 Called when Watchdog timeout occurs, and RTT clock must be updated.
watchdog.h WATCHDOG 2,3,4 Called when WatchdogSignal timeout occurs.
i2c_handler_atmega.h I2C 1,3,4 Called when I2C status changes (ATmega only).
devices/sonar.h HCSR04_INT 1,3,4 Called when HCSR04 echo INT pin changes level.
devices/sonar.h HCSR04_PCI 1,3,4 Called when HCSR04 echo PCINT pin changes level.
devices/sonar.h HCSR04_RTT_TIMEOUT 1,3,4 Called when HCSR04 RTT times out (without any echo).
devices/sonar.h DISTINCT_HCSR04_PCI 1 Called when HCSR04 any echo PCINT pin changes level.
devices/sonar.h MULTI_HCSR04_PCI 3,4 Called when MultiHCSR04 any echo PCINT pin changes level.
devices/sonar.h MULTI_HCSR04_RTT_TIMEOUT 1,3,4 Called when MultiHCSR04 RTT times out or at every 1ms tick.
devices/sonar.h MULTI_HCSR04_RTT_TIMEOUT_TRIGGER 3,4 Called when MultiHCSR04 RTT times out or at every 1ms tick.

For further details on ISR registration in FastArduino, you can check interrutps.h API for the general approach, and each individual API documentation for specific interrupts.

Pin Interrupts

One very common usage of AVR interrupts is to handle level changes of specific digital input pins. AVR MCU have two kinds of pin interrupts:

  • External Interrupts: an interrupt can be triggered for a specific pin when its level changes or is equal to some value (high or low); the number of such pins is quite limited (e.g. only 2 on Arduino UNO)
  • Pin Change Interrupts: an interrupt is triggered when one in a set of pins has a changing level; the same interrupt (hence the same ISR) is used for all pins (typically 8, but not necessarily), thus the ISR must determine, by its own means, which pin has triggered the interrupt; although more pins support this interrupt, it is less convenient to use than external interrupts.

External Interrupts

External Interrupts are handled with the API provided by int.h. This API is demonstrated in the example below:

1#include <fastarduino/gpio.h>
2#include <fastarduino/int.h>
3#include <fastarduino/power.h>
4
5constexpr const board::ExternalInterruptPin SWITCH = board::ExternalInterruptPin::D2_PD2_EXT0;
6
7class PinChangeHandler
8{
9public:
10 PinChangeHandler():_switch{gpio::PinMode::INPUT_PULLUP}, _led{gpio::PinMode::OUTPUT} {}
11
12 void on_pin_change()
13 {
14 if (_switch.value())
15 _led.clear();
16 else
17 _led.set();
18 }
19
20private:
23};
24
25// Define vectors we need in the example
26REGISTER_INT_ISR_METHOD(0, SWITCH, PinChangeHandler, &PinChangeHandler::on_pin_change)
27
28int main() __attribute__((OS_main));
29int main()
30{
32 // Enable interrupts at startup time
33 sei();
34
35 PinChangeHandler handler;
38 int0.enable();
39
40 // Event Loop
41 while (true)
42 {
43 power::Power::sleep(board::SleepMode::POWER_DOWN);
44 }
45}
Handler of an External Interrupt.
Definition: int.h:132
General API for handling External Interrupt pins.
#define REGISTER_INT_ISR_METHOD(INT_NUM, PIN, HANDLER, CALLBACK)
Register the necessary ISR (Interrupt Service Routine) for an External Interrupt pin.
Definition: int.h:39
ExternalInterruptPin
Defines all digital output pins of target MCU, usable as direct external interrupt pins.
Definition: empty.h:91
typename FastPinType< board::EXT_PIN< EPIN_ >()>::TYPE FAST_EXT_PIN
Useful alias type to the FastPin type matching a given board::ExternalInterruptPin.
Definition: gpio.h:740
@ ANY_CHANGE
Interrupt is triggered whenever pin level is changing (rising or falling).

In this example, most of the MCU time is spent sleeping in power down mode. The pin INT0 (D2 on UNO) is connected to a push button and used to switch on or off the UNO LED (on D13).

Note how the button pin is defined:

constexpr const board::ExternalInterruptPin SWITCH = board::ExternalInterruptPin::D2_PD2_EXT0;

Here we use board::ExternalInterruptPin enum to find out the possible pins that can be used as External Interrupt pins.

Then we define the class that will handle interrupts occurring when the button pin changes level:

class PinChangeHandler
{
public:
PinChangeHandler():_switch{gpio::PinMode::INPUT_PULLUP}, _led{gpio::PinMode::OUTPUT} {}
void on_pin_change()
{
if (_switch.value())
_led.clear();
else
_led.set();
}
private:
};

The interesting code here is the on_pin_change() method which be called back when the button is pushed or relaxed, i.e. on any level change. Since this method is called whatever the new pin level, it must explicitly check the button status (i.e. the pin level) to set the right output for the LED pin. Note that, since we use gpio::PinMode::INPUT_PULLUP, the pin value will be false when the button is pushed, and true when it is not.

The next line of code registers PinChangeHandler class and on_pin_change() method as callback for INT0 interrupt.

REGISTER_INT_ISR_METHOD(0, SWITCH, PinChangeHandler, &PinChangeHandler::on_pin_change)

In addition to the class and method, the macro takes the number of the INT interrupt (0) and the pin connected to this interrupt. Note that the pin reference is redundant; it is passed as a way to ensure that the provided INT number matches the pin that we use, if it doesn't, then your code will not compile.

Then the main() function oincludes the following initialization code:

Once the interrupt handler has been instantiated, it must be registered (2nd step of method callback registration). Then an INTSignal is created for INT0 and it is set for any level change of the pin. Finally, the interrupt is activated.

Note the infinite loop in main():

while (true)
{
power::Power::sleep(board::SleepMode::POWER_DOWN);
}

With this loop, we set the MCU to sleep in lowest energy consumption mode. Only some interrupts (INT0 is one of them) will awaken the MCU from its sleep, immediately after the matching ISR has been called.

Here is the equivalent example with Arduino API:

1#include <avr/sleep.h>
2
3void onPinChange()
4{
5 digitalWrite(LED_BUILTIN, !digitalRead(2));
6}
7
8void setup()
9{
10 pinMode(LED_BUILTIN, OUTPUT);
11 pinMode(2, INPUT_PULLUP);
12 attachInterrupt(digitalPinToInterrupt(2), onPinChange, CHANGE);
13}
14
15void loop()
16{
17 sleep_enable();
18 set_sleep_mode(SLEEP_MODE_PWR_DOWN);
19 sleep_cpu();
20}

First note that Arduino API has no API for sleep mode management, hence you have to include <avr/sleep.h> and handle all this by yourself. Also note that you can only attach a function to an interrupt. Arduino API offers no way to attach a class member instead.

Now let's just compare examples sizes, as usual:

Arduino API FastArduino
code size 1114 bytes 246 bytes
data size 13 bytes 2 bytes

Pin Change Interrupts

Pin Change Interrupts are handled with the API provided by pci.h. This API is demonstrated in the example below:

1#include <fastarduino/gpio.h>
2#include <fastarduino/pci.h>
3#include <fastarduino/power.h>
4
5constexpr const board::InterruptPin SWITCH = board::InterruptPin::D14_PC0_PCI1;
6#define PCI_NUM 1
7
8class PinChangeHandler
9{
10public:
11 PinChangeHandler():_switch{gpio::PinMode::INPUT_PULLUP}, _led{gpio::PinMode::OUTPUT} {}
12
13 void on_pin_change()
14 {
15 if (_switch.value())
16 _led.clear();
17 else
18 _led.set();
19 }
20
21private:
24};
25
26// Define vectors we need in the example
27REGISTER_PCI_ISR_METHOD(PCI_NUM, PinChangeHandler, &PinChangeHandler::on_pin_change, SWITCH)
28
29int main() __attribute__((OS_main));
30int main()
31{
33 // Enable interrupts at startup time
34 sei();
35
36 PinChangeHandler handler;
39
40 pci.enable_pin<SWITCH>();
41 pci.enable();
42
43 // Event Loop
44 while (true)
45 {
46 power::Power::sleep(board::SleepMode::POWER_DOWN);
47 }
48}
InterruptPin
Defines all digital output pins of target MCU, usable as pin change interrupt (PCI) pins.
Definition: empty.h:98
typename FastPinType< board::PCI_PIN< IPIN_ >()>::TYPE FAST_INT_PIN
Useful alias type to the FastPin type matching a given board::InterruptPin.
Definition: gpio.h:717
typename PCIType< PIN >::TYPE PCI_SIGNAL
Useful alias type to the PCISignal type matching a given board::InterruptPin.
Definition: pci.h:568
General API for handling Pin Change Interrupts.
#define REGISTER_PCI_ISR_METHOD(PCI_NUM, HANDLER, CALLBACK, PIN,...)
Register the necessary ISR (Interrupt Service Routine) for a Pin Change Interrupt vector.
Definition: pci.h:44

This example performs the same as the previous example except it uses another pin for the button. Most of the code is similar, hence we will focus only on differences.

The first difference is in the way we define the pin to use as interrupt source:

constexpr const board::InterruptPin SWITCH = board::InterruptPin::D14_PC0_PCI1;
#define PCI_NUM 1

Here we use board::InterruptPin enum to find out the possible pins that can be used as Pin Change Interrupt pins. We select pin D14 and see from its name that is belonging to PCINT1 interrupt vector. We also define the PCINT number as a constant, PCI_NUM, for later use.

Then we register PinChangeHandler class and on_pin_change() method as callback for PCINT1 interrupt.

REGISTER_PCI_ISR_METHOD(PCI_NUM, PinChangeHandler, &PinChangeHandler::on_pin_change, SWITCH)

Once we have, as before,registered our handler class, we then create a PCISignal instance that we will use to activate the proper interrupt:

pci.enable_pin<SWITCH>();
pci.enable();

Note that we use the useful template alias PCI_SIGNAL to find the proper PCISignal directly from the board::InterruptPin.

Before enabling the PCINT1 interrupt, we need to first indicate that we are only interested in changes of the button pin.

For this example, we cannot compare sizes with the Arduino API equivalent because Pin Change Interrupts are not supported by the API.

Advanced: Events, Scheduler

FastArduino supports (and even encourages) events-driven programming, where events are generally generated and queued by interrupts (timer, pin changes, watchdog...) but not only, and events are then dequeued in an infinite loop (named the "event loop") in main().

This way of programming allows short handling of interrupts in ISR (important because interrupts are disabled during ISR execution), and defer long tasks execution to the event loop.

Events API

We will use the following example to explain the general Event API:

1#include <fastarduino/gpio.h>
2#include <fastarduino/pci.h>
4#include <fastarduino/time.h>
5
6#define PCI_NUM 2
7static constexpr const board::Port BUTTONS_PORT = board::Port::PORT_D;
8static constexpr const board::DigitalPin LED = board::DigitalPin::LED;
9
10using namespace events;
11
12using EVENT = Event<uint8_t>;
13static constexpr const uint8_t BUTTON_EVENT = Type::USER_EVENT;
14
15// Class handling PCI interrupts and transforming them into events
16class EventGenerator
17{
18public:
19 EventGenerator(containers::Queue<EVENT>& event_queue):event_queue_{event_queue}, buttons_{0x00, 0xFF} {}
20
21 void on_pin_change()
22 {
23 event_queue_.push_(EVENT{BUTTON_EVENT, buttons_.get_PIN()});
24 }
25
26private:
27 containers::Queue<EVENT>& event_queue_;
29};
30
31REGISTER_PCI_ISR_METHOD(PCI_NUM, EventGenerator, &EventGenerator::on_pin_change, board::InterruptPin::D0_PD0_PCI2)
32
33void blink(uint8_t buttons)
34{
35 // If no button is pressed, do nothing
36 if (!buttons) return;
37
39 // Buttons are plit in 2 groups of four:
40 // - 1st group sets 5 iterations
41 // - 2nd group sets 10 iterations
42 // Note: we multiply by 2 because one blink iteration means toggling the LED twice
43 uint8_t iterations = (buttons & 0x0F ? 5 : 10) * 2;
44 // In each group, each buttons define the delay between LED toggles
45 // - 1st/5th button: 200ms
46 // - 2nd/6th button: 400ms
47 // - 3rd/7th button: 800ms
48 // - 4th/8th button: 1600ms
49 uint16_t delay = (buttons & 0x11 ? 200 : buttons & 0x22 ? 400 : buttons & 0x44 ? 800 : 1600);
50 while (iterations--)
51 {
52 led.toggle();
53 time::delay_ms(delay);
54 }
55}
56
57static const uint8_t EVENT_QUEUE_SIZE = 32;
58
59int main() __attribute__((OS_main));
60int main()
61{
63
64 // Prepare event queue
65 EVENT buffer[EVENT_QUEUE_SIZE];
66 containers::Queue<EVENT> event_queue{buffer};
67
68 // Create and register event generator
69 EventGenerator generator{event_queue};
71
72 // Setup PCI interrupts
74 signal.enable_pins_(0xFF);
75 signal.enable_();
76
77 // Setup LED pin as output
79
80 // Enable interrupts at startup time
81 sei();
82
83 // Event Loop
84 while (true)
85 {
86 EVENT event = containers::pull(event_queue);
87 if (event.type() == BUTTON_EVENT)
88 // Invert levels as 0 means button pushed (and we want 1 instead)
89 blink(event.value() ^ 0xFF);
90 }
91 return 0;
92}
Queue of type T_ items.
Definition: queue.h:59
bool push_(TREF item)
Push item to the end of this queue, provided there is still available space in its ring buffer.
A standard Event as managed by FastArduino event API.
Definition: events.h:144
Handler of a Pin Change Interrupt vector.
Definition: pci.h:177
void enable_pins_(uint8_t mask)
Enable pin change interrupts for several pins of this PCINT.
Definition: pci.h:441
void enable_()
Enable pin change interrupts for this PCISignal.
Definition: pci.h:366
Support for events management.
Port
Defines all available ports of the target MCU.
Definition: empty.h:49
DigitalPin
Defines all available digital input/output pins of the target MCU.
Definition: empty.h:56
T pull(Queue< T, TREF > &queue)
Pull an item from the beginning of queue.
Definition: queue.h:556
Defines all API to handle events within FastArduino programs.
Definition: events.h:82
DELAY_PTR delay
Delay program execution for the given amount of milliseconds.
Definition: time.cpp:19

In this example, 8 buttons are connected to one port and generate Pin Change Interrupts when pressed. The registered ISR generates an event for each pin change; the main event loop pulls one event from the events queue and starts a blinking sequence of the LED connected to D13 (Arduino UNO); the selected sequence (number of blinks and delay between blinks) depends on the pressed button.

First off, you need to include the heeader with FastArduino Events API:

Then we will be using namespace events in our example:

using namespace events;

Events API defines template<typename T> class Event<T> which basically contains 2 properties:

  • type() which is an uint8_t value used to discriminate different event types. FastArduino predefines a few event types and enables you to define your own types as you need,
  • value() is a T instance which you can use any way you need; each event will transport such a value. If you do not need any additional information on your own events, then you can use void for T.

In the example, we use events to transport the state of all 8 buttons (as a uint8_t):

using EVENT = Event<uint8_t>;

We also define a specific event type when buttons state changes:

static constexpr const uint8_t BUTTON_EVENT = Type::USER_EVENT;

Note that Type is a namespace inside events namespace, which lists FastArduino pre-defined Event types as constants:

const uint8_t NO_EVENT = 0;
const uint8_t WDT_TIMER = 1;
const uint8_t RTT_TIMER = 2;
// User-defined events start here (in range [128-255]))
const uint8_t USER_EVENT = 128;

Numbers from 3 to 127 are reserved for FastArduino future enhancements.

The we define the class that will handle Pin Change Interrupts and generate events:

class EventGenerator
{
public:
EventGenerator(containers::Queue<EVENT>& event_queue):event_queue_{event_queue}, buttons_{0x00, 0xFF} {}
void on_pin_change()
{
event_queue_.push_(EVENT{BUTTON_EVENT, buttons_.get_PIN()});
}
private:
containers::Queue<EVENT>& event_queue_;
};

Note the use of containers::Queue<EVENT> that represents a ring-buffer queue to which events are pushed when the state of buttons are changing, and from which events are pulled from the main event loop (as shown later).

The most interesting code is the single line:

event_queue_.push_(EVENT{BUTTON_EVENT, buttons_.get_PIN()});

where a new EVENT is instantiated, its value is the current level of all 8 buttons pins; then the new event is pushed to the events queue.

As usual, we have to register this handler to the right PCI ISR:

REGISTER_PCI_ISR_METHOD(PCI_NUM, EventGenerator, &EventGenerator::on_pin_change, board::InterruptPin::D0_PD0_PCI2)

Next we define the size of the events queue:

static const uint8_t EVENT_QUEUE_SIZE = 32;

This must be known at compile-time as we do not want dynamic memory allocation in an embedded program.

The actual events queue is instantiated inside main():

EVENT buffer[EVENT_QUEUE_SIZE];
containers::Queue<EVENT> event_queue{buffer};

Note that this is done in 2 steps:

  1. A buffer of the required size is allocated (in the stack for this example); the actual size, in this example, is 64 bytes, i.e. 32 times 1 byte (event type) + 1 byte (event value).
  2. A Queue is instantiated for this buffer; after this step, you should never directly access buffer from your code.

Finally, the main() function has the infinite event loop:

while (true)
{
EVENT event = containers::pull(event_queue);
if (event.type() == BUTTON_EVENT)
// Invert levels as 0 means button pushed (and we want 1 instead)
blink(event.value() ^ 0xFF);
}

The call to containers::pull(event_queue) is blocking until an event is available in the queue, i.e. has been pushed by EventGenerator::on_pin_change(). While waiting, this call uses time::yield() which may put the MCU in sleep mode (see Power management tutorial later).

Once an event is pulled from the event queue, we check its type and call blink() function with the event value, i.e. the state of buttons when Pin Change Interrupt has occurred.

The blink() function simply loops for the requested number of iterations and blinking delay, and returns only when the full blinking sequence is finished. This function is very simple and there is no need to explain it further.

Event Dispatcher

In the previous example, only one type of event is expected inside the event loop. In more complex applications though, many more types of events are foreseeable and we may end up with an event loop like:

while (true)
{
EVENT event = containers::pull(event_queue);
switch (event.type())
{
case EVENT_TYPE_1:
// Do something
break;
case EVENT_TYPE_2:
// Do something else
break;
case EVENT_TYPE_3:
// Do yet something else
break;
...
}
}

This generally makes code less readable and more difficult to maintain.

FastArduino API supports an event dispatching approach, where you define EventHandler classes and register instances for specific event types:

class MyHandler: public EventHandler<EVENT>
{
public:
MyHandler() : EventHandler<EVENT>{Type::USER_EVENT} {}
virtual void on_event(const EVENT& event) override
{
// Do something
}
};
int main()
{
...
// Prepare Handlers
MyHandler handler;
...
// Prepare Dispatcher and register Handlers
Dispatcher<EVENT> dispatcher;
dispatcher.insert(handler);
...
}
void insert(T &item) INLINE
Insert item at the beginning of this list.
Definition: linked_list.h:103
Utility to dispatch an event to a list of EventHandlers that are registered for its type.
Definition: events.h:242
Abstract event handler, used by Dispatcher to get called back when an event of the expected type is d...
Definition: events.h:288

With events::Dispatcher, you can register one handler for each event type, and then the main event loop is reduced to the simple code below:

while (true)
{
EVENT event = pull(event_queue);
dispatcher.dispatch(event);
}
void dispatch(const EVENT &event)
Dispatch the given event to the right EventHandler, based on the event type.
Definition: events.h:258

The right event handler gets automatically called for each pulled event.

Note that events::EventHandler uses a virtual method, hence this may have an impact on memory size consumed in Flash (vtables) and also on method call time.

Scheduler & Jobs

Some types of events are generated by FastArduino features. In particular, watchdog and realtime timer features can be setup to trigger some time events at given periods. From these events, FastArduino offers a events::Scheduler API, that allows you to define periodic events::Jobs.

The next code demonstrates a new way of blinking a LED by using watchdog generated events and associated jobs:

1#include <fastarduino/gpio.h>
5
6using namespace events;
7using EVENT = Event<void>;
8
9// Define vectors we need in the example
11
12static const uint32_t PERIOD = 1000;
13
14class LedBlinkerJob: public Job
15{
16public:
17 LedBlinkerJob() : Job{0, PERIOD}, led_{gpio::PinMode::OUTPUT} {}
18
19protected:
20 virtual void on_schedule(UNUSED uint32_t millis) override
21 {
22 led_.toggle();
23 }
24
25private:
27};
28
29// Define event queue
30static const uint8_t EVENT_QUEUE_SIZE = 32;
31static EVENT buffer[EVENT_QUEUE_SIZE];
32static containers::Queue<EVENT> event_queue{buffer};
33
34int main() __attribute__((OS_main));
35int main()
36{
38 // Enable interrupts at startup time
39 sei();
40
41 // Prepare Dispatcher and Handlers
42 Dispatcher<EVENT> dispatcher;
44 Scheduler<watchdog::Watchdog<EVENT>, EVENT> scheduler{watchdog, Type::WDT_TIMER};
45 dispatcher.insert(scheduler);
46
47 LedBlinkerJob job;
48 scheduler.schedule(job);
49
50 // Start watchdog
52
53 // Event Loop
54 while (true)
55 {
56 EVENT event = pull(event_queue);
57 dispatcher.dispatch(event);
58 }
59}
Abstract class holding some action to be executed at given periods of time.
Definition: scheduler.h:161
Schedule jobs at predefined periods of time.
Definition: scheduler.h:89
Simple API to use watchdog timer as a clock for events generation.
Definition: watchdog.h:302
#define UNUSED
Specific GCC attribute to declare an argument or variable unused, so that the compiler does not emit ...
Definition: defines.h:45
@ TO_64ms
Watchdog timeout 64 ms.
Support for jobs scheduling.
#define REGISTER_WATCHDOG_CLOCK_ISR(EVENT)
Register the necessary ISR (Interrupt Service Routine) for a watchdog::Watchdog to work properly.
Definition: watchdog.h:36

FastArduino Scheduler API is defined in its own header, but under the same events namespace:

This API is based on FastArduino events management, hence we need to define the Event type we need:

using EVENT = Event<void>;

FastArduino Scheduler does not use Event'svalue, hence we can simply use void, so that one EVENT will be exactly one byte in size.

Next step consists in registering Watchdog clock ISR:

We then define a Job subclass that will be called back at the required period:

class LedBlinkerJob: public Job
{
public:
LedBlinkerJob() : Job{0, PERIOD}, led_{gpio::PinMode::OUTPUT} {}
protected:
virtual void on_schedule(UNUSED uint32_t millis) override
{
led_.toggle();
}
private:
};

Job constructor takes 2 arguments:

  • next: the delay in ms after which this job will execute for the first time; 0 means this job should execute immediately
  • period: the period in ms at which the job shall be re-executed; 0 means this job will execute only once.

In this example, we set the job period to 1000ms, i.e. the actual blink period will be 2s.

The virtual method Job::on_schedule() will get called when the time to executed has elapsed, not necessarily at the exact expected time (the exact time depends on several factors exposed hereafter).

In the example, we then declare the event queue, this time as static memory (not in the stack):

static const uint8_t EVENT_QUEUE_SIZE = 32;
static EVENT buffer[EVENT_QUEUE_SIZE];
static containers::Queue<EVENT> event_queue{buffer};

In main(), we create a Dispatcher that will be used in the event loop, a Watchdog that will generate and queue Type::WDT_TIMER events on each watchdog timeout, and a Scheduler that will get notified of Type::WDT_TIMER events and will use the created Watchdog as a clock, in order to determine which Jobs to call.

Dispatcher<EVENT> dispatcher;
Scheduler<watchdog::Watchdog<EVENT>, EVENT> scheduler{watchdog, Type::WDT_TIMER};
dispatcher.insert(scheduler);

Note the last code line, which links the Scheduler to the Dispatcher so it can handle proper events.

Next, we instantiate the job that will make the LED blink, and schedule with Scheduler:

LedBlinkerJob job;
scheduler.schedule(job);

We can then start the watchdog with any suitable timeout value (the selected value impacts the accuracy of the watchdog clock):

Finally, then event loop is quite usual:

while (true)
{
EVENT event = pull(event_queue);
dispatcher.dispatch(event);
}

Jobs scheduling accuracy depends on several factors:

  • the accuracy of the CLOCK used for Scheduler; FastArduino provides two possible clocks (but you may also provide your own): watchdog::Watchdog and timer::RTT.
  • the CPU occupation of your main event loop, which may trigger a Job long after its expected time.

As mentioned above, Scheduler can also work with timer::RTT, that would mean only a few changes in the previous example, as shown in the next snippet, only showing new lines of code:

...
static const uint16_t RTT_EVENT_PERIOD = 1024;
REGISTER_RTT_EVENT_ISR(0, EVENT, RTT_EVENT_PERIOD)
...
void main()
{
...
...
Scheduler<timer::RTT<board::Timer::TIMER0>, EVENT> scheduler{rtt, Type::RTT_TIMER};
...
rtt.begin();
...
}
Utility to generate events from an RTT instance at a given period.
#define REGISTER_RTT_EVENT_ISR(TIMER_NUM, EVENT, PERIOD)
Register the necessary ISR (Interrupt Service Routine) for a timer::RTT to work properly,...

Since this code uses a realtime timer, two lines of code instantiate, then start one such timer:

In addition, we use timer::RTTEventCallback utility class, which purpose is to generate events from RTT ticks:

Here we create a callback that will generate an event every RTT_EVENT_PERIOD milliseconds.

We also need to register the right ISR for the RTT and the callback:

REGISTER_RTT_EVENT_ISR(0, EVENT, RTT_EVENT_PERIOD)

Now the scheduler can be instantiated with the RTT instance as its clock and Type::RTT_TIMER as the type of events to listen to:

Scheduler<timer::RTT<board::Timer::TIMER0>, EVENT> scheduler{rtt, Type::RTT_TIMER};

Advanced: Power Management

AVR MCU support various sleep modes that you can use to reduce power consumption of your circuits.

Each sleep mode has distinct characteristics regarding:

  • maximum current consumed
  • list of wake-up signals (interrupts)
  • time to wake up from sleep

FastArduino support for AVR power modes is quite simple, gathered in namespace power and consists in a single class power::Power with 3 static methods.

In addition, the available sleep modes for a given MCU are defined in board::SleepMode enumerated type.

Here is a simple example:

1#include <fastarduino/gpio.h>
2#include <fastarduino/power.h>
4
5// Define vectors we need in the example
7
8int main() __attribute__((OS_main));
9int main()
10{
12 sei();
13
15
18
19 while (true)
20 {
21 led.toggle();
22 power::Power::sleep(board::SleepMode::POWER_DOWN);
23 }
24}

You may have recognized it: this is the example presented in the Watchdog section of this tutorial.

In the main loop, we just toggle the LED pin and go to board::SleepMode::POWER_DOWN sleep mode (the mode that consumes the least current of all).

The Power::sleep() method will return only when the MCU is awakened by an external signal, in this example, the signal used is the watchdog timeout interrupt (every 500ms).

Note that the time::yield() method just calls Power::sleep() (with current default sleep mode) and this method is used by other FastArduino API on several occasions whenever these API need to wait for something:

Hence when using any of these API, it is important to select the proper sleep mode, based on your power consumption requirements.

Advanced: EEPROM

All AVR MCU include an EEPROM (Electrically Erasable Programmable Read-Only Memory) which can be useful for storing data that:

  • is quite stable but may change from time to time
  • is fixed but different for every MCU (for a large series of similar circuits)

Typical examples of use of EEPROM cells include (but are not limited to):

  • Circuit unique ID (e.g. used in a network of devices)
  • WIFI credentials
  • Calibration value (for internal RC clock)
  • Sequence of tones frequencies & periods for an alarm melody

In these examples, writing EEPROM cells would happen only once (or a few times), while reading would occur at startup.

Reading EEPROM content

Often, writing EEPROM can be done directly through an ISP programmer (UNO USB programming does not support EEPROM writing) and only reading is needed in a program. The next example illustrates this situation.

3#include <fastarduino/time.h>
4
5// Board-dependent settings
6static constexpr const board::Timer NTIMER = board::Timer::TIMER1;
7static constexpr const board::PWMPin OUTPUT = board::PWMPin::D9_PB1_OC1A;
8
10using namespace eeprom;
12
13struct TonePlay
14{
15 Tone tone;
16 uint16_t ms;
17};
18
19// Melody to be played
20TonePlay music[] EEMEM =
21{
22 // Intro
23 {Tone::A1, 500},
24 {Tone::A1, 500},
25 {Tone::A1, 500},
26 {Tone::F1, 350},
27 {Tone::C2, 150},
28 {Tone::A1, 500},
29 {Tone::F1, 350},
30 {Tone::C2, 150},
31 {Tone::A1, 650},
32
33 // Marker for end of melody
34 {Tone::USER0, 0}
35};
36
37int main()
38{
39 sei();
40 GENERATOR generator;
41 TonePlay* play = music;
42 while (true)
43 {
44 TonePlay tone;
45 EEPROM::read(play, tone);
46 if (tone.tone == Tone::USER0)
47 break;
48 generator.start_tone(tone.tone);
49 time::delay_ms(tone.ms);
50 generator.stop_tone();
51 ++play;
52 }
53}
API class for tone generation to a buzzer (or better an amplifier) connected to pin OUTPUT.
Definition: tones.h:183
API to handle EEPROM access in read and write modes.
Tone
This enum defines all possible audio tones that can be generated.
Definition: tones.h:57
Defines the API for accessing the EEPROM embedded in each AVR MCU.
Definition: eeprom.h:90
API to handle tones (simple square waves) generation to a buzzer.

This example plays a short melody, stored in EEPROM, to a buzzer wired on Arduino UNO D9 pin. It uses FastArduino devices::audio::ToneGenerator support to generate square waves at the required frequencies, in order to generate a melody intro that Star Wars fan will recognize!

Past the headers inclusion, UNO-specific definitions (timer and output pin) and various using directives, the example defines struct TonePlay which embeds a Tone (frequency) and a duration, which is then used in an array of notes to be played:

struct TonePlay
{
Tone tone;
uint16_t ms;
};
// Melody to be played
TonePlay music[] EEMEM =
{
// Intro
{Tone::A1, 500},
{Tone::A1, 500},
{Tone::A1, 500},
{Tone::F1, 350},
{Tone::C2, 150},
{Tone::A1, 500},
{Tone::F1, 350},
{Tone::C2, 150},
{Tone::A1, 650},
// Marker for end of melody
{Tone::USER0, 0}
};

Note that music array is defined with EEMEM attribute, which tells the compiler that this array shall be stored in EEPROM and not in Flash or SRAM.

The advantages of storing the melody in EEPROM are:

  • it can easily be changed to another melody without any change to the program (flash)
  • it does not use Flash memory for storage
  • it does not use SRAM memory in runtime (although it might, if entirely read from EEPROM to SRAM, but why would you do that?)

Note, however, that for some Arduino boards (e.g. UNO), storing a melody to the EEPROM may require using a specific device, called an ISP programmer. This is beacuse some Arduino bootloaders do not support EEPROM upload.

The important part of the program is the loop where each note is read for EEPROM before being played:

TonePlay* play = music;
while (true)
{
TonePlay tone;
EEPROM::read(play, tone);

In this snippet, play is used just as an "index" (an address actually) to the current note in music array. Beware that play cannot be used directly in your program because it points nowhere actually, really.

The interesting bit here is EEPROM::read(play, tone); which uses play address as a reference to the next note in EEPROM, reads the EEPROM content at this location and copies it into tone.

At the end of the loop, play address gets incremented in order to point to the next note of music in EEPROM:

++play;
}

At this point, you may wonder when the while (true) loop exits. Since the program does not in advance what melody it is going to play, it it not possible to use a for loop with an upper boundary as we could do if music was directly in SRAM.

Thus, we use another way to determine the end of the meoldy to play: we use a special Tone, Tone::END, which is just a marker of the end of the melody; we check it for every tone read from EEPROM:

EEPROM::read(play, tone);
if (tone.tone == Tone::END)
break;

All this is all good but how do we upload the melody to the EEPROM the first time?

FastArduino make system takes care of this:

  1. make build not only generates a .hex file for code upload to the flash, it also generates a .eep file with all content to be uploaded to EEPROM: this is based on all variables defined with attribute EEMEM in your program.
  2. make eeprom uses the above .eep file an upload it to the MCU to program EEPROM content.

IMPORTANT: note that, as relevant as this example can be for this tutorial, you do not need to write all this code, with FastArduino, to play a sequence of tones: you may use TonePlayer class that odes it all for you, and can handle arrays stored in SRAM, EEPROM or Flash memory.

Writing content to EEPROM

There are also programs that may need to store content to EEPROM during their execution. This is possible with FastArduino EEPROM support.

Please do note, however, that the number of writes an EEPROM can support is rather limited (100'000 as per AVR MCU datasheet), hence you should refrain from writing too many times to the EEPROM.

Also, do note that writing a byte to the EEPROM is not the same as writing a byte to SRAM, this is much slower (typically between 1.8ms and 3.4ms).

The following example stores a WIFI access point name and password in EEPROM; through USB connected to a serial console, it asks the user if she wants to use current settings (provided there are already WIFI settings in EEPROM), and if not, it asks the user to fill in new WIFI credentials and stores them to EEPROM for next time the program will be reset.

1#include <string.h>
3#include <fastarduino/uart.h>
4
5// Define vectors we need in the example
7static const board::USART USART = board::USART::USART0;
9
10// Buffers for UART
11static const uint8_t INPUT_BUFFER_SIZE = 64;
12static const uint8_t OUTPUT_BUFFER_SIZE = 64;
13static char input_buffer[INPUT_BUFFER_SIZE];
14static char output_buffer[OUTPUT_BUFFER_SIZE];
15
16// EEPROM stored information
17static const uint8_t MAX_LEN = 64;
18char wifi_name[MAX_LEN+1] EEMEM = "";
19char wifi_password[MAX_LEN+1] EEMEM = "";
20
21using namespace eeprom;
22using streams::endl;
23using streams::flush;
24using streams::noskipws;
25using streams::skipws;
26
27int main()
28{
30 sei();
31
32 // Start UART
33 serial::hard::UART<USART> uart{input_buffer, output_buffer};
34 uart.begin(115200);
35
36 streams::istream in = uart.in();
37 streams::ostream out = uart.out();
38
39 // Get current WIFI name/password from EEPROM
40 char wifi[MAX_LEN+1];
41 EEPROM::read(wifi_name, wifi, MAX_LEN+1);
42 char password[MAX_LEN+1];
43 EEPROM::read(wifi_password, password, MAX_LEN+1);
44
45 // If WIFI present check if user wants to keep it
46 bool ask = true;
47 if (strlen(wifi))
48 {
49 char answer;
50 out << F("Do you want to use WIFI `") << wifi << F("`? [Y/n] :") << flush;
51 in >> skipws >> answer;
52 in.ignore(0, '\n');
53 ask = (toupper(answer) == 'N');
54 }
55
56 // Ask for WIKI name & password if needed
57 if (ask)
58 {
59 out << F("Enter WIFI name: ") << flush;
60 in.getline(wifi, MAX_LEN+1);
61 out << F("Enter WIFI password: ") << flush;
62 in.getline(password, MAX_LEN+1);
63 // Store new settings to EEPROM
64 EEPROM::write(wifi_name, wifi, strlen(wifi) + 1);
65 EEPROM::write(wifi_password, password, strlen(password) + 1);
66 }
67
68 // Start real program here, using wifi and password
69 //...
70 out << F("WIFI: ") << wifi << endl;
71 out << F("Password: ") << password << endl;
72}
Hardware serial receiver/transceiver API.
Definition: uart.h:420
void begin(uint32_t rate, Parity parity=Parity::NONE, StopBits stop_bits=StopBits::ONE)
Enable the receiver/transceiver.
Definition: uart.h:456
istream & ignore(size_t n=1, int delim=istreambuf::EOF)
Extract characters from this input stream and discards them, until either n characters have been extr...
Definition: streams.h:464
istream & getline(char *str, size_t n, char delim='\n')
Extract characters from this input stream and stores them as a C-string, until either (n - 1) charact...
Definition: streams.h:445
void flush(FSTREAM &stream)
Manipulator for an output stream, which will flush the stream buffer.
Definition: streams.h:716
void endl(FSTREAM &stream)
Manipulator for an output stream, which will insert a new-line character and flush the stream buffer.
Definition: streams.h:725
#define REGISTER_UART_ISR(UART_NUM)
Register the necessary ISR (Interrupt Service Routine) for an serial::hard::UART to work correctly.
Definition: uart.h:59

Most of this snippet is code that deals with input/output streams through UART; we will not explain it here as it is already demonstrated in a previous tutorial section.

We will focus on EEPROM sections:

static const uint8_t MAX_LEN = 64;
char wifi_name[MAX_LEN+1] EEMEM = "";
char wifi_password[MAX_LEN+1] EEMEM = "";

Here we define wifi_name and wifi_password as strings of 64 characters maximum (+ terminating ‘’\0'`), stored in EEPROM and both "initialized" as empty strings.

Initialization here does not mean that these variables will automatically be empty string the first time this program is executed. It means that after make build is invoked, an .eep file is generated in order to be uploaded to EEPROM, and that file contains both variables as empty strings. Hence, provided you have uploaded that .eep file to your MCU EEPROM, both variables will then be empty strings when first read from EEPTOM by your program.

In the main() function, we then read both variables from EEPROM into local variables:

char wifi[MAX_LEN+1];
EEPROM::read(wifi_name, wifi, MAX_LEN+1);
char password[MAX_LEN+1];
EEPROM::read(wifi_password, password, MAX_LEN+1);

Here we use EEPROM::read(uint16_t address, T* value, uint16_t count) function template, instantiated with T = char: we have to specify the count of characters to be read, i.e. the maximum allowed string size, plus the null termination.

As in the previous example, we use wifi_name and wifi_password variables to provide the address in EEPROM, of the location of these 2 strings.

Later in main(), if the user decided to enter a new WIFI netwrok name and pasword, those are written immediately to EEPROM:

EEPROM::write(wifi_name, wifi, strlen(wifi) + 1);
EEPROM::write(wifi_password, password, strlen(password) + 1);

The EEPROM::write() function arguments are similar to EEPROM::read() arguments.

Note that here, we do not have to write the full string allocated space to the EEPROM but only the currently relevant count of characters, e.g. if wifi was input as "Dummy", we will write only 6 characters for wifi_name in EEPROM, i.e. the actual string length 5 + 1 for null termination. This is particularly improtant to do this as writing bytes to EEPROM is very slow (up to ~4ms) so we want to limit that writing time to the strict minimum necessary.

Because writing content to the EEPROM is a very slow operation, there are situations where you do not want to "stop" your program running because it is waiting for EEPROM::write() operations to complete.

This is the reason why eeprom namespace also defines a QueuedWriter class that uses interruptions to write content to EEPROM, allowing your main() function to run normally during EEPROM writes. For more details, please check the API.

Advanced: SPI devices example

FastArduino supports SPI (Serial Peripheral Interface) as provided by all AVR MCU (ATmega MCU support SPI natively, ATtiny MCU support it through their USI, Universal Serial Interface).

FastArduino also brings specific support to some SPI devices:

  • NRF24L01P (radio-frequency device)
  • WinBond chips (flash memory devices)
  • MCP3001-2-4-8, MCP3201-2-4-8, MCP3301-2-4 (ADC chips)

Basically, FastArduino core SPI API is limited to the following:

  • spi::init() function to call once before any use of SPI in your program
  • spi::SPIDevice class that is the abstract base class of all concrete SPI devices

Although very important, this sole API is useless for any tutorial example! If you want to develop an API for your own SPI device then please refer to spi::SPIDevice API documentation.

Hence, to illustrate SPI usage in this tutorial, we will focus on a concrete example with the WinBond memory chip.

The following example reads, erases, writes and reads again flash memory:

2#include <fastarduino/time.h>
3#include <fastarduino/uart.h>
4
5static const uint8_t OUTPUT_BUFFER_SIZE = 64;
6static char output_buffer[OUTPUT_BUFFER_SIZE];
7constexpr const board::USART UART = board::USART::USART0;
10
11using namespace streams;
12
13constexpr const board::DigitalPin CS = board::DigitalPin::D7_PD7;
14constexpr const size_t DATA_SIZE = 256;
15static uint8_t data[DATA_SIZE];
16constexpr const uint32_t PAGE = 0x010000;
17
18int main()
19{
21 sei();
22
23 serial::hard::UATX<UART> uart{output_buffer};
24 uart.begin(115200);
25 ostream out = uart.out();
26
27 // Initialize SPI and device
28 spi::init();
30 time::delay_ms(1000);
31 out << F("S: ") << hex << flash.status().value << endl;
32
33 // Read and display one page of flash memory
34 flash.read_data(PAGE, data, sizeof data);
35 out << F("RD, S: ") << hex << flash.status().value << endl;
36 out << F("Pg RD:") << endl;
37 for (uint16_t i = 0; i < sizeof data; ++i)
38 {
39 out << hex << data[i] << ' ';
40 if ((i + 1) % 16 == 0) out << endl;
41 }
42 out << endl;
43
44 // Erase one page of flash memory before writing
45 flash.enable_write();
46 flash.erase_sector(PAGE);
47 out << F("Erase, S: ") << hex << flash.status().value << endl;
48 flash.wait_until_ready(10);
49 out << F("Wait, S: ") << hex << flash.status().value << endl;
50
51 // Write one page of flash memory
52 for (uint16_t i = 0; i < sizeof data; ++i) data[i] = uint8_t(i);
53 flash.enable_write();
54 flash.write_page(PAGE, data, (DATA_SIZE >= 256 ? 0 : DATA_SIZE));
55 out << F("Write, S: ") << hex << flash.status().value << endl;
56 flash.wait_until_ready(10);
57 out << F("Wait, S: ") << hex << flash.status().value << endl;
58
59 // Read back and display page of flash memory just written
60 for (uint16_t i = 0; i < sizeof data; ++i) data[i] = 0;
61 flash.read_data(PAGE, data, sizeof data);
62 out << F("Read, S: ") << hex << flash.status().value << endl;
63 out << F("Pg RD:") << endl;
64 for (uint16_t i = 0; i < sizeof data; ++i)
65 {
66 out << hex << data[i] << ' ';
67 if ((i + 1) % 16 == 0) out << endl;
68 }
69 out << endl;
70}
SPI device driver for WinBond flash memory chips, like W25Q80BV (8 Mbit flash).
Definition: winbond.h:55
USART
Defines all USART modules of target MCU.
Definition: empty.h:105
Defines API to handle flash memory storage.
Definition: flash.h:31
void init()
This function must be called once in your program, before any use of an SPI device.
Definition: spi.cpp:20
void hex(FSTREAM &stream)
Manipulator for an output or input stream, which will set the base, used to represent (output) or int...
Definition: ios.h:774
API to handle WinBond flash memory chips through SPI interface.

The first important piece of code initializes the SPI system and the WinBond device:

// Initialize SPI and device
out << F("S: ") << hex << flash.status().value << endl;

Note that devices::WinBond is a template that takes as parameter the board::DigitalPin that will be used as "Chip Select" pin (CS, part of SPI wiring).

status() returns the WinBond status as specified in the chip datasheet and implemented in FastArduino winbond.h.

Next code piece is reading a part of a flash page from the device:

flash.read_data(PAGE, data, sizeof data);

There is nothing special to mention here, the API is straightforward.

Then the example write one page of flash memory. This must be done in 2 steps:

  1. Erase flash page
  2. Write flash page
// Erase one page of flash memory before writing
flash.enable_write();
flash.erase_sector(PAGE);
out << F("Erase, S: ") << hex << flash.status().value << endl;
flash.wait_until_ready(10);
out << F("Wait, S: ") << hex << flash.status().value << endl;

Any write action must be preceded with a call to enable_write() and followed by wait_until_ready() (because writing to flash memory is rather long, all timing values can be found in the datasheet). FastArduino provides API for every case.

Erasing, like writing, cannot be done on single bytes, but must be done on packs of bytes (e.g. pages, sectors, blocks, as defined in WinBond datasheet).

Here is the code performing the writing:

// Write one page of flash memory
for (uint16_t i = 0; i < sizeof data; ++i) data[i] = uint8_t(i);
flash.enable_write();
flash.write_page(PAGE, data, (DATA_SIZE >= 256 ? 0 : DATA_SIZE));
out << F("Write, S: ") << hex << flash.status().value << endl;
flash.wait_until_ready(10);
out << F("Wait, S: ") << hex << flash.status().value << endl;

There is only one API, writing at most one page (256 bytes) at once. Please note the last argument of write_page() which is the size of data to write. For performance reasons, size is an uint8_t, consequently, 0 is used to mean 256 bytes, i.e. a complete page.

The last part of the example reads again the data page and displays it for control. This is the same code as the reading code presented above.

Advanced: I2C devices example

FastArduino supports I2C (Inter-Integrated Circuit) as provided by all AVR MCU (ATmega MCU support I2C natively, ATtiny MCU support it through their USI, Universal Serial Interface). I2C is also often called TWI for Two-Wires Interface.

FastArduino also brings specific support to several I2C devices:

  • DS1307 (real-time clock)
  • MPU6050 (accelerometer and gyroscope)
  • HMC5883L (compass)
  • MCP23008 (8-Bit I/O Expander)
  • MCP23017 (16-Bit I/O Expander)
  • VL53L0X (laser Time-of-Flight range sensor chip)

FastArduino core I2C API is defined in several headers (namespace i2c) and made of a few types:

  • i2c.h contains a few constants and enumerations used everywhere else in the I2C API
  • i2c_handler.h defines several template classes defining different kinds of "I2C Manager" which are central to the API
  • i2c_device.h mainly defines i2c::I2CDevice template class, which is the abstract base class of all concrete I2C devices

Other more specific headers exist but shall not be directly included in programs.

In FastArduino, I2C communication is centralized by an I2C Manager; there are several flavors of I2C Manager defined in FastArduino, with distinct characteristics such as:

  • synchronous (all MCU) or asynchronous (ATmega only)
  • I2C mode supported (fast 400kHz or standard 100kHz)
  • policy to follow in case of failure during an I2C transaction
  • ...

In this tutorial, we will use the simplest I2C Manager provided by FastArduino: i2c::I2CSyncManager, which handles only synchronous (blocking) I2C operations.

There are also asynchronous I2C Managers but they will not be explained here. If you want to learn more about there, please take a look at the API and examples using it. The asynchronous API is heavily based on FastArduino Futures API support.

In order to illustrate concretely I2C API usage in this tutorial, we will focus on a concrete example with the DS1307 RTC chip. If you want to develop an API for your own I2C device then please refer to i2c::I2CDevice API documentation and the dedicated tutorial.

The following example reads the current clock time from a DS1307 chip:

1#include <fastarduino/time.h>
4#include <fastarduino/uart.h>
5
6static constexpr const board::USART UART = board::USART::USART0;
7static constexpr const uint8_t OUTPUT_BUFFER_SIZE = 64;
8static char output_buffer[OUTPUT_BUFFER_SIZE];
9
13
14using devices::rtc::DS1307;
15using devices::rtc::tm;
16using namespace streams;
17
18int main() __attribute__((OS_main));
19int main()
20{
22 sei();
23 serial::hard::UATX<UART> uart{output_buffer};
24 uart.begin(115200);
25 ostream out = uart.out();
26
28 MANAGER manager;
29 manager.begin();
30 DS1307 rtc{manager};
31
32 tm now;
33 rtc.get_datetime(now);
34 out << dec << F("RTC: [")
35 << uint8_t(now.tm_wday) << ']'
36 << now.tm_mday << '.'
37 << now.tm_mon << '.'
38 << now.tm_year << ' '
39 << now.tm_hour << ':'
40 << now.tm_min << ':'
41 << now.tm_sec << endl;
42
43 manager.end();
44}
void begin()
Prepare and enable the MCU for I2C transmission.
Synchronous I2C Manager for ATmega architecture.
API to handle Real-Time Clock DS1307 I2C chip.
#define REGISTER_FUTURE_NO_LISTENERS()
Register no callback at all to any Future notification.
Definition: future.h:166
Common I2C Manager API.
Defines all API for all external devices supported by FastArduino.
void dec(FSTREAM &stream)
Manipulator for an output or input stream, which will set the base, used to represent (output) or int...
Definition: ios.h:765

This example has 3 important parts.

The first part is the I2C and the RTC device initialization:

MANAGER manager;
manager.begin();
DS1307 rtc{manager};

i2c::I2CSyncManager is a template class with a parameter of type i2c::I2CMode, which can be anyone of:

The mode selection depends on all devices you wire on the I2C bus, if one is using standard mode, then all the bus must be set to standard mode. Since DS1307 chip does not support fast mode, its device forces standard mode, and that mode must be used for the I2CSyncManager.

For convenience reasons, we define the MANAGER type alias once and then reuse it where needed.

It is important to ensure begin() has been called on i2c::I2CSyncManager before any use of the I2C bus by devices.

Next code piece is reading current clock date and time from the RTC chip:

tm now;
rtc.get_datetime(now);

In that code, tm is a structure containing all fields related to a date/time, get_datetime() just fills it with every information, as can be seen in the following lines of code that display the current date and time.

The last line just stops the I2C circuitry of the AVR MCU.

manager.end();

Advanced: software UART

AVR ATtiny MCU do not include hardware UART. For these MCU, UART can be simulated if needed. FastArduino has support for software UART. That support can also be useful with ATmega MCU, where one would need more UART ports than available.

It is important to note that it is not possible, with software UART, to reach bitrates as high as with hardware UART.

Software UATX

Here a simple example using software UATX for output through USB on an Arduino UNO:

2
3constexpr const board::DigitalPin TX = board::DigitalPin::D1_PD1;
4
6
7static constexpr const uint8_t OUTPUT_BUFFER_SIZE = 64;
8static char output_buffer[OUTPUT_BUFFER_SIZE];
9
10using namespace streams;
11
12int main()
13{
15 sei();
16
17 serial::soft::UATX<TX> uart{output_buffer};
18 uart.begin(115200);
19
20 ostream out = uart.out();
21 uint16_t value = 0x8000;
22 out << F("value = 0x") << hex << value
23 << F(", ") << dec << value
24 << F(", 0") << oct << value
25 << F(", B") << bin << value << endl;
26 return 0;
27}
Software-emulated serial transmitter API.
Definition: soft_uart.h:239
void begin(uint32_t rate, Parity parity=Parity::NONE, StopBits stop_bits=StopBits::ONE)
Enable the transmitter.
Definition: soft_uart.h:265
Software-emulated serial API.

Except for a few lines, this is the same example as one hardware UART example above in this tutorial.

The main difference is in serial::soft::UATX<TX> uart{output_buffer}; where TX is the board::DigitalPin where the serial output will be directed.

Besides, the same operations as for serial::hard::UATX are available, in particular output streams work the same with serial::soft::UATX.

Software UARX

There are also two classes, serial::soft::UARX_EXT and serial::soft::UARX_PCI, that work similarly as serial::hard::UARX:

2
3constexpr const board::InterruptPin RX = board::InterruptPin::D0_PD0_PCI2;
4#define PCI_NUM 2
5
6REGISTER_UARX_PCI_ISR(RX, PCI_NUM)
7
8// Buffers for UARX
9static const uint8_t INPUT_BUFFER_SIZE = 64;
10static char input_buffer[INPUT_BUFFER_SIZE];
11
12int main()
13{
15 sei();
16
17 // Start UART
19 serial::soft::UARX_PCI<RX> uarx{input_buffer, pci};
20 pci.enable();
21 uarx.begin(115200);
22
23 streams::istream in = uarx.in();
24
25 // Wait for a char
26 char value1;
27 in >> streams::skipws >> value1;
28
29 // Wait for an uint16_t
30 uint16_t value2;
31 in >> streams::skipws >> value2;
32
33 return 0;
34}
void begin(uint32_t rate, Parity parity=Parity::NONE, StopBits stop_bits=StopBits::ONE)
Enable the receiver.
Definition: soft_uart.h:651
#define REGISTER_UARX_PCI_ISR(RX, PCI_NUM)
Register the necessary ISR (Interrupt Service Routine) for an serial::soft::UARX to work correctly.
Definition: soft_uart.h:39

Note the following differences with a previous example using serial::hard::UARX:

  1. RX must be an interrupt pin (either board::InterruptPin or board::ExternalInterruptPin)
  2. REGISTER_UARX_PCI_ISR(RX, PCI_NUM) (or REGISTER_UARX_INT_ISR(RX, INT_NUM)) is needed to register an ISR on pin RX changes
  3. the uarx variable must be defined as a serial::soft::UARX_PCI if it is connected to an board::InterruptPin or a serial::soft::UARX_EXT if it is connected to an board::ExternalInterruptPin
  4. an interrupt handler must be setup (either interrupt::PCISignal or interrupt::INTSignal), passed to the constructor then enabled

Software UART

Finally, there are also two classes, serial::soft::UART_PCI and serial::soft::UART_EXT, that combine serial::soft::UATX and serial::soft::UARX_PCI or serial::soft::UARX_EXT, and work similarly as serial::hard::UART.