Índice:

AVRSH: um interpretador de comandos para Arduino / AVR .: 6 etapas (com imagens)
AVRSH: um interpretador de comandos para Arduino / AVR .: 6 etapas (com imagens)

Vídeo: AVRSH: um interpretador de comandos para Arduino / AVR .: 6 etapas (com imagens)

Vídeo: AVRSH: um interpretador de comandos para Arduino / AVR .: 6 etapas (com imagens)
Vídeo: Начало работы с игровой консолью Uzebox (часть 1) 2024, Julho
Anonim
AVRSH: um interpretador de comandos Shell para Arduino / AVR
AVRSH: um interpretador de comandos Shell para Arduino / AVR

Sempre quis estar "conectado" ao seu microcontrolador AVR? Já pensou que seria legal "catar" um registro para ver seu conteúdo? Você sempre quis uma maneira de ligar e desligar subsistemas periféricos individuais de seu AVR ou Arduino em * tempo real *? Eu também, então escrevi o AVR Shell, um shell parecido com o UNIX. É como o UNIX porque é uma reminiscência da conta shell que você comprou para executar seus bots de colisão de nick do irc, além de ter um ou dois comandos em comum. Ele também tem um sistema de arquivos que se assemelha ao extfs UNIX, usando um EEPROM externo, mas isso se tornou um projeto por si só, então vou lançar esse módulo separadamente sob um instrutor diferente quando estiver pronto para produção. Aqui está uma lista das coisas que você pode fazer atualmente com o AVR Shell:

  • Leia todos os seus registros de direção de dados (DDRn), portas e pinos em tempo real
  • Grave em todos os seus DDRns, portas e pinos para ligar motores, LEDs ou ler sensores em tempo real
  • Liste todos os registros conhecidos no sistema
  • Crie e armazene valores em variáveis definidas pelo usuário com backup da EEPROM.
  • Crie uma senha root e autentique-se nela (usada para acesso telnet)
  • Leia a velocidade do clock da CPU configurada
  • Mude a velocidade do clock da CPU configurando um prescaler
  • Iniciar e parar temporizadores de 16 bits para cronometrar várias coisas
  • Ligue e / ou desligue os subsistemas periféricos: Conversores Analógico para Digital (ADC), Interface Periférica Serial (SPI), Interface de Dois Fios (TWI / I2C), UART / USART. Útil quando você deseja reduzir o consumo de energia do microcontrolador ou habilitar certas funções.
  • Escrito em C ++ com objetos reutilizáveis.

Este instrutível irá percorrer a instalação, uso e personalização do avrsh.

Etapa 1: O que você precisa

O que você precisará
O que você precisará

Este instrutível não exige muito, exceto que você:

  • Tenha um Arduino ou ATmega328P. Outros AVRs podem funcionar, mas você pode precisar modificar o código para listar quaisquer registros que sejam exclusivos do seu MCU. Os nomes só precisam corresponder ao que está listado no arquivo de cabeçalho exclusivo para seu MCU. Muitos dos nomes de registro são iguais entre os AVRs, portanto, sua milhagem pode variar durante a transferência.
  • Tenha uma maneira de se conectar ao USART serial do seu Arduino / AVR. O sistema foi testado amplamente com o Terminal AVR, um aplicativo do Windows que faz uma conexão serial através de sua porta USB ou COM. Funciona com Arduinos usando a conexão USB e qualquer AVR usando o USB-BUB de Moderndevice.com. Outras opções de terminal incluem: Putty, minicom (Linux e FreeBSD), tela (Linux / FreeBSD), Hyperterminal, Teraterm. Descobri que o putty e o teraterm enviam algum lixo ao se conectar, então seu primeiro comando pode ser truncado.
  • Tenha o firmware AVR Shell instalado e funcionando, que você pode baixar dessas páginas, ou sempre obtenha a versão mais recente em BattleDroids.net.

Para instalar o Terminal AVR, basta descompactá-lo e executá-lo. Para instalar o firmware AVR Shell, baixe-o e carregue diretamente o arquivo hex e conecte seu terminal serial a 9600 baud, ou compile-o com "make" e depois "make program" para fazer upload do hex. Observe que pode ser necessário alterar as configurações do AVRDUDE para refletir sua porta COM. Nota: O atributo PROGMEM está quebrado na implementação AVR GCC atual para C ++ e este é um bug conhecido. Se você compilá-lo, espere obter muitas mensagens de aviso dizendo "aviso: apenas variáveis inicializadas podem ser colocadas na área de memória do programa." Além de ser irritante de ver, esse aviso é inofensivo. Como o C ++ na plataforma embarcada não está no topo da lista de prioridades do AVR GCC, não se sabe quando isso será corrigido. Se você verificar o código, verá onde fiz soluções para reduzir esse aviso, implementando minhas próprias instruções de atributo. Muito simples. Baixe e instale tudo o que você precisa para virar a página e começar a crackin '.

Etapa 2: Leitura e gravação de registros

Leitura e escrita de registros
Leitura e escrita de registros

O AVR Shell foi escrito principalmente para acessar alguns sensores que eu conectei ao meu AVR. Tudo começou com um simples LED, depois mudou para sensores de luz, sensores de temperatura e, finalmente, para dois transdutores ultrassônicos. O avrsh pode definir os componentes digitais desses sensores gravando nos registradores que os controlam. Manipulando registros AVR durante a execução Para obter uma lista de todos os registros conhecidos em seu Arduino, digite:

imprimir registros e você obterá uma impressão parecida com esta

Eu sei sobre os seguintes registros:

TIFR0 PORTC TIFR1 PORTD TIFR2 DDRD PCIFR DDRB EIFR DDRC EIMSK PINB EECR PINC EEDR PIND SREG EEARL GPIOR0 EEARH GPIOR1 GTCCR GPIOR2 TCCR0A TCCR0B TCNT0 OCR0A OCR0B SPCR SPDR ACSR SMCR MCUSR MCUCR SPMCSR WDTCSR CLKPR PRR OSCCAL PCICR Eicra PCMSK0 PCMSK1 TIMSK0 TIMSK1 TIMSK2 LCAD ADCH ADCSRA ADCSRB ADMUX DIDR0 DIDR1 TCCR1A TCCR1B TCCR1C TCNT1L TCNT1H ICR1L ICR1H OCR1AL OCR1AH OCR1BL OCR1BH TCCR2A TCCR2B TCNT2 OCR2A OCR2B ASSR TWBR TWSR TWAR TWDR TWCR TWAMR UCSR0A UCSR0B UCSR0C UBRR0L UBRR0H UDR0 PORTB root @ ATmega328P> Para ver como os bits individuais são definidos em qualquer registro, use o comando cat ou echo

cat% GPIOR0 Aqui, estou pedindo ao interpretador de comandos para exibir, ou ecoar, o conteúdo do Registro de E / S de finalidade geral # 0. Observe o sinal de porcentagem (%) na frente do nome do registro. Você precisa disso para indicar ao shell que esta é uma palavra-chave reservada que identifica um registro. A saída típica de um comando echo se parece com isto

GPIOR0 (0x0) definido para [00000000] A saída mostra o nome do registro, o valor hexadecimal encontrado no registro e a representação binária do registro (mostrando cada bit como 1 ou 0). Para definir um determinado bit em qualquer registro, use o operador "índice de" . Por exemplo, digamos que eu queira que o terceiro bit seja 1

% GPIOR0 [3] = 1 e o shell lhe dará uma resposta indicando sua ação e o resultado

GPIOR0 (0x0) definido como [00000000] (0x8) definido como [00001000] Não se esqueça do sinal de porcentagem para informar ao shell que você está trabalhando com um registrador. Observe também que definindo o terceiro bit, são 4 bits porque nossos AVRs usam um índice baseado em zero. Em outras palavras, contando até o 3º bit você conta 0, 1, 2, 3, que é a 4ª casa, mas o 3º bit. Você pode limpar um bit da mesma maneira, definindo um bit como zero. Ao definir bits como este, você pode alterar o funcionamento do seu AVR em tempo real. Por exemplo, alterando o valor de correspondência do temporizador CTC encontrado em OCR1A. Ele também permite que você dê uma olhada em configurações específicas que você teria que verificar programaticamente em seu código, como o valor UBBR para sua taxa de transmissão. Trabalhando com DDRn, PORTn e PINn Os pinos de I / O também são atribuídos a registradores e podem ser configurados exatamente da mesma maneira, mas uma sintaxe especial foi criada para trabalhar com esses tipos de registradores. No código, existe um processo normal para, digamos, ligar um LED ou outro dispositivo que requer um alto ou baixo digital. É necessário configurar o Registrador de Direção de Dados para indicar que o pino é para saída e, em seguida, escrever 1 ou 0 no bit específico na porta correta. Supondo que temos um LED conectado ao pino digital 13 (PB5) e queremos ligá-lo, veja como fazer isso enquanto seu AVR está funcionando

definir pino pb5 outputwrite pino pb5 alto A saída, além de poder ver o seu LED acender, ficaria assim

root @ ATmega328p> definir pino pb5 outputSet pb5 para outputroot @ ATmega328p> escrever pino pb5 highWrote lógico alto para pino pb5 O "root @ ATmega328p>" é o prompt do shell que indica que ele está pronto para aceitar comandos de você. Para desligar o LED, você simplesmente escreveria um baixo no pino. Se você quiser ler a entrada digital de um pino, use o comando read. Usando nosso exemplo acima

root @ ATmega328p> ler pino pb5Pin: pb5 é ALTO Como alternativa, apenas ecoe o registrador de pino que controla essa porta de pino. Por exemplo, se tivermos interruptores DIP conectados aos pinos digitais 7 e 8 (PD7 e PD8), você pode enviar o comando

echo% PIND e o shell então exibiria o conteúdo desse registro, mostrando a você todos os estados de entrada / saída dos dispositivos conectados e se o estado da chave estava ligado ou desligado.

Etapa 3: Fusíveis de leitura e gravação

Fusíveis de leitura e escrita
Fusíveis de leitura e escrita

Os fusíveis são tipos especiais de registros. Eles controlam tudo, desde a velocidade do clock do seu microcontrolador até os métodos de programação disponíveis para proteção contra gravação de EEPROM. Às vezes, você precisará alterar essas configurações, especialmente se estiver criando um sistema AVR autônomo. Não tenho certeza se você deve alterar suas configurações de fusível no Arduino. Tenha cuidado com seus fusíveis; você pode bloquear a si mesmo se configurá-los incorretamente. Em um instrutível anterior, demonstrei como você pode ler e configurar seus fusíveis usando seu programador e avrdude. Aqui, vou mostrar como ler seus fusíveis em tempo de execução para ver como seu MCU realmente os configurou. Observe que esta não é a configuração de tempo de compilação que você obtém das definições, mas os fusíveis reais conforme o MCU os lê em tempo de execução. Da Tabela 27-9 na folha de dados ATmega328P (databook, mais parecido), os bits do Fuse Low Byte são os seguintes:

CKDIV8 CKOUT SUT1 SUT0 CKSEL3 CKSEL2 CKSEL1 CKSEL0Uma coisa interessante a notar é que, com fusíveis, 0 significa programado e 1 significa que esse bit em particular não está programado. Um tanto contra-intuitivo, mas uma vez que você sabe, você sabe disso.

  • CKDIV8 define o clock da CPU para ser dividido por 8. O ATmega328P vem programado de fábrica para usar seu oscilador interno a 8 MHz com CKDIV8 programado (ou seja, definido como 0), dando a você um F_CPU final ou frequência de CPU de 1 MHz. No Arduino, isso é alterado, pois eles são configurados para usar um oscilador externo a 16 MHz.
  • O CKOUT quando programado produzirá o clock da CPU no PB0, que é o pino digital 8 no Arduinos.
  • SUT [1..0] especifica o tempo de inicialização do seu AVR.
  • CKSEL [3..0] define a fonte do relógio, como o oscilador RC interno, oscilador externo, etc.

Quando você ler seus fusíveis, ele será devolvido a você em hexadecimal. Este é o formato de que você precisa se quiser gravar os fusíveis via avrdude. No meu arduino, eis o que obtenho quando leio o byte de fusível inferior:

root @ ATmega328p> leia lfuseLower Fuse: 0xffPortanto, todos os bits são definidos como 1. Fiz o mesmo procedimento em um clone do Arduino e obtive o mesmo valor. Verificando um de meus sistemas AVR autônomos, obtive 0xDA, que é o valor que defini há algum tempo ao configurar o chip. O mesmo procedimento é usado para verificar os fusíveis High Fuse Byte, Extended Fuse Byte e Lock. Os bytes do fusível de calibração e assinatura foram desabilitados no código com uma diretiva de pré-processador #if 0, que você pode alterar se se sentir desconfortável.

Etapa 4: Outros comandos

Outros Comandos
Outros Comandos

Existem vários outros comandos que o interpretador de comandos padrão entende que podem ser úteis. Você pode ver todos os comandos implementados e lançados no futuro, emitindo ajuda ou menu no prompt. Vou rapidamente cobri-los aqui, pois são em sua maioria autoexplicativos. Configurações de frequência do relógio da CPU Você pode descobrir o que seu firmware foi configurado para usar como as configurações de relógio da CPU com o comando fcpu:

root @ ATmega328p> fcpuCPU Freq: 16000000Isso é 16 milhões, ou 16 milhões de herz, mais comumente conhecido como 16 MHz. Você pode alterar isso na hora, por qualquer motivo, com o comando clock. Este comando tem um argumento: o prescaler a ser usado ao dividir a velocidade do clock. O comando clock entende estes valores do prescaler:

  • ckdiv2
  • ckdiv4
  • ckdiv8
  • ckdiv16
  • ckdiv32
  • ckdiv64
  • ckdiv128
  • ckdiv256

Usando o comando:

relógio ckdiv2 quando a velocidade da cpu é 16 MHz, isso resultaria na mudança da velocidade do clock para 8 MHz. Usar um prescaler de ckdiv64 com uma velocidade de clock inicial de 16 MHz resultará em uma velocidade de clock final de 250 KHz. Por que diabos você gostaria de tornar sua MCU mais lenta? Bem, por um lado, uma velocidade de clock menor consome menos energia e se você tem seu MCU funcionando com uma bateria em um gabinete de projeto, você pode não precisar que ele funcione em velocidade máxima e, portanto, pode diminuir a velocidade e reduzir o consumo de energia, aumentando a vida útil da bateria. Além disso, se você estiver usando o relógio para qualquer tipo de problema de tempo com outro MCU, digamos, implementando um software UART ou algo assim, você pode querer defini-lo para um valor particular que seja fácil de obter uma boa taxa de transmissão uniforme com taxas de erro mais baixas. Ligando e desligando subsistemas periféricos Na mesma nota da redução do consumo de energia mencionada anteriormente, você pode querer reduzir ainda mais a energia desligando alguns dos periféricos integrados que não está usando. O interpretador de comandos e o shell podem atualmente ligar e desligar os seguintes periféricos:

  • Conversor analógico-digital (ADC). Este periférico é usado quando você tem um sensor analógico fornecendo dados (como temperatura, luz, aceleração, etc.) e precisa convertê-los para um valor digital.
  • Interface Periférica Serial (SPI). O barramento SPI é usado para se comunicar com outros dispositivos habilitados para SPI, como memórias externas, drivers de LED, ADC's externos, etc. Partes do SPI são usadas para programação de ISP, ou pelo menos os pinos são, então tome cuidado ao desligá-lo se você estiver programando via ISP.
  • Interface de dois fios. Alguns dispositivos externos usam o barramento I2C para se comunicar, embora estes estejam sendo rapidamente substituídos por dispositivos habilitados para SPI, já que o SPI tem uma maior taxa de transferência.
  • USART. Esta é a sua interface serial. Você provavelmente não deseja desligar isso se estiver conectado ao AVR através da conexão serial! No entanto, adicionei isso aqui como um esqueleto para transferência para dispositivos que possuem vários USARTs, como o ATmega162 ou ATmega644P.
  • tudo. Este argumento para o comando powerup ou powerdown liga todos os periféricos mencionados ou desliga todos com um comando. Novamente, use este comando com sabedoria.

root @ ATmega328p> powerdown twiPowerdown de twi complete.root@ATmega328p> powerup twiPowerup de twi completo.

Iniciando e parando temporizadores O shell possui um temporizador de 16 bits integrado que está disponível para uso. Você inicia o cronômetro com o comando cronômetro:

início do cronômetroe pare o cronômetro com o argumento de parada

parada do cronômetroEste temporizador não entrará em conflito com o temporizador USART interno. Consulte o código para obter os detalhes de implementação do temporizador USART, se esse tipo de detalhe sangrento lhe interessar

root @ ATmega328p> timer startStarted timer.root@ATmega328p> timer stopTempo decorrido: ~ 157 segundos Autenticação O shell pode armazenar uma senha de 8 caracteres na EEPROM. Esse mecanismo de senha foi criado para oferecer suporte aos recursos de login do telnet, mas pode ser expandido para proteger outras coisas. Por exemplo, você pode exigir certos comandos, como alterar valores de registro, por meio do mecanismo de autenticação. Defina a senha com o comando de senha

root @ ATmega328p> passwd blahWrote senha de root para EEPROMAutorize contra a senha (ou solicite autorização programaticamente através do código) com o comando auth. Observe que se você tentar alterar a senha de root e já houver uma senha de root definida, você deve se autorizar contra a senha antiga antes de poder alterá-la para uma nova senha

root @ ATmega328p> passwd blinkyVocê deve se autorizar primeiro.root@ATmega328p> auth blahAuthorized.root@ATmega328p> passwd blinkyWrote NOVA senha de root para EEPROMObviamente, você precisará carregar o arquivo avrsh.eep se apagar o firmware para ter seus valores e variáveis antigos restaurados. O Makefile criará o arquivo EEPROM para você. Variáveis O shell entende a noção de variáveis definidas pelo usuário. O código limita isso a 20, mas você pode mudar isso se quiser, alterando a definição MAX_VARIABLES em script.h. Você pode salvar qualquer valor de 16 bits (ou seja, qualquer número até 65, 536) em uma variável para ser recuperada posteriormente. A sintaxe é semelhante a registradores, exceto que um cifrão ($) é usado para denotar variáveis para o shell. Liste todas as suas variáveis com o comando imprimir variáveis

variáveis de impressão Variáveis definidas pelo usuário: Nome do índice -> Valor (01): $ FREE $ -> 0 (02): $ FREE $ -> 0 (03): $ FREE $ -> 0 (04): $ FREE $ -> 0 (05): $ GRÁTIS $ -> 0 (06): $ GRÁTIS $ -> 0 (07): $ GRÁTIS $ -> 0 (08): $ GRÁTIS $ -> 0 (09): $ GRÁTIS $ -> 0 (10): $ GRÁTIS $ -> 0 (11): $ GRÁTIS $ -> 0 (12): $ GRÁTIS $ -> 0 (13): $ GRÁTIS $ -> 0 (14): $ GRÁTIS $ -> 0 (15): $ GRÁTIS $ -> 0 (16): $ GRÁTIS $ -> 0 (17): $ GRÁTIS $ -> 0 (18): $ GRÁTIS $ -> 0 (19): $ GRÁTIS $ -> 0 (20): $ GRÁTIS $ -> 0Completo. Defina uma variável

$ newvar = 25 $ tempo limite = 23245Obtenha o valor de uma determinada variável

root @ ATmega328p> echo $ newvar $ newvar 25Você pode ver quais variáveis você já instanciou com o comando de impressão que você já conhece

Variáveis definidas pelo usuário: Nome do índice -> Valor (01): novavar -> 25 (02): tempo limite -> 23245 (03): $ FREE $ -> 0 (04): $ FREE $ -> 0 (05): $ GRÁTIS $ -> 0 (06): $ GRÁTIS $ -> 0 (07): $ GRÁTIS $ -> 0 (08): $ GRÁTIS $ -> 0 (09): $ GRÁTIS $ -> 0 (10): $ GRÁTIS $ -> 0 (11): $ GRÁTIS $ -> 0 (12): $ GRÁTIS $ -> 0 (13): $ GRÁTIS $ -> 0 (14): $ GRÁTIS $ -> 0 (15): $ GRÁTIS $ -> 0 (16): $ GRÁTIS $ -> 0 (17): $ GRÁTIS $ -> 0 (18): $ GRÁTIS $ -> 0 (19): $ GRÁTIS $ -> 0 (20): $ GRÁTIS $ -> 0Completo. O nome $ FREE $ apenas indica que a localização da variável está livre e ainda não foi atribuída um nome de variável.

Etapa 5: Personalizando o Shell

Personalizando o Shell
Personalizando o Shell

Você está livre para hackear o código e personalizá-lo de acordo com suas necessidades, se desejar. Se eu soubesse que lançaria esse código, teria feito uma classe de interpretador de comando e uma estrutura de comando separadas e simplesmente iterado por meio disso chamando um ponteiro de função. Isso reduziria a quantidade de código, mas do jeito que está, o shell analisa a linha de comando e chama o método de shell apropriado. Para adicionar seus próprios comandos personalizados, faça o seguinte: 1. Adicione seu comando à lista de análise O analisador de comandos irá analise a linha de comando e forneça o comando e quaisquer argumentos separadamente. Os argumentos são passados como ponteiros para ponteiros, ou uma matriz de ponteiros, da maneira que você quiser trabalhar com eles. Isso é encontrado em shell.cpp. Abra shell.cpp e encontre o método ExecCmd da classe AVRShell. Você pode desejar adicionar o comando à memória do programa. Se o fizer, adicione o comando em progmem.he progmem.cpp. Você pode adicionar o comando para programar a memória diretamente usando a macro PSTR (), mas irá gerar outro aviso do tipo mencionado anteriormente. Novamente, este é um bug conhecido no C ++, mas você pode contornar isso adicionando o comando diretamente nos arquivos progmem. *, Como eu fiz. Se você não se importa em adicionar ao seu uso de SRAM, você pode adicionar o comando conforme ilustrei com o comando "clock". Digamos que você queira adicionar um novo comando chamado "newcmd". Vá para AVRShell:: ExecCmd e encontre um local conveniente para inserir o seguinte código:

else if (! strcmp (c, "newcmd")) cmdNewCmd (args);Isso adicionará seu comando e chamará o método cmdNewCmd que você escreverá na próxima etapa. 2. Escreva seu código de comando personalizado No mesmo arquivo, adicione seu código de comando personalizado. Esta é a definição do método. Você ainda desejará adicionar a declaração ao shell.h. Basta anexá-lo aos outros comandos. No exemplo anterior, o código pode ser parecido com este

voidAVRShell:: cmdNewCmd (char ** args) {sprintf_P (buff, PSTR ("Seu comando é% s / r / n", args [0]); WriteRAM (buff);}Existem várias coisas aqui. Primeiro, "buff" é um buffer de matriz de 40 caracteres fornecido no código para seu uso. Usamos a versão de memória de programa do sprintf, pois estamos passando um PSTR. Você pode usar a versão normal se desejar, mas certifique-se de não passar o formato em um PSTR. Além disso, os argumentos estão na matriz args. Se você digitou "newcmd arg1 arg2", pode obter esses argumentos com os subscritos args [0] e args [1]. Você pode passar no máximo MAX_ARGS argumentos, conforme definido no código. Sinta-se à vontade para alterar esse valor ao recompilar se precisar que muitos outros argumentos sejam passados de uma vez. WriteLine e WriteRAM são funções globais que retornam os métodos do UART de mesmo nome. O segundo argumento para esta função está implícito. Se você não passar nada, um prompt de comando será escrito depois. Se você passar um 0 como o segundo argumento, um prompt não será escrito. Isso é útil quando você deseja gravar várias strings separadas para a saída antes que o prompt de comando seja retornado ao usuário. 3. Faça com que o shell execute o código do comando Você já disse ao executor do shell para executar o método cmdNewCmd ao configurar o novo comando, mas adicione-o ao arquivo shell.h para que seja compreendido pelo objeto do shell. Basta adicioná-lo abaixo do último comando ou na frente do primeiro comando, ou em qualquer lugar lá. E é isso. Recompile e carregue o firmware em seu Arduino e seu novo comando estará disponível no shell no prompt.

Etapa 6: Resumo

Você deve saber como instalar e conectar ao seu AVR / Arduino e obter um prompt ao vivo no seu microcontrolador em execução. Você conhece vários comandos que extrairão dados de tempo de execução do MCU ou definirão valores para o MCU em tempo real. Você também aprendeu como adicionar seu próprio código personalizado para criar seus próprios comandos exclusivos no shell para personalizá-lo ainda mais de acordo com suas necessidades. Você pode até mesmo destruir o interpretador de comandos para que ele contenha apenas seus comandos personalizados, se isso atender às suas necessidades. Espero que tenha gostado deste instrutível e que o AVR Shell possa ser útil para você, seja como um interpretador de comandos em tempo real ou como um processo de aprendizagem na implementação do seu próprio. Como sempre, estou ansioso para quaisquer comentários ou sugestões sobre como este instrutível pode ser melhorado! Divirta-se com o seu AVR!

Recomendado: