ÍNDICE DE CONTEÚDO
- Preparando o Eclipse para microcontroladores AVR
- Lendo e Escrevendo nos pinos do Arduino com linguagem C/C++
Este artigo faz parte de uma série que tem como objetivo abordar de maneira prática a programação em C/C++ para microcontroladores AVR. Aqui utilizarei um Arduino UNO dotado de um ATMEGA328P-PU e um Shield EDU-IFSP para realizar certas atividades, onde apresentarei diversos periféricos do microcontrolador. Dessa vez veremos características da programação C/C++ voltada para microcontroladores e começaremos a trabalhar com os pinos e configurá-los.
O ATmega328/P
Os microcontroladores AVR foram desenvolvidos em 1995 na Noruega pela Atmel, que atualmente pertence a Microchip (fabricante dos microcontroladores PIC). Com uma estrutura RISC avançada e com hardware voltado para programação C, os microcontroladores AVR permitem uma melhor performance ao executar um código desse tipo.
Quando programamos um microcontrolador é importante conhecê-lo internamente. A seguir, nas Tabelas 1 e 2, temos algumas informações do ATMEGA328/P, porém é importante estudar seu datasheet pois as informações deste artigo encontram-se de forma resumidas.
Tabela 1 – Características básicas do MCU Atmega328P.
Características básicas | |
Tensão de operação: |
1,8 V a 5,5 V (CLOCK) |
Arquitetura: |
RISC avançada (8 bits) |
CLOCK externo: |
Até 20 MHz |
CLOCK interno: |
Até 8 MHz |
Memória de programa (FLASH): |
32 KBytes (16384 x 16) |
Memória EEPROM: |
1 KBytes (1024 x 8) |
Memória de de dados ou dinâmica (SRAM): |
2 KBytes (2048 x 8) |
Registradores de trabalho de uso geral: |
32 Bytes (32 x 8) |
Pinos programáveis: |
23 |
ADC: |
6 dos 23 |
(TIMER) PWM: |
Tabela 2 – Características básicas de memória Flash e SRAM.
Características básicas dos pinos do ATMEGA328P
Para trabalhar com os pinos do microcontrolador vamos utilizar grupos de registradores que são responsáveis pela configuração do modo que os pinos operam. Cada pino pode assumir três modos: a entrada, saída e entrada com pull-up como mostram as Tabelas 3 e 4. Cada grupo desses registradores refere-se a um port do controlador onde temos acesso a determinados pinos (Figura 1), sendo que cada pino corresponde diretamente a um bit desse PORT, para identificá-los utilizamos as letra, B, C e D (a nomenclatura pode mudar dependendo do modelo do microcontrolador: os registradores DDR determinam quem será entrada ou saída; os registradores PORT escrevem os valores lógicos HIGH ou LOW; para ler os valores de um port, utilizamos os registradores PIN.
Tabela 3 – Registrador de leitura e escrita.
Registradores de Controle e Configuração dos Terminais | |||||||
PINOS ARDUINO UNO |
BIT RELACIONADO |
FUNÇÃO ESPECIAL | |||||
BIT: |
Registrador: DDRB PORTB PINB | ||||||
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
OSC1 |
OSC2 |
13 |
12 |
11 |
10 |
9 |
8 |
PB7 |
PB6 |
PB5 |
PB4 |
PB3 |
PB2 |
PB1 |
PB0 |
Registrador: DDRC PORTC PINC | |||||||
PC6 |
PC5 |
PC4 |
PC3 |
PC2 |
PC1 |
PC0 | |
RESET |
A5 |
A4 |
A3 |
A2 |
A1 |
A0 | |
Registrador: DDRD PORTD PIND | |||||||
PD7 |
PD6 |
PD5 |
PD4 |
PD3 |
PD2 |
PD1 |
PD0 |
7 |
6 |
5 |
4 |
3 |
2 |
TXD |
RXD |
R/W |
R/W |
R/W |
R/W |
R/W |
R/W |
R/W |
R/W |
Tabela 4 – Estados possíveis dos pinos de entrada e saída.
Modos de entrada e saída (INPUT, OUTPUT) | |||
DDRxn |
PORTxn |
PULL UP |
Comentários |
0 |
0 |
Não |
INPUT sem PULL-UP (flutuante) |
0 |
1 |
Sim |
INPUT mornamente em estado HIGH (PULL-UP) |
1 |
0 |
Não |
OUTPUT em estado LOW |
1 |
1 |
Não |
OUTPUT em estado HIGH |
Todos os pinos possuem diodos de proteção para VCC e Terra e resistor de pull-up (Rpu, min 20 kΩ max 50 kΩ) individualmente selecionáveis conforme indicado na figura 2. Em relação às características elétricas, cada pino pode suportar uma corrente máxima de 40 mA, sendo que cada PORT pode suportar 200 mA. Mas de acordo com o fabricante, embora cada porta possa fornecer mais que as condições de teste (20 mA para 5 V e 10 mA para 3 V), deve ser observado que a soma de alguns pinos não deve exceder 100 mA.
- C0 – C5, D0- D4, ADC7, RESET;
- B0 – B5, D5 – D7, ADC6, XTAL1, XTAL2;
- C0 – C5, ADC7, ADC6;
- B0 – B5, D5 – D7, XTAL1, XTAL2;
- D0 – D4, RESET.
Tratando-se de um microcontrolador com uma a corrente máxima de operação baixa, e temos que garantir que ela não exceda a capacidade do micro. Para isso utilizamos alguma forma de acoplamento nos conformes do projeto. Eu particularmente uso MOSFET, driver de corrente (como o ULM2003) e, quando necessário, foto-acoplamento como PC817 (ou similares).
Shield EDU-IFSP
O Shield EDU-IFSP é um projeto criado pelo grupo de Estudos em Robótica e Sistemas Embarcados (GERSE) com o objetivo de facilitar o processo de ensino de eletrônica e programação. No decorrer da série iremos utilizá-lo para realizar experiências e prototipar alguns projetos. A imagem a seguir mostra a relação de pinos do microcontrolador com o shield.
Programando Microcontroladores
Quando programamos em linguagem C, assim como na linguagem utilizada no Arduino, temos que pré configurar algumas coisas, como o modo de operação de um terminal ou até mesmo a velocidade de comunicação serial. Isso geralmente ocorre na rotina void setup(), antes da rotina principal void loop(). Já na linguagem C isso ocorre na mesma rotina, mas antes de um loop infinito. A seguir, lendo e escrevendo os pinos, temos a estrutura básica do programa C/C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#define F_CPU 16000000 #include <avr/io.h> #include <util/delay.h> int main( void ) { // void setup(){} // configurações while(1) // void loop(){} { // programa } return 0; } |
O primeiro programa que vamos fazer é blink_C, onde faremos o led do pino 13 piscar. Conforme as informações de hardware mostradas anteriormente, podemos ver que o pino 13 corresponde ao bit PB5 do PORTB e ao LED A do shield. Cole o código a seguir no arquivo criado no artigo anterior (main.cpp), compile e grave conforme mostrado mostra a Figura 4:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(){ // BIT 76543210 DDRB = 0b00100000; //Pino 13 como saída e resto como entrada. PORTB = 0b00000000; //Todos os pinos em nível baixo (LOW) while( true ){ // 0b76543210 PORTB = 0b00100000; _delay_ms( 1000 ); PORTB = 0b00000000; _delay_ms( 1000 ); } return 0; } |
Uma boa prática na programação é transformar funções pequenas em macros. Elas facilitam a leitura do programa e aumentam o reuso do código. Toda vez que vamos configurar ou escrever em um bit de um registrador, temos que usar operadores lógicos e deslocar bits. Esse é um bom exemplo em que uma macro pode ser aplicada. O próximo programa implementa macros para acessarmos os registradores do micro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> #define SetBit(RES, BIT)(RES |= (1 << BIT)) // Por BIT em nível alto #define ClrBit(RES, BIT)(RES &= ~ (1 << BIT)) // Por BIT em nível baixo #define TstBit(RES, BIT)(RES & (1 << BIT)) // testar BIT, retorna 0 ou 1 #define CplBit(RES, BIT)(RES ^= (1 << BIT)) // Inverter estado do BIT int main(){ // BIT 76543210 SetBit(DDRB, PB5); //Pino 13 como saída. while( true ){ ClrBit(PORTB, PB5); // bit PB5 LOM _delay_ms( 1000 ); SetBit(PORTB, PB5); // bit PB5 HIGH _delay_ms( 1000 ); } return 0; } |
Agora vamos contar de 1 a 3 no display de 7 segmentos. Para alimentar o LCD mude o jumper JP1 de posição.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> #define MY_DELAY 1000 int main(){ // BIT 76543210 DDRB = 0B00111000; // configurar “saídas” DDRD = 0B00111100; PORTB = 0B00000000; // por todos os pinos em LOW PORTD = 0B00000000; while( true ){ // BIT 76543210 PORTB = 0B00011000; // 1 PORTD = 0B00000000; _delay_ms( MY_DELAY ); PORTB = 0B00110000; // 2 PORTD = 0B00110100; _delay_ms( MY_DELAY ); PORTB = 0B00111000; // 3 PORTD = 0B00100100; _delay_ms( MY_DELAY ); } return 0; } |
Note que existem casos que é mais prático para o programador usar a macro ou apenas os registradores. No entanto, dependendo da aplicação, ambos os casos irão dar trabalho. Outra coisa importante é todas as vezes que vamos escrever em um pino precisamos de duas informações: seu registrador e o bit correspondente. Sempre que duas variáveis são utilizadas juntas com muita frequência e, sendo elas relacionadas, é uma boa prática agrupá-las em um tipo struct. No próximo código irei agrupar os registradores e bits respectivos, tornando mais prático o uso em loops ou estruturas de decisões mais complexas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
#define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> #define SetBit(RES, BIT)(RES |= (1 << BIT)) // Por BIT em nível alto #define ClrBit(RES, BIT)(RES &= ~ (1 << BIT)) // Por BIT em nível baixo #define TstBit(RES, BIT)(RES & (1 << BIT)) // testar BIT, retorna 0 ou 1 #define CplBit(RES, BIT)(RES ^= (1 << BIT)) // Inverter estado do BIT #define MY_DELAY 1000 typedef struct{ // tipo gpio que agrupa volatile uint8_t port[ 7 ]; // endereços dos registradores uint8_t pin [ 7 ]; // e os bits respectivos } gpio; const gpio lcd7 = { //A B C D E F G { &PORTB, &PORTB, &PORTB, &PORTD, &PORTD, &PORTD, &PORTD }, { PB5, PB4, PB3, PD5, PD4, PD3, PD2 } }; const bool value_lcd[10][7] = { //A B C D E F G {1,1,1,1,1,1,0}, // 0 {0,1,1,0,0,0,0}, // 1 {1,1,0,1,1,0,1}, // 2 {1,1,1,1,0,0,1}, // 3 {0,1,1,0,0,1,1}, // 4 {1,0,1,1,0,1,1}, // 5 {1,0,1,1,1,1,1}, // 6 {1,1,1,0,0,0,0}, // 7 {1,1,1,1,1,1,1}, // 8 {1,1,1,1,0,1,1} // 9 }; void setPin(uint8_t *pr, uint8_t pn, bool value){ if(value) SetBit(*pr, pn); else ClrBit(*pr, pn); } int main(){ // BIT 76543210 DDRB = 0B00111000; DDRD = 0B00111100; PORTB = 0B00000000; PORTD = 0B00000000; while( true ){ for(uint8_t i=0; i<10; i++){ for(uint8_t j=0; j<7; j++){ setPin(lcd7.port[j], lcd7.pin[j], value_lcd[i][j]); } _delay_ms(MY_DELAY); } } return 0; } |
Primeiro crio um tipo struct chamado gpio que armazena dois vetores: um capaz de armazenar o endereço físico dos registradores e outro que guarda os bits respectivos. Para a inicialização da variável lcd7, no primeiro vetor (PORT) utilizo o operador & para retornar o endereço físico e não o valor do registrador (o registrador PORT não pode ser lido e sim apenas escrito). A variável value_lcd é uma matriz que contém os estados respectivos aos números mostrados no display de 7 segmentos. Dessa forma, torna-se prático a operação com os pinos em loops ou estruturas mais complexas.
Agora vamos ler os botões do shield e mostrar um valor correspondente no LCD de 7 segmentos. Os pinos utilizados pela placa são PC1, PD7 e PB0. Além disso os botões táteis não têm resistores de pull-down, o que implica na utilização dos pull-up internos do microcontrolador. Além de lermos LOW para botão pressionado e HIGH para botão não pressionado, para ler os valores em um pino específico utilizamos o registrador PIN.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
#define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> #define SetBit(RES, BIT)(RES |= (1 << BIT)) // Por BIT em nível alto #define ClrBit(RES, BIT)(RES &= ~ (1 << BIT)) // Por BIT em nível baixo #define TstBit(RES, BIT)(RES & (1 << BIT)) // testar BIT, retorna 0 ou 1 #define CplBit(RES, BIT)(RES ^= (1 << BIT)) // Inverter estado do BIT int main(){ ClrBit(DDRD, PD7); // BT PD7, pino PD7 entrada SetBit(PORTD, PD7); // ativar pull up do pino PD7 ClrBit(DDRB, PB0); // BT PB0, pino PB0 entrada SetBit(PORTB, PB0); // ativar pull up do pino PB0 // LCD DE 7 SEGIMENTOS SetBit(DDRD, PD5); SetBit(DDRD, PD4); SetBit(DDRD, PD3); SetBit(DDRD, PD2); SetBit(DDRB, PB5); SetBit(DDRB, PB4); SetBit(DDRB, PB3); while( true ){ ClrBit(PORTD, PD5); ClrBit(PORTD, PD4); ClrBit(PORTD, PD3); ClrBit(PORTD, PD2); ClrBit(PORTB, PB5); ClrBit(PORTB, PB4); ClrBit(PORTB, PB3); if(TstBit(PIND, PD7) == false){ // testar bit do botão ClrBit(PORTD, PD5); // 1 ClrBit(PORTD, PD4); ClrBit(PORTD, PD3); ClrBit(PORTD, PD2); ClrBit(PORTB, PB5); SetBit(PORTB, PB4); SetBit(PORTB, PB3); } if(TstBit(PINB, PB0) == false){ // testar bit do botão SetBit(PORTD, PD5); // 2 SetBit(PORTD, PD4); ClrBit(PORTD, PD3); SetBit(PORTD, PD2); SetBit(PORTB, PB5); SetBit(PORTB, PB4); ClrBit(PORTB, PB3); } } return 0; } |
Nesse programa as configurações são feitas em cada bit individualmente. Ao colocar os pinos dos botões como entrada, o bit correspondente do registrador PORT passa a controlar o estado do pull-up, sendo lógica 1 para ativado. O resto do programa segue o mesmo conceito, pois pinos com funções diferentes fazem parte do mesmo PORT, então temos que operar bit a bit para não causar bugs no programa.
Considerações finais
Vimos algumas formas de se trabalhar com os pinos do microcontrolador e possíveis estratégias de manipulação dos registradores no programa. Uma coisa importante para se levar em consideração é que não estamos programando um computador comum, e sim um hardware inferior criado especialmente para certas aplicações. Temos que cuidar para que nosso programa esteja otimizado, sempre poupando ao máximo a memória e garantindo a velocidade e robustez da aplicação.
No próximo artigo iremos continuar trabalhando com os pinos do microcontrolador, porém veremos de maneira mais prática. Vamos criar um robô seguidor de linha com o mínimo de código possível. Para isso iremos aprender a lidar com as interrupções externas do microcontrolador.
Saiba mais
ATmega328P Xplained Mini – Hardware
Integração entre Raspberry Pi e Atmega328P
Timers do ATmega328 no Arduino
Referências
ARDUINO. What is Arduino?. Disponível aqui. Acesso em: 22 jan. 2017.
ATMEL. ATmega328/P: ATmega328/P, DATASHEET COMPLETE. 2016. Disponível aqui. Acesso em: 22 jan. 2017.
AVELINO. Como programar para AVRs em C/C++? E utilizando o Arduino? 2015. Disponível aqui. Acesso em: 21 jan. 2017.
LIMA, Chaerles Borges. Vilaça, Marco V. M. AVR e Arduino Técnicas de Projeto. 2º edição. Florianópolis 2012, edição dos Autores.
LIMA, Chaerles Borges. Programação C para Arduino. 2013. Disponível aqui. Acesso em: 21 jan. 2017.
LIMA, Charles Borges. OS PODEROSOS µCONTROLADORES AVR. 2009. Instituto Federal de Educação, Ciência e Tecnologia de Santa Catarina, Florianópolis. Disponível aqui. Acesso em: 21 jan. 2017.
FLORENCIO, Heitor Medeiros. Sistemas Embarcados: Temporizadores e Contadores. Departamento de Engenharia de Computação e Automação, Universidade Federal do Rio Grande do Norte. Disponível aqui. Acesso em: 21 jan. 2017.
SOARES, Márcio José. CONFIGURANDO CORRETAMENTE OS REGISTRADORES DOS MICROCONTROLADORES AVR – parte 1: USO DOS REGISTROS PORTX, DDRX E PINX. Disponível aqui. Acesso em: 21 jan. 2017.
SOUZA, Fábio. Arduino UNO. Disponível aqui. Acesso em: 02 fev. 2017.
SOUZA, Fábio. Shield EDU-IFSP do GERSE – IFSP Guarulhos. 2017. Disponível aqui. Acesso em: 02 fev. 2017.
Arduino. Arduino Uno Rev3, Tech Specs. Disponível aqui. Acesso em: 02 fev. 2018.
GERSE, ShieldEdu-IFSP. Disponível aqui. Acesso em: 02 abril de 2018.
Muito bom!!!!!!!
Olá! Parabéns pelo texto. Nesse fim de semestre fiz um trabalho para a disciplina de Arquitetura de computadores, onde eu fiz uma “comunicação” entre dois Atmega 328p (arduino Uno) usando assembly. Fiz algo simples, mandar o sinal de um botão de um Arduíno A para o Arduíno B acender um led. Mas fiz tudo em 100% assembly usando o compilador Atmel. Ficou top de mais seu texto, eu só vi ele depois que fiz o trabalho, para mim só faltaria a questão de como referenciar a memoria (registrador), kkkkk de novo Parabéns! Uma coisa para finalizar, pq n usar um… Leia mais »
Estou muito agradecido pelo seu feedback, interessante, gostaria muito em ver um artigo sobre como fazer isso em assembly… Com relação ao acesso do registrador quando utilizo o termo PORTB por exemplo nada mais é uma macro de um ponteiro que aponta para o endereço de memória desse registrador, por isso utilizo “&PORTB” em um dado momento para armazenar o endereço (ou criar um novo ponteiro que aponta para o mesmo endereço) desse registrador. Mas podemos fazer isso na mão se quisermos, uma dica é apertar F3 na macro (serve com funções e variáveis ) o eclipse vai nos levar… Leia mais »
Texto muito bom ! Me ajudou bastante ! Ótimo !
Muito bom o texto.
Olá Pedro
Bem legal esse tópico, descrevendo o acesso direto aos registradores do AVR, são todos esses acessos que a API do Arduino “esconde”, no mais parabéns pelo artigo.
Só uma questão que você colocou varias vezes no seu texto, sobre o “display LCD”, pela foto do seu shield, você está usando um display LED de 7 segmentos. Display de LED e LCD ( liquid crystal display) são duas tecnologias diferentes e cada uma possui a sua forma de controle.
Obrigado pelo feedback. O Arduino esconde muita coisa, com intuído de toram mais acessível a tecnologia, mas para projetos mais complexos isso pode atrapalhar.
sim real mete foi um erro conceitual de deixei passar 🙁