Краткий курс HDL. Часть 12. Дополнения к разделу об автоматах состояний. Многоядерность, многозадачность, многопоточность

№ 4’2009
PDF версия
Это небольшое дополнение к разделу об автоматах состояний приходится дописать к напечатанному ранее разделу «Краткого курса HDL».

Все статьи цикла

Ранее был дан обычный раздел: определение автоматов, краткое описание и небольшой пример. Также было приведено описание программируемого таймера — как базового компонента для задания выдержки времени в автоматах. И автор считал, что этого будет вполне достаточно… Без сомнения, этого должно быть достаточно для описания языка Verilog. И автор полагал, что автоматы состояния — вещь известная, и более о них писать смысла не имеет.

Но, как оказалось, есть довольно много вопросов в почте и в конференциях на тему: «что это вообще такое» и «как это работает». Это была первая причина, побудившая автора к написанию этого раздела. А вторая причина — это разговор на недавно проходившей выставке «РАДЭЛ-2008». К автору подошел молодой человек и, среди прочих, задал вопрос о том, как на ранней стадии проекта определить требуемую «емкость» ПЛИС, и по каким критериям можно выбрать тип ПЛИС. К моменту объяснения о многопоточности молодой человек дал понять, что он не готов воспринимать объяснения устно… А это значит, что раздел об автоматах состояний надо усилить. И дать не только первичное описание, но и привести более «инженерные» случаи применения автоматов состояний.

Но, кроме описания автоматов, хочется в этом разделе также обсудить термины «многоядерность», «многозадачность», «многопоточность». Обычно они применяются как заклинания рекламистов и торговцев для описания преимуществ компьютеров и ноутбуков. И редко кто, услышав эти термины, вспоминает, что компьютер сам по себе — тоже статический автомат. Отсюда можно сделать вывод, что термины «многоядерность», «многозадачность», «многопоточность» далеко не у всех ассоциируются со статическими автоматами в ПЛИС. А зря! Вот уж где, как не в ПЛИС, и надо применять эти термины, но не как торговые заклинания, а как вполне проверенные и востребованные технические решения.

 

Автомат состояний — описание для начинающих

Если то описание автоматов, которое было приведено ранее, вам понятно, то этот раздел вы можете пропустить и перейти к следующему. Но автор готов привести значительно более облегченное, но более образное для понимания описание автомата. Автор заранее просит прощения у академических и университетских работников за «ненаучный» стиль описания… Хотя, если признаться, то хорошее описание, вполне понятное даже новичку, сделать непросто…

Итак, когда-то, в прошлом веке, в театрах страны шел классический финский спектакль «Господин Пунтилла и его слуга Матти». Так вот, в этом спектакле главный герой — господин Пунтилла — наглядно эмулировал два вида логических устройств. Вечером, после ряда неких процедур, происходил переход главного героя к поведению, эмулирующему простейшую комбинационную логику. В этом состоянии господин Пунтилла был очень веселым, реагировал только на простейшие сигналы, обещал выдать свою дочку за своего слугу Матти, и, самое главное, что он ничего больше не помнил — ни других своих обещаний, ни сословных различий. Его поведение определялось только теми входными сигналами, которые воздействовали на него непосредственно в данный момент. Но так происходило только вечером. А по утрам, также после ряда неких тяжелых процедур, имевших обратное действие, к господину Пунтилле возвращалась память, и происходил переход к поведению, эмулирующему уже не простейшую комбинационную логику, а автомат состояний. Он вспоминал, что у него есть богатый сосед, за которого он очень хочет выдать свою дочку. Он вспоминал также, что он хозяин, а Матти—всего лишь его слуга, поэтому утром к слуге Матти он относился уже совсем по-другому… Остальное уже не так важно. Важно только то, что утром господин Пунтилла действовал не только в соответствии с сигналами, которые воздействовали на него непосредственно, но и теми сигналами, которые действовали на него ранее. Его действия определялись и тем, в каком состоянии он находился в момент прихода этих сигналов.

Что же здесь главное? Какой можно сделать вывод? Вывод очень прост! Итак, запоминаем: если некоторый объект принимает состояния, зависящие не только от поступающих на его входы сигналов, но и от того, что он «помнит», то такой объект и является автоматом состояний. В противном же случае — это будет только простейшая комбинационная логика. Как видите, здесь все очевидно.

Осталось только дать объяснение того, как именно работает автомат. Вы, конечно, знаете, как скачут девочки «в классики». Вот это и есть совершенная модель автомата. Переходы выполняются из одного в другой, последовательно и в соответствии с диаграммой переходов, которая рисуется мелом на асфальте как шаблон. Теперь соотнесем прыжки с диаграммой состояний автомата. Покажем направление каждого прыжка стрелкой. Если прыжок приводит к перемещению из одного состояния в другое, то и стрелка будет нарисована от одного кружка к другому. Ну а «прыжки на месте», не приводящие к переходу автомата из одного состояния в другое, изображаются стрелкой, которая выходит из одного какого-то кружочка и возвращается в этот же кружок. Дополнительно над этими стрелками могут быть добавлены соотношения или описания сигналов, вызывающих переход автомата.

Да что там «классики»… Мы и сами-то обычно делаем переходы— «работа — дом и обратно»… Вот собственно и все описание. Автор надеется, что теперь все должно быть просто и понятно. Можно сказать, тривиально…

Теперь, когда все формальность по описанию автомата соблюдены, можно перейти к более детальным описаниям его работы.

 

Состояния автомата. Сколько их должно быть?

Давайте представим довольно типичную ситуацию. Например, мы хотим сделать какое-нибудь несложное устройство, которое бы выполняло несколько задач и результат работы представляло бы на ЖКИ в виде сообщений. Сообщения можно представить как массив байтов в памяти, заканчивающийся байтом, имеющим код «00». Именно так обычно кодируются сообщения в «микроконтроллерной» технике. Блок-схема алгоритма выдачи текстового сообщения показана на рис. 1.

Рис. 1. Блок-схема алгоритма выдачи текстового сообщения

Как бы мы поступили, если в качестве основного ядра устройства был выбран микроконтроллер? Скорее всего, так: выполнили бы программу управления в виде набора функций. Самая «нижняя» функция, назовем ее «А», выполняла бы запись байта в порт и делала бы соответствующие временные задержки. Функция более верхнего уровня (об уровне функций здесь говорится только для облегчения понимания; на самом деле, конечно, функции не делятся «по вертикали»), назовем ее «В», пересылала бы символы сообщения в функцию «А» и проверяла бы сообщение, чтобы определить, является ли следующий байт концом сообщения, то есть имеет ли он код «00». И, если код следующего символа не является концом сообщения, то функция «В» выполнялась бы повторно. Кроме этих двух функций — «А» и «В», мы бы, наверное, сделали еще одну функцию еще более верхнего уровня, чем функция «В». Назовем условно такую функцию «С». А что же должна делать эта функция? Она должна получить в качестве параметра указатель на адрес ячейки памяти, в котором находится требуемое нам сообщение, и далее функция «С» вызывает для вывода сообщения функцию «В». Таким образом, когда мы проектируем программу на микроконтроллере, мы заранее выстраиваем все служебные функции. А потом уже оперируем только с функциями типа «С», которые «сами знают» то, как надо выдавать сообщения и как надо работать с периферией.

И, без сомнения, редко кому в голову придет делать выдачу сообщений «в лоб», то есть выполнять это какой-то одной функцией. Блок-схема алгоритма выдачи текстового сообщения при помощи подпрограмм показана на рис. 2.

Рис. 2. Блок-схема алгоритма выдачи текстового сообщения при помощи подпрограмм

Ну, а теперь давайте снова вернемся к решению этой же задачи, но на ПЛИС. Вот он, подход «в лоб», то есть на одном автомате состояний (рис. 3).

Рис. 3. Диаграмма переходов автомата для выдачи текстового сообщения

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

Итак, мы видим, что состояний автомата достаточно много. А это значит, что, если мы делаем высокоскоростную схему, то за счет увеличения числа состояний и комбинационной логики мы проиграем в быстродействии… Теперь самое время задать вопрос: «А можно ли решить эту задачу так же, как и “в микроконтроллерах”»? И чтобы плавно подойти к ответу на этот вопрос, давайте сначала рассмотрим термин «многоядерность».

 

Многоядерность

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

Почему мы сейчас говорим именно о микроконтроллерах? Да только потому, что это «ближайшие родственники» автоматов состояний в ПЛИС, и мы в этой статье рассматриваем микроконтроллеры так же, как и автоматы. Действительно, давайте рассмотрим то, как производится выполнение задачи в микроконтроллерах. Задача пользователя обычно состоит из нескольких подзадач, которые хочется выполнять одновременно. Этому будет посвящен раздел о «многозадачности». А сейчас важно только то, что из многих подзадач можно выделить несколько типовых, которые используются достаточно часто. Например, обработка такого параметра, как время. Выдержка времени, подсчет числа импульсов за определенное время, привязка входного сигнала к тактовой частоте и т. д. Так что же сделано для этого в микроконтроллерах? Все такие задачи выделены для обработки на отдельном, специализированном узле. И, как вы знаете, этот узел называется «таймер». А как объединяются ядра в микроконтроллере? Обычно иерархически. То есть одно ядро «руководит» всеми остальными. Мало того, точно так же устроены и многие другие «многоядерные» микросхемы. Например, телекоммуникационные микроконтроллеры.

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

Рис. 4. Блок(схема телекоммуникационного микроконтроллера

Как видите, просто и удобно. Можно ли выполнить аналогичную архитектуру в ПЛИС? Да, конечно! Мало того, сейчас мы рассмотрим то, как это делается, а потом сравним полученную архитектуру с решением «в лоб».

Что касается логики работы рассмотренного примера, то давайте примем точно такое же разбиение алгоритма на три части — «А», «В» и «С». Нам уже известно, что и как выполняет каждая часть алгоритма. В соответствии с этими задачами разобьем автомат состояний также на три части. Посмотрите еще раз на рис. 3. Возле каждого кружочка, обозначающего состояние автомата, есть транспарант с текстом, описывающим действия в этом состоянии. Цвет транспарантов как раз и покажет вам, как мы будем разбивать автомат на части. Как будет работать каждая часть — понятно. А как они будут взаимодействовать? Когда мы говорим с программистами, то там все понятно, функция возвращает байты (не будем вдаваться в «дебри» программистских хитростей)… Аппаратные же узлы «возвращают» сигналы. Таким образом, давайте определим, как и какими сигналами мы будем управлять запуском автомата и какие сигналы мы от него получим.

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

Итак, назовем сигнал, запускающий выполнение задачи в автомате, «Старт» (Start). Сигнал, свидетельствующий о том, что автомат выполнил задачу, назовем «Готовность» (Ready). Дополнительно к этим двум основным сигналам можно еще определить сигнал «Занято» (Busy). Теперь определим время активности для этих сигналов. Исходно, после прохождения сигнала «Сброс» оба сигнала — «Готовность» и «Занято» — не активны. Давайте примем следующий порядок взаимодействия сигналов. При установлении сигнала «Старт» автомат запускается на выполнение задачи. Следовательно, к моменту подачи сигнала «Старт» все другие сигналы, которые поступают на автомат извне, должны быть в установившемся состоянии и достоверны. Автомат выполняет задачу столько времени, сколько определено его архитектурой и алгоритмом выполняемой задачи. Во время выполнения задачи сигнал «Занято» должен быть активным. По окончании выполнения задачи автомат должен выставить сигнал «Готовность» и снять активное состояние с сигнала «Занято».

Теперь необходимо сказать о соотношении сигналов «Старт», «Занято» и «Готовность». Первый вариант таков: по установлению сигнала «Занято» снимается сигнал «Старт». После выставления сигнала «Готовность» можно подавать следующий сигнал—«Старт». Более того, сигнал «Старт» может быть подан даже непрерывно, тогда автомат, выставив сигнал «Готовность» в этом же такте, снова запустится на выполнение задачи. Второй вариант: сигнал «Занято» использовать не будем, вместо него будем определять переход сигнала «Готовность» из неактивного состояния в активное. Будет использоваться меньше сигналов, но диаграмма будет несколько дольше. Эти диаграммы показаны на рис. 5. Верхняя диаграмма запускается сигналом «Старт», который сбрасывается при получении сигнала «Занято». Нижняя диаграмма отличается тем, что сигнал «Старт» сбрасывается при получении сигнала «Готовность».

Рис. 5. Диаграммы взаимодействия двух автоматов. Сигналы «Старт», «Занято» и «Готовность»

Давайте остановим свой выбор на первом варианте, так как с ним нам сейчас будет работать проще.

Остается только сделать одно небольшое дополнение. Как мы видим, для того чтобы начать читать данные, автомату необходимо произвести обработку сигнала «Готовность». На это требуется один или несколько тактов. В это время тот автомат, который производил вычисления, ждет. Для того чтобы убрать непроизводительные потери времени на ожидание, можно сделать упреждающую выдачу сигнала «Готовность». Если мы заранее знаем, сколько тактов потребуется на то, чтобы произвести обработку сигнала «Готовность», то давайте и передвинем вперед сигнал «Готовность» именно на это число тактов. Конечно, это можно применить только в тех случаях, когда мы знаем время, необходимое на обработку основной задачи, выполняемой автоматом при проведении вычислений. Такой вариант взаимодействия приведен на рис. 6, где представлена диаграмма сигналов с «опережающей» выдачей сигнала «Готовность». Синяя стрелка показывает начало выдачи опережающего сигнала «Готовность». Красная стрелка показывает начало чтения данных. Состояние ожидания чтения отсутствует. Но точно такого же эффекта можно добиться, если мы сможем выдавать опережающий сигнал «Занято», то есть передний фронт сигнала «Занято» будет находиться на прежнем месте, а задний фронт будет перемещен вперед на требуемое число тактов. Конечно, в этом случае мы не должны нарушить все другие случаи использования этих сигналов. Например, для последовательного арбитража, о чем будет рассказано далее.

Рис. 6. «Опережающая» готовность

Кроме приведенного на этой диаграмме соотношения сигналов, можно построить взаимодействие и с сокращенным набором сигналов. Если внимательно рассмотреть верхнюю диаграмму на рис. 5, то можно заметить, что для взаимодействия вполне достаточно сигналов «Старт» и «Занято». А для нижней диаграммы на этом же рисунке достаточно сигналов «Старт» и «Готовность». Но мы в дальнейшем описании будем использовать и сигнал «Занято», и сигнал «Готовность», поскольку так будет легче восприниматься описание устройств. В более общем случае можно дополнительно обратиться к разделу о пересечении сигналами клоковых доменов, где приведены различные описания синхронизаторов. Алгоритмы взаимодействия двух сторон проекта могут быть использованы и для синхронизации автоматов, работающих и на одной тактовой частоте. Конечно, с учетом того, что применяется общая тактовая синхрочастота.

Подведем промежуточный итог. Мы определили, что для взаимодействия автоматов нам необходимо 3 дополнительных сигнала. Теперь давайте разработаем связку из двух автоматов. Управляющий автомат назовем “Master”, а управляемый автомат — “Slave”. На рис. 7 приведена блок-схема сопряжения этих двух автоматов.

Рис. 7. Блок-схема сопряжения двух автоматов

Теперь по заданному выше алгоритму сформируем диаграммы состояний автоматов. Чтобы сформировать диаграмму переходов, мы должны тщательно рассмотреть временную диаграмму взаимодействия двух автоматов, приведенную на рис. 5. Для каждого момента времени T0–T7 мы должны отметить состояния, когда выходные сигналы автомата меняют свое значение. Так, например, сигнал «Старт» в момент времени Т0 поменял свое значение с 0 на 1. На диаграмме изменение состояний этого и всех других сигналов показано с задержкой относительно тактов. В момент времени Т2 сигнал «Старт» опять поменял свое значение, но уже с 1 на 0. Аналогично поступаем и с другими сигналами.

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

Диаграмма переходов начинается с состояния, соответствующего тому, которое автомат имеет после снятия сигнала «Сброс». Поскольку все действия автомат выполняет циклически, то состояния, соответствующего понятию «Останов», на диаграммах зачастую не бывает. На поле диаграммы переходов дополнительно записываются комментарии и другая дополнительная информация. Для каждого из автоматов составляется своя диаграмма переходов. На рис. 8 приведена диаграмма состояний для автомата Master, а на рис. 9 — для автомата Slave.

Рис. 8. Диаграмма переходов автомата Master

Рис. 9. Диаграмма переходов автомата Slave

Теперь, когда мы знаем, как взаимодействуют два автомата, можно легко представить себе, как реализуется задача, поставленная в начале статьи: связка из трех или четырех автоматов и дополнительные 8 сигналов (рис. 10). Чем же этот случай отличается от случая, приведенного на рис. 7? Там приведена блок-схема сопряжения только двух автоматов, поэтому каждый из этих автоматов выполнял только одну функцию: либо Master, либо Slave. Но в том случае, когда три или более автоматов подключаются «каскадно», один и тот же автомат может выполнять функции Master для последующего за ним автомата и в то же самое время выполнять функции Slave для предыдущего автомата. Так, на рис. 10 автомат «С» выполняет только функции Master для автомата «В». Автомат «А» выполняет только функции Slave для автомата «В». А вот автомат «В» работает как Slave для автомата «С» и как Master для автомата «А». Поэтому для его реализации у нас есть два варианта: первый — когда мы комбинируем два автомата, Master и Slave, в один автомат; второй вариант — это использовать связку из двух отдельных автоматов Master–Slave. Но и в том, и в другом случае исходный алгоритм задачи — вывод информации на ЖКИ— делится на три части. И как видим, теперь решение задачи выглядит почти так же легко, как и в случае применения микроконтроллера.

Рис. 10. Блок(схема сопряжения трех автоматов — «С», «В» и «А»

 

Многозадачность

Теперь от «многоядерности по вертикали» перейдем к «многоядерности по горизонтали». В предыдущем примере мы рассматривали то, как один автомат управлял одним автоматом Slave. А можно ли сделать так, чтобы у одного автомата Master было много автоматов Slave? Да, конечно можно! На рис. 11 приведен пример сопряжения одного автомата Master и нескольких автоматов Slave. Как и в предыдущем примере, всегда можно сделать так, чтобы автомат Slave мог воспринимать команды управления только в своих, вполне определенных состояниях. То есть, представим, что после загрузки автомат может только выполнять задачу и не может быть перезагружен новыми данными. И если при одном автомате Slave мы имеем только один набор сигналов «Занято» и «Готовность», то в случае применения нескольких автоматов Slave мы получим «набор» таких сигналов от каждого из автоматов.

Рис. 11. Пример сопряжения одного автомата Master и нескольких автоматов Slave

Если мы точно знаем, что автоматы Slave никогда не выставят сигналы «Готовность» одновременно, то тогда нам более ничего и не нужно. Набор сигналов «Готовность» и укажет нам, откуда можно выгружать данные.

Однако, если время выполнения задачи может быть разное, или по каким-либо другим причинам автоматы Slave имеют возможность выставлять сигналы «Готовность» одновременно, то тогда нам придется ввести в схему еще один узел — арбитр. На рис. 12 приведен пример сопряжения одного автомата Master и нескольких автоматов Slave, подключенных к автомату Master через арбитр — Arbiter. Для того чтобы «индивидуально» управлять автоматами Slave, арбитр имеет дополнительные сигналы Control, которые подаются на автоматы Slave. На рис. 12 эти сигналы выделены синим цветом.

Рис. 12. Пример сопряжения одного автомата Master и нескольких автоматов Slave, подключенных к автомату Master через арбитр — Arbiter

Итак, когда автоматы Slave выставят сигналы «Готовность», то арбитр определит, какой из автоматов Slave можно загружать задачей и из какого автомата можно считывать данные. Если автомат Master «хочет знать», с каким именно автоматом Slave он имеет дело, то эту информацию ему должен сообщить арбитр. А арбитр, как мы знаем, формирует эту информацию исходя из соотношений сигналов на его входах.

То, что показано на рис. 12, — это классический «выделенный» арбитр с параллельными входами или, скажем так, то, к чему мы привыкли, используя микроконтроллеры Intel. Но ведь кроме этого решения давно известно и другое: распределенный последовательный арбитр. И это решение довольно активно применялось в машинах фирмы DEC (рис. 13). Для создания распределенного последовательного арбитража необходимо выполнить следующие условия: каждый из автоматов должен выставлять свой сигнал в шину ID. Каждый из автоматов должен пропускать приходящий к нему сигнал запроса на обслуживание, если он сам в таком обслуживании не нуждается; каждый автомат должен блокировать приходящий к нему сигнал запроса на обслуживание, если он сам в нуждается в обслуживании. Автомат, находящийся в цепочке ближе к автомату Master, будет иметь больший приоритет, а автомат, находящийся в цепочке дальше всех от автомата Master, будет иметь низший приоритет и, соответственно, меньше шансов на обслуживание. Но необходимо отметить, что такой распределенный арбитр легче реализовывать. Фактически, если сигнал «Занято» какого-либо автомата Slave находится в высоком уровне, а это значит, что автомат производит вычисления, то этот автомат должен пропустить к предыдущему в цепочке автомату сигнал запроса на обслуживание. Если же сигнал «Занято» рассматриваемого автомата находится в низком уровне, то это значит, что автомат должен быть обслужен «мастером». Последний в цепочке автомат на входе запроса должен всегда иметь разрешающий сигнал.

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

Мы определили все необходимые схемные узлы для выполнения многозадачных вычислений. Все абсолютно аналогично тому, что делается «в микропроцессорах», ничего сверхнового… Итак, осталось рассмотреть еще одну интересную вещь — «многопоточность».

 

Многопоточность

Что такое «многопоточность»? Откуда пришел к нам этот термин? Он пришел к нам, как было сказано ранее, из описаний компьютеров. И там под термином «поток» понимается поток команд, которые выполняет процессор. Следовательно, из названия термина можно сделать вывод о том, что процессор одновременно выполняет несколько потоков команд. Так ли это? И да, и нет… Так как же он это делает? Без сомнения, процессор в один момент времени может выполнять только одну команду (мы пока не рассматриваем процессоры, выполняющие за один такт несколько команд). Но, вместе с тем, за некоторый интервал времени процессор может успеть выполнить несколько потоков команд, причем за одно и то же время. Хитрость в том, что в процессе выполнения программы есть некоторые интервалы времени, когда процессор простаивает. Почему простаивает процессор? Да потому, что он имеет конвейер команд.

Большинство современных процессоров работают уже на достаточно больших частотах, имеют микроархитектуру и довольно развитый конвейер команд. Число ступеней конвейера от 2–3 ступеней у старых микроконтроллеров теперь увеличилось до более чем 10–20. А, как известно, конвейер хорошо работает и дает максимальную производительность только тогда, когда нет команд переходов, или блок предсказания переходов выбрал правильное направление для перехода, и конвейер команд не простаивает. Ну, а если предсказание выдано неправильно или блока предсказания переходов нет вообще, то приходится сбрасывать очередь команд в конвейере. И, следовательно, 10–20 тактов процессор будет перезагружать очередь команд (рис. 14). Такие такты на рисунке выделены голубым цветом. Вот именно в эти моменты времени в конвейер команд загружается другой поток команд.

Рис. 14. Два потока команд, направляемых в процессор

Рис. 15. Конвейер команд в момент переключения обработки с одного потока на другой

На рис. 15 показан момент переключения обработки с одного потока на другой. В центре рисунка изображен конвейер команд. При переключении с одного потока команд на другой все промежуточные данные из конвейера переписываются в буфер для сохранения. В это же время в конвейер загружаются все промежуточные данные, полученные при обработке другого потока. И, таким образом, конвейер не простаивает, а выполняет теперь другой поток команд. Как только и в этом потоке будет необходимость сбросить конвейер команд, процессор снова автоматически произведет перезагрузку и вернется к выполнению предыдущего потока команд. Суть в том, что один и тот же процессор просчитывает поочередно несколько задач.

Как мы можем применить этот способ работы в ПЛИС? Да точно так же. Поскольку автомат состояний, в большинстве случаев, для своей работы не требует потока команд, то давайте заставим его работать с несколькими потоками данных. И точно так же сделаем автомат состояний многопоточным. Для этого нам необходимо сохранять память автомата для каждого потока. И уметь выгружать состояние автомата после вычислений в одном из потоков и, соответственно, уметь перезагружать автомат теми значениями данных, которые были у него в памяти при вычислениях в другом потоке.

Давайте снова вернемся к тому, что нам уже хорошо известно в «микроконтроллерах». Что «делает» микроконтроллер, когда надо выполнить переключение контекста? Ну, например, это довольно часто применяется при обслуживании прерываний. Сохранение контекста состоит из двух частей — из сохранения регистров и из переключения на другую область памяти. Регистры сохраняются при переписывании их в определенную зону памяти командами PUSH и восстанавливаются из этой же зоны памяти командами POP. Переключение из одной зоны памяти на другую зону в наиболее общем случае выполняется путем перезагрузки регистра-указателя. Применим эти же самые решения при проектировании автоматов состояний. И рассмотрим вполне конкретный пример, о котором автор уже упоминал в своих статьях.

Представим, что нам дали задание разработать многоканальный HDLC-контроллер. Этот контроллер обрабатывает до 30 потоков данных, находящихся в одном цифровом тракте данных. Самое первое, «лобовое» решение таково: делаем одноканальный контроллер, а потом «тиражируем» его 30 раз. Решение довольно простое. Делается быстро, симулируется и отлаживается так же быстро. И у него есть только один недостаток. Оно требует очень много ресурсов. Так давайте попробуем превратить одноканальное решение в многоканальное. Добавим к автомату состояний дополнительные такты для выполнения операций типа PUSH и POP, добавим указатель на область памяти, в которую мы будем класть промежуточные результаты вычислений и, конечно, добавим и саму память для сохранения промежуточных результатов вычислений. Также нам будет необходим многоканальный узел управления, который будет указывать время начала поступления данных в информационном канале и время окончания поступления данных. Теперь автомат будет поочередно обрабатывать от одного до 30 каналов данных. На рис. 16 в качестве примера приведены два потока данных и показано, как автомат может производить обработку этих потоков поочередно.

Рис. 16. Пример работы автомата при поочередной обработке двух потоков данных — «А» и «В»

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

В качестве примера реализации многопоточного вычислителя можно также привести следующий, довольно часто встречающийся на практике, «сюжет». Представим, что вам для управления вашей «технологией» потребуется блок таймеров. Например, для установления соединений по 30 каналам связи, вам необходимо иметь до 8 16-разрядных таймеров в каждом из этих каналов, чтобы проверять тайм-ауты по установлению соединения. Всего 240 16-разрядных таймеров. Скорости от таких таймеров особой не требуется, так как процесс соединения происходит гораздо медленнее, чем длятся 240 тактов системной синхрочастоты, присутствующей в проекте. Но весь вопрос в том, что таймеров требуется достаточно много.

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

А как будем реализовывать такой блок таймеров в ПЛИС? Реализовывать «в лоб»? Некрасиво. Ресурсов потребует не менее, чем 240_16 триггеров. Да еще и 240 схем сравнения с константой. А по «многопоточной» схеме как это будет выглядеть? Для реализации нам потребуются всего лишь следующие ресурсы:

  1. Загружаемый счетчик данных, разрядность 16 бит.
  2. Блок памяти, 240 адресов, разрядность 16 бит.
  3. Счетчик адресов памяти на 8 разрядов.
  4. Регистр разрядностью 16 бит.
  5. 8 триггеров для флагов запросов прерываний и еще несколько триггеров для «Готовности» и прочего.

На рис. 17 показана блок-схема такого многоканального таймера.

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

Как же работает такой блок таймеров?

Ядром работы такой схемы будет автомат и арифметический узел. Арифметический узел выполняет две операции: декремент (или инкремент) и сравнение полученного в результате кода с нулем. Или с кодом «1». Как это работает? Счетчик адресов памяти указывает на адрес, в котором находится текущее значение данных, соответствующее выдержке времени для этого таймера. Автомат дает команду загрузить 16-разрядный счетчик новыми данными. Далее производится декремент данных, после чего полученные в результате вычисления данные снова заносятся в память. Затем производится декремент счетчика адресов памяти. И так далее, циклически. Если счетчик в каком-то из каналов досчитал до «1», то это значение адреса счетчика записывается в регистр, состоящий из 9 триггеров, как адрес флага запросов прерываний + бит прерывания. (Поскольку возможен адрес «0».) Будем пока считать, что для срабатывания контроллера прерывания, получающего эти запросы от блока таймеров, будет достаточно одного такта. Ну, а если это не так, то для решения этого вопроса можно привлечь дополнительные аппаратные ресурсы. Например, FIFO… Пока же не будем на это отвлекаться. На следующем цикле вычислений для этого канала счетчик досчитает до кода «0», затем он останавливается, и далее в этом канале счет не производится. Это и есть вся основа многопоточного вычислителя. Причем, в идеальном случае все описываемые действия производятся вообще за 1 такт.

Итак, основные ресурсы и способ их работы мы перечислили. Далее все будет зависеть от того, можем ли мы ждать до 240 тактов, чтобы загрузить нужный нам таймер, или мы хотим работать без ожиданий. Если мы можем позволить ожидания длительностью до 240 тактов, то для загрузки требуемого канала счетчика нам будет нужен только один регистр данных на 16 бит, один регистр адреса на 8 бит и схема сравнения на 8 бит. Записываем код, который мы хотим загрузить в требуемый канал счетчика, в регистр данных, а адрес счетчика — в регистр адреса. Например, мы сначала будем записывать код данных, а потом код адреса в соответствующие регистры. При записи второго слова, то есть кода адреса в регистр адреса, триггер «Готовность» устанавливается в состояние «Не готов». Когда код адреса, записанный в регистре адреса, совпадет с адресом канала, то в счетчик будут записаны не данные из памяти, а код из регистра данных. Этот код будет далее записан в память как данные для этого канала. При этом триггер «Готовность» устанавливается в состояние «Готов». И мы сможем записывать следующее значение данных и адреса для другого канала таймера. А в том канале, куда мы уже записали новое значение данных, счетчик вскоре начнет счет, как только счетчик адресов памяти снова досчитает до этого канала таймера.

Если же мы не хотим ждать до 240 тактов, то тут реализация блока таймеров еще проще. Хотя надо отметить, что на это потребуются дополнительные ресурсы интерконнектов микросхемы ПЛИС. Мы просто можем взять двухпортовую память 240_16. И будем записывать или обнулять данные в требуемых нам каналах (рис. 18). При этом, конечно, должны быть сделаны изменения в алгоритме работы таймера. Как мы уже обсуждали, после проведения вычислений в каждом конкретном канале всегда производится запись результатов вычислений обратно в память. Но теперь нам необходимо будет делать запись результатов вычислений обратно в память только в том случае, когда код в счетчике не равен «0», иначе мы можем получить ситуацию, когда хост записывает в ячейку памяти свою информацию, а арифметический узел пытается переписать эту информацию кодом «0». Если же мы хотим переписывать значения данных в тех ячейках каналов, которые еще не досчитали до кода 0, то нам необходимо предусмотреть соответствующие блокировки.

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

Кроме двух предыдущих вариантов выполнения многоканального таймера, есть и такой вариант, когда вместо памяти можно применить FIFO соответствующей «глубины». И использовать его для хранения данных.

Как видим, в любом из вариантов ресурсов требуется гораздо меньше, чем для решения «в лоб». И, поскольку данная задача решается чисто аппаратно, то она значительно экономит вычислительную мощность микроконтроллеров, задействованных в проекте пользователя.

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

 

Отладка

Теперь, после рассмотрения трех режимов работы автоматов, необходимо сказать дополнительно несколько слов об отладке. Влияет ли изменение архитектуры и способ работы автомата на отладку проекта в целом? Можно смело утверждать, да, влияет.

Начнем с многоядерности. Представим, что мы решили задачу «в лоб». Мы получаем чрезвычайно громоздкое и плохо отлаживаемое решение. Почему плохо отлаживаемое? Давайте еще раз вернемся к задаче о подключении ЖКИ. Чтобы проверить, работает ли автомат, нам необходима плата с установленным на ней ЖКИ, причем именно ЖКИ той модели, для которой мы выполнили все временные задержки. Теперь сравним этот вариант отладки с тем случаем, когда мы сделали иерархическое многоядерное построение автоматов. Можем ли мы заменить на этапе отладки нижний автомат на любой другой? Конечно, да! Возьмем файл, описывающий, например, UART, и подключим его к проекту. И продолжим отладку. Теперь мы уже не зависим от того, есть ли у нас именно тот ЖКИ, который заложен у нас в проекте. И, произведя отладку верхних уровней проекта, нам остается сделать уже гораздо более простой ход. Мы сделаем автомат нижнего уровня, а управлять им будем так, чтобы он непрерывно делал выдачу какого-нибудь символа. Теперь вся диаграмма будет хорошо видна даже на самом простом осциллографе.

Далее мы рассматривали подключение нескольких автоматов для увеличения вычислительной мощности. Здесь можно сказать следующее. Если есть выбор: два автомата «не задумываясь» или один, но при этом проект становится более критичным по быстродействию, то какой из вариантов вы выберете? Скорее всего— два автомата. Хотя, конечно, найдутся и оппоненты, которые будут утверждать, что один автомат— лучше. Автор придерживается той точки зрения, что если кристалл позволяет разместить два автомата, и вы не имеете других проблем при размещении проекта на кристалле, то именно этим путем и надо идти.

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

 

Заключение

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

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

Литература
  1. Curtis K. Embedded multitasking with small MCUs. Microchip.
  2. Garrett R. Designing custom embedded multicore processors. Embedded Systems Design.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *