Pull to refresh

Использование блоков UDB контроллеров PSoC фирмы Cypress для уменьшения числа прерываний в 3D-принтере

Reading time 14 min
Views 3.7K


В комментариях к переводу фирменной документации по UDB было верно замечено, что просто сухие факты не способствуют пониманию материала. Но в том документе расположены именно сухие факты. Чтобы разбавить их практикой, давайте отвлечёмся от перевода. Давайте повертим в руках этот блок и посмотрим, чего и как от него можно добиться в практической плоскости.

Длинное введение


Эта статья является второй частью задуманной трилогии. Первая часть располагается здесь (Управление RGB светодиодами через блок UDB микроконтроллеров PSoC фирмы Cypress).

Кроме применений UDB контроллеров PSoC фирмы Cypress, где на них реализуются те или иные интерфейсы, было бы интересно проверить, как эти блоки могут облегчить жизнь программистам, разгрузив центральный процессор от тех или иных ресурсоёмких задач. Но чтобы пояснить, что я собираюсь делать, придётся написать обширное предисловие.

Осенью 2015 года я купил новенький 3D-принтер MZ3D, а к весне 2016-го мне надоело, как гремят его шаговые двигатели. Времена были дикими, мы выживали, как могли, поэтому единственным решением тогда был переход с микрошага 1/16 на 1/32. Переписка с заводом показала, что на Arduino это невозможно. Как выяснилось, в «прошивке» тех лет стояло ограничение, при частоте шагов выше 10 КГц делать не честный шаг, а два виртуальных, иначе системе просто не хватало времени для обработки всех «шаговых» прерываний. Оставался один выход — перетаскивать всё на платформу ARM. Именно перетаскивать, а не скачать, так как готовых ARM решений на тот момент тоже не было найдено. За пару недель я перенёс всё это на STM32F4, звук двигателей стал приятней, вопрос был решён.

Потом у нас в компании началась разработка ОС, и мне на совещаниях приходилось долго доказывать, что типовой подход к обработке прерываний не всегда приемлем по быстродействию, апеллируя как раз к тому типичному, но очень прожорливому случаю. Рассуждения на эту тему опубликованы в моей статье, посвящённой прерываниям в ОС здесь (Обзор одной российской RTOS, часть 8. Работа с прерываниями). В общем, в голове надолго засела проблема: частые вспомогательные прерывания, обслуживающие одну подсистему, подтормаживают всё остальное. Простое умощнение центрального процессора, конечно, устраняет проблему, но не приносит Глубокого Морального Удовлетворения, что всё сделано верно.

Периодически я возвращался к этому вопросу в чисто теоретическом плане. Например, однажды мне закралась в голову мысль, что вместо использования дорогого контроллера можно взять три STM32F103C8T6, у которых готовая макетка стоит 110 рублей с учётом доставки, а сам чип и того дешевле. В один из них вынести только функцию управления двигателями. Пусть он тратит все свои вычислительные мощности на эту функцию. А пара остальных (может даже и один) решает прочие задачи (обработка команд, работа с ШИМ, поддержание температуры и т.п.) в спокойной обстановке. У такого решения также есть огромный побочный плюс — суммарное число выводов у нескольких контроллеров просто огромно. На одном STM32 мне пришлось долго раскладывать пасьянс, какую ножку на что назначить. Хоть ножки выходов таймеров и ножки АЦП у ARM-ов назначены более гибко, чем у старых контроллеров (один выход аппаратного блока может выходить на одну из нескольких физических ножек), но при раскладывании того самого пасьянса, понимаешь, что гибкости может и не хватить. Если контроллеров много, то выбор увеличивается. На том, который обслуживает шаговые двигатели, вообще просто назначаем все ножки, как цифровые выходы. На остальных тоже есть, где развернуться.

Одна беда у такого подхода, как синхронизировать эти контроллеры? В теории, ОСРВ МАКС содержит всё необходимое. Обработчик команд формирует список заданий для перемещения головок. Периодически он их модифицирует (согласуя ускорения со вновь прибывшими заданиями). Так что память для формирователя и исполнителя должна быть общая. ОСРВ МАКС содержит функционал для организации такой общей памяти. Я описывал его тут (Обзор одной российской RTOS, часть 7. Средства обмена данными между задачами). Но на практике всё портит один нюанс: обслуживание шаговых двигателей относится к критичному по времени типу задач. Малейшая задержка, и получаем для 3D-принтера наплывы пластика, для иных станков с ЧПУ — ну, например, неверно нарезанную резьбу. Любая связь через последовательные интерфейсы не самая быстрая. Плюс — время на арбитраж и прочие служебные нужды. И получается, что весь выигрыш от выноса функционала с основного процессора уходит на накладные расходы. Само собой, я воспользовался служебным положением: пошёл и обсудил этот вопрос с разработчиками данной подсистемы. Увы. Они сказали, что синхронизация без особых накладных расходов в ОС есть, но для оборудования, поддерживающего соответствующие шины. Вот если я возьму за основу архитектуру TigerShark, ОС мне всё организует совсем без накладных расходов. Только контроллеры, сделанные по этой архитектуре, стоят в разы дороже, чем весь тот 3D-принтер, в который я хотел всё это засунуть. В общем, снова неприемлемо.

Подходим к финалу затянувшегося вступления. Кто-то скажет, что чего-то я всё ищу принца на белом коне. Можно взять и сделать всё без ОС, а я тут рассматриваю всякие варианты… Можно-то можно, но, когда возникла практическая проблема «Надоело слушать грохот принтера», она была быстро устранена. Всё. Её больше нет. Мало того, с тех пор появились новые драйверы шаговых двигателей, которые вообще решают ту проблему совсем иным путём (они получают микрошаг 1/16, а наружу отдают 1/256). А в данном введении я описываю, именно что «Нет красивого решения проблемы частых прерываний». Некрасивое решение давно сделано. На проверку других некрасивых решений тратить время не хотелось. Они просто прокручивались в голове.

Но когда я разбирался с блоками UDB, мне показалось, что проблему можно решить красиво и кардинально. Можно просто увести обработку прерываний с программного на микропрограммный уровень, оставив вычислительную часть на совести основного процессора. Не надо дополнительных контроллеров! Всё размещено на одном и том же кристалле! Итак, приступаем.

Сферический конь в вакууме


В этой статье во главе угла будет стоять сама работа с UDB. Если бы я рассказывал о привязке к конкретной «прошивке», мне бы могли справедливо указать, что я ошибся с хабом. Что это для GeekTimes. Поэтому UDB первичен, а шаговые двигатели — это просто красивая вещь для иллюстрации. В этой части я вообще сделаю сферического коня в вакууме. У него будут практические недостатки, которые я устраню уже во второй части. Но повторяя мои действия, читатели смогут освоить методику разработки микропрограмм для UDB.

Итак. Как работает механизм управления шаговыми двигателями? Есть задача, ставящая в очередь отрезки, которые должна пройти головка с линейной скоростью. Пока что я сделаю вид, что не помню про ускорения в начале и конце отрезка. Просто головка должна пройти. Новые отрезки ставятся в хвост очереди. На основании записи из головы, отдельная задача посылает сигналы STEP на все активные двигатели.

Пусть у принтера максимальная скорость головки равна 200 мм/с. Пусть на 1 миллиметр перемещения требуется сделать 200 шагов (такая цифра соответствует реальному принтеру MZ3D-256C с микрошагом 1/32). Тогда импульсы должны подаваться с частотой вплоть до 200 * 200 = 40000 Гц = 40 КГц. Именно с такой частотой вполне может вызываться задача, посылающая шаговые импульсы. Она должна программно сформировать сами импульсы, а также произвести расчёт, через какой промежуток времени следует вызвать следующее активирующее её прерывание.

Вспоминается анекдот про Колобка и Трёх Богатырей, где Колобок последовательно здоровался с Богатырями, затем последовательно задавал им вопросы и получал ответы. Затем последовательно прощался с ними. Ну, а затем он встретился с Тридцатью Тремя Богатырями. Процессор здесь в роли колобка, а шаговые двигатели — в роли Богатырей. Понятно, что при наличии большого количества блоков UDB, можно распараллелить работу с двигателями, поручив обслуживание каждого двигателя своему блоку. И раз уж у нас имеются отрезки, на протяжении которых двигатели будут шагать равномерно, давайте попробуем заставить аппаратуру работать именно с такими транзакциями, а не с каждым шагом.

Какие сведения требуются, для того чтобы сферический конь в вакууме прошагал линейный участок?

  • Число шагов.
  • Период времени между шагами.

Два параметра. В UDB как раз имеется два аккумулятора и два регистра параметров D0 и D1. Вроде, всё реализуемо. Прикинем только разрядность, которая должна быть у этих регистров.

Сначала число шагов. Если будет 8 разрядов, то за один цикл работы UDB принтер сможет сдвинуть головку картезианского принтера чуть более, чем на 1 мм (200 микрошагов). Маловато. Если разрядность будет 16 бит, то число шагов будет уже 65536. Это 65536/200=327 миллиметров. Для большинства моделей приемлемо. Для Core, Дельт и прочих надо прикидывать, но в целом — для полного хода отрезок можно разбить и на несколько частей. Их будет не так много (две, ну максимум три).

Теперь период. Пусть тактовая частота равна 48 МГц. 48000000/65536=732. То есть минимальная допустимая частота, которую можно получить при помощи 16 разрядного делителя равна 732 Гц. Многовато. В «Прошивке» Marlin минимум стоит 120 Гц (что примерно соответствует 8 МГц, делённым на ту же константу 65536). Придётся делать регистры 24 разрядными. Тогда минимальная частота станет равна 48000000/(2^24) = 48000000/16777216=2.861 Гц.

Хорошо. Хватит занудной теории! Переходим к практике! Запускаем PSoC Creator и выбираем File->New->Project:



Далее я выбрал имеющуюся у меня макетку, из которой среда возьмёт базовые сведения об используемом контроллере и его настройках:



Я уже чувствую себя готовым создать проект с нуля, поэтому выбираю Empty Schematic:



Даём рабочей среде имя PSoC3DTest:



И вот он, готовый проект!



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



Щёлкаем правой кнопкой по проекту и выбираем Add Component Item:



Говорим, что нам надо добавить UDB Document, меняем имя на StepperController и щёлкаем по Create New:



Компонент появился в дереве, плюс — открылся редактор этого компонента:



Помещаем на форму блок Datapath:



Выделив этот блок, идём в его свойства и меняем разрядность с 8 на 24. Остальные параметры можно оставить без изменения.



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



Прежде чем рассказывать о логике автомата, опишу ещё одну чисто инженерную проблему: задание длительности импульса Step. Документация на драйвер DRV8825 требует, чтобы ширина этого импульса была не менее, чем 1.9 мкс. Прочие драйверы менее требовательны к его ширине. Как уже отмечалось в теоретической части, имеющиеся регистры уже заняты под задание длительности шага и числа шагов. Как ни крути, а на схему следует поместить семибитный счётчик. Назовём его одновибратором, который задаёт шаговый импульс. При частоте 48 МГц для обеспечения длительности 1.9 мкс, этот счётчик должен отсчитать не менее 91.2 шага. Округлим до 92. Любое значение, превышающее это, будет не менее. Получается следующая настройка:



Имя счётчика SingleVibrator. Он никогда не сбрасывается, поэтому вход Reset всегда подключён к нулю, он считает, когда автомат (описанный чуть ниже) находится в состоянии One, загружается он во всех прочих состояниях (сначала я выбирал конкретные состояния автомата, но оказалось, что при таком хитром методе, требуется намного меньше ресурсов PLD, а результат — тот же самый). Загружаемое значение равно десятичному 92. Правда, добрый редактор сразу же заменит это значение на шестнадцатеричное:



Когда счётчик досчитал до нуля, он сообщит об этом в цепь с именем One_Finished. Со счётчиком — всё.

Какие нашему автомату понадобятся флаги статуса? У меня получилось вот так (напоминаю, чтобы задать их, требуется дважды щёлкнуть в Datapath по списку выходов):





Аккумулятор A0 я буду использовать как счётчик длительности импульса, поэтому, когда его значение достигло нуля, будет взведён флаг, которому я дал имя Pulse_Finished. Аккумулятор A1 у меня будет считать импульсы. Поэтому его зануление будет взводить флаг Process_Finished.

Строим граф переходов автомата:



Переменная, задающая его состояние, называется State. Сразу же сопоставляем эту переменную регистру адреса инструкции АЛУ. Я сначала забыл это сделать, поэтому довольно долго не мог понять, почему у меня автомат не работает. Дважды щёлкаем по блоку входов в Datapath:



И сопоставляем:



Начинаем разбираться с графом переходов и сопоставленными с ним инструкциями АЛУ.

Начнём с состояния Idle. Оно довольно насыщено по своим действиям.

Во-первых, в нём в аккумуляторы A0 и A1 постоянно помещается значение регистров данных D0 и D1, соответственно:



Из этой записи намётанный глаз увидит всё нужное. Так как у нас ещё глаз не намётан, дважды щёлкаем по записи и увидим то же самое, но подробнее:



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

Теперь смотрим, что делается на уровне графа переходов:



Вспомогательный триггер Start_Prev позволяет отлавливать положительный перепад на входе Start, организуя линию задержки на 1 такт. В нём всегда будет находиться состояние входа Start, которое было на предыдущем такте. Кому-то привычнее увидеть это на языке Verilog:



То же самое текстом
always @ (posedge clock)
begin : Idle_state_logic
    case(State)
        Idle : 
        begin
            Start_Prev <= (Start);
            IsIdle <= (1);
            if (( Start&(!Start_Prev)&(!Process_Finished) ) == 1'b1)
            begin
                State <= One ;
            end
        end


Соответственно, условие Start&(!Start_Prev) истинно, только когда между тактами произошёл положительный перепад линии Start.

Кроме того, когда автомат находится в этом состоянии, выход IsIdle переводится в единичное состояние, информируя внешнюю среду, что блок пассивен. При таком подходе тратится меньше ресурсов PLD, чем если на выход подать конструкцию State==Idle.

Когда из внешней среды пришёл перепад сигнала Start, и в аккумуляторе A1 находится ненулевое значение, автомат выйдет из состояния Idle. Если в A1 занесён ноль, то двигатель не участвует в отработке данного отрезка, так что перепад на линии Start игнорируется. Это касается неиспользуемого экструдера. Для ряда принтеров также довольно редко используется двигатель по оси Z. Напомню, как формируется условие, выявляющее нулевое значение в A1 (а ненулевое — это его инверсия):



Далее автомат попадает в состояние One:



В этом состоянии на выходе Step задано значение 1. На драйвер подаётся шаговый импульс. Кроме того, сбрасывается значение триггера IsIdle. Внешняя среда информируется, что блок находится в активной фазе.

Выход из этого состояния производится по сигналу One_Finished, который взведётся в единицу, когда семибитный счётчик досчитает до нуля. Напомню, что сигнал One_Finished формируется именно этим счётчиком:



Пока автомат находится в этом состоянии, АЛУ загружает в аккумулятор A0 (задающий длительность импульса) значение из регистра D0. Давайте я покажу только краткую запись, говорящую об этом:



Загруженное значение будет использовано в следующем состоянии. Находясь в нём, автомат формирует задержку, задающую длительность импульса:



Выход Step сбрасывается в нулевое значение. Аккумулятор A0 уменьшается, о чём свидетельствует следующая краткая запись:



А если по ней дважды щёлкнуть — полная запись:



Когда значение A0 достигнет нуля, взведётся флаг Pules_Finished, и автомат перейдёт в состояние Decrement:



В этом состоянии в АЛУ уменьшается значение аккумулятора A1, задающего число импульсов:



Полный вариант записи:



В зависимости от результата происходит переход либо к следующему импульсу, либо в состояние Idle. Дважды щёлкнем по состоянию, чтобы увидеть переходы с учётом приоритетов:



Собственно, с UDB всё. Теперь делаем соответствующий символ. Для этого щёлкаем правой кнопкой по редактору и выбираем Generate Symbol:



Идём на схему проекта:



И вводим схему, в которой имеется некоторое число этих контроллеров. Я выбрал пять (три оси плюс два экструдера). Принтеры с большим числом экструдеров к дешёвым относить не будем. На них можно и FPGA поставить. Попутно, чтобы увидеть реальную сложность, я бросил на схему блок USB-UART (для приёма данных от ЭВМ или той же Raspberry Pi) и настоящий UART (он будет обеспечивать связь с дешёвым Wi-Fi модулем ESP8266 или, скажем, интеллектуальным дисплеем, умеющим слать GCODE через UART). ШИМы и прочее не добавлял, так как их сложность примерно ясна, а до реальной системы ещё далеко. Получилось как-то так:



Управляющий регистр формирует сигнал запуска, который идёт на все блоки одновременно. Кроме того, из него же пусть выходят и сигналы, которые во время формирования отрезка статичны. Все выходы Idle я собрал по «И» и подал на вход прерывания. Прерывание я назначил по положительному фронту. Если хоть один двигатель начал работу, вход прерывания будет сброшен. По окончании работы последнего двигателя он будет взведён, что проинформирует процессор о готовности к выводу следующего отрезка. Теперь настроим частоты, дважды щёлкнув по элементу дерева Clocks:



В появившейся таблице дважды щёлкнем по элементу PLL_OUT:



Заполним таблицу как-то так (я ещё недостаточно хорошо постиг правила настройки этой таблицы, именно поэтому применяю термин «Как-то так»):



Теперь дважды щёлкаем по строке Clock_1:



Задаём частоту тактирования блоков UDB равной 48 МГц:



Так как проект экспериментальный, API к нему делать нет смысла. Но чтобы закрепить материал, изученный в прошлой статье, снова идём на вкладку Components и для проекта StepperController правой кнопкой через Add Component Item добавляем сначала заголовочный файл, а затем файл исходного кода C:





Чисто поверхностно покажу те две функции инициализации и старта отрезка, которые я добавил. Остальное можно посмотреть в примере к статье.

void `$INSTANCE_NAME`_Start()
{
    `$INSTANCE_NAME`_SingleVibrator_Start();        //"One" Generator start
}
void `$INSTANCE_NAME`_PrepareStep(int nSteps,int duration)
{
    CY_SET_XTND_REG24(`$INSTANCE_NAME`_Datapath_1_D0_PTR, duration>92?duration-92:0);
    CY_SET_XTND_REG24(`$INSTANCE_NAME`_Datapath_1_D1_PTR, nSteps>1?nSteps-1:0);
}

Имя файла main.c я заменил на main.cpp, чтобы проверить, что среда разработки нормально отреагирует на использование С++, ведь «прошивка» Marlin объектно-ориентированная. Предсказуемо посыпались ошибки, которые предсказуемо же были устранены добавлением штатной вещи:



То же самое текстом
extern "C"
{
    #include "project.h"
}


Для глобального запуска двигателей я сделал такую функцию (она очень грубая, но для опытов со сферическим конём в вакууме сойдёт, при опытах время разработки важнее красоты):
void StartSteppers()
{
    Stepper_Control_Reg_Write (1);
    Stepper_Control_Reg_Write (1);
    Stepper_Control_Reg_Write (1);
    Stepper_Control_Reg_Write (0);
}

Она взводит сигнал Start, на всякий случай, сразу на три такта, затем снова роняет его.

Ну, и приступаем к экспериментам. Сначала просто шагнём двигателями X и Y (в примере первая группа вызовов инициализирует все контроллеры, вторая настраивает контроллеры X и Y на требуемое число шагов и запускает процесс):

int main(void)
{
    CyGlobalIntEnable; /* Enable global interrupts. */
    
    
    StepperController_X_Start();
    StepperController_Y_Start();
    StepperController_Z_Start();
    StepperController_E0_Start();
    StepperController_E1_Start();

    StepperController_X_PrepareStep (10,1000); // Задали параметры шагов
    StepperController_Y_PrepareStep (50,500);
    StartSteppers();    				// Запустили процесс
    
    for(;;)
    {
    }
}

Смотрим на результат:



Проверяем длительность положительного импульса:



Всё верно. Наконец, проверяем, насколько хорошо работает прерывание. Добавляем глобальную переменную-счётчик:

static int nStep=0;

Эта переменная в функции main присваивается единице, а в функции обработчика прерывания увеличивается. Обработчик прерывания сработает только один раз, чисто для проверки. Я его сделал таким:

extern "C"
{
    CY_ISR(StepperFinished)
    {
        if (nStep == 1)
        {
            StepperController_X_PrepareStep (5,500);
            StartSteppers();
            nStep += 1;
        }

    }
}

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



То же самое текстом
int main(void)
{
    CyGlobalIntEnable; /* Enable global interrupts. */
    isr_1_StartEx(StepperFinished);
    
    
    StepperController_X_Start();
    StepperController_Y_Start();
    StepperController_Z_Start();
    StepperController_E0_Start();
    StepperController_E1_Start();

    /* Place your initialization/startup code here (e.g. MyInst_Start()) */
    StepperController_X_PrepareStep (10,1000);
    StepperController_Y_PrepareStep (20,500);
    StartSteppers();    
    nStep = 1;
    
    for(;;)
    {
    }
}


Проверяем результат (на втором шаге должен сработать только двигатель X, причём шаги должны стать вдвое реже):



Всё верно.

Заключение


В целом, уже видно, что блоки UDB могут использоваться не только для задания быстрых аппаратных функций, но и для выноса логики с программного на микропрограммный уровень. К сожалению, объём статьи получился таким большим, что закончить рассмотрение и получить однозначный ответ, хватит ли возможностей UDB для окончательного решения поставленной задачи или нет, не представляется возможным. Пока готов лишь сферический конь в вакууме, действия которого в принципе очень похожи на требуемые, но въедливый читатель, знакомый с теорией управления шаговыми двигателями, найдёт в нём массу недостатков. Представленный блок не поддерживает ускорения, без которых невозможна работа реального шагового двигателя. Вернее, поддерживает, но на этом этапе потребуется высокая скорость прерываний, а всё задумывалось, чтобы избежать этого.

Точность задания частоты представленного блока далека от приемлемой. В частности, он обеспечит частоту импульсов 40000 Гц при делителе 1200 и 39966 Гц при делителе 1201. Промежуточные частоты между этими двумя значениями на данном блоке недостижимы.

Возможно, есть в нём и какие-то иные недостатки. Но их устранением с проверкой, хватит ли ресурсов UDB, займёмся в следующей статье.

Пока же читатели получили среди прочего, реальный пример создания блока на базе UDB с нуля. Тестовый проект, получившийся при написании этой статьи, можно взять здесь.
Tags:
Hubs:
+13
Comments 6
Comments Comments 6

Articles