Manual

do

Maker

.

com

Programar em C/C++ para microcontroladoras PIC e Arduino

Programar em C/C++ para microcontroladoras PIC e Arduino

Era apenas "PIC e Arduino" na época da primeira edição desse artigo, mas atualmente serve para tudo, inclusive ESP8266, ESP32, ARM, Raspberry etc.

Primeiramente, citei nomes de micro controladoras para ter alguma referência sobre o assunto. Não que eu darei aqui uma aula de programação para enchê-los de conceitos, porque também não sou nenhum senhor dos códigos, mas com certeza aqui você poderá encontrar ao menos uma dica que lhe sirva. Escolhi falar desse assunto porque tenho visto algumas ações não muito elegantes com códigos para Arduino. Claro, para tê-lo como um hobbie não é necessário ser especialista, mas tenho visto a adoção de costumes ruins de programação, herdados de dicas alheias.

Vamos esclarecer primeiramente a diferença em programar para um desktop/notebook e uma microcontroladora - e não pule a leitura, não vou falar de arquitetura de processador e microcontrolador - vou falar de RECURSOS.

Primeiramente, um parrudão; o Arduino UNO. E digo "parrudão" porque ele tem impressionantes 32KB de flash (Atmega328P). Tem a fabulosa marca de 2KB de SRAM! O clock é singelo; 16MHz, mas não o deixa ser menos por conta disso. Mas quanto recurso é isso no final das contas? - Vou compará-lo a um PIC beeem simples de 28 pinos (atenção; tamanho não é documento) porque o tenho à mão nesse exato momento.

O PIC16F883 é um ótimo microcontrolador, possuindo 256 Bytes de RAM e 7KB de flash, com frequencia de CPU variante entre 20MHz com oscilador externo e 8Mhz à 32KHz (K mesmo) de frequência utilizando o oscilador interno. Aí os pinos do Atmega e do PIC, sejam eles quais forem, tem seus recursos específicos, mas quero falar mesmo de MEMÓRIA, tanto de gravação quanto volátil.

Esse PIC custa (virgem) menos de R$8,00 contra R$13,00 de um Atmega328P. E olhe que ainda vai precisar de filtros, resistores e oscilador. Já neste PIC (utilizando o oscilador interno), apenas ele. Mas ainda não é esse o assunto a ser abordado aqui.

Suponhamos que o motivo seja exclusivamente custos e a opção tenha sido PIC, ou que alguém lhe tenha ofertado desenvolver um firmware para um determinado PIC. Suponhamos que você é um feliz usuário desse generoso Arduino que oferece uma fonte de recursos quase inesgotável para a maioria dos projetos de lazer que você faz. Agora vamos falar dos vícios que tenho visto e discorrer a respeito.

char, int, boolean, byte

Primeira coisa que pode te chocar, se você for um programador hobista; todo o tipo é inteiro. Um char é um inteiro também. Um boolean é um inteiro. E qual é a diferença afinal?

char

Um char ocupa 1 byte de memória, ou seja, 8 bits, que é igual a 256 valores possíveis (0 à 255). Cada valor representa um caractere da tabela ASCII e o programa sabe que a representação daquele inteiro deve ser expressada como seu signo relacionado na tabela ASCII.

int

Esse é matador. Um int no Arduino UNO ocupa 16 bits, ou 2 bytes, ou -2^15 até (2^15)-1. Isso representa 32,768 valores positivos possíveis, contra 256 valores possíveis do char. E se for no DUE por exemplo, ele armazena 4Bytes, ou, 32 bits, ou seja, de -2.147.483.648 até 2.147.483.647.

boolean é um valor pra true ou false que utiliza 1 byte e byte armazena valores numéricos não assinalados de 0 a 255; um byte.

float e double

Se possível, evite-os. Um float tem 32bits, indo de -3.4E+38 a +3.4E+38. Já o double, é o dobro: 64 bits, indo de -1.7E+308 a +1.7E+308. Se tiver realmente que utilizá-la, pode ser necessário limitar o número de casas tanto à esquerda quanto à direita. Existem algumas maneiras de fazê-lo, mas no Arduino a forma mais simples é utilizando dtostrf():

dtostrf(variavelFloat,2, 3, strOutput);

Desse modo, duas casas à esquerda, 3 à direita. A variável de entrada é a variavelFloat e a saída é armazenada no buffer strOutput.

Economia vai além do dinheiro

Aí precisamos marcar uma condição a ser verificada e utilizamos o chamado "flag", que é uma bandeira sinalizando que uma condição ocorreu ou não ocorreu. Mas é comum encontrar programas com várias flags - e não que seja errado, mas imagine isso:

int pretoOuBranco;
int cervejaOuGuarana;
int podeOuNaoPode;

Nesse caso, será reservada uma área de memória de 32.768*3 (positivo) mesmo que só uma seja usada no final. Tratando-se de 2 estados, o boolean é uma opção melhor porque só ocupa um byte:

boolean pretoOuBranco,cervejaOuGuarana,podeOuNaoPode;

O gasto aí é de 256*3 posições de memória! Mas dá pra melhorar da seguinte maneira:
São 3 flags que totalizam 6 estados; nesse caso há de se ter cuidado, mas a economia será maior fazendo assim:

char flagCorBebidaSimNao;

Utilizando essa ordem, pense assim: '1' e '2' para cor; '3' e '4' para bebida; '5' e '6' para sim ou não. Desse modo basta assinalar:

flagCorBebidaSimNao = '3';

Pronto! com 1 Byte você poderá ter até 256 flags! "Mas e se as 3 tivessem que ser comparadas simultaneamente?" - pensa alguém. Bom, nesse caso, será necessário criar um array:

char flagCorBebidaSimNao[3];

E aí cada posição do array poderá guardar só 2 estados, ou até 256 estados cada um, ja que você foi obrigado a reservar 3 endereços para bytes. Ainda assim é extremamente mais economico que utilizar int, como pode ser notado.

Loop econômico

Muitas vezes uma variável é incrementada através de um loop, para facilitar as coisas, mas em alguns casos tudo o que você precisa é contar 10 voltas, assim:

for (int i=0;i<10;i++){
    ...
}

E lá vai um inteiro ocupar memória por causa de 10 posicionamentos! Mas é fácil economizar memória nesse caso, reservando apenas um char:

for (char counter='a';counter<'j';counter++){
    ...
}

Utilizar menos de 1 byte

Se você tiver que receber por exemplo, dados de um dispositivo que envie seu ID (até 15, por exemplo) e um valor de status ON ou OFF, seria um pouco dispendioso criar 2 ints para guardar esses valores, certo? Bem, você pode criar estruturas de dados, definindo seu próprio tipo.

Não é um raciocínio trivial e não pretendo explicar muito a respeito de estruturas nesse post, mas uma estrutura é um tipo de dado que você cria como se fosse um int ou um char, mas essa estrutura será SEU tipo. Por exemplo:

struct devices
{
    int ID;
    int status;
};

devices UNO; //cria uma variável do tipo devices chamada UNO

Lembra que mais acima discorri sobre o tipo int que usa um espaço enorme? Bem, podemos usar int ocupando menos espaço que char! Quer ver como?

struct devices
{
    int ID     : 4; // 4 bits, ou 1111, ou 2^4. Pode alocar até 16 valores!
    int status : 1; // 0 ou 1 apenas!
};

struct devices UNO;

Vamos aos detalhes. O tipo int foi modificado para comportar 4 bits, assim ele aloca valores de 0 a 15. Já status é apenas ON ou OFF, logo, 0 ou 1 e nesse caso basta 1 bit.

Por fim, eu criei a variável de modos diferentes, se você reparar. A primeira eu não adicionei a palavra reservada 'struct', porque você pode omiti-la em C++. Já na segunda, 'struct' precede a criação da variável porque em C você é obrigado a fazê-lo. Então, se estiver utilizando MikroC, MPlab ou outro que use a linguagem C, a segunda opção é obrigatória. Já no caso de Arduino a programação é em C++, portanto o primeiro tipo é valido, mas o segundo também. A última observação é que você tem que ser cauteloso com o uso da variável modificada, porque se você exceder o tamanho definido, você receberá um 0 de retorno, ou um dos valores possíveis no estouro da base. Ex.:

struct devices
{
    int ID     : 4;
    int status : 1;
} UNO = {
    15,
    1
};

UNO.status = 3;
/*Ou retornará 0, ou retornará 1.*/

Há ainda uma maneira mais de economizar, utilizando menos que 1 byte.Tem pinos sobrando em sua MCU? Bem, ele pode ser tornar uma flag de 2 bits:

if (....){
    PORTBx_bit = 0;
}
else{
    PORTBx_bit = 1;
}
...
if (PORTBx_bit > 0){
    ....
}

Com isso, mais um byte foi economizado. E se você tiver então 3 pinos sobrando, é uma festa, porque você poderá utilizar vários estados combinando-os:

000
001
010
011
100
101
110
111

Depois os etados podem ser resolvidos em uma função pequena ou diretamente em condicionais.

Entra então uma última questão nesse momento; decorar estados não é legal e pode causar confusão, e para resolver sem ocupar memória...

define

O define é uma macro utilizada para interpretar um valor representado por uma definição do programador. Por exemplo:

//No header:
#define PRETO '0'
#define BRANCO '1'
#define CERVEJA '2'
#define GUARANA '3'
#define NAO '4'
#define SIM '5'

//E na atribuição do valor:
char flagCorBebidaSimNao = BRANCO;

Sendo que define pode armazenar string, int ou char, bastando seguir as regras da linguagem (int sem proteção, char com apóstrofe e string com aspas).

Com isso, podemos simplificar essa condicional:

PORTBx_bit = n-2 >10 ? LOW : HIGH;
...
if (PORTBx_bit){
    ...
}

Eu mesmo não sigo todas as regras, ainda mais quando o programa é curto e não estou procurando o 'estado da arte' Arduínica (essa eu mesmo inventei). Mas sério, tem PICs com pouquíssimos bytes, mas com recursos muito bons; PICs de 8 a 40 pinos, e o dimensionamento do MCU vai além do custo dela própria; pode estar relacionada com tamanho, arquitetura da board, consumo, recursos disponíveis e o que mais for. Se a necessidade for programa pra uma MCU modesta, é bom adotar essas práticas.

Um pouco de redução de código

Tem algumas coisas escritas em código que o deixa muito claro, auto-explicativo. Não é errado e pode inclusive não influenciar em nada, mas algumas expressões modificadas podem dar uma beleza extra ao código. Por exemplo, o uso de operador ternário:

if (n-3 > 0){
    X = HIGH;
}
else{
    X = LOW;
}
...

Com o uso de operador ternário, isso ficaria:

X = n-3 > 0 ? 1 : 0;

Ele pode ser utilizado também para controlar fluxo de funções. Por exemplo:

int a(int valueX){
    int n = 0;
    n = (n*2)-valueX;
    return n;
}
int b(int valueX){
    return valueX*2*2;
}
int c(valueX){
 return valueX*-3;
}
X = a() > 0 ? c(a()) : b();

O exemplo não foi baseado em nada, é apenas uma representação, não se atenha à lógica das funções.

Comparar byte e bytes

Aqui dá pra economizar um pouco de processamento, mas criar uma função ocupará espaço em memória, é necessário analisar a melhor condição.

A maneira mais simples de comparar um byte é obviamente, diretamente com o comparador de igualdade, porque afinal de contas char é um tipo de inteiro também, como citado anteriormente:

if (valueX == 'c'){
   ...
}

Se for para comparar um array de char (como já dito, não existe o tipo string em C, tudo não passa de um array de char com terminador nulo), você tem opção de economizar em memória, processamento ou linhas de código:

int compare(char *string, char *buffer, strSize){
    for (int i = 0; i < strSize; i++){
        if string[i] != buffer[i]){
            return -1;
        }
    }
    return 0
}

A primeira coisa importante acima é que foi criada uma função que comparar duas 'strings' conforme um tamanho passado. Se o tamanho dessa variável for constante, provavelmente essa é a melhor opção porque strcmp() utiliza mais recurso do que isso, apesar de ser mais simples:

strcmp(a,b);

Como essa função não recebe o tamanho do array (no máximo, um limite de busca pelo terminador nulo), uma tarefa extra é executada, que é ao menos uma condicional que compara além do byte, o terminador nulo, para que o processamento seja interrompido automaticamente caso necessário. Será que vale a pena declarar a sua própria? Eu não fiz benchmark.

A segunda coisa legal nesse exemplo é a simplificação da condicional graças a seu uso dentro de uma função. A condicional if encerra o processamento caso a condição não seja atendida. Se for atendida até o final, o código segue seu fluxo. Nesse caso, não foi necessário fazer um 'else', já que garantidamente a condição foi atendida. Isso economizou 1 instrução condicional.

Preenchendo um array em sua declaração (ou, 'inicializando um array')

Normalmente tento prevenir erros inicializando todas as variáveis que declaro, porque se uma leitura errônea (por erro de lógica minha) for executada em um endereço de memória despreparado, o mínimo que pode acontecer é retornar lixo. O mesmo faço com array, mas não é necessário iniciar um loop para tal preenchimento. Além do mais, '\0' é 0 literal. Um array de char declarado em meu código normalmente tem a seguinte forma:

char str[10] = {0};

E a variável é preenchida em tempo de compilação, dispensando seu preenchimento inicial dentro de um loop (forma que não adoto):

for (int i=0; i<10;i++){
    str[i] = '\0';
}

Nesse caso, uma variável de contagem e um loop foram utilizados desnecessariamente.

Deslocamento de bits

Gosto demais desse recurso, muito simples de utilizar inclusive. Você nunca mais vai esquecer se já tem intimidade com base binária. Para quem não tem, inicio com uma rápida explicação a respeito.

O valor de uma posição binária é 2^X, iniciando em 0. Qualquer valor elevado a 0 é 1, portanto se você tem apenas 1 bit, dois valores são possíveis; 0 ou 1.

1 byte são 8 bits, ou 1 octeto. Sua representação é:

00000000

Em 1 byte até 256 valores são armazenados (0 à 255), porque 2^7 = 128. Somando seus anteriores:

$latex 2^7+2^6+2^5+2^4+2^3+2^2+2^1+2^0 = (2^8)-1 \therefore 255 $

Lembre-se, a contagem é da direita para a esquerda, iniciando 0 exponencial e crescendo um a um.

Passemos agora ao deslocamento de bits, iniciando em...

'shift left'

Quando os bits se deslocam para a esquerda, a operação é de multiplicação de seu valor por 2. Vamos ver o porquê disto:

$latex 2^3 = 8$

$latex 2^4 = 16$

$latex 2^5 = 32$

Reparou que a próxima posição é sempre o dobro? Então se eu pegar um bit que está na posição 3 e colocá-lo na posição 4, seu valor dobra.

Por isso:

int x = 4;
x = x << 2

O resultado será 4*2*2, ou 16, porque 4 em binário é igual a 100. Se desloco 2 bits para a esquerda, esse valor passa a ser 10000, ou 16.

Utilizar o deslocamento de bits tem mais de uma aplicação, mas uma aplicação garantida é saber o valor de uma posição binária sem fazer loop ou utilizar funções.

'shift right'

Se você andou para a esquerda e o valor dobrou, obviamente que ao voltar você já sabe o valor é n/2, porque ele era a metade do valor atual:

x = 16 >> 2;

O resultado será $latex \frac{(\frac{16}{2})}{2} = 4$

Isso porque:

16 = 10000. 16 >> 2 = ..100  ou, 4 binário.

Representação de char para binário

Já que citei deslocamento de bits, vamos ver um de seus usos na prática.

Existem casos que uma string binária pode ser recebida via algum protocolo e isso virá no formato de array de char. Como saber o valor binário de "10000011"?

Uma função rápida para isso:

int charToBin(char *str){
    int j       = 0;
    int counter = 0;
    for (int i=7;i>-1;i--){
        counter += str[i]-48 > 0 ? 1<<j : 0;
        j++;
    }
    return counter;
}

Depois é só guardar o retorno de uma chamada dessa função (um teste):

    char value[] = "10000011";
    cout << charToBin(value);

Ponteiro invés de string

Esse é um assunto que não gosto de tratar porque ponteiro é algo muito delicado e um erro certamente será crítico. De qualquer modo, se você está programando em C ou quer economizar memória, alocando exclusivamente um array de char, você pode fazer o seguinte (acompanhe primeiro o raciocínio e o último exemplo é o válido):

1 - Crie o ponteiro:

char *myStr;

2 - Reserve a memória que ele utilizará (para não invadir memória de programa, etc):

myStr = malloc(STR_SIZE);

3 - Ponteiro não tem tipo, então especifique o tipo que ele está alocando:

myStr = (char*) malloc(STR_SIZE);

4 - O tamanho do tipo pode variar conforme arquitetura e compilador, portanto defina o tamanho de alocação para tornar a alocação segura:

char *myStr = (char *) malloc(STR_SIZE*sizeof(char));

Agora vou discorrer só um pouquinho a respeito. No raciocínio 4 o ponteiro myStr foi criado e alocado com malloc. logo após o sinal de igualdade, (char *) é um cast. Casting é utilizado para indicar o tipo pretendido, mas não vou entrar nesse assunto agora.

Seguidamente ao cast, a chamada de alocação de memória malloc recebe como parâmetro o tamanho de alocação. Supondo:

#define STR_SIZE 10

Então malloc reserva um endereço de 10 posições para char. Multiplicando pelo tamanho de char da arquitetura com sizeof(char), a memória será alocada do tamanho preciso. Isso é porque o tamanho do tipo varia de plataforma pra plataforma. Depois você pode exibí-lo na tela como se fosse uma string mesmo:

printf("%sn",myStr);

Seja como for, tenha sempre o cuidado de limpar a área reservada antes de utilizá-la para não ler sujeira. O exemplo de um clear() está lá mais acima.

Um pouco mais sobre ponteiros

Essa é uma atualização desse artigo que achei importante colocar por causa do ESP32. Chegarei no ponto de explicar o porquê, então comecemos com os conceitos

Um ponteiro é como um link simbólico em sistemas Linux (a analogia é boa, mas se você só usa Windows, vai ter que continuar a leitura antes de pensar a respeito).

Suponhamos que uma variável do tipo int foi criada com o valor 10:

int tutorial = 10;

Quando criamos uma variável, um endereço de memória que caiba o tipo da variável é reservada. Como se trata de um int, 2 bytes de memória foram reservados para ele. Agora, se quisermos acessar o valor dessa variável através de um ponteiro, fazemos:

int tutorial = 10;
int *p;
p = &tutorial;

O asterisco (*) é utilizado para pegar um endereço de memória que caiba o tipo declarado - no caso, um endereço livre que caiba um inteiro. O & (amp) é utilizado para acessar o endereço de memória que foi dedicado à variável.

Quando fazemos p = &tutorial, estamos criando um apontamento para o endereço da variável tutorial. Isso significa  que se alterarmos p, estaremos alterando na verdade a variável tutorial, pois o ponteiro p está apenas armazenando o endereço da variável tutorial.

Para imprimir o endereço, utilizamos printf("%p",&variavel); em C. Para imprimir o valor da variável, utilizamos printf("%d",variavel);.

Agora um exemplo de acesso da variável e de um ponteiro para essa variável:

int tutorial = 10;
int *p;
p = tutorial;

printf("%d",tutorial);
printf("%d",p);

//Agora os endereços:

