/att85

C++17 components to program ATtiny85 microcontrollers

Primary LanguageC++Boost Software License 1.0BSL-1.0

att85

It’s a collection of generic components written in C++17 to program ATtiny85 microcontrollers with a high level of abstraction, code reusability and high space(memory) efficiency.

Components

ssd1306

An abstraction to represent an OLED SSD1306 display that is used through the I2C communication interface.

Demo

#include <att85/ssd1306/display.hpp>
#include <att85/ssd1306/font/8x8/chars.hpp>

using namespace att85::ssd1306;

int main() {
    //Represents a SSD1306 display with resolution of 128x32 connected
    //to a ATtiny85 through the pins SDA(PB0) and SCL(PB2). This
    //display has an I2C address of the value 0x78, the screen is
    //cleared on the initialization(which means the ctor execution)
    //and the Charge Pump Regulator is used. Take a look at the class
    //template `display` to know how to customize these properties to
    //use your SSD1306 device.
    display_128x32<> disp;

    //It outputs a sequence of chars defined in compile-time at the
    //position page=0 and column=0 of the display. One font with
    //resolution 8x8 is used. Note, it's more efficient to use a
    //compile-time instead of a run-time sequence when the length of
    //the sequence isn't too much long.(You can consider a length less
    //than eleven(11) chars as a reference)
    disp.out<font::_8x8>(0, 0, ATT85_SSD1306_STR("alarm"));
    
    //Power on the display
    disp.on();
    
    while(true);
}

This program occupies 322 bytes of the program memory space using avr-g++ 8.2.0 with -Os.

The first step

The first step is construct an object that represents the display. The class template display has a collection of parameters to model the display that will be used in the circuit. There are aliases to common usages like a display with a 128x32 or 128x64 resolution that is manipulated through the pins SDA(PB0) and SCL(PB2), for example:

display_128x32<> disp; //this object is ready to receive commands and requests to output characters

Actually, this alias implies in other configurations like a display that uses the Charge Pump Regulator and have the slave address 0x78.

My display is connected through other pins, have other resolution or slave address. What I need to do?

If possible it’s better to use the SDA and SCL pins provided by the microcontroller because the USI(Universal Serial Interface) hardware can be used to optimize the program. The program will be smaller and faster than a version that uses other pins. In the last case the communication is taken by a solution that is only using software to send a byte. Take your measures, please.

You can customize the instance of the class template with the arguments that are suitable to your case:

template<uint8_t Columns = 128,
         uint8_t Rows = 64,
         typename Scl_ = scl,
         typename Sda_ = sda,
         uint8_t Addr_ = 0b1111000 /*0x78*/,
         typename ClearOnInitPolicy = ClearOnInit,
         typename ChargePumpPolicy_ = commands::ChargePump,
         typename DisplayClock_ = DefDisplayClock>
class display;

Compile-time strings

disp.out<font::_8x8>(0, 0, ATT85_SSD1306_STR("alarm"));

We can have a more efficient code in space and in time using compile-time strings when it’s possible to know the string before the run-time phase and that string isn’t too long. This approach avoids a waste of memory to store unused characters and the code that is necessary to handle strings that are only know in run-time.

How much is too long?
You can use a length greater than eleven(11) characters as a reference, but you should take your own measures between the two approaches: run-time and compile-time strings.

Run-time strings

const char* digits;
//digits assumes some value
disp.out<font::_16x32>(0, 0, runtime::c_str<font::_16x32::digits>{digits});

If we have strings that are only know in run-time or have a too long length we should use the minimal charset that represents the possible sequences. The care in the choice of the charset is important to avoid a waste of memory with data which will never be used by the program.

Positive integers

uint8_t num;
//num assumes some value
disp.out<font::_8x8>(0, 0, num);

Commands

You must send at least a command to power on the display:

disp.on();

You can also change the contrast of the display or invert the value of the images that are displayed by the device:

disp.contrast(1); //the lowest possible contrast
disp.inverse(); //bit 1 changes to bit 0 and vice versa
Optimizations to send commands

Instead of make independent calls for each command, you should first consider if the commands can be sent during the instantiation of the object that represents the display. Let’s say that you would like to set a contrast of value 1 and enable the inverse mode:

using namespace att85::ssd1306::commands;
display_128x32<> disp{contrast, of<1>{}, inverse};

The above code is more efficient in space and in time because it reuses the context to send commands that are available by the constructor. If it isn’t possible because you need to send the commands in a later time, you can use a single context to send all the commands:

disp.commands([&](auto&& disp) {
    disp.contrast(1);
    disp.inverse();
    disp.on();
});

Requirements

  1. avr-gcc >= 7.3.0 with support to C++17 enabled (-std=c++17)
  2. avrlibc 2.0
  3. <type_traits> [If there isn’t a freestanging implementation of libstdc++, this subset of the header can be used.

Note: this work doesn’t use the libstdc++ for now.

Key points

High level of abstraction

The gory details of a driver, protocol, procedure or device are hidden from the programmer when it is possible. This approach tends to offer a smaller and concise code avoiding error-prone programming practices. Compile-time strategies like the usage of metaprogramming are used to achieve these goals as also to transform some run-time bugs in compile-time bugs which increases the robustness of the software.

Code reusability

The components are developed using the generic programming(GP) as the main paradigm. One problem, like a sequence of beeps and pauses to be sent to a buzzer is broken into a group of smaller problems which are solved through a set of fundamental abstractions based on concepts that are highly reusable. For example, the above problem is solved reusing an abstraction of a pulse(signal processing), a deadline timer and a pulse generator.

High space efficiency

A set of decisions related to the specific usage of the component by the programmer, should be taken to achieve the best code size in memory. A lot of these decisions can be determined in compile-time by the own component, leaving the programmer without some worries about specific cases or nasty details about the code that must be generated to achieve the best performance.

Important: all the components are developed with the optimization in space -Os of the avr-g++ in mind.

Benchmarks

Versus adhoc or naive solutions

During the development I like to verify if my abstractions are really working without decreasing the space performance. I check my solutions with adhoc alternatives which reuses basic functions used by the proposed solution. I also like to check the efficiency of optional solutions to increase the performance, like display::commands() or compile-time strings, for example.

For each problem there is a related file with the suffix _adhoc or _naive. For example, to benchmark display::display() there is a file ctor.cpp and another file ctor_adhoc.cpp.

The benchmarks can be run through the script benchmarks/adhoc_naive/make_check_size.sh. The performance space from an adhoc or naive solution shouldn’t be better than the att85 solution.

Versus other libraries

We compare programs to print the string alarm in the first row and the 50 column of a display 128x32.

The other library solution(third_party/tinusaur_SSD1306xLED/third_party.cpp):

sectiontextdatabss
bytes2446169

The att85 solution(third_party/tinusaur_SSD1306xLED/att85.cpp):

sectiontextdatabss
bytes32200

Builds using avr-gcc 8.2.0 with -Os.

The program should clear the display in the initialization and print the string alarm in the first row and first column of a 128x64 display.

The other library solution with 6x8(third_party/arduino/Tiny4kOLED_1_5_4v/Tiny4kOLED_alarm.cpp):

sectiontextdatabss
bytes25722268

Note: This version of Tiny4kOLED doesn’t have a 8x8 font. The font 6x8 was chosen to give an advantage to the library in this benchmark.

The att85 solution with 8x8 font and compile-time strings(third_party/arduino/Tiny4kOLED_1_5_4v/alarm_compiletime.cpp):

sectiontextdatabss
bytes53009

The att85 solution with 8x8 font and run-time strings(third_party/arduino/Tiny4kOLED_1_5_4v/alarm_runtime.cpp):

sectiontextdatabss
bytes72069

Builds using Arduino IDE 1.8.13 with std=c++17 enabled.

Arduino

You need an Arduino IDE which uses an avr-gcc with the support to C++17 enabled. It may be necessary to change the C++ standard that is used by the Arduino IDE. You should edit the file platform.txt and look for the property compiler.cpp.flags. Take a look at the flag -std=, it must be a value that supports C++17, for example: -std=c++17. BTW, this work was tested with Arduino 1.8.10.

Installation

Compress the folder include/att85 into a zip file and import the library through the Arduino IDE. The path to be taken using Arduino IDE 1.8.10 is: Sketch -> Include Library -> Add. ZIP Library…

Demo

#include <att85.hpp>

using namespace att85::ssd1306;

void setup() {
  display_128x32<att85::pb1> disp;
  disp.out<font::_8x8>(0, 0, ATT85_SSD1306_STR("alarm"));
  disp.on();
}

void loop() {}