Manufaturação industrial
Internet das coisas industrial | Materiais industriais | Manutenção e reparo de equipamentos | Programação industrial |
home  MfgRobots >> Manufaturação industrial >  >> Manufacturing Technology >> Processo de manufatura

Truques para controlar motores CC

Componentes e suprimentos

Arduino Due
Na verdade, você pode usar qualquer placa Arduino.
× 1
Makeblock Me TFT LCD
Este é um componente opcional. Você pode usar outro tipo de monitor ou preferir não usar nenhum.
× 1
Resistor 4,75 k ohm
Os resistores podem variar dependendo de sua implementação.
× 4
Potenciômetro rotativo (genérico)
3 deles são opcionais (apenas para ajustar os coeficientes do controlador).
× 4
Transistor de uso geral PNP
Os transistores a serem usados ​​podem variar dependendo de sua implementação.
× 4
Motor DC (genérico)
× 1
Slide Switch
Isso é usado para seleção de direção.
× 1
Sensor fotoelétrico de velocidade HC-020K
× 1
Breadboard (genérico)
× 1
Fios de jumpers (genérico)
× 1

Aplicativos e serviços online

Arduino IDE

Sobre este projeto


Controle de velocidade e direção de motores CC por controlador PID e saídas PWM

Introdução

Quase em todos os projetos disponíveis, os criadores gostariam de controlar as velocidades e a direção do motor juntos, mas preferem enviar PWM diretamente para os motores CC, mesmo por meio de um circuito de controle do motor. Mas esse método sempre falha se você precisa combinar a velocidade exatamente como você deseja por causa de irmãos gêmeos chamados de “atrito” e “inércia”.

(Por favor, nunca culpe os gêmeos. O que quer e quando você quiser realizar algumas ações com ou sem algo, os gêmeos imediatamente vêm e agem apenas para ajudá-lo a manter tudo sob controle. Enquanto a inércia permite que as coisas “pensem” antes de agir, O atrito limita a aceleração e a velocidade. E "potência" é "nada" se não estiver sob controle total.)

Portanto, se você tentar controlar a velocidade de um motor diretamente enviando sua entrada como sinal PWM para a saída, a velocidade real nunca deverá atingir seu ponto de ajuste e haverá uma diferença significativa (erro), como pode ser visto na imagem acima. Aqui, precisamos de outra forma, que é chamada de “controle PID”.

Controlador PID

O que é controle PID? Imagine como dirigir seu carro:para começar a mover-se da parada total, você tem que pisar no pedal do acelerador mais do que durante o cruzeiro normal. Durante o movimento a uma velocidade (quase) constante, você não precisa pisar muito no pedal do acelerador, mas apenas recupera a perda de velocidade quando necessário. Além disso, você o libera ligeiramente se a aceleração for superior às suas necessidades. Esta também é a forma de "direção eficiente".

Portanto, o controlador PID faz exatamente o mesmo:o controlador lê a diferença “Sinal de erro (e)” entre o ponto de ajuste e a saída real. Possui 3 componentes diferentes denominados “Proporcional”, “Integral” e “Derivado”; então o nome do controlador aparece após a primeira letra de cada um. O componente proporcional simplesmente define a inclinação (aceleração) da saída do controlador em relação ao sinal de erro real. A parte integral soma os sinais de erro no tempo para minimizar o erro final. E o componente Derivativo observa a aceleração do sinal de erro e coloca um “ajuste”. Não vou dar aqui mais e mais detalhes, por favor, pesquise na internet para mais informações se você tiver interesse.

No meu programa Arduino, o controlador PID é escrito como uma função mostrada abaixo:
  float controladorPID (float _E, float _Eprev, float _dT, float _Kp, float _Ki, float _Kd) {float P, I, D; / * Fórmula de base:U =_Kp * (_E + 0,5 * (1 / _Ki) * (_ E + _Eprev) * _ dT + _Kd * (_ E-_Eprev) / _ dT); * / P =_Kp * _E; / * Componente proporcional * / I =_Kp * 0,5 * _Ki * (_E + _Eprev) * _dT; / * Componente Integral * / D =_Kp * _Kd * (_E-_Eprev) / _dT; / * Componente derivado * / return (P + I + D);}  

Então, o valor de saída final é determinado simplesmente adicionando o valor de saída atual e a saída do controlador PID. É a seguinte seção do programa principal, juntamente com o cálculo do Sinal de Erro e Saída do Controlador PID:
  / * Sinal de erro, saída do controlador PID e saída final (PWM) para o motor * / E =RPMset - RPM; float cPID =controladorPID (E, Eprev, dT, Kp, Ki, Kd); se ( RPMset ==0) OutputRPM =0; caso contrário, OutputRPM =OutputRPM + cPID; if (OutputRPM <_minRPM) OutputRPM =_minRPM;  



Circuito de alimentação do motor DC

Claro que nunca é recomendado acionar um motor DC diretamente da saída do Arduino ou placa de controle semelhante. Os motores DC precisam de uma quantidade significativa de corrente em comparação com aqueles que não podem ser fornecidos pelas saídas das placas do controlador. Portanto, você precisa acionar as bobinas de relé. Mas aqui surge outro problema:os relés têm peças mecânicas e podem falhar a médio ou longo prazo. Precisamos de outro componente aqui, transistores.

Na verdade, os motores DC são movidos por corrente, não por tensão. Então, usando esse princípio, decidi usar transistores. Mas você tem que pegar o transistor correto, capaz de suportar a corrente do motor. Em primeiro lugar, opere o motor diretamente conectando-o à fonte de alimentação e meça a corrente nas condições máximas de operação ou consulte as especificações do fabricante.

Depois de fazer isso, decidi usar quatro transistores de junção bipolar PNP BC307A em uma "ponte" para determinar a direção da corrente através das bobinas do motor (na verdade, um conjunto NPN BC337 funcionaria melhor devido à capacidade de suportar correntes de coletor significativamente mais altas, mas não t tê-los no momento).

