Перейти к содержанию
    

Migration VDSP++->bfin-elf

Всем привет!

 

* * *

 

Преамбула.

 

Некоторое время назад у меня случился переезд с Windows на Linux (смена основной рабочей платформы). И если с софтом для обычной работы/досуга никаких (кроме привыкания) серьёзных проблем нет, то с САПРами всё далеко не так шоколадно. Основная трудность в том, что у некоторых из них нет Linux версий. Отчасти обходным путём может быть использование виртуальной машины, но это не лучший вариант — от Windows тут избавиться не удаётся, а использование становится ещё менее удобным. Поэтому естественно желание применять нативный софт.

 

Для меня основной проблемой стало то, что на Windows использовался ADI VisualDSP++/Blackfin. На виртуалке это всё поднялось (в том числе и коннект с железкой через ICE-100B), т.ч. как запасной вариант годилось, но хотелось, повторяю, нативного пакета. Особого выбора тут нет — это ADI GNU Toolchain (GCC Blackfin в разных комплектациях). Особенность данного тулчейна в том, что он не предназначен для обычной работы в bare-metal окружении как тот же VDSP++, он предназначен для запуска uClinux на платформе Blackfin.

 

Исходя из этого, содержание ADI GNU Toolchain'а составляет вариант для сборки приложений под uClinux — называется bfin-uclinux, набор библиотек и опций для всего этого и вариант для сборки С-приложения uboot — называется он bfin-elf. Поскольку uClinux меня не интересует, а нужен инструмент для работы в bare-metal окружении на С++, то выбор пал на bfin-elf.

 

При освоении данного тулчейна пришлось столкнуться с рядом трудностей, которые были более-менее успешно преодолены, а рабочие проекты портированы с VDSP++ на bfin-elf. Одной из таких трудностей стало то, что не всё необходимое для работы в bare-metal режиме на С++ содержалось в самом тулчейне (bfin-elf), а кое-что оказалось неприемлемого качества. Поэтому в результате этой работы было скомплектовано несколько библиотек (подробнее тут).

 

 

* * *

 

Амбула.

 

Вся тема возникла из-за того, что штатно (т. е. «из коробки») упомянутый тулчейн не позволяет полноценно работать. Он не включает в себя средства для регистрации обработчиков исключений (прерываний), не поддерживает вызов конструкторов глобальных объектов и имеет некоторые другие недостатки. Собственно, это обусловлено тем, что пакет сырой, «недоделанный» - по сути он предназначался для для того, чтобы собрать uboot, никто на нём разрабатывать bare-metal с использованием C++ приложения не собирался.

 

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

 

Ниже перечислены основные проблемы, варианты их решения и обходные пути.

 

* * *

Отсутствие функции-регистратора обработчиков исключений было восполнено путём подтягивания соответствующей функции из VDSP++ (характерный момент: там в заголовочных файлах всё есть, а вот в библиотеке — нет, поэтому компилируется без вопросов, а на сборке возникает ошибка).

 

* * *

Вызов конструкторов глобальных объектов в нынешних GCC пакетах осуществляется с помощью функции __libc_init_array из библиотеки тулчейна. Такая функция там есть. Вот только её вызова в basiccrt.S и других стартапах из состава bfin-elf нет.

 

И второй момент: __libc_init_array вызывает пользовательскую функцию _init(), которая по замыслу предназначена для выполнения низкоуровневой инициализации. Но вот далеко не всегда такая функция нужна, а пользователь вынужден её определять. По-хорошему, такая функция должна была бы лежать в стандартной библиотеке рядом с __libc_init_array, объявленная с атрибутом weak, но этого там не сделано. Поэтому такая weak функция помещена в упомянутую библиотеку.

 

Кроме того, добавлен файл crt.S, который есть минимально необходимый код стартапа:

  • настройка регистров процессора;
  • перевод в режим супервизора на нижнем приоритете;
  • обработчики исключений по умолчанию;
  • обнуление секции .bss;
  • и, конечно, вызов конструкторов глобальных объектов (__libc_init_array).

 

Обнуление секции .bss потребовалось потому, что тулчейн генерирует эту секцию незагружаемой (флаг ALLOCATE есть, флага LOAD нету), поэтому при загрузке программы в процессор с помощью gdb данная секция оказывается необнулённой со всеми вытекающими.

 

* * *

В тулчейн bfin-elf из VDSP++ портировано некоторое количество интринсиков (builtins.h), но, к сожалению, далеко не все, и реализация некоторых не вполне корректна. В частности, мне не хватило доступа к регистру CYCLES (такой интринсик был добавлен), а интринсики cli/sei содержат баг, который приводит к малопонятному сообщению об ошибке (типа, недопустимый символ с индексом nnnn в потоке).

 

Баг уже известный, суть его сводится к тому, что в реализации, которая сделана с помощью inline assembler, используется паттерн 'r', который соответствует R0-R7 и P0-P5 регистрам процессора, в то время, как инструкции cli/sei допускают только регистры данных, т. е. R0-R7, поэтому правильный паттерн для этих функций не 'r', а 'd'. Оба интринсика так же добавлены в описываемую библиотеку в виде встраиваемых функций __cli/__sei.

 

* * *

Тулчейн bfin-elf v4.3.5 содержит очень гадкий баг: при некоторых обстоятельствах в обработчике прерываний портится один из регистров (я налетел на порчу регистра I0): регистр используется в теле обработчика, но не сохраняется/восстанавливается в прологе/эпилоге. Это приводит к очень неприятным, непредсказуемым и трудноуловимым глюкам при работе программы. На эту тему составлен багрепорт, но полагаю, что исправляться это не будет.

 

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

 

* * *

Из состава VDSP++ подтянут файл с низкоуровневыми функциями настройки pll/clock (pll_set_system_vco/pll_set_system_clocks).

 

* * *

Утилита для генерации загрузочного образа bfin-elf-ldr имеет следующие недостатки:

 

  • она тупо грузит все секции, какие есть — например, если в SDRAM находится здоровенная секция, которую не нужно инициализировать, секция всё равно будет загружена, что порождает большой размер файла загрузочного образа и увеличивает время загрузки;
  • загрузочный образ оказывается непригодным для использования в некоторых случаях — конкретно, для загрузки в режиме SPI Slave (описано тут: https://ez.analog.com/thread/71256?start=0&tstart=0), т. к. загрузчик процессора имеет фичу, которая ориентирована на мультизагрузочную конфигурацию образа (загрузчик из состава VDSP++ всегда делает мультизагрузочную конфигурацию, поэтому проблем не создаёт). Фича нигде не описана.

 

Для обхода этой проблемы написан загрузчик, свободный от этих недостатков: помещает в LDR файл только секции с флагом LOAD и генериует LDR файл в виде мультизагрузочного образа. Загрузчик написан на языке python. Взять его можно тут. Использовать можно как из командной строки, так и из сборочного скрипта. Требует утилит bfin-elf-objdump и bfin-elf-objcopy, обе есть в составе bfin-elf.

 

* * *

Ну, и последнее. Немало возни было с организацией сборки. Поэтому для облегчения старта создан простой пример, который собирается и работает. Сборка осуществляется с помощью утилиты Scons. Если используется другой инструмент для сборки, то всё равно может оказаться полезным посмотреть опции сборки в сборочном скрипте SConstruct.

 

 

 

P.S. При прохождении этого нелёгкого пути очень большую помощь мне оказал AHTOXA. Как технического плана при погружении в мир GCC, так и морально-психологического, выслушивая нытьё на тему «что ж оно всё такое кривое-горбатое?!..» :) Выражаю ему и Сергею Борщу признательность и благодарность.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Колоссальная работа.

 

Скажите, есть ли у вас какие-то оценки результата сборки при переходе на gcc?

Как изменился footprint?

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

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Скажите, есть ли у вас какие-то оценки результата сборки при переходе на gcc?

