Medidor de tensão e corrente AC com ESP32 e Arduíno
Olá, queridos alunos!
Hoje eu tenho um projeto aqui, no qual acabei fazendo um case, onde vamos medir tensão e corrente AC utilizando sensores que eu ainda não havia utilizado.
INTENÇÃO DESSA AULA
Disponibilizar um circuito de captura de tensão e corrente aplicados em uma carga.
MATERIAIS UTILIZADOS
- Módulo ESP32 WROOM 32
- Resistores de 10kW 1% e 82kW
- Capacitores de 0,22nF, 4n7 e 47nF
- Capacitor de 1000uF / 25V
- Led
- Sensor de tensão AC ZMPT101B
- Sensor de corrente ACS758LCB-050B
- Multímetro
- Osciloscópio
- Plugue NBR14136
- Tomada NBR14136
- Pcb universal
- Variac
- 5 Cargas resistivas de 150W
- Aplicativo datalogger SerialFK
CIRCUITO - Rede AC e sensores
Abaixo vemos a interligação dos sensores à rede AC a ser monitorada:
O sensor de tensão é conectado em paralelo com a carga.
O sensor de corrente é conectado em série com a carga.
Um interruptor é adicionado ao circuito para permitir o controle da alimentação da carga.
Um led indicador também é incluindo, indicando quando o a carga está energizada.
Mais detalhes sobre o sensor de tensão baseado no Trafo ZMPT101B:
No esquema podemos observar que o ZMPT101B age como um isolador entre a rede e o circuito de medição.
O sinal detectado é da tensão induzida sobre o resisto R1, capturada por um amplificador de diferença com ganho 10.
O sinal resultante é aplicado em um divisor de tensão formado pelo trimpot R12 e em seguida, aplicado em uma outra etapa amplificadora de mesmo tipo e ganho.
Detalhes sobre o sensor de corrente baseado no ACS758LCB-050B:
O sinal resultante do sensor ACS758 pode ser obtido diretamente, passando apenas por um filtro passa-baixas formado por R1 e C1, bastando para isso conectar o ADC na saída OUT1.
Mas também pode ser obtido passando por um amp-op seguidor de tensão, formado por U2.
CIRCUITO - Filtros de ADCs
Para garantir um sinal mais livre de ruídos nas entradas dos ADC e com níveis de tensão adequados aos 3V3 limite dos ADC, os sinais de cada sensor foram aplicados:
- Primeiro a um divisor de tensão formado por dois resistores de 10kW /1%, fazendo com que as tensões máximas de 5V das saídas sejam reduzidas a 2,5V. Não só adequado ao limite como também, dentro da faixa de maior linearidade dos ADCs do ESP32, o que resultará em ajustes de curva mais simples.
- Em seguida, esses sinais, provenientes dos divisores de tensão passam por dois filtros passa-baixas sobrepostos, para frequências de 10 e 100x a frequência da rede AC (600Hz e 6kHz).
- Um jumper foi adicionado ao filtro de 600Hz do sensor de tensão para desativação opcional, caso os ruídos sejam o objeto de estudo.
CIRCUITO - ESP32 e fonte
Depois de passar pelos divisores e filtros, os sinais são aplicados a suas respectivas entradas ADC no ESP32 (pinos 32 e 33 para o sinal de tensão e de corrente respectivamente).
Vemos também um detalhe dos capacitores da fonte de alimentação de 5V, que deve ser de boa qualidade, estável, livre de ruídos e com potência suficiente para manter o circuito.
CALIBRAÇÃO - Sensor de tensão - offset
Para efetuar a calibração do sensor de tensão, temos que recordar que sua saída mantém um offset de metade da tensão da alimentação. Neste caso 5V÷2 = 2,5V.
Esses 2,5V passam em seguida por um divisor de tensão de resistores iguais, logo, senso novamente dividido por 2. Assim, quando uma tensão de 0VAC estiver na entrada do sensor de tensão, devemos esperar uma tensão de 1,25V aplicada ao ADC de tensão.
Essa tensão de offset produzirá um código de offset no ADC. Para determiná-la, basta ler o valor do ADC com 0VAC aplicados no sensor.
A figura abaixo mostra uma parte dos dados capturados e o valor médio obtido para o código de offset do sensor de tensão.
Esse valor será importante para a conversão dos valores em valores de tensão e será utilizada como uma constante no código-fonte de captura.
//valores de offset de cada sensor (determinados estatisticamente através de capturas) const float offsetVAC = 1383.328;
CALIBRAÇÃO - Sensor de corrente - offset
Mas podemos determiná-la da mesma forma, como mostrado abaixo:
Da mesma forma, vamos utilizar futuramente este valor nos cálculos de conversão dos códigos do ADC em valores de tensão.
//valores de offset de cada sensor (determinados estatisticamente através de capturas) const float offsetVAC = 1383.328; const float offsetIAC = 1395.393991;
CALIBRAÇÃO - Sensor de tensão - valores instantâneos
Uma vez determinado o offset, podemos então aplicar uma tensão senoidal conhecida e através dela, comparar os códigos obtidos.
Aplicamos neste exemplo uma tensão senoidal de 137Vrms. Essa produzirá picos de +193,7V e -193,7V.
Usamos a função da reta de regressão como função de conversão:
Seu coeficiente angular e linear (A e B) são utilizados no código fonte.
void loop() //coeficientes das regressões lineares. //Usados para a conversão dos códigos const float coefA_VAC = 0.2546; const float coefB_VAC = 0.3807; Serial.print((float(tensao) - offsetVAC)*coefA_VAC - coefB_VAC);
CALIBRAÇÃO - Sensor de corrente - valores instantâneos
O mesmo princípio pode ser aplicado ao sensor de corrente. Nele, aplicamos uma corrente senoidal de 4,924Arms, que produzirá picos de +6,964A e -6,964A.
Capturando os códigos gerados e comparando os valores máximos e mínimos obtidos (descartando os picos causados por ruídos eventuais), podemos obter uma relação direta entre o código do ADC e os valores instantâneos, como mostrado na figura abaixo:
Usamos a função da reta de regressão como função de conversão:
Seu coeficiente angular e linear (A e B) são utilizados no código fonte.
void loop() //coeficientes das regressões lineares. //Usados para a conversão dos códigos const float coefA_IAC = 0.117; const float coefB_IAC = 0.1299; Serial.print((float(corrente) - offsetIAC)*coefA_IAC - coefB_IAC);
CALIBRAÇÃO - OBSERVAÇÃO!
Nestes métodos de calibração mostrados acima, as retas de regressão usadas nas conversões foram obtidas usando apenas três pontos. Fizemos desta forma aqui simplesmente para simplificar a aplicação e por conhecermos a priori o comportamento e o grau de linearidade dos sensores de tensão e corrente e dos circuitos acessórios.
Mesmo assim, todos as medidas obtidas com estes sensores posteriormente, foram verificadas diversas vezes e por processos diferentes após a calibração, descartando os possíveis desvios para além das limitações dos próprios sensores.
Em aplicações mais exigentes, mesmo conhecendo os sensores, utilize uma metodologia estatística mais robusta para garantir a determinação correta dos parâmetros de conversão.
CALIBRAÇÃO - Sensor de tensão - valores RMS
Para calibração dos valores RMS de tensão, utilizamos um variac para criar diversas tensões AC na entrada do sensor de tensão.
Já os códigos usados na regressão, são o resultado do algoritmo de cálculo da raíz quadrada média (root mean square ou RMS) dos códigos obtidos em um certo número de períodos da onda AC, capturados.
Utilizando os pares tensão e código do ADC obtidos, podemos então encontrar uma reta de conversão, como mostrado abaixo.
Definimos para a regressão o valor de 0V para o código 0 do ADC.
Usamos a função da reta de regressão como função de conversão:
Seus coeficiente angular e linear (A e B) são utilizados no código fonte.
//coeficientes das regressões lineares. //Usados para a conversão dos códigos const float coefA_VAC = 0.2652; const float coefB_VAC = 0.0; acumuladorTensao = acumuladorTensao * coefA_VAC + coefB_VAC; //Conversão em volts
CALIBRAÇÃO - Sensor de corrente - valores RMS
Para calibração dos valores RMS de corrente, utilizamos um 5 cargas resistivas de 150W, para variar a corrente na entrada do sensor, introduzindo-as sequencialmente em paralelo.
Já os códigos usados na regressão, são o resultado do algoritmo de cálculo da raiz quadrada média (root mean square ou RMS) dos códigos obtidos em um certo número de períodos da onda AC, capturados, assim como foi feito com os códigos para cálculo da tensão RMS.
Utilizando os pares corrente e código do ADC obtidos, podemos então encontrar uma reta de conversão, como mostrado abaixo.
Definimos para a regressão o valor de 0A para o código 0 do ADC.
Usamos a função da reta de regressão como função de conversão:
Seu coeficiente angular e linear (A e B) são utilizados no código fonte.
//coeficientes das regressões lineares. //Usados para a conversão dos códigos const float coefA_IAC = 0.1191; const float coefB_IAC = 0.0; acumuladorCorrente = acumuladorCorrente * coefA_IAC + coefB_IAC; //Conversão em ampères
CÓDIGO-FONTE - Cálculo do valor RMS
Para calcular o valor RMS (Root Mean Square), também conhecido como valor eficaz, para uma coleção de medidas discretas, podemos usar a expressão:
Para realizar esta operação, vamos dividi-la em etapas;
- Primeiro, vamos acumular o valor do quadrado de cada medida, bastando para isso, multiplicar cada medida realizada por ela mesma e, em seguida, adicioná-la a uma variável acumuladora.
- Depois de todas as medidas serem realizadas, dividimos o resultado do acumulador pelo número de medidas.
- Aplicamos então a raiz quadrada neste último valor. O resultado é então o valor RMS da coleção de medidas
CÓDIGOS-FONTE: Declarações e setup()
/* Seleção do MODO de operação: 0 - Transmite os valores brutos dos ADCs 1 - Transmite os valores instantâneos de tensão e corrente, em volts e ampères 2 - Transmite os valores RMS de tensão e corrente, em volts e ampères */ #define MODO 0 //Pinos de entrada utilizados const uint8_t pinAdcTensao = 32; const uint8_t pinAdcCorrente = 33; //Caracter separador dos dados const char separador = '\t'; //valores de offset de cada sensor (determinados estatisticamente através de capturas) const float offsetVAC = 1383.328; const float offsetIAC = 1395.393991; void setup() { Serial.begin(1000000); //Iniciando a comunicação serial }
CÓDIGOS-FONTE: loop no MODO 0
void loop() { #if MODO == 0 //Envio direto dos códigos do ADC sem nenhum tratamento //Variáveis int32_t tensao = 0; int32_t corrente = 0; //Captura tensao = analogRead(pinAdcTensao); corrente = analogRead(pinAdcCorrente); //Transmissão //Serial.print(micros()); //Serial.print(separador); Serial.print(tensao); Serial.print(separador); Serial.print(corrente); Serial.println(); #endif
CÓDIGOS-FONTE: loop no MODO 1
#if MODO == 1 //Envio dos códigos convertidos em tensão e corrente instantâneos //coeficientes das regressões lineares. Usados para a conversão dos códigos const float coefA_VAC = 0.2546; const float coefB_VAC = 0.3807; const float coefA_IAC = 0.117; const float coefB_IAC = 0.1299; //Variáveis int32_t tensao = 0; int32_t corrente = 0; //Captura tensao = analogRead(pinAdcTensao); corrente = analogRead(pinAdcCorrente); //Transmissão //Serial.print(micros()); //Serial.print(separador); Serial.print((float(tensao) - offsetVAC)*coefA_VAC + coefB_VAC); Serial.print(separador); Serial.print((float(corrente) - offsetIAC)*coefA_IAC + coefB_IAC); Serial.println(); #endif
CÓDIGOS-FONTE: loop no MODO 2
#if MODO == 2 //Envio dos códigos convertidos da tensão e corrente RMS //coeficientes das regressões lineares. Usados para a conversão dos códigos const float coefA_VAC = 0.2652; const float coefB_VAC = 0.0; const float coefA_IAC = 0.1191; const float coefB_IAC = 0.0; //Período mínimo de captura de 16,7ms para garantir que ao menos um ciclo seja capturado const int32_t duracaoDaAmostragem = (int32_t)((1000.0 / 60.0) * 6.0); //6 ciclos de 1000ms/60 (60Hz) //Variáveis int32_t tensao = 0; int32_t corrente = 0; uint32_t qtdDeAmostras = 0; float acumuladorTensao = 0.0; float acumuladorCorrente = 0.0; //Captura unsigned long inicio = millis(); do { tensao = analogRead(pinAdcTensao) - offsetVAC; corrente = analogRead(pinAdcCorrente) - offsetIAC; acumuladorTensao = acumuladorTensao + (tensao * tensao); //Soma dos quadrados acumuladorCorrente = acumuladorCorrente + (corrente * corrente);//Soma dos quadrados qtdDeAmostras++; } while ((millis() - inicio) < duracaoDaAmostragem); //Cálculo dos valores RMS acumuladorTensao = acumuladorTensao / float(qtdDeAmostras); //Média dos quadrados acumuladorCorrente = acumuladorCorrente / float(qtdDeAmostras); //Média dos quadrados acumuladorTensao = sqrt(acumuladorTensao); //RMS da captura acumuladorCorrente = sqrt(acumuladorCorrente); //RMS da captura acumuladorTensao = acumuladorTensao * coefA_VAC + coefB_VAC; //Conversão em volts acumuladorCorrente = acumuladorCorrente * coefA_IAC + coefB_IAC; //Conversão em ampères //Transmissão //Serial.print(micros()); //Serial.print(separador); Serial.print(acumuladorTensao); Serial.print(separador); Serial.print(acumuladorCorrente); Serial.println(); #endif }
SERIAL FK - Enviando a Tensão e Corrente AC RMS
Vamos utilizar agora o dispositivo para enviar as medidas de tensão e corrente AC RMS para o software Serial FK, onde podemos acompanhar seu comportamento através de indicadores, gravar logs e observar graficamente as variações.
Para isso, vamos introduzir mais um modo de operação do programa, o MODO 3. Nele, vamos apenas alterar as informações enviadas para o formato de recepção do Serial FK, incluindo o nome das variáveis e enviando também o instante da captura para registro no log, se necessário.
//Transmissão Serial.print("t"); Serial.print(separador); Serial.print(micros()); Serial.print(separador); Serial.print("Vac"); Serial.print(separador); Serial.print(acumuladorTensao); Serial.print(separador); Serial.print("Iac"); Serial.print(separador); Serial.println(acumuladorCorrente); #endif
O MODO 3 de operação é idêntico ao MODO 2, a não ser pela mudança nas informações enviadas.
O Serial FK espera que as informações cheguem no formato:
<NOME DA VARIÁVEL> <CARACTER DE TABULAÇÃO> <VALOR> <CARACTER DE TABULAÇÃO> <NOME DA SEGUNDA VARIÁVEL> <CARACTER DE TABULAÇÃO> <VALOR> <CARACTER DE TABULAÇÃO> . . . <CARACTER DE NOVA LINHA>
Vamos mudar também o Baud Rate para 115200. No setup().
Serial.begin(115200); //Iniciando a comunicação serial
No circuito de teste, vamos utilizar 5 cargas de 150W, as mesmas utilizadas durante a calibração. Também conectaremos dois multímetros, um como amperímetro e outro como voltímetro, somente para controle.
A seguir, foto do barramento AC com as cargas e os multímetros de controle de tensão e corrente. Ao lado o medidor montado.
Abaixo mais detalhes do software Serial FK.COMPLEMENTO - Conexão do Display
Abaixo, a tabela de conexão entre o ESP32 e o Display.COMPLEMENTO - Biblioteca
- Biblioteca TFT_eSPI
2.Pesquise por tft_espi
3.Selecione e instale a biblioteca TFT_eSPI distribuída por Bodmer
Precisamos configurar a pinagem utilizada e a frequência de comunicação na biblioteca TFT_eSPI
Para isso, substitua os arquivos User_Setup.h e User_Setup_Select.h da pasta:
C:\Users\[SEU NOME DE USUARIO]\Documents\Arduino\libraries\TFT_eSPI
Pelos arquivos de mesmo nome baixados junto com o projeto (disponíveis para download)
CÓDIGO
FLUXOGRAMA CÓDIGO-FONTE
Código ESP32: Declarações e variáveis
#include <esp_task_wdt.h> // Lib do watchdog #include <TFT_eSPI.h> // Lib do display #include <SPI.h> // Lib de comunicação SPI // Tamanho do display #define WIDTH 480 #define HEIGHT 320 // Tamanho da fonte #define FONT_SIZE 2 // Cor da linha de valores #define COLOR_LINE TFT_CYAN // Objeto do display TFT_eSPI tft; // Constantes referentes ao meio da tela do display const int CENTRE_X = WIDTH/2; const int CENTRE_Y = HEIGHT/2; // Espaçamento à esquerda do eixo Y const int MARGIN_Y = 30; // Valores máximo e mínimo iniciais do eixo Y float maxValue = 160, minValue = -160; // Pino do sinal e do botão de congelamento de tela const int signalPin = 25, button = 21; // Valor máximo do buffer, quantidade total de valores (amostras) const int MAX_SIZE_BUFFER = WIDTH - MARGIN_Y; // Vetor com as amostras obtidas, a captura é feita até encher o buffer com 450 valores int bufferValues[MAX_SIZE_BUFFER]; // Flag que indica o congelamento de tela bool hold = false;
Código ESP32: Setup
void setup() { // Desabilitamos o watchdog de hardware do core 0 disableCore0WDT(); // Inicializamos o watchdog com 5 segundos de timeout esp_task_wdt_init(5, true); // Inicializamos o display tft.init(); // Setamos a rotação (de 0 a 3) tft.setRotation(1); // Setamos os pinos de sinal e o do botão como entrada pinMode(signalPin, INPUT); pinMode(button, INPUT); // Limpamos o display tft.fillScreen(TFT_BLACK); // Criamos a task de botão xTaskCreatePinnedToCore(taskButton, "taskButton", 10000, NULL, 1, NULL, 1); // Criamos a task de gráfico xTaskCreatePinnedToCore(taskGraph, "taskGraph", 10000, NULL, 1, NULL, 0); }
Código ESP32: taskButton
// Task que lê o botão de congelamento e muda o estado da flag "hold" void taskButton(void *p) { // Adicionamos a tarefa na lista de monitoramento do watchdog esp_task_wdt_add(NULL); while(true) { // Resetamos o watchdog esp_task_wdt_reset(); //Se o botão foi pressionado if(digitalRead(button)) { // Mudamos o estado da variável global hold = !hold; // Enquanto o botão não for solto, resetamos o watchdog while(digitalRead(button)) esp_task_wdt_reset(); // Aguardamos 20ms delay(20); } } }
Código ESP32: taskGraph
// Task que cria e plota os valores no gráfico void taskGraph(void *p) { // Variável que percorre o ponto X int X; // Variável auxiliar de contagem de tempo de amostragem (runTime) long prevMicros, finishMicros; // Desenhamos os eixos X e Y drawAxis(); // Escrevemos os valores do eixo Y drawYValues(); // Adicionamos a tarefa na lista de monitoramento do watchdog esp_task_wdt_add(NULL); while(true) { // Resetamos o watchdog esp_task_wdt_reset(); // Obtemos o valor de micros anterior a execução (contagem de tempo) prevMicros = micros(); // Leitura for(int i = 0; i < MAX_SIZE_BUFFER; i++) bufferValues[i] = analogRead(signalPin); // Obtemos o valor de micros posterior a execução (contagem de tempo) finishMicros = micros(); // Tratamento de valores com posicionamento no display formatValuesToDisplay(); // Exibição de valores no display X = MARGIN_Y+1; for(int i=0; i<MAX_SIZE_BUFFER; i++) { if(i>0) tft.drawLine(X-1, bufferValues[i-1], X, bufferValues[i], COLOR_LINE); X++; } // Exibimos o tempo de execução showRuntime(finishMicros-prevMicros); // Desenhamos o eixo x novamente tft.drawLine(0, CENTRE_Y, WIDTH, CENTRE_Y, TFT_WHITE); // Verificamos se a tela deve ser congelada, se sim resetamos o watchdog até que a tela seja descongelada while(hold) esp_task_wdt_reset(); // Limpamos os valores no display X = MARGIN_Y+1; for(int i=0; i<MAX_SIZE_BUFFER; i++) { if(i>0) tft.drawLine(X-1, bufferValues[i-1], X, bufferValues[i], TFT_BLACK); X++; } } }
Código ESP32: formatValuesToDisplay (taskGraph)
// Função que trata os valores AD e os transformam em posições Y do display void formatValuesToDisplay() { int value; // Percorremos o buffer com os valores AD for(int i=0; i<MAX_SIZE_BUFFER; i++) { value = bufferValues[i]; // Se o valor ultrapassou o valor máximo exibido do eixo Y, reconfiguramos os valores positivos do eixo Y if(value > maxValue) { maxValue = value; tft.fillRect(0,0,MARGIN_Y, HEIGHT, TFT_BLACK); drawYValues(); drawAxis(); } // Dividimos o valor lido pelo valor maximo e pela altura do display value /= maxValue / HEIGHT; // O valor neste momento estará invertido, arrumamos isso retirando o valor HEIGHT value -= HEIGHT; // Se for negativo, deixamos o valor como positivo if(value < 0) value*=-1; // Devemos dividir o valor por 2, mas antes verificamos se ele não é zero if(value > 0) value/=2; // Atribuímos o novo valor no buffer bufferValues[i] = value; } }
Código ESP32: drawYValues (taskGraph)
// Função que desenha os valores do eixo Y void drawYValues() { float value; float i=0; // Setamos a cor do texto como verde tft.setTextColor(TFT_GREEN); // Exibição de valores acima do eixo X (positivos) for(int y = CENTRE_Y-10; y>=0; y-=10) { value = 10*++i*(maxValue/(HEIGHT/2)); tft.drawString(String((int)value), 0, y); } value = -10; i=0; // Exibição de valores abaixo do eixo X (negativos) for(int y = CENTRE_Y; y<HEIGHT; y+=10) { value = 10*++i*(minValue*-1/(HEIGHT/2))*-1; tft.drawString(String((int)value), 0, y); } // Setamos cor do texto como branco novamente tft.setTextColor(TFT_WHITE); }
Código ESP32: drawAxis (taskGraph)
// Função que desenha os eixos X e Y void drawAxis() { // Eixo x tft.drawLine(0, CENTRE_Y, WIDTH, CENTRE_Y, TFT_WHITE); // Eixo y tft.drawLine(MARGIN_Y, 0, MARGIN_Y, HEIGHT, TFT_WHITE); }
Código ESP32: showRunTime (taskGraph)
// Função que exibe o tempo de execução da captura total de amostras void showRuntime(uint32_t runtimeMsg) { // Altura e largura da fonte int fontHeight = 10, fontWidth = 14; // Limpa valor antigo tft.fillRect(CENTRE_X - fontHeight * 3, 300, fontHeight * 6, fontWidth, 0x0000); // Exibe a label tft.setTextColor(TFT_GREEN); tft.drawCentreString("Runtime: (usecs)", CENTRE_X, 280, FONT_SIZE); // Exibe o novo valor tft.setTextColor(TFT_WHITE); tft.setTextDatum(TC_DATUM); tft.drawNumber(runtimeMsg, CENTRE_X, 300, FONT_SIZE); }
0 Comentários