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:

 

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!

Comments

comments

Djames Suhanko

Djames Suhanko é Perito Forense Digital. Já atuou com deployer em sistemas de missão critica em diversos países pelo mundão. Programador Shell, Python, C, C++ e Qt, tendo contato com embarcados ( ora profissionalmente, ora por lazer ) desde 2009.

Deixe uma resposta