Manual

do

Maker

.

com

PID - Proporcional Integral Derivativa

PID - Proporcional Integral Derivativa

PID; Eu sabia o que queria, mas não conhecia a fórmula. Então, a primeira vez que a vi, fui tomado pelo desânimo, pensando se valeria a pena o esforço. Olhe bem; essa fórmula é boa pra fazer cubo de LED, porque o cara vê e fala:

pid-myOwn-1-300x28.webp

"Ah, deixa quieto, vou fazer um cubo de LED".

Mas acredite em minhas palavras porque é fato: Isso é simples de implementar. E se você é programador, depois que ver o código vai ler essa fórmula como se fosse um comentário.

Se você chegou nesse post por acaso e não sabe de que se trata, vamos ao conceito.

O PID agrega as ações Proporcional, Integral e Derivativa para realizar a tarefa de ajuste fino. Esse ajuste pode ter diversos propósitos como controle de válvulas de pressão (por exemplo, controle das válvulas de plataformas de petróleo), ajuste de temperaturas em ambientes que necessitam de controle e qualquer outro sistema que possa ser empregado esse sistema que trabalha em malha fechada ou aberta (e aí também a fórmula varia), inclusive robótica. Para que seja possível enxergar seu funcionamento, empregaremos aqui em um projeto robótico, mas primeiramente vejamos a função de cada ação.

Proporcional

Diretamente ao assunto, a proporcional é exatamente o que você faria mesmo não sabendo da existência do PID. Basicamente, você tem um valor pretendido. Em momentos de tempo você calcula a diferença entre a leitura e o valor pretendido e aplica a correção. Esse método por sí só já é funcional, mas aplicar o PID permite uma resposta mais precisa e suave.

O robô que fiz estava muito sensível, não achei o centro de gravidade e ele está desequilibrado. Além disso, o primeiro teste foi conectado ao FTDI, que o desequilibra mais ainda. Ainda assim, utilizando apenas a proporcional ele estabilizou ao final desse video. Infelizmente eu desliguei a câmera em seguida, pois eu não esperava que ele atingisse esse ponto, mas veja (ao final também tem um vídeo, calma):

Testes iniciais

Integral

A integral acumula as taxas de erro até que ele possa se aplicar à sua correção e assim essa taxa tenderá a 0, dando uma excelente estabilidade já em apoio à proporcional. Diversos casos são solúveis apenas com PI. Pode parecer estranho acumular indefinidamente, mas o algorítmo se encarrega do tratamento da variável acumuladora, uma vez que ela tenderá a 0. O tendão de Aquiles aqui é um variação repentina para um valor muito acima ou muito abaixo do limítrofe. Essa condição é chamada "windup", vista mais adiante.

Derivativa

A derivada reage de forma proporcional ao sistema, suavizando a saída ainda que haja aumento abrupto de variação. Aplicando uma taxa mais alta da derivada fará com que o sistema reaja com mais impeto quando em resposta ao parâmetro de erro, aumentando a velocidade de resposta. Uma taxa mais baixa trará uma resposta mais suave e normalmente é utilizado assim para evitar instabilidade devido à sua sensibilidade a ruídos.

Ajustes

Eu corto minhas duas orelhas se você conseguir utilizar os mesmos parâmetros que eu. Simplesmente não dá. Um conjunto de variáveis deve ser levada em consideração, como centro de gravidade, torque do motor, tamanho da alavanca (o topo do robô até a altura do eixo), atrito e o que mais for. Por isso, farei uma breve introdução sobre os ajustes.

Li muito, mas muito mesmo sobre PID, métodos de correção e bibliotecas. Se você está lendo para entender PID, certamente você escreverá sua própria biblioteca. Eu criei a minha, mas para ser realmente GENÉRICA. Ela simplesmente calcula e devolve o resultado, o resto deve ser aplicado em outras partes do código seja lá como for. Por isso, para esse robô eu dei uma "garibada" naquela biblioteca do robô que desvia de obstáculos, que escrevi já há algum tempo e que mais uma vez será útil como auxiliadora no controle do motor (porque outras implementações serão feitas sobre esse robô).

Retomando. O ideal é sofrer um pouco para entender o efeito de cada um dos processos sobre a saída resultante. Desse modo, um teste aplicável é deixar os coeficientes Ki e Kd em 0, então aumentar o ganho de Kp até que o loop colapse ou seja percebido variações irregulares. Basicamente, o objetivo da ação proporcional é tornar a resposta tão ágil quanto possível até que esteja exatamente sobre o ponto de oscilações - e assim deve ser deixado. Posteriormente aplica-se o termo integral até que as oscilações sejam corrigidas. Se você não conseguir estabilizar com a integral, então volte a constante integral para 0 e reajuste a ação proporcional.

Nesse ponto tive bastante trabalho porque estava considerando apenas o ajuste da proporcional, mas tratando-se de controle de um motor, existem outras duas variáveis importantíssimas a considerar, que é a velocidade do motor e o intervalo de tempo de resposta.

Velocidade do motor

Minha primeira tentativa foi utilizando um kit de 2 motores com caixa de redução configurável, mas ainda que na velocidade máxima, ele não reagia a contento, então troquei pelo chassi de um carrinho de pilha ordinário, cortando-o e utilizando apenas o eixo traseiro, onde está atrelado o motor. A questão da velocidade tem dois extremos; torque inicial suficiente para reagir à inclinação da estrutura e a segunda é oposta - reação moderada para que não inverta o desequilíbrio - isto é, força demais pode inverter o erro, lançando-o para o lado oposto, tornando assim ineficaz a reação, já que a solução gera outro problema.

A solução não é linear e como fiz "de ouvido", fui aplicando algumas variações entre mínima e máxima do motor, utilizando constrain() invés de map() para interagir com o PWM. Além disso, me preocupei em não colocar muita massa no robô para que não prejudicar o desempenho na correção e no final, ele ficou tão leve que sobrou torque. Quero dizer com isso que o PWM não foi utilizado em toda sua faixa, o que é bom, uma vez que sobrou recurso e isso sinaliza que as coisas vão bem. Por fim, a velocidade mínima não é zero, mas algo em torno de 80, o que não é força suficiente para movimentar o motor agregado ao robô, mas o deixa em ponto de reagir a tempo.

Provavelmente seu motor permanecerá fazendo ruídos constantes e a maior parte do tempo lembrará uma impressora matricial, ainda que aparentemente inerte. Isso é porque a correção será feita o tempo inteiro, pois dificilmente ele permanecerá estável a ponto de não precisar interação, tratando-se do equilíbrio sobre um eixo de um brinquedo. Não se preocupe com os ruídos.

Intervalo entre leituras

Assim como o motor, aqui existem dois extremos opostos. Se o valor for baixo demais, sobrecarregará o processamento e gerará overflow do buffer do MPU6050. Obviamente o sistema colapsará. Em contrapartida, se o valor for alto demais, a reação será tardia, ainda que bem calculada. Por isso é fundamental também testar essa condição, variando para mais ou para menos conforme sua observação. Eu iniciei em 100ms e baixei até 20ms, onde encontrei as melhores respostas na situação atual.

Uma resposta rápida e em determinados momentos abrupta pode ser necessária, mas deve-se cuidar para não fragilizar a ação final pelo excesso da ação integral. Quando estas ações estiverem estabilizadas, não se dê por satisfeito. Inicie o ajuste da ação derivativa para aumentar o ganho, mas como dito anteriormente - cuidado com exageros, porque essa ação torna o sistema altamente sensível a ruídos. Não é errado nem amador aplicar esse tipo de ajuste, faça-o com orgulho. Eu coloquei em 0.2, mas pode ser que eu elimine ou aumente, vou atuar nesse projetinho por um longo tempo ainda. Já o Kd coloquei em 0.5, parece que ficou satisfatório.

Existem mais dois modos populares de ajuste, como o Ziegler-Nichols, similar ao modo citado previamente, mas não vou falar sobre esse ou outros métodos para evitar a possibilidade de errar ao tratar de um assunto do qual não sou especialista e também para não escrever um livro em um artigo.

Outro fator a ser ajustado é o intervalo de leitura. Eu comecei em 100ms, mas por mais ajustes que eu fizesse o robô não se equilibrava porque quando ele ia tomar uma ação o tempo que havia passado já era longo demais. Por fim, o ajuste ideal para mim foi 60ms.

Centro de gravidade

Procure no google por "centro de gravidade de massa uniforme". É um cálculo simples para encontrar o centro de gravidade, mas você pode fazer por observação também, claro. Só que você precisa garantir que haja esse ajuste senão seu robô vai ficar fazendo volta olímpica pra tentar não cair.

Pra finalizar, dependendo da aplicação, o termo integral ou o derivativo podem ser dispensados, como no caso de loop rápido com variações constantes (nosso caso). Siga a leitura e depois veja o vídeo.

Proposta de projeto

O que pode ser mais interessante que um robô que se equilibra em um eixo só, sem cair? Eu não consigo imaginar nada mais legal para utilizar PID, mas infelizmente, não tem como evitar gastos, alguns materiais importantes serão necessários, adapte conforme possível.

Lista de materiais
1 - Arduino Pro Mini 5v
1 - Giroscópio com acelerômetro (tutorial aqui)
1 - Ponte H L293D (tutorial aqui)
1 - Carrinho à pilha cujo motor DC tenha uma engrenagem só ou seja direto no eixo
1 - Bateria 9v
1 - Regulador de tensão 5v "tijolão" ou SMD
1 - Sensor ultrasônico (só no próximo post de upgrade)
1 - Metro de cabo de rede para fazer os jumpers
1 - Mini protoboard (para depois que resolver desmontar o robô, reaproveitar tudo)
1 - Pacote de palitos de sorvete

Adicionalmente, será necessário soldar os slots no Arduino Pro Mini. Eu comprei 2 de 5v e 2 de 3.3v, por isso quis arriscar fazer diferente em um deles, soldando os slots por cima. Não acho que seja errado, mas todo mundo solda os pinos para baixo, faça como preferir, mas nesse caso eu queria enxergar o topo da board e teria que prendê-la contra a caixa de redução se soldasse os pinos para baixo.

Um leigo explicando PID

Existe limite para um maker? Um maker pode tudo, do jeito que quiser e, se der certo, com direito a tutorial. Assim será; não sou um grande matemático, mas vou passar o meu entendimento a respeito.

u(τ)

A saída (P+I+D).

setpoint

Valor pretendido.

delta time (dt)

A diferença entre o tempo da última leitura e o tempo atual.

Integral (0∫τ)

A integral aqui tem a taxa de variação conhecida (entre 0 e τ)

Tunning

O ajuste dos coeficientes Kp, Ki, Kd tem três meios populares para a solução, sendo que duas delas envolvem mais fórmulas matemáticas (Cohen-Coon e Ziegler Nichols). #SomosTodosHobistas, portanto vamos considerar a utilização do terceiro método, que é ajuste "de ouvido" ou, "análise" do comportamento, supracitado.

Transformando a fórmula em código

Nesse momento só peço que não copie essas porções de código de exemplo. Ao final disponibilizo uma biblioteca que escrevi, bem organizada e funcional. Essas porções de código são apenas esclarecimento, se tiver curiosidade de ler o código da biblioteca, entenderá seu funcionamento sem problemas, apenas leia esse post inteiro.

Vou começar citando o que vem depois do sinal de igualdade ("="). A resposta é P+I+D. Indo por partes, iniciemos pela proporcional.

P = Kpe(τ)

E o que isso significa? - Quando fazemos uma leitura digital ou analógica, fazemos a comparação. Por exemplo, um sensor de temperatura. suponhamos que o ambiente seja um CPD que deve permanecer a estáveis 18 graus. Se a constante é 18, então criamos uma constante que guarde esse valor para que seja o setpoint, seguidamente calculamos o erro:

#define SETPOINT 18
#define Kp 1.0
...
float error = 0.0;
float P     = 0.0;
...
error = SETPOINT - map(analogRead(0),0,1023,0,100);
P = Kp * error;

Não é incrível? A proporcional já está implementada. Só isso já serve pra muito casos! Se não entendeu o porquê do modo que declarei variáveis e constantes, sugiro que leia esse post.

Como visto, a diferença de temperatura entre o setpoint e a leitura do sensor é considerada o erro. Se for 0, não haverá erro a somar. Variando para mais ou para menos, então P acumulará esse valor que será somado ao final (P+I+D).

I = Ki 0∫τe(τ)dt

Como citado anteriormente, a integral acumula os erros mínimos até que sejam contabilizáveis pela correção. A integral está entre os dois sinais de soma e no código ele fica assim:

I += Ki * error * dt;

Não tenha pressa; analise profundamente essa complexa linha de código que inclui soma, multiplicação e valores. Deu? Ok, sigamos.

D = Kd(d⁄dt)e(τ)

A derivada precisa de parâmetro pra ser calculada. Para isso, será necessário uma variável que guarde sempre o último valor, para que quando chegar nessa parte o parâmetro para o cálculo da derivativa exista.

float lastValue = 0.0;
...
D = Kd * ((lastValue - value) / dt) * e;

PID

E agora, o resultado PID.

PID = P + I + D; //0 é OK, qualquer coisa diferente precisa de correção.

Tornar o valor aplicável

Temos o cálculo, mas como aplicá-lo ao programa? A primeira etapa é definir de que forma a correção será feita. Tanto para o exemplo anterior de temperatura quanto para o robô, um PWM mapeado deve ser o suficiente - e sério, não me ocorre outra forma de exemplificar.

float controler = 75.0;
...
tunning = PID + controller;

Variação no tempo

Se você pode fazer medições em intervalos de tempo tão longos quanto 1 segundo, certamente você pode utilizar P,I e D. No caso de um robô, não dá pra trabalhar com tempo fixo como 1 segundo, porque as reações variam no tempo. O robô pode "tropeçar" e a correção tem que ser feita imediatamente, não dá pra esperar um espaço de tempo dentro de 1 segundo até que a microcontroladora inicie o cálculo para acionar toda a parte mecânica. Por isso será necessário adicionar o tempo variável controlado através de um método implementado em minha biblioteca.

tempo

O tempo pode ser um valor constante, mas não para o robô, que deve reagir à variação de seu equilibrio, portanto uma variável de tempo deve ser inicializada para guardar o valor passado e esse valor será comparado com o valor presente para calcular a variação. Eu predefini 100ms na lib, mas esse valor poderá ser adequado através de um método modificador chamado em setup().

A medição do tempo é basicamente a variação entre uma medição e outra. Para guardá-lo, criamos um delta time chamado dt como está na fórmula e, uma variável lastTime pra guardar o tempo da última medição.

float dt = 0; //inicializar eh uma boa pratica. Uma hora qualquer justifico.
long lastTime = millis();
...
dt = (millis() - lastTime)/1000.0; //ms convertido pra fracao de seg.
lastTime  = millis();
... 

Agora a variação de tempo na Integral é deltaTime, que está em ms. A Derivativa recebe agora o complemento de tempo:

de        = lastValue - value;
D         = Kd * de / dt;
lastValue = value;

Isso poderia ser implementado diretamente no código principal, mas transformá-lo em um objeto e trabalhar com ele pode ser vantajoso. Portanto, sem mais delongas, vamos à implementação da classe pidIno (não sou bom com nomes, sinto muito) que você encontra aqui no gitHub. Adicionalmente, para controlar os 2 motores do kit Tamiya eu escrevi outra classe utilizada em outro post, mas agora está subutilizada, disponibilizo assim que estiver reorganizada. Enfim, baixe a lib indicada e descomprima dentro do diretório arduino-xxxxx/libraries/ (troque xxxxx pela versão ou remova desde o hífen caso tenha criado um link simbólico ou renomeado o diretório).

Da classe dos motores, a única parte utilizada é essa:

void Brain::balance(int valueFrom, int speedFor)
{
    if (valueFrom > this->setPoint && flag != FRENTE){
        analogWrite(this->motor1P2, LOW);
        analogWrite(this->motor1P1, speedFor);
        flag = FRENTE;

    }
    else if (valueFrom < this->setPoint && flag != RE){
        analogWrite(this->motor1P1, LOW);
        analogWrite(this->motor1P2, speedFor);
        flag = RE;
    }
     else if (flag != PARADO){
        analogWrite(this->motor1P1,LOW);
        analogWrite(this->motor1P2,LOW);
        flag = PARADO;
    }
}

Transforme esse método em uma função fora de loop() e setup() que deverá resolver seu problema.

Implementando

Pra implementar o PID, inclua o código a seguir no sketch do MPU6050:

#include <pidIno.h>


#define MOTOR1_P1 6
#define MOTOR1_P2 5
#define Kp 35.0
#define Ki  0.2
#define Kd  0.5
#define SETPOINT 66.20

int sleeper = 0;
pidIno pid(Kp,Ki,Kd,SETPOINT);

void doPid(float fromSensor){
  bool stat = pid.calculate(fromSensor,SETPOINT); //se for true, pode ler o pid
  if (!stat){
    return;
  }
  float pidVal = pid.getPid();
  int pidMap = pidVal > -1 ? pidVal + SETPOINT : (pidVal * -1) + SETPOINT; 
  int speedMap = constrain(pidMap,80,190);
  motors.balance(pidVal,speedMap);

  Serial.print("PID: ");
  Serial.print(pidVal);
  Serial.print(" Source: ");
  Serial.print(fromSensor);
  Serial.print(" Correction: ");
  Serial.print(fromSensor-pidVal);
  Serial.print(" PWM: ");
  Serial.print(speedMap);
  Serial.println(" ");
}

E em setup(), ajuste o tempo entre leituras (em milisegundos):

pid.setTimerRuler(20);

A configuração do giroscópio foi bico. Usei o código de exemplo e só implementei esse código em cima, mais nada. Escrevi esse post sobre o MPU6050, que acredito ser o melhor (ou um dos melhores) IMU disponível hoje. Lá na parte que está definido o algorítmo a utilizar eu adicionei apenas isso:

#ifdef OUTPUT_READABLE_YAWPITCHROLL
            // display Euler angles in degrees
            mpu.dmpGetQuaternion(&q, fifoBuffer);
            mpu.dmpGetGravity(&gravity, &q);
            mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);

            if (sleeper & 1000){
                sleeper++;
            }
            else{
                doPid(ypr[1] * 180/M_PI);
            }
            
        #endif

Esse delay é necessário pra dar tempo de o sensor estabilizar, senão dá windup de cara. A propósito, vamos falar disso agora.

Windup

Integral Windup se refere a uma condição de grande mudança do valor de setpoint. Isso dá um estouro no cálculo. Alguns ajustes podem contornar a situação:

  • Ajuste adequado do coeficiente integral
  • Incremento da curva do setpoint (também conhecido como "acochambração". Não serve para o nosso caso)
  • Desabilitar a ação integral até que o sistema tenha estabilizado
  • Limitar o acumulo da ação integral de forma a previnir um estouro
  • Pre-analisar a saída da integral para evitar que a saida do PID resulte em um estouro

O sistema ao qual se aplica o PID tem a condição de "ideal" ou "impossível". Isto é, caso o erro seja constante, estará fora de controle em dado momento porque não tem ponto de estabilização e acumulará erro além dos limites, portanto é um sistema impossível. Se você estiver fazendo um robô desses, claro que ele vai variar constantemente, mas haverá pontos de tolerância e nesse limite ele estará equilibrado, chegando fisicamente próximo de 0, ou ocasionalmente em 0. Também, existe um limite para o ângulo, se exceder não tem mesmo como recuperar, então o controle deve ser muito fino pra não exceder esse limite. Além disso, o torque do motor foi limitado pelo PWM. Se você reparar na função doPid(), a velocidade mínima está em 80 (pode ser mais ou menos, dependendo da resistência para sair da inércia, seja a razão massa ou atrito) e limitado à 190 para evitar "coice" da alavanca de retorno. Perceba que isso significa o uso moderado mas suficiente da força para a recuperação da estabilidade.

A teoria na prática funciona de outro jeito

robozin.webp

Esse foi meu primeiro projeto com PID e já tenho um mais ousado, mas infelizmente nao atingi o estado ideal ainda. Aqui identifiquei vários problemas, espero que dicas de mais experientes apareçam:

  • O robô não tem massa, apenas o motor embaixo e a protoboard em cima. Soprando ele já desequilibra.
  • Tive que balancear o robô pelo FTDI e mantê-lo conectado assim, porque desconectar os fios e colocá-los na protoboard para alimentação por bateria mudava o centro de gravidade. Esse é o problema mais sério. Estou pensando em fazer um robô com ESP8266 para poder reconfigurar remotamente, assim fica mais fácil fazê-lo sem cabo.
  • Não tem um ponto de equilibrio nem centro de gravidade.

Devido ao motor estar deslocado sobre o eixo, o ponto de equilibrio não está em 90 graus, aí o robô fica compensando o tempo inteiro.

  • Demora para calibrar o sensor MPU6050.

Mesmo dando um delay inicial, o sensor só estabiliza depois de um tempo, por isso eu inicio o robô tentando mantê-lo o mais próximo possível do centro de gravidade para reduzir as distorções.

  • O robô está tão leve que os cabos pesam.

No vídeo, perceba que estou mantendo os cabos mais ou menos suspensos e, pra mostrar que não estou equilibrando como uma marionete, dou um puxão com o polegar em dado momento para forçar o desequilíbrio.

https://youtu.be/bRAO---BOAk

Siga-nos no Manual do Maker no Facebook.

Inscreva-se no nosso canal Manual do Maker Brasil no YouTube.

Próximo post a caminho!

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.