Por simplificação estou usando sempre caracteres mono-espaçados e usando nos exemplos fontes somente com os dígitos hexadecimais.
Algumas considerações sobre o display e seu controlador
A primeira coisa a considerar é o mapeamento dos pontos na memória do controlador SSD1306:
Repare que cada byte da memória corresponde a 8 pontos na vertical. Por este motivo é mais simples trabalhar com fontes que tenham o tamanho vertical múltiplo de 8.
Normalmente eu utilizo a interface I2C para comunicar com o display, o que traz algumas limitações adicionais. A primeira é que a memória do controlador não pode ser lida, se você precisar do conteúdo atual vai ter que manter uma cópia da imagem no controlador. A memória para armazenar a tela toda é 128*64/8 = 1024 bytes, o que é metade da memória disponível no ATmega328 usado nos Arduinos Uno, Nano, etc.
Uma outra questão é que a biblioteca Wire do Arduino limita uma transmissão ao tamanho do seu buffer interno (32 bytes no caso do Uno e cia). Isto obriga, por exemplo, a quebrar a limpeza de tela em várias transmissões.
Por último temos a velocidade. O datasheet fala que o ciclo mínimo do clock é 2,5 us, o que equivale a uma frequência máxima de 400KHz (existem relatos de operar com frequência maior). O default da biblioteca Wire do Arduino é 100KHz, lembrando que se tiver outros dispositivos barramento I2C deve ser adotada a menor frequência máxima.
Vamos calcular o tempo mínimo para enviar uma tela inteira via I2C para o controlador usando o clock default:
- Vamos dividir o envio dos 1024 bytes em 64 transmissões de 16 bytes
- Cada bloco de 16 bytes precisa ser precedido de mais dois bytes: o endereço e o byte de controle
- A transferência de cada byte envolve 9 bits (8 de dados + 1 de ACK/NAK)
- Total de bits: 64*18*9
- Tempo usando um clock de 100Khz: (64*18*9)/100000 = 0,1 seg (aproximadamente)
Caracteres n x 8
Este é o que tenho usado normalmente. Uma altura de 8 pixels implica em termos 8 linhas de texto. Um tamanho tradicional de caracteres é 5x7, acrescentando um pixel de margem horizontal e vertical obtemos uma célula de 6x8, o que resulta em 21 colunas. Podemos também usar caracteres um pouco mais largos, resultando em células de 8x8 e 16 colunas de caracteres.Cada caracter ocupa n bytes no gerador de caracteres.
As rotinas para escrita de caracteres com os fontes nx8 são bem simples, abaixo uso uma célula de 6x8:
// Escreve um digito hexadecimal na linha l(0 a 7), coluna c(0 a 19) void Display_write_6x8 (byte l, byte c, byte dig) { uint8_t *gc = &font_6x8[dig*6]; // Endereça o caracter Display_sendcmd (SSD1306_COLUMNADDR); Display_sendcmd (c*6); Display_sendcmd (c*6 + 5); 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 < 6; x++) { Wire.write(pgm_read_byte(gc++)); } Wire.endTransmission(); }
Caracteres n x 16
Os caracteres n x 8 são bem pequenos. A próxima altura, sem malabarismos, é 16 pixels, e resulta em 4 linhas de texto. As rotinas são um pouco mais complicadas, o exemplo abaixo usa uma célula de 8x16, com cada caracter ocupando 16 bytes:
// Escreve um dígito hexadecimal na linha l(0 a 3), coluna c(0 a 15) void Display_write_8x16 (byte l, byte c, byte dig) { uint8_t *gc = &font_8x16[dig*16]; // Endereça e escreve a parte de cima Display_sendcmd (SSD1306_COLUMNADDR); Display_sendcmd (c << 3); Display_sendcmd ((c << 3) + 7); Display_sendcmd (SSD1306_PAGEADDR); Display_sendcmd (l << 1); Display_sendcmd (l << 1); Wire.beginTransmission(DISP_ADDR); Wire.write(0x40); // Co=0, DC = 1 for (uint8_t x = 0; x < 8; x++) { Wire.write(pgm_read_byte(gc++)); } Wire.endTransmission(); // Endereça e escreve a parte de baixo Display_sendcmd (SSD1306_COLUMNADDR); Display_sendcmd (c << 3); Display_sendcmd ((c << 3) + 7); Display_sendcmd (SSD1306_PAGEADDR); Display_sendcmd ((l << 1) + 1); Display_sendcmd ((l << 1) + 1); Wire.beginTransmission(DISP_ADDR); Wire.write(0x40); // Co=0, DC = 1 for (uint8_t x = 0; x < 8; x++) { Wire.write(pgm_read_byte(gc++)); } Wire.endTransmission(); }
Caracteres n x 12
Este caso é bem mais complicado e requer manter uma cópia da imagem da tela e utilizar operações de manipulação de bits para combinar o conteúdo anterior com o novo. As escritas são feitas primeiro na cópia em memória e depois a memória interna do controlador é atualizada.
Com altura 12 conseguimos 5 linhas de texto e sobram 4 linhas gráficas. Para utilizar melhor os displays onde a parte de cima da tela tem cor diferente, vou usar um mapeamento onde a primeira linha tem altura 16 (com as 4 linhas gráficas inferiores sempre apagadas) e as demais 12.
O código abaixo usa uma célula de 8x12. Por simplificação, a rotina abaixo escreve vários caracteres em uma linha e depois copia sempre a linha inteira ao invés de somente as posições alteradas.
// Cópia da imagem da tela na Ram uint8_t tela [SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8]; // Tabelas para facilitar o endereçamento const int addrLin[] = { 0x000, 0x100, 0x180, 0x280, 0x300}; const uint8_t pagLin [] = { 0, 2, 3, 5, 6}; // Escreve um "string hexadecimal" a partir da linha l(0 a 4), coluna c(0 a 15) void Display_print_8x12 (byte l, byte c, char *str) { // Atualiza na Ram int addr = addrLin[l]; while (*str != 0) { byte dig = 0xFF; if ((*str >= '0') && (*str <= '9')) { dig = *str-'0'; } else if ((*str >= 'A') && (*str <= 'F')) { dig = *str-'A'+10; } if (dig != 0xFF) { uint8_t *gc = &font_8x12[dig*16]; if ((l == 2) || (l == 4)) { // Alinhada em baixo for (int x = 0; x < 8; x++) { tela[addr+c*8+x] = (tela[addr+c*8+x] & 0x0F) | ((pgm_read_byte(gc+x) & 0x0F) << 4); } for (int x = 0; x < 8; x++) { tela[addr+0x80+c*8+x] = ((pgm_read_byte(gc+x) & 0xF0) >> 4) | ((pgm_read_byte(gc+x+8) & 0x0F) << 4); } } else { // Alinhada em cima for (int x = 0; x < 8; x++) { tela[addr+c*8+x] = pgm_read_byte(gc+x); } for (int x = 0; x < 8; x++) { tela[addr+0x80+c*8+x] = (tela[addr+0x80+c*8+x] & 0xF0) | (pgm_read_byte(gc+x+8) & 0x0F); } } } str++; c++; } // Copia para o controlador uint8_t pag = pagLin[l]; copyPag (pag); copyPag (pag+1); } // Copia uma página da Ram para o controlador void copyPag (uint8_t pag) { int addr = pag << 7; // pag * 128 = pag * SSD1306_LCDWIDTH Display_sendcmd (SSD1306_COLUMNADDR); Display_sendcmd (0); Display_sendcmd (SSD1306_LCDWIDTH-1); Display_sendcmd (SSD1306_PAGEADDR); Display_sendcmd (pag); Display_sendcmd (pag); for (uint16_t i = 0; i < SSD1306_LCDWIDTH/16; i++) { Wire.beginTransmission(DISP_ADDR); Wire.write(0x40); // Co=0, DC = 1 for (uint8_t x=0; x<16; x++) { Wire.write(tela[addr++]); } Wire.endTransmission(); } }
Uma possibilidade para reduzir a memória Ram utilizada seria armazenar a tela alfanumérica (com tamanho 5x16 = 80 bytes). Com isso as rotinas de escrita ficariam ainda mais complicadas.
Outros Tamanhos
Para suportar outros tamanhos (e fontes proporcionais) as rotinas seriam bem mais complexas e teria que guardar a imagem da tela na Ram. A estratégia de muitas bibliotecas é fazer todas as operações sobre a cópia na Ram e exigir a chamada de uma rotina refresh() para atualizar a imagem no controlador.
O código completo dos testes pode ser baixado do meu github.
Nenhum comentário:
Postar um comentário