Регулятор освещенности - решение задачи

В постановке задачи уже выдвигались требования к регулятору. После изучения задачи, теории по задаче, а также после изучения макета были сформулированы более конкретные требования.

Эти требования можно описать в виде следующей диаграммы:

Требования к регулятору
Рис.1. Требования к регулятору

Итак, требуется реализовать:

  1. Два канала входного каскада для ввода сигнала с фотодиода $$x(t)$$ и сигнала опорного уровня $$r(t)$$, разность между которыми будет являться сигналом рассогласования $$e(t)$$ (см. ПИД-регулятор).
  2. Выходной каскад для управления яркостью светодиода посредством ШИМ-сигнала (сигнала с широтно-импульсной модуляцией).
  3. Алгоритм управления -- ПИД-регулятор.
  4. Периферийные интерфейсы: SPI, USART, I2C.

Воплотить все это в виде печатной платы.

Будем рассматривать все по порядку.

Выходной каскад

Выходной каскад в купе с алгоритмом управления предназначен для управления яркостью контролируемого светодиода. Яркостью можно управлять посредством ШИМ, что и используется. Частота ШИМ, получаемой с микроконтроллера, -- порядка нескольких килогерц.

На рис. показан выходной каскад. Он строился из единственного условия согласования уровней напряжения микроконтроллера и макета.

Выходной каскад. Стрелками обозначено подключение через разъем
Рис.2. Выходной каскад. Стрелками обозначено подключение через разъем

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

Входные каскады

Для включения фотодиода был выбран генераторный режим включения на базе преобразователя ток-напряжение на ОУ (рис.).

Преобразователь напряжение-ток
Рис.3. Преобразователь напряжение-ток

После получения доступа к макету было выяснено, что ток с фотодиода идет примерно 1мкА. В обратную связь ОУ был поставлен резистор на 1МОм, что обеспечило на выходе преобразователя положительные импульсы напряжения амплитудой примерно 1В.

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

  • Частота единичного усиления -- 20Гц
  • Частота среза -- 50Гц
  • Неравномерность ослабления в полосе пропускания -- 3дБ
  • Ослабление на частоте среда -- 30дБ.

Фильтр синтезировался в Micro Cap 9. Он и его АЧХ и ФЧХ приведены на рис.

Фильтр входного каскада
Рис.4. Фильтр входного каскада
АЧХ и ФЧХ фильтра
Рис.5. АЧХ и ФЧХ фильтра

Однако после фильтра напряжение снова не превосходит 1В. А опорный уровень АЦП ATmega8A равен 2.56В, что означает, что минимальный код ($$0$$) будет соответствовать 0В, а максимальный код ($$2^{10}-1$$) будет соответствовать 2.56В. Чтобы напрасно не тратить больше половины диапазона АЦП, сигнал после фильтрации дополнительно усиливается усилителем в 2..2.5 раза.

Для контроля коэффициента усиления усилитель снабжен подстроечным резистором.

(Можно было бы совместить фильтрацию и усиление, однако такая практика не очень распространена.)

Усилитель с подстроечным резистором
Рис.6. Усилитель с подстроечным резистором

Выход усилителя уходит сразу на АЦП.

Второй канал, отвечающий за выставление опорного уровня освещенности, весьма тривиален. Он состоит из двух резисторов, один из которых подстроечный. Номиналы резисторов выбирались таким образом, чтобы при любом положении ползунка подстроечного резистора напряжение, поступающее на АЦП, максимально полно покрывало и не выходило за его допустимый диапазон 0..2.56В.

Цепь установки опорного уровня при помощи подстроечного резистора. Выход цепи заводится на второй канал АЦП
Рис.7. Цепь установки опорного уровня при помощи подстроечного резистора. Выход цепи заводится на второй канал АЦП

Производство печатной платы

Исходя из требований и имеющихся материалов на плате должны быть следующие разъемы:

  • Разъем питание $$+12V$$
  • Разъем для подключения преобразователя напряжения $$+12V \rightarrow \pm5V$$
  • Разъем для фотодиода (провод с макета)
  • Разъем для светодиода (провод с макета)
  • Разъем для интерфейса USART/RS-232 -- отладка и обработка команд
  • Разъем ISP10-AVR-Byteblaster для интерфейса SPI -- программирование памяти
  • Разъем для интерфейса I2C (потребуется для следующей задачи)

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

Для организации принудительного сброса, питания микроконтроллера, стабилизации работы АЦП и т.д была разработана схема обвески микроконтроллера согласно даташитам (рис.).

Питающие каскады микроконтроллера, а также внешний сброс
Рис.8. Питающие каскады микроконтроллера, а также внешний сброс

Стабилизатор питающего напряжения и защита от переполюсовки были выполнены на электролитическом конденсаторе и мощном диоде (рис.).

Стабилизатор напряжения с защитой от переполюсовки питания
Рис.9. Стабилизатор напряжения с защитой от переполюсовки питания

После определения с тем, какой таймер будет использоваться для генерации ШИМ, какой канал АЦП будет использоваться для завода данных, после уточнения списка компонентов, необходимых для синтеза схемы, была произведена разводка печатной платы в программе Sprint Layout 5.0 (рис.).

Разводка печатной платы
Рис.10. Разводка печатной платы

На разводке можно заметить все разъемы, а также некоторые цифровые устройства -- набор усилителей TL084 рядом с каскадом фильтров и драйвер интерфейса RS-232 MAX232 (рядом с USART-разъемом).

Линии интерфейса I2C вручную подтягиваются к питанию резисторами в соответствии с даташитами.

Распиновка разъема ISP-10 для интерфейса SPI была взята отсюда.

Распиновка разъема `ISP-10` (справа снизу; ужасное качество)
Рис.11. Распиновка разъема `ISP-10` (справа снизу; ужасное качество)

После разводки платы производилось непосредственно создание самой платы на текстолитовой пластинке. О т.н. лазерно-утюжной технологии можно прочитать здесь.

В итоге после пайки получилась вот такая печатная плата:

Печатная плата -- вид сзади
Рис.12. Печатная плата -- вид сзади
Печатная плата -- вид сверху
Рис.13. Печатная плата -- вид сверху

Код

Код должен был решить сразу несколько проблем.

  1. Во-первых, нужно было получать с АЦП отсчеты сигнала с фотодиода, опорного уровня, вычислять управляющее воздействие на основе ПИД-регулятора и вырабатывать ШИМ.
  2. Во-вторых, нужно было обеспечить возможность вывода текущих значений всех этих величин в отладочных целях (поскольку ATmega8A не имеет встроенных механизмов отладки).
  3. В-третьих, требовалось обеспечить ввод коэффициентов непосредственно с компьютера, чтобы каждый раз при тестировании новых значений коэффициентов не производить перекомпиляцию и перепрошивку.

Первая проблема прекрасно описана в здешних мануалах по ATmega8A (работа с АЦП, таймерами и ШИМ). ПИД-регулятор тоже описан в здешней теории. Потому приходилось только следить за переполнением типов, а также обходить еще две проблемы. Первая и самая отвратительная -- отсутствие математического сопроцессора у ATmega8A. Оно выливается в то, что использование типов с плавающей точкой выливается в огромные листинги ассемблерного кода -- компилятор вставляет код, который "вручную" производит вычисления. Вторая -- тот факт, что это 8-битный микроконтроллер. Следовательно, сопроцессор с long уже не справляется. И когда надо перемножить два int, приходится либо мириться с увеличением размера кода, либо придумывать различные хитрости.

Вторая и третья проблема требует также еще и реализации буферов USART. В ATmega8A реализован небуферизированный байт-ориентированный USART. Буферизировать пришлось посредством использования кольцевых буферов. Был написан вот такой публичный API поверх USART:

bool transmit_all(const byte *in, byte size);
bool receive_all (byte *out, byte size);

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

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

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

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

Для обеспечения возможности производить все это был выдуман простейший протокол. Суть его раскрыта в соответствующем pull-реквесте и том issue, которое этот pull-реквест закрывает. А здесь кратко.

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

  • Метод ECHO отправляющий обратно идущий следом байт.
  • Метод GET -- запрос на получение указанной в следующем байте переменной (микроконтроллер далее будет непрерывно отправлять на компьютер эту переменную, пока ему не скажут отправлять другую или не отправлять ничего).
  • Метод SET -- запрос на установку указанной в следующем байте переменной / группы переменных в указанное в следующих байтах значение.

Этого было вполне достаточно для решения поставленных задач.

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

Все сказанное можно проиллюстрировать следующим кодом2:

void main()
{
    /* Инициализация всего, чего можно */
    init();

    for(;;)
    {
        /* Отработка команд */
        process_input();

        /* Производство вычислений */
        do_computations();

        /* Отладочный вывод */
        debug_output();
    }
}

Функцию process_input() можно схематично представить так:

bool has_command = false;
byte method, variable;

void process_input()
{
    if (has_command) 
    {
        byte byte;
        switch(method)
        {
            case ECHO:
                if (!receive(&byte)) return;
                send(byte);
                break;
            case GET:
                // TODO: body here
                break;
            case SET:
                // TODO: body here
                break;
        }
        has_command = false;
        return;
    }
    has_command = receive_byte(&method);
}

После чего задачу можно было бы считать решенной. Оставалось только правильно подобрать коэффициенты ПИД-регулятора. Но не тут-то было. Очень захотелось написать еще и программу-визуализатор той информации, которая шла с микроконтроллера. Очень. Она также должна была упростить ввод коэффициентов, поскольку вводились они в формате <HI LO D>, где <HI LO> -- число-множитель, а D -- величина сдвига вправо после умножения (т.е. в итоге такой "коэффициент" действовал на некоторую величину $$S$$ вот как: $$R = (M * S)\ /\ 2^D$$; естествено, что $$M = (HI << 8)\ |\ LO$$).

Такая программа была написана (рис.). Работала она очень просто через COM-порт. На прием (в основном) и передачу данных. Для обеспечения ее работы были также добавлены пакетные форматы переменных для GET и SET-методов. Встала, однако, проблема, как делить на эти пакеты непрерывный поток байтов с микроконтроллера? Она была сначала решена добавлением в пакет стартового байта, но были многочисленные срывы, при которых начинался долгий и мучительный поиск начала следующего пакета (почему-то очень долгий; почему -- так понятно и не стало). В итоге в пакет был добавлен и стоп-байт, вычислявшийся как некая контрольная сумма пакета (максимально простая -- XOR некоторых байтов пакета), после чего срывы более не наблюдались.

Иллюстрация работы программы на очень сильно попорченных тестовых данных -- случайная потеря 50% и порча оставшихся 50% пакетов
Рис.14. Иллюстрация работы программы на очень сильно попорченных тестовых данных -- случайная потеря 50% и порча оставшихся 50% пакетов

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

Итог

В итоге было реализовано все. Получена масса удовольствия. Сформирована эта WIKI. Кое-где пришлось приврать :D.

Все схемы, документы, код, отчет располагается в GitHub-репозитории проекта. Все вспомогательные документы находятся в разделе "ссылки".

Замечания

1. Не совсем. На момент написания статьи проблема не была решена. Решение планируется к моменту следующего проекта.
2. В реальности код, конечно же, не такой. Это лишь "демонстрация идеи".
3. Однако, не взлетела. На тестовых данных, поступавших по COM-порту с другой программы, все было идеально. Но в боевых условиях все пошло сильно не так. С чем это связано -- пока не ясно. Было решено не производить никаких разбирательств -- коэффициенты подобрались и так. Есть подозрение, что 99% все-таки дело в программе микроконтроллера, а не в данной программе. И, вероятнее всего, подвели неявные приведелния типов signed и unsigned. (Тестирование производилось при помощи драйвера виртуальных портов от Eltima Software).
4. В ходе тестирования выяснилась удивительная вещь -- программа ну никак не хотела работать с COM3и COM7. Пришлось руками переставлять COM-порт на COM2. На COM8 все работало прекрасно. Есть подозрение, что дело в нечетности портов, хотя в него верится крайне слабо. Тот же монитор COM-порта, через который подбирались команды, работал со всеми портами, тогда как наша программа наотрез отказывалась работать с нечетными.

results matching ""

    No results matching ""