Как изменился footprint?

Да, конечно, это в первую очередь оценивалось, т.к. у меня ориентация на то, что программа должна помещаться во внутренней памяти программ. Общая оценка такая: front-end компилятора очень хорош, код общего назначения оптимизирует отлично - всё что можно "просчитать" на этапе сборки, он делает, в итоге код (тестовый) из нескольких вызовов (правда простых) функций с какой-то тупой логикой (ну, просто для теста там всякая бессмысленная арифметика делалась) в сгенерённом коде выливался всего в пару инструкций. Это поначалу вводило в подозрение - что это, де, за фигня, поди ошибка, но тщательный разбор показывал, что действительно это нагромождение кода по факту выливается в декремент на два, что в итоге компилятор и выдал.

 

Полагаю, это благодаря тому, что gcc front-end является весьма высококачественным и с точки зрения поддержки средств языка (С++), и с точки зрения оптимизации логики.

 

С кодогенератором тут похуже, всё-таки vdsp++ в этом смысле более доведён до ума. Но на коде общего назначения эта разница почти не сказывается, а DSP код, моё мнение, надо писать либо на ассемблере (при использовании обоих тулчейнов), либо использовать специализированные функции.

 

Конкретно на чём bfin-elf проигрывает vdsp++:

  • использование аппаратных циклов;
  • оптимизация конвейера;

Если vdsp++ втыкает lsetup(...) LCn = ... каждом случае, то bfin-elf это делает куда менее охотно даже при опции компилятора -funsafe-loop-optimizations. Какая логика им рулит, для меня пока осталось неясным. Конечно, аппаратный цикл чаще всего лучше, чем цикл с явным переходом по инструкции ветвления. Лучше и по производительности, и по размеру кода.

 

С конвейером vdsp++ производит очень жёсткие оптимизации - переставляет инструкции так, что там вручную понять, что к чему относится, всегда было непростой задачей, что очень затрудняло отладку по коду. Меня по началу это выводило, а потом когда я писал свой кусок на асме, то налетел на предупреждения о простаивании конвейера, дескать, из-за того, что вот загрузил адрес в P-регистр, и в следующей инструкции уже пытаюсь обращаться по нему, а адрес-то по конвейеру ещё не доехал до аппаратуры, которая осуществляет адресацию, поэтому конвейер stall 3 такта... Думаю, вот чего он будет простаивать, я пока что-то другое могу поделать... И тут замечаю, что я и сам начинаю вот точно так же переставлять инструкции в потоке. В общем, зауважал компилятор.

 

bfin-elf такой виртуозности не достиг, у него код более линейный, может он и переставляет там что-то, но я как-то не заметил - отладка кода куда проще - всё читается сходу. Что заметил у этого компилятора: он хорошо "держит" контекст, т.е. как бы "помнит" какие ресурсы он на что использовал и в каком они состоянии. Например, смотрю фрагмент кода (кода бился с этим гадким багом с порчей регистра в прерывании), вижу, там почему-то адрес массива не грузится в P-регистра, а делается операция декремента этого регистра на какое-то значение. Думаю, что это - баг, что-ли - прога-то вылетает, вот на любое место подозрение.

 

Стал разбираться, оказалось, что всё корректно: регистр был ранее загружен значением адреса, связанным с адресом упомянутого массива, и компилятор просто "помнит" это и "знает" эту связь, а получить нужное значение адреса выгоднее одной арифметической операцией с P-регистром, чем явно грузить половинки адреса - это две инструкции, и они, вдобавок, ещё и "толстые" - каждая тащит 16 бит значения адреса.

 

Т.е. нельзя сказать, что кодогенератор совсем уж тупой - вот такие вещи он делает очень прилично. Возможно, это связано с тем, что это оптимизации общего плана, и тут gcc уже не одну "собаку съел", а вот с вещами, "завязанными" на аппаратуру конкретного процессора (конвейер, аппаратные циклы) у него послабее.

 

По факту портировал два проекта, финальные размеры кода (в байтах):

 

Project#    1      2
vdsp++:   20288  66194
bfin-elf: 19748  67880

Очень сильно на это влияет качество библиотек. Первый проект, когда в первый раз запустил, размер кода был что-то под 45 килобайт! Я даже офигел поначалу. Потом стал смотреть в мапе, что же там столько жрёт. Оказалось это библиотека плавающей точки и библиотека форматированного вывода.

 

С плавающей точкой удалось разобраться относительно легко - оказалось, в составе bfin-elf есть та самая оптимизированная либа плавающей точки из vdsp++ (очень хорошая реализация), просто её надо было правильно подключить: там есть опция gcc "-mfast-fp", которая должна была бы это сделать, я её передал компилятору, и толку не было. А оказывается её надо линкеру передавать. Сам фронт-энд gcc, видимо, умеет правильно конфигурировать вызовы, но я сборку организую как раздельные вызовы компилятора, ассемблера, линкера, вот передавал не тому тулу. Это нигде не освещается, опция gcc и всё. Ошибочно полагал, что при этой опции компилятор будет генерировать другие имена библиотечных функций. Но нет, имена те же, просто линкер подсовывает другую либу, что, в общем, проще и логичнее. Только бы документировать этот момент почётче.

 

А та библиотека, которую он по умолчанию вставляет, это, по ходу, тянуто с РС, там всё очень неоптимально. Для сравнения: функция умножения плавающей точки имеет размер 1.7 кбайта в то время как оптимизированная библиотека эту же функцию реализует что-то в 270 байт. Вот так вот и разница набегает.

 

Оптимизированная либа не совсем полноценна - она не все требования стандарта выполняет (проверок на NaN нету, ещё что-то подобное), но для наших применений это, как правило, несущественно. Тем более, что на vdsp++ используется она же.

 

Форматированный вывод тоже достаточно безобразно сделан. Поэтому была подтянута сторонняя реализация - она есть в упомянутой в предыдущем посте библиотеке на гитхабе.

 

Ну, и во втором проекте используются немного контейнеры stl, что тянет за собой алокаторы, т.е. работу со свободной памятью. Реализация из bfin-elf тоже никакая, поэтому тоже была подтянута сторонняя библиотека.

 

В с этими либами результат такой, как показан выше. Собственно, идея с этой коллекцией библиотек отсюда и поизросла.

 

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

Явных тестбенчей не проводил, единственное, что замерял - это время передачи управления между процессами в scmRTOS, в случае vdsp++ оно было 1.5 мкс (200 МГц тактовая проца), на bfin-elf - 1.6 мкс. Т.е. чуть похуже, но не фатально.

 

В общем, мой вывод: несмотря на все трудности, жить можно, результат на bfin-elf не лучше, но и почти не хуже. Опасения за качество кодогенерации, можно сказать, не подтвердились. Больше всего напрягает то, что тулчейн, видимо, не развивается.

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Титанический труд. Спасибо.

Если подводить итог: вы полностью переезжаете на gcc или для старых проектов остаетесь на vdsp?

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Титанический труд. Спасибо.

И вам спасибо за оценку. :)

 

Если подводить итог: вы полностью переезжаете на gcc или для старых проектов остаетесь на vdsp?

Полностью. У меня на данный момент реально всего пара-тройка долгоиграющих проектов с vdsp++, которые актуально продолжать. Если что-то потребуется ещё - портирование само по себе достаточно простое, когда уже известны и обойдены трудности и есть весь набор инструментов, включая сборочный скрипт, утилиту для наглядного вывода информации о потреблении ресурсов, утилиту загрузчика, библиотеки поддержки... В этих условиях, IMHO, нет особой разницы по сложности между "начать новый проект" и "портировать имеющийся".

Поделиться сообщением


Ссылка на сообщение
Поделиться на другие сайты

Присоединяйтесь к обсуждению

Вы можете написать сейчас и зарегистрироваться позже. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.

Гость
Ответить в этой теме...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...