Meine Smartwatch :-) Mit I2C ein OLED-Display ansteuern

OLEDFür gerade einmal 10 € gibt es ein 0,96 zoll Display mit einer Auflösung von 128×64 Pixel zum Beispiel bei amazon. Das Display ist groß genug, um kleine Grafiken und Benachrichtigungen anzuzeigen. Mein Display ist ein chinesisches Produkt, angepriesen als 100 % kompatibel zum Original von Adafruit und daher wohl 100% kompatibel zur Adafruit-Bibliothek Adafruit_SSD1306.h für den Arduino. Es wird mit 5V betrieben und verfügt über eine I2C-Schnttstelle. Die Orignal-Displays gibt es mit I2C und SPI-Schnittstelle (siehe SSD1306 OLED Displays with Raspberry Pi and BeagleBone Black – englisch). Da ich das Display direkt am RasPi betreiben will, stellen sich zwei Fragen:

  1. Läuft es auch mit 3.3V? Dann kann auf einen Level-Shifter zwischen den SDA/SCL-Anschlüssen von RasPi und Display verzichtet werden.
  2. Ist die Adafruit-Bibliothek auch für den RasPi nutzbar? Dann muss ich das Datenblatt des Displays nicht selbst programmieren.

Der erste praktische Versuch zeigte: Ja, das Display läuft problemlos auch mit 3.3V. Übrigens so auch im Datenblatt für den SDD1306-Chipsatz beschrieben. Auf der Adafruit-Webseite wird eine python-Bibliothek für die Nutzung des Displays mit dem RasPi angeboten. Ein Blick ins Internet liefert per Google mit ArduiPi_SDD1306 auch eine C-Bibliothek für den RasPi. Im Prinzip bringen die Bibliotheken alles mit, um gleich mit den Displays loszulegen. Voraussetzung ist lediglich, dass die Kernelmodule für die i2c- und SPI-Schnittstellen geladen wurden.

Die I2C-Kernelmodule laden
Um das Display mit der I2C-Schnittstelle in Betrieb zu nehmen, müssen die Kerneltreiber geladen werden. Dazu werden bei dem Raspian-Image (Debian) die beiden Kernelmodule bcm2708 und i2c-dev in der Datei /etc/modules hinzugefügt. Anschließend müssen die Module aus der Blacklist entfernt werden, die das Laden der beiden Module verhindern würde. Dazu werden in der Datei /etc/modprobe.d/raspi-blacklist.conf die beiden Einträge „blacklist spi-bcm2708“ und „blacklist i2c-bcm2708“ mit einem „#“ am Anfang der Zeilen auskommentiert. Beim aktuellen Rasperian-Image ist ein weiterer Eintrag nötig. In der Datei /boot/config.txt müssen folgende Einträge hinzugefügt werden:

dtparam=i2c1=on
dtparam=i2c_arm=on

Ich habe mich dann für die C-Bibliothek entschieden, da ich die Nutzung der i2c-Schnittstelle mit Hilfe der BCM2805-lib von Mike McCauley schon einige Male geübt habe. Die Bibliothek ArduiPi_SDD1306 von Charles Hallard portiert den SDD1306 Arduino-OLED-Treiber auf den RasPi und nutzt für das Ansprechen der I2C- und SPI-Schnittstelle diese BCM2805 Bibliothek. Die ArduiPi_SDD1306-Bibliothek von Charles Hallard wird auf github.com angeboten. Mit folgendem Aufruf wird der Source-Code der Bibliothek auf den RasPi geladen:

git clone https://github.com/hallard/ArduiPi_SSD1306.git

Anschließend wird die Bibliothek kompiliert und installiert:

cd ArduiPi_SSD1306
sudo make

Für einen ersten Test werden die Demo-Programme im Verzeichnis examples kompiliert und mit root-Rechten gestartet:

cd examples
make
sudo ./ssd1306_demo --help

