Что такое пид регулятор. Преобразователь частоты и пид-регулятор — общая настройка. Непрерывный регулятор с импульсным выходом

Системы автоматического управления (САУ) предназначены для автоматического изменения одного или нескольких параметров объекта управления с целью установления требуемого режима его работы. САУ обеспечивает поддержание постоянства заданных значений регулируемых параметров или их изменение по заданному закону либо оптимизирует определенные критерии качества управления. Например, к таким системам относятся:

  • системы стабилизации,
  • системы программного управления,
  • следящие системы

Это достаточно широкий класс систем, которые можно найти где угодно. Но какое это отношение имеет к Unity3D и вероятно к играм в частности? В принципе прямое: в любой игре так или иначе использующей симуляцию как элемент геймплея реализуются САУ, к таким играм относятся, например, Kerbal Space Programm, Digital Combat Simulator (бывший Lock On), Strike Suit Zero и т.д. (кто знает еще примеры - пишите в комментариях). В принципе любая игра, моделирующая реальные физические процессы, в том числе и просто кинематику с динамикой движения, может реализовывать те или иные САУ - этот подход проще, естественнее, а у разработчика уже есть есть набор готовых инструментов, предоставленных всякими Вышнеградскими, Ляпуновыми, Калманами, Чебышевами и прочими Коломогоровами, поэтому можно обойтись без изобретения велосипеда, т.к. его уже изобрели, да так, что получилась отдельная наука: Теория автоматического управления. Главное тут не переусердствовать. Одна тут только проблема: рассказывают про ТАУ не везде, не всем, зачастую мало и не очень понятно.

Немножко теории

Классическая система автоматического управления представленная на следующем рисунке:



Ключевым элементом любой САУ является регулятор представляющий из себя устройство, которое следит за состоянием объекта управления и обеспечивает требуемый закон управления. Процесс управления включает в себя: вычисление ошибки управления или сигнала рассогласования e (t ) как разницы между желаемой уставкой (set point или SP ) и текущей величиной процесса (process vale или PV ), после чего регулятор вырабатывает управляющие сигналы (manipulated value или MV ).


Одной из разновидностью регуляторов является пропорционально-интегрально-дифференцирующий (ПИД) регулятор , который формирует управляющий сигнал, являющийся суммой трёх слагаемых: пропорционального, интегрального и дифференциального.



Где, ошибка рассогласования, а также, - пропорциональная, - интегральная, - дифференциальная составляющие (термы) закона управления, который в итоговом виде описывается следующими формулами




Пропорциональная составляющая P - отвечает за т.н. пропорциональное управление, смысл которого в том, что выходной сигнал регулятора, противодействует отклонению регулируемой величины (ошибки рассогласования или еще это называют невязкой) от заданного значения. Чем больше ошибка рассогласования, тем больше командное отклонение регулятора. Это самый простой и очевидный закон управления. Недостаток пропорционального закона управления заключается в том, что регулятор никогда не стабилизируется в заданном значении, а увеличение коэффициента пропорциональности всегда приводит к автоколебаниям. Именно поэтому в довесок к пропорциональному закону управления приходиться использовать интегральный и дифференциальный.


Интегральная составляющая I накапливает (интегрирует) ошибку регулирования, что позволяет ПИД-регулятору устранять статическую ошибку (установившуюся ошибку, остаточное рассогласование). Или другими словами: интегральное звено всегда вносит некоторое смещение и если система подвержена некоторыми постоянным ошибкам, то оно их компенсирует (за счет своего смещения). А вот если же этих ошибок нет или они пренебрежительно малы, то эффект будет обратным - интегральная составляющая сама будет вносить ошибку смещения. Именно по этой причине её не используют, например, в задачах сверхточного позиционирования. Ключевым недостатком интегрального закона управления является эффект насыщения интегратора (Integrator windup).


Дифференциальная составляющая D пропорциональна темпу изменения отклонения регулируемой величины и предназначена для противодействия отклонениям от целевого значения, которые прогнозируются в будущем . Примечательно то, что дифференциальная компонента устраняет затухающие колебания. Дифференциальное регулирование особенно эффективно для процессов, которые имеют большие запаздывания. Недостатком дифференциального закона управления является его неустойчивость к воздействую шумов (Differentiation noise).


Таким образом, в зависимости от ситуации могут применятся П-, ПД-, ПИ- и ПИД-регуляторы, но основным законом управления в основном является пропорциональный (хотя в некоторых специфических задачах и могут использоваться исключительно только звенья дифференциаторов и интеграторов).


Казалось бы, вопрос реализации ПИД-регуляторов уже давно избит и здесь на Хабре есть парочка неплохих статей на эту тему в том числе и на Unity3D , также есть неплохая статья PID Without a PhD (перевод) и цикл статей в журнале "Современные технологии автоматизации" в двух частях: первая и вторая . Также к вашим услугам статья на Википедии (наиболее полную читайте в английском варианте). А на форумах коммьюнити Unity3D нет-нет, да и всплывет PID controller как и на gamedev.stackexchange


При вопрос по реализации ПИД-регуляторов несколько глубже чем и кажется. Настолько, что юных самоделкиных, решивших, реализовать такую схему регулирования ждет немало открытий чудных, а тема актуальная. Так что надеюсь сей опус, кому-нибудь да пригодиться, поэтому приступим.

Попытка номер раз

В качестве примера попытаемся реализовать схему регулирования на примере управления поворотом в простенькой космической 2D-аркаде, по шагам, начиная с самого начала (не забыли, что это туториал?).


Почему не 3D? Потому что реализация не измениться, за исключением того, что придется воротить ПИД-регулятор для контроля тангажа, рысканья и крена. Хотя вопрос корректного применения ПИД-регулирования вместе с кватернионами действительно интересный, возможно в будущем его и освящу, но даже в NASA предпочитают углы Эйлера вместо кватернионов, так что обойдемся простенькой моделью на двухмерной плоскости.


Для начала создадим сам объект игровой объект космического корабля, который будет состоять из собственно самого объекта корабля на верхнем уровне иерархии, прикрепим к нему дочерний объект Engine (чисто спецэффектов ради). Вот как это выглядит у меня:



А на сам объект космического корабля накидаем в инспекторе всяческих компонент. Забегая вперед, приведу скрин того, как он будет выглядеть в конце:



Но это потом, а пока в нем еще нет никаких скриптов, только стандартный джентльменский набор: Sprite Render, RigidBody2D, Polygon Collider, Audio Source (зачем?).


Собственно физика у нас сейчас самое главное и управление будет осуществляться исключительно через неё, в противном случае, применение ПИД-регулятора потеряло бы смысл. Масса нашего космического корабля оставим также в 1 кг, а все коэффициенты трения и гравитации равны нулю - в космосе же.


Т.к. помимо самого космического корабля есть куча других, менее умных космических объектов, то сначала опишем родительский класс BaseBody , который в себе будет содержать ссылки на на наши компоненты, методы инициализации и уничтожения, а также ряд дополнительных полей и методов, например для реализации небесной механики:


BaseBody.cs

using UnityEngine; using System.Collections; using System.Collections.Generic; namespace Assets.Scripts.SpaceShooter.Bodies { public class BaseBody: MonoBehaviour { readonly float _deafultTimeDelay = 0.05f; public static List _bodies = new List(); #region RigidBody public Rigidbody2D _rb2d; public Collider2D _c2d; #endregion #region References public Transform _myTransform; public GameObject _myObject; ///

/// Объект, который появляется при уничтожении /// public GameObject _explodePrefab; #endregion #region Audio public AudioSource _audioSource; /// /// Звуки, которые проигрываются при получении повреждения /// public AudioClip _hitSounds; /// /// Звуки, которые проигрываются при появлении объекта /// public AudioClip _awakeSounds; /// /// Звуки, которые воспроизводятся перед смертью /// public AudioClip _deadSounds; #endregion #region External Force Variables /// /// Внешние силы воздйствующие на объект /// public Vector2 _ExternalForces = new Vector2(); /// /// Текущий вектор скорости /// public Vector2 _V = new Vector2(); /// /// Текущий вектор силы гравитации /// public Vector2 _G = new Vector2(); #endregion public virtual void Awake() { Init(); } public virtual void Start() { } public virtual void Init() { _myTransform = this.transform; _myObject = gameObject; _rb2d = GetComponent(); _c2d = GetComponentsInChildren(); _audioSource = GetComponent(); PlayRandomSound(_awakeSounds); BaseBody bb = GetComponent(); _bodies.Add(bb); } /// /// Уничтожение персонажа /// public virtual void Destroy() { _bodies.Remove(this); for (int i = 0; i < _c2d.Length; i++) { _c2d[i].enabled = false; } float _t = PlayRandomSound(_deadSounds); StartCoroutine(WaitAndDestroy(_t)); } /// /// Ждем некоторое время перед уничтожением /// /// Время ожидания /// public IEnumerator WaitAndDestroy(float waitTime) { yield return new WaitForSeconds(waitTime); if (_explodePrefab) { Instantiate(_explodePrefab, transform.position, Quaternion.identity); } Destroy(gameObject, _deafultTimeDelay); } /// /// Проигрывание случайного звука /// /// Массив звуков /// Длительность проигрываемого звука public float PlayRandomSound(AudioClip audioClip) { float _t = 0; if (audioClip.Length > 0) { int _i = UnityEngine.Random.Range(0, audioClip.Length - 1); AudioClip _audioClip = audioClip[_i]; _t = _audioClip.length; _audioSource.PlayOneShot(_audioClip); } return _t; } /// /// Получение урона /// /// Уровень урона public virtual void Damage(float damage) { PlayRandomSound(_hitSounds); } } }


Вроде описали все что надо, даже больше чем нужно (в рамках этой статьи). Теперь отнаследуем от него класс корабля Ship , который должен уметь двигаться и поворачивать:


SpaceShip.cs

using UnityEngine; using System.Collections; using System.Collections.Generic; namespace Assets.Scripts.SpaceShooter.Bodies { public class Ship: BaseBody { public Vector2 _movement = new Vector2(); public Vector2 _target = new Vector2(); public float _rotation = 0f; public void FixedUpdate() { float torque = ControlRotate(_rotation); Vector2 force = ControlForce(_movement); _rb2d.AddTorque(torque); _rb2d.AddRelativeForce(force); } public float ControlRotate(Vector2 rotate) { float result = 0f; return result; } public Vector2 ControlForce(Vector2 movement) { Vector2 result = new Vector2(); return result; } } }


Пока в нем нет ничего интересно, на текущий момент это просто класс-заглушка.


Также опишем базовый(абстрактный) класс для всех контроллеров ввода BaseInputController:


BaseInputController.cs

using UnityEngine; using Assets.Scripts.SpaceShooter.Bodies; namespace Assets.Scripts.SpaceShooter.InputController { public enum eSpriteRotation { Rigth = 0, Up = -90, Left = -180, Down = -270 } public abstract class BaseInputController: MonoBehaviour { public GameObject _agentObject; public Ship _agentBody; // Ссылка на компонент логики корабля public eSpriteRotation _spriteOrientation = eSpriteRotation.Up; //Это связано с нестандартной // ориентации спрайта "вверх" вместо "вправо" public abstract void ControlRotate(float dt); public abstract void ControlForce(float dt); public virtual void Start() { _agentObject = gameObject; _agentBody = gameObject.GetComponent(); } public virtual void FixedUpdate() { float dt = Time.fixedDeltaTime; ControlRotate(dt); ControlForce(dt); } public virtual void Update() { //TO DO } } }


И наконец, класс контроллера игрока PlayerFigtherInput :


PlayerInput.cs

using UnityEngine; using Assets.Scripts.SpaceShooter.Bodies; namespace Assets.Scripts.SpaceShooter.InputController { public class PlayerFigtherInput: BaseInputController { public override void ControlRotate(float dt) { // Определяем позицию мыши относительно игрока Vector3 worldPos = Input.mousePosition; worldPos = Camera.main.ScreenToWorldPoint(worldPos); // Сохраняем координаты указателя мыши float dx = -this.transform.position.x + worldPos.x; float dy = -this.transform.position.y + worldPos.y; //Передаем направление Vector2 target = new Vector2(dx, dy); _agentBody._target = target; //Вычисляем поворот в соответствии с нажатием клавиш float targetAngle = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg; _agentBody._targetAngle = targetAngle + (float)_spriteOrientation; } public override void ControlForce(float dt) { //Передаем movement _agentBody._movement = Input.GetAxis("Vertical") * Vector2.up + Input.GetAxis("Horizontal") * Vector2.right; } } }


Вроде бы закончили, теперь наконец можно перейти к тому, ради чего все это затевалось, т.е. ПИД-регуляторам (не забыли надеюсь?). Его реализация кажется простой до безобразия:


using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Assets.Scripts.Regulator { // Этот атрибут необходим для того что бы поля регулятора // отображались в инспекторе и сериализовывались public class SimplePID { public float Kp, Ki, Kd; private float lastError; private float P, I, D; public SimplePID() { Kp = 1f; Ki = 0; Kd = 0.2f; } public SimplePID(float pFactor, float iFactor, float dFactor) { this.Kp = pFactor; this.Ki = iFactor; this.Kd = dFactor; } public float Update(float error, float dt) { P = error; I += error * dt; D = (error - lastError) / dt; lastError = error; float CO = P * Kp + I * Ki + D * Kd; return CO; } } }

Значения коэффициентов по умолчанию возьмем с потолка: это будет тривиальный единичный коэффициент пропорционального закона управления Kp = 1, небольшое значение коэффициента для дифференциального закона управления Kd = 0.2, который должен устранить ожидаемые колебания и нулевое значение для Ki, которое выбрано потому, что в нашей программной модели нет никаких статичных ошибок (но вы всегда можете их внести, а потом героически побороться с помощью интегратора).


Теперь вернемся к нашему классу SpaceShip и попробуем заюзать наше творение в качестве регулятора поворота космического корабля в методе ControlRotate:


public float ControlRotate(Vector2 rotate) { float MV = 0f; float dt = Time.fixedDeltaTime; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); //Получаем корректирующее ускорение MV = _angleController.Update(angleError, dt); return MV; }

ПИД-регулятор будет осуществлять точное угловое позиционировая космического корабля только за счет крутящего момента . Все честно, физика и САУ, почти как в реальной жизни.


И без этих ваших Quaternion.Lerp

if (!_rb2d.freezeRotation) rb2d.freezeRotation = true; float deltaAngle = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); float T = dt * Mathf.Abs(_rotationSpeed / deltaAngle); // Трансформируем угол в вектор Quaternion rot = Quaternion.Lerp(_myTransform.rotation, Quaternion.Euler(new Vector3(0, 0, targetAngle)), T); // Изменяем поворот объекта _myTransform.rotation = rot;


Получившейся исходный код Ship.cs под спойлером

using UnityEngine; using Assets.Scripts.Regulator; namespace Assets.Scripts.SpaceShooter.Bodies { public class Ship: BaseBody { public GameObject _flame; public Vector2 _movement = new Vector2(); public Vector2 _target = new Vector2(); public float _targetAngle = 0f; public float _angle = 0f; public SimplePID _angleController = new SimplePID(); public void FixedUpdate() { float torque = ControlRotate(_targetAngle); Vector2 force = ControlForce(_movement); _rb2d.AddTorque(torque); _rb2d.AddRelativeForce(force); } public float ControlRotate(float rotate) { float MV = 0f; float dt = Time.fixedDeltaTime; _angle = _myTransform.eulerAngles.z; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_angle, rotate); //Получаем корректирующее ускорение MV = _angleController.Update(angleError, dt); return MV; } public Vector2 ControlForce(Vector2 movement) { Vector2 MV = new Vector2(); //Кусок кода спецэффекта работающего двигателя ради if (movement != Vector2.zero) { if (_flame != null) { _flame.SetActive(true); } } else { if (_flame != null) { _flame.SetActive(false); } } MV = movement; return MV; } } }


Все? Расходимся по домам?



WTF! Что происходит? Почему корабль поворачивается как-то странно? И почему он так резко отскакивает от других объектов? Неужели этот глупый ПИД-регулятор не работает?


Без паники! Давайте попробуем разобраться что происходит.


В момент получения нового значения SP, происходит резкий (ступенчатый) скачок рассогласования ошибки, которая, как мы помним, вычисляется вот так: соответственно происходит резкий скачок производной ошибки , которую мы вычисляем в этой строчке кода:


D = (error - lastError) / dt;

Можно, конечно, попробовать другие схемы дифференцирования , например, трехточечную, или пятиточечную, или… но все равно это не поможет. Ну вот не любят производные резких скачков - в таких точках функция не является дифференцируемой . Однако поэкспериментировать с разными схемами дифференцирования и интегрирования стоит, но потом и не в этой статье.


Думаю что настал момент построить графики переходного процесса : ступенчатое воздействие от S(t) = 0 в SP(t) = 90 градусов для тела массой в 1 кг, длинной плеча силы в 1 метр и шагом сетки дифференцирования 0.02 с - прям как в нашем примере на Unity3D (на самом деле не совсем, при построении этих графиков не учитывалось, что момент инерции зависит от геометрии твердого тела, поэтому переходный процесс будет немножко другой, но все же достаточно похожий для демонстрации). Все величены на грифике приведены в абсолютных значениях:


Хм, что здесь происходит? Куда улетел отклик ПИД-регулятора?


Поздравляю, мы только что столкнулись с таким явлением как "удар" (kick). Очевидно, что в момент времени, когда процесс еще PV = 0, а уставка уже SP = 90, то при численном дифференцировании получим значение производной порядка 4500, которое умножится на Kd=0.2 и сложится с пропорциональным теромом, так что на выходе мы получим значение углового ускорения 990, а это уже форменное надругательство над физической моделью Unity3D (угловые скорости будут достигать 18000 град/с… я думаю это предельное значение угловой скорости для RigidBody2D).


  • Может стоит подобрать коэффициенты ручками, так чтобы скачок был не таким сильным?
  • Нет! Самое лучше чего мы таким образом сможем добиться - небольшая амплитуда скачка производной, однако сам скачок как был так и останется, при этом можно докрутиться до полной неэффективности дифференциальной составляющей.

Впрочем можете поэкспериментировать.

Попытка номер два. Сатурация

Логично, что привод (в нашем случае виртуальные маневровые двигатели SpaceShip), не может отрабатывать сколько угодно большие значения которые может выдать наш безумный регулятор. Так что первое что мы сделаем - сатурируем выход регулятора:


public float ControlRotate(Vector2 rotate, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); //Получаем корректирующее ускорение CO = _angleController.Update(angleError, dt); //Сатурируем MV = CO; if (MV > thrust) MV = thrust; if (MV< -thrust) MV = -thrust; return MV; }

А очередной раз переписанный класс Ship полностью выглядит так

namespace Assets.Scripts.SpaceShooter.Bodies { public class Ship: BaseBody { public GameObject _flame; public Vector2 _movement = new Vector2(); public Vector2 _target = new Vector2(); public float _targetAngle = 0f; public float _angle = 0f; public float _thrust = 1f; public SimplePID _angleController = new SimplePID(0.1f,0f,0.05f); public void FixedUpdate() { _torque = ControlRotate(_targetAngle, _thrust); _force = ControlForce(_movement); _rb2d.AddTorque(_torque); _rb2d.AddRelativeForce(_force); } public float ControlRotate(float targetAngle, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); //Получаем корректирующее ускорение CO = _angleController.Update(angleError, dt); //Сатурируем MV = CO; if (MV > thrust) MV = thrust; if (MV< -thrust) MV = -thrust; return MV; } public Vector2 ControlForce(Vector2 movement) { Vector2 MV = new Vector2(); if (movement != Vector2.zero) { if (_flame != null) { _flame.SetActive(true); } } else { if (_flame != null) { _flame.SetActive(false); } } MV = movement * _thrust; return MV; } public void Update() { } } }


Итоговая схема нашего САУ тогда станет уже вот такой


При этом уже становится понятно, что выход контроллера CO(t) немного не одно и тоже, что управляемая величина процесса MV(t) .


Собственно с этого места можно уже добавлять новую игровую сущность - привод , через которую и будет осуществляться управление процессом, логика работы которой может быть более сложной, чем просто Mathf.Clamp(), например, можно ввести дискретизацию значений (дабы не перегружать игровую физику величинами идущими шестыми после запятой), мертвую зону (опять таки не имеет смысл перегружать физику сверхмалыми реакциями), ввести задержку в упраление и нелинейность (например, сигмоиду) привода, после чего посмотреть, что из этого получится.


Запустив игру, мы обнаружим, что космический корабль стал наконец управляемым:



Если построить графики, то можно увидеть, что реакция контроллера стала уже вот такой:


Здесь уже используются нормированные величены, углы поделены на значение SP, а выход контроллера отнормирован относительно максимального значения на котором уже происходит сатурация.

Ниже приведена известна таблица влияния увеличения параметров ПИД-регулятора (как уменьшить шрифт, а то таблица безе переносов не лезет? ):



А общий алгоритм ручной настройки ПИД-регулятора следующий:


  1. Подбираем пропорциональный коэффициенты при отключенных дифференциальных и интегральных звеньях до тех пор пока не начнутся автоколебания.
  2. Постепенно увеличивая дифференциальную составляющую избавляемся от автоколебаний
  3. Если наблюдается остаточная ошибка регулирования (смещение), то устраняем её за счет интегральной составляющей.

Каких-то общих значений параметров ПИД-регулятора нет: конкретные значения зависят исключительно от параметров процесса (его передаточной характеристики): ПИД-регулятор отлично работающий с одним объектом управления окажется неработоспособным с другим. Более того, коэффициенты при пропорциональной, интегральной и дифференциальной составляющих еще и взаимозависимы.


Попытка номер три. Еще раз производные

Приделав костыль в виде ограничения значений выхода контроллера мы так и не решили самую главную проблему нашего регулятора - дифференциальная составляющая плохо себя чувствует при ступенчатом изменении ошибки на входе регуляторе. На самом деле есть множество других костылей, например, в момент скачкообразного изменения SP "отключать" дифференциальную составляющую или же поставить фильтры нижних частот между SP(t) и операцией за счет которого будет происходить плавное нарастание ошибки, а можно совсем развернуться и впендюрить самый настоящий фильтр Калмана для сглаживания входных данных. В общем костылей много, и добавить наблюдателя конечно хотелось бы, но не в этот раз.


Поэтому снова вернемся к производной ошибки рассогласования и внимательно на неё посмотрим:



Ничего не заметили? Если хорошенько присмотреться, то можно обнаружить, что вообще-то SP(t), не меняется во времени (за исключением моментов ступенчатого изменения, когда регулятор получает новую команду), т.е. её производная равна нулю:





Иными словами, вместо производной ошибки, которая дифференцируема не везде мы можем использовать производную от процесса, который в мире классической механики как правило непрерывен и дифференцируем везде, а схема нашей САУ уже приобретет следующий вид:




Модифицируем код регулятора:


using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Assets.Scripts.Regulator { public class SimplePID { public float Kp, Ki, Kd; private float P, I, D; private float lastPV = 0f; public SimplePID() { Kp = 1f; Ki = 0f; Kd = 0.2f; } public SimplePID(float pFactor, float iFactor, float dFactor) { this.Kp = pFactor; this.Ki = iFactor; this.Kd = dFactor; } public float Update(float error, float PV, float dt) { P = error; I += error * dt; D = -(PV - lastPV) / dt; lastPV = PV; float CO = Kp * P + Ki * I + Kd * D; return CO; } } }

И немного изменим метод ControlRotate:


public float ControlRotate(Vector2 rotate, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); //Получаем корректирующее ускорение CO = _angleController.Update(angleError, _myTransform.eulerAngles.z, dt); //Сатурируем MV = CO; if (CO > < -thrust) MV = -thrust; return MV; }

И-и-и-и… если запустить игру, то обнаружиться, что на самом деле ничего ничего не изменилось с последней попытки, что и требовалось доказать. Однако, если убрать сатурацию, то график реакции регулятора будет выглядеть вот так:


Скачок CO(t) по прежнему присутствует, однако он уже не такой большой как был в самом начале, а самое главное - он стал предсказуемым, т.к. обеспечивается исключительно пропорциональной составляющей, и ограничен максимально возможной ошибкой рассогласования и пропорциональным коэффициентом ПИД-регулятора (а это уже намекает на то, что Kp имеет смысл выбрать все же меньше единицы, например, 1/90f), но не зависит от шага сетки дифференцирования (т.е. dt ). В общем, я настоятельно рекомендую использовать именно производную процесса, а не ошибки.


Думаю теперь никого не удивит, но таким же макаром можно заменить на , однако останавливаться на этом мы не будем, можете сами поэкспериментировать и рассказать в комментариях, что из этого получилось (самому интересно)

Попытка номер четыре. Альтернативные реализации ПИД-регулятор

Помимо описанного выше идеального представления ПИД-регулятора, на практике часто применяется стандартная форма, без коэффициентов Ki и Kd , вместо которых используются временные постоянные.


Такой подход связан с тем, что ряд методик настройки ПИД-регулятора основан на частотных характеристиках ПИД-регулятора и процесса. Собственно вся ТАУ и крутится вокруг частотных характеристик процессов, поэтому для желающих углубиться, и, внезапно, столкнувшихся с альтернативной номенклатурой, приведу пример т.н. стандартной формы ПИД-регулятора:




где, - постоянная дифференцирования, влияющая на прогнозирование состояния системы регулятором,
- постоянная интегрирования, влияющая на интервал усреднения ошибки интегральным звеном.


Основные принципы настройки ПИД-регулятора в стандартной форме аналогичны идеализированному ПИД-регулятору:

  • увеличение пропорционального коэффициента увеличивает быстродействие и снижает запас устойчивости;
  • с уменьшением интегральной составляющей ошибка регулирования с течением времени уменьшается быстрее;
  • уменьшение постоянной интегрирования уменьшает запас устойчивости;
  • увеличение дифференциальной составляющей увеличивает запас устойчивости и быстродействие

Исходный код стандартной формы, вы можете найти под спойлером

namespace Assets.Scripts.Regulator { public class StandartPID { public float Kp, Ti, Td; public float error, CO; public float P, I, D; private float lastPV = 0f; public StandartPID() { Kp = 0.1f; Ti = 10000f; Td = 0.5f; bias = 0f; } public StandartPID(float Kp, float Ti, float Td) { this.Kp = Kp; this.Ti = Ti; this.Td = Td; } public float Update(float error, float PV, float dt) { this.error = error; P = error; I += (1 / Ti) * error * dt; D = -Td * (PV - lastPV) / dt; CO = Kp * (P + I + D); lastPV = PV; return CO; } } }

В качестве значений по умолчанию, выбраны Kp = 0.01, Ti = 10000, Td = 0.5 - при таких значениях корабль поворачивается достаточно быстро и обладает некоторым запасом устойчивости.


Помимо такой формы ПИД-регулятора, часто используется т.н. реккурентная форма :



Не будем на ней останавливаться, т.к. она актуальна прежде всего для хардверных программистов, работающих с FPGA и микроконтроллерами, где такая реализация значительно удобнее и эффективнее. В нашем же случае - давайте что-нибудь сваям на Unity3D - это просто еще одна реализация ПИД-контроллера, которая ни чем не лучше других и даже менее понятная, так что еще раз дружно порадуемся как хорошо программировать в уютненьком C#, а не в жутком и страшном VHDL, например.

Вместо заключения. Куда бы еще присобачить ПИД-регулятор

Теперь попробуем немного усложнить управление корабля используя двухконтурное управление: один ПИД-регулятор, уже знакомый нам _angleController, отвечает по прежнему за угловое позиционирование, а вот второй - новый, _angularVelocityController - контролирует скорость поворота:


public float ControlRotate(float targetAngle, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; _angle = _myTransform.eulerAngles.z; //Контроллер угла поворота float angleError = Mathf.DeltaAngle(_angle, targetAngle); float torqueCorrectionForAngle = _angleController.Update(angleError, _angle, dt); //Контроллер стабилизации скорости float angularVelocityError = -_rb2d.angularVelocity; float torqueCorrectionForAngularVelocity = _angularVelocityController.Update(angularVelocityError, -angularVelocityError, dt); //Суммарный выход контроллера CO = torqueCorrectionForAngle + torqueCorrectionForAngularVelocity; //Дискретизируем с шагом 100 CO = Mathf.Round(100f * CO) / 100f; //Сатурируем MV = CO; if (CO > thrust) MV = thrust; if (CO < -thrust) MV = -thrust; return MV; }

Назначение второго регулятора - гашение избыточных угловых скоростей, за счет изменения крутящего момента - это сродни наличию углового трения, которое мы отключили еще при создании игрового объекта. Такая схема управления [возможно] позволит получить более стабильное поведение корабля, и даже обойтись только пропорциональными коэффициентами управления - второй регулятор будет гасить все колебания, выполняя функцию, аналогичную дифференциальной составляющей первого регулятора.


Помимо этого, добавим новый класс ввода игрока - PlayerInputCorvette, в котором повороты буду осуществляться уже за счет нажатия клавиш "вправо-влево", а целеуказание с помощью мыши мы оставим для чего-нибудь более полезного, например, для управления турелью. Заодно у нас теперь появился такой параметр как _turnRate - отвечающий за скорость/отзывчивость поворота (не понятно только куда его поместить лучше в InputCOntroller или все же Ship).


public class PlayerCorvetteInput: BaseInputController { public float _turnSpeed = 90f; public override void ControlRotate() { // Находим указатель мыши Vector3 worldPos = Input.mousePosition; worldPos = Camera.main.ScreenToWorldPoint(worldPos); // Сохраняем относительные координаты указателя мыши float dx = -this.transform.position.x + worldPos.x; float dy = -this.transform.position.y + worldPos.y; //Передаем направление указателя мыши Vector2 target = new Vector2(dx, dy); _agentBody._target = target; //Вычисляем поворот в соответствии с нажатием клавиш _agentBody._rotation -= Input.GetAxis("Horizontal") * _turnSpeed * Time.deltaTime; } public override void ControlForce() { //Передаем movement _agentBody._movement = Input.GetAxis("Vertical") * Vector2.up; } }

Также для наглядности накидаем на коленках скрипт для отображения отладочной информации

namespace Assets.Scripts.SpaceShooter.UI { public class Debugger: MonoBehaviour { Ship _ship; BaseInputController _controller; List _pids = new List(); List _names = new List(); Vector2 _orientation = new Vector2(); // Use this for initialization void Start() { _ship = GetComponent(); _controller = GetComponent(); _pids.Add(_ship._angleController); _names.Add("Angle controller"); _pids.Add(_ship._angularVelocityController); _names.Add("Angular velocity controller"); } // Update is called once per frame void Update() { DrawDebug(); } Vector3 GetDiretion(eSpriteRotation spriteRotation) { switch (_controller._spriteOrientation) { case eSpriteRotation.Rigth: return transform.right; case eSpriteRotation.Up: return transform.up; case eSpriteRotation.Left: return -transform.right; case eSpriteRotation.Down: return -transform.up; } return Vector3.zero; } void DrawDebug() { //Направление поворота Vector3 vectorToTarget = transform.position + 5f * new Vector3(-Mathf.Sin(_ship._targetAngle * Mathf.Deg2Rad), Mathf.Cos(_ship._targetAngle * Mathf.Deg2Rad), 0f); // Текущее направление Vector3 heading = transform.position + 4f * GetDiretion(_controller._spriteOrientation); //Угловое ускорение Vector3 torque = heading - transform.right * _ship._Torque; Debug.DrawLine(transform.position, vectorToTarget, Color.white); Debug.DrawLine(transform.position, heading, Color.green); Debug.DrawLine(heading, torque, Color.red); } void OnGUI() { float x0 = 10; float y0 = 100; float dx = 200; float dy = 40; float SliderKpMax = 1; float SliderKpMin = 0; float SliderKiMax = .5f; float SliderKiMin = -.5f; float SliderKdMax = .5f; float SliderKdMin = 0; int i = 0; foreach (SimplePID pid in _pids) { y0 += 2 * dy; GUI.Box(new Rect(25 + x0, 5 + y0, dx, dy), ""); pid.Kp = GUI.HorizontalSlider(new Rect(25 + x0, 5 + y0, 200, 10), pid.Kp, SliderKpMin, SliderKpMax); pid.Ki = GUI.HorizontalSlider(new Rect(25 + x0, 20 + y0, 200, 10), pid.Ki, SliderKiMin, SliderKiMax); pid.Kd = GUI.HorizontalSlider(new Rect(25 + x0, 35 + y0, 200, 10), pid.Kd, SliderKdMin, SliderKdMax); GUIStyle style1 = new GUIStyle(); style1.alignment = TextAnchor.MiddleRight; style1.fontStyle = FontStyle.Bold; style1.normal.textColor = Color.yellow; style1.fontSize = 9; GUI.Label(new Rect(0 + x0, 5 + y0, 20, 10), "Kp", style1); GUI.Label(new Rect(0 + x0, 20 + y0, 20, 10), "Ki", style1); GUI.Label(new Rect(0 + x0, 35 + y0, 20, 10), "Kd", style1); GUIStyle style2 = new GUIStyle(); style2.alignment = TextAnchor.MiddleLeft; style2.fontStyle = FontStyle.Bold; style2.normal.textColor = Color.yellow; style2.fontSize = 9; GUI.TextField(new Rect(235 + x0, 5 + y0, 60, 10), pid.Kp.ToString(), style2); GUI.TextField(new Rect(235 + x0, 20 + y0, 60, 10), pid.Ki.ToString(), style2); GUI.TextField(new Rect(235 + x0, 35 + y0, 60, 10), pid.Kd.ToString(), style2); GUI.Label(new Rect(0 + x0, -8 + y0, 200, 10), _names, style2); } } } }


Класс Ship также претерпел необратимые мутации и теперь должен выглядеть вот так:

namespace Assets.Scripts.SpaceShooter.Bodies { public class Ship: BaseBody { public GameObject _flame; public Vector2 _movement = new Vector2(); public Vector2 _target = new Vector2(); public float _targetAngle = 0f; public float _angle = 0f; public float _thrust = 1f; public SimplePID _angleController = new SimplePID(0.1f,0f,0.05f); public SimplePID _angularVelocityController = new SimplePID(0f,0f,0f); private float _torque = 0f; public float _Torque { get { return _torque; } } private Vector2 _force = new Vector2(); public Vector2 _Force { get { return _force; } } public void FixedUpdate() { _torque = ControlRotate(_targetAngle, _thrust); _force = ControlForce(_movement, _thrust); _rb2d.AddTorque(_torque); _rb2d.AddRelativeForce(_force); } public float ControlRotate(float targetAngle, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; _angle = _myTransform.eulerAngles.z; //Контроллер угла поворота float angleError = Mathf.DeltaAngle(_angle, targetAngle); float torqueCorrectionForAngle = _angleController.Update(angleError, _angle, dt); //Контроллер стабилизации скорости float angularVelocityError = -_rb2d.angularVelocity; float torqueCorrectionForAngularVelocity = _angularVelocityController.Update(angularVelocityError, -angularVelocityError, dt); //Суммарный выход контроллера CO = torqueCorrectionForAngle + torqueCorrectionForAngularVelocity; //Дискретизируем с шагом 100 CO = Mathf.Round(100f * CO) / 100f; //Сатурируем MV = CO; if (CO > thrust) MV = thrust; if (CO < -thrust) MV = -thrust; return MV; } public Vector2 ControlForce(Vector2 movement, float thrust) { Vector2 MV = new Vector2(); if (movement != Vector2.zero) { if (_flame != null) { _flame.SetActive(true); } } else { if (_flame != null) { _flame.SetActive(false); } } MV = movement * thrust; return MV; } public void Update() { } } }

Еще немного ссылок на другие примеры

Для процессов системы требуется способность параметров к реагированию на внешнее действие и поддержание системных постоянных величин. Для примера, система насосов с клапанами отвода. Для каждого клапана поддержание потока в постоянном виде обеспечивает постоянное давление в трубах. Помпа в системе приводится в действие приводом, при открывании клапана скорость двигателя увеличивается и снижается при закрытии, чтобы поддерживать давление в трубах на одном уровне.

Настраивание ПИД-регулятора общего вида

Для такого поддержания давления существует прибор, который называется регулятором задания. Давление в трубах на датчике идет в сравнение с параметром заданного давления. Регулятор сравнивает системное давление с давлением задания, определяет задачу скорости для двигателя для изменения ошибки. Простой вид регулятора применяет план действий ПИД-регулирования. В нем применяются три составляющие типа регуляторов для удаления ошибки: дифференциальный, интегральный и пропорциональный регулятор.

Регулятор пропорционального типа

Такой регулятор – главный, скорость задается в прямой зависимости от ошибки. При применении пропорционального регулятора система будет иметь ошибку. Малые значения коэффициента регулятора пропорционального типа дают вялость системы, а высокие параметры к колебаниям и нестабильности системы.

Регулятор интегрального типа

Такой регулятор применяется для удаления ошибки. Скорость увеличится до удаления ошибки (снизится при негативной ошибке). Небольшие значения суммирующей составляющей слишком оказывают влияние на деятельность регулятора в общем. При установлении больших значений происходит промахивание системы, она функционирует с перерегулированием.

Регулятор дифференциального типа

Такой регулятор измеряет скорость корректировки ошибки, применяет для повышения системного быстродействия, увеличивает регуляторное быстродействие в общем. Во время увеличения быстродействия регулятора повышается перерегулирование. Это обуславливает к системной нестабильности. Во многих случаях составляющая дифференциальная становится равной нулю или близкой к наименьшему значению для того, чтобы предотвратить это состояние. Она бывает полезной в позиционирующей системе.

Работа регулятора в обратном и прямом действии

Множество регуляторов имеют принцип прямого действия. Повышение скорости двигателя приводит к повышению переменной величины процесса. Это случай в системе насосов, давление это величина переменная процесса. Повышение скорости двигателя обуславливает повышение давления. Во многих системах повышение скорости двигателя обуславливает к снижению параметра переменной процесса. Температура вещества, которое обдувается вентиляционной системой теплообменника – процессная переменная величина: при повышении скорости вентиляционной системы температура вещества снижается. В этом разе нужно применить регулятор действия обратного вида.

Настраивание ПИД-регулятора

Для моторной управляемости системы настраивание ПИД-регулятора бывает сложным процессом. Расскажем, какие шаги для настройки могут сделать проще эту процедуру.

  1. Определите значение дифференциальной и интегральной равной нулю. Определите наибольшую скорость и контролируйте системную реакцию.
  2. Повышайте составляющую прямопропорционально и выполните первый пункт. Продолжайте действия до момента начала процесса с автоматическими колебаниями возле точки определения скорости.
  3. Снижайте пропорциональную величину, пока система не стабилизируется. Волны колебаний начнут затухать.
  4. Определите пропорциональную величину около 15% меньше этого постоянного пункта.
  5. Определяйте наибольшую скорость прерывисто, повышайте суммирующую составляющую до начала уменьшения колебаний скорости перед стабильным состоянием системы. Снижайте суммирующую составляющую до достижения системой определенной скорости без ошибки и колебаний.
  6. Во многих системах настраивание составляющей дифференциального вида не нужно. Если нужно быстродействие системы больше, то можно достигнуть этого путем настройки составляющей дифференциального вида. Устанавливайте скорость по интервалам, повышайте составляющую дифференциального вида, пока не стабилизируется система с наименьшим временем действия (повышайте медленно, избегая состояния нестабильности). Система станет оптимальной при одном перерегулировании.
  7. Контролируйте стабильность системы, устанавливая значения скорости с интервалами и периодами для гарантированной стабильности системы при плохом исполнении задания.

Настраивание датчика на 20 миллиампер ПИД-регулированием

1. Действия в программном меню

3. Подсоединение датчика (выход на 20 миллиампер)

  1. Установку производить при .
  2. Напряжение датчика подсоединить к контакту «+24В», сигнал соединить с контактом «AI1», установить перемычку на контакты «COM» и «GND».

Переставить соединение «J1» в состояние «I».

4. Контроль обратной связи

  1. Подключите напряжение на частотный преобразователь, на экране возникнет подсветка 50 герц.
  2. Нажмите клавишу «сдвиг» 2 раза.
  3. На экране будет параметр обратной связи в интервале 0-10 (0-20 мА), зависит от настраиваемого параметра.

Связь обратного вида (4 мА).

  1. После подтверждения обратной связи нажмите три раза клавишу «сдвиг», появится на экране 50 герц.
  2. Установите наименьшее значение сигнала входа в величине Р4-13=2.00 (4 мА).

5.Как настраивать значение параметра ПИД-регулирования.

  1. Установите источник основной частоты Р0-03=8 (частоту определяет ПИД-регулятор).
  2. Поставьте значение ПИД-регулятора в значение РА-01= результат поддерживаемой величины в процентах (от 0 до 100%) от интервала датчика, РА-01= (результат поддерживаемого параметра/интервал датчика)*100%.

Пример установки значения:

Подсоединен датчик давления на 16 бар с сигналом выхода от 4 до 20 мА. Для давления в 10 бар нужно установить значение

РА-01=(10/16)*100%=62,5%

Произведите тестовый пуск. Проверяйте поддерживаемое значение параметра по приборам, дублирующим измерения (ротаметр, термометр, манометр). Если система регулировки функционирует нестабильно или долгий отклик на замену проверяемого параметра, то применяйте настройки значений РА-05, -06, -07. Эти значения предназначены для точной настройки ПИД-регулятора.

Пример использования регулирования ПИД

Данные

  1. Механизм вентиляторного управления.
  2. Характеристика градуировочная датчика давления, интервал 1000-5000 Па, ток 4-20 мА.
  3. Значение давления 1500 Па.
  4. Мощность механизма и инерционные данные вентилятора отсутствуют.

Наружные подключения

Датчик обратной связи подсоединен к токовому входу аналогового типа, датчик значения уставки к входу аналогового типа напряжения.

Обратная связь

Датчик связи определен по токовому выходу, входом связи обратного вида применяется токовый вход. Задается РR.10-00=02 (обратная связь с минусом по входу, повышение частоты выхода, повышает давление).

Отградуированная характеристика датчика

Сигнал связи обратного вида в масштабе

Вход связи обратного вида не создает масштаб по усилению и смещению. Применяя параметр PR10-01 можно изменять значение сигнала связи обратного вида в расчетах.

Применение параметра PR10-01 для корректировки значения сигнала связи обратного типа.

Значением PR10-01 можно корректировать значение сигнала связи обратного вида, который применяется в вычислениях. Интервал пропорциональности 0-10, по настройкам завода 1.

Сигнал связи обратного вида повышается в 2 раза перед установкой в ПИД-регулятор. Это равно снижению интервала входа в 2 раза.

Сигнал связи обратного вида снижается в 2 раза перед установкой в регулятор, это эквивалентно увеличению интервала входа в 2 раза. Сейчас интервал ограничен значением датчика.

Пример установки значения параметра PR10-01 (масштаб усиления обратной связи).

Интервал действия датчика:

1000Ра – 5000Ра.

Наибольшее давление функционирования: 2000Ра.

Применяемая часть интервала работы датчика (закрепленная): -1000Ра-2000Ра.

Это будет равно: 2000Ра –(-1000Ра)

5000Ра –(-1000Ра) = 50%

Если интервал действия не больше 2000Ра с датчиком, то величина параметра

PR10-01 = 1/50%=2

Формула вычисления параметра PR10-01.

Наибольший сигнал датчика: MaxVal

Наименьший сигнал датчика: MinVal

Наибольший нужный сигнал связи обратного вида MaxFBVal

Величина значения ПИД (установленная частота).

Установленную частоту можно изменять операторами наклона и перемещения опции преобразования.

Направление момента вращения установки вентилятора не изменяется, лучше применять AVI вход с заданием значения PR 02-00=01.

PR10-01 (наибольшая частота).

Задать в PR01-00 величину наибольшей вентиляции (PR01-00 = 50 герц).

Наименьшая частота.

Наименьшая частота не оказывает влияния на действие регулировки.

Наклон и перемещение опции преобразования.

Задать PR04-00 AVI перемещение интервала.

PR04-01 AVI полярность.

PR04-02 AVI корректировка наклона.

Вращение производится в одну сторону, PR04-03 = 0 (по заводским настройкам).

Величина уставки.

Для установки величины входа интервал частоты рассчитывается 0-100%.

Установка значения уставки.

При функционировании вентилятора давлению в 1500 Ра равен сигнал датчика 10,67 мА. Величине уставки 1500 Ра равна частота выхода 42%*50 герц = 21 герц и 84%*50 герц = 42 герц.

Можно устанавливать значение в Ра. Если 100% интервала равно 2000 Ра, то при коэффициенте 00-05 = 2000/Fmax = 2000/50 = 40, установленная величина 1500 и задается 1500 Ра.

Интервал частоты выхода.

Верхняя граница частоты выхода при регулировке определяется формулой:

Fmax=Pr01-00хPr10-07.

ПИД-регулирование

Ускорение – замедление.

При взаимодействии с регулированием ПИД нужно время ускорения и замедления устанавливать минимальным для качественной регулировки.

Настраивание регулятора:

  1. Задать величину I для легкого отклика, без перерегулировки.
  2. Значение параметра для вентилятора не нужно, из-за замедления процесса.
  3. Задать другие значения величин.

Советы по настраиванию:

  1. Повышение Р разгоняет процесс, снижает ошибки.
  2. При большом Р появляется неустойчивость процесса.
  3. Снижение величины I ускоряет процесс, делает нестабильным.
  4. Быстрота дает снижение Р и I.
  5. Замедление вентилятора определяет большего значения Р.
  6. Задайте время ускорения и замедления наименьшим.
Нужно держать температуру на заданном неком уровне и менять задание. Есть микроконтроллер, к которому прицеплены измеритель температуры, и симистор для управления мощностью. Не будем греть голову на ТАУ, ни разностными схемами , просто возьмём и сделаем «в лоб» ПИД-регулятор.

II. Теоретическая вводная

Как получается ПИД-регулятор? Берём разницу между текущей температурой и нужной, умножаем на настраиваемый коэффициент, получаем мощность, которую надо выдать в данный момент. Это пропорциональная составляющая, она работает в момент появления рассогласования - то есть моментально откликается как на изменение уставки, так и на поведение объекта. Начал подогреваться? Мощность начинает спадать. Перегрелся? Выключилось, или даже дали сигнал охлаждения. Всё хорошо, вот только в реальной жизни эффект от воздействия проявляется с запаздыванием, а на объект воздействуем не только мы, но еще и окружающая среда: разогретый реактор не только внутри горячий, но еще и остывает, отдавая тепло комнате, а потому как только выключаем мощность, он сразу начинает остывать. Поэтому чистый пропорциональный регулятор колеблется вокруг точки поддержания, и тем сильнее колеблется, чем выше воздействие окружающей среды / содержимого реактора.

Чтобы компенсировать «внешние» воздействия на реактор, в цепь добавляют интегральную составляющую. Всё рассогласование, которое было в системе, идёт на интегратор (соответственно, как только мы перегрели - сумма уменьшается, пока недогрето - сумма увеличивается). И накопленный интеграл, со своим коэффициентом, даёт свою прибавку-убавку к мощности. В результате такого подхода, при стационарном процессе, через некоторое время интеграл подбирает такой вклад в сумму с мощностью, который компенсирует потери окружающей среды, и колебания исчезают - интеграл становится стабильным, поэтому величина выдаваемой мощности становится постоянной. Причем так как при этом держится нужная температура, рассогласование отсутствует, пропорциональная составляющая не работает вообще.

Для компенсации влияния задержек между воздействием и реакцией системы, в систему добавляют дифференциальную составляющую. Просто пропорциональный регулятор даёт мощность всё время, пока температура не достигнет нужной точки, пропорционально-дифференциальный начинает снижать подаваемую мощность раньше, чем догрелся до нужной точки - так как рассогласование уменьшается, имеется наличие отрицательной производной, уменьшающей воздействие. Это позволяет минимизировать перегрев при больших переходах.

Итак, с физическим смыслом разобрались, перейдём к основым вопросам реализации.

III. Кому пользоваться регулятором?

- Техникам.

Что из этого следует? Из этого следует, что техники понимают физическую составляющую, и имеют опыт настройки аппаратных пид регуляторов. А значит, программная реализация должна исходить из удобства настройки техниками - повторяя физическую модель. И это крайне важно! Очень часто в угоду упрощения кода коэффициенты меняют, например, на обратные - чтобы избавиться от деления. В результате, настройка превращается в ад и кошмар, и требуется опыт настройки данного конкретного регулятора, вместо понимания процесса. Отсюда получаем, что наши коэффициенты - постоянная интегрирования и постоянная дифференцирования - должны иметь размерность времени, то есть задаваться в секундах, а никак не в «1/с», как это любят делать.

IV. Область функционирования.

Мы пытаемся сделать универсальный регулятор, а значит, он должен работать как на мелких быстрых объектах, так и на мощных большущих печах. Значит, следует исходить из того, что регулируемая температура ограничена в общем-то измерителем. Наиболее часто используемые - ХА(K) и ХК(L). Их область применимости - где-то до 1200°C. Охлаждение требует более сложного оборудования (криостаты), управление доп.охлаждением (вентиляторы и открываемые дверки термошкафов) также требуется редко - значит, пока исключаем из рассмотрения. Получаем, что управляемая температура от ~15°C до ~1200°C, управляется только подача мощности.

Точность управления определяется во-1х точностью измерения: градуировочные таблицы даны через 0.1 градуса; линейность внутри таблиц в принципе достойная, поэтому точность ограничена в первую очередь усилителем и измерителем тока. В моём случае, хотелось добиться точности поддержания 0.1 градуса, поэтому измеритель настроен на 1/32 градуса: это даёт ~3 кванта на 0.1 градуса, таким образом, имея нормальный «шум» регулирования +-1 квант мы остаёмся в пределах всё тех же 0.1 градуса. Использование 1/32 позволяет работать с фиксированной точкой - 5 бит = дробная часть, остальное - целая. В 16 бит это получается представить от 0 до 2047 °. Вместо работы с отрицательными числами, мы будем работать в кельвинах вместо цельсиев, таким образом - представляется от 0 до 2047 °K, что эквивалентно от -273 до 1775 °C; с шагом в 0,03125 °.

V. Диапазон настраиваемости.

Для управления микрореактором с мощной силовой установкой может оказаться что для нагрева на 10 градусов достаточно 1% мощности, в то время как для большой инертной печи для того чтобы подогреть на градус едва-едва хватает 100% мощности подогрева. (В реальной жизни, это выглядит так - есть несколько подогревателей с ручным управлением - они включаются отдельным рубильником и производят начальный нагрев, в дальнейшем поддержание рабочей точки обеспечивает терморегулятор, управляя еще одним подогревателем, который на полной мощности выдаёт максимум +10°C к тому, что нагрели постоянно включенные). Исходя из этого, предельным коэффициентом пропорциональности логично предположить 100% мощности на 1 градус. Больше не имеет смысла, так как мы хотим получить управляемость в 0.1 градуса. Минимальный, для простоты, я взял инверсным - 1% мощности на 100 градусов.

Диапазоны временных коэффициентов вычисляются просто исходя из наших условий работы регулятора. Так как мы управляем через мощностью симистор путём вычисления задержки момента включения после прохождения через 0, предельная частота работы регулятора - 50Гц. Если мы уверены, что управляем мощностью которой пофиг плюс или минус, мы можем работать на 100Гц, но это не всегда так, и потому лучше каждый раз дозировать равное количество как положительной так и отрицательной полуволны. Для упрощения жизни, я снизил время работы до 25Гц, тем самым любое вычисленное воздействие будет действовать в течение 4 полуволн, и за это время у меня будет возможность рассчитать новое воздействие.

Таким образом, постоянные времени задаются через 1/25 сек, от 0 до ~2000 сек (2000*25 = 50000, как раз в 16бит влазит).

Ну и еще у нас есть ограничение мощности минимальное и максимальное, от 0 до 100%.

VI. Управление мощностью.

Начиная с этого момента все теоретические выкладки заканчиваются, начинается горькая практика, привязанная к конкретной реализации.

Итак, мы уже решили что управляем задержкой открывания симистора после прохождения через 0. Таким образом, задержка в 0 означает 100% мощность, бесконечная задержка = 0% мощности.

Вопрос: с какой точностью мы можем управлять мощностью? Вообще, с точностью отсчета времени нашего таймера. С другой стороны, какая нужна мощность? Мы вычисляем какой % мощности нужно подать на 0.04сек. В принципе, по опыту, управления мощностью даже с точностью в 1% на частоте в 0.1сек хватает для поддержания температуры в 1 градус. У нас управление 0.04сек (в 2.5раза быстрее). Поэтому было принято решение рассчитать таблицу мощности через 1/250 от максимума (с шагом в 0.4%). Это позволяет таблицу иметь не сильно большую (500 байт), и при этом иметь точность выше 1%. Если ваш случай требует бОльшей точности - пересчитать не так сложно.

Теперь поговорим о расчете этой самой таблицы. Во-1х следует учесть, что есть момент срабатывания сигнала прохождения через ноль. В моем случае - 12В. То есть когда входное напряжение упадёт ниже 12В, я получу сигнал прохождения через 0.

Это означает, что для 100% мощности время запуска = времени прохождения 12В.

Решим систему уравнений

; IntMoment:= 12V ; Max:= sqr(220*sqrt(2)) ; { Sqr(Sin(Pi/2)*K) = Max ; { Sqr(Sin(X)*K) = IntMoment ; ; 2*k/MaxCode = 1 - cos(T*Pi) ; cos(T*Pi) = 1-2*k/MaxCode ; T*Pi = arccos(1-2*k/MaxCode) ; T = arccos(1-2*k/MaxCode) / Pi

Процессор у меня работает на частоте 32786, PLL настроен на 384/2, полуволна имеет 100Гц, откуда получаем, что код для загрузки константы в таймер для времени T имеет вид:

65536-(T*(32768*384/2)/100.0 + 773)

Нам нужно рассчитать время задержки, дающее равномерное увеличение площади включенной части синусоиды. То есть нам нужно иметь отсчеты времени, дающие равномерное увеличение мощности. Полная мощность, которую мы выдаём - это интеграл по всей синусоиде. [кто знает, как на хабре формулы вставлять? никак? пишу в maple-нотации тогда].

Max = int(sqr(sin(x)), x=0..Pi) int(sqr(sin(x)), x=0..T*Pi) = x/2 - sin(2*x)/4 + C | 0..T*PI = (T*Pi)/2 - sin(2*T*Pi)/4 (T*Pi)/2 - sin(2*T*Pi)/4 = Q*Pi/2

Таким образом, нам нужно пройтись по всем Q с заданной точностью, и для каждой из них найти T.

Я для себя это решил вот таким тупым способом:

Генератор на перле

#!/usr/bin/perl # (T*Pi)/2 - sin(2*T*Pi)/4 = Q*Pi/2 use constant PI => 4 * atan2(1, 1); $T = 1; for($i = 250; $i >= 0; $i--) { $int = $i*PI/2/250; $ev = ($T*PI)/2-sin(2*$T*PI)/4; while(abs($ev-$int) > 0.0005) { $T -= 0.0001; $ev = ($T*PI)/2-sin(2*$T*PI)/4; } #print $i."\t".$T."\n"; $code = 65536-($T*(32768*384/2)/100.0 + 773); printf "DB 0%02Xh, 0%02Xh ; %04Xh = $i/250 of power\n", $code%256, int($code/256), $code, $i; }

Всё, на выходе мы получили табличку в 250 значений, соответствующих константам загрузки таймера до момента поджига после получения сигнала о прохождении через 0 (точнее, через 12В, как я говорил выше).

VII. Измерение входных данных

Я пропускаю этот вопрос, потому как он достоен отдельной большой статьи. О том, как я решал вопрос с термосопротивлением, можно найти в архиве почившего в бозе моего блога.

Главное что нам надо знать, это что мы измеряем данные с нужной нам частотой (в данном случае - 25Гц), и нужной точностью (на выходе - число от 0 до 2048 градусов кельвина через 1/32 градуса). Данные предполагаются уже нормализованные для всех дальнейших расчетов.

Если будет кому интересно - пишите в комментах, распишу в следующий раз как это делается для термопар.

VIII. Вычисление воздействия

И вот свершилось: у нас есть все данные для того, чтобы наконец-то произвести то, ради чего мы всё затевали: вычислить какую же мощность следует подать на управляющий элемент.

Вспомним еще раз формулу ПИД регулятора:

U = K * (Err + (1/Ti)*Int + Td*dErr)

  • U - мощность, которую следует выдать;
  • K - пропорциональный коэффициент (обратите внимание - вынесен за скобки, почему - чуть ниже опишу);
  • Ti - постоянная времени интегрирования. Обратите внимание - в расчетах используется обратная величина;
  • Td - постоянная времени дифференцирования
  • Err - текущее рассогласование (разница между уставкой и измеренной температурой
  • dErr - производная рассогласования (разница между текущей и прошлой ошибкой)
  • Int - накопленный интеграл рассогласования (сумма всех Err"ов, кои мы видели)

Мы снова пришли к вопросу, который поднимался в разделе III : этим будут пользоваться техники. Поэтомоу крайне важно не допустить классической ошибки всех реализаций - «размерности коэффициентов как получится». Мы делаем прибор для управления физическим процессом, а значит, модель должна соответствовать.

Произведём вывод всех размерностей. Частично забегая вперёд я уже описал в , но теперь раскроем подробнее:

  • U - имеет величину в % мощности. Еще точнее - в 2/5 от % мощности, так как у нас таблица идёт через 1/250 от 100%.
  • Err - рассогласование, задаётся в градусах. Точнее - через 1/32 градуса.
  • Int - интеграл, представляет собой сумму градусов во времени - а значит, имеет размерность градус*сек. Точнее - (1/32 градуса)*(1/25 сек)
  • Ti - задаётся через 1/25 сек
  • (1/Ti)*Int - после вычисления даёт вклад, имеющий размерность (1/32 градуса).
  • dErr - производная, имеет размерность градус/сек, а точнее (1/32 градуса)/(1/25 сек)
  • Td - задаётся через 1/25 сек
  • Td*dErr - после произведения приводит вклад к размерности (1/32 градуса)
  • (...) - итак, все слагаемые под скобками приведены к размерности (1/32 градуса)
  • K - согласует U и (...) , а значит имеет размерность процента-на-градус, точнее (2/5)%/(1/32 градуса)

Вот теперь хорошо видно, зачем выносится за скобки пропорциональный коэффициент - это позволяет оставить диф и инт коэффициенты просто постоянными времени, в результате оператор при настройке оперирует простыми и понятными числами - процентом на градус для пропорциональной и секундами для интегральной и дифференциальной коэффициентами.

А благодаря удобному подбору положения точек и размерностей времени, как мы сейчас увидим, все расчеты производятся практически «в лоб».

Кроме одного - у нас есть величина Ti , а для расчета требуется 1/Ti . Операция деления большой разрядности - очень дорогая. Операция умножения в разы дешевле, поэтому воспользуемся отличной статьёй Division by Invariant Integers using Multiplication . У нас ведь K / Ti / Td меняются крайне редко, а потому мы можем себе позволить как угодно извращаться с ними после их изменения, главное чтобы основной цикл расчетов работал быстро.

Таким образом, вместо Ti для расчетов мы раскладываем в набор Ti_m , Ti_sh1 , Ti_sh2 ; и на каждом цикле производим вычисление:
T1 = MULUH(Ti_m, Int) Q = SHR(T1+SHR(Int-T1, Ti_sh1), Ti_sh2)

Теперь производим расчет баланса разрядности. Для этого распишем полную формулу пошагово:

  1. Eo = E ; Нам нужна прошла ошибка. Ошибки - по 16бит
  2. E = Y-X ; Вычисляем новое рассогласование. 16bit
  3. Int = Int + (E+Eo)/2 ; Интегрируем ошибку. При этом считаем полусумму разности (разностная схема). 32bit = 32bit + 16bit
  4. cI = Int * (1/Ti) ; Считаем интегральный вклад - 32bit * 32bit => 32bit
  5. cD = Td * (E-Eo) ; Считаем диф вклад - 16*16 => 32bit
  6. PID = E + cI + cD ; Подскобочное; 16+32+32 => 32bit
  7. U = K*PID/256 ; Коэфф; 32*16/8 bit => 40bit.

При всех расчетах положение точки вплоть до 7го шага остаётся на 5м справа месте. В последний момент происходит интересный финт ушами. K задаётся через 1/256, соответственно, после умножения точка сдвигается влево до 5+8=13 места, поэтому мы должны у результата отбросить младшие 8 бит. И самый нижний байт результата - нужная нам мощность через 2/5%. Это - еще одна причина, по которой мощность выровнена по шагам в 1/250 - это позволяет результат уложить в один байт и получить легко по таблице нужный результат.

Дальше, помним, что нас интересует мощность только от 0 до 250 - поэтому 7й шаг вычислений идёт очень просто, как только мы получаем отрицательное число - сразу складываем uMin. Как только выяснили что любой старший байт не ноль - сразу складываем uMax. И только если мощность складывается в диапазоне - производим проверку на меньше uMin или больше uMax.

Если вдруг кому интересно:

полная портянка расчетов

; PID управление CalcMainEnd: ; Вычисления, Go-Go. CalcPid: ; 1. Eo = E | 16bit Pid1: MOV Err0H, ErrH MOV Err0L, ErrL ; 2. E = Y-X | 16bit Pid2: CLR C MOV A, SettingL SUBB A, ThermoL MOV ErrL, A MOV A, SettingH SUBB A, ThermoH MOV ErrH, A JNB OV, Pid2Ov JB ACC.7, Pid2Max Pid2Min: MOV ErrL, #LOW(-500*32) MOV ErrH, #HIGH(-500*32) SJMP Pid2End Pid2Max: MOV ErrL, #LOW(500*32) MOV ErrH, #HIGH(500*32) SJMP Pid2End Pid2Ov: JNB ACC.7, Pid2OvP Pid2OvN: ; Проверим на ограничение вниз CLR C MOV A, ErrL SUBB A, #LOW(-500*32) MOV A, ErrH SUBB A, #HIGH(-500*32) JNC Pid2End ; Если > -500 => всё ок SJMP Pid2Min Pid2OvP: CLR C MOV A, ErrL SUBB A, #LOW(500*32) MOV A, ErrH SUBB A, #HIGH(500*32) JNC Pid2Max ; Если < 500 => всё ок Pid2End: ; 3. Int = Int + (E+Eo)/2 | 32bit+16bit Pid3: JNB PowerReady, Pid3End ; Если нет сети -- интегральную часть не копим MOV A, ErrL ADD A, Err0L MOV R0, A ; временно MOV A, ErrH ADDC A, Err0H MOV C, ACC.7 ; Полусумма всегда влезает в 16 бит, поэтому при сдвиге надо сохранить знак RRC A ; Поделим без потери знака XCH A, R0 ; A= младшая часть, R0 - старшая часть полусуммы RRC A ; Доделили JNB IntS, Pid3IntPos ; Int отрицательный, изменим знак для R0:A, тем самым можно будет просто сложить с Int CLR C CPL A ADD A, #1 XCH A, R0 CPL A ADDC A, #0 XCH A, R0 Pid3IntPos: ; У Int и R0:A сейчас согласованы знаки, поэтому складываем обычным образом ADD A, IntLL MOV IntLL, A MOV A, IntLH ADDC A, R0 MOV IntLH, A MOV A, R0 JB ACC.7, Pid3Neg ; Прибавляли отрицательную разность? ; Если разность положительная, просто распространим перенос JNC jPid3End ; Если прибавили слово и переноса небыло -- делать нам ничего не требуется. INC IntHL ; Распространяем перенос выше MOV A, IntHL JNZ Pid3End ; Если перенос не ушел в 4й байт -- всё нормально INC IntHH ; Распространяем перенос на САМЫЙ старший байт MOV A, IntHH JNZ Pid3End ; Если перенос не ушел еще выше -- всё нормально MOV IntHH, #0FFh ; Если перенс был выше -- ограничиваем интеграл потолком MOV IntHL, #0FFh MOV IntLH, #0FFh MOV IntLL, #0FFh jPid3End: SJMP Pid3End Pid3Neg: ; Если разность отрицательная, то надо продолжать добавлять оба раза, но FFh MOV A, IntHL ADDC A, #0FFh MOV IntHL, A MOV A, IntHH ADDC A, #0FFh MOV IntHH, A JC Pid3End ; Если тут был перенос, значит знак интеграла не изменился CPL IntS ; Если переноса небыло, значит у интеграла изменился знак CPL C ; Обратим знак получившегося числа MOV A, #0 SUBB A, IntLL MOV IntLL, A MOV A, #0 SUBB A, IntLH MOV IntLH, A MOV A, #0 SUBB A, IntHL MOV IntHL, A MOV A, #0 SUBB A, IntHH MOV IntHH, A ; так как оно стало отрицательным -- то перенос тут будет всегда Pid3End: ; 5. cI = Int*(1/Ti) | 32*32=>32bit Pid5: ; R3:R2:R1:R0 = Int*(1/Ti) JB Ti_sh1, Pid5Calc ; если Ti_sh1=0, то 1/Ti=1 или Ti=0. и ничего делать не надо MOV A, Ti_mLL ORL A, Ti_mLH ORL A, Ti_mHL ORL A, Ti_mHH JZ Pid5Zero MOV R0, IntLL MOV R1, IntLH MOV R2, IntHL MOV R3, IntHH AJMP Pid5End Pid5Zero: MOV A, #0 MOV R0, A MOV R1, A MOV R2, A MOV R3, A MOV IntLL, A MOV IntLH, A MOV IntHL, A MOV IntHH, A AJMP Pid5End Pid5Calc: ; R7:R6:R5:R4[:R3] = MULUH(Int*Ti_m) // R3 считаем как часть для округления MOV R2, #0 ;; R7:R6 = IntHH*Ti_mHH MOV A, IntHH MOV B, Ti_mHH MUL AB MOV R7, B MOV R6, A ; R6:R5 += IntHL*Ti_mHH MOV A, IntHL MOV B, Ti_mHH MUL AB MOV R5, A MOV A, R6 ADD A, B MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A ; R5:R4 += IntLH*Ti_mHH MOV A, IntLH MOV B, Ti_mHH MUL AB MOV R4, A MOV A, R5 ADD A, B MOV R5, A MOV A, R2 ; A=0 ADDC A, R6 MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A ; R4:R3 += IntLL*Ti_mHH MOV A, IntLL MOV B, Ti_mHH MUL AB MOV R3, A MOV A, R4 ADD A, B MOV R4, A MOV A, R2 ; A=0 ADDC A, R5 MOV R5, A MOV A, R2 ; A=0 ADDC A, R6 MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A ;; R6:R5 += IntHH*Ti_mHL MOV A, IntHH MOV B, Ti_mHL MUL AB ADD A, R5 MOV R5, A MOV A, R6 ADDC A, B MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A ; R5:R4 += IntHL*Ti_mHL MOV A, IntHL MOV B, Ti_mHL MUL AB ADD A, R4 MOV R4, A MOV A, R5 ADDC A, B MOV R5, A MOV A, R2 ; A=0 ADDC A, R6 MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A ; R4:R3 += IntLH*Ti_mHL MOV A, IntLH MOV B, Ti_mHL MUL AB MOV A, R3 MOV R3, A MOV A, R4 ADDC A, B MOV R4, A MOV A, R2 ; A=0 ADDC A, R5 MOV R5, A MOV A, R2 ; A=0 ADDC A, R6 MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A ;; R5:R4 += IntHH*Ti_mLH MOV A, IntHH MOV B, Ti_mLH MUL AB ADD A, R4 MOV R4, A MOV A, R5 ADDC A, B MOV R5, A MOV A, R2 ; A=0 ADDC A, R6 MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A ; R4:R3 += IntHL*Ti_mLH MOV A, IntHL MOV B, Ti_mLH MUL AB ADD A, R3 MOV R3, A MOV A, R4 ADDC A, B MOV R4, A MOV A, R2 ; A=0 ADDC A, R5 MOV R5, A MOV A, R2 ; A=0 ADDC A, R6 MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A ;; R4:R3 += IntHH*Ti_mLL MOV A, IntHH MOV B, Ti_mLL MUL AB ADD A, R3 MOV R3, A MOV A, R4 ADDC A, B MOV R4, A MOV A, R2 ; A=0 ADDC A, R5 MOV R5, A MOV A, R2 ; A=0 ADDC A, R6 MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A ;;; Если R3 > 7F -- MOV A, R3 JNB ACC.7, Pid5Shift ; Если R3<80 -- округление не надо ANL A, #7Fh JZ Pid5Round ; Если = 80 -- округляем до нечетного MOV A, #1 ADD A, R4 MOV R4, A MOV A, R2 ; A=0 ADDC A, R5 MOV R5, A MOV A, R2 ; A=0 ADDC A, R6 MOV R6, A MOV A, R2 ; A=0 ADDC A, R7 MOV R7, A SJMP Pid5Shift Pid5Round: MOV A, R4 ORL A, #01h MOV R4, A ;JMP Pid5Shift Pid5Shift: ; R3:R2:R1:R0 = (Int-R7:R6:R5:R4) >> 1 CLR C MOV A, IntLL SUBB A, R4 MOV R0, A MOV A, IntLH SUBB A, R5 MOV R1, A MOV A, IntHL SUBB A, R6 MOV R2, A MOV A, IntHH SUBB A, R7 RRC A ; >>1 без потери переноса MOV R3, A MOV A, R2 RRC A MOV R2, A MOV A, R1 RRC A MOV R1, A MOV A, R0 RRC A ;MOV R0, A ; R3:R2:R1:R0 += R7:R6:R5:R4 ;MOV A, R0 ADD A, R4 MOV R0, A MOV A, R1 ADDC A, R5 MOV R1, A MOV A, R2 ADDC A, R6 MOV R2, A MOV A, R3 ADDC A, R7 MOV R7, A ; Теперь сдвинуть вправо на sh2. ; sh2 может быть до 16 (так как у нас Ti 16разрядный; проверим необходимость сдвига на 16 бит) MOV A, Ti_sh2 JNB ACC.4, Pid5ShiftUnder16 ; Надо сдвинуть >=16 -- 2 байта сдвинем mov"ами MOV R0, 18h+2; R2, bank 3 MOV R1, 18h+3; R3, bank 3 MOV R2, #0 MOV R3, #0 Pid5ShiftUnder16: JNB ACC.3, Pid5ShiftUnder8 ; Надо сдвинуть на >=8 -- 1 байт сдвигаем mov"ами MOV R0, 18h+1; R1, bank 3 MOV R1, 18h+2; R2, bank 3 MOV R2, 18h+3; R3, bank 3 MOV R3, #0 Pid5ShiftUnder8: ANL A, #07h JZ Pid5End ; Если внутри байта двигать не надо -- всё MOV R4, A SJMP Pid5ShiftRight Pid5NextShift: CLR C ; К этому моменту C у нас еще возможнозначимый старший бит! Pid5ShiftRight: MOV A, R3 RRC A MOV R3, A MOV A, R2 RRC A MOV R2, A MOV A, R1 RRC A MOV R1, A MOV A, R0 RRC A MOV R0, A DJNZ R4, Pid5NextShift ; Всё, после всех сдвигов получили результат; Не забываем, что у вычисленного в R3:R2:R1:R0 ; сейчас число положительное, а знак его в IntS Pid5End: ; 4. PID += [ cD = Td * (E-Eo) ] | 16*16=>32bit Pid4: ; cD = R7:R6:R5:R4; ErrD = E-Eo CLR C MOV A, ErrL SUBB A, Err0L MOV DiffL, A MOV A, ErrH SUBB A, Err0H MOV DiffH, A MOV C, ACC.7 ; Берём знак результата MOV DiffS, C ; Сохраним знак E-Eo JNC Pid4Mul ; Diff -- орицательный, обратим знак MOV A, DiffL CPL A ADD A, #1 MOV DiffL, A MOV A, DiffH CPL A ADDC A, #0 MOV DiffH, A Pid4Mul: ; R7:R6 = DiffH*TdH ; MOV A, DiffH = в любом случае A=DiffH MOV B, TdH MUL AB MOV R6, A MOV R7, B ; R5:R4 = DiffL*TdL MOV A, DiffL MOV B, TdL MUL AB MOV R4, A MOV R5, B ; R6:R5 += DiffH*TdL MOV A, DiffH MOV B, TdL MUL AB ADD A, R5 MOV R5, A MOV A, R6 ADD A, B MOV R6, A MOV A, R7 ADDC A, #0 MOV R7, A ; R6:R5 += DiffL*TdH MOV A, DiffL MOV B, TdH MUL AB ADD A, R5 MOV R5, A MOV A, R6 ADD A, B MOV R6, A MOV A, R7 ADDC A, #0 MOV R7, A ; 6. PID = E + cI + cD | 32bit Pid6: ; R3:R2:R1:R0 равно cI, знак в IntS; ; R7:R6:R5:R4 = cD; знак в DiffS ; E в обратном дополнительном коде JB IntS, ChkDiffN JNB DiffS, Pid6Add ; Int>0, Diff>0 => Add SJMP Pid6Sub ; Int>0, Diff<0 => Sub ChkDiffN: JNB DiffS, Pid6Sub ; Int<0, Diff>0 => Sub ; Int<0, Diff<0 => Add Pid6Add: ; Одинаковый знак => складываем их с проверкой на переполнение MOV A, R0 ADD A, R4 MOV R0, A MOV A, R1 ADDC A, R5 MOV R1, A MOV A, R2 ADDC A, R6 MOV R2, A MOV A, R3 ADDC A, R7 MOV R3, A JNC Pid6Err ; Если нет переноса - в результате сложения переполнения небыло MOV R3, #0FFh MOV R2, #0FFh MOV R1, #0FFh MOV R0, #0FFh SJMP Pid6Err Pid6Sub: ; Знаки разные -- вычтем одно из другого и проверим знак результата CLR C MOV A, R4 SUBB A, R0 MOV R0, A MOV A, R5 SUBB A, R1 MOV R1, A MOV A, R6 SUBB A, R2 MOV R2, A MOV A, R7 SUBB A, R3 MOV R3, A JNC Pid6Err ; Если нет заимствования -- знак результата равен знаку DiffS CPL DiffS ; Если заимствование было, у DiffS и результата надо обратить знак MOV R6, #0 ; R6=0 MOV A, R0 CPL A ADDC A, R6 ; R6=0, C=1 => действие +1 MOV R0, A MOV A, R1 CPL A ADDC A, R6 ; +перенос MOV R1, A MOV A, R2 CPL A ADDC A, R6 MOV R2, A MOV A, R3 CPL A ADDC A, R6 MOV R3, A Pid6Err: MOV R6, #0 ; R6=0 ; В R3:R2:R1:R0 -- лежит cI+cD; знак суммы в DiffS ; надо прибавить/отнять Err, записанное в обратном коде; Приведём знак Err к DiffS MOV R4, ErrL MOV A, ErrH JB ACC.7, Pid6ChkDiffS JNB DiffS, Pid6SumErrNoInv ; Err>0, Diff>0 => NoInv SJMP Pid6SumErrInv Pid6ChkDiffS: JNB DiffS, Pid6SumErrNoInv ; Err<0, Diff>0 => NoInv Pid6SumErrInv: ; У Err знак отличается от DiffS -- инвертируем SETB C ; Не уверен в состоянии C MOV A, ErrL CPL A ADDC A, R6 ; A+=R6+C, R6=0 C=1 => A+=1 MOV R4, A ; R4=ErrL MOV A, ErrH CPL A ADDC A, R6 Pid6SumErrNoInv: MOV R5, A ; ErrH Pid6SumErr: ; Итак, в R5:R4 лежит Err, знак которого согласован с DiffS; но в обратно-дополнительном коде MOV A, R0 ADD A, R4 MOV R0, A MOV A, R5 CLR F0 JNB ACC.7, Pid6SubErrPos SETB F0 MOV R6, #0FFh ; Добавляем отрицательное => дополняем FFами Pid6SubErrPos: ADDC A, R1 MOV R1, A MOV A, R2 ADDC A, R6 ; +расширение MOV R2, A MOV A, R3 ADDC A, R6 ; +расширение MOV R3, A MOV R6, #0 ; Надо проверить нет ли смены знака итоговой суммы JNC Pid6ChkF0 JB F0, Pid7 ; Err<0, был перенос => Знак не сменился, переполнения нет SJMP Pid6SumOv ; Err>0, был перенос => переполнение Pid6ChkF0: JNB F0, Pid7 ; Err>0, небыло переноса => нет переполнения;SJMP Pid6SumUf ; Err<0, небыло переноса => сменился знак Pid6SumUf: ; Если Err<0 и небыло переноса => сменился знак CPL DiffS MOV A, R0 CPL A ADD A, #1 ; C=?, поэтому прибавляем 1 обычным методом MOV R0, A MOV A, R1 CPL A ADDC A, R6 MOV R1, A MOV A, R2 CPL A ADDC A, R6 MOV R2, A MOV A, R3 CPL A ADDC A, R6 MOV R3, A SJMP Pid7 ; Знак у результата и DiffS приведены в норму Pid6SumOv: ; Было переполнение => округляем до максимума MOV R0, #0FFh MOV R1, #0FFh MOV R2, #0FFh MOV R3, #0FFh ; 7. U = K*PID/256 | 32bit*16bit/8bit => 40bit, ; | которые усекаются до 10bit ; | при вычислениях Pid7: ; В R3:R2:R1:R0 лежит результат PID, в DiffS его знак; Нужно вычислить K*PID/256, ограничив результат до 10бит; K всегда положительно, поэтому если PID < 0 => минимум JB DiffS, Pid7Umin ; поскольку мы можем жестко ограничить сверху 16ю битами, ; то если R3 != 0 => ставим максимум в любом случае MOV A, R3 JNZ Pid7Umax ; = ; вычисляем, учитывая что должно получиться R7=0 R6=0, ; иначе переполнение, поэтому R7 и R6 вообще не трогаем; но проверяем результат; R7:R6 = R2*KH MOV A, R2 JZ Pid7S1 MOV A, KH JNZ Pid7Umax ; Если R2!=0 и KH!=0 => R7:R6>0 => переполнение Pid7S1: ; R6:R5 = R2*KL MOV A, R2 MOV B, KL MUL AB MOV R5, A MOV A, B JNZ Pid7Umax ; Если R6 > 0 => переполнение; R6:R5 = R1*KH MOV A, R1 MOV B, KH MUL AB ADD A, R5 JC Pid7Umax ; Если R6 > 0 => переполнение MOV R5, A MOV A, B JNZ Pid7Umax ; Если R6 > 0 => переполнение; R5:R4 = R0*KH MOV A, R0 MOV B, KH MUL AB MOV R4, A MOV A, R5 ADD A, B JC Pid7Umax ; Если R6 > 0 => переполнение MOV R5, A ; R5:R4 = R1*KL MOV A, R1 MOV B, KL MUL AB ADD A, R4 MOV R4, A MOV A, R5 ADDC A, B JC Pid7Umax ; Если R6 > 0 => переполнение MOV R5, A ; R4:R3 = R0*KL MOV A, R0 MOV B, KL MUL AB RLC A ; C = R3>=0x80, Z=R3>0x80 MOV R3, #0FFh ; R3<>0x80 => ничего JNZ Pid7S2 MOV R3, #0FEh ; R3==0x80 => округление до четного Pid7S2: MOV A, R4 ADDC A, B ; Складываем умножение, регистр, и перенос-округление ANL A, R3 ; А так же если округление до четного -- отбрасываем после младший бит MOV R4, A MOV A, R5 ADDC A, R6 ; R6=0 у нас с давних пор, хоть мы туда и не складывали ничего во время перемножения JC Pid7Umax ; Если R6 > 0 => переполнение MOV R5, A ; R5:R4 => ограниченный в 16 бит результат; Теперь надо ограничить R5:R4 до Umax/Umin MOV A, UmaxL SUBB A, R4 ; C=0 на текущий момент MOV A, UmaxH SUBB A, R5 JC Pid7Umax ; Если R5:R4>Umax => R5:R4 = Umax MOV A, UminL SUBB A, R4 ; C=0 на текущий момент MOV A, UminH SUBB A, R5 JNC Pid7Umin ; Если R5:R4 R5:R4 = Umin ; Мощность вычислена MOV UH, R5 MOV UL, R4 SETB UReady AJMP CalcExit Pid7Umax: ; Установить максимальную мощность MOV UH, UmaxH MOV UL, UmaxL SETB UReady AJMP CalcExit Pid7Umin: ; Установить минимальную мощность MOV UH, UminH MOV UL, UminL SETB UReady AJMP CalcExit

IX. Применение воздействия.

Итак, у нас есть рассчитанное воздействие, и наша задача - применить его. Для этого работает общий цикл работы с частотой 50Гц. На четном цикле - производится измерение и вычисление, на нечетном - применение воздействия. Таким образом, общая схема получается: выставлена мощность, через одну синусоиду производится измерение и вычисление, еще через одну - применение новой.

X. Подводные камни.

По сравнению с разностной схемой, подводных камней у прямой схемы крайне мало, вот список тех, которые я видел:
  • Учет размерностей . Самое важное, и самая частая ошибка. Нельзя просто взять U=K*(Err+Ki*Int+Kd*Diff), без оговаривания ЧТО есть K, Ki, Kd. И с какой точностью. Особенно важно для коэффициента Ki, который имеет размерность обратную времени - если операция идёт в целых числах, НЕЛЬЗЯ просто умножать на него - так как там должно быть ДЕЛЕНИЕ, а обратное число в целых числах не представимо.
  • Учет знака . Второе очень важное - учет знака. Все операции должны быть знаковыми, интеграл обязан накапливаться знаковый - так как он не только замещает пропорциональную составляющую, но и позволяет сопротивляться внешним воздействиям, например - выделению тепла самой смеси; и тогда его знак отрицательный.
  • Учет переполнения . Нам важно получить либо мощность от 0% до 100%, либо факт того, что вычисленная мощность больше 100% или меньше 0%. Нет нужды производить все вычисления, если мы получили отрицательный подскобочный результат, например. Но при этом важно учесть, что при произведении-сложении может произойти переполнение - и его нужно учесть как «больше 100%», а ни в коем образе не оставить результат после переполнения. Это чревато в первую очередь отсутствием регулирования когда требуется - объект ниже требуемой температуры, а мощность не подаётся
  • Учет времени вычислений . Необходимость великоразрядных умножений (при кривой реализации - еще и деления) требует времени, поэтому крайне важно просчитать время выполнения самого худшего варианта вычислений, и оно должно быть меньше, чем свободное время между измерениями. Невыполнение этого условия ведёт к неуправляемому объекту, который «вроде работает, но как-то не так

XI. Выводы.

В результате, прямая схема реализации не имеет тех проблем, какие имеет разностная схема , но требует больше вычислительных затрат. Однако, при правильной реализации, прямая схема вполне применима даже на дешёвых 8 битных микроконтроллерах, и даёт более предсказуемые результаты.

В данном разделе приведены описания алгоритмов работы и непрерывных П-, ПИ-, ПД-, ПИД-регуляторов с различными структурами выходного сигнала - аналоговым выходом, дискретным (импульсным) выходом или ШИМ-выходом (широтно импульсным модулированным сигналом).

Структурные схемы непрерывных регуляторов

В данном разделе приведены структурные схемы непрерывных регуляторов с аналоговым выходом -рис.2, с импульсным выходом - рис.3 и с ШИМ (широтно импульсным модулированным) выходом -рис.4.

В процессе работы система автоматического регулирования АР (регулятор) сравнивает текущее значение измеряемого параметра Х, полученного от датчика Д, с заданным значением (заданием SP) и устраняет рассогласование регулирования E (B=SP-PV). Внешние возмущающие воздействия Z также устраняются регулятором. Работа приведенных структурных схем отличается методом формирования выходного управляющего сигнала регулятора.

Непрерывный регулятор с аналоговым выходом

Структурная схема непрерывного регулятора с аналоговым выходом приведена на рис.2.

Выход Y регулятора АР (например, сигнал 0-20мА, 4-20мА, 0-5мА или 0-10В) воздействует через электропневматический Е/Р сигналов (например, с выходным сигналом 20-100кПа) или электропневматический позиционный регулятор на исполнительный элемент К (регулирующий орган).

Рисунок 2 - Структурная схема регулятора с аналоговым выходом

где:
АР - непрерывный ПИД-регулятор с аналоговым выходом,



Д - датчик,
НП - нормирующий преобразователь (в современных регуляторах является входным устройством)
Y - выходной аналоговый управляющий сигнал Е/Р - электропневматический преобразователь,

Непрерывный регулятор с импульсным выходом

Структурная схема непрерывного регулятора с импульсным выходом приведена на рис.3.

Выходные управляющие сигналы регулятора - сигналы Больше и Меньше (транзистор, реле, симистор) через контактные или бесконтактные управляющие устройства (П) воздействуют на исполнительный элемент К (регулирующий орган).

Рисунок 3 - Структурная схема регулятора с импульсным выходом

где:
АР - непрерывный ПИД-регулятор с импульсным выходом,
SP - узел формирования заданной точки,
PV=X- регулируемый технологический параметр,
Е - рассогласование регулятора,
Д - датчик,
НП - нормирующий преобразователь (в современных регуляторах является входным устройством) ИМП - импульсный ШИМ модулятор, преобразующий выходной сигнал Y в последовательность импульсов со скважностью, пропорциональной выходному сигналу: Q=\Y\/100. Сигналы Больше и Меньше - управляющие воздействия,

К - клапан регулирующий (регулирующий орган).

Непрерывный регулятор с ШИМ (широтно импульсным модулированным) выходом

Структурная схема непрерывного регулятора с ШИМ (широтно импульсным модулированным) выходом приведена на рис.4.

Выходной управляющий сигнал регулятора (транзистор, реле, симистор) через контактные или бесконтактные управляющие устройства (П) воздействуют на исполнительный элемент К (регулирующий орган).

Непрерывные регуляторы с ШИМ выходом широко применяются в системах регулирования температуры, где выходной управляющий симисторный элемент (или твердотельное реле, пускатель) воздействуют на термоэлектрический нагреватель ТЭН, или вентилятор.

Рисунок 4 - Структурная схема регулятора с ШИМ выходом

АР - непрерывный ПИД-регулятор с импульсным ШИМ выходом,
SP - узел формирования заданной точки,
PV=X- регулируемый технологический параметр,
Е - рассогласование регулятора,
Д - датчик,
НП - нормирующий преобразователь (в современных регуляторах является входным устройством) ШИМ - импульсный ШИМ модулятор, преобразующий выходной сигнал Y в последовательность импульсов со скважностью, пропорциональной выходному сигналу: Q=\Y\/100.
П - пускатель контактный или бесконтактный,
К - клапан регулирующий (регулирующий орган).

Согласование выходных устройств непрерывных регуляторов

В ыходной сигнал регулятора должен быть согласован с исполнительным механизмом и исполнительным устройством.

В соответствии с видом привода и исполнительным механизмом необходимо использовать выходное устройство непрерывного регулятора соответствующего типа, см. таблицу 1.

Таблица 1 - Согласование выходных устройств непрерывных регуляторов

Выходное устройство непрерывного регулятора Тип выходного устройства Исполнительный механизм или устройство Вид привода Регулирующий орган
Аналоговый выход ЦАП с выходом 0-5мА, 0-20мА, 4-20мА, 0-10В П-, ПИ-,ПД-, ПИД-закон Преобразователи и позиционные регуляторы электро-пневматические и гидравлические Пневматические исполнительные приводы (с сжатым воздухом в качестве вспомогательной энергии) и электропневматические преобразователи сигналов или электропневматические позиционные регуляторы, электрические (частотные привода)
Импульсный выход Транзистор, реле, симистор П-, ПИ-, ПД-, ПИД-закон Электрические приводы (с редуктором), в т. ч. реверсивные
ШИМ выход Транзистор, реле, симистор П-, ПИ-, ПД-, ПИД-закон Контактные (реле) и бесконтактные (симисторные) пускатели Термоэлектрический нагреватель(ТЭН) и др.

Реакция регулятора на единичное ступенчатое воздействие

Если на вход регулятора подается скачкообразная функция изменения заданной точки - см. рис. 5, то на выходе регулятора возникает реакция на единичное ступенчатое воздействие в соответствии с характеристикой регулятора в функции времени.

Настройка регуляторов

Связи между показателями качества

Описанные выше показатели качества связаны между собой примерными соотношениями, справедливыми только для систем не выше второго порядка:

; t p = ; ; M = .

Для регулирования объектами управления, как правило, используют типовые регуляторы, которые можно разделить на аналоговые и дискретные. К дискретным регуляторам относятся импульсные, релейные и цифровые. Аналоговые реализуют типовые законы регулирования, названия которых соответствуют названиям типовых звеньев.

Входным сигналом для аналоговых регуляторов является величина ошибки регулирования, которая определяется как разность между заданным и текущим значениями регулируемого параметра (e = х - у). Выходным сигналом является величина управляющего воздействия u, подаваемая на объект управления. Преобразование входного сигнала в выходной производится согласно типовым законам регулирования, рассматриваемым ниже.

1) П-закон (пропорциональное регулирование) . Согласно закон пропорционального регулирования управляющее воздействие должно быть пропорционально величине ошибки. Например, если регулируемый параметр начинает отклоняться от заданного значения, то воздействие на объект следует увеличивать в соответствующую сторону. Коэффициент пропорциональности часто обозначают как K 1:

Тогда передаточная функция П-регулятора имеет вид

W П (s) = K 1 .

Если величина ошибки стала равна, например, единице, то управляющее воздействие станет равным K 1 (см. рисунок 1.52).


Рисунок 1.52

Примером системы с П-регулятором может служить система автоматического наполнения емкости (сливной бачок). На рисунке 1.53 обозначены:

L и L зад - текущий уровень в емкости (регулируемая величина) и его заданная величина,

F пр и F сток - расходы жидкости притекающей и стекающей из емкости.

Управляющим воздействием является F пр. F сток - возмущение.

Принцип действия понятен из рисунка: при опустошении емкости поплавок через кронштейн открывает задвижку подачи жидкости. Причем, чем больше разница уровней е = L зад - L, тем ниже поплавок, тем больше открыта задвижка и, соответственно, больше поток жидкости F пр. По мере наполнения емкости ошибка уменьшается до нуля и, соответственно, уменьшается F пр до полного прекращения подачи. То есть F пр = K 1 . (L зад - L).

Достоинство данного принципа регулирования в быстродействии. Недостаток - в наличии статической ошибки в системе. Например, если жидкость вытекает из емкости постоянно, то уровень всегда будет меньше заданного.

2) И-закон (интегральное регулирование) . Управляющее воздействие пропорционально интегралу от ошибки. То есть чем дольше существует отклонение регулируемого параметра от заданного значения, тем больше управляющее воздействие:


.

Передаточная функция И-регулятора:

При возникновении ошибки управляющее воздействие начинает увеличиваться со скоростью, пропорциональной величине ошибки. Например, при е = 1 скорость будет равна K 0 (см. рисунок 1.54).


Рисунок 1.54

Достоинство данного принципа регулирования в отсутствии статической ошибки, т.е. при возникновении ошибки регулятор будет увеличивать управляющее воздействие, пока не добьется заданного значения регулируемой величины. Недостаток - в низком быстродействии.

3) Д-закон (дифференциальное регулирование) . Регулирование ведется по величине скорости изменения регулируемой величины:

То есть при быстром отклонении регулирующей величины управляющее воздействие по модулю будет больше. При медленном - меньше. Передаточная функция Д-регулятора:

W Д (s) = K 2 s.

Регулятор генерирует управляющее воздействие только при изменении регулируемой величины. Например, если ошибка имеет вид ступенчатого сигнала е = 1, то на выходе такого регулятора будет наблюдаться один импульс (d-функция). В этом заключается его недостаток, который обусловил отсутствие практического использования такого регулятора в чистом виде.

На практике типовые П-, И- и Д-законы регулирования редко используются в чистом виде. Чаще они комбинируются и реализуются в виде ПИ-регуляторов, ПД-регуляторов, ПИД-регуляторов и др.

ПИ-регулятор (пропорционально-интегральный регулятор) представляет собой два параллельно работающих регулятора: П- и И-регуляторы (см. рисунок 1.55). Данное соединение сочетает в себе достоинства обоих регуляторов: быстродействие и отсутствие статической ошибки.

ПИ-закон регулирования описывается уравнением

и передаточной функцией

W ПИ (s) = K 1 + .

То есть регулятор имеет два независимых параметра (настройки): K 0 - коэффициент интегральной части и K 1 - коэффициент пропорциональной.

При возникновении ошибки е = 1 управляющее воздействие изменяется как показано на рисунке 1.56.

Рисунок 1.56

ПД-регулятор (пропорционально-дифференциальный регулятор) включает в себя П- и Д-регуляторы (см. рисунок 1.57). Данный закон регулирования описывается уравнением

и передаточной функцией:

W ПД (s) = K 1 + K 2 s.

Данный регулятор обладает самым большим быстродействием, но также и статической ошибкой. Реакция регулятора на единичное ступенчатое изменение ошибки показана на рисунке 1.58.


Рисунок 1.58

ПИД-регулятор (пропорционально-интегрально-дифференциальный регулятор) можно представить как соединение трех параллельно работающих регуляторов (см. рисунок 1.59). Закон ПИД-регулирования описывается уравнением:

и передаточной функцией

W ПИД (s) = K 1 + + K 2 s.

ПИД-регулятор в отличие от других имеет три настройки: K 0 , K 1 и K 2 .

ПИД-регулятор используется достаточно часто, поскольку он сочетает в себе достоинства всех трех типовых регуляторов. Реакция регулятора на единичное ступенчатое изменение ошибки показана на рисунке 1.60.