ArduinoGeralPICProgramação

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

bin

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 frequencia 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çao 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 vicios 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 caracter 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():

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:

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:

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:

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:

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:

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:

 

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:

 

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:

 

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?

 

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 omití-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.:

 

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:

 

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:




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:

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:

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:

 

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

 

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

 

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:

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:

 

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:

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:

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

 




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 é:

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

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:

2^3 = 8

2^4 = 16

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:

 

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:

O resultado será \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:

 

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

 

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:

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

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

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

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:

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:

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.

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.

Alocar memória com C++

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

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’:

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:

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:

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

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

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:

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:

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:

Union

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

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.

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:

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.

E você, que dica pode adicionar a este post?
Se gostou, acompanhe-nos no Do bit Ao Byte no facebook e até o próximo!

6 comments
  1. Cleiton Bueno

    Muito bacana o artigo, parabens!

    Um duvida, porque não trocou por char aqui:

    //for (int counter=’a’;counter<'j'; counter++){
    for (char counter='a';counter<'j'; counter++){

    }

    Acha legal enfatizar que ao compilar deve-se passar c99 no -std para que use a declaração dentro do for()?

    Abraço

  2. Suhanko

    Não, não foi por causa do c99, foi um lapso mesmo. Se declarar como int não adianta nada passar um char que a vira inteiro :-)
    Valeu por ter notado, já corrigi!!!
    E obrigado pela apreciação.

  3. Suhanko

    É, deve ser c99 por padrão mesmo, eu nunca reparei as flags desse compilador #)
    Eu só não citei o c99 porque foquei em MCUs, mas sua observação é perfeita.
    Abraços e obrigado por colaborar novamente!

%d blogueiros gostam disto: