Перейти к основному содержимому

Создание проекта в 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 и вычитывает инструкцию, расположенную по нему. Этот адрес относится к адресам внешней памяти, а значит, на выходах адресной шины микросхемы выставляется такое сочетание уровней:

313029282726252423222120191817161514131211109876543210
00011111110000000000000000000000
1FC00000

В первой строке таблицы приведены номера выводов шины адреса, во второй — уровень напряжения на этом выводе (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

Разработка программы на языке Си

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

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

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

void main() {}

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

  • доступ к периферийным регистрам;
  • доступ к регистрам сопроцессора CP0.

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

  • доступ к периферийным регистрам;
  • доступ к регистрам сопроцессора CP0. Как было уже отмечено в предыдущем разделе, с точки зрения процессора, доступ к периферийному регистру ничем не отличается от доступа к ячейке памяти. Для операций с памятью в языке Си предусмотрено понятие «указатель». Например, объявление вида:
unsigned int *ptr;

Это значит, что объявлен символ ptr, который представляет из себя значение адреса памяти. Для MIPS32 это 32-разрядный адрес. То есть, ptr — это 32-разрядное число. И если произвести чтение по адресу, равному этому числу — будет прочитано значение, которое лежит по этому адресу. Ключевым словом unsigned int в языке Си обозначается целочисленная беззнаковая переменная. В зависимости от архитектуры, переменная типа int может иметь различную разрядность. Для MIPS32 переменная типа int — 32-разрядная. Слово unsigned говорит компилятору, что со значением надо оперировать как с беззнаковым, то есть, могущим принимать значения от нуля до (232-1). Звездочка «*» говорит о том, что ptr — это не переменная типа unsigned int, а указатель на такую переменную.

С указателем в языке Си можно производить следующие операции:

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

Чтобы изменить значение указателя, необходимо написать такую строку:

ptr = (unsigned int *) 0xBFC00000;

Чтобы записать значение по адресу, на который указывает указатель, необходимо написать такую конструкцию:

*ptr = 0x12345678;

После выполнения этих двух строк по адресу 0xBFC00000 будет записано значение 0x12345678. Если написать

ptr = 0xBFC00000;

то компилятор не пропустит такую конструкцию, так как язык Си требует четкого указания типов данных. Если ptr — это тип unsigned int *, то 0xBFC00000 — это тип unsigned int. Поэтому при изменении адреса используется так называемое приведение типов, как это показано в первом примере, демонстрирующем изменение адреса.

void main() {
*((unsigned int *) 0xB82F4004) = 0xFFFFFFFF;
}

Это более оптимально с точки зрения компилятора, но менее читаемо. Более правильно сделать так:

#define CLK_EN (*((unsigned int *) 0xB82F4004))

void main() {
CLK_EN = 0xFFFFFFFF;
}

В данном случае была использована так называемая директива препроцессора #define. Перед тем, как исходный код программы будет обработан компилятором, он обрабатывается препроцессором. В данном случае препроцессор обрабатывает директиву #define и запоминает, что текст CLK_EN должен быть заменен на (*(unsigned int*) 0xB82F4004). То есть, после прохода препроцессора на вход компилятору поступает точно такой же исходный код, как в предыдущем случае.

Бывают случаи, когда объявленные с помощью директивы #define значения ошибочно именуют константами. С точки зрения языка Си это неправильно, так как константа — это переменная, для которой выделяется место в памяти. Но синтаксис языка Си запрещает изменять значение данной переменной. Для удобства программистов каждый производитель микропроцессоров для встраиваемых применений формирует так называемый заголовочный файл (для каждой микросхемы свой), где с помощью подобных директив #define определены адреса и поля всех периферийных регистров микросхемы. Для процессоров серии «Мультикор» такие заголовочные файлы тоже существуют. Они входят в состав среды разработки MCStudio 4. Упомянутые заголовочные файлы включаются в тело программы с помощью директивы препроцессора #include.Препроцессор, встречая такую директиву в теле файла, заменяет строку с этой директивой на содержимое всего заголовочного файла.

Запись в регистр CLK_EN с использованием в программе заголовочного файла будет выглядеть так:

#include "memory_nvcom_02t.h"
void main() {
CLK_EN = 0xFFFFFFFF;
}

Заголовочный файл должен лежать либо в одной директории с проектом, либо в директории, указанной как директория с заголовочными файлами. Чтение из периферийных регистров осуществляется почти также. Но считанное значение необходимо где-то охранить для дальнейшей обработки. В примере на ассемблере считанное значение сохраняется в указанном регистре. В программе на языке Си для этого необходимо объявить переменную. Переменная типа unsigned int (беззнаковое целое, для MIPS32 — шириной 32 разряда) объявляется так:

unsigned int tmp; // tmp – имя переменной

Тогда изменение состояния на выходах GPIO будет выглядеть так:

#include "multicore/nvcom02t.h"

void main() {
unsigned int tmp;
tmp = GPIO_DR;
tmp ^= 0xC0;
GPIO_DR = tmp;
}

Значок «^» — операция побитового исключающего ИЛИ (XOR). Оператор «^=» равнозначен такой операции: «tmp = tmp^0xC0».

Доступ к регистрам сопроцессора CP0

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

unsigned int GetCP0_Count() {
unsigned int result;
asm volatile ("mfc0 %0, $9" :"=r"(result));
return result;
}

void main() {
unsigned int count;
count = GetCP0_Count();
}

От первого unsigned int и до первой закрывающей фигурной скобки идет объявление функции GetCP0_Count. В отличии от функции main(), код который выполняется сразу после загрузки программы в ОЗУ (после небольшого кода стартовой инициализации, о котором будет сказано позже), другие функции в программе не будут выполняться, если их не вызывают в main() или в других функциях, в свою очередь вызванных из main().

Используется тип функции unsigned int, а не void, потому что данная функция должна возвращать значение регистра CP0.Count. Ширина регистра CP0.Status — 32 разряда, значит, и возвращаемое значение должно быть типа unsigned int.

В функции GetCP0_Count() объявлена переменная result. Это связано с тем, что возврат значения осуществляется с помощью ключевого слова return. Чтобы вернуть считанное значение, его необходимо куда-то сохранить. Для этого и служит переменная result. Запись в регистры CP0 осуществляется аналогично:

void SetCP0_Count(unsigned int value) {
asm volatile ("mtc0 %0, $9" ::"r"(value));
}

void main() {
SetCP0_Count(0);
}

Видно, что в отличие от предыдущих используемых функций, в функцию SetCP0_Count() передается также параметр — то значение, которое необходимо записать в регистр сопроцессора CP0.

Окончательный текст программы

С учетом вышесказанного, текст программы на Си будет выглядеть следующим образом:

// файл со ссылками на макроопределения регистров
#include "nvcom02t.h"
// установка длительности паузы в тактах частоты CPU, 1 секунда
#define TIMEOUT 5000000

unsigned int GetCP0_Count() {
unsigned int result;
asm volatile ("mfc0 %0, $9" :"=r"(result));
return result;
}

void SetCP0_Count(unsigned int value) {
asm volatile ("mtc0 %0, $9" ::"r"(value));
}

void main() {
SYS_REG.CLK_EN.data = 0xFFFFFFFF;// запись единиц в системный регистр CLK_EN
MFBSP1.DIR.data = 0xC0; // переключение выводов LDAT[5:4] в режим выхода
MFBSP1.GPIO_DR.data = 0x80; // установка противоположные значения разрядов
//GPIO_DR1[7] и GPIO_DR1[6]
while (1) { // бесконечный цикл
MFBSP1.GPIO_DR.data ^= 0xC0; // инвертирование битов GPIO_DR1[7:6]
SetCP0_Count(0); // обнуление CP0.Count
while (GetCP0_Count()<TIMEOUT) ; // ожидание 1 секунду
}
}

Создание проекта в MCStudio 4

Создание проекта на ассемблере

Для создания проекта необходимо выбрать пункт меню «File → New Project», в появившемся диалоговом окне выбрать раздел «MultiCore → MultiCore Project» и нажать Next:

Рисунок 1. Создание проекта.

В следующем окне необходимо ввести название проекта, а также выбрать тип процессора и параметры проекта. Программа предназначена для процессора 1892ВМ10Я (NVCom-02T). В данном проекте не требуются функции стандартной библиотеки языка Си и не используются DSP-ядра, надо выбрать пункт «Empty Project».

Рисунок 2. Выбор типа процессора и параметров проекта.

На следующем этапе нужно выбрать варианты собираемой конфигурации. В данном случае нужна только конфигурация Debug:

Рисунок 3. Выбор вариантов собираемой конфигурации.

В следующем окне можно указать еще ряд параметров проекта. Но в данном случае подходят параметры по умолчанию, корректировать их не требуется. Необходимо нажать «Finish».

Рисунок 4. Выбор дополнительных параметров проекта.

После этого проект создан и отображается в окне «Project explorer»:

Рисунок 5. Выбор дополнительных параметров проекта.

Интерфейс Eclipse, на базе которого создана среда разработки MCStudio 4, имеет понятие «перспектива»

Режим отображения окон среды. В ходе работы потребуются две перспективы — «Debug» и «C/C++». Первая, как видно из названия, предназначена (более удобна) для отладки, и включена по умолчанию. Вторая более удобна для написания кода, и на данном этапе лучше переключиться на нее:

Рисунок 6. Выбор перспектив проекта.

После создания проекта надо добавить в него файл с исходным кодом. Для этого надо сделать правый клик на проекте и выбрать пункт «New → RISC Source File»:

Рисунок 7. Добавление файла с исходным кодом.

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

Рисунок 8. Добавление секций программы.

По умолчанию программа располагается в адресах внешней памяти (поле Address). На конечном устройстве исполнение программы начнется с виртуального адреса 0xBFC0_0000 (физический 0x1FC0_0000) — это заложено в архитектуре MIPS32. В рассматриваемом случае первично отладка будет происходить в программном симуляторе, а потом на отладочном модуле с использованием интерфейса JTAG, который позволяет загрузить программу в любые адреса и начать исполнение оттуда.

Рисунок 9. Выбор адреса для расположения программы.

Видно, что программа состоит из двух секций — text (исполняемый код) и data (данные). Секции данных в данном проекте физически нет, но в настройках она все равно должна присутствовать. В поле «VMA» задается виртуальный адрес расположения секции. То, что у секции данных и секции кода заданы одинаковые адреса, означает, что секция данных будет расположена сразу за секцией кода. После этого надо нажать «Finish», и файл будет добавлен в проект. Если потребуется корректировать адреса сборки в проекте, нужно открыть окно «Project → Properties → Multicore Project Properties»:

Рисунок 10. Выбор настроек проекта.

После изменения расположения секций программы в поле Adress надо нажать кнопку «Apply» и собрать проект «Project → Build Project». Необходимо открыть добавленный файл двойным кликом по нему и скопировать в него текст программы:

Рисунок 11. Просмотр кода программы.

Чтобы собрать проект (транслировать программу на ассемблере в машинный код), необходимо сделать правый клик на проекте и выбрать пункт «Build project»:

Рисунок 12. Трансляция ассемблера в машинный код.

В нижней части окна (вкладка Console) будет выведено:

**** Build of configuration MultiCore_Configuration_Debug for project NVCOM02TEM3U_LED ****
make all
Building file: ../main.asm
Invoking: RISC Assembler - mipsel-elf32-as
"c:\\elvees\\MCStudio4_17_FULL28_10_2014"/ToolsMGCC/bin/mipsel-elf32-as -G0 -mips32 --mc24r2 --defsy
m NVCOM=1 -EL -mhard-float -g --gdwarf-2 -o "main.o" "../main.asm"
Finished building: ../main.asm
Building target: NVCOM02TEM3U_LED.elf
Invoking: MultiCore Linker - mipsel-elf32-gcc
"c:\\elvees\\MCStudio4_17_FULL28_10_2014"/ToolsMGCC/bin/mipsel-elf32-gcc -nostartfiles -nodefaultlib
s -nostdlib -T Project.xl -EL -mhard-float -o "NVCOM02TEM3U_LED.elf" ./main.o __DSP0__.o __DSP1
__.o __DSP2__.o __DSP3__.o
c:/elvees/mcstudio4_17_full28_10_2014/toolsmgcc/bin/../lib/gcc/mipsel-elf32/4.4.5/../../../../mipsel
-elf32/bin/ld.exe: warning: cannot find entry symbol _start; defaulting to 0000000080001000
Finished building target: NVCOM02TEM3U_LED.elf
**** Build Finished ****

Видно, что программа собралась успешно, но есть ряд вопросов:

  • во-первых, нужно обозначить точку входа в программу. Это делается с помощью метки _start в начале программы. Кроме того, ее нужно обозначить как «глобальную»:

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

    Начало секции кода (обозначается директивой «.text»):

    Рисунок 14. Начало секции кода.

    Начало секции данных (обозначается директивой «.data»):

    Рисунок 15. Начало секции данных.
**** Build of configuration MultiCore_Configuration_Debug for project NVCOM02TEM3U_LED ****
make all
Building file: ../main.asm
Invoking: RISC Assembler - mipsel-elf32-as
"c:\\elvees\\MCStudio4_17_FULL28_10_2014"/ToolsMGCC/bin/mipsel-elf32-as -G0 -mips32 --mc24r2 --defsy
m NVCOM=1 -EL -mhard-float -g --gdwarf-2 -o "main.o" "../main.asm"
Finished building: ../main.asm
Building target: NVCOM02TEM3U_LED.elf
Invoking: MultiCore Linker - mipsel-elf32-gcc
"c:\\elvees\\MCStudio4_17_FULL28_10_2014"/ToolsMGCC/bin/mipsel-elf32-gcc -nostartfiles -nodefaultlib
s -nostdlib -T Project.xl -EL -mhard-float -o "NVCOM02TEM3U_LED.elf" ./main.o __DSP0__.o __DSP1
__.o __DSP2__.o __DSP3__.o
Finished building target: NVCOM02TEM3U_LED.elf
**** Build Finished ****

Теперь еще раз делаем «Build Project» и убеждаемся в отсутствии сообщений об ошибках в окнaх «Console» и «Problems». Если все прошло успешно, можно приступать непосредственно к проверке на программном симуляторе.

Создание проекта с использованием языка Си

Процедура создания проекта с использованием языка Си аналогична таковой для проекта, использующего только ассемблер. Приведены основные различия. Если бы рассматриваемый проект использовал стандартную библиотеку языка Си, надо было бы выбрать тип проекта «Empty NewLib Project» или «NewLib Project». Однако стандартная библиотека не используется, поэтому надо выбрать тип «Simple Project»:

Рисунок 16. Создание проекта.

Проект будет создан уже с файлами StartUp.asm и main.c, так что добавлять файлы в проект не потребуется. Однако, «Simple project» создается уже с кодом для ядер DSP, поэтому в нем содержатся файлы Dsp0.s0 и Dsp1.s1. Этот код служит примером использования DSP-ядер, но в рассматриваемом примере данные файлы не нужны, их необходимо удалить из проекта, сделав на них правый клик и выбрав пункт «Remove». Код, находящийся в файле main.c, необходимо удалить, и вставить на его место код созданной ранее про граммы на языке Си. Код, находящийся в файле StartUp.asm, универсален и является минимально необходимым кодом, осуществляющим инициализацию для возможности корректного исполнения программы, написанной на языке Си. Сборка проекта происходит аналогично тому, как это делалось с проектом на ассемблере.

Запуск отладки в программном симуляторе

В данном разделе рассмотрена отладка проекта в программном симуляторе. Рассмотрение проведено на примере проекта на ассемблере. Для проекта на языке Си процедура аналогична. В первую очередь, необходимо создать конфигурацию отладки. Для этого надо переключиться в перспективу «Debug», выбрать в «Project Explorer» отлаживаемый проект и зайти в меню «Run → Debug Configurations». В появившемся диалоговом окне помощника создания отладочных конфигураций необходимо выбрать строку создания новой конфигурации отладчика IModel3 «IModel3 Debug Configuration».

Рисунок 17. Выбор типа процессора и параметров проекта.

В поле «Name:» можно изменить имя конфигурируемой сессии отладки. В поле Project следует указать имя проекта (NVCOM02TEM3U), а в поле Executable file — имя выполняемого elf-файла, который планируется запускать под отладкой (NVCOM02TEM3U.elf из директории MultiCore_Configuration_Debug). Далее необходимо выбрать тип отладчика — «Simulator».

Рисунок 18. Выбор новых настроек.

Следует учитывать следующий нюанс. Программа написана, исходя из расчета на то, что процессор работает на частоте 5 МГц. Между тем, в составе микросхемы есть блок PLL, который позволяет умножать частоту, подаваемую на вход процессора, и работать на этой повышенной частоте. Во вкладке «Startup Registers» содержатся команды, которые выполняются при отладке перед загрузкой программы в память. Команды, находящиеся там по умолчанию для процессора 1892ВМ10Я, настраивают внешнюю память и устанавливают рабочую частоту CPU-ядра, равную 240 МГц:

Все эти команды надо просто стереть, (для этого надо сделать поле ввода активным, убрав флаг «Use Default») и нажать на кнопку «Apply»:

Рисунок 19. Обновление начальных регистров.

Сразу после нажатия на кнопку «Debug» проект будет загружен в память программного симулятора. Чтобы понять, работает ли программа, необходимо посмотреть отладчиком состояние задействованных блоков микросхемы. Для этого служит вкладка «MCRegisters» в нижней части окна. В ней есть вкладки CPU, CP0 и MFBSP1:

Рисунок 20. Просмотр инициализированных регистров.

Чтобы проконтролировать правильность исполнения программы, необходимо проверить:

  • происходит ли переключение соответствующих битов регистра GPIO_DR;
  • происходит ли это переключение происходит раз в секунду.

Проще всего для решения этой задачи поставить точку останова на инструкции, осуществляющей запись в регистр GPIO_DR. Это делается с помощью двойного клика на поле слева от номера строки:

Рисунок 21. Установки точки останова.

Теперь можно запустить программу исполняться и ожидать останова. Для этого нужно снова выбрать пункт меню «Run → Debug Configurations…» и запустить созданную конфигурацию. После останова:

Рисунок 22. Запуск программы через отладчик.

Видно, что значения битов [7] и [6] регистра GPIO_DR порта MFBSP1 инвертировались:

было до точки останова:

Рисунок 23. Значения регистров до точки останова.

стало на точке останова:

Рисунок 24. Значения регистров во время точки останова.

В регистре CP0.Count содержится значение 0x4С4B5B, что в десятичном отображении равняется 5000027:

Рисунок 25. Значения регистра CP0.Count.

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

Запуск отладки на отладочном модуле

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

  • подключить эмулятор к отладочному модулю;
  • подключить эмулятор USB-JTAG к компьютеру;
  • подать питание на отладочный модуль.

Далее надо, собственно, запустить отладку. Для этого необходимо создать новую конфигурацию отладки аналогично тому, как это сделано в предыдущем разделе. Только вместо «Simulator» надо выбрать «Emulator», и в ставшем активном выпадающем списке «Connected targets» выбрать подключенный процессор.

Рисунок 26. Запуск программы на отладочном модуле.

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