Использование языка Си
В статье процедура сборки проекта были рассмотрены базовые принципы работы с периферийными устройствами при разработке ПО для процессора серии «Мультикор», а также проведено первое знакомство со средой разработки MCStudio 4. В этой главе будут рассмотрены преимущества, предоставляемые языком Си, а также реализована аналогичная задача с использованием входящего в состав среды разработки MCStudio 4 компилятора Си для CPU-ядра. Ознакомление весьма поверхностное, так как невозможно в одной главе охватить хотя бы мало-мальски значимую часть серьезной книги по языку Си. Рекомендуется дополнительное обращение к литературе по данному языку программирования.
Главный недостаток ассемблера — тот факт, что программист вынужден слишком много задумываться о вещах, которые не имеют никакого отношения к функционалу разрабатываемого устройства. Какой регистр сейчас использовать, какой использовать нельзя, поставить NOP после команды перехода, так, а вот тут цикл организовать — всего-то пять строчек… Языки высокого уровня позволяют не задумываться о таких нюансах. Скажем, ту же запись в периферийный регистр можно сделать одной строкой. А уж как это будет на языке машинных кодов — дело компилятора. А о регистрах самого процессора программисту на Си и вовсе задумываться неуместно — такие тонкости его могут волновать только в крайних случаях. На этом с теоретической частью можно закончить и приступить к созданию проекта, реализующего то же самое моргание светодиодов, но на языке Си.
Разобьем процесс на три этапа:
- Написание самого кода;
- Создание проекта в MCStudio 4;
- Демонстрацию дополнительных возможностей при отладке в MCStudio 4.
Написание кода
Какая б ни была программа на языке Си, она обязана содержать в себе функцию main()
. То есть, в
базовом варианте она выглядит как:
void main() {
}
Теперь вернемся к функционалу. Как мы помним, наша программа на ассемблере состояла из двух частей — инициализации и рабочего цикла. В инициализации мы записывали ряд значений в ряд периферийные регистры, а в основном цикле — сначала читали регистр сопроцессора CP0.Count, а потом опять же читали и записывали в периферийные регистры. Значит, нам надо научиться делать на Си две вещи:
- оперировать с периферийными регистрами;
- оперировать с регистрами сопроцессора CP0.
Операции с периферийными регистрами
Запись в периферийный регистр — это запись по определенному адресу памяти. Для таких операций в языке Си предусмотрено понятие «указатель». Вот, скажем, такое объявление:
unsigned int *ptr;
Это значит, что объявлен символ ptr, который из себя представляет значение адреса памяти, для MIPS32
— это 32-разрядный адрес. То есть, ptr — это 32-разрядное число. И если произвести
чтение по адресу, равному этому числу — мы прочитаем значение, которое лежит в этом адресе
памяти. unsigned int
обозначает, что по данному адресу лежит число шириной 32 разряда. Ширина
числа определена ключевым словом int
. Слово unsigned
дает компилятору понять, что с этим числом
надо оперировать как с беззнаковым, то есть, могущим принимать значения от нуля до
(232-1).
С указателем в языке Си можно делать следующие операции:
- менять значение самого указателя (то есть, менять адрес, на который он указывает);
- менять значение того числа, которое лежит по этому адресу;
На самом деле, с указателями в Си можно делать вообще все, что угодно, но мы сейчас говорим в контексте реализации нашей задачи по миганию светодиодами.
Чтобы записать что-то по адресу, на который показывает наш указатель, нужно написать такую конструкцию:
*ptr = 0x12345678;
Здесь звездочка обозначает, что операция производится не со значением указателя, а с той ячейкой памяти, на которую он указывает. Если написать:
ptr = 0x12345678;
Это будет попыткой изменить значение самого указателя. В силу особенностей языка Си, последний
вариант он назовет ошибочным и не соберет. Логика простая — слева от знака равенства указан
тип (unsigned int *)
, а справа — число, которое компилятором воспринимается как тип
(unsigned int)
. Чтобы напрямую присвоить указателю значение 0x12345678, необходимо использовать
так называемое преобразование типов:
ptr = (unsigned int *) 0x12345678;
Вот здесь компилятор поймет, что указатель должен указывать на адрес 0x12345678, и соберет все без проблем. Учитывая вышесказанное, рассмотрим, как будет выглядеть запись значения 0xFFFF_FFFF в регистр CLK_EN на языке Си:
void main() {
unsigned int *CLK_EN;
CLK_EN = (unsigned int *) 0xB82F4004; // адрес регистра
*CLK_EN = 0xFFFFFFFF;
}
Здесь, однако, есть ряд излишеств. С точки зрения генерируемого кода, все это не так страшно, тем более, если у компилятора будет включена оптимизация. А вот с точки зрения громоздкости программы — все плохо, потому что мы делаем в три строчки то, что можно сделать в одну. Без излишних действий, можно было бы записать так:
void main() {
*((unsigned int *) 0xB82F4004) = 0xFFFFFFFF;
}
И это приведет ровно к такому же эффекту. При этом, первый вариант кода — это объявление переменной. А переменная нам не нужна — ведь мы не собираемся менять значение этого указателя. Второй вариант кода компактнее, но неудобно каждый раз выписывать вот эти вот звездочки и скобки. Здесь нам на помощь приходит такой механизм языка Си, как макроопределения:
#define CLK_EN *((unsigned int *) 0xB82F4004)
void main() {
CLK_EN = 0xFFFFFFFF;
}
Компилятор, проходя по коду, видит директиву #define
. Обрабатывая ее, он понимает,
что если где-то далее в коде он увидит макроопределение CLK_EN
- ему необходимо подставить вместо
этого макроопределения *((unsigned int*) 0xB82F4004)
. Что он успешно и делает. В итоге, наш код
приобретает читабельный и удобный вид.
По факту, для каждого процессора серии «Мультикор» формируется заголовочный файл, содержащий такие
макроопределения для каждого периферийного регистра. Эти файлы включаются в коде директивой
#include
, после чего становится возможно обращаться к периферийным регистрам по именам.
Поэтому, указанная запись в регистр CLK_EN, в совсем уж финальном варианте, будет выглядеть так:
#include "memory_nvcom_02t.h"
void main() {
CLK_EN = 0xFFFFFFFF;
}
Однако это еще не все. Пока мы говорили все сплошь о записи в периферийные регистры. Но нам также требуется производить и чтение оттуда, и дальнейшую обработку полученного значения. Вспомним главный цикл программы: считать значение регистра GPIO_DR, осуществить XOR, записать значение регистра. Чтобы считать и сохранить значение периферийного регистра, мы объявим переменную. Не указатель, а именно переменную. Вот так будет выглядеть объявление:
unsigned int tmp;
Это 32-раз рядная беззнаковая переменная. А код главного цикла будет выглядеть так:
#include "multicore/nvcom02t.h"
void main() {
unsigned int tmp;
tmp = GPIO_DR;
tmp ^= 0xC0;
GPIO_DR = tmp;
}
Значок «^» — операция побитового исключающего ИЛИ (XOR).
Оператор «^=» равнозначен такому: tmp = tmp^0xC0
.