Предыдущие статьи цикла:
- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd, и отладка на примере теста памяти.
- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd. Часть 2. Программный код.
- Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС.
- Разработка программ для центрального процессора Redd на примере доступа к ПЛИС.
- Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd.
- Веселая Квартусель, или как процессор докатился до такой жизни.
- Методы оптимизации кода для Redd. Часть 1: влияние кэша.
- Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин.
Немного теоретических рассуждений
Давайте прикинем, какую частоту мы можем безболезненно задать для тактирования всего нашего железа. Микросхема SDRAM, применённая в комплексе, допускает предельную частоту 133 МГц. Про предельные тактовые частоты для процессорного ядра можно узнать из документа Nios II Performance Benchmarks. Там для нашей ПЛИС Cyclone IV E гарантируется частота работы ядра Nios II/f, равная160 МГц. Я не сторонник выжимания всех соков из системы, поэтому будем рассуждать про работу на частоте 100 МГц.
Скажу честно, я до сих пор не проникся методикой вычислений сдвига тактовой частоты, приведённой в разделе 32.7. Clock, PLL and Timing Considerations документа Embedded Peripherals IP User Guide, но похоже, что не я один такой. По крайней мере, долгий поиск в сети не привёл меня к статьям, которые содержали бы какие-либо результаты, вычисленные аналогичным образом, но не для той частоты, которая приведена в основном документе (те самые 50 МГц).
Имеется интересная статья, на которую я дам прямую ссылку www.emb4fun.de/fpga/nutos1/index.html. Можно было бы вообще просто сослаться на неё и сказать «Давайте делать, как у автора», если бы не одно «но»: автор этой статьи использует блок PLL (по-русски — ФАПЧ, а на бытовом уровне — преобразователь частот), вставляя его в собственный код на языке VHDL. Я же, как уже отмечал в статье про весёлую Квартусель, придерживаюсь идеологии, что процессорная система у нас должна находиться на верхнем уровне иерархии проекта. Не надо никаких вставок ни на каком языке, будь то VHDL или Verilog. Недавно этот мой подход получил ещё одно подтверждение: у нас появился новый сотрудник, студент, который пока что не владеет языком Verilog, но прекрасно делает код для комплекса Redd, благо выбранный подход позволяет это.
Получается, что мы просто возьмём за основу, что у автора всё работает при сдвиге минус 54 градуса (что за градусы — описано в статье, ссылку на которую я дал абзацем выше).
Дальше обратим внимание на ещё одну интересную статью asral.unimap.edu.my/wp-content/uploads/2019/07/2019_IJEECS_Chan_Implementation-Camera-System.pdf. Там у авторов всё работает при сдвиге минус 65 градусов.
Попробуем сделать свою систему, используя значение из этого диапазона. Если при суточном тесте ОЗУ не возникнет ни одного сбоя, то и оставим это значение, как боевое. Имеем право, так как разрабатываемые «прошивки» под Redd не пойдут к Заказчикам, а будут использоваться для внутренних нужд, причём в штучных количествах. Если что, всегда можно будет всё исправить без лишних сложностей (сложности возникают, когда надо обновить «прошивку» в тысячах проданных устройств, да и просто у удалённого Заказчика).
Новая часть аппаратной конфигурации
Мне почему-то кажется, что процессорную систему под эту статью проще сделать с нуля, чем переделать из старой. Просто чем демонстрировать процесс «кручу, верчу, запутать хочу», постоянно отсылая к прошлым статьям, лучше я ещё раз всё покажу с самого начала. Заодно закрепим материал. Итак, приступаем.
В самом начале нам показывают совершенно пустую систему, содержащую только источник тактового сигнала и сигнала сброса.
Обычно я ничего там не меняю, но сегодня сделаю исключение. Мне не хочется отвлекаться на схему сброса, так как всё равно работать мы будем из-под отладчика. Поэтому я переключу условие сброса с уровня на отрицательный перепад, а саму ножку впоследствии занулю.
Но здесь тактовый сигнал имеет частоту 50 МГц (эта частота задаётся характеристиками припаянного на плату генератора). В первой из упомянутых мною выше статей, использовался блок PLL, добавленный в основной проект. Где мы его возьмём здесь? А вот он!
Это тот же самый блок, но здесь нам не придётся вставлять никакого кода на языках Verilog или VHDL. Всё уже вставлено за нас! Правда, настройка для разных типов ПЛИС различается чуть больше, чем полностью. Если точнее, то настраиваемые параметры более-менее совпадают, но находятся в принципиально разных местах диалогов настройки. Так как в комплексе Redd применена ПЛИС Cyclone IV E, то рассмотрим настройку этого варианта.
На первой вкладке, входную частоту заменяем на 50 МГц (по умолчанию там было 100) и переходим на следующую вкладку (жмём Next, для Cyclone IV E нам это предстоит сделать много раз).
Снимаем флажки дополнительных входов и выходов. Нам они не нужны:
Несколько следующих вкладок пропускаем, пока не дойдём до настройки выхода C0. Там переключаем радиокнопку на задание частоты и вводим значение 100 МГц:
С выходом C1 всё чуть сложнее. Во-первых, выбираем флажок, говорящий, что его тоже следует использовать. Во-вторых, аналогично задаём частоту 100 МГц. Ну, и в-третьих, задаём сдвиг частоты. Какой задать? Минус 58 или минус 65? Разумеется, я попробовал оба варианта. У меня заработали оба. Но аргументация на тему минус 58 выглядит чуть менее убедительно, поэтому здесь я буду рекомендовать вписать значение минус 65 градусов (при этом автоматика мне скажет, что реально достигнутым значением будет минус 63 градуса).
Ну всё. Теперь можно прошагать кнопкой Next до конца, а можно просто нажать Finish. Подключаем входы inclk_interface и inclk_interface_reset. Выход c0 будем использовать как тактовый для всей системы. Выход c1 экспортируем для тактирования микросхемы sdram. В будущем надо будет не забыть подключить шину данных ко входу pll_slave. Для Cyclone V такое было бы не нужно.
Прочие части аппаратной части, чисто для закрепления материала
Добавляем процессорное ядро. Сегодня у нас SDRAM будет подлежать тестированию. Значит, код в ней располагаться не должен. А это, в свою очередь, означает, что весь код будет располагаться во внутреннем ОЗУ ПЛИС. То есть, нам не нужен кэш инструкций. Отключаем его, сэкономив память ПЛИС. Также подключаем по одной сильно связной шине инструкций и данных. Больше пока никаких интересных настроек для процессорного ядра не требуется.
Привычным движением руки добавляем два блока внутреннего ОЗУ ПЛИС. Один — двухпортовый, ёмкостью 16 килобайт и один — однопортовый, ёмкостью 4 килобайта. Как их назвать и как подключить, надеюсь, все помнят. В прошлый раз мне понравилось подсвечивать шины цветами, пожалуй, для простоты чтения, я сделаю так и в этой статье.
Не забудем назначить этим блокам памяти особые адреса в персональном диапазоне и запереть их. Пусть память CodeMemory будет назначена на 0x20000000, а DataMemory — на 0x20004000.
Ну, и добавим в систему блок SDRAM, настроив его, а также блоки JTAG-UART для вывода сообщений и однобитный GPIO, на котором мы будем измерять реальную частоту, чтобы убедиться, что она увеличилась. Приведу для справки некоторые не очевидные настройки:
Итого, получаем такую систему (я выделил шину данных, так как она разбегается по всем внешним устройствам):
Назначаем вектора процессору, автоматически назначаем адреса, автоматически назначаем номера прерываний, генерим систему.
Подключаем систему к проекту, делаем черновую сборку, назначаем номера ножек, причём в этот раз делаем виртуальными не только CKE, но и reset_n (как это делается, я рассказывал в одной из прошлых статей, там ищите по словам Virtual Pin). Делаем чистовую сборку, заливаем аппаратуру в ПЛИС. Всё. С аппаратурой покончено, переходим к программной части.
Настраиваем BSP под нашу среду
Давайте для разнообразия создадим проект на базе шаблона не Hello World Small, а Memory Test Small:
Когда он создался, идём в редактор BSP. Как обычно, первым делом отключаем проверку SysID и разрешаем использование С++ (правда, в этот раз я не буду менять тип файла, но у меня это уже вошло в привычку):
Но самое главное нам предстоит поправить на вкладке Linker Script. Автоматика распознала, что шина инструкций у нас идёт только на память CodeMemory, поэтому разместила секцию кода (она называется .text) в памяти CodeMemory. А вот всё остальное она, заботясь о нас, поместила в самый большой регион данных, который размещён в SDRAM. Откуда ей было знать, что мы будем эту память нещадно затирать?
Придётся нам вручную, строка за строкой, заменить регион на DataMemory (там будут появляться списки выбора, в них следует переставить выделение). У нас должна получиться такая картина:
Эксперименты с программой
Выходим из редактора, генерим BSP, пробуем запустить программу на отладку. Нам выводят такой текст:
Если нажать Enter — у меня ничего не получалось. Я вводил что-нибудь (да хоть пробел) и затем нажимал Enter. Тогда у меня спрашивали:
Час от часу не легче. И какой адрес вводить? Можно открыть Platform Designer и посмотреть значение там. Но я обычно смотрю в универсальный справочник — файл system.h (полный путь для моего проекта C:WorkCachePlay5softwareMemoryTest_bspsystem.h). Там нас интересуют две строки:
#define ALT_MODULE_CLASS_new_sdram_controller_0 altera_avalon_new_sdram_controller #define NEW_SDRAM_CONTROLLER_0_BASE 0x0 #define NEW_SDRAM_CONTROLLER_0_CAS_LATENCY 3 #define NEW_SDRAM_CONTROLLER_0_CONTENTS_INFO #define NEW_SDRAM_CONTROLLER_0_INIT_NOP_DELAY 0.0 #define NEW_SDRAM_CONTROLLER_0_INIT_REFRESH_COMMANDS 2 #define NEW_SDRAM_CONTROLLER_0_IRQ -1 #define NEW_SDRAM_CONTROLLER_0_IRQ_INTERRUPT_CONTROLLER_ID -1 #define NEW_SDRAM_CONTROLLER_0_IS_INITIALIZED 1 #define NEW_SDRAM_CONTROLLER_0_NAME "/dev/new_sdram_controller_0" #define NEW_SDRAM_CONTROLLER_0_POWERUP_DELAY 100.0 #define NEW_SDRAM_CONTROLLER_0_REFRESH_PERIOD 15.625 #define NEW_SDRAM_CONTROLLER_0_REGISTER_DATA_IN 1 #define NEW_SDRAM_CONTROLLER_0_SDRAM_ADDR_WIDTH 0x18 #define NEW_SDRAM_CONTROLLER_0_SDRAM_BANK_WIDTH 2 #define NEW_SDRAM_CONTROLLER_0_SDRAM_COL_WIDTH 9 #define NEW_SDRAM_CONTROLLER_0_SDRAM_DATA_WIDTH 16 #define NEW_SDRAM_CONTROLLER_0_SDRAM_NUM_BANKS 4 #define NEW_SDRAM_CONTROLLER_0_SDRAM_NUM_CHIPSELECTS 1 #define NEW_SDRAM_CONTROLLER_0_SDRAM_ROW_WIDTH 13 #define NEW_SDRAM_CONTROLLER_0_SHARED_DATA 0 #define NEW_SDRAM_CONTROLLER_0_SIM_MODEL_BASE 0 #define NEW_SDRAM_CONTROLLER_0_SPAN 33554432 #define NEW_SDRAM_CONTROLLER_0_STARVATION_INDICATOR 0
где десятичное 33554432 равно шестнадцатеричному 0x2000000. Поэтому мои ответы и результат работы должны выглядеть так:
Отлично, но на суточный тест это не годится. Я переписал функцию main так:
int main(void) { int step = 0; while (1) { if (step++%100 == 0) { alt_printf ("."); } if (MemTestDevice(NEW_SDRAM_CONTROLLER_0_BASE, NEW_SDRAM_CONTROLLER_0_SPAN)!=0) { printf ("*"); } } return (0); }
Точки показывают, что программа не «зависла». Если же будет ошибка, то отобразится звёздочка. Для надёжности, можно поставить на её вывод точку останова, тогда точно не проспим её.
Правда, откуда-то полезли «левые» точки. Оказалось, что они выводятся внутри функции MemTestDevice(). Там их вывод я затёр. Тест прошёл успешно. Полученную систему можно применять, как минимум, для внутренних нужд (а именно такие разработки и ведутся под комплекс Redd).
Проверка итоговой производительности системы
Но я уже привык, что при работе с аппаратурой нельзя доверять ничему. Всё следует тщательно проверять. Давайте убедимся, что мы работаем на удвоенной относительно прошлых статей частоте. Добавляем хорошо знакомую нам функцию MagicFunction1().
void MagicFunction1() { IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); IOWR (PIO_0_BASE,0,1); IOWR (PIO_0_BASE,0,0); }
Вызовем её из main(), поймаем импульсы на осциллографе, но на этот раз обратим внимание не только на их красоту, но и на частоту (напомню, что каждый перепад хоть вверх, хоть вниз — это одна команда, поэтому измерять можно расстояния между перепадами).
Всего 50 мегагерц. Неужели частота не повысилась? Сравниваем с частотой из кода, разработанного при написании прошлой статьи, и понимаем, что всё в порядке. Просто штатный блок pio требует 2 такта на один вывод в порт (это в самодельном у меня получался 1 такт, но здесь нам достаточно просто убедиться, что производительность системы удвоилась).
Заключение
Мы научились вместо генератора фиксированной тактовой частоты использовать настраиваемый блок PLL. Правда, обнаруженные константы предназначены для частоты 100 МГц, но все желающие могут подогнать их под любую другую частоту либо пользуясь известными расчетами, либо методом проб и ошибок. Также мы закрепили навыки создания оптимальной процессорной системы и убедились, что память на повышенной частоте работает устойчиво, а частота действительно увеличилась.
В целом, производить какие-либо вычислительные вещи мы уже можем, можем даже обмениваться с центральным процессором, но с банальными вычислениями центральный процессор комплекса справится более производительно. ПЛИС добавлена в Redd для того, чтобы реализовывать какие-либо быстродействующие интерфейсы либо захватывать (ну, или воспроизводить) потоки информации. Основы проектирования мы уже освоили, обеспечить более-менее высокую производительность — научились. Пора продолжать работу с интерфейсами, чем мы и займёмся в следующей статье. Точнее, наборе статей, памятуя о правиле «одна статья — одна вещь».
Источник