Der Anschluss am Beispiel des original Adafruit-Displays, das auf I2C-Betrieb konfiguriert wurde (Data = SDA, CS = SCL): OLED_Steckplatine Mein erstes eigenes Programm mit dem Display bringt eine analoge Uhr auf das Display, stellt das Datum dar und gibt die Systemtemperatur sowie die Speicherbelegung der SD-Karte aus.


#include <ArduiPi_SSD1306.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <math.h>
#include <time.h>
#include <sys/statvfs.h>

#define X0 0
#define Y0 1
#define X1 2
#define Y1 3
#define PI 3.14159265

typedef struct {
  // Struct, um die Koordinaten aller Zeiger-Positionen
  // für die drei Zeiger (Sekunden, Minuten, Stunden)
  // vorauszuberechnen
  uint16_t x0;
  uint16_t y0;
  uint16_t sek[60][2];
  uint16_t min[60][2];
  uint16_t std[12][2];
} zeiger_werk;

void initZeiger(uint16_t x, uint16_t y, uint16_t r, zeiger_werk *zeiger) {
  // Berechnet für einen Mittelpunkt und einen Radius alle Zeigerkoordinaten
  zeiger->x0 = x;
  zeiger->y0 = y;
  for(uint8_t i=0;i<60;i++) {
    float alpha = 6.0*i*PI/180.0 - PI/2.0;
    float cosinus = cos(alpha);
    float sinus = sin(alpha);
    zeiger->sek[i][0] = r * cosinus + x;
    zeiger->sek[i][1] = r * sinus + y;
    zeiger->min[i][0] = (r - 5) * cosinus + x;
    zeiger->min[i][1] = (r - 5) * sinus + y;
    if(i % 5 == 0) {
      zeiger->std[i/5][0] = (r - 10) * cosinus + x;
      zeiger->std[i/5][1] = (r - 10) * sinus + y;
    }
  }
}

Adafruit_SSD1306 display;

void drawClock(uint16_t x, uint16_t y, uint16_t r, uint8_t tick_length) {

  display.drawCircle(x, y, r, WHITE);

  for (uint8_t i=0;i<12;i++) {
    double alpha = 30.0*i*PI/180.0;
    uint16_t x1 = (r - tick_length) * cos(alpha) + x;
    uint16_t y1 = (r - tick_length) * sin(alpha) + y;
    uint16_t x2 = r * cos(alpha) + x;
    uint16_t y2 = r * sin(alpha) + y;
    display.drawLine(x1,y1,x2,y2,WHITE);
  }

  display.display();
}

void drawSeconds(zeiger_werk zeiger, uint8_t sec, uint16_t color)
{
  // zeichnet für einen Sekundenwert den Sekundenzeiger mit
  // Hilfe der abgespeicherten Zeigerkoordinaten
  display.drawLine(zeiger.x0, zeiger.y0, zeiger.sek[sec][0],zeiger.sek[sec][1], color);
  display.display();
}

void drawMinutes(zeiger_werk zeiger, uint8_t min, uint16_t color)
{
  // zeichnet für einen Minutenwert den Minutenzeiger mit
  // Hilfe der abgespeicherten Zeigerkoordinaten
  display.drawLine(zeiger.x0, zeiger.y0, zeiger.min[min][0],zeiger.min[min][1], color);
  display.display();
}

void drawHours(zeiger_werk zeiger, uint8_t std, uint16_t color)
{
  // zeichnet für einen Stundenwert den Stundenzeiger mit
  // Hilfe der abgespeicherten Zeigerkoordinaten, bildet dabei
  // 24-Stundenangaben auf 12-Stunden der analogen Uhr ab
  display.drawLine(zeiger.x0, zeiger.y0, zeiger.std[std % 12][0],zeiger.std[std % 12][1], color);
  display.display();
}

