Implementação de um controle PID utilizando Arduino e FreeRTOS

Controle PID

O PID (Proporcional-Integral-Derivativo) é atualmente, segundo a National Instruments, o algoritmo de controle mais utilizado na indústria. Isso se deve tanto ao seu desempenho robusto como também à sua simplicidade funcional. Esse algoritmo tem por objetivo realizar o controle preciso de uma variável em um sistema em malha fechada, isto é, sistemas em que a ação de controle depende da saída (estado atual) da variável a ser controlada. De forma mais prática, o controle se dá analizando o sinal de erro, que consiste na diferença entre o valor desejado (setpoint) e o valor atual (lido por um sensor). A todo momento o sinal de erro é recalculado pelo algoritmo e utilizado para determinar as ações de controle sobre um determinado atuador, a fim de minimizar o sinal de erro e, idealmente, torná-lo zero. Vamos agora dar uma breve analisada em quais são as funções desempenhadas pelos agentes P, I e D no controle de processos.

Ação Proporcional – P

Age proporcionalmente à amplitude do erro do sistema, multiplicando uma constante de proporcionalidade Kp pelo erro do sistemal, Então:

P = Kp * Erro

A ação proporcional elimina as oscilações da variável, tornando o sistema estável, mas não garante que a mesma esteja no valor desejado (setpoint), esse desvio é denominado off-set. Um ganho proporcional muito alto gera um alto sinal de saída, o que pode desestabilizar o sistema. Porém, se o ganho proporcional é muito baixo, o sistema falha em aplicar a ação necessária para corrigir os distúrbios.

Ação Integral – I

Tem por objetivo eliminar o erro de estado estacionário (off-set), fazendo com que a variável controlada fique muito próxima ao valor de setpoint mesmo que ocorram perturbações no sistema. A ação integral produz um sinal de saída que é proporcional à magnitude e à duração do erro, ou seja, realiza a integração do erro no tempo, portanto quanto maior for o tempo de permanência do erro no sistema, maior será a amplitude da ação integral. Computacionalmente falando a intregação pode ser vista como uma somatória, com isso, a componente integral se equivale a um acumulador do erro do sistema, multiplicado por uma constante de integração Ki, veja a fórmula:

I = I + (Ki * Erro)

Ação Derivativa – D

Fornece uma correção antecipada do erro, diminuindo o tempo de resposta e melhorando a estabilidade do sistema. Este parametro produz um sinal de saída que é proporcional à taxa de variação da variável controlada, sendo inversamente proporcional à velocidade da mesma. Por se tratar da variação de um sinal, a componente derivativa pode ser calculada pela diferença dos dois últimos valores da variável de interesse, multiplicados por uma constante Kd, vejamos:

D = Kd * (ValorAtual – ValorAnterior)

Aumentar o parâmetro do tempo derivativo (Kd) fará com que o sistema de controle reaja mais fortemente à mudanças do erro. Na prática, a maioria dos sistemas de controle utilizam um Kd muito pequeno, pois a derivada tem a resposta muito sensível ao ruído no sinal da variável de processo.

Apresentados todos os agentes do algoritmo PID, chegamos a sua fórmula final, sendo:

PID = P + I + D

Então:

PID = Kp * Erro + (I + (Ki * Erro)) + Kd * (ValorAtual – ValorAnterior)

O projeto

Servomotor

Neste artigo iremos realizar o controle da posição de um servomotor, portanto essa (a posição) será a nossa variável de interesse. Para facilitar a compreensão, veja abaixo a estrutura interna de um servo:

Estrutura de um servomotor
Fonte: Site Mundo da Elétrica

Como podemos ver na imagem um servomotor possui três elementos principais: O motor CC que emprega o movimento de rotação; A caixa de engrenagens que reduz a velocidade e aumenta o torque; E o potênciometro que está conectado junto ao eixo do servomotor. Portanto, quando o eixo gira, gira também o cursor do potênciometro, o que nos permite saber, através da leitura da tensao no terminal central do potenciômetro, a posição atual em que o motor se encontra.

Ponte H

Para realizar o acionamento do servomotor será utilizado um circuito ponte H, cujo diagrama esquemático é o seguinte:

Estrutura ponte H

Com a ponte H podemos alterar o sentido em que a corrente passará pelo motor, alterando assim seu sentido de giro, apenas controlando o chaveamento do switches. Isso é ilustrado na figura abaixo:

Modos de funcionamento de uma ponte H

Para implementação da ponte H será utilizado o circuito integrado L298, esse CI possui 2 circuitos ponte H em seu encaplusamento, veja no diagrama esquemático:

Esquemático do CI L298

O L298 permite o controle do servomotor utilizando apenas duas portas do microcontrolador, isso é possível da seguinte forma: Com a porta EnA (enable) em nível alto (+5V) uma das pontes está habilitada para o uso. Para definir o sentido de rotação do motor basta alterar as entradas In1 e In2, de modo que:

Modos de trabalho do CI L298

Além disso, o L298 permite que motores de até 50V sejam comandados por apenas por 5V, já que ele possui entradas específicas para acionamento e para alimentação.

PWM

Para realizar o controle da velocidade do servomotor será utilizada a técnica de modulação por largura de pulso (PWM), Com o ela é possível controlar a tensão e corrente que serão entregues ao motor, isso é feito ao ligar e desligar (chavear) o fornecimento de energia entre a fonte e a carga em uma taxa muito rápida. Quanto mais tempo a alimentação permanece ligada, em comparação com o tempo desligada, maior a quantidade total de potência fornecida à carga. A razão de tempo em que o sinal pernace em nível alto é denomida Duty Cicle, o funcionamento prático do PWM é ilustrado na figura abaixo:

Funcionamento do PWM

Note que a frequência dos três sinais é sempre a mesma, isso é fundamental para o funcionamento correto desta técnica, portanto, a única condição que se altera é o duty cicle, sendo que, quanto maior o duty cicle, maior será a tensão média entregue à carga.

Montagem do circuito

Definido e descrito o hardware e as técnicas utilizadas, podemos finalmente realizar a montagem do nosso circuito. Além dos componentes já mecionados será utilizado também um potenciômetro, para que possamos alterar o setpoint manualmente e assim, controlar de fato o motor. Acompanhe abaixo a ligação dos componentes:

Montagem do projeto

Os dois potenciômetros, o do servomotor e o de controle do setpoint, estão conectados às entradas analógicas do Arduino. A ponte H é alimentada com as tensões de controle (5V) e de alimentação do servomotor (representado pela bateria), o Enable A é conectado diretamente aos 5V, mantendo uma das pontes sempre habilitada. Os pinos de controle In1 e In2 estão ligados nas portas digitais PWM do Arduino, já que serão controladas através deste método. Finalmente, as saídas da ponte H OUT1 e OUT2 são ligadas aos terminais de alimentação do servomotor.

Software

O software a ser embarcado no microcontrolador será desenvolvido em linguagem C no ambiente Atmel Studio, não utilizaremos a IDE do Arduino tampouco as bibliotecas nativas dela, isso será feito visando um melhor desempenho do microcontrolador, já que as bibliotecas do Arduino possuem um nível muito alto de abstração, prejudicando a velocidade de execução das tarefas. Abaixo veja um paralelo entre os terminais do Arduino e do microcontrolador ATmega328P, embarcado no Arduino:

Pinout do ATmega328P no Arduino Uno

Neste projeto também será utilizado um sistema operacional de tempo real (RTOS), com ele conseguimos criar um ambiente multitarefas, já que ele alterna rapidamente entre as tarefas a serem executadas, diferentes de programas em single loop, que executam tudo de forma sequencial. O sistema escolhido para esse projeto foi o FreeRTOS, um dos mais utilizados do mundo, e que possui código aberto.

De forma resumida, a programação com o FreeRTOS baseia-se em três conceitos principais: A divisão do programa em tarefas (tasks), onde cada tarefa representa uma funcionalidade do sistema e é tratada como uma rotina isolada; O uso de filas (queues) como meio de comunicação entre as tarefas, caso seja necessária a interação entre elas; E, finalmente, a criação de semáforos (semaphores), para que diferentes tarefas possam compartilhar recursos de hardware de forma segura.

Programação

Partimos agora para a programação de nosso software. Inicialmente é definida a frequência de trabalho e são declaradas as bibliotecas padrão dos microcontroladores AVR e do FreeRTOS:

#define F_CPU 16000000UL

#include <avr/io.h>
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "queue.h"

Logo após, é feito o mapeamento de hardware e são declaradas as tarefas, as filas e o semáforo que serão utilizados:

//------Mapeamento de Hardware------

