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.
An abstraction to represent an OLED SSD1306 display that is used through the I2C communication interface.
#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 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;
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.
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.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.
uint8_t num;
//num assumes some value
disp.out<font::_8x8>(0, 0, num);
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
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();
});
avr-gcc
>= 7.3.0 with support to C++17 enabled (-std=c++17
)avrlibc 2.0
<type_traits>
[If there isn’t a freestanging implementation oflibstdc++
, this subset of the header can be used.
Note: this work doesn’t use the libstdc++
for now.
Important: all the components are developed with the optimization in space -Os
of the avr-g++
in mind.
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.
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
):
section | text | data | bss |
bytes | 2446 | 16 | 9 |
The att85 solution(third_party/tinusaur_SSD1306xLED/att85.cpp
):
section | text | data | bss |
bytes | 322 | 0 | 0 |
Builds using avr-gcc 8.2.0
with -Os
.
Tiny4kOLED 1.5.4 [Arduino]
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
):
section | text | data | bss |
bytes | 2572 | 22 | 68 |
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
):
section | text | data | bss |
bytes | 530 | 0 | 9 |
The att85 solution with 8x8 font and run-time strings(third_party/arduino/Tiny4kOLED_1_5_4v/alarm_runtime.cpp
):
section | text | data | bss |
bytes | 720 | 6 | 9 |
Builds using Arduino IDE 1.8.13
with std=c++17
enabled.
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.
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…
#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() {}