printf("%p",&tutorial);
printf("%p,p);

//Agora o endereço do ponteiro:

printf("%p,&p);

Acredito que tenha ficado claro que para acessar o endereço utilizamos "&".

Para apontar para algum lugar, usamos "*".

Se quisermos criar um array de char de tamanho fixo, fazemos:

char dez[10];

Serão reservados 10 endereços de 1 Byte. Se quisermos pegar um lugar da memória para alocar indefinidos Bytes do tipo char, fazemos:

char *indefinido;

O problema do segundo é que o gerenciamento de memória fica delicado, podendo invadir regiões de memória ocupadas com um overflow. Do primeiro modo, um tamanho fixo é alocado e fica fácil tratar esse endereço com memset (disponível nesse tutorial).

Nesse artigo também cito alguma coisa de cast. Precisei dar uma incrementada agora porque tem um caso específico que precisamos fazer cast da variável  void* na task do ESP32. Um exemplo está nesse artigo sobre a CLP i4.0. O cast foi feito assim:

void myTask(void *pvParameters){
   uint8_t &get_value = *(uint8_t*) pvParameters;
...

Desse modo foi alocada uma variável do tipo unsigned char com o ponteiro do ponteiro, fazendo um cast de void para unsigned char. Criar apenas uma variável comum e casting não funcionaria nesse caso, então a dica é: como fazer casting de um ponteiro void para char.

Liberar a memória

Isso é um espinho no olho. Se você não tratar da memória alocada, você vai criar o chamado "memory leak". Quando você fizer um ponteiro, se estiver dentro de uma função, toda a vez que terminar seu uso e ideal utilizar free(myStr) pra liberar a memória, senão será um estrago certamente. Se a alocação for utilizada por outra função, alguém dentro desse programa terá que gerenciar essas alocações para não dar problema, mas a memória não deve ser abandonada sem tratamento, senão toda a vez que função for chamada, um endereço de memória diferente vai alocar a string.

Qual a diferença entre free() e memset()?

Essa é simples. Com free() a memória é liberada e devolvida ao sistema. Com memset() os valores da variável são reatribuídas. A função free() recebe como parâmetro a variável e a função memset() recebe três valores, sendo a variável, o valor a atribuir e o tamanho:

//supondo:

int *teste = (int*) malloc(sizeof(int)*3);
...
free(teste);
//ou, para reatribuir os valores):
memset(teste,0,3);

Alocar memória com C++

Com C++ é um pouco mais confortável, utilizando o operador new.

int *myNum = new int;

Por quê int aqui? Bem, C++ tem o tipo string, e aqui só estou exemplificando a utilização do operador. A regra em C++ é que TUDO o que for criado com 'new' deve ser excluido com 'delete':

delete myNum;

Só isso a respeito de ponteiro, senão vai longe.

Substring em C

Tem várias maneiras de fazê-lo, mas a mais rápida eu presumo que seja assim:

const char* source = "abcdefgh";
char *target = (char*) malloc(6);
strncpy(target, source+2, 5);

Ou seja, a partir de "c" copia para o buffer "target". Claro que para isso você precisa incluir os headers. Um código de exemplo:

#include <stdio.h>
#include <string.h>
#include <malloc.h>
int main(){
    const char* source = "abcdefgh";
    char *target = (char*) malloc(6);
    strncpy(target, source+2, 5);
    printf("%s\n",target);
    return 0;
}

Porém nesse caso, pensando em microcontroladoras, a melhor opção é reduzir includes. Ainda prezando pelo número de linhas, outra opção seria:

char buff[5];
memcpy(buff,&buffer[10],4);
buff[4] = '\0';

E do modo mais "cru", escreve-se mais código mas utiliza-se menos recursos:

void substr(char str[],char sub[],int p,int len) {
    int ch = 0;
    while (ch < len) {
        sub[ch] = s[p+ch-1];
        ch++;
    }
    sub[ch] = '\0';
}

Enum

Já que citei a struct mais acima, vou aproveitar pra discorrer sobre seus 'parentes'. O enum é uma maneira de criar algo parecido com define, podendo ser criado de duas maneiras:

enum literalNumber {
    zero,
    um,
    dois,
    tres,
    quatro
};

Desse jeito, automaticamente serão atribuidos valores de 0 à 4 aos respectivos nomes (que poderiam ser frutas ou algo como definições de estados para OFF, ON etc).

Depois, em qualquer lugar do programa, bastará chamar pelos nomes definidos dentro do enum:

void loop(){
...
    Serial.println(um);
...
}

A declaração é similar a uma struct, mas não faça confusão, são coisas distintas.

Se desejar, pode definir os valores para cada um dos componentes do enumerador, bastando utilizar o valor após um sinal de igual:

enum literalNumber {
    zero   = 4,
    um     = 3,
    dois   = 2,
    tres   = 1,
    quatro = 0
};

Union

union é como uma struct, mas que só pode armazenar 1 tipo de dado, por exemplo,  int, longdouble - O union só pode comportar um desses tipos por vez. No caso de int, inclue-se o char.

union val{
 int  numero;
 char letra;
} teste;

teste.numero = 10;
Serial.println(teste.numero);

Não tenho certeza se a dica é válida aqui, mas repare que dentro da estrutura os valores agora são separados por ';'. Isso fica fácil de compreender, porque estamos utilizando tipagem para nossas variáveis, da mesma forma que fazemos no corpo de nosso programa. Logo após a declaração da estrutura do union, antes de ';', coloquei um nome para o tipo. Teste passa a ser do tipo val.

A utilização de enum é clara, mas em que momento utilizar union? Digamos que você tem um http server rodando no Arduino. Na página que ele exibe, tem um botão e esse botão tem algumas propriedades como cor, largura, altura e texto. O union serviria para guardar os valores numéricos ou texto. Mas para guardar todos os valores de diferentes tipos, vou exemplificar com uma struct.

struct widget {
    char color[7]; // ex.:"#FFCC00";
    int width;
    int height; 
    char text[20]; //pode ser qualquer coisa
};

widget button;
strcpy(button.color,"#FFCC00");
strcpy(button.text,"Abrir");
button.width  = 15;
button.height = 30;
...

Agora temos um widget button com todas as definições pertinentes. O código ficará mais compreensível e indolor. O legal é que se precisarmos criar um widget inputtext por exemplo, é só criar mais um widget dessa struct.

Inline

Quando compilamos nosso código com uma função comum, a porção de execução referente a essa função (quanto 'ão', não?) fica em um determinado ponto do programa. Pense em um monte de livros empilhados. O livro de matemática ficou lá em cima, o livro sobre design ficou em terceiro e  principal está lá embaixo.

Quando você precisa chamar uma função em seu programa, o que acontece é um 'salto' do endereço atual do código para o endereço onde está armazenada a função. Depois de executar, o programa retoma do endereço em que parou. Isso pode fazer com que o programa tenha uma responsividade menor (perceptível principalmente em MCUs), dependendo da quantidade de vezes que essa função é utilizada no decorrer do código.

Para sanar esse problema, podemos utilizar uma função inline. O que ela faz é colocar a execução da função alinhada com sua chamada, assim não acontecem saltos e o programa terá sua execução melhorada. Mas acalme-se, não é só vantagem. Utilizar o inline faz com que o processo da função seja copiada para todos os pontos em que for chamada, utilizando assim mais memória, proporcional ao número de chamadas, portanto, utilizar a função inline é um caso a ser pensado e medido.

Um exemplo de função inline:

static inline void soma(int a, int b){
    int c = a+b;
    Serial.println(c);
}

O inline é mais complicado em seu comportamento do que em sua declaração. Estar 'inline' depende do compilador, ele decidirá se a função será ou não online baseado em alguns fatores. Um exemplo seria quando a posição para o compilar está melhor do que a definida pelo programador. Tem diversas outras questões, mas esse não é um artigo sobre o inline, apenas um artigo que discorre sobre ele.

Inscreva-se em nosso canal Dobitaobyte no youtube!

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.