Manipulando os registradores - ESP32
Registradores - ESP32
Com o sucesso gigantesco do ESP32 que veio para bater de frente dos PIC32 e STM32, ganhando em alguns aspectos como velocidade, não podemos deixar de lado a manipulação de registradores que não estavam presentes, de maneira fácil, no ESP8266. Neste tutorial, vamos aprender a manipular os registradores de porta (GPIO) do ESP32 de forma simples para efetuar um Blink extremamente compacto e o mais rápido possível.
[toc]
O que são registradores em um microcontrolador?
São pequenos endereços de memória que nos permitem, via software, controlar pinos, processos, interrupções e coisas do tipo de forma extremamente eficiente. Podemos dizer que isso se assemelha ao assembly, visto que é um dos itens de mais baixo nível que conseguimos controlar diretamente pelo código e com isso temos uma eficiente forma de programação em questão de velocidade e confiança.
O ESP32 conta com centenas de registradores de 32bits para que possamos manipular com eficiência todo o hardware disponível, todas as informações estão disponíveis no Datasheet que estará ao fim deste tutorial.
Observação importante: em alguns casos de registradores como o correspondente ao famoso "pinMode()" que declara um pino como saída por exemplo, há mais de um registrador que faz essa mesma tarefa e também mais formas de escrever ou ler registradores, entretanto, sobre os registradores, usaremos os registradores atômicos, que nos garantem a escrita ordenada em um ambiente Triple-Core.
Mãos a obra - Piscando um LED através da manipulação de registradores
Componentes necessários
- 1x - ESP32.
- 1x - LED (usaremos o LED Onboard).
Código do projeto
void setup() { REG_WRITE(GPIO_ENABLE_REG, BIT2);//Define o GPIO2 como saída } void loop() { REG_WRITE(GPIO_OUT_W1TS_REG, BIT2);//GPIO2 HIGH (set) delay(250); REG_WRITE(GPIO_OUT_W1TC_REG, BIT2);//GPIO2 LOW (clear) delay(250); }
Colocando para funcionar
Entendendo a fundo
Software
Quem esta acostumado com registradores pode estranhar esse jeito de manipula-los, parecendo muito mais fácil do que em outros microcontroladores. A verdade é que "REG_WRITE" é uma macro para facilitar a manipulação dos registradores que é definida no arquivo "soc.h", lá você também encontra macros como REG_READ que é usada para leitura de registradores, REG_SET_FIELD e etc.
Os três registradores usados são:
- GPIO_ENABLE_REG (Figura 1): Registrador que habilita o GPIO(0-31) como saída.
- GPIO_OUT_W1TS_REG (Figura 2): Registrador que define o GPIO(0-31) em HIGH (Vcc). (SET)
- GPIO_OUT_W1TC_REG (Figura 3): Registrador que define o GPIO(0-31) em LOW (GND). (CLEAR)
Uma forma bem comum de se utilizar registradores para pinos, é a manipulação direta (nesse caso há a 32 bits, logo, 32 pinos) de uma única vez em uma linha! Com isso é possível economizar varias linhas e também deixar o código mais rápido. Se você pretende definir dois pinos como saída, a sentença ficará dessa forma (serve para os outros registradores também):
REG_WRITE(GPIO_ENABLE_REG, BIT2 + BIT4);//Define o GPIO2 e GPIO4 como saída
Esses são os registradores básicos para manipular um pino de saída, se você pretende ler pinos de entrada, terá que usar outros registradores que estão detalhados no Datasheet.
Observação: alguns registradores não estão com os nomes definidos nos arquivos do ESP32, logo, você não conseguirá manipular o registrador pelo nome (igual feito acima com GPIO_ENABLE_OUT e etc). Para manipular os registradores que não estão definidos, é necessário pegar o endereço do registrador na memória que se encontra no Datasheet. Veja como ficaria a manipulação sem o nome definido:
REG_WRITE(0x3ff44020, BIT2);//Define o GPIO2 como saída while (1) { REG_WRITE(0x3ff44008, BIT2);//GPIO2 = HIGH delay(250); REG_WRITE(0x3ff4400C, BIT2);//GPIO2 = LOW delay(250); }
Considerações finais
Manipulando diretamente os registradores, conseguimos fazer tarefas com uma extrema velocidade e confiança, sendo necessária para vários projetos. Leia bem o Datasheet se você pretende dominar este incrível microcontrolador e boa sorte com os registradores.
Referências
Todos os registradores, informações e detalhes sobre o ESP32 se encontram nesse Datasheet:
http://espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf
Entrada e Saída - Manipulando Registradores
Entrada e Saída - Manipulando Registradores
Para determinar os estados de suas entradas e saídas o microcontrolador possui registradores na qual esses dados são armazenados. Ao chamar as funções de entrada e saída fornecidas pela biblioteca padrão do Arduino o que fazemos é nada mais que modificar tais registradores. Então porque acessar estes registradores diretamente?
A manipulação direta de registradores permite que:
- A leitura e escrita em pinos seja feita muito mais rápida.
- Ler e escrever em mais de um pino de uma mesma porta por vez.
- O código produzido é menor, em alguns casos esse fator pode fazer a diferença entre seu código caber na memória flash ou não.
Essa técnica também possuí desvantagens:
- O código produzido é menos portável, difícil de debbugar e de ser mantido.
Na abordagem dos conceitos a seguir o microcontrolador atmega328 presente na placa Arduino Uno será utilizado. Para acompanhar esse tutorial é necessário algum conhecimento de lógica digital.
Todos os registradores desse microcontrolador possuem 8 bits. Os pinos de entrada e saída são divididos em PORTs. O atmega328 possui 3 PORTs, como podemos observar no diagrama a seguir:
Cada PORT possui 3 registradores com diferentes funções:
DDR
Os registradores do tipo DDR (Data Direction Register) são responsáveis por determinar se os pinos de um determinado PORT se comportarão como entrada ou saída. Cada bit do registrador DDR controla o estado do respectivo pino. Por exemplo: O bit 1 do registrador DDRB (DDB1) controlará o estado do pino PB1 e consequentemente o pino D9 do Arduino Uno como mostrado no mapa.
Para definir um pino como saída devemos setar seu respectivo bit do registrador DDR como 1 e para defini-lo como entrada seta-lo para 0.
/* Equivalente: pinMode(9,OUTPUT); pinMode(9,INPUT); */ DDRB |= (1 << DDB1); DDRB &= ~(1 << DDB1);
PORT
Os registradores do tipo PORT são responsáveis por determinar se um pino está definido como alto (HIGH) ou baixo (LOW).
Para definir um pino como alto devemos setar seu respectivo bit do registrador PORT como 1 e para defini-lo como baixo seta-lo para 0.
/* Equivalente: pinMode(9,OUTPUT); digitalWrite(9,LOW); */ DDRB |= (1 << DDB1); PORTB &= ~(1 << PORTB1);
Outro exemplo:
/* Equivalente: digitalWrite(8,HIGH); digitalWrite(9,HIGH); digitalWrite(10,HIGH); digitalWrite(11,HIGH); digitalWrite(12,HIGH); digitalWrite(13,HIGH); */ PORTB = 0xFF;
PIN
Os registradores do tipo PIN são responsáveis por guardar o estado lógico de um pino.
/* Equivalente: pinMode(9,INPUT); digitalWrite(9,HIGH); //Nesse contexto, ativa o pull-up interno. bool x = digitalRead(9); */ DDRB &= ~(1 << DDB1); PORTB |= (1 << PORTB1); bool x = (PINB & (1 << PINB1));Todo o conteúdo apresentado pode ser encontrado no datasheet do microcontrolador.
Introdução a Interrupções e PCINT
INTRODUÇÃO A INTERRUPÇÕES E PCINT
A compreensão do funcionamento de interrupções é essencial para uma programação eficiente de microcontroladores. Entretanto existem muitas dúvidas em torno desse tópico. Nesse post veremos uma pequena explicação de como interrupções funcionam e em seguida focaremos na interrupção por mudança de estado (PCINT). Na abordagem dos conceitos a seguir o microcontrolador atmega328 presente na placa Arduino Uno será utilizado. Apesar de utilizarmos o atmega328, os conceitos abordados aqui podem ser estendidos para outros atmegas. Para acompanhar esse tutorial é indicado que você tenha entendido o post sobre manipulação de registradores de entrada e saída que pode se encontrado aqui.
O QUE SÃO INTERRUPÇÕES?
Imagine que você está em casa extremamente focado em escrever um texto no computador, porém de repente seu celular toca. Mesmo sendo uma ligação inesperada, você é capaz de parar a escrita do texto, atender a ligação e voltar para onde você parou quando o telefone tocou.
Dessa maneira que funcionam as a interrupções em um microcontrolador. Nós podemos programá-lo para observar eventos externos, como um pino mudando de estado, enquanto o microcontrolador executa as instruções do código principal. Quando um evento ocorrer, o microcontrolador interromperá a execução do código principal, tratará o evento chamando uma função especificada por nós e retornará a execução do código principal. Dessa maneira nosso programa ganha flexibilidade, uma vez que não é mais necessário ficar checando a todo momento se o evento ocorreu. Nós podemos simplesmente configurar a interrupção e a função de tratamento e assim que o evento ocorrer, ele será tratado.
É importante lembrar que nós não estamos fazendo duas coisas ao mesmo tempo. O programa principal será parado enquanto a função de tratamento estiver sendo executada. Esta função de tratamento é denominada ISR (Interrupt Service Routine). Cada interrupção terá seu vetor de interrupção que nada mais é que um índice em uma tabela de interrupções que apontará para nossa rotina de tratamento.
PCINT - PIN CHANGE INTERRUPT
Como o nome indica este tipo de interrupção ocorrerá quando houver uma mudança no estado do pino escolhido. No atmega328 existem 3 vetores de interrupção para esse tipo de interrupção. Cada um deles está ligado a um PORT. Consequentemente, caso dois pinos de um mesmo PORT estejam utilizando essa interrupção ambos compartilharão a mesma rotina de interrupção. Cabe a nós então, implementar a rotina de interrupção de forma que esta seja capaz de identificar em qual dos dois pinos ocorreu a interrupção. O diagrama da Figura 1 nos ajudará no entendimento das explicações a seguir.
PCICR - Pin Change Interrupt Control Register
Este registrador é reponsável por habilitar a interrupção em um determinado PORT quando o respectivo bit PCIEx for setado para 1.
PCMSK - Pin Change Mask Register
Este registrador é responsável por habilitar a interrupção de um pino em um determinado PORT. Logo, existem 3 registradores desse tipo PCMSK0, PCMSK1 e PCMSK2 referentes aos PORTS B, C e D respectivamente.
SREG - Global Interrupt Flag
Essa flag é responsável por controlar as interrupções de todo o microcontrolador. Funciona como uma chave geral. Uma maneira de modifica-la é através das macros sei() e cli().
- sei() - Habilita as interrupções globalmente;
- cli() - Bloqueia as interrupções globalmente.
Analogia com Chaves
Para facilitar a visualização do papel de cada registrador nós preparamos o diagrama a seguir. Ele mostra o caminho que a interrupção deve fazer até chegar ao respectivo vetor de interrupção. As chaves representam cada bit do registrador especificado. Chave fechada representa que o bit está setado (1) e chave aberta que ele está limpo (0).
EXEMPLOS
Exemplo 1
Nesse exemplo nós veremos como habilitar a interrupção por mudança de estado no pino D12. Como podemos ver no diagrama da Figura 1 o pino D12 é equivalente ao pino PB4 do microcontrolador atmega328. Começamos o programa configurando este pino como uma entrada. Durante a execução das configurações devemos desligar as interrupções globalmente. O pino PB4 possui a interrupção PCINT4 essa interrupção é habilitada por PCMSK0 que por sua vez é habilitado por PCIE0. Logo, devemos setar todos esses registradores. Finalmente devemos configurar a função que será chamada quando a interrupção ocorrer. Isso é feito com o auxílio da macro ISR() que recebe como parâmetro o vetor de interrupção que desejamos configurar. No nosso caso o vetor é o PCINT0_vect.
void setup() { cli(); // Equivalente a pinMode(12, INPUT_PULLUP); DDRB &= ~(1 << DDB4); // Seta D12 como entrada; PORTB |= (1 << PORTB4); // Liga Pull-up; // Seta as "chaves" necessárias para que a interrupção chegue a seu vetor; PCICR |= (1 << PCIE0); PCMSK0 |= (1 << PCINT4); sei(); } void loop() { //... } /* Função de Tratamento de Interrupção Como somente o pino D12 foi configurado para chamar esta função, não precisamos realizar checagens complexas para entender o que ocorreu. */ ISR(PCINT0_vect) { if (PINB & (1 << PINB4)) { // D12 mudou de LOW para HIGH; } else { // D12 mudou de HIGH para LOW; } }
Exemplo 2
Nesse exemplo nós veremos como habilitar a interrupção por mudança de estado nos pinos D12, D11 e D10. Como podemos ver no diagrama da Figura 1 o estes pinos são equivalentes aos pinos PB4, PB3 e PB2 respectivamente do microcontrolador atmega328. Repetimos todos os passos anteriores, só que agora para todos os pinos. Diferentemente do exemplo anterior agora um mesmo vetor será chamado por vários pinos. Para definir qual pino causou a interrupção devemos guardar um histórico do ultimo estado de todo o PORTB.
void setup() { cli(); /* Equivalente a pinMode(12, INPUT_PULLUP); pinMode(11, INPUT_PULLUP); pinMode(10, INPUT_PULLUP); */ DDRB &= ~( (1 << DDB4) | (1 << DDB3) | (1 << DDB2) ); PORTB |= ( (1 << PORTB4) | (1 << PORTB3) | (1 << PORTB2) ); // Seta as "chaves" necessárias para que as interrupções cheguem ao vetor; PCICR |= (1 << PCIE0); PCMSK0 |= ( (1 << PCINT4) | (1 << PCINT3) | (1 << PCINT2) ); sei(); } void loop() { //... }
// Variáveis globais que são acessadas por interrupções devem ser declaradas volatile; volatile uint8_t last_PINB = PINB; /* Função de Tratamento de Interrupção */ ISR(PCINT0_vect) { uint8_t changed_bits; changed_bits = PINB ^ last_PINB; last_PINB = PINB; if (changed_bits & (1 << PINB4)) { if (PINB & (1 << PINB4)) { // D12 mudou de LOW para HIGH; } else { // D12 mudou de HIGH para LOW; } } else if (changed_bits & (1 << PINB3)) { if (PINB & (1 << PINB3)) { // D11 mudou de LOW para HIGH; } else { // D11 mudou de HIGH para LOW; } } else if (changed_bits & (1 << PINB2)) { if (PINB & (1 << PINB2)) { // D10 mudou de LOW para HIGH; } else { // D10 mudou de HIGH para LOW; } } }
FINALIZANDO
Este conteúdo foi totalmente voltado para esclarecer alguns pontos à respeito das interrupções. Esperamos que você tenha gostado deste conteúdo, sinta-se à vontade para nos dar sugestões, críticas ou elogios. Lembre-se de deixar suas dúvidas nos comentários abaixo.