#define PWM      PD3  //Sinal que controla a velocidade do motor		
#define sentido  PD5  //Sinal que controla o sentido de giro do motor
#define controle PC0  //Sinal de leitura do potenciometro de controle
#define motor    PC1  //Sinal de leitura do potenciometro do motor
#define led      PC5  //Led para o blink

#define dutyCicle OCR2B //Registrador que controla o duty cicle do PWM		

//------Declaração das tarefas------

void TaskSetpoint(void *pvParameters);
void TaskPosicaoAtual(void *pvParameters);
void TaskPID(void *pvParameters);
void TaskBlink(void *pvParameters);

//------Declaração das filas------

QueueHandle_t QueueSetpoint;
QueueHandle_t QueuePosicaoAtual;

//------Declaração do semáforo------

SemaphoreHandle_t SemaforoADC; 

Ao todo teremos 4 tarefas:

  • TaskSetpoint – Nessa tarefa será feita a leitura do potenciômetro que controla o setpoint.
  • TaskPosicaoAtual – Tarefa responsável por ler o potenciômetro do servomotor.
  • TaskPID – Tarefa na qual será realizado o controle PID efetivamente, com o cálculo dos parâmetro e da saída.
  • TaskBlink – Nosso projeto terá um led piscando à uma frequência constante, a fim de comprovar que o código está em execução.

2 filas:

  • QueueSetpoint – Meio de comunicação pelo qual a tarefa TaskSetpoint enviará o valor do setpoint para a tarefa TaskPID.
  • QueuePosicaoAtual – Faz a comunicação entre TaskPosicaoAtual eTaskPID.

1 semáforo:

  • SemáforoADC – Tem a função de limitar o acesso ao conversor AD do microcontrolador, evitando que duas tarefas tentem fazer uso dele ao mesmo tempo, o que iria corromper os dados e comprometer todo o funcionamento do sistema.

Em sequência, é declarada a função principal do programa e são feitas as configurações iniciais

int main(void)
{
    DDRD |= (1<<PWM) | (1<<sentido); //Bits de controle do 
                                     //motor como saídas
	
    DDRC |= (1<<led);    //Led como saída
	
    ADMUX = 0b01000000;  //Habilita tensão de referência interna 
                         //e seleciona o ADC0 (setpoint) 
    ADCSRA = 0b10000111; //Habilita o conversor AD e 
                         //configura o prescaler para 128

Conforme o datasheet do microcontrolador ATmega328P (cuja leitura é indispensável), os registradores DDR definem se determinada porta será de entrada ou saída (1 = saída), os registradores PORT são responsáveis por escrever HIGH ou LOW nas saídas. Já os registradores ADMUX e ADCSRA dizem respeito à configuração do conversor Analógico-Digital, necessário para a leitura dos potenciômetros presentes no sistema.

Ainda na função principal são criados o semáforo, as filas e as tarefas:

	//------Criação do semáforo------
	
	SemaforoADC = xSemaphoreCreateMutex();
	
	//------Criação das filas------
	
	QueueSetpoint = xQueueCreate(1, sizeof(double));
	QueuePosicaoAtual = xQueueCreate(1, sizeof(double));
	
	//------Criação das tarefas------
	
	xTaskCreate(
		TaskSetpoint            //Tarefa a ser configurada
		, "Leitura do Setpoint" //Nome (para debug)
		, 128                   //Stack reservada para a tarefa
		, NULL                  //Parâmetros passados para a tarefa
		, 0                     //Prioridade da tarefa
		, NULL                  //Handle da tarefa
	);
		
	xTaskCreate(
		TaskPosicaoAtual
		, "Leitura da posição atual do motor"
		, 128
		, NULL
		, 0
		, NULL
	);
		
	xTaskCreate(
		TaskBlink
		, "Blink led"
		, 128
		, NULL
		, 1
		, NULL
	);
	
	xTaskCreate(
		TaskPID
		, "PID"
		, 200
		, NULL
		, 0
		, NULL
	);
		
	vTaskStartScheduler(); // Inicia o RTOS
	
    while (1) 
    {
		  //Vazio, tudo é executado nas tarefas
    }
}

O semáforo será do tipo MUTEX (exclusão mútua) e as filas de apenas um elemento do tamanho de uma variável do tipo double. É importante ressaltar que, quando se utiliza da programação com RTOS, o loop infinito deve permanecer vazio, já que todos os comandos e instruções são realizados dentro das tarefas.

Terminada a função main, podemos finalmente focar na programação das tarefas em si, vejamos:

