OLED-Display (128x64 px, 0.96") an STM32 betreiben
Mittlerweile sind OLED-Displays, zumindestens die kleineren echt bezahlbar geworden. Auch sind sie zuverlässiger geworden. Ich kann mich noch an mein erstes OLED (steht für organic light emitting diode / organische Leuchtdiode) erinnern. Die war als Display in meiner Sony Ericsson Smartwatch MBW-150 erinnern. Das war 2008 und die OLED-Technologie war noch ganz neu und unausgereift. Ergebnis: Nach einem halben Jahr wurde das Display immer dunkler bis schließlich fast überhaupt nichts mehr angezeigt wurde.Heute ist die OLED-Technologie viel weiter. Es werden nicht mehr nur Winz-Displays für Uhren hergestellt, sondern ganze Fernseher mit 55 und mehr Zoll Disgonale. Auch mein aktuelles Smartphone, ein Samsung J5 (2016) hat ein OLED-Display, das einfach brillant ist und seit den 3 Jahren, in dem ich es jetzt habe, nicht einen Deut dunkler geworden ist.
Heute geht es inbesondere um das verbreitete und kostengünstige OLED Display mit 128x64 Pixel und I2C-Anschluss.
Das OLED bietet gegenüber dem Nokia 5110 Display folgende Vor- und Nachteile:
- (+) keine Hintergrundbeleuchtung nötig, OLEDs leuchten von allein
- (+) weniger Stromverbrauch
- (+) bessere Auflösung (128x64 px statt 84x48)
- (+) preislich etwas günstiger
- (+) aus jedem Winkel gleich gut ablesbar
- (-) aufgrund der hohen Pixeldichte sehr kleine Schrift
- (+) einfacheres Bus-System (I2C)
- (-) sehr empfindlich und zerbrechlich - schon auf dem Versand ist mir manches OLED kaputt gegangen
- (+) schnell schaltbar, keine Schlieren bei Bewegungen, keine Schatten
Am unteren Ende an den Ecken ist das OLED sehr empfindlich. Denn unter dem vielleicht gerade mal 0.6 mm starkem Glas dort an den Ecken ist Luft und ein leichter Druck auf eine Ecke reicht, damit das Glas hier bricht. Der Bruch unterbricht dann die feinen Verbindungen, die mit dem Flachbandkabel zum OLED geführt werden. Folge: Es wird nur noch jede zweite oder dritte Pixelreihe angezeigt, oder auch gar nichts mehr. Irgendwie eine Fehlkonstruktion.
Ich habe als Gegenmaßnahme ein kleines Gehäuse entworfen und mit meinem 3D-Drucker gedruckt. Falls ihr auch einen 3D-Drucker habt, könnt ihr hier die STL-Datei herunterladen. Der kleine Streifen gehört als Stabilisierung vorsichtig (Glas sehr zerbrechlich!) unter den unteren Rand des OLED-Glases geschoben, dann das Modul sanft in den Halter eingeklippst. Keine Gewalt anwenden. Die Maße der Platinen weichen manchmal etwas ab. Wenn der Halter zu klein für eure Platine ist, lasst es lieber.
Gegenüber einem 16x2 LC-Display hat es ähnliche Vorteile, wobei das LC-Display nur sehr bedingt grafikfähig ist (selbst definierte Zeichen).
Und gegenüber einer 8-fach 7-Segment-Anzeige punktet es mit viel geringerem Stromverbrauch und Grafikfähigkeit. Dafür ist es natürlich nicht so robust.
Aber mit einem schützenden Gehäuse bietet ein OLED-Display fast nur Vorteile und wäre meine erste Wahl zur Anzeige von Dingen in Mikrocontroller-Projekten. Zumindest die kleinen, monochromen, kostengünstigen Varianten für wenige Euro das Stück.
Natürlich gibt es auch größere und teurere Modelle wie das 1.5 Zoll große OLED mit 128x128 Pixel und mehr als 65.000 Farben. Hier wäre aber noch ein IPS (gutes Farb-LCD) mit 2 Zoll und 240x320 Pixel und 262.000 Farben die günstigere Wahl.
Für den Aufbau der Schaltung benutzen wir ein Blue-Pill Board mit STM32F103 Prozessor und STM32duino-bootloader, der den FTDI-Adapter überflüssig macht.
Die Blue Pill läuft mit 3.3V statt 5V wie beim Arduino, verträgt aber dennoch an vielen Pins auch 5V. Auch der Formfaktor ist ein anderer als beim Arduino Uno und eher vergleichbar mit einem Arduino Nano. Weiterer Vorteil der Blue Pill ist die integrierte Echtzeituhr, die sich evtl. im späteren Verlauf des Projektes noch als nützlich erweisen wird.
Das OLED-Modul läuft mit 3 bis 5V Volt, kommt also mit den 3.3V vom STM32 ideal zurecht, würde aber auch an den 5 Volt eines Arduinos laufen.
Das OLED-Modul gibt es in unterschiedlichen Farben zu erstehen. Gelb / Blau , Blau und weiß.
Die technischen Daten des OLED sind:
Blue board(Blue display)
Features:
No backlight, the display unit can be self-luminous
High resolution: 128 * 64
Viewing angle: > 160 °
Supports many control chip: Fully compatible with Arduino, 51 series, MSP430 series, STM32 / 2, CSR chip, etc.
Ultra-low power consumption: full screen lit 0.08W, 0.06W normal full-screen display of Chinese characters
Wide voltage support: without any modification, directly supports 3V ~ 5V DC
Working temperature: -30 °C ~ 70 °C
Module volume (generous): 27.0MM * 27.0MM * 4.1MM
Minimum occupancy on Earth IO port display: The IIC/I2C communication, as long as the two IO ports will be able to drive!
Driver IC: SSD1306
No font: take the word with modulo software
Anschluss des OLED an den STM32
Das OLED gehört an den I2C-Bus des STM32. Davon hat der STM32 zwei Stück. Wir entscheiden uns für den ersten. Wir entnehmen dem GPIO-Pinout des STM32, dass SCL1 (Clock) auf PB6 und SDA1 (Data) auf PB7 liegt:Wir schließen also ein Jumperkabel (grün) für I2C-SCL an PB6 des STM32 an und ein weiteres (gelb) für I2C-SDA an PB7 des STM32:
Diese verbinden wir mit den entsprechenden Pins des OLEDs, dass wir an der rechten Seite des Breadboards platziert haben:
Außerdem versorgen wir das Display mit GND und 3.3 Volt über die obere + / - Schiene des Breadboards (kommt von der Blue Pill).
Library zur Ansteuerung installieren
Als nächstes suchen wir uns eine Library aus, mit der man das OLED ansteuern kann, denn wir wollen das Rad nicht neu erfinden. Wir suchen im Bibliotheksmanager nach SSD106 oder nach OLED und werden auch bald fündig.
Ich entscheide mich für die SSD1306 Library von Adafruit. Mit Adafruit Libraries habe ich bisher eigentlich immer gute Erfahrungen gemacht. Außerdem gibt es hier noch einmal eine besonders für den STM32 optimierte Version.
Nachdem ich den Beispiel-Sourcecode und den Code der Library durchgeschaut habe stören mich eigentlich nur zwei Sachen: Erstens gibt es nur einen Font, der dann einfach größer skaliert. Das führt zu pixelige Schriften. Schade, wenn das Display so hochauflösend ist. Zweitens gibt es in das Riesen-Logo mit 128x64 Pixel in der Library definiert und nicht im Beispiel und frisst jetzt immer unnötigen Speicher - denn ich habe nicht das Bedürfnis, das Adafruit-Logo bildschirmfülend anzuzeigen. Aber ich glaube, es wird kein großes Problem, das aus der Library rauszuschmeissen, sollte der Speicher mal knapp werden.
Ansonsten gibt der Beispiel-Sketch Adafruit_SSD1306 / ssd1306_128x64_i2c_STM32 einen guten Einblick in die Funktionen, die die Library bietet.
Mir ist auch aufgefallen, dass die Library gewisse Parallelen zu den Befehlen der Grafik-Library für den Odroid Go ESP32 mit seinem 320x240 Pixel TFT aufweist. Einige Befehle gibt es für das OLED nicht, aber fillRect, drawLine etc. haben die gleichen Parameter und funktionieren gleich. Als Farbe wird beim Monochrome-OLED übrigens immer WHITE verwendet.
Ich will für dieses Beispiel die Kompilierungszeit des Sketches in der Realtime-Clock des STM32 ablegen (kann hier näher nachgelesen werden) und dann die jeweilige Zeit aus der RTC wieder auslesen und sekündlich auf dem OLED anzeigen.
Source-Code
defines.h (klicken, um diesen Abschnitt aufzuklappen)
#define PinPowerLED PC13
#define PinSCL PB6 // I2C-Bus für OLED
#define PinSDA PB7
#define OLED_RESET 4
oled-stm32.ino (klicken, um diesen Abschnitt aufzuklappen)
////////////////////////////////////////////////////////
// (C) 2020 by Oliver Kuhlemann //
// Bei Verwendung freue ich mich über Namensnennung, //
// Quellenangabe und Verlinkung //
// Quelle: http://cool-web.de/arduino/ //
////////////////////////////////////////////////////////
#include <libmaple/libmaple.h>
#include <libmaple/bkp.h>
#include <RTClock.h> // uses https://github.com/rogerclarkmelbourne/Arduino_STM32/tree/master/STM32F1/libraries/RTClock
#include "defines.h"
#include "DateTime.h"
#include "RTCInterface.h"
#include <Wire.h> // I2C
#include <Adafruit_GFX.h> // für das OLED
#include <Adafruit_SSD1306_STM32.h> // für das OLED
// den genauen 32768 Hertz-Timer für die Uhr benutzen (LSE),
// auch ist nur der LSE durch VBat abgesichert
RTClock rtclock (RTCSEL_LSE);
// globale Variablen //////////////
DateTime rtcdat; // RTC-Zeit
Adafruit_SSD1306 oled(OLED_RESET); // OLED-Objekt
void setup() {
char buf[25]; // zum zusammenbasteln der Ausgaben
Serial.begin(115200);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C); // initialize with the I2C addr 0x3D (for the 128x64)
// digitalWrite (PinPowerLED, HIGH); // grüne OnBoard-LED aus
// delay(1000); // auf die ser. Schnittstelle warten
Serial.println (F("Compile-DateTime:"));
Serial.println (__DATE__);
Serial.println (__TIME__);
DateTime compdat;
compdat = getCompileDateTime();
storeDateTimeInString (compdat, buf, false); // Compile-Datum ausgeben
Serial.println (buf);
storeDateTimeInString (compdat, buf, true); // Compile-Datum mit Wochentag
Serial.println (buf);
uint64_t ts64;
ts64 = getDateTimeAsUint64(compdat); // Compile-Datum und Zeit als Zahl
Serial.println (ts64);
uint32_t ts32;
ts32 = getDateAsUint32(compdat); // Compile-Datum als Zahl
Serial.println (ts32);
ts32 = getTimeAsUint32(compdat); // Compile-Zeit als Zahl
Serial.println (ts32);
Serial.println (F("\nRTC-DateTime:"));
rtcdat = getRtcDateTime(rtclock);
storeDateTimeInString (rtcdat, buf, true); // RTC-Datum ausgeben
Serial.println (buf);
if ( getDateTimeAsUint64(rtcdat) < getDateTimeAsUint64(compdat)) { // RTC-Zeit aus der Vergangenheit, kann nicht stimmen
// Kopilierungszeit in Echtzeituhr speichern
setRtcDateTime (rtclock, compdat); // könnte eigentlich nochmal 10 Sekunden drauf
Serial.println (F("RTC-Uhrzeit auf Kompilierungszeit gesetzt."));
// ansonsten mit der Zeit in der RTC weitermachen
}
// Für jede Uhr individuell, muss nach jedem völligen Stromausfall (auch VBat weg oder leer) neu gesetzt werden
// calibrateRtc(1.04); // RTC geht um 1.04 Sekunden am Tag zu schnell
// calibrateRtc(-8.0); // RTC geht um 8 Sekunden am Tag zu langsam
}
void loop() {
DateTime rtcdat;
char buf[25];
byte swHour = 0;
byte swMin = 0;
byte swSec = 0;
uint32_t swStart = 0;
uint32_t dur = 0;
boolean swRuns = false;
time_t tt; // Sekunden seit 01.01.1970
while (1) {
if ( Serial.available() > 14 ) { // 20190515-091017 // setzen der Uhrzeit per Ser-Schnittstelle möglich machen
DateTime serdat;
for (byte i = 0; i < 15; i++) {
buf[i] = Serial.read();
}
Serial.flush();
buf [15] = 0;
sscanf (buf, "%4d%2d%2d-%2d%2d%2d", &serdat.year, &serdat.month, &serdat.day, &serdat.hour, &serdat.minute, &serdat.second);
setRtcDateTime (rtclock, serdat);
}
//tt = rtclock.getTime();
//Serial.println(tt);
rtcdat = getRtcDateTime(rtclock);
//storeDateTimeInString (rtcdat, buf, true); // RTC-Datum mit Wochentag
//Serial.println (buf);
oled.clearDisplay();
oled.setTextSize(1);
oled.setTextColor(WHITE);
// Datum
oled.setCursor(0,0);
snprintf (buf, 20, "%s, %02d.%02d.%04d", wochentage[rtcdat.weekday-1], rtcdat.day, rtcdat.month, rtcdat.year);
oled.print(buf);
// Uhrzeit
oled.setCursor(0,12);
oled.setTextSize(4);
snprintf (buf, 20, "%02d:%02d", rtcdat.hour, rtcdat.minute, rtcdat.second);
oled.print(buf);
// Sekundenbalken
oled.fillRect(3,45, rtcdat.second*2, 6, WHITE);
// Sekunden
oled.setCursor(0,55);
oled.setTextSize(1);
snprintf (buf, 20, "%2d Sekunden", rtcdat.second);
oled.println(buf);
oled.display(); // ohne display() keine Anzeige
// digitalWrite (PinPowerLED, !digitalRead(PinPowerLED)); // ggf. PowerLED blinken lassen
delay(500);
}
}
datetime.h (für RTC) (klicken, um diesen Abschnitt aufzuklappen)
////////////////////////////////////////////////////////
// (C) 2019-2020 by Oliver Kuhlemann //
// Bei Verwendung freue ich mich über Namensnennung, //
// Quellenangabe und Verlinkung //
// Quelle: http://cool-web.de/arduino/ //
////////////////////////////////////////////////////////
struct DateTime {
uint16_t year=0;
uint8_t month=0;
uint8_t day=0;
uint8_t hour=0;
uint8_t minute=0;
uint8_t second=0;
uint8_t weekday=0;
};
static const char *weekDays[7] = {
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"
};
static const char *wochentage[7] = {
"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"
};
static const char *monthNames[12] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
/* schon in RTClock.h definiert, Februar allerding smit 28 Tagen
static const byte monthDays[12] = {
31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
}; */
uint8_t calcWeekday (int d, int m, int y) {
uint8_t w;
if (m < 3) y -= 1;
w = ((d + (int) (2.6 * ((m + 9) % 12 + 1) - 0.2) + y % 100 + (int) (y % 100 / 4) + (int) (y / 400) - 2 * (int) (y / 100) - 1) % 7 + 7) % 7 + 1;
return w;
}
DateTime getCompileDateTime () {
// __DATE__ = May 9 2019
DateTime dat;
char monthName[20];
uint8_t day = 0;
uint8_t month = 0;
uint16_t year = 0;
char compDate[13];
strncpy(compDate, __DATE__, 12);
sscanf (compDate, "%s %2d %4d", monthName, &day, &year);
for (int i = 0; i < 12; i++) {
if (strncmp (monthNames[i], monthName, 3) == 0) {
month = i + 1;
break;
}
}
dat.year = year;
dat.month = month;
dat.day = day;
// __TIME__ = 20:56:25
uint8_t hour = 0;
uint8_t minute = 0;
uint8_t second = 0;
sscanf (__TIME__, "%2d:%2d:%2d", &hour, &minute, &second);
dat.hour = hour;
dat.minute = minute;
dat.second = second;
dat.weekday = calcWeekday (dat.day, dat.month, dat.year);
return dat;
}
void storeDateTimeInString (DateTime dat, char *buf, boolean withWeekday) { // buf min. 20 bytes, mit Weekday mit 25
if (withWeekday) {
snprintf (buf, 25, "%s, %d-%02d-%02d %02d:%02d:%02d",
(dat.weekday <1 || dat.weekday >7) ? "???" : weekDays[dat.weekday - 1],
dat.year, dat.month, dat.day,
dat.hour, dat.minute, dat.second);
} else {
snprintf (buf, 20, "%d-%02d-%02d %02d:%02d:%02d",
dat.year, dat.month, dat.day,
dat.hour, dat.minute, dat.second);
}
}
uint32_t getDateAsUint32 (DateTime dat) { // gibt z. B. 20190510 zurück, alles Ziffern YMD
uint32_t ts;
ts=dat.year*10000;
ts+=dat.month*100;
ts+=dat.day;
return ts;
}
uint32_t getTimeAsUint32 (DateTime dat) { // gibt z. B. 20190510155906 zurück, alles Ziffern YMDhms
uint32_t ts;
ts=dat.hour*10000;
ts+=dat.minute*100;
ts+=dat.second;
return ts;
}
uint64_t getDateTimeAsUint64 (DateTime dat) { // gibt z. B. 20190510155906 zurück, alles Ziffern YMDhms
uint64_t ts;
ts=dat.year*10000000000;
ts+=dat.month*100000000;
ts+=dat.day*1000000;
ts+=dat.hour*10000;
ts+=dat.minute*100;
ts+=dat.second;
return ts;
}
RTCInterface.h (für RTC) (klicken, um diesen Abschnitt aufzuklappen)
////////////////////////////////////////////////////////
// (C) 2019 by Oliver Kuhlemann //
// Bei Verwendung freue ich mich über Namensnennung, //
// Quellenangabe und Verlinkung //
// Quelle: http://cool-web.de/arduino/ //
////////////////////////////////////////////////////////
// uses https://github.com/rogerclarkmelbourne/Arduino_STM32/tree/master/STM32F1/libraries/RTClock
#include <libmaple/libmaple.h>
#include <libmaple/bkp.h>
time_t timetFromDateTime (RTClock rtclock, DateTime dat) {
time_t tt;
tm_t mtt;
mtt.year = dat.year-1970;
mtt.month = dat.month;
mtt.day = dat.day;
mtt.hour = dat.hour;
mtt.minute = dat.minute;
mtt.second = dat.second;
mtt.weekday = dat.weekday == 1 ? 7 : dat.weekday-1;
tt = rtclock.makeTime(mtt);
return tt;
}
void setRtcDateTime (RTClock rtclock, DateTime dat) {
time_t tt;
tt=timetFromDateTime (rtclock, dat);
rtclock.setTime(tt);
}
void setRtcDateTime (RTClock rtclock, time_t tt) {
rtclock.setTime(tt);
}
DateTime getRtcDateTime (RTClock rtclock) {
DateTime dat;
time_t tt;
tm_t mtt;
tt=rtc_get_count();
rtclock.breakTime(tt, mtt);
dat.year = mtt.year+1970;
dat.month = mtt.month;
dat.day = mtt.day;
dat.hour = mtt.hour;
dat.minute = mtt.minute;
dat.second = mtt.second;
dat.weekday = mtt.weekday == 7 ? 1 : mtt.weekday+1;
return dat;
}
time_t getRtcTimeT (RTClock rtclock) {
time_t tt;
tt=rtc_get_count();
return tt;
}
void calibrateRtc (double seksPerDayOffset) { //seksPerDayOffset negativ = RTC läuft zu langsam / positiv = RTC läuft zu schnell
double seksPRL = 2.63671875;
double seksRTCCR = 0.0823974609375;
long prlOffset=0;
double seksRest=0;
uint32_t valPRL=32767;
byte valRTCCR=0;
prlOffset = (seksPerDayOffset/-seksPRL)+.99;
if (prlOffset < 0) prlOffset--;
valPRL= 32767-prlOffset;
seksRest = seksPerDayOffset + (prlOffset*seksPRL);
valRTCCR = (uint8_t) (seksRest/seksRTCCR+0.5); // runden, um möglichst genau zu sein. Max. Abw. dann 0.041 s
rtc_set_prescaler_load(valPRL); // PRL-Register setzen
//bb_peri_set_bit(&RCC_BASE->BDCR, RCC_BDCR_RTCEN_BIT, 1); // Enable the RTC
rcc_start_lse(); // Uhr wieder starten, wurde angehalten
// BKP->RTCCR setzen ------------
//hardware.h #define pBKP ((struct bkp_reg_map*)0x40006C00)
//bkp.h struct bkp_reg_map __io uint32 RTCCR // RTC control register.
// nur die 6 untersten Bits sind Kalibrierungswert (0-127)
if (valRTCCR > 127) valRTCCR -= 128;
// RTC clock calibration register (BKP_RTCCR)
// Address offset: 0x2C
// BKP at 0x40006C00
// RTCCR at 0x40006c2c
// 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
// Reserved xxxxxxxx rw rw rw rw rw rw rw rw rw rw
// ----- CAL[6:0] -----
__IO uint32* tmpreg = (uint32_t*) 0x40006c2c; // BKP_BASE + 0x2c;
/*
uint32_t valTest = *tmpreg;
Serial.println ("BKP->RTCCR");
Serial.println ((uint32) tmpreg,HEX);
Serial.println (valTest);
Serial.println (valTest & 0x8F);
*/
*tmpreg &= 0xFFFFFFF80; // RTCCR CAL löschen (unteren 7 Bits)
*tmpreg |= valRTCCR; // RTCCR CAL setzen
}
Die Komponenten datetime.h und RTCInterface.h können immer wieder verwendet werden. datetime.h habe ich dieses mal erweitert um die Wochentage, um eine deutsche Anzeige zu haben.
Im Sourcecode sollte durch die Kommentare klar werden, um was es geht. Eventuell sollte man sich zu den RTC-Komponenten noch einmal einlesen, wenn einen das interessiert, ansonsten kann man sie auch einfach benutzen und sich an die aufrufenden Befehle in oled-stm32.ino halten.
Einige Zeilen sind auskommentiert, etwa die zur RTC-Kalibrierung. Gegebenenfalls sind diese aber für euch interessant. Dann könnt ihr ihr sie wieder einkommentieren.
Das Ganze habe ich natürlich wieder in einem Video zusammengefaßt. Das zeigt noch einmal den Anschluss des OLED, eine Demonstration der Fähigkeiten der Library und auch das fertige Ergebnis, spricht die OLED-Uhr: