terça-feira, outubro 20, 2015

Display OLED com interface I2C - Parte 1

Mais um display com interface I2C, mas vesta vez gráfico (128x64) e OLED ao invés de LCD.


A tecnologia OLED permite uma maior legibilidade, principalmente em ambientes mal iluminados, por emitirem diretamente luz. Este display, comprado na DX, é bem pequeno e apresenta uma faixa superior na cor amarela (16 pontos de altura) e o restante na cor azul. Existe uma separação entre as duas faixas, com altura de um ponto.

Para quem quiser sair usando direto com o Arduino, tem uma biblioteca da Adafruit no github:

https://github.com/adafruit/Adafruit_SSD1306

Os exemplos desta biblioteca usam uma outra biblioteca da Adafruit que implementa várias funções gráficas:

https://github.com/adafruit/Adafruit-GFX-Library

O controlador do display é o SSD1306, cujo datasheet pode ser visto aqui. O endereço I2C pode ser 0x3C ou 0x3D (o módulo veio configurado para 0x3C, tem uns pontos de solda atrás que devem permitir alterar o endereço).

Cada transferência de iniciar com um byte de controle. Neste byte apenas os dois bits mais significativos são usados: Co (bit 7) e D/C (bit 6). O bit D/C indica se o byte seguinte é um comando (0) ou dado a ser escrito na memória de vídeo(1). Se Co for 1, o byte de controle se aplica apenas ao byte seguinte e um novo byte de controle deve ser fornecido após o dado ou comando. Se Co for 0, os bytes seguintes serão todos tratados como dado ou comando. O datasheet detalha os comandos disponíveis.

Confuso? Vejamos alguns exemplos:

0x00 0x20 -> 0x20 é tratado como comando
0x00 0x22 0x01 0x07 -> 0x22, 0x01 e 0x07 são tratados como comandos
0x40 0x01 0x02 -> 0x01 e 0x02 são escritos na memória de vídeo
0x80 0x20 0x40 0x01 0x02 -> 0x20 é tratado como comando e 0x01 e 0x02 são escritos na memória de vídeo

A memória do display possui 1Kbyte, dividido em oito páginas. Cada byte corresponde a 8 pontos em uma coluna, com o bit menos significativo correspondendo à linha superior:


No meu primeiro teste, liguei o display a um Arduino. A ligação é trivial: Vcc em +5V, GND ao GND, SDA a A4 e SCL a A5 (estes dois últimos pinos são para Uno e Nano, no Mega os pinos são outros).



A iniciação do display é feita enviando vários comandos, que eu peguei do código da Adafruit. Para simplificar, enviei todos os comandos de iniciação em uma única transação I2C.

A escrita na memória também é um pouco confusa. O comando Set Column Address define uma coluna inicial e uma coluna final. O comando Set Page Address define uma página inicial e uma página final. O comando Set Memory Address Mode define como é o feito o incremento da posição atual após uma escrita. Usei o modo "horizontal" no qual a coluna começa na inicial e vai sendo incrementada até atingir a final, neste ponto volta à coluna inicial e a página é incrementada.

A minha rotina de limpeza de tela mostra isto em funcionamento, usando todas as colunas e páginas da memória.

Para completar este teste, uma rotina de escrita de caracteres. Para simplificar, usei uma célula de 8 x 8 pontos, o que gerou 8 linhas de 16 caracteres bem pequenos.

//
// Teste de comunicação com Display OLED
// baseado no controlador SSD1306
//

#include <Wire.h>

// Endereço I2C do display
#define DISP_ADDR  0x3C

// Comandos
#define SSD1306_SETCONTRAST 0x81
#define SSD1306_DISPLAYALLON_RESUME 0xA4
#define SSD1306_DISPLAYALLON 0xA5
#define SSD1306_NORMALDISPLAY 0xA6
#define SSD1306_INVERTDISPLAY 0xA7
#define SSD1306_DISPLAYOFF 0xAE
#define SSD1306_DISPLAYON 0xAF
#define SSD1306_SETDISPLAYOFFSET 0xD3
#define SSD1306_SETCOMPINS 0xDA
#define SSD1306_SETVCOMDETECT 0xDB
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5
#define SSD1306_SETPRECHARGE 0xD9
#define SSD1306_SETMULTIPLEX 0xA8
#define SSD1306_SETLOWCOLUMN 0x00
#define SSD1306_SETHIGHCOLUMN 0x10
#define SSD1306_SETSTARTLINE 0x40
#define SSD1306_MEMORYMODE 0x20
#define SSD1306_COLUMNADDR 0x21
#define SSD1306_PAGEADDR   0x22
#define SSD1306_COMSCANINC 0xC0
#define SSD1306_COMSCANDEC 0xC8
#define SSD1306_SEGREMAP 0xA0
#define SSD1306_CHARGEPUMP 0x8D
#define SSD1306_EXTERNALVCC 0x1
#define SSD1306_SWITCHCAPVCC 0x2

// Tamanho da tela
#define SSD1306_LCDWIDTH                  128
#define SSD1306_LCDHEIGHT                 64

// Iniciação
void setup()
{
  Wire.begin();
  Display_init();
}

// Laço principal
void loop()
{
  int d = 0;
  for (int l = 0; l < 8; l++)
  {
    for (int c = 0; c < 16; c++)
    {
      Display_write (l, c, d);
      if (++d == 10)
        d = 0;
      delay (500);
    }
  }
  delay (10000);
  Display_clear();
}


// Sequência de iniciação do display
byte cmdInit[] =
{
    SSD1306_DISPLAYOFF,
    SSD1306_SETDISPLAYCLOCKDIV, 0x80,
    SSD1306_SETMULTIPLEX, 0x3F,
    SSD1306_SETDISPLAYOFFSET, 0x00,
    SSD1306_SETSTARTLINE | 0x0,
    SSD1306_CHARGEPUMP, 0x14,
    SSD1306_MEMORYMODE, 0x00,
    SSD1306_SEGREMAP | 0x1,
    SSD1306_COMSCANDEC,
    SSD1306_SETCOMPINS, 0x12,
    SSD1306_SETCONTRAST, 0xCF,
    SSD1306_SETPRECHARGE, 0xF1,
    SSD1306_SETVCOMDETECT, 0x40,
    SSD1306_DISPLAYALLON_RESUME,
    SSD1306_NORMALDISPLAY,
    SSD1306_DISPLAYON
};

// Iniciação do display
void Display_init()
{
  Display_sendcmd (cmdInit, sizeof(cmdInit));
  Display_clear();
}

// Limpa o display
void Display_clear()
{
  // Define endereços iniciais e finais de colunas e páginas
  Display_sendcmd (SSD1306_COLUMNADDR);
  Display_sendcmd (0);
  Display_sendcmd (SSD1306_LCDWIDTH-1);
  Display_sendcmd (SSD1306_PAGEADDR);
  Display_sendcmd (0);
  Display_sendcmd (7);

  // Preenche a memória com zeros
  for (uint16_t i=0; i<(SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8); i++) 
  {
      Wire.beginTransmission(DISP_ADDR);
      Wire.write(0x40);  // Co=0, DC = 1
      for (uint8_t x=0; x<16; x++) 
      {
 Wire.write(0);
      }
      Wire.endTransmission();
  }
}

// Desenho dos números de 0 a 9
byte gc[][7] =
{
 {0x3E, 0x61, 0x51, 0x49, 0x45, 0x43, 0x3E} // 0
,{0x40, 0x40, 0x42, 0x7F, 0x40, 0x40, 0x40} // 1
,{0x42, 0x61, 0x51, 0x49, 0x45, 0x42, 0x40} // 2
,{0x22, 0x41, 0x49, 0x49, 0x49, 0x49, 0x36} // 3
,{0x10, 0x18, 0x14, 0x12, 0x11, 0x7F, 0x50} // 4
,{0x27, 0x45, 0x45, 0x45, 0x45, 0x49, 0x31} // 5
,{0x3C, 0x4A, 0x49, 0x49, 0x49, 0x49, 0x32} // 6
,{0x41, 0x21, 0x11, 0x09, 0x05, 0x03, 0x01} // 7
,{0x36, 0x49, 0x49, 0x49, 0x49, 0x49, 0x36} // 8
,{0x26, 0x49, 0x49, 0x49, 0x49, 0x29, 0x1E} // 9
};

// Escreve um caracter na linha l(0 a 7), coluna c(0 a 16)
void Display_write (byte l, byte c, byte car)
{
  byte *pc = gc[car];

  // Endereça o caracter
  Display_sendcmd (SSD1306_COLUMNADDR);
  Display_sendcmd (c*8);
  Display_sendcmd (c*8 + 7);
  Display_sendcmd (SSD1306_PAGEADDR);
  Display_sendcmd (l);
  Display_sendcmd (l);
  
  // Escreve
  Wire.beginTransmission(DISP_ADDR);
  Wire.write(0x40);  // Co=0, DC = 1
  for (uint8_t x = 0;  x < 7; x++) 
  {
    Wire.write(*pc++);
  }
  Wire.endTransmission();
}

// Envia sequência de comandos ao display
void Display_sendcmd (byte *cmd, int nCmds)
{
  Wire.beginTransmission(DISP_ADDR);
  Wire.write (0);  // Co= 0. DC = 0
  for (int i = 0; i < nCmds; i++)
  {
    Wire.write(cmd[i]);
  }
  Wire.endTransmission();
}

// Envia um byte de comando ao display
void Display_sendcmd (byte cmd)
{
  Wire.beginTransmission(DISP_ADDR);
  Wire.write (0);  // Co= 0. DC = 0
  Wire.write(cmd);
  Wire.endTransmission();
}

Nenhum comentário: