Arduino e interrupções de temporização: mais eficiência no exemplo do dimmer

Como usar melhor as interrupções do Arduino para gerenciar sincronização entre eventos.

No recente artigo “Arduino e dimmer com Triac”, em que vimos como usar um módulo dimmer AC para gerar o efeito de amanhecer artificial variando a luminosidade de uma lâmpada incandescente comum, observamos que o uso de pausas para temporização dentro de uma rotina de tratamento de interrupções reduz a eficiência de execução de código pelo Arduino, e pode ter outros efeitos adversos.

Para demonstrar como a mesma temporização pode ser alcançada (sem causar o mesmo tipo de bloqueio indesejado) por meio de interrupções baseadas nos timers do ATmega328, produzi uma nova versão do programa, bem mais complexa, acompanhada de comentários que explicam seu funcionamento.

O experimento usou o mesmo Módulo Dimmer para Arduino do artigo anterior, mostrado na foto acima, e cedido pela Usinainfo, que patrocina ambos os experimentos.

O circuito foi ligeiramente modificado para incluir um botão de acionamento, mas mantém as demais conexões idênticas às do artigo anterior.


Índice das seções:

  1. Componentes e montagem
  2. Aplicabilidade X Complexidade X Oportunidade de praticar
  3. Interrupções do Arduino: introdução ao Timer1
  4. O programa
  5. Patrocínio: Usinainfo

Componentes e montagem

Os componentes são os mesmos do artigo anterior, cuja leitura eu recomendo para a identificação da pinagem e das funcionalidades do módulo.

Na versão deste artigo, além do Arduino e do módulo, incluí ainda uma chave liga-desliga (SPST) para dar início ao amanhecer artificial, e um resistor (eu usei no valor de 330Ω).

A montagem realizada está representada no diagrama a seguir:

Antes de prosseguir, um alerta importante:

Atenção: o projeto deste artigo utiliza conexão direta à rede elétrica residencial, o que causa riscos de CHOQUE ELÉTRICO e INCÊNDIO, entre outros, com danos pessoais, morais e materiais sobre os quais este autor não se responsabiliza. Ele é apresentado como descrição de um experimento realizado, e não como proposta de implementação. Só execute um projeto como este caso você tenha experiência e habilitação com instalações elétricas. Sempre siga as normas de segurança aplicáveis, e certifique-se de que todos os equipamentos e infraestrutura associada também as seguem. Use equipamentos de proteção individual, e sempre acrescente os componentes de segurança (fusíveis, resistores, isolamento, etc.) adequados a cada projeto, sabendo que eles não estão representados neste artigo e devem ser selecionados de acordo com as condições de cada execução.

Acrescento: vários dos conectores e componentes do módulo dimmer estarão energizados com a eletricidade recebida da tomada da sua casa. Tocá-los diretamente, ou encostá-los em qualquer material condutor (incluindo suas ferramentas) é análogo a encostar diretamente na tomada. Operar esse circuito exige atenção e cautela especiais.

Uma observação: por sua própria natureza, o circuito do Triac esquenta bastante durante o uso. A Usinainfo registra em sua documentação que ele é voltado especificamente à função de dimmer de lâmpadas incandescentes, e que há necessidade de usar dissipador de calor. Sugiro consultar as datasheets dos componentes, para estimar a demanda disso de acordo com a carga e as condições ambientais.

Aplicabilidade X Complexidade X Oportunidade de praticar

Pessoalmente sou avesso a complexidades desnecessárias, e procuro seguir a filosofia descrita por Da Vinci em uma frase também simples: a simplicidade é o grau máximo da sofisticação.

Nesse sentido, o artigo anterior está muito mais próximo do ideal: usou um programa curto para demonstrar um efeito complexo, desprezando (por razões didáticas) os efeitos adversos dessa simplificação.

Hoje, neste artigo em que identificaremos uma forma complexa de evitar esses efeitos adversos, temos complicadores que poderiam ser facilmente evitados. Não chega a ser um projeto digno do Coiote do desenho do Papa-léguas, mas ainda assim são aspectos que poderiam ser ignorados, especialmente se o meu foco de hoje fosse criar um produto.

Por exemplo, eu poderia:

  1. ter optado por usar LEDs alimentados diretamente pelo Arduino, e não uma lâmpada incandescente ligada na tomada;
  2. ter usado bibliotecas pré-prontas como a ACPWM, e evitado referenciar diretamente no meu programa qualquer interrupção do hardware;
  3. ter evitado bibliotecas pré-prontas, mas limitado o uso de interrupções ao mínimo necessário (detecção do pulso no pino ZC do módulo), lidando com as temporizações diretamente no loop(), por meio da função delayMicroseconds();
  4. etc.

O terceiro item é exatamente o que fiz no artigo anterior: incluí longas chamadas à função delayMicroseconds() dentro da rotina de tratamento da interrupção, confiando que ela não atrapalharia a funcionalidade. Para um programa de exemplo cujo loop() não inclui outras funcionalidades, isso serve, mas dominar a alternativa é interessante para os casos em que se deseja ter uma funcionalidade adicional responsiva e que não seja prejudicada por paradas evitáveis, causadas desnecessariamente por uma rotina de interrupção mal planejada.

Além disso, a oportunidade de praticar o uso de técnicas mais complexas em uma solução facilmente verificável (afinal, o controle do brilho de uma lâmpada é bem visível, ainda que o uso de AC exija cuidados especiais) nem sempre surge em um dia em que há tempo para isso e as ferramentas estão todas à mão ;-)

Assim, você fica avisado: quanto à técnica de programação empregada, o que virá a seguir expõe bem mais complexidade do que precisaria, por uma opção minha, que tenho interesse nas técnicas, independentemente da aplicabilidade exemplificada exigir ou não.

Boa parte dessa complexidade adicional é relacionada ao uso das interrupções do timer, explicadas a seguir.

Interrupções do Arduino: introdução ao Timer1

No artigo anterior já vimos que, embora não seja multitarefa, o microcontrolador ATmega328 que é o cérebro do seu Arduino Uno tem uma série de recursos que permitem reagir a eventos, suspendendo brevemente a atividade rotineira que estava em execução, executando rapidamente uma função correspondente ao evento ocorrido, e retornando transparentemente à atividade rotineira anterior.

Esse mecanismo é chamado, genericamente, de Interrupção, e pode reagir a uma série de eventos, incluindo 2 que serão especialmente úteis para nosso artigo de hoje: a mudança de estado em determinado pino (já vista no artigo anterior), e a passagem de um determinado tempo, que pode ser contada de várias formas, duas das quais serão apresentadas hoje.

No programa de hoje, usarei 3 interrupções diferentes, cada uma delas sendo a responsável por ativar a próxima. A primeira é nossa já conhecida INT0, acionada quando um pulso em ZC muda o estado do pino 2.

No programa do artigo anterior, a rotina acionada pela INT0 usava 2 vezes a função delayMicroseconds() para paralisar a execução do Arduino: a primeira, por um prazo que chamamos de t1, era a pausa entre a detecção do pulso em ZC e o envio de um pulso para o pino DIM, que faz a lâmpada acender; e a segunda, que é a pausa (cuja duração chamamos de t2) entre o início do pulso em DIM e a sua finalização.

Trocando 2 chamadas ao delayMicroseconds() (feitas pela INT0) por 2 interrupções do Timer1, nosso loop() ganha a oportunidade de ficar mais tempo em execução

No programa de hoje, a rotina acionada pela INT0 não fará nenhuma pausa. Ao invés disso, ela vai configurar uma segunda interrupção interna do ATmega328 – chamada TIMER1_COMPA – para ocorrer após a passagem do tempo t1. Após programar esse timer, a INT0 rapidamente encerrará, devolvendo imediatamente o controle ao loop().

O loop() continuará sendo executado normalmente por algum tempo (que no programa original estaria sendo consumido por um longo delayMicroseconds()) até que o tempo t1 transcorra, o que deflagrará a TIMER1_COMPA (que foi programada pela INT0).

A rotina da TIMER1_COMPA estará programada para dar início ao pulso no pino DIM. Além disso, ela vai imitar parcialmente o que a INT0 fez: imediatamente antes de encerrar, vai agendar outro timer, na interrupção TIMER1_OVF, para ocorrer após a passagem do tempo t2. Após agendar, ela também rapidamente encerrará, devolvendo o controle ao loop().

O loop() voltará a ser executado normalmente por mais algum tempo (que no programa original estaria sendo consumido por um outro delayMicroseconds()) até que por sua vez a TIMER1_OVF (que foi programada pela TIMER1_COMPA) seja deflagrada após a passagem do tempo t2.

Finalmente, a rotina da TIMER1_OVF estará programada para encerrar o pulso no pino DIM, e desativar todos os timers. Isso, é claro, até que a próxima INT0 ocorra e reinicie o ciclo

A seguir descreverei em mais detalhes cada uma dessas 3 interrupções. Enquanto estiver lendo, tenha em mente que cada uma delas vai ser chamada 120 vezes por segundo, que entre o final de uma e o começo da outra o controle sempre retornará ao loop(), e que você provavelmente vai entendê-las melhor quando observar o código do programa, adiante, e as referências à datasheet!

São 3 interrupções entrelaçadas:

  • INT0: a interrupção externa INT0 é associada ao pino 2 do Arduino Uno. A cada vez que a chave é acionada, uma rotina na função loop() faz uso da função attachInterrupt() (padrão do Arduino) para associar a INT0 a uma função definida no meu programa (o nome dela é zeroCross()), ativando-a no modo RISING. Simplificando, isso significa que a cada vez que o estado do pino 2 mudar para HIGH (indicando que o ciclo AC passou pelo valor 0), o Arduino vai parar o que estiver fazendo, executar a minha função zeroCross() e em seguida retornar ao que estava fazendo. O que a minha função zeroCross() faz é ativar o Timer1 interno do ATmega328 em modo comparador, e zerar o contador correspondente (TCNT1).
  • TIMER1_COMPA: Ocorre quando o contador do Timer1 atinge o valor definido no registrador interno OCR1A. Depois de a função zeroCross() ter ativado o Timer1, este inicia a sua contagem automática (configurada para acontecer no modo em que o clock é dividido por 2561). A cada ciclo do Timer1, o ATmega328 vai incrementar o valor do contador TCNT1; caso ele atinja o valor que previamente tiver sido definido (em outra parte do programa) no registrador interno OCR1A, uma interrupção TIMER1_COMPA será gerada, executando automaticamente a rotina do vetor TIMER1_COMPA_vect, que eu defini de modo a mandar HIGH para o pino DIM do nosso módulo, permitindo assim que a lâmpada acenda. Na mesma rotina, aproveitei para mover um número bem alto para o mesmo contador TCNT1, de modo a logo a seguir provocar mais uma interrupção, causada pelo atingimento do valor máximo permitido para o contador TCNT1.
  • TIMER1_OVF: Ocorre quando o contador do Timer1 ultrapassa seu maior valor possível. Quando o contador TCNT1 atinge um valor alto demais 2, o ATmega provoca uma interrupção TIMER1_OVF, executando automaticamente a rotina do vetor TIMER1_OVF_vect. A exemplo do que descrevi no item acima, eu a aproveitei para meus fins, mais especificamente mandando o valor LOW novamente para o pino DIM do nosso módulo (permitindo que a lâmpada apague automaticamente assim que o ciclo AC chegar ao 0 novamente). No mesmo ato, aproveito para desativar o contador do Timer1, que foi originalmente acionado pela zeroCross().

O efeito da sequência de interrupções acima, que não usa nenhum tipo de delay que suspenda a execução do programa, é uma temporização precisa de envio de um pulso para o pino DIM do módulo (comandada para iniciar a cada vez que o pino ZC avisa que o ciclo AC passou pelos 0V, como vimos no gráfico do artigo anterior), que devolve o controle ao loop() entre cada um dos seus eventos.

No programa do artigo anterior, trabalhamos diretamente com unidades de tempo (microssegundos). Para o programa deste artigo, a temporização é baseada em contagem de ciclos do clock e do timer, e os cálculos relacionando a frequência da rede elétrica (os ciclos de 60Hz e os intervalos de 1/120s para atuação da temporização) à velocidade de clock do ATmega328 do Arduino Uno (16MHz) e do seu prescaler são cruciais para definir os tempos do programa. A datasheet tem os detalhes, mas o essencial é compreender que cada meio ciclo AC corresponde a cerca de 520 contagens do Timer1 do Arduino (na forma como ele foi configurado no programa, com uso de prescaler para dividi-lo por 256).

No nosso programa, a seguir, t2 é calculado a partir do valor definido na constante PULSO, e t1 é um valor inversamente proporcional às constantes FRACO e FORTE, que representam, respectivamente, os limites mínimo e máximo de luminosidade a serem gerados pelo programa.

Nota: Para saber mais sobre as interrupções do Arduino Uno, recomendo as seções 7.7 e 12 da datasheet do ATmega328, e este detalhado artigo do Nick Gammon. Além disso, inseri no meu programa abaixo, como comentários, as seções específicas da datasheet que descrevem cada um dos registradores utilizados.

O programa

Existe uma variedade de bibliotecas prontas para o controle de cargas AC com Triacs, algumas melhor testadas, outras nem tanto, e você pode optar livremente entre elas.

Para minha própria prática de conceitos referentes às interrupções do Arduino, optei (neste artigo) por uma implementação em um nível arquitetural mais baixo, baseada em bibliotecas básicas de interrupções do ATmega328 (já incluídas na IDE do Arduino), que reimplementa de forma mais direta alguns recursos que provavelmente poderiam ser alcançados também com o uso de PWM ou outros recursos mais amigáveis disponíveis em outras bibliotecas3. Cada alternativa tem suas vantagens e desvantagens.

A opção de fazer referência a detalhes da arquitetura significa que o programa terá uma profusão de referências a nomes de interrupções e variáveis de controle internas da plataforma AVR, tais como TIMSK1, TCCR1A, TCCR1B, OCR1A e TCNT1. Não me atreveria a tentar explicá-los diretamente, mas acrescentei, em comentários junto a cada uma das menções, o número do item correspondente na datasheet do ATmega328 para você ter uma fonte de consulta à mão.

O resultado você vê a seguir, com explicações logo após.

#include <avr/io.h>
#include <avr/interrupt.h>

#define PINO_ZC 2    // recomendo não mudar este pino
#define PINO_DIM 9   // recomendo não mudar este pino
#define PINO_CHAVE 4

#define PULSO 4    // tempo em que o pino DIM ficara em HIGH, contado em ciclos do Timer1
#define FRACO 60   // intensidade minima desejada para a lampada (0 a ~520) 
#define FORTE 485  // intensidade maxima desejada para a lampada (0 a ~520) 

void setup() {
  pinMode(PINO_ZC, INPUT_PULLUP);
  pinMode(PINO_DIM, OUTPUT);
  pinMode(PINO_CHAVE,INPUT_PULLUP);

  // ativa o Timer/Counter1 com os modos dos bits 0 e 1, ou seja,
  // ativa Comparador A e detector de overflow
  TIMSK1 = 0x03;    // 16.11.8 na datasheet 

  // desabilita inicialmente o timer:
  TCCR1A = 0x00;    // 16.11.1 na datasheet   
  TCCR1B = 0x00;    // 16.11.2 na datasheet 
}  

void defineIntensidade(int k) { 
  // recebe um valor de 0 a 1024 para a intensidade da lampada, e define o valor correspondente
  // (entre as constantes FORTE e FRACO) para o comparador do Timer1
  OCR1A=map(k,0,1024,FORTE,FRACO);  // 16.11.5 na datasheet 
}

void zeroCross() { // chamada automaticamente sempre que o pino 2 mudar para HIGH  
  TCCR1B=0x04; // 16.11.2 - habilita o timer no modo que divide o clock real por 256
  TCNT1=0;     // 16.11.4 - zera o contador do timer
  
  // a partir daqui, esta interrupcao encerra (ate a proxima repeticao do ciclo AC), 
  // mas o ATmega vai começar a incrementar o valor de TCNT1 automaticamente a cada 
  // ciclo do Timer1, provocando uma nova interrupcao (TIMER1_COMPA) quando o contador 
  // alcancar o valor definido em OCR1A.
  // Assim, se OCR1A for um valor alto, vai demorar mais para a interrupcao acontecer, 
  // e vice-versa. Essa demora corresponde ao tempo em que a lampada fica apagada.
}

ISR(TIMER1_COMPA_vect) { // chamada automaticamente sempre que o TCNT1 alcanca OCR1A.
  // manda um valor HIGH para o pino DIM, acendendo a lampada
  digitalWrite(PINO_DIM,HIGH);  
  // inicia um novo contador para o Timer1, calculado para provocar um overflow (e 
  // uma nova interrupcao - TIMER1_OVF) apos o numero de ciclos definido em PULSO.
  TCNT1 = 65536-PULSO;
  
  // a partir daqui, esta interrupcao encerra, e o ATmega comeca a incrementar o 
  // valor de TCNT1 automaticamente a cada ciclo do Timer1, provocando uma nova 
  // interrupcao (OVF) quando o contador entrar em overflow. Como o contador TCNT1
  // tem 16 bits, o overflow acontece quando ele ultrapassa o valor de 65535.
}

ISR(TIMER1_OVF_vect) { // chamada automaticamente sempre que ha overflow de TCNT1.
  // manda um valor LOW para o pino DIM, voltando a apagar a lampada
  digitalWrite(PINO_DIM,LOW);
  // desativa o timer que foi inicialmente ativado pela nossa funcao zeroCross().
  TCCR1B = 0x00;    
  
  // a partir daqui, esta interrupcao encerra. O normal eh que a proxima 
  // interrupcao a acontecer seja a do pino 2 (zeroCross()), no proximo 
  // meio ciclo AC.      
}


void loop() {
  if (digitalRead(PINO_CHAVE)==LOW) {
    // Ativa interrupção 0 para chamar nossa funcao zeroCross() 
    // sempre que o pino 2 mudar para HIGH 
    attachInterrupt(0,zeroCross, RISING);    
    for (int j=FRACO; j<FORTE; j++) {
      defineIntensidade(j);
      delay(40);
    }
    while (digitalRead(PINO_CHAVE)==LOW) {}
    delay(50);
    // desativa a interrupcao 0 e apaga a lampada  
    detachInterrupt(0); 
  }
}

Dica: sugiro fortemente que você leia o artigo anterior e o texto acima do programa (a introdução às interrupções e a breve visão teórica) antes de ler a explicação do programa, a seguir. Sugiro também que a leitura dos comentários que acompanham o próprio código, acima, complemente a interpretação das explicações, abaixo.

O programa começa com a inclusão de 2 bibliotecas de funções básicas, específicas das plataformas AVR (incluindo a linha ATmega). Ambas fazem parte da distribuição da IDE do Arduino, não exigindo instalação específica.

A seguir vêm as definições de parâmetros: os números dos pinos ligados a cada funcionalidade, a duração do pulso no pino DIM (correspondente ao tempo t2, no nosso gráfico acima), e os parâmetros mínimo e máximo que permitirão calcular o tempo t1 do nosso gráfico do artigo anterior, para cada nível de luminosidade da nossa alvorada artificial.

A função setup() começa simples, com as definições dos modos de 3 pinos, mas em seguida já muda para o modo avançado: o registrador interno TIMSK1 do ATmega (16.11.8 na datasheet) recebe o valor correspondente aos 2 modos de timer que usaremos, e os registradores internos TCCR1A e TCCR1B (16.11.1 e .2 na datasheet) são usados para desativar, por enquanto, o timer.

Já a função defineIntensidade(), que será chamada repetidas vezes para ir aumentando gradativamente a iluminação durante o nosso amanhecer artificial, usa a função interna map() para fazer uma operação matemática simples que converte o valor recebido como parâmetro (que pode variar entre 0 e 1023, e indica a luminosidade desejada com mais precisão do que o mecanismo usado no artigo anterior, que só variava entre 0 e 100) em um valor a ser usado como limite do contador, para deflagrar a interrupção TIMER1_COMPA (vista acima). Esse valor, armazenado no registrador interno OCR1A, é o que define o tempo t1, visto no nosso gráfico do artigo anterior.

A função zeroCross() não é chamada diretamente por nenhuma outra parte do programa, apenas pelo gerenciamento de interrupções do microcontrolador.

A função zeroCross() tem uma característica especial: ela não será chamada diretamente pelo nosso programa. Ao invés disso, no início da nossa alvorada artificial, será incluída como parâmetro da função interna attachInterrupt(), de modo a ser chamada sempre que acontecer a interrupção INT0 (vista acima). Simplificadamente, isso significa que ela será chamada pelo ATmega328 a cada vez que o pino ZC do nosso módulo passar para o estado HIGH, indicando que a tensão recebida da tomada passou pelo valor 0V.

A seguir temos 2 casos especiais: funções definidas por meio da macro ISR(), que serve exatamente para que possamos atribuir facilmente uma rotina para reagir a determinadas interrupções do hardware.

A primeira delas, ISR(TIMER1_COMPA_vect), define o que fazer sempre que acontecer uma interrupção TIMER1_COMPA (que já vimos, acima), e que – basicamente – é o que acontece ao final do tempo t1 que vimos no nosso gráfico: o valor HIGH é movido para o pino DIM do módulo (fazendo a lâmpada acender), e o timer é alterado para que logo a seguir aconteça outra interrupção.

Essa outra interrupção é a que definimos como ISR(TIMER1_OVF_vect), e que corresponde à TIMER1_OVF que já vimos. Ela ocorre ao final do tempo t2 do nosso gráfico, e é nela que voltamos a mandar LOW para o pino DIM do módulo, e desativamos o timer (até que a próxima zeroCross() o reative, claro).

A seguir temos nosso tradicional loop(), e ele monitora (da forma tradicional, sem recorrer a interrupções) o estado de uma chave (física) conectado ao pino 4 do Arduino.

A chave normalmente está em HIGH, porque definimos o pino 4 como INPUT_PULLUP. Se ela estiver em LOW indica que o usuário a pressionou, e quer dar início a um amanhecer artificial, ao que responderemos com a ativação da interrupção INT0 (que vai chamar nossa função zeroCross() a cada vez que a tensão da tomada cruzar a linha de 0V), e um prosaico loop for() que vai chamar, ao longo de cerca de 1 minuto (que você pode aumentar ou diminuir mudando o delay(40) do código), repetidas vezes a função defineIntensidade() para ir aumentando, aos poucos, a luminosidade, depois deixando-a fixa ao atingir o nível máximo, até que a chave seja desligada.

O loop for ignora completamente o controle do módulo: ele define o parâmetro t1, e deixa as interrupções cuidarem do resto.

Observe que esse loop for não faz chamadas diretas à zeroCross() ou às demais interrupções. Elas serão chamadas (120 vezes por segundo) pelo ATmega328, automaticamente. Tudo que o loop precisa fazer (no caso, por meio da chamada à defineIntensidade()) é providenciar para que o valor do registrador interno OCR1A vá sendo atualizado aos poucos para garantir tempos t1 (do nosso gráfico) cada vez menores, o que produzirá luminosidade cada vez maior, e a lâmpada irá gradativamente iluminar cada vez mais intensamente.

Se você substituir a chave por um módulo RTC (relógio de tempo real) externo, como o que vimos no artigo “Data, hora e memória extra no Arduino com o módulo Tiny RTC e o chip DS1307”, pode até planejar um despertador com amanhecer artificial.

Mas não perca o sono implementando-o! ;-)

Patrocínio: Usinainfo

Quero agradecer à UsinaInfo, que vem patrocinando alguns experimentos do BR-Arduino, inclusive este com o Módulo Dimmer para Arduino.

Além de receber material da empresa para os experimentos como parte do acordo de patrocínio, eu já fiz compras de componentes lá, aproveitando a variedade, o estoque bem suprido, as descrições detalhadas e a qualidade do seu sistema de comércio eletrônico. Recomendo sem ressalvas.

Agradeço também pela confiança que ficou expressa nos termos do patrocínio: a empresa me enviou os componentes necessários ao experimento combinado, mas não fez qualquer exigência sobre a forma e nem buscou exercer controle sobre o que eu fosse escrever. Obrigado, UsinaInfo!

 
  1.  Devido a uma opção definida no registrador interno TCCR1B, como parte da função zeroCross().

  2.  Mais especificamente, quando ele ultrapassa 65535, que é o número máximo que pode ser representado em seus 16 bits.

  3.  Ou trocando as 2 interrupções do Timer1 por uma mera sequência de digitalWrites e delayMicroseconds, como fizemos no programa do artigo anterior

Comentar

Dos leds ao Arduino, ESP8266 e mais

Aprenda eletrônica com as experiências de um geek veterano dos bits e bytes que nunca tinha soldado um led na vida, e resolveu narrar para você o que descobre enquanto explora esse universo – a partir da eletrônica básica, até chegar aos circuitos modernos.

Por Augusto Campos, autor do BR-Linux e Efetividade.net.

Recomendados

Livro recomendado


Artigos já disponíveis

Comunidade Arduino

O BR-Arduino é integrante da comunidade internacional de entusiastas do Arduino, mas não tem relação com os criadores e distribuidores do produto, nem com os detentores das marcas registradas.

Livros recomendados