Интерфейс "глобальных настроек"

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


Рисунок 1. Модель системы глобальных настроек с точки зрения пользователя.

Для установки минимальной и максимальной температуры и шага изменения задаваемой температуры будем предлагать пользователю на выбор несколько вариантов – например, значения минимальной температуры могут быть от 0˚С до 60 ˚С с шагом 20 ˚С; максимальной – от 80 ˚С до 400 ˚С с тем же шагом. Шаг изменения может быть выбран из следующих значений: 0.1; 0.5 и 1 ˚С.
Особый интерес представляет настройка коэффициентов ПИД-регулятора. Можно попробовать сперва найти приблизительные значения коэффициентов (например, по методу Зиглера-Никольса, подробнее об этом методе можно почитать здесь), а затем уже ручной настройкой довести их до совершенства.
При этом коэффициенты нужно задавать достаточно точно – до десятых, а то и до сотых долей; но если мы сделаем настройку как обычно – задав текущее, максимальное-минимальное значение и шаг 0.01, то крутить энкодер мы будем бесконечно. Поэтому в данном случае лучше усовершенствовать способ настройки. Например, так:


Рисунок 2. Модель настройки коэффициентов ПИД-регулятора с точки зрения пользователя.

Все очень просто: мы разделим настройку каждого разряда: сначала будем настраивать сотни (шаг изменения у счётчика энкодера = 100), затем десятков (шаг изменения = 10), затем единиц и так далее. При этом P- и D-коэффициенты будут настраиваться как (_ _ _ . _ _) – число от 0 до 999.99, а вот I-коэффициент обычно небольшой, меньше 1, а иногда может быть и отрицательным – поэтому он будет настраиваться как (_ _ . _ _ _) или (- _ . _ _ _) – от -9.999 до 99.999.
Попробуем реализовать проект по частям – начнём с настройки коэффициентов ПИД-регулятора.
Скажем сразу – несмотря на кажущуюся простоту, это оказалось не так уж просто.
Для начала, мы переписали функцию отображения дробных чисел – нам нужно уметь отображать любые комбинации количества целых и дробных разрядов, а также нужно уметь отображать нули на месте старших неиспользуемых разрядов (например, 012.34) – так что мы сделали функцию универсальной: её можно использовать для отображения температуры и при калибровке.

//перевод дробного числа в строку 
void DoubleToStr(double Value, char* buff, uint8_t IsElderZero, int8_t IntDecade, int8_t FractDecade)
{
  uint16_t Divider, Dividend;
  uint8_t currDecade;
  char* currSymbol;
  uint8_t i;
  currSymbol = buff;
  //если отрицательное число - записываем минус
  if (Value < 0)
  {
    *currSymbol = '-';
    currSymbol++;
    Value *= (-1);
    IntDecade--;
  }
  Dividend = (uint16_t)Value;
  Divider = 1;
  //формируем делитель для максимального разряда
  for (i = 0; i < IntDecade - 1; i++)
    Divider *= 10;
  //отображаем целые разряды до единиц
  for (i = 0; i < IntDecade - 1; i++)
  {
    currDecade = Dividend / Divider;
    //записываем значение разряда или ноль, если нужно записывать "старшие" нули (вид числа "0123.4")
    if ((currDecade) || (IsElderZero) || (Value > Divider))
      *currSymbol = currDecade + '0';
    else
      *currSymbol = ' ';
    currSymbol++;
    Dividend = Dividend % Divider;
    Divider /= 10; 
  }
  //разряд единиц мы записываем, даже если там 0
  currDecade = Dividend;
  *currSymbol = currDecade + '0';
  currSymbol++;
  *currSymbol = '.';
  currSymbol++;
  //записываем нужное количество дробных разрядов
  Divider = 1;
  for (i = 0; i < FractDecade; i++)
    Divider *= 10;
  Dividend = ((uint32_t)(Value * Divider)) % Divider;                           //следим, чтобы обрабатывались даже числа, которые при смещении дробных разрядов > uint16_t
  for(i = 0; i < FractDecade; i++)
  {
    Divider /= 10;
    currDecade = Dividend / Divider;
    *currSymbol = currDecade + '0';
    currSymbol++;
    Dividend = Dividend % Divider;
  }   
}

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

//отображение значения коэффициента ПИД-регулятора при настройке
void DisplayShowPIDCoef(char CoefName, double Value, uint8_t IntDecade, uint8_t FractDecade)
{
  char buff[DISP_SEGMENT_COUNT + 2];
  buff[0] = CoefName;
  DoubleToStr(Value, &buff[1], 1, IntDecade, FractDecade);
  buff[DISP_SEGMENT_COUNT + 1] = 0;
  DisplayShowStr(buff);
}

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


Рисунок 3. Виртуальная машина настройки коэффициентов ПИД-регулятора.

Все было хорошо до тех пор, пока мы не начали проверять работу машины на отрицательных значениях.
В то время, как мы хотели получить что-то типа этого:
- 9 8 . 7 6 5
повернули энкодер вправо
0 9 8 . 7 6 5

Мы, естественно, получили вот это:
- 9 8 . 7 6 5
повернули энкодер вправо
0 0 1 . 2 3 5

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

//настройка коэффициентов ПИД-регулятора
//состояния виртуальной подмашины настройки ПИД-коэффициентов
typedef enum {pssSetup, pssExit} TVMPIDSetupState;
//структура подмашины настройки ПИД-коэффициентов
typedef struct
{
  char psCoefName;                                                              //символ, обозначающий текущий настраиваемый коэффициент ('P', 'I' или 'D')
  uint8_t psFractDecade: 3;                                                     //количество дробных разрядов при настройке коэффициента
  uint8_t psIntDecade: 3;                                                       //количество целых разрядов при настройке коэффициента
  uint8_t psDecadeCounter;                                                      //счётчик разрядов
  double *psCurrCoef;                                                           //указатель на переменную, в которой хранится значение настраиваемого коэффициента
  double psLastEncValue;                                                        //предыдущее значение энкодера (исп-ся при переходе от отрицательных значений к положительным и обратно)
  TVMPIDSetupState psMachineState;                                              
    
} TVMPIDSetup;
TVMPIDSetup VMPIDSetup;

//инициализация подмашины настройки ПИД-коэффициентов; на вход название коэф-та, наличие отрицательных значений, число дробных разрядов
void PIDSetupMachineInit(char CoefName, uint8_t IsNeg, uint8_t FractDecade)
{
  uint8_t i;
  double minValue, maxValue, step;
  VMPIDSetup.psCoefName = CoefName;
  //определяем указатель на переменную со значением нужного коэффициента
  switch (VMPIDSetup.psCoefName)
  {
  case 'P':
    VMPIDSetup.psCurrCoef = &(VMHeatElement.hePIDPCoef);
    break;
  case 'I':
    VMPIDSetup.psCurrCoef = &(VMHeatElement.hePIDICoef);
    break;
  case 'D':
    VMPIDSetup.psCurrCoef = &(VMHeatElement.hePIDDCoef);
    break;
  }
  VMPIDSetup.psLastEncValue = *(VMPIDSetup.psCurrCoef);
  //определяем количество дробных и целых разрядов
  VMPIDSetup.psFractDecade = FractDecade;                       
  VMPIDSetup.psIntDecade = (DISP_SEGMENT_COUNT - 1) - FractDecade;
  VMPIDSetup.psDecadeCounter = 0;
  //рассчитываем максимальное и минимальное значение
  maxValue = 1;
  for (i = 0; i < VMPIDSetup.psIntDecade; i++)
    maxValue *= 10;
  step = maxValue / 10;
  //уменьшаем на "чуть-чуть" значение максимума, чтобы не "перевалить" за старший разряд
  maxValue -= 0.0001;
  if (IsNeg == 0)
    minValue = 0;
  else
    minValue = (maxValue / 10) * (-1);
  //настраиваем энкодер
  EncoderCounterSetValue(*(VMPIDSetup.psCurrCoef), maxValue, minValue, step);
  //отображаем на дисплее текущее значение коэффициента
  DisplayShowPIDCoef(VMPIDSetup.psCoefName, *(VMPIDSetup.psCurrCoef), VMPIDSetup.psIntDecade, VMPIDSetup.psFractDecade);
  VMPIDSetup.psMachineState = pssSetup;
}
//подмашина настройки ПИД-коэффициентов
void PIDSetupMachine()
{
  switch (VMPIDSetup.psMachineState)
  {
  case pssSetup:
    //если энкодер был повернут - меняем значение
    if (VMEncoderCounter.ecntOnValueChanged)
    {
      VMEncoderCounter.ecntOnValueChanged = 0;
      //отдельная ситуация, когда происходит смена знака - тогда просто умножаем предыдущее значение энкодера на (-1)
      if (((VMPIDSetup.psLastEncValue < 0) && (VMEncoderCounter.ecntValue > 0)) ||
          ((VMPIDSetup.psLastEncValue > 0) && (VMEncoderCounter.ecntValue < 0))) 
        VMEncoderCounter.ecntValue = VMPIDSetup.psLastEncValue * (-1);
      VMPIDSetup.psLastEncValue = VMEncoderCounter.ecntValue;
      DisplayShowPIDCoef(VMPIDSetup.psCoefName, VMEncoderCounter.ecntValue, VMPIDSetup.psIntDecade, VMPIDSetup.psFractDecade);
    }
    //если пользовательская кнопка была коротко нажата
    else if (VMLogicalButtons[0].lbOnShortPress)
    {
      VMLogicalButtons[0].lbOnShortPress = 0;
      VMPIDSetup.psDecadeCounter++;
      //если нужно уменьшить разряд
      if (VMPIDSetup.psDecadeCounter < (VMPIDSetup.psFractDecade + VMPIDSetup.psIntDecade))
        VMEncoderCounter.ecntStep = VMEncoderCounter.ecntStep / 10;
      //если разряды "кончились"
      else
      {
        *(VMPIDSetup.psCurrCoef) = VMEncoderCounter.ecntValue;                  //сохраняем новое значение  
        VMPIDSetup.psMachineState = pssExit;                                    //выходим
      }
    }
    break;
  }
}

Вот прошивка с настройкой P-коэффициента.

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

Но, в принципе, всё работает! Возвращаемся на уровень выше и моделируем работу машины глобальных настроек:


Рисунок 4. Часть виртуальной машины глобальных настроек (настройка минимальной температуры).

Настройка минимальной-максимальной температуры одинакова. С шагом температуры чуть похитрее: нам нужно будет создать некоторый массив возможных значений, например, {0.1, 0.5, 1, 2}; счётчик энкодера будет выбирать индексы, и сохранять в отдельную переменную мы тоже будем не значение шага, а значение индекса массива шагов температуры.
Попробуем реализовать эту часть.
Сначала в библиотеку «ADCTemperature» добавляем информацию о минимальной-максимальной температуре и шаге температуры.
.h-файл:

#define TEMPERATURE_STEP_VALUES_COUNT 4
extern double TEMPERATURE_STEP_ARR[TEMPERATURE_STEP_VALUES_COUNT];
//границы минимальной и максимальной температуры
extern uint16_t MinTemperatureBorder[2];
extern uint16_t MaxTemperatureBorder[2];
#define MIN_MAX_TEMPERATURE_SETUP_STEP 20

typedef struct
{
  //максимальные-минимальные значения температуры и шаг изменения при задании температуры
  uint16_t atMaxTemperatureValue;
  uint16_t atMinTemperatureValue;
  uint8_t atTemperatureStepIndex;
    …
} TADCTemperature;

.с-файл:

//границы минимальной и максимальной температуры
uint16_t MinTemperatureBorder[2] ={0, 60};
uint16_t MaxTemperatureBorder[2] = {80, 400};
//варианты шага температуры при задании желаемой температуры
double TEMPERATURE_STEP_ARR[TEMPERATURE_STEP_VALUES_COUNT] = {0.1, 0.5, 1, 2};

В  библиотеку дисплея добавляем функцию отображения шага температуры:

//отображение шага температуры
void DisplayShowTemperatureStep(double Value)
{
  char buff[DISP_SEGMENT_COUNT + 3] = "ST ";
  DoubleToStr(Value, &buff[3], 0, 1, 2);
  buff[DISP_SEGMENT_COUNT] = 'C';
  buff[DISP_SEGMENT_COUNT + 1] = DispPointSymbol;
  buff[DISP_SEGMENT_COUNT + 2] = 0;
  DisplayShowStr(buff);
}

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

//состояния виртуальной машины глобальных настроек
typedef enum {gssGreetingInit, gssGreeting, gssMinTemperatureInit, gssMinTemperature, gssMaxTemperatureInit, 
              gssMaxTemperature, gssTemperatureStepInit, gssTemperatureStep, gssPID_PCoefInit, gssPID_PCoef,
              gssPID_ICoefInit, gssPID_ICoef, gssPID_DCoefInit, gssPID_DCoef} TVMGlobalSetupState;

TVMGlobalSetupState VMGlobalSetupState;

char GS_GREETING[] = "SETUP";

//инициализация виртуальной машины глобальных настроек
void GlobalSetupMachineInit()
{
  VMGlobalSetupState = gssGreetingInit;
}

//виртуальная машина глобальных настроек
void GlobalSetupMachine()
{
  switch (VMGlobalSetupState)
  {
  case gssGreetingInit:
    DisplayShowStr(GS_GREETING);
    VMGlobalSetupState = gssGreeting;
    break;
  case gssGreeting:
    if (VMLogicalButtons[0].lbOnShortPress)
    {
      VMLogicalButtons[0].lbOnShortPress = 0;
      EncoderCounterSleep();														//усыпляем счётчик	
      VMGlobalSetupState = gssMinTemperatureInit; 
    }
    break;
  case gssMinTemperatureInit:
    EncoderCounterSetValue(ADCTemperature.atMinTemperatureValue, MinTemperatureBorder[1],     //настраиваем энкодер
                           MinTemperatureBorder[0], MIN_MAX_TEMPERATURE_SETUP_STEP);
    DisplayShowCalibrationValue(0, ADCTemperature.atMinTemperatureValue);       //отображаем на дисплее текущую минимальную температуру
    VMGlobalSetupState = gssMinTemperature;
    break;
  case gssMinTemperature:
    if (VMEncoderCounter.ecntOnValueChanged)                                    //при повороте энкодера отображаем новое значение
    {
      VMEncoderCounter.ecntOnValueChanged = 0;
      DisplayShowCalibrationValue(0, VMEncoderCounter.ecntValue);
    }
    else if (VMLogicalButtons[0].lbOnShortPress)                                //по короткому нажатию на пользовательскую кнопку сохраняем значение и переходим на настройку макс. температуры
    {
      VMLogicalButtons[0].lbOnShortPress = 0;
      ADCTemperature.atMinTemperatureValue = (uint16_t)VMEncoderCounter.ecntValue; 
      VMGlobalSetupState = gssMaxTemperatureInit;                              
    }
    break;
  case gssMaxTemperatureInit:
    EncoderCounterSetValue(ADCTemperature.atMaxTemperatureValue, MaxTemperatureBorder[1],       //настраиваем энкодер
                           MaxTemperatureBorder[0], MIN_MAX_TEMPERATURE_SETUP_STEP);
    DisplayShowCalibrationValue(1, ADCTemperature.atMaxTemperatureValue);       //отображаем на дисплее текущую максимальную температуру
    VMGlobalSetupState = gssMaxTemperature;
    break;
  case gssMaxTemperature:
    if (VMEncoderCounter.ecntOnValueChanged)                                    //при повороте энкодера отображаем новое значение
    {
      VMEncoderCounter.ecntOnValueChanged = 0;
      DisplayShowCalibrationValue(1, VMEncoderCounter.ecntValue);
    }
    else if (VMLogicalButtons[0].lbOnShortPress)                                //по короткому нажатию на пользовательскую кнопку сохраняем значение и переходим на настройку шага температуры
    {
      VMLogicalButtons[0].lbOnShortPress = 0;
      ADCTemperature.atMaxTemperatureValue = (uint16_t)VMEncoderCounter.ecntValue;        
      VMGlobalSetupState = gssTemperatureStepInit;                              
    }
    break;
  case gssTemperatureStepInit:
    EncoderCounterSetValue(ADCTemperature.atTemperatureStepIndex,               //настраиваем энкодер
                           (TEMPERATURE_STEP_VALUES_COUNT - 1), 0, 1);
    DisplayShowTemperatureStep(TEMPERATURE_STEP_ARR[ADCTemperature.atTemperatureStepIndex]);//отображаем на дисплее текущий шаг температуры
    VMGlobalSetupState = gssTemperatureStep;
    break;
  case gssTemperatureStep:
    if (VMEncoderCounter.ecntOnValueChanged)                                    //при повороте энкодера отображаем новое значение
    {
      VMEncoderCounter.ecntOnValueChanged = 0;
      DisplayShowTemperatureStep(TEMPERATURE_STEP_ARR[(uint8_t)VMEncoderCounter.ecntValue]);
    }
    else if (VMLogicalButtons[0].lbOnShortPress)                                //по короткому нажатию на пользовательскую кнопку сохраняем значение и переходим на настройку П-коэффициента
    {
      VMLogicalButtons[0].lbOnShortPress = 0;
      ADCTemperature.atTemperatureStepIndex = (uint8_t)VMEncoderCounter.ecntValue;        
      VMGlobalSetupState = gssMinTemperatureInit;                              
    }
    break;
  }
}

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


Рисунок 5. Часть виртуальной машины глобальных настроек (настройка П-коэффициента ПИД-регулятора).

Добавили следующий код:

//виртуальная машина глобальных настроек
void GlobalSetupMachine()
{
  switch (VMGlobalSetupState)
  {
  …
  case gssTemperatureStep:
    …
else if (VMLogicalButtons[0].lbOnShortPress)                                	//по короткому нажатию на пользовательскую кнопку сохраняем значение и переходим на настройку П-коэффициента
    {
      VMLogicalButtons[0].lbOnShortPress = 0;
      ADCTemperature.atTemperatureStepIndex = (uint8_t)VMEncoderCounter.ecntValue;        
      VMGlobalSetupState = gssPID_PCoefInit;                              
    }
    break;
  case gssPID_PCoefInit:
    PIDSetupMachineInit('P', 0, 2);
    VMGlobalSetupState = gssPID_PCoef;
    break;
  case gssPID_PCoef:
    PIDSetupMachine();
    if (VMPIDSetup.psMachineState == pssExit)                                   //если машина закончила работать - переходим на настройку И-коэффициента
      VMGlobalSetupState = gssPID_ICoefInit;
    break;
  case gssPID_ICoefInit:
    PIDSetupMachineInit('I', 1, 3);
    VMGlobalSetupState = gssPID_ICoef;
    break;
  case gssPID_ICoef:
    PIDSetupMachine();
    if (VMPIDSetup.psMachineState == pssExit)                                   //если машина закончила работать - переходим на настройку D-коэффициента
      VMGlobalSetupState = gssPID_DCoefInit;
    break;
  case gssPID_DCoefInit:
    PIDSetupMachineInit('D', 0, 1);
    VMGlobalSetupState = gssPID_DCoef;
    break;
  case gssPID_DCoef:
    PIDSetupMachine();
    if (VMPIDSetup.psMachineState == pssExit)                                   //если машина закончила работать - переходим на настройку минимальной температуры
      VMGlobalSetupState = gssMinTemperatureInit;
    break;
  }
}

Написано вроде все верно, но вот странно – программа сходит с ума! Такое может быть, когда программе не хватает стека – и верно, при увеличении его до 0x60 все становится нормально.

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

//проверяем, не оказалась ли теперь заданная температура меньше минимальной
if (ADCTemperature.atSetTemperature < ADCTemperature.atMinTemperatureValue)
  ADCTemperature.atSetTemperature = ADCTemperature.atMinTemperatureValue;

Вот проект и прошивка.

Наверх

Автор - Moriam  =ˆˆ=

Обсудить на форуме