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