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