void TaskBlink(void *pvParameters)
{
   (void)pvParameters;
	
   while(1)
   {
       PORTC ^= (1<<led);                  //Inverte o estado do led
       vTaskDelay(500/portTICK_PERIOD_MS); //Aguarda 500 ms
   }
}

Começando pela tarefa mais simples, o blink do led, essa tarefa simplesmente inverte o estado do led (se está aceso, apaga, e se está apagado, acende) a cada 500 milissegundos.

Na sequência, temos a tarefa responsável pelo controle do setpoint. Ela é responsável por fazer a leitura do potenciômetro de controle, através do conversor AD, veja:

void TaskSetpoint(void *pvParameters)
{
   (void)pvParameters;
	
   double setpointADC;
	
   while(1)
   {
       //Solicita o semáforo (aguarda até conseguir)
	    xSemaphoreTake(SemaforoADC, portMAX_DELAY);

       //Seleciona o ADC0 (potenciômetro de controle)
	    ADMUX &= ~(1<<MUX3) & ~(1<<MUX1) & ~(1<<MUX2) & ~(1<<MUX0);

       //Inicia a conversão
	    ADCSRA |= (1<<ADSC);

       //Espera a conversão terminar
	    while(ADCSRA & (1<<ADSC));

       //Atribui o valor lido à variável
	    setpointADC = ADC;

       //Escreve o valor do setpoint na fila
	    xQueueOverwrite(QueueSetpoint, &setpointADC);

       //Libera o semáforo
	    xSemaphoreGive(SemaforoADC);
   }
}

Observe que o primeira instrução dentro da tarefa é aguardar até que o semáforo esteja disponível, só depois é feita a conversão AD de fato. O valor da leitura é escrito na fila e então o semáforo é liberado.

Em seguida, está a tarefa que diz respeito a leitura da posição atual do motor que é muito semelhante a tarefa de controle do setpoint, as únicas alterações são na seleção do canal analógico a ser lido e na fila em que serão escritos os dados, observe:

void TaskPosicaoAtual(void *pvParameters)
{
   (void)pvParameters;
	
   double posicaoAtualADC;
	
   while(1)
   {
       //Solicita o semáforo até conseguir
	    xSemaphoreTake(SemaforoADC, portMAX_DELAY);
	
       //Seleciona o ADC1 (posição atual do motor)	
	    ADMUX |= (1<<MUX0);										
	    ADMUX &= ~(1<<MUX3) & ~(1<<MUX1) & ~(1<<MUX2);

       //Inicia a conversão
	    ADCSRA |= (1<<ADSC);

       //Espera a conversão terminar								
	    while (ADCSRA & (1<<ADSC));
	
       //Atribui o valor lido à variável							
	    posicaoAtualADC = ADC;

       //Escreve o valor da posição atual na fila
	    xQueueOverwrite(QueuePosicaoAtual, &posicaoAtualADC);

       //Libera o semáforo
       xSemaphoreGive(SemaforoADC);							
   }
}

Finalmente, chegamos na tarefa do PID. Acompanhe o código e em seguida sua explicação:

void TaskPID(void *pvParameters)
{
   (void)pvParameters;
	
   //Configuração do PWM
   TCCR2A = 0b00100011; //Fast PWM, não inversor, saída OC2B (PD3)
   TCCR2B = 5;		    //Aproximadamente 500Hz
	
   //Declaração e inicialização das variáveis necessárias
   double   setpoint,
            posicaoAtual,
	         posicaoAnterior = 512,
	         erro = 0,
	         kp = 1.0,
	         ki = 0.001,
	         kd = 0.1,
	         proporcional = 0,
	         integral = 0,
	         derivativo = 0,
	         PID = 0;
	
   while(1)
   {
       //Lê os dados da fila
       xQueueReceive(QueueSetpoint, &setpoint, portMAX_DELAY);
       xQueueReceive(QueuePosicaoAtual, &posicaoAtual, portMAX_DELAY);

       //Cálculo do erro
       erro = setpoint - posicaoAtual;

       //Cálculo dos parâmetros P, I e D
       proporcional = kp*erro;
       integral += ki*erro;
       derivativo = kd*(posicaoAtual - posicaoAnterior);
       PID = proporcional + integral + derivativo;
 
       //Teste para saber para qual lado o motor deve girar
       if(erro > 0)
       {				
            PORTD &= ~(1<<sentido);	//Ajusta o sentido de rotação
            TCCR2A &= ~(1<<COM2B0);	//Desabilita o modo inversor			
       }
       else
       {
            PORTD |= (1<<sentido);	//Ajusta o sentido de rotação
            TCCR2A |= (1<<COM2B0);	//Habilita o modo inversor
       }

       posicaoAnterior = posicaoAtual; //Atualiza a posição anterior
       dutyCicle = PID/4; //Atualiza o ciclo ativo do PWM
   }
}

