Robótica

Controle de Corrente em Servomotores

Eletrogate 12 de janeiro de 2023

Introdução

Servomotores como os SG90 não possuem proteção contra sobrecorrente ou sobrecorrente embutidas, podendo queimar se submetidos, por longos períodos, a altos esforços. Para evitar isso, foi desenvolvido um algoritmo que permite o ajuste do sinal enviado ao servo para manter a corrente abaixo de um valor estipulado. Aqui, esse é apresentado como a biblioteca ServoProtegido.h. Para as demonstrações e testes, serão utilizados:

cta_cart

Estes estarão conectados conforme o esquemático abaixo.

Diagrama do sistema montado para demonstrações.

ATENÇÃO: ESTE CIRCUITO NÃO DEVE SER ENERGIZADO SE O CÓDIGO CARREGADO NA PLACA UNO REALIZAR LEITURAS ANALÓGICAS SEM QUE A FUNÇÃO analogReference(EXTERNAL) TENHA SIDO EXECUTADA.

Quando montado em uma protoboard, todos os jumpers de alimentação, incluindo os dos servos, devem estar trançados em pares (cada positivo trançado com seu negativo). Estes devem ser mantidos distantes dos jumpers de sinais. Os resistores de potência devem estar distantes um do outro e do ampop. A fonte utilizada para alimentar os servos deve ser distinta da utilizada para alimentar a Uno.

Obs. I: Os resistores devem ser escolhidos para atender, simultaneamente:

  • A distorção no sinal de saída do Amp-Op esteja dentro da faixa aceitável;
  • Seja Imax a maior corrente que pode vir a circular pelo servo (não a estimada no código, mas a corrente máxima do servo), Rp a resistência do resistor de potência (neste caso, 0.1 ohm), Vcc a tensão de alimentação do circuito de sinais e R1, R2, R3 e R4 destacados no diagrama:

O Problema

O código abaixo não utiliza a biblioteca, somente monitora os sensores.

#include <Servo.h>

Servo servo1, servo2, servo3, servo4;               //declara os objetos referentes aos servos que serão controlados
Servo servos[] = {servo1, servo2, servo3, servo4};  //inicializa um vetor com estes objetos

int filtro(int sensor)  {
  static const uint8_t tamFiltro = 128;                       //define o tamanho do vetor de amostras para cada entrada
  static const uint8_t entradas[] = {A0, A1, A2, A3, A4, A5}; //inicializa o vetor de entradas analogicas
  static unsigned medicoes[sizeof(entradas)][tamFiltro];      //declara a matriz com os vetores de amostra
  unsigned long soma = 0;                                     //inicia soma com 0
  static uint8_t i;                                           //declara o iterador
  for(i = 0; i < tamFiltro - 1; medicoes[sensor][i] = medicoes[sensor][i + 1], i ++)  //de 0 a tamFiltro - 2, deslocando cada valor para o endereço anterior a cada iteração, iterar de 1 em 1
    soma += medicoes[sensor][i];                                                      //a cada iteração, acumular o valor do respectivo endereço
  soma += (medicoes[sensor][tamFiltro - 1] = analogRead(entradas[sensor]));           //substituir o ultimo valor pela leitura do sensor e acumular esta
  return soma / tamFiltro;                                                            //retornar o valor acumulado divido pelo tamanho do vetor de amostras
}

void setup()  {
  analogReference(EXTERNAL);            //seleciona a tensão no pino AREF como referência para as leituras 
  Serial.begin(115200);                 //inicia a interface UART
  const int saidas[] = {11, 10, 9, 6};  //inicializa o vetor com os pinos referentes às saídas de sinal para os servos
  for(uint8_t i = 0; i < 4; i ++)       //de 0 a 3, incrementando 1 a 1
    (servos[i]).attach(saidas[i]);      //conectar cada saída ao respectivo servo
}

void loop() {
  static uint8_t q;                 //declara o iterador
  static unsigned valorFiltrado[6]; //declara o vetor com as leituras filtradas
  for(q = 0; q < 4; q ++) {                                 //de 0 a 3, incrementando 1 a 1
    valorFiltrado[q] = map(filtro(q), 0, 1023, 500, 2500);  //armazena, em cada posição do vetor, a leitura filtrada e mapeada para a faixa da respectiva saída
    (servos[q]).writeMicroseconds(valorFiltrado[q]);        //define o sinal enviado para o respectivo servo
  }

  static unsigned long tempo;   //declara a variavel auxiliar para o controle das mensagens
  valorFiltrado[4] = filtro(4); //armazena cada leitura
  valorFiltrado[5] = filtro(5); //em uma posição do vetor
  if(millis() - tempo > 100) {  //a cada 100 milisegundos
    Serial.print(",usBase/2:"); Serial.println(valorFiltrado[0]/2);  //registra o sinal enviado para os servos
    Serial.print(",usGarra/2:"); Serial.println(valorFiltrado[3]/2); //divido por 2, para melhor visualização
    Serial.print(",sensorBase:"); Serial.println(valorFiltrado[4]);  //registra o valor lido
    Serial.print(",sensorGarra:"); Serial.println(valorFiltrado[5]); //pelos respectivos sensores
    tempo = millis(); //atualiza a variavel auxiliar
  }
}

Executando-o, tem-se o resultado visto no vídeo abaixo. Como não há ajustes automáticos em prol da redução da corrente consumida, se o operador mantiver os potenciômetros em posições que forcem os servos, a corrente de cada um é mantida em valores que podem o danificar rapidamente.


A Biblioteca

As classes e estruturas de ServoProtegido, assim como o uso interface de seus métodos, são explicados abaixo. Seus arquivos podem ser baixados de nosso repositório.

A classe EntradaAnalogica, como o nome sugere, visa abstrair as entradas analógicas do micro. Seus métodos são:

  • EntradaAnalogica(const uint8_t pino): declara um objeto correspondente à entrada pino e com tamanho padrão para o filtro;
  • EntradaAnalogica(const uint8_t pino, const uint8_t _tamFiltro): declara um objeto correspondente à entrada pino e com tamanho _tamFiltro para o filtro;
  • ~EntradaAnalogica(): libera o espaço alocado para o filtro;
  • void setPino(const uint8_t pino): altera a entrada pela qual as leituras são feitas;
  • uint8_t getPino(): informa a entrada pela qual as leituras são feitas;
  • unsigned medeComFiltro(): realiza uma leitura e retorna a média das últimas _tamFiltro leituras.

A classe ServoProtegido herda todos os membros da Servo e acrescenta a camada de proteção. Seus métodos são:

  • ServoProtegido(const uint8_t pinoSensor): declara um objeto que abstrai a proteção contra sobrecorrente de um servo e define que a leitura da corrente deste servo será feita pelo pinoSensor;
  • ~ServoProtegido(): libera o espaço alocado para a EntradaAnalogica referente à leitura do sensor deste servo;
  • void setLimiteSaida(const bool extremo, const unsigned novoLimite): se extremo for ServoProtegido::inferior, novoLimite passa a ser o menor valor que o sinal gerado automaticamente pode assumir. Se for ServoProtegido::superiornovoLimite passa a ser o maior valor que o sinal gerado automaticamente pode assumir;
  • unsigned getLimiteSaida(const bool extremo): informa o valor limite definido para o extremo extremo da saída;
  • void setSensorMax(const unsigned novoMax): define o valor a partir do qual as leituras do sensor levam ao controle automático do sinal de saída;
  • unsigned getSensorMax(): informa o valor a partir do qual as leituras do sensor levam ao controle automático do sinal de saída;
  • void setMargemEntradas(const uint8_t novaMargem): novaMargem é utilizado como referência em duas computações, principalmente:
    • o controle automático do sinal de saída tenta reduzir a leitura do sensor a sensorMax - novaMargem;
    • o controle da saída volta a ser manual somente quando a diferença entre o valor automático e o manual for maior do que novaMargem na oposta à que aumenta o sinal do sensor;
  • uint8_t getMargemEntradas(): informa o valor definido para a margem das entradas;
  • void setPasso(const uint8_t novoPasso): define o valor base para a variação do sinal de saída automático a cada computação do controle. Quanto maior, mais rápido o ajuste é feito, mas maior a probabilidade da posição do servo ser alterada mais do que deveria;
  • uint8_t getPasso(): informa o valor base para a variação do sinal de saída automático a cada computação do controle;
  • void setIntervaloControle(const unsigned novoIntervalo): define o período mínimo entre as computações referentes ao controle do servo;
  • unsigned getIntervaloControle(): informa o período mínimo entre as computações referentes ao controle do servo;
  • struct DetalhesExecucao controlaServo(const unsigned usManual): processa, primeiro, o sinal lido pelo sensor da corrente do servo. A depender do valor desta, processa usManual e envia este ou o valor automático para o servo. Retorna uma struct que contém as principais informações pertinentes ao controle:
    • unsigned ultimaLeitura: a leitura feita pelo sensor de corrente nesta computação;
    • uint8_t usUsado: qual sinal foi escolhido pelo controle, DetalhesExecucao::manual ou DetalhesExecucao::automatico;
    • unsigned valorUs: o valor do sinal escolhido;

Algoritmo com Proteção

Este código utiliza os principais recursos desta biblioteca.

#include <ServoProtegido.h>

Servo servoBraco, servoAntebraco;             //declara os objetos referentes aos servos sem proteção
ServoProtegido servoGarra(A5), servoBase(A4); //declara os objetos referentes aos servos protegidos

void setup()  {
  analogReference(EXTERNAL);  //seleciona a tensão no pino AREF como referência para as leituras
  Serial.begin(115200);       //inicia a interface UART
  servoBraco.attach(10);      //conecta cada
  servoAntebraco.attach(9);   //servo à
  servoGarra.attach(6);       //respectiva
  servoBase.attach(11);       //saída
}

void loop() {
  static EntradaAnalogica potenciometro0(A0), potenciometro1(A1),   //declara os objetos referentes
                            potenciometro2(A2), potenciometro3(A3); //às entradas analógicas
  static struct DetalhesExecucao detalhes[2];                       //declara as estruturas referentes aos resultados do controle dos servos
  servoBraco.writeMicroseconds(494 + potenciometro0.medeComFiltro());           //envia, para o servo, a leitura do
  servoAntebraco.writeMicroseconds(988 + potenciometro1.medeComFiltro());       //potenciometro somada a um valor de compensação
  detalhes[0] = servoGarra.controlaServo(988 + potenciometro2.medeComFiltro()); //passa o sinal que seria enviado pela função de controle, que avalia
  detalhes[1] = servoBase.controlaServo(988 + potenciometro3.medeComFiltro());  //a possibilidade de o enviar ao servo e registra os resultados do processo

  static unsigned long tempo; //declara a variavel auxiliar para o controle das mensagens
  if(millis() - tempo > 100) {
    Serial.print(",usGarra/2:"); Serial.println(detalhes[0].valorUs/2);       //escreve o sinal enviado para os servos
    Serial.print(",usBase/2:"); Serial.println(detalhes[1].valorUs/2);        //divido por 2, para melhor visualização
    Serial.print(",sensorGarra:"); Serial.println(detalhes[0].ultimaLeitura); //escreve o valor lido
    Serial.print(",sensorBase:"); Serial.println(detalhes[1].ultimaLeitura);  //pelos respectivos sensores
    tempo = millis(); //atualiza a variavel auxiliar
  }
}

Executando-o, tem-se o resultado visto no vídeo abaixo. A leitura escolhida como máxima do sensor ainda leva a algum aquecimento, mas muito menor.


Conclusão

A biblioteca explorada neste post implementa o básico do controle proposto. Há possibilidade de muita melhoria, tanto em performance, quanto em legibilidade, o que é incentivado. Além disso, há outros recursos de segurança que podem ser implementados, como a proteção contra sobreaquecimento com base em um sensor de temperatura.


Referências

Créditos à Lorena pela edição dos vídeos. Obrigado, Lorena!


Sobre o Autor


Eduardo Henrique

Formado técnico em mecatrônica no CEFET-MG, atualmente estuda Engenharia de Controle e Automação na UFMG.


Eletrogate

12 de janeiro de 2023

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!

Eletrogate Robô

Assine nossa newsletter e
receba  10% OFF  na sua
primeira compra!