Теория по КМПУ Готовые элементы систем Технологии и хитрости Прочее Магазин Контакты
 


Микроконтроллер LGT8F328P: китайская ATmega328P по-русски


Ядро LGT8XM и особенности выполнения программы


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

Общие сведения

Микроконтроллеры LGT8F328P построены по Гарвардской архитектуре, характеризующейся раздельной памятью программ и памятью данных. Каждая память имеет собственную шину для доступа к ней, что позволяет работать с обоими типами памяти одновременно, а также использовать при этом шины данных различной разрядности. В рассматриваемых кирпичах нет механизма DMA, обеспечивающего прямое общение периферии с ОЗУ – вся работа осуществляется через ядро. Вследствие этого, во внутреннем мире камней LGT8F328P оное занимает центральную позицию, ибо на него завязаны все остальные узлы микроконтроллера:



Основные особенности ядра LGT8XM перечислены ниже:

•  использование высокоэффективной архитектуры RISC;
•  низкое энергопотребление;
•  полностью статическая архитектура (минимальная тактовая частота равна нулю);

•  поддержка 130 инструкций;
•  119 инструкций выполняется всего за один такт, остальные – за 2 такта;
•  126 инструкций имеет размер 2 байта, остальные – 4 байта;

•  16-разрядное АЛУ, подключенное непосредственно к регистрам общего назначения;
•  встроенный аппаратный умножитель 8х8, выполняющий операцию умножения за один такт;

•  двухуровневый конвейер команд с уменьшенной частотой обращения к памяти;
•  модуль предварительного выполнения команды;

•  16-разрядная шина для доступа к памяти программ;
•  8-разрядная шина для доступа к памяти данных при работе в обычном режиме;
•  16-разрядная шина для доступа к памяти данных и к ядру при работе с ускорителем вычислений;

•  многоуровневая система прерываний;
•  поддержка очереди прерываний;

•  наличие программного стека;

•  возможность отладки программы при помощи модуля OCD (On-Chip Debugging).

Внутренняя структура ядра LGT8XM показана на этом рисунке:



Из приведенной картинки видно, что ядро камней LGT8F328P состоит из конвейера команд (Pipeline) и исполнительного модуля (Execute Unit), который имеет доступ к пачке регистров общего назначения (РОН), объединенных в регистровый файл (Register File), а также к схеме управления конвейером (Pipeline Control). С остальными внутренностями кирпича ядро общается при помощи 16-битной шины программ и 8-битной шины данных. Кроме того, в китайские чипы добавлена дополнительная шина данных разрядностью 16 бит, при помощи которой исполнительный модуль может общаться с ускорителем вычислений, а также осуществлять обмен информацией между ускорителем и ОЗУ. На рисунке выше эта часть камня обозначена темно-рыжим цветом, причем, 16-разрядная шина, судя по всему, присутствует в кирпиче физически, а не хитро эмулируется при помощи 8-битной.

В сильно упрощенном виде работа ядра может быть описана следующим образом (напомню, что основная задача данного узла – обеспечение корректного выполнения прошивки). Сначала конвейер достает из памяти программ очередную инструкцию, после чего декодирует ее и передает в исполнительный модуль. Исполнительный модуль смотрит на тип команды и в зависимости от него решает, что делать дальше:

•  если требуется выполнить логическую или арифметическую операцию, модуль обращается к своей части под названием «ALU»: мол, умножь-ка мне (сложи, вычти и т.д.) вот эти вот два регистра и положи результат в один из них. Отметим, что в зависимости от выполняемой команды, манипуляции могут производиться только над одним регистром, но суть остается та же – если требуются какие-либо вычисления, в дело вступает арифметико-логическое устройство (АЛУ);

•  если требуется выполнить инструкцию переноса данных, исполнительный модуль обращается к одной из своих частей под названием «MIF»: сходи в такую-то ячейку памяти и положи ее содержимое в такой-то регистр (или наоборот – запиши в память значение одного из регистров). Что обозначает аббревиатура «MIF», нигде не написано, но я думаю, что это что-то типа шинного драйвера с дополнительным декодером команды: если пришла «обычная» инструкция, лезем в память по 8-битной шине, а если требуется работа с ускорителем вычислений – по 16-битной. Особняком здесь стоят команды переноса данных между регистрами и команда загрузки в регистр константы – поскольку для их выполнения общение с памятью не требуется, работа ведется при помощи АЛУ;

•  если требуется выполнить инструкцию перехода, исполнительный модуль обращается к схеме «Pipeline Control», чтобы в следующий раз конвейер вытащил из памяти не ту команду, которая следует за текущей, а ту, на которую нужно совершить переход.

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

Арифметико-логическое устройство