Inicialmente é feita a configuração do PWM, selecionando a porta que será utilizada e, a princípio, utilizando o modo não inversor. Em seguida são declaradas e inicializadas as variáveis que serão utilizadas para implementação do PID. Em um primeiro momento é recomendado realizar a implementação apenas da parcela proporcional do controlador, ou seja, com Ki e Kd iguais a zero e, à medida que testes são realizados, ir variando esses parâmetros em busca da melhor resposta do sistema na prática.

No loop infinito, o programa lê os dados das filas e então são realizados os calculos do erro e dos parâmetros P, I e D, conforme as fórmulas apresentadas anteriormente nesse artigo. Feito isso, é necessário realizar um teste para saber para qual lado o motor deve rotacionar e então tomar algumas decisões, vejamos:

O conversor AD do ATmega328P possui 10 bits de resolução, isso implica que os valores de setpoint e da posição atual (lidos pelos potenciômetros) podem assumir valores de 0 a 1023. Portanto, sendo a posição 0 o fim de curso no sentido anti-horário e 1023 a posição de fim de curso no sentido horário, sempre que o valor do setpoint for maior que o valor da posição atual (erro > 0) o motor deverá rotacionar no sentido horário, caso contrário (erro < 0) deverá girar no sentido anti-horário. Para implementar isso em nosso código, utilizaremos uma das saídas da ponte H (definida no programa como “sentido”) para controlar o sentido de rotação, e a outra saída (definida como “PWM”) para controlar a velocidade com que o motor gira, dependendo da distância em que a posição atual está do setpoint. No fim das contas, o que se tem é o seguinte:

Modos de funcionamento do servomotor

Quando o erro é menor que zero, o PWM é configurado para trabalhar no modo inversor, ou seja, o valor atribuído para o duty cicle diz respeito ao tempo que o sinal permanecerá em nível baixo. Já quando o erro é maior que zero, o PWM trabalha no modo não inversor, com o duty cicle sendo proporcional ao período em nível alto do sinal PWM. Em ambos os casos, quando o duty cicle (P+I+D) for igual a zero o motor ficará parado, ja que terá a mesma tensão em seus dois terminais.

A última ação a se fazer nessa tarefa é a atualização da variável posição anterior, que passa a ser o valor da última leitura, e a atribuição do duty cicle sendo a variável PID dividida por 4, para efeitos de normalização, já que ela possui 10 bits e o duty cicle possui apenas 8 bits.

Com isso, se dá por finalizado o nosso projeto de controle de servomotor utilizando o algoritmo PID, arduino e o sistema operacional de tempo real FreeRTOS. Clicando aqui, você pode fazer o download dos arquivos desenvolvidos ao longo desse artigo, incluindo um arquivo de simulação.

Obrigado pela atenção!

Referências

Explicando a Teoria PID. Disponível em: <https://www.ni.com/pt-br/innovations/white-papers/06/pid-theory-explained.html>. Acesso em: 18 nov. 2020.

O que é Servo motor e como funciona? Disponível em: <https://www.mundodaeletrica.com.br/o-que-e-servo-motor-e-como-funciona/>. Acesso em: 19 nov. 2020.

Controlador PID digital: Uma modelagem prática para microcontroladores. Disponível em: <https://www.embarcados.com.br/controlador-pid-digital-parte-1/>. Acesso em: 19 nov. 2020.

ATmega328P 8-bit AVR Microcontroller with 32K Bytes In-System Programmable Flash DATASHEET. Disponível em: <https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf>.

Arduino – PinMapping. Disponível em: <https://www.arduino.cc/en/Hacking/PinMapping>. Acesso em: 23 nov. 2020.

Curso de Eletrônica – O que é PWM – Pulse Width Modulation. Disponível em: <http://www.bosontreinamentos.com.br/eletronica/curso-de-eletronica/curso-de-eletronica-o-que-e-pwm-pulse-width-modulation/>. Acesso em: 25 nov. 2020.

Seja o primeiro a comentar

Faça um comentário

Seu e-mail não será publicado.


*