В этом уроке мы научимся управлять двигателем постоянного тока с помощью ПИД-регулятора через Интернет и Arduino Uno.
Комплектующие
Детали, используемые в этом проекте мы перечислим ниже.
- Двигатель постоянного тока с датчиком × 1
- Контроллер двигателя постоянного тока (PHPoC DC Motor Controller) × 1
- Шилд PHPoC WiFi Shield 2 для Arduino × 1
- Arduino Uno × 1
Как это устроено
Когда пользователь получает доступ к веб-странице PHPoC [WiFi] Shield из веб-браузера на смартфоне или ПК, между Arduino и веб-браузером будет установлено соединение WebSocket. Соединение WebSocket позволяет в реальном времени обмениваться данными между веб-браузером и Arduino без перезагрузки веб-страницы.
Когда пользователь поворачивает стрелку на веб-странице, угол поворота будет отправлен в Arduino. Arduino преобразует угол в эквивалентную позицию (единица измерения - импульс), а затем использует алгоритм ПИД (PID) для вращения двигателя постоянного тока на эквивалентную позицию.
Библиотека PID для Arduino можно скачать в нашем каталоге Библиотек по этой ссылке. Она также содержит функцию автонастройки для положения и скорости.
Расчет угол-позиция
Предположение:
- Разрешение инкрементного энкодера (количество на оборот): ENC_RESOLUTION
- Передаточное число: GEAR_RATIO
Сигнал от энкодера будет декодироваться контроллером двигателя постоянного тока. Контроллер двигателя постоянного тока действует как квадратурный декодер, который будет умножать в четыре раза (х4) число импульсов за оборот.
У нас есть:
- оборот <=> 360 градусов <=> ENC_RESOLUTION * GEAR_RATIO * 4
- угол <=> положение ( angle <=> position )
=> позиция = angle * ENC_RESOLUTION * GEAR_RATIO * 4/360
Мотор, который использовался, имеет:
- ENC_RESOLUTION = 13
- GEAR_RATIO = 100
- => position = angle * 13 * 100 * 4/360
(см. строку 68 кода Arduino ниже)
Соединение комплектующих
- Стек PHPoC Shield или PHPoC WiFi Shield на Arduino
- Контроллер стека постоянного тока PES-2604 на PHPoC Shield или PHPoC WiFi Shield
- Подключите клеммную колодку контроллера двигателя постоянного тока к двигателю постоянного тока и к источнику питания двигателя постоянного тока следующим образом:
- Клеммная колодка - провод VM <----> (+) - источник питания для двигателя постоянного тока
- Клеммная колодка - GND <----> (-) провод - источник питания для двигателя постоянного тока
- Клеммная колодка - провод M1 + <----> (+) - двигатель постоянного тока
- Клеммная колодка - провод M1- <----> (-) - двигатель постоянного тока
- (Необязательно) Подключите порт датчика контроллера двигателя постоянного тока к контакту датчика двигателя постоянного тока следующим образом:
- Энкодер 1 - 5 В <----> Vcc - Энкодер двигателя постоянного тока
- Энкодер 1 - GND <----> GND - Энкодер двигателя постоянного тока
- Энкодер 1 - 1A <----> A фаза - Энкодер двигателя постоянного тока
- Энкодер 1 - фаза 1B <----> B - Энкодер двигателя постоянного тока
- Кодер 1 расположен в верхнем ряду порта датчика. Более подробно здесь
Изображения для проекта
Ниже вы можете скачать архив с изображениями или скачать их. Они пригодятся позже.
Веб-интерфейс
Ниже файл remote_dc.php - это файл, который содержит веб-интерфейс пользователя. Он должен храниться в PHPoC [WiFi] Shield. Чтобы загрузить файл в PHPoC [WiFi] Shield, выполните следующие действия:
- Скопируйте приведенный ниже код и сохраните его в файле remote_dc.php.
- Установите PHPoC Debugger
- Подключите PHPoC Debugger к PHPoC [WiFi] Shield с помощью кабеля micro-USB согласно следующей инструкции по ссылке.
- Обратите внимание, что Arduino должен быть включен.
- Загрузите файл remote_dc.php в PHPoC [WiFi] Shield согласно этой инструкции по этой ссылке.
В коде используются изображения, которые вы можете скачать выше.
<!DOCTYPE html> <html> <head> <title>PHPoC / <?echo system("uname -i")?></title> <meta name="viewport" content="width=device-width, initial-scale=0.7"> <style> body { text-align: center; background-color: whiite;} canvas { background: url(plate.png) no-repeat; background-size: contain;} } </style> <script> var MIN_TOUCH_RADIUS = 20; var MAX_TOUCH_RADIUS = 200; var CANVAS_WIDTH, CANVAS_HEIGHT; var PIVOT_X, PIVOT_Y; var PLATE_WIDTH = 659.0, PLATE_HEIGHT = 569.0; // size of background image var NEEDLE_WIDTH = 121, NEEDLE_HEIGHT = 492; // size of needle image var RATIO; var plateAngle = 0; var needleImage = new Image(); var clickState = 0; var lastAngle = 0; var mouse_xyra = {x:0, y:0, r:0.0, a:0.0}; var ws; needleImage.src = "needle.png"; function init() { CANVAS_WIDTH = window.innerWidth - 50; CANVAS_HEIGHT = window.innerHeight - 50; if(CANVAS_WIDTH < CANVAS_HEIGHT) CANVAS_HEIGHT = CANVAS_WIDTH; else CANVAS_WIDTH = CANVAS_HEIGHT; PIVOT_X = CANVAS_WIDTH/2; PIVOT_Y = CANVAS_HEIGHT/2; RATIO = CANVAS_WIDTH / PLATE_WIDTH; NEEDLE_WIDTH = Math.round(NEEDLE_WIDTH * RATIO); NEEDLE_HEIGHT = Math.round(NEEDLE_HEIGHT * RATIO); var canvas = document.getElementById("canvas"); canvas.width = CANVAS_WIDTH; canvas.height = CANVAS_HEIGHT; canvas.addEventListener("touchstart", mouse_down); canvas.addEventListener("touchend", mouse_up); canvas.addEventListener("touchmove", mouse_move); canvas.addEventListener("mousedown", mouse_down); canvas.addEventListener("mouseup", mouse_up); canvas.addEventListener("mousemove", mouse_move); var ctx = canvas.getContext("2d"); ctx.translate(PIVOT_X, PIVOT_Y); rotate_plate(0); ws = new WebSocket("ws://<?echo _SERVER("HTTP_HOST")?>/remote_dc", "text.phpoc"); document.getElementById("ws_state").innerHTML = "CONNECTING"; ws.onopen = function(){ document.getElementById("ws_state").innerHTML = "OPEN" }; ws.onclose = function(){ document.getElementById("ws_state").innerHTML = "CLOSED"}; ws.onerror = function(){ alert("websocket error " + this.url) }; ws.onmessage = ws_onmessage; } function ws_onmessage(e_msg) { e_msg = e_msg || window.event; // MessageEvent plateAngle = Number(e_msg.data); rotate_plate(plateAngle); } function rotate_plate(angle) { var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); ctx.clearRect(-PIVOT_X, -PIVOT_Y, CANVAS_WIDTH, CANVAS_HEIGHT); ctx.rotate(-angle / 180 * Math.PI); ctx.drawImage(needleImage, 0, 0, needleImage.width, needleImage.height, -NEEDLE_WIDTH/2, -NEEDLE_HEIGHT/2, NEEDLE_WIDTH, NEEDLE_HEIGHT); ctx.rotate(angle / 180 * Math.PI); debug = document.getElementById("debug"); debug.innerHTML = plateAngle.toFixed(1); } function check_update_xyra(event, mouse_xyra) { var x, y, r, a; var min_r, max_r, width; if(event.touches) { var touches = event.touches; x = (touches[0].pageX - touches[0].target.offsetLeft) - PIVOT_X; y = PIVOT_Y - (touches[0].pageY - touches[0].target.offsetTop); } else { x = event.offsetX - PIVOT_X; y = PIVOT_Y - event.offsetY; } /* cartesian to polar coordinate conversion */ r = Math.sqrt(x * x + y * y); a = Math.atan2(y, x); mouse_xyra.x = x; mouse_xyra.y = y; mouse_xyra.r = r; mouse_xyra.a = a; if((r >= MIN_TOUCH_RADIUS) && (r <= MAX_TOUCH_RADIUS)) return true; else return false; } function mouse_down() { if(event.touches && (event.touches.length > 1)) clickState = event.touches.length; if(clickState > 1) return; if(check_update_xyra(event, mouse_xyra)) { clickState = 1; lastAngle = mouse_xyra.a / Math.PI * 180.0; } } function mouse_up() { clickState = 0; } function mouse_move() { var angle, angleOffset; if(event.touches && (event.touches.length > 1)) clickState = event.touches.length; if(clickState != 1) return; if(!check_update_xyra(event, mouse_xyra)) { clickState = 0; } else { angle = mouse_xyra.a / Math.PI * 180.0; if(angle < 0.0) angle = angle + 360.0; angleOffset = angle - lastAngle; lastAngle = angle; if(angleOffset > 180.0) angleOffset = -360.0 + angleOffset; else if(angleOffset < -180.0) angleOffset = 360 + angleOffset; plateAngle += angleOffset; rotate_plate(plateAngle); if(ws != null & ws.readyState == 1) ws.send(plateAngle.toFixed(4) + "\r\n"); } event.preventDefault(); } window.onload = init; </script> </head> <body> <h2> Arduino - DC Motor Position Control<br><br> <canvas id="canvas"></canvas> <p> WebSocket : <span id="ws_state">null</span><br> Angle : <span id="debug">0</span> </p> </h2> </body> </html>
Код Ардуино
Это код Arduino, который работает в бесконечном цикле.
#include <Phpoc.h> #include <PhpocExpansion.h> #include <Pid.h> PhpocServer server(80); DcMotorPID dcPid1(1, 1); long targetPosition; void setup(){ Serial.begin(9600); while(!Serial) ; //Phpoc.begin(PF_LOG_SPI | PF_LOG_NET); Phpoc.begin(); server.beginWebSocket("remote_dc"); Serial.print("WebSocket server address : "); Serial.println(Phpoc.localIP()); Expansion.begin(460800); dcPid1.setPeriod(10000); dcPid1.setEncoderPSR(64); dcPid1.setDecay(0); dcPid1.setFilterFrequency(5000); dcPid1.setScaleFactor(5); // depending on motor, should test manually to have the best value dcPid1.setIntegralLimit(-2000, 2000); dcPid1.begin(PID_POSITION); /*----------- check enc pol-------------- */ int pwm = 0; long pos = 0; while(!pos) { pwm += 10; dcPid1.setWidth(pwm); pos = dcPid1.getEncoderPosition(); delay(1); } if(pos < 0) dcPid1.setEncoderPolarity(-1); dcPid1.setWidth(0); //dcPid1.setGain(24.099171193725, 10.78947680748, 0.0617886); /* PID auto-tuning */ dcPid1.beginTune(); while(!dcPid1.loopTune()) ; dcPid1.setEncoderPosition(0); Serial.print(F("PID TUNED Kp: "));Serial.println(dcPid1.getKp()); Serial.print(F("PID TUNED Ki: "));Serial.println(dcPid1.getKi()); Serial.print(F("PID TUNED Kd: "));Serial.println(dcPid1.getKd()); } void loop(){ PhpocClient client = server.available(); if(client) { String data = client.readLine(); if(data) { float targetAngle = data.toFloat(); targetPosition = (long)(targetAngle / 360.0 * (13 * 100 * 4)); } } long position = dcPid1.loop(targetPosition); //Serial.print(targetPosition); //Serial.print(" "); //Serial.println(position); }
Итоговый результат
На этом всё.