Арифметико-логическое устройство (АЛУ) – это обязательный «математический» узел любого современного микроконтроллера, позволяющий выполнять арифметические, логические и битовые операции. Помимо этого, в камнях LGT8F328P АЛУ включает в себя аппаратный умножитель, дающий возможность работы с беззнаковым и знаковым содержимым 8-битных регистров, в том числе с поддержкой дробного формата. Обратите внимание на то, что операция умножения в китайских кирпичах длится всего один такт, в отличие от AVR-ок, где на это требуется два машинных цикла.

Для своей работы арифметико-логическое устройство ядра LGT8XM использует регистры общего назначения – в них хранятся значения операндов и результат выполненной операции. Необходимые математические действия могут выполняться между двумя регистрами, между регистром и константой, а также с одним регистром (например, если нужно поменять местами его полубайты). Для улучшения производительности и достижения максимальной гибкости работы АЛУ с регистровым файлом, поддерживаются следующие схемы ввода/вывода данных:

•  один 8-битный операнд и один 8-битный результат операции (схема характерна для команд с одним регистром, например «побитовый сдвиг» или «смена тетрад в байте»);

•  два 8-битных операнда и один 8-битный результат (схема характерна для команд с двумя регистрами, например «сложение», «вычитание» или «логическое И»);

•  два 8-битных операнда и один 16-битный результат (честно говоря, здесь кроме умножения в голову ничего не приходит);

•  один 16-битный операнд и один 16-битный результат (схема характерна для операций с регистровыми парами, например, прибавление к паре константы или перенос данных из одной пары в другую).

Арифметико-логическое устройство подключено к регистровому файлу напрямую, благодаря чему обеспечивается максимальное быстродействие камня. Отметим, что в чипах LGT8F328P длительность любой операции АЛУ составляет всего один такт, в отличие от микроконтроллеров AVR, где для выполнения умножения и изменения значения регистровой пары требуется два машинных цикла. При этом арифметико-логическому устройству доступны все регистры общего назначения, что существенно сокращает время на различные промежуточные действия типа перекладывания значений из «общих» регистров в рабочие и обратно. Результат операции АЛУ автоматически заносится в регистровый файл; косвенно о нем также можно судить по состоянию специального регистра SREG.

Регистр состояния (SREG)

Регистр состояния SREG представляет собой набор флагов, показывающих текущее состояние камня. Все разряды регистра состояния доступны как для чтения, так и для записи в любой момент времени; после сброса микроконтроллера содержимое этого регистра равно нулю:



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

Помимо «математических» флагов, регистр состояния содержит пользовательский бит («T») и бит разрешения прерываний («I»). Основное применение первого – копирование какого-либо разряда из одного регистра общего назначения в другой, либо временное хранение одного из битов любого РОН (см. инструкции BLD и BST). Однако, никто не мешает использовать бит «T» по своему усмотрению, например, в качестве глобального флага программы. Старший же разряд регистра SREG отвечает за общее разрешение прерываний: если он установлен, прерывания разрешены, а если сброшен – запрещены. Установка/сброс флага «I» производится при помощи инструкций SEI и CLI соответственно, либо можно использовать универсальные команды BSET/BCLR. Кроме того, флаг «I» сбрасывается аппаратно при переходе на обработчик прерывания и автоматически восстанавливается командой RETI при выходе из этого обработчика.

Регистры общего назначения

Регистры общего назначения (РОН) – это основные рабочие лошади любого кирпича. Как говорилось выше, именно они используются арифметико-логическим устройством для хранения операндов и результатов, и только с их помощью мы можем общаться с памятью камня. Ядро LGT8XM включает в себя аж тридцать два (!) 8-битных регистра общего назначения R0…R31, которые объединены в единый регистровый файл. По факту данный файл является отдельной и самой быстрой памятью микроконтроллера LGT8F328P, оптимизированной под набор инструкций именно китайского ядра. При этом каждый регистр общего назначения также имеет свой собственный адрес в общем пространстве памяти данных, поэтому к РОН можно обращаться как к обычным ячейкам ОЗУ (несмотря на то, что физически эти регистры не являются частью SRAM).

Большинство инструкций, работающих с регистровым файлом, в китайском камне имеют прямой доступ ко всем регистрам. Исключение составляют лишь несколько команд, выполняющих действия между регистром и константой, знаковое и дробное умножение, а также команда прямой загрузки константы в регистр (эти инструкции могут обращаться только к «нижней» половине РОН, т.е. к R16…R31). Кроме того, регистры R26...R31 могут объединяться в 16-разрядные указатели «X», «Y» и «Z», которые фактически являются регистровыми парами XH:XL, YH:YL, ZH:ZL и используются для косвенной адресации памяти камня1:


1 – отметим, что указатели «X», «Y» и «Z», наряду с комбинацией R25:R24, могут выступать операндом в командах изменения содержимого регистровой пары (ADIW и SBIW), а также при 16-битном доступе ядра к ускорителю вычислений.

Более подробно этот вопрос будет рассмотрен в отдельной заметке, здесь же просто отметим, что при помощи данных указателей может быть адресована вообще любая ячейка памяти кирпича LGT8F328P – хоть SRAM, хоть FLASH, хоть EEPROM. При этом в китайских МК доступ к памяти программ возможен не только посредством инструкции LPM, но и при помощи обычных LD/LDD/LDS, поэтому здесь для чтения данных из флэша можно использовать все три указателя, а не только «Z» (как в AVR-ках).

Инструкции (команды)

Программа для любого микроконтроллера представляет собой последовательность команд (инструкций), записанных в память МК. Большинство команд при выполнении изменяют содержимое регистров или ячеек ОЗУ, либо переносят нас в определенное место программы (в соответствии с каким-либо условием, или же просто на конкретный адрес). Кроме того, обычно ядро поддерживает также несколько специальных инструкций, выполняющих особые действия. В случае микроконтроллера LGT8F328P все команды можно разбить на пять групп:

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

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

•  команды пересылки данных. Производят обмен данными либо между регистрами общего назначения, либо между РОН и памятью данных (периферией или ОЗУ);

•  команды передачи управления. Позволяют выполнять условные и безусловные переходы, вызов подпрограмм и возврат из них, а также возвращаться из обработчиков прерываний. Кроме того, сюда же относятся инструкции сравнения, формирующие флаги регистра SREG и обычно предназначенные для работы совместно с командами условного перехода;

•  команды управления системой. К ним относятся инструкции, выполняющие хитрые действия, которые нельзя отнести ни к одной из групп, перечисленных выше. В камнях LGT8F328P таких команд всего четыре: NOP (пустая операция), SLEEP (впадание в спячку), WDR (сброс сторожевого таймера) и BREAK (установка точки останова программы).

Полный перечень команд китайского камня, включая команды работы с 16-битной шиной данных, приведен в отдельной заметке. Отмечу, что в некотором роде этот перечень уникален, поскольку в нем исправлены все косяки, присутствующие в оригинальной документации (по крайней мере, подавляющее их большинство). Вообще говоря, в китайском даташыте в сводной таблице команд творится просто лютый пиздец – то флаги не те укажут (или не укажут нужные), то для инструкций LD в одном месте напишут «длительность 1 или 2 такта», а в другом – «1 такт», то вообще забудут привести команду (см. SEH и CLH) или, наоборот, приведут ее несколько раз (см. LD, LDD, LDS, NOP, SLEEP). В итоге пришлось проверять длительность выполнения всех инструкций вручную, в том числе, и тех, которые работают по 16-битной шине (задача оказалась, мягко говоря, нетривиальной). Результатом этих трудов и стала моя таблица команд, ссылка на которую дана выше (это мой дар Человечеству). Правда, я пока не проверял, правильно ли выполняются в китайском камне вообще все инструкции, но к тем командам, которые мне на данный момент приходилось использовать, вопросов нет – всё работает как надо.

Ядро LGT8XM поддерживает в общей сложности 130 разных инструкций (если не считать «16-битные»), однако, лишь 89 из них являются «уникальными». Остальные команды – это просто производные от «уникальных», введенные исключительно для удобства пользователя и лучшей читабельности программы. К этим «производным» инструкциям относятся:

•  все кастомизированные команды условного перехода типа BRNE, BREQ, BRLO и т.д. (всего их 18 штук). Любая подобная инструкция может быть получена из универсальной команды BRBC или BRBS;

•  все кастомизированные команды установки/сброса флага в регистре SREG (типа SEI, CLI, CLC и т.д.; всего таких 16 штук). Любая подобная инструкция может быть получена из универсальной команды BSET или BCLR;

•  команды установки/сброса определенного бита (или всех битов) в регистре общего назначения (SBR, CBR, CLR, SER). Первые две могут быть заменены обычными логическими операциями «ИЛИ»/«И», следующая – исключающим «ИЛИ», а SER – это вообще тупая запись в РОН числа 0xFF;

•  команды логического и побитового сдвига влево (LSL и ROL). Данные действия могут быть выполнены при помощи сложения исходного операнда с самим собой. Поэтому компилятор вместо приведенных выше команд использует инструкции ADD и ADC;

•  команда проверки регистра на нулевое или отрицательное значение (TST). Поскольку в ходе выполнения данной команды производится операция обычного логического «И» требуемого РОН с самим собой, инструкция TST может быть заменена на AND.

При компиляции программы вместо перечисленных выше «производных» инструкций будут подставлены их «уникальные» аналоги с необходимыми операндами. Например, команда «BRNE k» компилятором будет интерпретирована как «BRBC 1, k», а «SEI» – как «BSET 7». А вот фразы «LPM» и «LPM R0, Z» для ядра LGT8XM – это две разные команды, хотя обе они загружают байт из памяти программ в регистр R0 (адрес необходимой ячейки лежит в указателе «Z»). Так сложилось исторически, ибо камни AVR из семейства Classic не имели возможности загрузки данных из флэша в любой произвольный РОН (только в R0), и чтобы сохранить совместимость с ними, в более свежих моделях кирпичей решили оставить оба варианта.

Выше уже отмечалось, что микроконтроллеры LGT8F328P построены по RISC-архитектуре, основным преимуществом которой является увеличение быстродействия ядра за счет сокращения количества поддерживаемых команд и уменьшения размера каждой из них. Иными словами, RISC-кирпичи понимают меньше инструкций, чем их собраться из CISC, зато эти инструкции занимают меньше места в памяти и выполняются быстрее1. В соответствии с этим принципом, в китайских камнях практически все команды умещаются всего в одной ячейке памяти, причем за счет того, что ее размер сделан равным 16 бит, набор инструкций получился довольно обширным2. Исключение составляют четыре команды, использующие прямую адресацию – LDS, STS, CALL и JMP. Эти инструкции содержат в себе 16- или 22-разрядный адрес требуемого байта (или сло́ва), поэтому они занимают в памяти две ячейки. Таким образом, размер команды в камне LGT8F328P может быть равен 2 байта или 4 байта, причем, доля первых команд составляет почти 97% от общего объема.

1 – обратная сторона архитектуры RISC заключается в том, что для сложных задач «универсальных» простых команд может понадобиться очень много, поэтому в сумме они займут до хера места в памяти и будут выполняться слишком долго;

2 – к примеру, у камней PIC16F из семейства «Enchanced Mid-Range» размер одной ячейки памяти программ равен 14 бит; при этом количество доступных команд составляет всего 50.

Время выполнения любой команды кирпича LGT8F328P составляет либо один, либо два машинных такта, при этом количество однотактных инструкций равно 119 (91,5%). Отмечу, что по сравнению с AVR-ками это достаточно много – у ATmega328P однотактных команд всего 84, т.е. 64,1% от общего количества. Кроме того, в 328-й Меге время выполнения команды может достигать трех и даже четырех машинных циклов, в китайском же камне, повторюсь, ни одна инструкция не выполняется дольше двух тактов Формально, конечно, к этому утверждению можно придраться – см. инструкции CPSE, SBRC/SBRS и SBIC/SBIS. Однако, на мой взгляд, здесь к самим командам вопросов нет, ибо в «чистом» виде (т.е. когда условие в них ложно) они длятся всего один такт. Увеличение длительности до трех циклов происходит лишь в том случае, когда размер следующей инструкции составляет четыре байта вместо двух, т.е. это обусловлено структурой программы, а не «плохими» командами. Кстати, в оригинальной документации на китайские кирпичи для команд CPSE, SBRC/SBRS и SBIC/SBIS указана длительность не более двух машинных тактов. Однако, я лично проверял – если после рассматриваемых инструкций идет команда размером 4 байта, то время их выполнения увеличивается до трех циклов.

Отметим, что нигде не говорится, за счет чего более 90% инструкций китайского камня являются однотактными. Исключение составляют только операции умножения – здесь четко оговаривается, что ядро LGT8XM содержит однотактный умножитель, а не двухтактный (как в случае с AVR-ками). Про остальные инструкции можно только строить предположения, но лично я думаю, что такой хороший результат связан с улучшенным конвейером команд. А посему пришла пора более подробно рассмотреть этот узел китайского кирпича.

Конвейер

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



Конвейерный подход позволяет сократить время выполнения большинства команд до одного такта, что в пределе делает производительность камня равной 1MIPS/МГц. Такого результата удается добиться благодаря тому, что не нужно отдельно реализовывать цикл «выборка + исполнение» для каждой команды. При этом китайское ядро имеет преимущество перед AVR-овским, ибо оно позволяет вытаскивать из памяти сразу две инструкции за один такт. Откуда берется эта возможность, в китайской документации не объясняется, но, думаю, она связана с тем, что память EFLASH, встроенная в микроконтроллеры LGT8FX8P, физически имеет 32-разрядную шину данных, а размер подавляющего большинства команд составляет всего 16 бит. Таким образом, за одну выборку мы чаще всего получим сразу две инструкции, поэтому в следующем машинном такте память можно не дергать. Отметим, что «удвоенная» выборка команд в пределе позволяет снизить частоту обращения ядра к памяти программ аж в два раза, что значительно уменьшает энергопотребление всей системы.

Обратите внимание на то, что в состав китайского конвейера входит блок предварительной обработки команды («Instruction Pre-execute»). И, на мой взгляд, именно ему мы должны быть благодарным за то, что многие инструкции камня LGT8F328P выполняются быстрее AVR-овских. Доказательств этого у меня, конечно, нет, но чтобы не быть голословным, хотя бы поясню свою мысль. Как говорилось выше, с точки зрения исполнительного модуля ядра LGT8XM все команды могут быть разделены на три типа – команды, выполняемые при помощи арифметико-логического устройства, команды работы с памятью и команды переходов (куда относится также вызов подпрограммы и возврат из нее или из обработчика прерывания). И если посмотреть на перечень команд AVR (именно AVR), выполняемых за два и более машинных цикла, то все они не будут связаны с АЛУ (за исключением умножения, но там два такта дает укуренный AVR-овский умножитель). И это вполне логично – на рисунке ниже показан типичный процесс выполнения одной инструкции арифметико-логическим устройством:



Из данного рисунка можно видеть, что АЛУ в AVR-ках и китайских камнях выполняет одну команду за один такт, благодаря подключению непосредственно к регистровому файлу. Тут, правда, есть один мутный момент – не совсем понятно, с чем синхронизован процесс чтения содержимого регистров-операндов, выполнения операции и записи ее результата в регистр-приемник, и синхронизован ли он с чем-либо вообще. Ответа на этот вопрос нет ни в китайской документации, ни в AVR-овских даташытах, поэтому просто поверим производителю на слово, что ядро успевает всё провернуть за один машинный цикл (тем более что практика этот факт действительно подтверждает). Совсем другая картина будет при работе с памятью – здесь камню сначала необходимо сформировать адрес ячейки, с которой он будет работать, и только потом он сможет выполнить требуемое действие (чтение или запись):



Очевидно, что при таких раскладах команда не может выполняться меньше двух машинных тактов – в первом такте ядро формирует адрес, во втором производит доступ к ячейке. Однако, факт остается фактом – время выполнения инструкций LD/LDD и ST/STD в китайских кирпичах составляет всего один машинный цикл. И на мой взгляд, это возможно только в том случае, когда на борту ядра есть какой-нибудь вспомогательный узел, который может заранее сформировать требуемый адрес, а при необходимости еще и увеличить/уменьшить его. То же самое касается команд относительного безусловного перехода (RJMP) и вызова подпрограммы (RCALL) – поскольку они не содержат никаких условий, то при наличии помощника конвейер может не тратить лишний цикл на выборку команды при переходе на требуемый адрес (см. ниже), а сделать это заранее. Да и вообще – если составить перечень «долгих» команд в AVR, то будет заметно, что все они связаны с какими-то дополнительными действиями, которые должно выполнить ядро для корректного завершения операции (по большей части – формирование адреса ячейки памяти и сохранение/извлечение данных из стека). И если предположить, что все эти дополнительные действия берет на себя помощник, становится ясно, почему в китайских камнях длительность команды не превышает двух тактов, хотя те же CALL, RET и RETI в AVR-ках выполняются целых четыре цикла.

Конечно, всё это мои предположения, и вполне может оказаться, что блок предварительной обработки команды здесь совершенно ни при чем. Но на ровном месте чудес тоже не бывает, и если китайские камни настолько лучше AVR-ок в плане производительности, на то должна быть веская причина. Может, вышеупомянутый помощник встроен не в конвейер, а в исполнительный модуль, может, китайцы применили еще какое-то мощное колдунство, но внутренности ядра LGT8F328P должны сильно отличаться от ATmega328P. Однако, лично мне всё же кажется, что конвейер здесь играет не последнюю роль, поскольку в китайском камне ускорена работа команды относительного безусловного перехода RJMP. А в следующем пункте будет показано, что этого можно достичь только устранив дополнительную выборку инструкции из памяти, ибо при выполнении RJMP даже работа со стеком не ведется и единственное «лишнее» действие – это именно дополнительная выборка.

Счетчик команд (PC) и задержки в конвейере

При работе конвейера адрес инструкции, которая будет извлечена из памяти следующей, берется из счетчика команд (Program Counter, PC). Размер данного счетчика для китайских камней не приводится, однако, по аналогии с AVR-ками можно предположить, что его разрядность составляет 12 бит для LGT8F88P, 13 бит для LGT8F168P и 14 бит для LGT8F328P. Обратите внимание на то, что счетчик команд адресует именно команды, а поскольку размер большинства инструкций составляет 16 бит, он считает не отдельные байты, а слова́. Именно поэтому для адресации 32КБ памяти нам будет достаточно 14-битного значения, а не 15-битного, как это было бы при побайтовой адресации.

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

После включения питания, а также после сброса микроконтроллера в счетчик команд автоматически загружается значение 0x0000. При линейном ходе программы, когда инструкции выполняются последовательно одна за другой, содержимое счетчика PC каждый раз автоматически увеличивается на 1 или на 2, в зависимости от исполняемой команды1. При возникновении прерывания, а также при выполнении инструкций перехода, вызова подпрограмм и возврата из них, в счетчик PC записывается адрес, на который конвейер должен перепрыгнуть в следующем такте. Отметим, что при этом происходит нарушение линейного функционирования конвейера, влекущее за собой увеличение времени выполнения команды (т.е. задержку). Если, например, в данный момент ядро исполняет инструкцию условного перехода BREQ k, то в случае истинности проверяемого условия («Z» = 1) выполнение программы должно быть продолжено с некоторого адреса, указанного в команде. Но в конвейере ранее уже́ была произведена выборка команды, расположенной за командой BREQ, поэтому время выполнения последней увеличится на один машинный цикл, в течение которого будет сделана выборка инструкции, расположенной по адресу PC+k+1. Таким образом, длительность команд условного перехода (BRNE, BRCS, BRBC и т.д.) составляет 1 такт, если условие, указанное в них ложно, и 2 такта, если условие истинно.

1 – значение счетчика команд увеличивается на 2 при выполнении инструкции, занимающей две ячейки памяти программ (4 байта). К таким инструкциям относятся команды, использующие прямую адресацию: LDS, STS, CALL и JMP.

В случае команд типа «Test & Skip» (CPSE, SBRC/SBRS и SBIC/SBIS) следующая инструкция не выполняется, если проверяемое условие истинно (регистры равны, либо требуемый бит регистра сброшен/установлен). Однако выборка пропускаемой команды уже произошла, и вследствие того, что она не выполняется, в конвейере образуется «дырка», которая заключается в пропуске одного или двух (в зависимости от размера пропускаемой инструкции1) машинных циклов. Соответственно команды типа «Test & Skip» выполняются за 1 такт, если условие, указанное в команде ложно, и за 2 или 3 такта, если указанное условие истинно.

1 – если размер команды, следующей за инструкцией типа «Test & Skip», составляет 2 байта, производится пропуск одного машинного такта. Если размер следующей команды равен 4 байта, будет пропущено два машинных такта.

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



Из приведенной таблицы видно, что в ATmega328P время выполнения команд из рассматриваемой группы составляет от двух до четырех тактов. При этом прослеживается четкая связь – инструкции, работающие со стеком (RCALL, ICALL, CALL), выполняются ровно на один такт дольше своих «аналогов», которые стек не используют (RJMP, IJMP, JMP). Отсюда можно сделать вывод о том, что в ядре AVR работа со стеком отнимает один машинный цикл, а всё остальное «дополнительное» время – это задержка в работе конвейера из-за необходимости формирования адреса перехода и извлечения инструкции по этому адресу.

А вот в китайских камнях всё иначе – там время выполнения команд из рассматриваемой группы не превышает двух тактов. Думаю, это связано с усложнением структуры ядра LGT8XM и введением в него различных вспомогательных узлов, хотя в официальной документации про это ничего не говорится. Однако, объяснить чем-то другим настолько отличное время выполнения инструкций в китайце лично я не могу. Например, в LGT8F328P длительность относительных переходов (RJMP, IJMP, JMP) равна длительности соответствующих вызовов подпрограмм (RCALL, ICALL, CALL), т.е. исполнительный модуль ядра LGT8XM не тратит дополнительный такт на сохранение счетчика команд в стеке (как это было в AVR-ках). А это значит, что в китайце данная задача ложится на плечи какого-то вспомогательного узла (концепция «помощника» была мной подробно изложена в предыдущем пункте). Но самое главное – разработчики LGT8F328P сумели добиться исполнения команд с «короткими» переходами (RJMP/RCALL) всего за один машинный цикл, что для ATmega328P невозможно в принципе. В AVR любое «скачкообразное» изменение счетчика команд влечет за собой задержку в конвейере как минимум на один такт, в течение которого будет осуществляться выборка инструкции по новому адресу, на который должна перейти программа. А если такой задержки нет (как в китайце), то это может значить лишь одно – необходимая команда была вытащена из памяти либо до, либо в момент выполнения инструкции перехода. Очевидно, что сделать это может только конвейер, и наиболее подходящий узел для данной задачи, на мой взгляд, это блок предварительной обработки команды (но тут я могу и ошибаться – в официальной документации, повторюсь, про этот момент нет ни слова). Тот же факт, что выполнение команд с более «длинными» переходами занимает не один, а два такта, можно объяснить тем, что в них задается любой произвольный адрес в памяти, а не «небольшая» прибавка ±2К к текущему значению PC, и для формирования этого адреса всё-таки требуется дополнительный машинный цикл. Отметим также, что подход китайцев, применяемый в инструкциях RJMP/RCALL, к сожалению, нельзя использовать в командах условного перехода и в командах типа «Test & Skip». В данных командах адрес следующей инструкции становится известен только после проверки указанного условия (т.е. только после выполнения само́й команды), поэтому конвейер не может знать заранее, с каким адресом в памяти программ ему придется работать далее.

Стек и указатель стека (SP)

Одной из важных составляющих микроконтроллеров LGT8F328P является стек – специальный кусок ОЗУ, используемый системой для вспомогательных задач. В подавляющем большинстве случаев этот кусок задействуется в качестве временного хранилища содержимого регистров, а также адресов возврата из подпрограмм и обработчиков прерываний. Если пользователю нужно на время сохранить текущее значение какого-либо регистра, для этого можно использовать стек. Если пользователь вызывает подпрограмму, в стеке автоматически сохранится адрес, на который нам потом нужно будет вернуться. Аналогичным образом ядро поступает и при возникновении разрешенного прерывания – содержимое счетчика команд PC кладется в стек, после чего программа переходит на соответствующий вектор в таблице прерываний.

Ячейка ОЗУ, в которую будет произведена запись или чтение данных, называется вершиной (головой) стека:



Адрес этой ячейки лежит в указателе стека SP (Stack Pointer), в качестве которого выступает регистровая пара SPH:SPL, расположенная в пространстве ввода-вывода:



Отметим, что, в отличие от счетчика программ, с данной парой можно работать как с обычными регистрами ввода-вывода, т.е. как считывать ее содержимое, так и изменять его, причем, для этого допускается использовать команды IN и OUT.

В китайских камнях стек реализован по принципу «от старшего адреса к младшему». Это означает, что при сохранении данных в стеке его вершина в ОЗУ сдвигается вверх, а содержимое указателя SP уменьшается. Соответственно, при извлечении данных из стека его вершина становится ближе к основанию, а содержимое пары SPH:SPL увеличивается. Поэтому логичнее всего располагать стек в самом конце ОЗУ – при таком подходе вероятность того, что он пересечется с данными пользователя в памяти МК, минимальна:



Конечно, пользователь волен располагать стек в любом месте оперативной памяти кирпича, однако, обычно под его основание всё же отводят последнюю ячейку ОЗУ. Именно исходя из этой логики, после включения питания, а также после сброса микроконтроллера в регистровую пару SPH:SPL автоматически загружается значение RAMEND (в случае камня LGT8F328P оно равно 0x08FF, для младших моделей RAMEND = 0x04FF). Это дает возможность пользователю не инициализировать указатель стека специально, поскольку всё само собой получится так, как нужно. Если же вы хотите перенести стек в другое место памяти, необходимо будет самостоятельно записать требуемый адрес в регистровую пару SPH:SPL.

Отметим, что если программа требует большого количества переменных для своей работы, а также интенсивно использует вложенные вызовы подпрограмм, существует вероятность того, что стек рано или поздно зацепит область данных. Как говорилось выше, расположение стека в конце ОЗУ минимизирует эту вероятность, но не сводит ее к нулю. К сожалению, это неустранимый недостаток программной реализации стека – здесь приходится либо вводить ограничение на диапазон возможных значений указателя SP, либо тщательно следить за расходованием оперативной памяти. Китайские камни не накладывают ограничений на содержимое пары SPH:SPL, и это даже подается как фича: мол, смотрите какая красота – пользователь может занять под стек вообще всё внутреннее ОЗУ кирпича. Поэтому приходится самостоятельно следить за тем, чтобы переменные в динамической памяти занимали поменьше места (например, среда «Arduino» выдает строгое предупреждение, если под стек остается меньше 25% внутреннего ОЗУ). Понятно, что два килобайта оперативки – это достаточно много, но всё равно лучше лишний раз всё проверить.

Следует иметь ввиду, что указатель стека может изменяться не только при прямой перезаписи содержимого регистровой пары SPH:SPL. В наборе команд ядра LGT8XM имеется также 7 инструкций, после выполнения которых SP изменится автоматически:



Кроме того, к изменению указателя стека приведет возникновение разрешенного прерывания, поскольку в этом случае в стеке будет сохранен адрес возврата. Обратите внимание на то, что после команд работы с байтами (PUSH/POP) содержимое указателя SP изменяется на 1, т.к. ОЗУ микроконтроллера имеет побайтовую организацию. По этой же причине выполнение «адресных» инструкций (RCALL/ICALL/CALL/RET/RETI) ведет к изменению регистровой пары SPH:SPL на 2 – размер адреса команды в МК LGT8F328P составляет 16 бит.

Прерывания

При работе с периферией и внешними сигналами довольно часто используется такая вещь, как прерывания. Под прерыванием понимают прекращение нормального хода программы для выполнения какой-либо задачи, определяемой внутренним или внешним событием микроконтроллера. Таким событием может быть переполнение одного из таймеров, приход синхроимпульса на определенный порт МК, передача данных по SPI и т.д. Например, пользователь может настроить кирпич таким образом, что с каждым приходом байта по шине UART камень будет отвлекаться от своих текущих задач на обработку этого байта (под текущими здесь понимаются задачи, определяемые нормальным ходом программы).

Микроконтроллеры LGT8F328P поддерживают 28 источников прерываний (не считая прерывания по сбросу МК). Контроллер прерываний, входящий в состав ядра LGT8XM, позволяет гибко управлять ими – пользователь может разрешить или запретить работу каждого прерывания по отдельности, а также осуществить одновременное разрешение или запрет всех разрешенных прерываний. Запрет и разрешение конкретного отдельного прерывания осуществляется при помощи соответствующих регистров ввода-вывода (EIMSK, TIMSK1, SPCR и т.д.). Для общего включения и выключения всех разрешенных прерываний используется флаг «I» регистра состояния МК SREG. При сбросе данного флага все прерывания будут запрещены немедленно, и запрос ни на одно из них не будет выполнен, даже если прерывание возникло одновременно со сбросом флага «I».

Ядро LGT8XM поддерживает также такую полезную функцию, как очередь прерываний. Суть данной функции проста – если во время общего запрета («I» = 0) возникает один или несколько запросов на прерывание, камень запоминает это и после того, как флаг «I» будет установлен, начинает их обработку. Ожидающие запросы будут обрабатываться по очереди в порядке уменьшения приоритета – вначале кирпич займется более «важными». Приоритет прерываний задан производителем и не может быть изменен пользователем.

Совокупность действий, которые микроконтроллер должен выполнить при возникновении запроса на прерывание, называют обработчиком данного прерывания (ОП). Фактически, ОП – это обычная подпрограмма, только вызывается она не пользователем, а ядром (автоматически) при возникновении соответствующего запроса. При вызове обработчика камень аппаратно сбрасывает флаг «I» в регистре состояния SREG, запрещая ядру реагировать на все прерывания, возникающие в процессе выполнения ОП. Поэтому выход из обработчика лучше осуществлять при помощи команды RETI, а не RET, т.к. она не только возвращает нас в нужную точку программы, но и автоматически устанавливает флаг «I». Отметим, что при необходимости пользователь может разрешить вложенные прерывания, самостоятельно установив флаг «I» внутри ОП – в этом случае обработчик будет реагировать на все разрешенные прерывания.

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

При возникновении запроса на прерывание (ра́вно как и при вызове подпрограммы) ядро сначала сохраняет в стеке текущее значение счетчика команд PC, чтобы в дальнейшем можно было вернуться в нужную точку программы. После этого происходит аппаратный сброс флага «I», а далее – переход на требуемый ОП. Данный процесс занимает 4 машинных цикла (в AVR-ках было 7 циклов), но если прерывание произойдет во время выполнения двух- или трехтактной команды, ядро обязательно дождется ее завершения. Кроме того, при возникновении запроса в момент нахождения камня в режиме спячки, время перехода в ОП увеличится еще на 4 такта – эта задержка добавится ко времени выхода из выбранного спящего режима. Возврат из обработчика прерывания занимает 2 такта, в течение которых сначала извлекается из стека счетчик команд PC, после чего устанавливается флаг «I» в регистре SREG и происходит переход в нужную точку основной программы. При этом даже если в очереди прерываний есть невыполненные запросы, ядро выполнит хотя бы одну инструкцию перед тем, как перейти на обработчик ожидающего прерывания.


Обсудить эту заметку можно в Телеге или в ВК




◄▪▪▪ Предыдущая заметка К перечню заметок Следующая заметка ▪▪▪►

Место для разного (сдается)

 




Создание, "дизайн", содержание "сайта": podkassetnik
Для писем и газет: Почта России электрическая

Место для © (копирайта, понятно, нет, но ссылайтесь хотя бы на первоисточник)

Since 2013 и до наших дней