void drawZeit(const uint16_t x0, const uint16_t y0, const uint16_t zeile_len, struct tm *zeit)
{
  char uhrzeit[9];
  strftime(uhrzeit,9,"%H:%M:%S", zeit);
  display.fillRect(x0,y0, zeile_len, 8,BLACK);
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(x0,y0);
  display.print(uhrzeit);
  display.display();
}

void drawWochentag(const uint16_t x0, const uint16_t y0, const uint16_t zeile_len, struct tm *zeit)
{
  char *wday[] = { "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "???" };
  display.fillRect(x0,y0, zeile_len, 8,BLACK);
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(x0,y0);
  display.print(wday[zeit->tm_wday]);
  display.display();
}

void drawDatum(const uint16_t x0, const uint16_t y0, const uint16_t zeile_len, struct tm *zeit)
{
  char datum[11];
  strftime(datum,11,"%d.%m.%y", zeit);
  display.fillRect(x0,y0, zeile_len, 8,BLACK);
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(x0,y0);
  display.print(datum);
  display.display();
}

void drawCPUTemp(const uint16_t x0, const uint16_t y0, const uint16_t zeile_len)
{
  FILE *temperatureFile;
  double T = 0.0;

  temperatureFile = fopen ("/sys/class/thermal/thermal_zone0/temp", "r");
  if (temperatureFile) {
   fscanf (temperatureFile, "%lf", &T);
   fclose (temperatureFile);
  }
  T = T/1000.0;
  display.fillRect(x0,y0, zeile_len, 8,BLACK);
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(x0,y0);
  display.printf("CPU: %.0fC",T);
  display.display();
}

void drawDiskUsage(const uint16_t x0, const uint16_t y0, const uint16_t zeile_len)
{
  struct statvfs buf;
  double usage = 0.0;

  if (!statvfs("/etc/rc.local", &buf)) {
    unsigned long hd_used;
    hd_used = buf.f_blocks - buf.f_bfree;
    usage = ((double) hd_used) / ((double) buf.f_blocks) * 100;
  }

  display.fillRect(x0,y0, zeile_len, 8,BLACK);
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(x0,y0);
  display.printf("HD: %.0f%%",round(usage));
  display.display();
}

int main(int argc, char **argv)
{
  time_t unix_sek;
  struct tm *zeit;
  uint8_t sekunden;
  uint8_t minuten;
  uint8_t stunden;
  zeiger_werk zeiger;

  if ( !display.init(OLED_I2C_RESET,OLED_ADAFRUIT_I2C_128x64) )
    exit(EXIT_FAILURE);

  display.begin();

  uint16_t clock_center[] =  { display.width()/2 - 32, display.height()/2 };
  const uint16_t RADIUS = display.height()/2-1;
  const uint16_t ZEIGER_SEK_LEN = display.height()/2-5;
  const uint16_t TEXT_AREA_START_X = display.width()/2;

  initZeiger(clock_center[0],clock_center[1],ZEIGER_SEK_LEN, &zeiger);

  display.clearDisplay();

  drawClock(clock_center[X0], clock_center[Y0], RADIUS, 3);

  unix_sek = time(NULL);
  zeit = localtime(&unix_sek);
  sekunden = zeit->tm_sec;
  minuten = zeit->tm_min;
  stunden = zeit->tm_hour;
  drawSeconds(zeiger, zeit->tm_sec, WHITE);
  drawMinutes(zeiger, zeit->tm_min, WHITE);
  drawHours(zeiger, zeit->tm_hour, WHITE);

  while (1) {
    unix_sek = time(NULL);
    zeit = localtime(&unix_sek);
    if (zeit->tm_sec != sekunden) {
      drawSeconds(zeiger, sekunden, BLACK);
      if (zeit->tm_min != minuten) {
        drawMinutes(zeiger, minuten, BLACK);
        minuten = zeit->tm_min;
      }
      if (zeit->tm_hour != stunden) {
        drawHours(zeiger, stunden, BLACK);
        minuten = zeit->tm_hour;
      }
      drawHours(zeiger, zeit->tm_hour, WHITE);
      drawMinutes(zeiger, zeit->tm_min, WHITE);
      drawSeconds(zeiger, zeit->tm_sec, WHITE);
      drawZeit(TEXT_AREA_START_X + 10, 5, 54, zeit);
      drawWochentag(TEXT_AREA_START_X + 10, 15, 54, zeit);
      drawDatum(TEXT_AREA_START_X + 10, 25, 54, zeit);
      drawCPUTemp(TEXT_AREA_START_X + 10, 45, 54);
      drawDiskUsage(TEXT_AREA_START_X + 10, 55, 54);
      sekunden = zeit->tm_sec;
    }
    delay(300);
  }

  display.close();
}

Kompiliert wird das Programm bei installierter ArduiPi_SDD1306-Bibliothek mit:

gcc -Ofast -mfpu=vfp -mfloat-abi=hard -march=armv6zk -mtune=arm1176jzf-s -Wall  -lssd1306 -lm clock.cpp -o clock

Falls nichts auf dem Display erscheint, muss das i2c-Kernelmodul möglicherweise noch nachgeladen werden:

sudo modeprobe i2c-dev

, , , , ,

  1. #1 von Jochen am 18. Juni 2016 - 11:28

    Cooles Programm! Mal kucken wozu ich das Display nutze.

  2. #2 von Helmut Wunder am 6. August 2016 - 22:10

    hallo!
    Großartig, das ist das erste (und einzige) Mal, dass ich eine C lib für ein OLED am Raspi sehe!

    Das OLED soll auch nur für eine zusätzliche Debug-Ausgabe benutzt werden, für die „normale“ Ausgabe verwende ich ein HDMIDisplay mit LXTerminal und openVG, beide Displays sollen also parallel und unabhängig voneinander benutzt werden können.

    Leider bin ich absolut neu mit dem Pi und von der McCauley C lib verstehe ich kein Wort – ich nutzte bisher immer die WiringPi i2c Lib von Gordon Henderson .

    Außerdem kann ich keine Command LIne benutzen (ich verstehe es nicht und es ist auch viel zu umständlich, und man vertippt sich immer ewig und drei Tage….)
    Stattdessen verwende ich nur Geany mit gpp –
    – wie aber kann man hier (dauerhaft) die Build- Parameter zum Compilieren korrekt einstellen?
    Meine Build-Parameter für Geany lauten bisher schon – sehr umfangreich – :
    g++ -Wall -I/opt/vc/include -I/opt/vc/include/interface/vmcs_host/linux -I/opt/vc/include/interface/vcos/pthreads -o „%e“ „%f“ -pthread -lshapes -L/opt/vc/lib -lOpenVG -lEGL -lrt -lwiringPi -lpigpio
    Was kommt da jetzt noch dazu für das OLED?

    Haben Sie ggf.sogar eine Idee, wie man die wiringPi lib statt der von Mike McCauley mit dem OLED benutzen kann?

    Viele Grüße
    Helmut

    • #3 von mnasarek am 19. August 2016 - 11:45

      Vielen Dank! Die WiringPi ist sogar einfacher. Ich habe McCauley lib genommen, um möglichst viel über die Protokolle zu lernen. Der Blog hier ist tatsächlich darauf ausgelegt, an der Basis zu schrauben und die technischen Grundlagen zu ergründen. Daher versuche ich so wenig wie möglich auf Black-Boxes zurückzugreifen (und wenig libs und header einzubinden). Bei der Einbindung fremder libs muss man eben wissen, wovon die wiederum abhängig sind, und das kann man allgemein nicht sagen. Eine konkrete Bibliothek (lib) bindest Du mit -l ein, ein Bibliotheksverzeichnis mit -L und die Verzeichnisse der Header-Dateien mit -I.
      Marcus

  1. #BlogBlick No. 3 | Plaintron Blog

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s

%d Bloggern gefällt das: