Создание проекта в MCStudio 4
В данном документе описано создание простейшего проекта, осуществляющего мигание светодиодов на отладочном модуле NVCom-02TEM-3U. Рассмотрено создание программы на ассемблере и языке Си, запуск программы на программном симуляторе и запуск программы непосредственно на отладочном модуле. Документ ориентирован в том числе на пользователей, незнакомых с программированием, поэтому создание программы рассмотрено максимально подробно.
Разработка программы на ассемблере
Базовые понятия
Приступая к выполнению разработки ПО, следует определиться с терминологией. Словом "процессор" может обозначаться как отдельно взятое CPU-ядро (или DSP-ядро) в составе микросхемы, так и микросхема, содержащая в своем составе эти ядра («процессор 1892ВМ10Я). В данном документе под термином «процессор» подразумевается CPU-ядро процессора 1892ВМ10Я.
Процессор при условии подачи на него напряжения питания и тактовой частоты, с каждым тактом вычитывает из памяти очередное слово (инструкцию), декодирует и исполняет его. Вычитывание инструкции из памяти полностью аналогично обычной процедуре обращения к памяти. На шину адреса микросхемы выставляется требуемый адрес, на стробы чтения — активные уровни, и с шины данных считывается слово данных.
Cтробы чтения — активные уровни, и с шины данных считывается слово данных. CPU-ядро процессора 1892ВМ10Я имеет архитектуру MIPS32. В данном случае это означает, что исполнение оно начинает с виртуального адреса 0xBFC0_0000. Виртуальному адресу 0xBFC0_0000 соответствует физический адрес 0x1FC0_0000. Подробнее о виртуальных и физических адресах, а также их преобразовании рассказано в документации на микросхему 1892ВМ10Я, литературе, описывающей архитектуру MIPS32, а также в документах по применению процессоров серии «Мультикор», доступных на сайте.
В данном же случае важен тот факт, что при старте процессора после снятия сигнала начальной установки (reset, вход микросхемы nRST) процессор обращается по физическому адресу 0x1FC0_0000 и вычитывает инструкцию, расположенную по нему. Этот адрес относится к адресам внешней памяти, а значит, на выходах адресной шины микросхемы выставляется такое сочетание уровней:
31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | F | C | 0 | 0 | 0 | 0 | 0 |
В первой строке таблицы приведены номера выводов шины адреса, во второй — уровень напряжения на этом выводе (0 — это 0 вольт, 1 — это 3.3В для микросхемы 1892ВМ10Я), в третьей строке — представление каждых четырех разрядов в виде шестнадцатеричного числа. Следует заметить, что шестнадцатеричное представление, равно как и десятичное — это сугубо человеческая абстракция, для процессора никаких других понятий, кроме логического нуля и логической единицы, объединенных в различные структуры, не существует. Через некоторое время (определяется настройками порта внешней памяти, которые должны соответствовать параметрам микросхемы памяти) микросхема фиксирует уровни напряжений, выставленные микросхемой памяти на шине данных. Зафиксированное сочетание уровней — это 32-разрядное слово.
В случае вычитывания инструкции, а именно оно сейчас рассматривается, это слово — код инструкции (команды) процессора. Если система команд процессора предусматривает инструкцию с таким кодом — она будет выполнена, если не предусматривает — будет отдельная ситуация, которая называется «Исключениепо зарезервированной инструкции». Данная ситуация в этом документе не рассматривается и подробно описана в литературе по ядру MIPS32. После выполнения инструкции будет вычитана выполнена инструкция, лежащая по следующему адресу. Либо, если выполненная инструкция была инструкцией перехода — по адресу, указанному в ней. Таким образом, конечная задача программиста — сформировать такую последовательность кодов инструкций, которая обеспечивает выполнение задач, сформулированных в ТЗ.
Прикладные задачи и инструкции процессора
Совершенно очевидно, что процессор не оперирует математическими понятиями «переменная», «функция» и так далее. Не оперирует он и более высокоуровневыми абстракциями. Для него существует лишь набор данных, находящихся в памяти, и инструкции, которые позволяют считывать данные в регистры, записывать данные из регистров в память и производить действия над данными, находящимися в регистрах. Программа, выполняющая сложение двух чисел, таким образом должна выполнить следующие действия:
- записать одно число в один регистр процессора;
- записать второе число в другой регистр процессора;
- сложить значения двух регистров и положить результат в третий регистр;
Каждое из действий — это уже инструкция процессора и может быть закодировано в 32-разрядное слово, то есть, ему соответствует код инструкции. Писать программы — сразу в кодах инструкций (еще это называют термином «машинным коды») — сложно и практически неприменимо, поэтому были созданы мнемонические обозначения — так называемый язык ассемблера. Названия инструкций ассемблера происходят от англоязычных сокращений выражений, обозначающих действия, которые данные инструкции производят, либо, если сокращать нет необходимости, инструкцией может являться короткое слово. Например, LI — «Load immediate» (непосредственная загрузка), ADD — «сложить». Вот как выглядит программа сложения двух чисел на ассемблере:
LI $2, 12
LI $3, 34
ADD $4, $2, $3
Первое слово в каждой строке — непосредственно инструкция, то, что идет дальше, называется
«операндами». Первым операндом инструкции LI
является регистр процессора, в который надо загрузить
число. Вторым — само загружаемое число. В первых двух строчках сначала число 12 загружается в
регистр $2, апотом число 34 загружается в регистр $3.
У инструкции ADD
три операнда: первый — это регистр, в который на до сохранить результат, а
второй и третий — регистры, значения из которых надо складывать.
Слово «регистр» тоже требует пояснения. Процессору для хранения данных, тех же слагаемых, требуется своя память, с которой он работает быстро и напрямую. Такая память называется «набором регистров». В случае MIPS32 это 32 регистра, каждый из которых имеет размер (ширину) в 32 разряда. Обозначаются они $0...$31. Есть также мнемонические имена для них, но это в данном случае неважно. Из программы, написанной на языке ассемблера, помощью транслятора (который в обиходе так и называется - «ассемблер») из нее генерируются коды соответствующих инструкций. Для вышеприведенной программы это будет три 32-разрядных слова. Если их записать по адресу 0x1FC0_0000, процессор каждый раз при включении питания будет выполнять сложение двух указанных чисел.
Учитывая вышесказанное, изначальная задача может быть переформулирована следующим образом: необходимо создать такую последовательность инструкций на ассемблере, которая бы обеспечивала включение и выключение светодиодов с заданным периодом. В дальнейшем эта программа должна быть преобразована в машинный код, который будет загружен в память и исполнен процессором.
Управление выводами микросхемы
Имеются в виду команды, предназначенные для настройки тактирования и порта внешней памяти микросхемы перед загрузкой программы в память. В среде разработки MCStudio эти команды расположены в окне «Startup registers» и могут быть выполнены в отладчике MDB без изменений. На данном этапе может проявиться ошибка, связанная с отсутствием тактирования, которая описана в предыдущем разделе. Проявляться она будет тоже в форме длительных таймаутов при выполнении каждой из команд.
На отладочном модуле NVCom-02TEM-3U светодиоды подключены к выводам порта MFBSP. Каждый периферийный блок имеет в своем составе регистры, служащие для управления данным блоком и считывания его состояния. Следует различать регистры процессорного ядра и периферийные регистры. Периферийные регистры отображаются на адресное пространство процессора. Для доступа к ним используются инструкции, служащие для обращения процессора к памяти. С точки зрения процессорного ядра периферийный регистр — это ячейка памяти, ничем не отличающаяся от любой другой. Поскольку для решения нашей задачи мы планируем использовать порт MFBSP1 в режиме GPIO, необходимо задать этот режим в управляющем регистре порта. Управляющий регистр порта MFBSP1 называется CSR_MFBSP и имеет виртуальный адрес 0xB82F_7104 в общем адресном пространстве. Таким образом, CPU должен записать значение 0x0000_0000 по адресу 0xB82F_7104. Для этого он должен:
- занести значение 0xB82F_7004 в один регистр;
- занести значение 0 в другой регистр;
- выполнить запись значения одного регистра по адресу, сохраненному в другом регистре.
На ассемблере это будет выглядеть так:
LI $2, 0xB82F7104
SW $0, 0($2)
SW — это сокращение от «Store word», и выполняет она как раз запись, в данном случае, регистра $0 в адрес, записанный в регистре $2. В регистр $0 никакого значения не заносится, поскольку согласно спецификации MIPS32 регистр $0 всегда содержит значение 0. Ноль вне скобок во втором операнде команды SW — это смещение относительно адреса, указанного в регистре $2. Для записи нуля не по адресу 0xB82F7104, а по адресу 0xB82F7108, программа могла бы выглядеть так:
LI $2, 0xB82F0000
SW $0, 0x7004($2)
Следующая задача — определить, что и в какие регистры порта MFBSP1 необходимо записывать, чтобы управлять состоянием выводов GPIO. Согласно пункту 11.6.1 руководства пользователя на микросхему 1892ВМ10Я, блок MFBSP1 имеет 10 внешних выводов, каждый из которых в режиме GPIO может работать в режиме входа или выхода. Согласно разделу 4 руководства пользователя на отладочный модуль NVCom-02TEM-3U, светодиоды VD3, VD4 подключены соответственно к выводам LDAT4, LDAT5 порта MFBSP1. Чтобы формировать нужный уровень напряжения на выводах LDAT[5:4], необходимо сначала переключить их в режим выходов. Для этого есть регистр DIR_MFBSP, определяющий направление каждого вывода GPIO. В этом регистре выводам LDAT[5:4] соответствуют разряды [7:6]. Чтобы переключить выводы LDAT[5:4] в режим выхода, необходимо записать в эти разряды единицу. Поскольку в данном случае состояния остальных выводов нам неважны, самым простым вариантом будет записать в регистр DIR слово, в котором все биты выставлены в нули, и только биты [7:6] —в единицу. Слово это выглядит в двоичном виде так:
0000_0000_0000_0000_0000_0000_1100_0000
Или в шестнадцатеричном:
0x0000_00C0
Согласно таблице 2.37 (раздел 2.9) руководства пользователя на микросхему 1892ВМ10Я, адрес регистра DIR MFBSP1 равен 0x182F_7108. Значит, для записи в него нужного значения надо выполнить следующую последовательность инструкций:
LI $2, 0xB82F0000
LI $3, 0x00C0
SW $3, 0x7108($2)
Эта часть программы относится к инициализации, то есть, выполнена она будет один раз при старте. С учетом перевода MFBSP1 в режим GPIO, программа целиком будет выглядеть так:
LI $2, 0xB82F0000
SW $0, 0x7104($2)
LI $3, 0x00C0
SW $3, 0x7108($2)
Поскольку с регистром $2 действий не производилось, значение в нем сохраняется, следовательно, повторная запись в него значения 0xB82F_0000 не требуется. Уровень напряжения на выводах GPIO, переведенных в режим выхода, управляется регистром GPIO_DR. Выводам LDAT[5:4] порта MFBSP1 соответствуют разряды GPIO_DR[7:6] (пункт 11.6.1 руководства пользователя на микросхему 1892ВМ10Я). Чтобы выставить уровень 3.3 В на выходах LDAT[5:4], необходимо в эти биты единицы. Чтобы выставить уровень 0 В, необходимо записать в эти биты нули:
LI $3, 0
LI $4, 0xC
SW $3,0x710C($2) # выставили ноль
SW $4,0x710C($2) # выставили единицу.
Поскольку стоит задача менять значение на противоположное — более правильно будет использовать операцию «исключающее ИЛИ». В этом случае программа выглядит так:
LI $4, 0xC
LW $3,0x710C($2)
XOR $3, $3, $4
SW $3,0x710C($2)
Здесь происходит следующее:
- инструкцией LW (Load Word) в регистр $3 загружается текущее значение регистра GPIO_DR1;
- инструкцией XOR производится операцию побитового «исключающего ИЛИ» этого значения с числом 0x0000_00C0, в результате чего все биты прочитанного числа остаются неизменными, кроме битов [7:6] Результат записывается в регистр $3;
- инструкцией SW (Store Word) число с измененным значением регистра GPIO_DR записывается обратно в регистр GPIO_DR. Таким образом, значение битов [7:6] в регистре GPIO_DR меняется на противоположное, а значит — меняется и уровень напряжения на выходах LDAT[5:4].
Однако вышеприведенный код является линейным. Процессор пройдет его, переключив светодиоды (то есть, состояние выходов GPIO) один раз. Далее процессор станет исполнять инструкции, расположенные в памяти за указанной программой. Необходимо же, чтобы это происходило в бесконечном цикле. Для этого используется инструкция J (jump). С учетом цикла программа выглядит так:
LI $2, 0xB82F0000
SW $0, 0x7104($2)
LI $3, 0xC0
SW $3, 0x7108($2)
LI $4, 0xC0
# инициализация закончена
cycle:
LW $3,0x710C($2)
XOR $3, $3, $4
SW $3,0x710C($2)
J cycle
NOP
Инструкция J должна содержать в себе смещение относительно текущего адреса исполнения. Но поскольку в процессе написания программы неизвестно, по какому адресу будет располагаться та или иная инструкция, в языке ассемблера используются так называемые метки. В приведенной программе объявлена метка cycle. При трансляции в машинные коды становятся известны адреса каждой инструкции, и транслятор (ассемблер) записывает адреса или смещения для инструкций перехода и других инструкций, использующих метки.
Инструкция «NOP» - это так называемая «пустая операция», «No operation». В данном случае она нужна, так как в силу особенностей RISC-процессоров, при операции перехода выполняется инструкция, следующая за командой перехода. Это называется термином «слот задержки перехода» («delay slot» в англоязычной литературе). В CPU-ядрах процессоров серии «Мультикор» может присутствовать необходимость располагать в слоте задержки перехода только инструкции NOP. Для конкретной микросхемы это указано в руководстве пользователя (раздел, описывающий центральный процессор). Строка, начинающаяся со знака «#» - это комментарий. Транслятор, видя знак «#», просто игнорирует все, что идет после значка и до конца строки.
Текущий вариант программы практически готов. Он выполняет требуемый функционал — инициализирует порт MFBSP1 и в замкнутом цикле переключает состояния нужных выходов. Но в текущем виде переключение будет осуществляться практически непрерывно. Промежуток времени между переключениями будет равен времени, необходимому на исполнение двух инструкций после SW и двух инструкций перед ней. При таком коротком промежутке времени визуально не будет видно, что светодиоды мигают. Чтобы задать фиксированную паузу длительностью в одну секунду, необходимо использовать таймеры, входящие в состав микросхемы. В составе микросхемы 1892ВМ10Я есть три таймера, являющихся отдельными периферийными блоками, и один таймер, входящий в состав процессорного ядра. В данном примере целесообразно использовать последний.
Счетчиком этого таймера является регистр системного сопроцессора CP0. О системном сопроцессоре CP0 подробно написано в соответствующем разделе руководства пользователя на процессор, а также в литературе, описывающей архитектуру MIPS32. Счетчик таймера отображается в регистре CP0.Count и увеличивается на единицу каждый такт частоты, поступающей на CPU-ядро.
Чтение и запись регистров сопроцессора осуществляется соответственно с помощью инструкций MFC0 (move from coprocessor0) и MTC0 (move to coprocessor0).
После снятия сигнала начальной установки (nRST) CPU-ядро в составе микросхемы 1892ВМ10Я работает с тактовой частотой, равной половине частоты на входе XTI. На отладочном модуле NVCom-02TEM-3U на вход XTI подается частота 10 МГц. Соответственно, сразу после старта CPU-ядро работает на частоте 5 МГц. Длительности в одну секунду соответствует 5000000 тактов. То есть, для реализации нужной задержки программа должна зафиксировать (или обнулить) значение счетчика тактов, а потом дождаться, пока значение счетчика не увеличится на 5000000. Реализация такой задержки на ассемблере выглядит так:
LI $6, 5000000
MTC0 $0,$9
wait:
MFC0 $5,$9
SUB $7, $6, $5
BGEZ $7, wait
NOP
- инструкция «MTC0 $0, $9» заносит ноль в регистр сопроцессора CP0 под номером 9 — это и есть регистр CP0.Count;
- инструкция «MFC0 $5, $9» сохраняет текущее значение регистра CP0.Count в регистр CPU-ядра $5;
- инструкция «SUB $7, $6, $5» производит вычитание значения регист ра $5 из значения регистра $6, и результат записывает в регистр $7;
- инструкция «BGEZ» (branch if greater or equal zero) совершает переход на метку wait, если получившаяся разность больше или равна нулю, то есть, если прочитанное значение регистра CP0.Count меньше или равно значению 5000000, то есть, если 5000000 тактов еще не прошло. Если же прошло более 5000000 тактов, переход совершен не будет и программа пойдет исполняться дальше.
Проверяется именно тот факт, что отсчитано более 5000000 тактов. Это связано с тем, что между чтениями регистра CP0.Count выполняется некоторое количество инструкций, каждая из которых может занимать не менее одного такта. Таким образом, мала вероятность того, что чтение произойдет именно в момент, когда CP0.Count = 5000000. И если проверять строгое равенство — программа может никогда не зафиксировать того, что нужный временной промежуток уже пройден.
# инициализация
LI $2, 0xB82F0000 # базовый адрес периферийных регистров
SW $0, 0x7104($2) # DIR_MFBSP1 = 0
LI $3, 0xC0 #
SW $3, 0x7108($2) # DIR_MFBSP1[7:6] = 11b
LI $4, 0xC0
LI $6, 5000000 # длительность таймаута в тактах процессора
# основной цикл
cycle:
MTC0 $0,$9 # обнуляем счетчик CP0.Count
# пауза в 1 секунду
wait:
MFC0 $5,$9
SUB $7, $6, $5
BGEZ $7, wait
NOP
LW $3,0x710C($2)
XOR $3, $3, $4
SW $3,0x710C($2)
J cycle
NOP
Помимо этого, в процедуре инициализации необходимо включать тактирование используемых периферийных блоков. Для этого служит регистр CLK_EN, отдельные биты которого отвечают за тактирование различных блоков. Адрес этого регистра — 0x182F_4004. В данном случае проще включить тактирование всех периферийных блоков сразу. В реальном приложении в целях энергосбережения есть смысл включать тактирование только используемых блоков.
С учетом необходимости включать тактирование, программа преобразуется в такую:
# инициализация
LI $2, 0xB82F0000 # базовый адрес периферийных регистров
LI $3, 0xFFFFFFFF
SW $3, 0x4004($2)
SW $0, 0x7104($2) # DIR_MFBSP1 = 0
LI $3, 0xC0 #
SW $3, 0x7108($2) # DIR_MFBSP1[7:6] = 11b
LI $4, 0xC0
LI $6, 5000000 # длительность таймаута в тактах процессора
# основной цикл
cycle:
MTC0 $0,$9 # обнуляем счетчик CP0.Count
# пауза в 1 секунду
wait:
MFC0 $5,$9
SUB $7, $6, $5
BGEZ $7, wait
NOP
LW $3,0x710C($2)
XOR $3, $3, $4
SW $3,0x710C($2)
J cycle
NOP