jeudi 31 juillet 2014

OLED





Un petit breakout avec un afficheur OLED 64x128 pixels piloté par un SSD1306 via i2c...  Chez Adafruit pour les puristes (ou pour quelques euros sur eBay pour les opportunistes).  Attention qu'il existe en SPI, en I2C,... et que c'est configuré en hardware (circuit imprimé et/ou pontages SMD) en plus du nombre de fils disponibles sur l'interface.

J'ai eu du mal à trouver un bout de code simple pour l'essayer sur mon Raspberry Pi.  Finalement, je me suis rabattu sur le code C++ proposé par JPB-HK sur la discussion 'I2C with Adafruit 128x32 I2C OLED' sur RaspberryPi.org (128x32i2c.tar.gz).

Comme je voulais un truc en 'C', le plus simple possible, j'ai taillé le code à la hache.  Ce qui donne finalement :

#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <stdio.h>
#include <string.h>

typedef unsigned char uchar;
#define _GLCDFONT
#include "glcdfont.h"

#define I2C_PORT "/dev/i2c-1" // Using /dev/i2c-1 port
#define I2C_ADDR 0x3C         // Address for oled i2c, 2 possible, 0x3C and 0x3D

#define WIDTH    128          // 128 columns of 8bits deep
#define HEIGHT   4            // 4 rows, 8 bits high


uchar    oled_buf[HEIGHT * WIDTH];
int    i2c_fd;

void OLED_close()
    {
    if (i2c_fd)
        close(i2c_fd);
    }
void OLED_clear_buf()
    {
    memset(oled_buf, 0x00, (WIDTH * HEIGHT));
    }
void OLED_write_screen()
    {
    int     i, si = 0; /* si : screen index */
    uchar     buf[0x09];

    /* set screen pointer to (0,0) */
    OLED_i2c_write("\x00\x21\x00\x7f", 4);    // Set column address; start 0, end 127
    OLED_i2c_write("\x00\x22\x00\x03", 4);    // Set row address; start 0, end 3

    buf[0] = 0x40;           // Co = 0; D/C# = 1 for data
    while (si < (WIDTH * HEIGHT))
        {
        for (i = 0; i < 8; i++)
            buf[i+1] = oled_buf[si+i];
        OLED_i2c_write (buf, 9);
        si += 8;
        }
    }
void OLED_clear_screen()
    {
    OLED_clear_buf();
    OLED_write_screen();
    }
void OLED_write_text (char *str, int li, int co)
    {
    int    j;
    int    si;     /* screen index */

    li %= HEIGHT; co %= WIDTH;    // ensure in screen
    si = (li * WIDTH) + co;             // Set absolute start position in buffer

    while (*str)
        {
        for (j = 0; j < 5; j++)             // Loop through each 7bit segment of the character
            oled_buf[si++] = font[((*str)*5)+j];
        oled_buf[si++] = 0x00;                  // This puts a space between the characters on screen
        str++;
        }
    }
int OLED_init(void)
    {
    if ((i2c_fd = open(I2C_PORT, O_RDWR)) < 0) // Try to open /dev/i2c-x port
        {
        perror(I2C_PORT);
        return(-1);
        }

    if (ioctl(i2c_fd, I2C_SLAVE, I2C_ADDR) < 0) // Try to access the oled display
        {
        perror("i2c ioctl()");
        close(i2c_fd);
        i2c_fd = -1;
         return(-1);
        }
    /* commands are prefixed with 0x00 */
    OLED_i2c_write("\x00\xae", 2);        // Display off
    OLED_i2c_write("\x00\xd5\x80", 3);    // set display clock division
    OLED_i2c_write("\x00\xa8\x1f", 3);    // set multiplex
    OLED_i2c_write("\x00\xd3\x00", 3);    // set display offset
    OLED_i2c_write("\x00\x40", 2);        // set start line #0
    OLED_i2c_write("\x00\x8d\x14", 3);    // set charge pump
    OLED_i2c_write("\x00\x20\x00", 3);    // Memory mode
    OLED_i2c_write("\x00\xa1", 2);        // Segremap(0xA0 = reset, 0xA1 = 127 = SEG0)
    OLED_i2c_write("\x00\xc8", 2);        // Com scan dec (0xC0 = reset normal, 0xC8 = scan  from Com[n-1] - Com 0
    OLED_i2c_write("\x00\xda\x02", 3);    // Set com pins
    OLED_i2c_write("\x00\x81\x8f", 3);    // Set contrast
    OLED_i2c_write("\x00\xd9\xf1", 3);    // Set precharge
    OLED_i2c_write("\x00\xdb\x40", 3);    // Set Vcom select
    OLED_i2c_write("\x00\xa4", 2);        // Resume RAM content display
    OLED_i2c_write("\x00\xa6", 2);        // Normal display not inverted
    OLED_i2c_write("\x00\x21\x00\x7f", 4);    // Set column address; start 0, end 127
    OLED_i2c_write("\x00\x22\x00\x03", 4);    // Set row address; start 0, end 3
    return(OLED_i2c_write("\x00\xaf", 2));    // Display ON
    }
int OLED_i2c_write(uchar *ucp, int len)
    {
    if (write(i2c_fd, ucp, len) != len)
        {
        perror("OLED_i2c_write()");
        return(-1);
        }
    usleep(1);

    return(len);
    }

main()
    {
    OLED_init();
    OLED_clear_screen();
    OLED_write_text("Oufti!", 10, 2);
    OLED_write_screen();
    }

Cela pourrait être un peu plus propre mais pour le moment, c'est bon comme ça. Cela permet de constater que l'afficheur fonctionne sans se casser la tête. (Par exemple, OLED_write_text() devrait proposer x, y avant le texte et 'x' est en pixels et 'y' en ligne de texte...)

Le fichier se compile en faisant simplement 'gcc -o oled oled.c'.  Le fichier 'glcdfont.h', qui contient les caractères en bitmap, est quasi l'original (juste remplacé 'unsigned char' par 'uchar').  Le code de JPB-HK est dérivé de ce qu'il avait trouvé sur AdaFruit.com et était abondamment commenté.  En plus d'avoir écrémé le C++, j'ai fini par également enlever les commentaires qui, à mon sens alourdissaient le fichier inutilement.  En fait, il n'y a que trois choses importantes dans le code : l'initialisation (OLED_init()), l'écriture dans le frame-buffer local (OLED_write_text(string, x, y)) et l'envoi du frame-buffer vers l'afficheur (OLED_write_screen()).

L'initialisation est complexe.  Outre qu'il faille correctement configurer le RPi avant de pouvoir accéder aux périphériques sur le bus /dev/i2c-1 avec les i2c-tools (par exemple, 'sudo i2cdetect -y 1' nous apprend que le display est bien à l'adresse 0x3C), une petite vingtaine de commandes semblent nécessaires pour initialiser le chip.  Je ne suis pas sûr qu'on y arrive en lisant seulement la datasheet du SSD1306.  Une fois acquis ce 'Hello World!', elle est probablement indispensable pour explorer plus avant les fonctionnalités de ce composant qui semble offrir de nombreuses possibilités.

'sudo ./oled' affiche 'Oufti!' sur la troisième ligne de l'afficheur.