Uma vez que as correntes do motor devem ser passadas através do caminho Emissor-Coletor do transistor, é necessário usar transistores com aproximadamente os mesmos coeficientes de ganho de corrente CC (hfe). Para verificar isso, você pode usar o seguinte circuito e coletar transistores que fornecem aproximadamente a mesma leitura de corrente no amperímetro. Para projetar esses circuitos preliminares, você deve considerar o seguinte:
  • Encontre “ Base-Emitter On-Voltage ”( VBEon ) do transistor. É a tensão mínima a ser aplicada à base para ligar o transistor.
  • Encontre um típico “ Ganho de corrente DC ”( hfe ) do transistor em torno da corrente do coletor próximo à corrente do motor. Normalmente é a proporção entre Corrente de coletor ( IC ) e Base atual ( IB ), hfe =IC / IB .
  • Encontre “ Corrente máxima contínua do coletor ”De transistores ( ICmax ) A corrente DC do motor nunca deve exceder este valor em termos de valores absolutos. Posso usar o BC307, pois o motor que uso precisa de 70 mA, enquanto o transistor tem ICmax (abs) =100 mA.





Agora você pode determinar o valor do resistor a ser conectado à Base:Em primeiro lugar, você deve considerar as limitações da saída da sua placa controladora e tentar manter a corrente da Base o mínimo possível (por isso é recomendado selecionar o Ganho de Corrente DC dos transistores como máximo possível. Pegue a classificação de tensão de saída na placa do controlador como “ Tensão de acionamento ”( VT ) e encontre a base de corrente necessária ( IBreq ) dividindo a corrente do motor ( IM ) para ganho de corrente DC ( hfe ) do transistor: IBreq =IM / hfe .

Em seguida, determine a tensão a cair no resistor ( VR ), subtraindo Base-Emitter On-Voltage ( VBEon ) de Trigger Voltage : VR =VT - VBEon .

Finalmente, divida a tensão a ser reduzida em Resistor ( VR ) para Corrente de base necessária ( IBreq ) para encontrar Valor do resistor ( R ): R =VR / IBreq .

[Formulação combinada: R =(VT - VBEon) * hfe / IM ]

No meu caso:
  • Corrente do motor:IM =70 mA
  • Parâmetros BC307A:ICmax =100 mA, hfe =140 (medi aproximadamente), VBEon =0,62 V
  • Tensão do gatilho:VT =3,3 V (saída PWM do Arduino Due)
  • R =5360 ohm (então decidi usar 4900 ohms feitos por um 2K2 e 2K7, para garantir a cobertura total da faixa de RPM e o circuito suga ~ 0,6 mA da saída PWM - um design adequado).

Reverter direção e observações importantes

Para reverter a direção de um motor DC, é o suficiente para reverter o fluxo de corrente. Para fazer isso, podemos simplesmente fazer um circuito em ponte com quatro conjuntos de transistores. No esquema; A saída PWM nº 2 ativa T1A e T1B enquanto a saída PWM nº 3 ativa T2A e T2B, de modo que a corrente que passa pelo motor é alterada.

Mas aqui temos que considerar outra questão:quando são iniciados, os motores elétricos sugam uma corrente de inicialização transitória significativamente mais alta do que a corrente nominal que você lê durante a operação normal / contínua (os fabricantes fornecem apenas correntes nominais). A corrente de inicialização pode ser de aproximadamente 130% da nominal para motores de pequena potência e aumenta dependendo da potência do motor. Portanto, se você alimentar o motor diretamente de uma fonte de tensão e inverter imediatamente a polaridade durante a operação, o motor suga níveis extremos de corrente, uma vez que não está totalmente parado. Finalmente, isso pode resultar na explosão da fonte de alimentação ou na queima das bobinas do motor. Isso pode não ser tão importante e sentido para motores de potência muito pequenos, mas se torna importante se os níveis de potência em que você está trabalhando aumentam. Mas se você estiver ligando o motor por meio de um transistor ou um conjunto de transistores (como o Darlington Couple), você não terá esse problema, pois os transistores já limitam a corrente.

De qualquer forma, considerei uma pequena rotina no programa:quando a seleção de direção é alterada durante a execução, o programa leva ambas as saídas de comando a zero no início e espera o motor até a parada total. Em seguida, ele termina sua tarefa e devolve todo o controle à rotina principal.
  if (Direction! =prevDirection) {/ * Matando ambas as saídas PWM para o motor * / analogWrite (_chMotorCmdCCW, 0); analogWrite (_chMotorCmdCW, 0); / * Espere até que a velocidade do motor diminua * / do {RPM =60 * (float) readFrequency (_chSpeedRead, 4) / _ DiscSlots; } enquanto (RPM> _minRPM); }  

Leitura de velocidade

No meu aplicativo, usei um sensor de velocidade HC-020K barato. Ele envia pulsos no nível de sua tensão de alimentação, e a folha de dados informa que a tensão de alimentação é 5V. Porém minha placa é Arduino Due e não pode aceitá-la. Então, eu liguei diretamente da saída de 3,3 V do Due e, sim, funcionou. E a função a seguir é escrita para ler a frequência e a saída do HC-020K.
  int readFrequency (int _DI_FrequencyCounter_Pin, float _ReadingSpeed) {pinMode (_DI_FrequencyCounter_Pin, INPUT); byte _DigitalRead, _DigitalRead_Previous =0; longo sem sinal _Time =0, _Time_Init; float _Frequency =0; if ((_ReadingSpeed ​​<=0) || (_ReadingSpeed> 10)) return (-1); senão {_Time_Init =micros (); faça {_DigitalRead =digitalRead (_DI_FrequencyCounter_Pin); if ((_DigitalRead_Previous ==1) &&(_DigitalRead ==0)) _Frequency ++; _DigitalRead_Previous =_DigitalRead; _Time =micros (); } while (_Time <(_Time_Init + (1000000 / _ReadingSpeed))); } return (_ReadingSpeed ​​* _Frequency); }  

Observe que a roda do HC-020K tem 20 slots, basta ler a frequência que deve ser dividida por 20 para obter revoluções por segundo como frequência. O resultado deve ser multiplicado por 60 para obter RPM.
  RPM =60 * (float) readFrequency (_chSpeedRead, 4) / _DiscSlots;  

Touch-Ups Gráficos

Para exibir as entradas e os resultados, usei um Makeblock Me TFT LCD e escrevi a função CommandToTFT () para enviar comandos a ele. Na verdade, o motivo desta função é apenas alterar o ponto de conexão serial apenas em uma única linha no programa, quando necessário.

As funções Cartesian_Setup (), Cartesian_ClearPlotAreas () e Cartesian_Line () são escritas para preparar a área de plotagem gráfica, limpar a área de plotagem quando alcançada ao final do eixo horizontal (aqui é o "tempo") e gráficos de plotagem, respectivamente. Consulte o manual Makeblock Me TFT LCD para obter mais detalhes se você se interessar pelas funções gráficas aqui, porque não as explicarei aqui, uma vez que, na verdade, estão fora do escopo deste blog.

Finalizando

Aqui, copio o programa com e sem funções gráficas separadamente, para que você possa revisar a Implementação do Controle de Velocidade e Direção independentemente ou com gráficos. Além disso, nos Códigos você encontrará mais explicações sobre as funções programadas.

Finalmente, normalmente você não pode diminuir a velocidade de um motor DC abaixo de 10-20% de sua velocidade nominal, mesmo sem nenhuma carga. No entanto, é possível diminuir para quase 5% usando o controle PID para um motor CC descarregado, uma vez que é iniciado.

Editar (25.Fev.2018): Se você gostaria de usar transistores NPN em vez de PNP, você deve considerar também o fluxo de corrente reversa em ambos os tipos. Quando acionado (ligado), a corrente flui do Emissor para o Coletor nos transistores PNP, mas é inversa para os tipos NPN (do Coletor para o Emissor). Conseqüentemente, os transistores PNP são polarizados como E (+) C (-) e para NPN deve ser C (+) E (-).


Código

  • Código com touch-ups gráficos
  • Código sem touch-ups gráficos
Código com Touch-Ups Gráficos Arduino
 / * ###################################################### ## Constantes de cor para Makeblock Me TFT LCD ######################################################################################################################## ####### * / # define _BLACK 0 # define _RED 1 # define _GREEN 2 # define _BLUE 3 # define _YELLOW 4 # define _CYAN 5 # define _PINK 6 # define _WHITE 7 / * ######### ################################################# Atribuições de E / S ####### ################################################### * / int _chSpeedSet =A0, // Velocidade setpoint _chKp =A1, // Leitura do coeficiente proporcional para o controlador PID _chKi =A2, // Leitura do coeficiente integral para o controlador PID _chKd =A3, // Leitura do coeficiente derivativo para o controlador PID _chMotorCmdCCW =3, // Saída PWM para o motor para o contador giro no sentido horário _chMotorCmdCW =2, // Saída PWM para o motor para giro no sentido horário _chSpeedRead =24, // Leitura da velocidade _chDirection =25; // Leitura do seletor de direção / * ##################################################### #### Outras constantes ###################################################### ### * / # define _minRPM 0 // RPM mínimo para iniciar a mudança de direção # define _maxRPM 6000 // Limite máximo de RPM # define _Tmax 90 // Limite de tempo máximo para representar graficamente # define _DiscSlots 20 // Quantidade de slots no disco de índice / * ############################################### Variáveis ​​globais ############################################### */Corda Cartesian_SetupDetails; boolean Direction, prevDirection; // Configurações de alarmefloat RALL =500,0, RAL =1000,0, RAH =4000,0, RAHH =4500,0; float Seconds =0,0, prevSeconds =0,0, prevRPM =0,0, prevRPMset =0,0, RPM =0,0, RPMset =0,0 0,0, OutputRPM =0,0, Kp =0,0, Ki =0,0, Kd =0,0, Kpmax =2,0, Kimax =1,0, Kdmax =1,0, E =0,0, Eprev =0,0, dT =1,0; / * ####### #################################################### CommandToTFT (TFTCmd) Comando Função para Makeblock Me Parâmetros de entrada TFT LCD:(String) TFTCmd:String de comando ################################################################################################################### ########### * / void CommandToTFT (String TFTCmd) {/ * Conexão serial usada para exibição * / Serial1.println (TFTCmd); delay (5);} / * ############ Fim de CommandToTFT () ############# * // * ############ ########################################## * // * ############### ############################################### Cartesian_Setup (Xmin, Xmax, Ymin, Ymax, Window_X1, Window_Y1, Window_X2 , Window_Y2, MinDashQty, ColorF, ColorX, ColorY) Função de desenho do eixo XY cartesiano para Makeblock Me Parâmetros de entrada TFT LCD:(float) Xmin, Xmax, Ymin, Ymax:valores do intervalo do eixo (int) Window_X1, Window_Y1___:Canto superior esquerdo de janela do gráfico (int) Window_X2, Window_Y2___:Canto inferior direito da janela do gráfico (int) MinDashQty_____________:Quantidade de traços no eixo mais curto (int) CorB, CorX, CorY:Cores de desenho para uso de quadros, eixos X e Y função externa CommandToTFT (). ################################################################################################################ ### * / String Cartesian_Setup (float Xmin, float Xmax, float Ymin, float Ymax, int Window_X1, int Window_Y1, int Window_X2, int Window_Y2, int MinDashQty, int ColorF, int ColorX, int ColorY) {/ * Limitações da tela * / const int Display ResolutionX =319, DisplayResolutionY =239; / * Limitar Strings de Título * / String XminTxt; if (abs (Xmin)> =1000000000) XminTxt ="X =" + String (Xmin / 1000000000) + "G"; else if (abs (Xmin)> =1000000) XminTxt ="X =" + String (Xmin / 1000000) + "M"; else if (abs (Xmin)> =1000) XminTxt ="X =" + String (Xmin / 1000) + "K"; else XminTxt ="X =" + String (Xmin); String XmaxTxt; if (abs (Xmax)> =1000000000) XmaxTxt ="X =" + String (Xmax / 1000000000) + "G"; else if (abs (Xmax)> =1000000) XmaxTxt ="X =" + String (Xmax / 1000000) + "M"; else if (abs (Xmax)> =1000) XmaxTxt ="X =" + String (Xmax / 1000) + "K"; else XmaxTxt ="X =" + String (Xmax); String YminTxt; if (abs (Ymin)> =1000000000) YminTxt ="Y =" + String (Ymin / 1000000000) + "G"; else if (abs (Ymin)> =1000000) YminTxt ="Y =" + String (Ymin / 1000000) + "M"; else if (abs (Ymin)> =1000) YminTxt ="Y =" + String (Ymin / 1000) + "K"; senão YminTxt ="Y =" + String (Ymin); String YmaxTxt; if (abs (Ymax)> =1000000000) YmaxTxt ="Y =" + String (Ymax / 1000000000) + "G"; else if (abs (Ymax)> =1000000) YmaxTxt ="Y =" + String (Ymax / 1000000) + "M"; else if (abs (Ymax)> =1000) YmaxTxt ="Y =" + String (Ymax / 1000) + "K"; senão YmaxTxt ="Y =" + String (Ymax); / * Limites * / int XminPx =Window_X1 + 1; int XmaxPx =Window_X2-1; int YmaxPx =Window_Y1 + 1; int YminPx =Window_Y2-1; / * Origem * / int OriginX =XminPx + (int) ((XmaxPx - XminPx) * abs (Xmin) / (abs (Xmax) + abs (Xmin))); int OriginY =YmaxPx + (int) ((YminPx - YmaxPx) * abs (Ymax) / (abs (Ymax) + abs (Ymin))); / * Frame * / CommandToTFT ("BOX (" + String (Window_X1) + "," + String (Window_Y1) + "," + String (Window_X2) + "," + String (Window_Y2) + "," + String ( ColorF) + ");"); / * X Axis * / CommandToTFT ("PL (" + String (Window_X1 + 1) + "," + String (OriginY) + "," + String (Window_X2-1) + "," + String (OriginY) + " , "+ String (ColorX) +"); "); / * Eixo Y * / CommandToTFT ("PL (" + String (OriginX) + "," + String (Window_Y1 + 1) + "," + String (OriginX) + "," + String (Window_Y2-1) + " , "+ String (ColorY) +"); "); / * Dashing:A quantidade mínima de traços é fornecida por "MinDashQty" e será tracejada no lado do eixo mais curto em relação à origem. Nas outras seções, os traços a serem marcados devem ser determinados considerando a proporção para o lado do eixo mais curto. * / / * Traço * / int XlengthLeft =abs (XminPx-OriginX); int XlengthRight =abs (XmaxPx-OriginX); int YlengthLower =abs (YminPx-OriginY); int YlengthUpper =abs (YmaxPx-OriginY); int XlengthLeft_Mod, XlengthRight_Mod, YlengthLower_Mod, YlengthUpper_Mod; if (XlengthLeft <=1) XlengthLeft_Mod =32767; else XlengthLeft_Mod =XlengthLeft; if (XlengthRight <=1) XlengthRight_Mod =32767; else XlengthRight_Mod =XlengthRight; if (YlengthLower <=1) YlengthLower_Mod =32767; else YlengthLower_Mod =YlengthLower; if (YlengthUpper <=1) YlengthUpper_Mod =32767; else YlengthUpper_Mod =YlengthUpper; int MinAxisLength =min (min (XlengthLeft_Mod, XlengthRight_Mod), min (YlengthLower_Mod, YlengthUpper_Mod)); int XdashesLeft =MinDashQty * XlengthLeft / MinAxisLength; int XdashesRight =MinDashQty * XlengthRight / MinAxisLength; int YdashesLower =MinDashQty * YlengthLower / MinAxisLength; int YdashesUpper =MinDashQty * YlengthUpper / MinAxisLength; int DashingInterval =2; // Min.interval btw.dashes / * X-Dash L * / DashingInterval =(int) (XlengthLeft / XdashesLeft); if (! (DashingInterval <2)) for (int i =OriginX; i> =XminPx; i- =DashingInterval) CommandToTFT ("PL (" + String (i) + "," + String (OriginY-2) + " , "+ String (i) +", "+ String (OriginY + 2) +", "+ String (ColorX) +"); "); / * X-Dash R * / DashingInterval =(int) (XlengthRight / XdashesRight); if (! (DashingInterval <2)) for (int i =OriginX; i <=XmaxPx; i + =DashingInterval) CommandToTFT ("PL (" + String (i) + "," + String (OriginY-2) + ", "+ String (i) +", "+ String (OriginY + 2) +", "+ String (ColorX) +"); "); / * Y-Dash-L * / DashingInterval =(int) (YlengthLower / YdashesLower); if (! (DashingInterval <2)) for (int i =OriginY; i <=YminPx; i + =DashingInterval) CommandToTFT ("PL (" + String (OriginX-2) + "," + String (i) + ", "+ String (OriginX + 2) +", "+ String (i) +", "+ String (ColorY) +"); "); / * Y-Dash-U * / DashingInterval =(int) (YlengthUpper / YdashesUpper); if (! (DashingInterval <2)) for (int i =OriginY; i> =YmaxPx; i- =DashingInterval) CommandToTFT ("PL (" + String (OriginX-2) + "," + String (i) + " , "+ String (OriginX + 2) +", "+ String (i) +", "+ String (ColorY) +"); "); / * Calculando coordenadas para exibir os valores do ponto final do eixo * / int XminTxtX =Window_X1 - (int) (XminTxt.length () * 6) - 1, XminTxtY =OriginY, XmaxTxtX =Window_X2 + 1, XmaxTxtY =OriginY, YminTxtX =OriginX, YminTxtY =Window_Y2 + 1, YmaxTxtX =OriginX, YmaxTxtY =Window_Y1 - 12-1; / * Controles:Se qualquer coordenada for -1, ela deve cair além dos limites de exibição e o respectivo valor não deve ser exibido * / if (XminTxtX <0) XminTxtX =-1; if ((XminTxtY-12) <0) XminTxtY =-1; if ((XmaxTxtX + 6 * XmaxTxt.length ())> DisplayResolutionX) XmaxTxtX =-1; if ((XmaxTxtY + 12)> DisplayResolutionY) XmaxTxtY =-1; if ((YminTxtX + 6 * YminTxt.length ())> DisplayResolutionX) YminTxtX =-1; if ((YminTxtY + 12)> DisplayResolutionY) YminTxtY =-1; if ((YmaxTxtX + 6 * YmaxTxt.length ())> DisplayResolutionX) YmaxTxtX =-1; if (YmaxTxtY <0) YmaxTxtY =-1; / * Range Limit Titles * / if ((XminTxtX! =-1) &&(XminTxtY! =-1)) CommandToTFT ("DS12 (" + String (XminTxtX) + "," + String (XminTxtY) + ", '" + String (XminTxt) + "'," + String (ColorX) + ");"); if ((XmaxTxtX! =-1) &&(XmaxTxtY! =-1)) CommandToTFT ("DS12 (" + String (XmaxTxtX) + "," + String (XmaxTxtY) + ", '" + String (XmaxTxt) + " ', "+ String (ColorX) +"); "); if ((YminTxtX! =-1) &&(YminTxtY! =-1)) CommandToTFT ("DS12 (" + String (YminTxtX) + "," + String (YminTxtY) + ", '" + String (YminTxt) + " ', "+ String (ColorY) +"); "); if ((YmaxTxtX! =-1) &&(YmaxTxtY! =-1)) CommandToTFT ("DS12 (" + String (YmaxTxtX) + "," + String (YmaxTxtY) + ", '" + String (YmaxTxt) + " ', "+ String (ColorY) +"); "); / * Valor de retorno String Cartesian_Setup () retornará uma configuração gráfica de embalagem de string no seguinte formato:"" String começa com '<' e termina em '>' . Cada valor é delimitado por ',' * / / * Initialize * / String Cartesian_SetupDetails ="<"; Cartesian_SetupDetails + =(String (Xmin) + ","); Cartesian_SetupDetails + =(String (Xmax) + ","); Cartesian_SetupDetails + =(String (Ymin) + ","); Cartesian_SetupDetails + =(String (Ymax) + ","); Cartesian_SetupDetails + =(String (Window_X1) + ","); Cartesian_SetupDetails + =(String (Window_Y1) + ","); Cartesian_SetupDetails + =(String (Window_X2) + ","); Cartesian_SetupDetails + =(String (Window_Y2) + ","); / * Close-Out * / Cartesian_SetupDetails + =">"; return Cartesian_SetupDetails;} / * ############## Fim de Cartesian_Setup () ############ * // * ############################################################ * // * ######################################################### Cartesian_ClearPlotAreas (Descriptor, Color) Redefinir / Limpar área de plotagem Função para Makeblock Me TFT LCD Parâmetros de entrada:(String) Descritor:Descritor de configuração - retornado por Cartesian_Setup () (int) Color______:Cor a ser usada para preencher a área de plotagem Usa a função externa CommandToTFT (). ######################################################### * / void Cartesian_ClearPlotAreas (Descritor de string, cor int) {int X1, Y1, X2, Y2; / * Coordenadas de limite para áreas de plotagem * / / * Extraindo valores do Descritor * / / * L [0] L [1] L [2] L [3] W [0] W [1] W [2] W [3 ] * / / * Xmin Xmax Ymin Ymax Window_X1 Window_Y1 Window_X2 Window_Y2 * / float L [4]; int W [4]; / * Valores armazenados no descritor * / int j =0; / * Contador * / String D_Str =""; for (int i =1; i <=(Descriptor.length () - 1); i ++) if (Descriptor [i] ==',') {if (j <4) L [j] =D_Str.toFloat ( ); else W [j-4] =D_Str.toInt (); D_Str =""; j ++; } else D_Str + =Descritor [i]; / * Origem * / int OriginX =(W [0] +1) + (int) (((W [2] -1) - (W [0] +1)) * abs (L [0]) / ( abs (L [1]) + abs (L [0]))); int OriginY =(W [1] +1) + (int) (((W [3] -1) - (W [1] +1)) * abs (L [3]) / (abs (L [3 ]) + abs (L [2]))); / * Limpando áreas de plotagem * / // Área.1:X + Y + X1 =OriginX + 2; Y1 =W [1] + 1; X2 =W [2] - 1; Y2 =Origem Y - 2; CommandToTFT ("BOXF (" + String (X1) + "," + String (Y1) + "," + String (X2) + "," + String (Y2) + "," + String (Cor) + "); "); // Área.2:X- Y + X1 =W [0] + 1; Y1 =W [1] + 1; X2 =OriginX - 2; Y2 =Origem Y - 2; CommandToTFT ("BOXF (" + String (X1) + "," + String (Y1) + "," + String (X2) + "," + String (Y2) + "," + String (Cor) + "); "); // Área.3:X- Y- X1 =W [0] + 1; Y1 =OrigemY + 2; X2 =OriginX - 2; Y2 =W [3] - 1; CommandToTFT ("BOXF (" + String (X1) + "," + String (Y1) + "," + String (X2) + "," + String (Y2) + "," + String (Cor) + "); "); // Área.4:X + Y- X1 =OriginX + 2; Y1 =OrigemY + 2; X2 =W [2] - 1; Y2 =W [3] - 1; CommandToTFT ("BOXF (" + String (X1) + "," + String (Y1) + "," + String (X2) + "," + String (Y2) + "," + String (Cor) + "); ");} / * ############ Fim de Cartesian_ClearPlotAreas () ############# * // * ############ ######################################################### * // * # ######################################################### Cartesian_Line (Xp, Yp, X, Y, Descritor, Cor) Função de linha cartesiana para Makeblock Me Parâmetros de entrada TFT LCD:(int) Xp, Yp_____:Coordenadas anteriores do gráfico - valor y vs x (int) X, Y_______:Coordenadas atuais do gráfico - valor y vs Descritor de x (String):Descritor de configuração - retornado por Cartesian_Setup () (int) Color______:Cor de marcação a ser usada em (x, y) Usa a função externa CommandToTFT (). ############### ############################################# * / void Cartesian_Line (float Xp, float Yp, float X, float Y , String Descriptor, int Color) {/ * Extraindo valores do Descritor * / / * L [0] L [1] L [2] L [3] W [0] W [1] W [2] W [3] * / / * Xmin Xmax Ymin Ymax Window_X1 Window_Y1 Window_X2 Window_Y2 * / float L [4 ]; int W [4]; / * Valores armazenados no descritor * / int j =0; / * Contador * / String D_Str =""; for (int i =1; i <=(Descriptor.length () - 1); i ++) if (Descriptor [i] ==',') {if (j <4) L [j] =D_Str.toFloat ( ); else W [j-4] =D_Str.toInt (); D_Str =""; j ++; } else D_Str + =Descritor [i]; / * Origem * / int OriginX =(W [0] +1) + (int) (((W [2] -1) - (W [0] +1)) * abs (L [0]) / ( abs (L [1]) + abs (L [0]))); int OriginY =(W [1] +1) + (int) (((W [3] -1) - (W [1] +1)) * abs (L [3]) / (abs (L [3 ]) + abs (L [2]))); int XminPx =W [0] + 1; int XmaxPx =W [2] - 1; int YmaxPx =W [1] + 1; int YminPx =W [3] - 1; se (Y> L [3]) Y =L [3]; se (Y 
 =(OriginX-2)) &&(DispXp <=(OriginX + 2))) || ((DispYp>
 =(OriginY-2)) &&(DispYp <=(OriginY + 2) )) || ((DispX> =(OriginX-2)) &&(DispX <=(OriginX + 2))) || ((DispY> =(OriginY-2)) &&(DispY <=(OriginY + 2) )))) CommandToTFT ("PL (" + String (DispXp) + "," + String (DispYp) + "," + String (DispX) + "," + String (DispY) + "," + String (Color ) + ");");} / * ############# Fim da Cartesian_Line () ############ * // * ######### ################################################ * // * ######## ##################################################### ReadFrequency (_DI_FrequencyCounter_Pin, _ReadingSpeed) Entrada da função de leitura de frequência Parâmetros:(int) _DI_FrequencyCounter_Pin:Pino digital a ser lido (float) _ReadingSpeed____________:Velocidade de leitura personalizada entre 0 ... 10 (Nota.1) Nota.1:_ReadingSpeed ​​é um valor para especificar por quanto tempo as alterações serão contadas. Não pode ser 0 (zero), valores negativos ou um valor maior que 10. Quando _ReadingSpeed ​​mudou, 1 segundo deve ser dividido por este valor para calcular a duração de contagem necessária. Por exemplo; - _ReadingSpeed ​​=0,1 -> entrada deve ser contada durante 10 segundos (=1 / 0,1) - _ReadingSpeed ​​=0,5 -> entrada deve ser contada durante 2 segundos (=1 / 0,5) - _ReadingSpeed ​​=2,0 -> entrada deve ser contada durante 0,5 segundos (=1/2) - _ReadingSpeed ​​=4,0 -> a entrada deve ser contada durante 0,25 segundos (=1/4). É importante notar que o aumento de _ReadingSpeed ​​é uma desvantagem, especialmente em frequências mais baixas (geralmente abaixo de 100 Hz), pois o erro de contagem aumenta até 20% ~ 40% diminuindo a frequência. #################################################################################################################### ######### * / int readFrequency (int _DI_FrequencyCounter_Pin, float _ReadingSpeed) {pinMode (_DI_FrequencyCounter_Pin, INPUT); byte _DigitalRead, _DigitalRead_Previous =0; longo sem sinal _Time =0, _Time_Init; float _Frequency =0; if ((_ReadingSpeed ​​<=0) || (_ReadingSpeed> 10)) return (-1); senão {_Time_Init =micros (); faça {_DigitalRead =digitalRead (_DI_FrequencyCounter_Pin); if ( (_DigitalRead_Previous==1) &&(_DigitalRead==0) ) _Frequency++; _DigitalRead_Previous =_DigitalRead; _Time =micros(); } while ( _Time <(_Time_Init + (1000000/_ReadingSpeed)) ); } return (_ReadingSpeed * _Frequency);}/* ########### End of readFrequency() ########### *//* ############################################## *//* ############################################### controllerPID(RangeMin, RangeMax, _E, _Eprev, _dT, _Kp, _Ki, _Kd) PID Controller Function Input Parameters:(float) RangeMin:Minimum limit for output (float) RangeMax:Maximum limit for output (float) _E_____:Current error signal (float) _Eprev :Previous error signal (float) _dT____:Time difference as seconds (float) _Kp____:Proportional coefficient (float) _Ki____:Integral coefficient (float) _Kp____:Derivative coefficient Adjustment procedure:1. Set Kp=0, Ki=0, Kd=0. 2. Start to increase Kp until the system oscillates at fixed period (Pc) and note critical gain Kc =Kp. 3. Adjust final coefficients as follows. for P-control only :Kp =0.50*Kc for PI-control only :Kp =0.45*Kc, Ki =1.2/Pc for PID-control :Kp =0.60*Kc, Ki =2.0/Pc, Kd=Pc/8 4. Fine tuning could be done by slightly changing each coefficient.############################################### */ float controllerPID(float _E, float _Eprev, float _dT, float _Kp, float _Ki, float _Kd){ float P, I, D; /* Base Formula:U =_Kp * ( _E + 0.5*(1/_Ki)*(_E+_Eprev)*_dT + _Kd*(_E-_Eprev)/_dT ); */ P =_Kp * _E; /* Proportional Component */ I =_Kp * 0.5 * _Ki * (_E+_Eprev) * _dT; /* Integral Component */ D =_Kp * _Kd * (_E-_Eprev) / _dT; /* Derivative Component */ return (P+I+D);}/* ########### End of controllerPID() ########### *//* ############################################## *//* ############################################### Setup############################################### */void setup(){ Serial1.begin(9600); Serial1.println("CLS(0);");delay(20); analogReadResolution(12); pinMode(_chDirection,INPUT); // Direction selector reading pinMode(_chMotorCmdCCW,OUTPUT); // PWM output to motor for counter-clockwise turn pinMode(_chMotorCmdCW,OUTPUT); // PWM output to motor for clockwise turn // Initial killing the PWM outputs to motor analogWrite(_chMotorCmdCCW,0); analogWrite(_chMotorCmdCW,0); // Initial reading for direction selection Direction=digitalRead(_chDirection); // HIGH=CCW, LOW=CW prevDirection=Direction; // The section below prepares TFT LCD // Cartesian_Setup(Xmin, Xmax, Ymin, Ymax, Window_X1, Window_Y1, Window_X2, Window_Y2, MinDashQty, ColorF, ColorX, ColorY) Cartesian_SetupDetails =Cartesian_Setup(0, _Tmax, _minRPM, _maxRPM, 20, 20, 220, 120, 10, 0, 7, 7); CommandToTFT("DS12(250,10,'Dir:CW '," + String(_WHITE) + ");"); CommandToTFT("DS12(250,25,'____ Set'," + String(_YELLOW) + ");"); CommandToTFT("DS12(250,40,'____ RPM'," + String(_GREEN) + ");"); /* Alarm Values */ CommandToTFT("DS12(250,55,'AHH:" + String(RAHH) + "'," + String(_WHITE) + ");"); CommandToTFT("DS12(250,70,'AH :" + String(RAH) + "'," + String(_WHITE) + ");"); CommandToTFT("DS12(250,85,'AL :" + String(RAL) + "'," + String(_WHITE) + ");"); CommandToTFT("DS12(250,100,'ALL:"+ String(RALL) + "'," + String(_WHITE) + ");"); /* Alarm Window */ CommandToTFT("BOX(240,55,319,115," + String(_WHITE) + ");"); /* Alarm Lamps */ CommandToTFT("BOX(240,55,248,70," + String(_WHITE) + ");"); CommandToTFT("BOX(240,70,248,85," + String(_WHITE) + ");"); CommandToTFT("BOX(240,85,248,100," + String(_WHITE) + ");"); CommandToTFT("BOX(240,100,248,115," + String(_WHITE) + ");");}/* ############################################### Loop############################################### */void loop(){ // Initialization Time:Necessary for PID controller. int InitTime =micros(); // X-Axis Auto-Reset for Graphing if ( Seconds> 90.0 ) { Seconds =0.0; Cartesian_ClearPlotAreas(Cartesian_SetupDetails,0); } // Reading Inputs /* Controller Coefficients */ Kp =Kpmax * (float)analogRead(_chKp) / 4095; Ki =Kimax * (float)analogRead(_chKi) / 4095; Kd =Kdmax * (float)analogRead(_chKd) / 4095; /* Direction Selector */ Direction =digitalRead(_chDirection); /* HIGH=CCW, LOW=CW */ /* Actual RPM and RPM Setpoint Note that maximum selectable RPM is 5000. */ RPM =60 * (float)readFrequency(_chSpeedRead,4) / _DiscSlots; RPMset =5000 * (float)analogRead(_chSpeedSet) / 4095; // Calculations and Actions /* Error Signal, PID Controller Output and Final Output (PWM) to Motor */ E =RPMset - RPM; float cPID =controllerPID(E, Eprev, dT, Kp, Ki, Kd); if ( RPMset ==0 ) OutputRPM =0; else OutputRPM =OutputRPM + cPID; if ( OutputRPM <_minRPM ) OutputRPM =_minRPM; if ( OutputRPM> _maxRPM ) OutputRPM =_maxRPM; /* Changing Direction when inverted Note that no any graphical indication is performed on this function. */ if ( Direction !=prevDirection ) { /* Killing both of the PWM outputs to motor */ analogWrite(_chMotorCmdCCW,0); analogWrite(_chMotorCmdCW,0); /* Wait until motor speed decreases */ do { RPM =60 * (float)readFrequency(_chSpeedRead,4) / _DiscSlots; } while ( RPM> _minRPM ); } // Writing Outputs if (Direction==HIGH) analogWrite(_chMotorCmdCCW,(int)(255*OutputRPM/_maxRPM)); else analogWrite(_chMotorCmdCW, (int)(255*OutputRPM/_maxRPM)); // Graphing /* Indicating Direction */ if (Direction==HIGH) CommandToTFT("DS12(280,10,'CCW '," + String(_WHITE) + ");"); else CommandToTFT("DS12(280,10,'CW '," + String(_WHITE) + ");"); /* Plotting Curve */ Cartesian_Line(prevSeconds, prevRPMset, Seconds, RPMset, Cartesian_SetupDetails, _YELLOW); Cartesian_Line(prevSeconds, prevRPM, Seconds, RPM, Cartesian_SetupDetails, _GREEN); /* Indicating values of RPM Setpoint, PID Controller Coefficients, Error Signal, PID Controller Output and Final RPM Output (PWM) */ CommandToTFT( "DS12(20,150,'Set:" + String(RPMset) + " rpm " + "RPM:" + String(RPM) + " rpm '," + String(_WHITE) + ");"); CommandToTFT( "DS12(20,170,'Kp=" + String(Kp) + " " + "Ki=" + String(Ki) + " " + "Kd=" + String(Kd) + " " + "dT=" + String(dT*1000) + " ms '," + String(_WHITE) + ");"); CommandToTFT( "DS12(20,190,'e=" + String(E) + " " + "cPID=" + String(cPID) + " " + "RPMout=" + String(OutputRPM) + " '," + String(_WHITE) + ");"); /* Resetting Alarm Lamps */ CommandToTFT("BOXF(241,56,247,69," + String(_BLACK) + ");"); CommandToTFT("BOXF(241,71,247,84," + String(_BLACK) + ");"); CommandToTFT("BOXF(241,86,247,99," + String(_BLACK) + ");"); CommandToTFT("BOXF(241,101,247,114," + String(_BLACK) + ");"); /* Activating Necessary Alarm Lamps */ if (RPM>=RAHH) CommandToTFT("BOXF(241,56,247,69," + String(_RED) + ");"); if ((RPM>=RAH)&&(RPMRALL)&&(RPM<=RAL)) CommandToTFT("BOXF(241,86,247,99," + String(_RED) + ");"); if (RPM<=RALL) CommandToTFT("BOXF(241,101,247,114," + String(_RED) + ");"); // Storing Values generated on previous cycle Eprev =E; prevRPMset =RPMset; prevRPM =RPM; prevSeconds =Seconds; prevDirection =Direction; // Calculating control application cycle time and passed Seconds dT =float ( micros() - InitTime ) / 1000000.0; Seconds+=dT; } 
Code without Graphical Touch-UpsArduino
/* ############################################### I/O Assignments############################################### */int _chSpeedSet =A0, // Speed setpoint _chKp =A1, // Proportional coefficient reading for PID controller _chKi =A2, // Integral coefficient reading for PID controller _chKd =A3, // Derivative coefficient reading for PID controller _chMotorCmdCCW =3, // PWM output to motor for counter-clockwise turn _chMotorCmdCW =2, // PWM output to motor for clockwise turn _chSpeedRead =24, // Speed reading _chDirection =25; // Direction selector reading/* ############################################### Other Constants ############################################### */#define _minRPM 0 // Minimum RPM to initiate direction changing#define _maxRPM 6000 // Maximum RPM limit#define _DiscSlots 20 // Qty of slots on Index Disc/* ############################################### Global Variables############################################### */boolean Direction, prevDirection;float RPM=0.0, RPMset=0.0, OutputRPM=0.0, Kp=0.0, Ki=0.0, Kd=0.0, Kpmax=2.0, Kimax=1.0, Kdmax=1.0, E=0.0, Eprev=0.0, dT=1.0;/* ############################################### readFrequency(_DI_FrequencyCounter_Pin, _ReadingSpeed) Frequency Reading Function Input Parameters:(int) _DI_FrequencyCounter_Pin :Digital pin to be read (float) _ReadingSpeed____________:Custom reading speed between 0...10 (Note.1) Note.1:_ReadingSpeed is a value to specify how long shall the changes be counted. It cannot be 0(zero), negative values or a value greater than 10. When _ReadingSpeed changed, 1 second shall be divided by this value to calculate required counting duration. For example; - _ReadingSpeed =0.1 -> input shall be counted during 10 seconds (=1/0.1) - _ReadingSpeed =0.5 -> input shall be counted during 2 seconds (=1/0.5) - _ReadingSpeed =2.0 -> input shall be counted during 0.5 seconds (=1/2) - _ReadingSpeed =4.0 -> input shall be counted during 0.25 seconds (=1/4) Importantly note that, increasing of _ReadingSpeed is a disadvantage especially on lower frequencies (generally below 100 Hz) since counting error increases up to 20%~40% by decreasing frequency.############################################### */int readFrequency(int _DI_FrequencyCounter_Pin, float _ReadingSpeed){ pinMode(_DI_FrequencyCounter_Pin,INPUT); byte _DigitalRead, _DigitalRead_Previous =0; unsigned long _Time =0, _Time_Init; float _Frequency =0; if ( (_ReadingSpeed<=0) || (_ReadingSpeed>10) ) return (-1); else { _Time_Init =micros(); do { _DigitalRead =digitalRead(_DI_FrequencyCounter_Pin); if ( (_DigitalRead_Previous==1) &&(_DigitalRead==0) ) _Frequency++; _DigitalRead_Previous =_DigitalRead; _Time =micros(); } while ( _Time <(_Time_Init + (1000000/_ReadingSpeed)) ); } return (_ReadingSpeed * _Frequency);}/* ########### End of readFrequency() ########### *//* ############################################## *//* ############################################### controllerPID(RangeMin, RangeMax, _E, _Eprev, _dT, _Kp, _Ki, _Kd) PID Controller Function Input Parameters:(float) RangeMin:Minimum limit for output (float) RangeMax:Maximum limit for output (float) _E_____:Current error signal (float) _Eprev :Previous error signal (float) _dT____:Time difference as seconds (float) _Kp____:Proportional coefficient (float) _Ki____:Integral coefficient (float) _Kp____:Derivative coefficient Adjustment procedure:1. Set Kp=0, Ki=0, Kd=0. 2. Start to increase Kp until the system oscillates at fixed period (Pc) and note critical gain Kc =Kp. 3. Adjust final coefficients as follows. for P-control only :Kp =0.50*Kc for PI-control only :Kp =0.45*Kc, Ki =1.2/Pc for PID-control :Kp =0.60*Kc, Ki =2.0/Pc, Kd=Pc/8 4. Fine tuning could be done by slightly changing each coefficient.############################################### */ float controllerPID(float _E, float _Eprev, float _dT, float _Kp, float _Ki, float _Kd){ float P, I, D; /* Base Formula:U =_Kp * ( _E + 0.5*(1/_Ki)*(_E+_Eprev)*_dT + _Kd*(_E-_Eprev)/_dT ); */ P =_Kp * _E; /* Proportional Component */ I =_Kp * 0.5 * _Ki * (_E+_Eprev) * _dT; /* Integral Component */ D =_Kp * _Kd * (_E-_Eprev) / _dT; /* Derivative Component */ return (P+I+D);}/* ########### End of controllerPID() ########### *//* ############################################## *//* ############################################### Setup############################################### */void setup(){ analogReadResolution(12); pinMode(_chDirection,INPUT); // Direction selector reading pinMode(_chMotorCmdCCW,OUTPUT); // PWM output to motor for counter-clockwise turn pinMode(_chMotorCmdCW,OUTPUT); // PWM output to motor for clockwise turn // Initial killing the PWM outputs to motor analogWrite(_chMotorCmdCCW,0); analogWrite(_chMotorCmdCW,0); // Initial reading for direction selection Direction=digitalRead(_chDirection); // HIGH=CCW, LOW=CW prevDirection=Direction;}/* ############################################### Loop############################################### */void loop(){ // Initialization Time:Necessary for PID controller. int InitTime =micros(); // Reading Inputs /* Controller Coefficients */ Kp =Kpmax * (float)analogRead(_chKp) / 4095; Ki =Kimax * (float)analogRead(_chKi) / 4095; Kd =Kdmax * (float)analogRead(_chKd) / 4095; /* Direction Selector */ Direction =digitalRead(_chDirection); /* HIGH=CCW, LOW=CW */ /* Actual RPM and RPM Setpoint Note that maximum selectable RPM is 5000. */ RPM =60 * (float)readFrequency(_chSpeedRead,4) / _DiscSlots; RPMset =5000 * (float)analogRead(_chSpeedSet) / 4095; // Calculations and Actions /* Error Signal, PID Controller Output and Final Output (PWM) to Motor */ E =RPMset - RPM; float cPID =controllerPID(E, Eprev, dT, Kp, Ki, Kd); if ( RPMset ==0 ) OutputRPM =0; else OutputRPM =OutputRPM + cPID; if ( OutputRPM <_minRPM ) OutputRPM =_minRPM; if ( OutputRPM> _maxRPM ) OutputRPM =_maxRPM; /* Changing Direction when inverted */ if ( Direction !=prevDirection ) { /* Killing both of the PWM outputs to motor */ analogWrite(_chMotorCmdCCW,0); analogWrite(_chMotorCmdCW,0); /* Wait until motor speed decreases */ do { RPM =60 * (float)readFrequency(_chSpeedRead,4) / _DiscSlots; } while ( RPM> _minRPM ); } // Writing Outputs if (Direction==HIGH) analogWrite(_chMotorCmdCCW,(int)(255*OutputRPM/_maxRPM)); else analogWrite(_chMotorCmdCW, (int)(255*OutputRPM/_maxRPM)); // Storing Values generated on previous cycle Eprev =E; prevDirection =Direction; // Calculating control application cycle time and passed Seconds dT =float ( micros() - InitTime ) / 1000000.0;}

Esquemas

It's a prototype to explain DC motor speed control by using PID controller, and what should be considered for reversing.

Processo de manufatura

  1. Ligas de cobre de tungstênio para motores
  2. Controle de um efeito com sensores reais
  3. Arduino Nano:Controle 2 motores de passo com joystick
  4. Controlando uma matriz de LED com Arduino Uno
  5. Monitoramento de temperatura SMART para escolas
  6. Biblioteca de portas IO de 8 bits para Arduino
  7. Matriz de teclado de prototipagem de 64 teclas para Arduino
  8. TFT Shield para Arduino Nano - Iniciar
  9. Uma entrada analógica isolada para Arduino
  10. Robô para navegação interna supercool