



Neste post, vamos ensinar como criar sua própria biblioteca para Arduino, mostrando todos os detalhes de desenvolvimento, desde o planejamento até a publicação, usando, de exemplo, uma biblioteca para LCDs alfanuméricos. É necessário conhecimento da linguagem C++ e da programação em Arduino. Por isso, recomendo a leitura das apostilas da eletrogate.
Para essa primeira parte do desenvolvimento, vamos utilizar:
Também vamos utilizar o software 7-Zip, que pode ser baixado aqui. Antes de começarmos o desenvolvimento da nossa biblioteca, temos que entender como a Arduino funciona.
Como podemos ver, a Arduino tem várias versões das mesmas bibliotecas-base para cada arquitetura diferente, esse “Core” são as bibliotecas obrigatórias para uma arquitetura funcionar na Arduino, e a IDE faz o trabalho de selecionar a correta para cada placa. Isso vai influenciar diretamente na criação de nossa biblioteca, quando queremos criar algo capaz de rodar em todas as placas temos que tomar cuidado com as dependências, por exemplo, se incluímos uma biblioteca como a keyboard.h no projeto, esse projeto não vai rodar em placas diferentes de Leonardo, Esplora, Zero, Due e família MKR como podemos ver no site da Arduino, pois Keyboard.h não faz parte do Core, então não é obrigatório que ela exista para todas as placas, também existem bibliotecas feitas especialmente para uma placa, um exemplo é a esp8266WiFi.h, como o próprio nome diz, é uma biblioteca para se trabalhar com o WiFi no esp8266. Vai depender do objetivo do seu projeto, quais dependências incluir e quais placas ela vai suportar.
Neste projeto, vamos criar uma biblioteca para LCDs alfanuméricos compatíveis com o HITACHI HD44780 chamada OmniCrystal, mostrando todos os passos do planejamento até a publicação.
Assim, o usuário pode escolher o meio de comunicação do LCD sem trocar de biblioteca, bastando apenas "encaixar" a interface desejada na classe.
Durante o desenvolvimento, certos pontos do planejamento podem acabar mudando. Mas, em vez de voltar e replanejar tudo de novo, foque em desenvolver um protótipo funcional, teste, anote bugs e possíveis melhorias e, então, volte, revisando esses pontos. Às vezes, a solução para um problema atual, pode ser o problema futuro. Agora começa a parte prática: escolha um IDE/editor de texto de sua preferência (o IDE é apenas para ter uma ambiente adequado para programar, o código será testado no Arduino IDE), no meu caso, Vscode + PlatformIO e siga a estrutura a seguir:
No nosso projeto:
A primeira coisa que temos que fazer é ir em cada arquivo de header (.h) e adicionar um "Include guard" (também é interessante colocar comentários explicando o que essa arquivo faz).
defaltmoules.h
//Modulos Pre-definos para nossa lib #ifndef DEFULT_MODULES_H #define DEFULT_MODULES_H #endiflcdinterface.h
//Interface customizavel para displays LCD alfanumericos #ifndef LCD_INTERFACE_H #define LCD_INTERFACE_H #endifomnicrystal.h
//classe principal #ifndef OMNICRYSTAL_H #define OMNICRYSTAL_H #endifInclude guard é uma definição (geralmente o nome do arquivo em caixa alta) que diz para o compilador se esse header já foi incluído. Isso previne que o arquivo seja incluído duas vezes e cause erros. Seguindo nosso planejamento, vamos criar o interface: lcdinterface.h
//Interface customizavel para displays LCD alfanumericos
#ifndef LCD_INTERFACE_H
#define LCD_INTERFACE_H
#include <stdint.h>
struct LCDInterface{
virtual void send(uint8_t config, uint8_t data) = 0;
};
#endif
esse código pode gerar algumas duvidas como:
//classe principal
#ifndef OMNICRYSTAL_H
#define OMNICRYSTAL_H
#include <Arduino.h>
#include <stdint.h>
#include <interface/defultmodules.h>
#include <interface/lcdinterface.h>
enum BusType {Bus4Bits, Bus8Bits};
class Omnicrystal{
private:
LCDInterface &_bridge;
const BusType _bus; // tipo de comunicação 4 ou 8 bits
const uint8_t _line; // quantidade de linhas no display
const uint8_t _col; //quantidade de colunas por linhas
public:
Omnicrystal(LCDInterface &bridge, const BusType bus, uint8_t line, uint8_t col) : _bridge{bridge}, _bus{bus},
_line{line}, _col{col}{}
};
#endif
Primeiro, incluímos as dependências. Depois, criamos uma enum para definir o tipo de comunicação (bus). Por que enums e não #defin ? tipos Enum permite criar uma lista limitada de opções em que é feito uma associação de nomes com valores constantes, evitando erros e melhorando a legibilidade do código. Exemplo:
//imagine uma função que recebe um numero inteiro
//onde cada numero realiza uma tarefa
#define TAREFA_1 0
#define TAREFA_2 1
#define TAREFA_3 2
int Fun_int(int tarefa);
//o compilador apenas vê os valores como inteiro
//ele não sabe que só existem tarefas de 0 a 3
Fun_int(TAREFA_1);
Fun_int(TAREFA_3);
Fun_int(99); //não gera erros
//agora com enums a historia é diferente
//definindo um tipo enumerado, você garante que as opções de entrada são sempre validas
enum Tarefa {
TAREFA1,
TAREFA2,
TAREFA3,
};
int Fun_enum(Tarefa tarefa);
Fun_enum(TAREFA1);
Fun_enum(TAREFA3);
Fun_enum(99); //invalido 99 não faz parte da Enum Tarefas
Vamos ver bastante uso de Enums nesse projeto.
Continuando, o que significa ": _bridge{bridge}, _bus{bus}...."? Isso é uma lista de inicialização que permite inicializar valores no construtor da classe. Umas das vantagens é inicializar valores constantes dentro de uma classe, como nos vemos no código. Mas, por que ele precisa ter um constante na classe? Valores constantes ajudam o seu compilador a entender o código. Se você tem certeza que um valor nunca muda, como o tipo de comunicação com o display ou o tamanho dele, é sempre bom declarar como constantes. (os valores devem ser inicializados na mesma ordem que forem declarados no classe, como nos declaramos: _bridge, _bus, _line, _col, temos que inicializar seguindo essa ordem)
A próxima etapa é criar as funções para enviar dados para o LCD. Para isso, temos que olhar o datasheet. Lá é onde vamos encontrar informações muito uteis. Primeiro, temos que criar uma função para iniciar o display. Olhando no datasheet, temos esse diagrama.
(https://www.futurlec.com/LED/LCD16X2BLa.shtml pag: 45-46)
Aqui, já temos todas as informações necessárias para começar. Como podemos ver, os modos para LCD 4 e 8 Bits são iguais no seu inicio, a diferença começando após a inicialização do display. Também temos que notar que existe esses pinos RS e RW. RS é o Register Select, que diz a nosso LCD se a informação que ele vai receber é um texto(HIGH/1) ou um comando(LOW/0). O R/W é o pino que diz se nos vamos ler ou escrever uma informação no LCD. Como não temos função para ler do LCD, vamos manter ele sempre no LOW, que significa apenas escrita. É com isso que nos temos que trabalhar agora. Primeiro declaramos a funções no header da classe principal: omnicrystal.h//classe principal
#ifndef OMNICRYSTAL_H
#define OMNICRYSTAL_H
#include <Arduino.h>
#include <stdint.h>
#include <interface/defultmodules.h>
#include <interface/lcdinterface.h>
enum BusType {Bus4Bits, Bus8Bits};
class Omnicrystal{
private:
LCDInterface &_bridge; //interface de comunicação
const BusType _bus; // tipo de comunicação 4 ou 8 bits
const uint8_t _line; // quantidade de linhas no display
const uint8_t _col; //quantidade de colunas por linhas
void send4Bits(uint8_t data, uint8_t RS_state);
void send8Bits(uint8_t data, uint8_t RS_state);
void send(uint8_t data, uint8_t RS_state);
public:
Omnicrystal(LCDInterface &bridge, const BusType bus, uint8_t line, uint8_t col) : _bridge{bridge}, _bus{bus},
_line{line}, _col{col}{}
Omnicrystal& begin();
Omnicrystal& write(const char *text);
};
#endif
Adicionamos uma função que envia 8 bits e outra que envia 4 bits para os modos de comunicação, uma função "send", que vai selecionar automaticamente a função correta baseada no "_bus", uma função para iniciar o LCD e uma para escrever informações nele. Agora, temos que criar essas funções no "source" (.cpp) da nossa classe principal. Mas, antes, precisamos de informações adicionais, como vimos após iniciar o LCD temos que enviar algum comandos. Então, vamos voltar ao datasheet:
(https://www.futurlec.com/LED/LCD16X2BLa.shtml Pag:24)
(https://www.futurlec.com/LED/LCD16X2BLa.shtml Pag:49)
#include <omnicrystal.h>
void Omnicrystal::send8Bits(uint8_t data, uint8_t RS_state){
_bridge.send(RS_state, data);
_bridge.send(RS_state | 0b00000100, data);
delayMicroseconds(1);
_bridge.send(RS_state, data);
}
//divide 8 bits em dois pacotes de 4bits
//(Ex: tranforma 0b01101001 em 0b01100000 e 0b10010000)
void Omnicrystal::send4Bits(uint8_t data, uint8_t RS_state){
uint8_t high_nibble = data & 0xF0;
uint8_t low_nibble = data << 4;
send8Bits(high_nibble, RS_state);
delayMicroseconds(1);
send8Bits(low_nibble, RS_state);
}
void Omnicrystal::send(uint8_t data, uint8_t RS_state){
if(_bus == Bus8Bits){
send8Bits(data, RS_state);
}else{
send4Bits(data, RS_state);
}
if(RS_state == 1){
delayMicroseconds(2);
}
else{
delayMicroseconds(40);
}
}
Omnicrystal& Omnicrystal::begin(){
delay(50);//aguarda o LCD iniciar
send8Bits(0x30, 0);
delayMicroseconds(4100);
send8Bits(0x30, 0);
delayMicroseconds(100);
send8Bits(0x30, 0);
delayMicroseconds(100);
//configura o modo de comunicação corretamente
if(_bus == Bus4Bits){
send8Bits(0x20, 0);
send(0x28, 0);
}else{
send(0x38, 0);
}
send(0x01, 0); //limpa o LCD
delay(2);
send(0x02,0); //Reinicia variaveis internas
delay(2);
send(0x06,0); //liga modo incremental do display
send(0x0F,0); //LCD on, cursor on, cursor piscando
return *this;
}
//lê cada letra de uma string e escreve no LCD
Omnicrystal& Omnicrystal::write(const char *text){
uint16_t i = 0;
while(text[i] != '\0'){
send((uint8_t)text[i], 1);
i++;
}
return *this;
}
Iniciamos o display de acordo com o datasheet. Mas por que o datasheet diz para enviar 0x30(0b0011_0000) três vezes? Se olhar na tabela de comandos, podemos ver que 0x30 é o comando que define a quantidade de bits na comunicação em 8bits (0b0010_0000 + 0b0001_0000). Então, significa que ele força a comunicação em 8 bits. Como 4 e 8 bits usam os mesmos pinos para configurar a comunicação, forçar em 8 bits garante que ele vai funcionar nos dois. É por isso que o fluxograma de inicialização é igual no inicio. Agora, falta apenas uma coisa para terminamos nosso primeiro protótipo: criar uma interface. Então, vamos no arquivo defultmodules.h e criar uma. Vamos começar com a comunicação paralela, a padrão do LCD.
defultmodules.h
//Modulos Pre-definos para nossa lib
#ifndef DEFULT_MODULES_H
#define DEFULT_MODULES_H
#include <stdint.h>
#include "lcdinterface.h"
#include "Arduino.h"
class LCDParallel : public LCDInterface{
private:
const uint8_t RS_pin;
const uint8_t EN_pin;
const uint8_t com_pins[8];
const uint8_t pins_offset;
//inicia os pinos usados no LCD
void init_pins(){
pinMode(EN_pin, OUTPUT);
pinMode(RS_pin, OUTPUT);
for(size_t i = pins_offset; i < 8; i++){
pinMode(com_pins[i], OUTPUT);
}
}
public:
LCDParallel(uint8_t RS, uint8_t EN, uint8_t D4,uint8_t D5,uint8_t D6, uint8_t D7):
RS_pin{RS},
EN_pin{EN},
com_pins{255, 255, 255, 255, D4, D5, D6, D7},
pins_offset{4}
{
init_pins();
}
LCDParallel(uint8_t RS, uint8_t EN, uint8_t D0,uint8_t D1,uint8_t D2, uint8_t D3,
uint8_t D4, uint8_t D5, uint8_t D6, uint8_t D7):
RS_pin{RS},
EN_pin{EN},
com_pins{D0, D1, D2, D3, D4, D5, D6, D7},
pins_offset{0}
{
init_pins();
}
void send(uint8_t config, uint8_t data){
digitalWrite(RS_pin, config & 0x01);
for(size_t i = pins_offset; i < 8; i++){
digitalWrite(com_pins[i], data & (1 << i));
}
digitalWrite(EN_pin, config & 0x04);
}
};
#endif
Primeiro, devemos herdar LCDIntrerface de forma "public" (a função "send" também precisa ficar na área "public" da classe). Após isso, definimos os pinos como constantes, pois eles não mudam durante a execução. pin_offset diz se estamos usando 4 ou 8 pinos. Se usarmos 4, ele ignora os pinos D0-D3. Se usamos 8, ele usa todos os pinos de D0-D7. Depois, criamos uma função para iniciar os pinos e um construtor. Caso o usuário use apenas 4 pinos, nosso construtor completa nosso array de pinos com 255. Esse numero não tem um significado especial, não representa uma porta, é só um valor para preencher espaço. Caso escolha 8 pinos, ele completa todo array com os pinos e, depois, chama nossa função para iniciar os pinos. A função send faz exatamente o que definimos no esquema de funcionamento durante o planejamento: verifica os bits na posição i de "data". Se for 1, ele coloca a porta da posição i em HIGH, se for 0 coloca a porta da posição i em LOW.
Você pode baixar o protótipo desse projeto em https://github.com/RecursiveError/omnicrystal/releases/download/v0.1.0-alpha/omnicrystal.zip
Agora, basta abrir o Arduino IDE, ir na opção Sketch->Include Libary -> Add .ZIP Libary
navegar até a pasta que esta o arquivo .zip e abrir.
#include <Arduino.h>
#include <omnicrystal.h>
LCDParallel lcdInter(6,7,8,9,10,11);
Omnicrystal lcd(lcdInter, Bus4Bits, 2, 16);
void setup() {
lcd.begin();
}
void loop() {
lcd.write("teste 1 ");
delay(5000);
}
|
Neste post, vamos ensinar como criar sua própria biblioteca para Arduino, mostrando todos os detalhes de desenvolvimento, usando, de exemplo, uma biblioteca para LCDs alfanuméricos. É necessário conhecimento da linguagem C++ e da programação em Arduino.
Encontre tudo na Loja Eletrogate com frete grátis para compras acima de R$ 200