Программный графический сопроцессор на STM32
Прошел год и многие вечера коротались написанием очередного, куда более крупного и на этот раз полезного проекта.
В прошлый раз везде приходилось ужиматься, как только возможно. Ресурсов того многострадального камня мне стало не хватать и в какой-то момент пришло интересное решение. Отдать часть задач другому контроллеру.
(Как и в прошлый раз, под катом много воды и изображений)
Несмотря на наличие таких проектов как Nextion HMI и Gameduino, я решил сделать свое решение по ряду причин.
Gameduino хоть и казалась интересным решением (VGA выход и FPGA на борту), но невозможность достать вторую версию этой платы, вынудила приобрести ее первую ревизию. Это были боль и страдания:
- малопонятный протокол;
- малое количество примеров в сети;
- хранение графики в хосте (можно на SD карте, но тогда нужен FatFS, а он съедает куда больше, и смысл теряется);
- множество магических чисел, которые поймет только Гуру и.т.д.
- в целом этот проект скорее мертв, чем жив.
Несмотря на взрывную популярность Nextion сейчас, проект начал появляться до столь широкого распространения и вот другие причины отказа:
- мне не хотелось разбираться с nextion (начиная с IDE);
- опасность нарваться на не европейскую плату (хотя, это вроде лечится);
- доступны только заранее запрограммированные действия из IDE (поправьте, если не прав);
- нужно писать и для хоста и для дисплея;
- большой overhead в протоколе (что, правда, объяснимо).
И самое главное, в обоих случаях нет возможности дописать что-то свое.
Для чего это вообще
Может показаться, что sGPU очень похож на Nextion дисплей, но это не так. Он значительно проще и появился из интереса, но со временем раздулся настолько, что захотелось поделиться с сообществом, особенно после немногочисленных просьб.
Часть идей была позаимствована от всевозможных библиотек, личное по мелочам и старых консолей (после прочтения кучи мануалов по устройству, но далеко не все, что хотелось бы).
Описывать все возможности в одной статье явно не выйдет, более того все доступные команды можно посмотреть в исходниках для хоста (Arduino библиотеки) или в файле «commandDiscriptions.txt».
Делать было нечего
Что делать если есть неплохой дисплей и простой контроллер stm32? Очевидно же! Скинуть все взаимодействие с экраном на него!
Первым делом была взята библиотека для работы с дисплеем от Adafruit и почти полностью переписана, потом прикручено DMA. Графические примитивы есть, вывод текста есть,
Можно было бы остановиться на этом и писать уже под stm32, но ведь так трудно устоять перед очередным троллейбусом!
Контроллер для sGPU
От серии STM32F4xx отказался сразу, ввиду несуразности идеи (использовать такой контроллер для такой ерунды! Если только для pro2…).
Выбраны были STM32F103C8T6 с 20 кб RAM (далее mini версия) и STM32F103VET6 с 64 кб RAM (далее pro версия).
Так как mini имеет куда большее распространение благодаря китайцам и их дешевому и любимую клону maple mini, было решено как основное ядро выбрать его, несмотря на некоторые ограничения, описанные далее (плату с pro версией китайцы тоже выпускают как клон ministm32).
Не просто так написал ядро, достаточно сменить тип контроллера в проекте и изменить пару инклюдов и получаем sGPU с большим объемом памяти и контроллером FSMC
(дешевизна и распространённость mini версии, не дают покоя не использовать ее).
Экран
Использовался экран, на базе драйвера ILI9341 (разрешение экрана 320×240).
Взаимодействие с экраном идет по SPI (так же имеется возможность по FSMC, но протестировать так и не удалось по различным причинам) на максимально возможной скорости для данного контроллера — 36 Мбит/с (из реально замеренных, примерно 2,7 Мб/с или 22 Мбит/с), но только при DMA передаче, как и было указано в Reference manual (RM0008 for F101-F107).
После тщетных попыток, так и не удалось завести SPI на большей скорости. Блок SPI в контроллере банально начинает сходить с ума (стабильный максимум 80 МГц для F_CPU, вместо 72 МГц, дает чуть больше не разогнанных 36 Мбит/с).
Интерфейс
Самое интересное началось, когда предстал выбор перед интерфейсом общения хоста и sGPU.
Требования к интерфейсу были следующие:
- Поддержка почти любым контроллером;
- Легко реализовать;
- Обладает достаточной пропускной способностью;
- Обладает минимальным количеством используемых линий.
Выбор пал на UART. С выбором скорости было уже не так очевидно, так как далеко не все устройства могут поддерживать скорость обмена в 1МБод. Недолго думая просто сделал четыре возможных скорости: 9600, 57600, 115200 и 1М. Так же сделана возможность выбора скорости аппаратно, при помощи 3-х GPIO (соответствие GPIO и скоростей можно найти в STM32_GPU_GPIO_Pinout.txt), что дает 8 возможных значений.
Из них только 4 использовано, остальное в резерве (можно сделать хоть 1200 Бод).
Скорость выбирается по маске, посредством
switch(GPIOA->IDR & 0x07) // считываем состояние GPIO
{
case 0x01: {
init_UART1(USART_BAUD_9600);
print(T_BAUD_9600);
} break;
case 0x02: {
init_UART1(USART_BAUD_57600);
print(T_BAUD_57K);
} break;
case 0x03: {
init_UART1(USART_BAUD_115200);
print(T_BAUD_115K);
} break;
case 0x04: {
init_UART1(USART_BAUD_1M);
print(T_BAUD_1M);
} break;
/*
* params 0x05-0x07 and 0x00: reserved
*
*/
default: {
init_UART1(USART_BAUD_57600);
print(T_DAUD_DEFAULT);
} break;
}
Тайлы
Это первое, что должен уметь sGPU.
Каждый тайл, это всего-навсего массив байт с индексами цветов в текущей палитре цветов.
0E 0E 0E 0E 0E 0E 0E 0E 0E 38 38 0E 0E 38 38 0E 0E 38 28 0E 0E 28 38 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 38 0E 0E 0E 0E 38 0E 0E 0E 38 38 38 38 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E
Подробнее описывать, что такое тайлы я точно не буду, понадеюсь на вашу способность найти информацию в сети, уж больно велик объем информации.
Так как тайлов могут быть сотни и тысячи, то они будут занимать безумные объемы ПЗУ.
Совершенно нелогично передавать их со стороны хоста (так сделано в Gameduino первой ревизии).
Выход был цеплять карту памяти по SPI, SDIO пока нет.
Самым тяжелым, оказалось, поднять FatFS. Так как все пишется при помощи SPL библиотеки, а не HAL и CubeMX и возможно потому, что плохо проверил подключение питания для SD карты (к питанию щепетильны жутко они), в какой-то момент все просто заработало.
В конечном счете, со стороны хоста достаточно отправить название файла и сколько тайлов загрузить в RAM sGPU.
gpu.loadTile8x8("pcs8x8", TILE_SET_W, RAM_POS, 1);
// pcs8x8 – название файла с тайлами (указывать без расширения);
// TILE_SET_W – ширина набора тайлов в тайлах;
// это нужно для правильного расчета оффсетов внутри файла;
// RAM_POS – номер тайла в RAM, куда следует поместить загруженный тайл;
// 1 – номер загружаемого тайла в файле.
Согласитесь, что команда размером в 5 байт (это минимальный размер для загрузки одного тайла на момент написания статьи) куда меньше чем громоздкая библиотека FatFS съедающая значительную часть как оперативной, так и постоянной памяти, что совершенно не заметно на контролере уровня stm32.
Поддерживаются следующие размеры тайлов:
- 8×8 — самые маленькие, используют всего 64 байта RAM;
- 16×16 — средний тип, используют уже в 4 раза больше RAM (256 байт);
- 32×32 — самые большие, снова в 4 раза больше и требуют в 4 раза больше RAM (1024 байта).
Последний тип тайлов доступен только для pro версии, так как всего 10 таких съедают половину памяти mini версии, что весьма очевидно — неприемлемо.
Так же упомяну, что воспользовался хитростью при выводе их на экран. Помимо использования DMA (есть буфер для одного преобразованного тайла в RGB565), я проверяю индекс каждого тайла, и если он совпадает с предыдущим, то преобразование пропускается, и старый тайл выводится по новым координатам. Не смотря на такую трату памяти, это имеет неоспоримое преимущество, когда подряд на экран выводятся одинаковые тайлы. Время на преобразование отсутствует, поэтому тайл выводится почти сразу.
Ложка дегтя — на видео сверху код для хоста использует С версию библиотеки (идентичного можно достигнуть только если использовать STM32 под Arduino).
По ней можно заметить множество пустот во время выставления адресного окна. Эти пустоты по большей части переключение линии DC (выбор данных или команды, второй канал красная линия) и ожидание освобождения буфера SPI (ожидание передачи всех данных). С этим из-за особенностей дисплея почти ничего не сделаешь (только 9 бит режим или FSMC).
Создание тайлов
Прежде чем любой тайл попадет RAM sGPU, его необходимо предварительно загрузить с SD карты, но до этого его нужно туда поместить.
Создать тайл не так сложно, достаточно сделать следующее:
- В GIMP готовое изображение привести к индексированному режиму;
- экспортировать как raw data (стандартный R, G, B);
- переименовать файл по маске 8.3 (8 символов максимум на имя файла и 3 на расширение, это ограничение текущих настроек FatFS), я обычно использую расширение *.tle (sGPU сам подставляет расширение, так что хосту не придется его передавать);
- поместить файл *.tle на SD карту в корень;
- — ???;
- — PROFIT!!!
Не совсем, так как нужно создать само изображение тайла или набора тайлов (tileset). Среди множества инструментов для pixel art мною был выбран PixelEdit
(снова понадеюсь на вашу способность найти информацию в сети).
Сделав там изображение его достаточно экспортировать как *.png и скормить GIMP (как описано выше).
главное, не забывать передавать корректную, конечную ширину изображения в тайлах, иначе sGPU загрузит мусор! Также нужно соблюдать кратность размера тайлов к 8 (8×8, 16×16, 32×32). Это справедливо и для набора тайлов.
Спрайты
Второе, что должен уметь sGPU: создавать спрайты из тайлов.
Спрайт это объединение тайлов в группу (если упрощенно).
Спрайт Марио состоит из четырех тайлов размером 8×8.
Количество спрайтов для mini — 56, тогда как для pro версии немногим больше — 63.
Их количество для версий mini и pro вычислялось по-разному. Так для mini это сумма половины максимального количества тайлов каждого типа, тогда как для pro это сумма четвертей 63 спрайта хватит всем.
Спрайты могут состоять из любого, но одинакового типа тайлов, т.е. нельзя сделать спрайт из тайлов 8×8 и 16×16 одновременно, но, можно два спрайта из тайлов 8×8 и 16×16. Каждый спрайт состоит из четырех тайлов (максимум) спрайта 64×64 хватит всем, для всего остального есть вывод *.bmp.
Возможны следующие комбинации размеров:
- 1×1;
- 1×2;
- 2×1;
- 2×2,
где первая цифра это высота в тайлах, а вторая ширина в тайлах.
Помимо этого каждый спрайт содержит координаты в пикселях, где он будет рисоваться (может помочь для расчета столкновений двух спрайтов, да это тут тоже есть).
typedef struct {
uint16_t posX; // \__ координаты где выводить на экран
uint16_t posY; // /
uint8_t type; // размер и тип спрайта ( 1x1, 1x2… 8x8, 16x16…)
uint8_t visible;// должен выводится на экран или нет
uint8_t tle[4]; // индексы используемых тайлов
} sprite_t;
Тайловый фон
Третье, что должен уметь sGPU.
В памяти содержится массив из 1200 байт (40×30 тайлов) с индексами тайлов только размером 8×8 (в будущем может быть будет возможность выбора).
Тайловая карта хранится в RAM и грузится с SD карты (расширение *.map).
На текущий момент инструменты, которые позволили бы создавать файл карты, отсутствуют.
Единственный способ — экспортировать карту из PixelEdit в *.txt, стирать все начало до индексов тайлов и отдавать в tilemapConverter (написанную мною на Qt по принципу лишь бы было). Рекомендую написать свою версию конвертера, так как мой кривой, но если он вам нужен, то пишите в комментариях.
Пример тайлового фона можно увидеть во время подачи питания.
Теперь самое неприятное. Спрайты и тайловый фон используют одни и те же тайлы 8×8. Это означает, что для фона и спрайтов необходимо использовать разные тайлы (да, которых не так много на mini версии).
Распределение памяти
Очевидно, что RAM память stm32 ограничена и много в нее не впихнешь. По неведомым мне уже причинам решил отдать память под тайлы для mini — 7680 байт, для pro — 40960 байт.
Но мне не хотелось всю эту память отдавать под один тип тайлов.
Проблема возникла с распределением этой памяти, точнее какое количество каких тайлов использовать.
Ранее, когда читал статьи про старые консоли, натыкался на то, что в зависимости от консоли всегда не хватало какого либо типа тайлов.
Поэтому учтя кучу возможных сценариев, было решено распределить так:
- 80 тайлов 8×8, как самый используемый ресурс;
- 10 тайлов 16×16, к сожалению их мало, так как это все, что осталось от свободной памяти.
Не стоит забывать про более крупный контроллер, в котором 64 кб RAM, для него распределение уже будет следующим:
- 160 тайлов 8×8, снова как самый используемый ресурс;
- 80 тайлов 16×16, на этот раз их уже куда больше;
- 10 тайлов 32×32, их снова мало, но на этот раз стоит учитывать их размер.
До сих пор сомневаюсь в правильности распределения (динамика совсем не выход).
Остальное можно найти в файле «RAMmath.txt».
Цветовая палитра
Для уменьшения используемой памяти, тайлы используют не цвет целиком для каждого пикселя (жаль, нет столько памяти), что значительно бы увеличило производительность, а всего-навсего индексы цветов из цветовой палитры. Это решение снизило требование к памяти вдвое и сделало возможным использование трюка по смене палитры и выводу тех же тайлов (не нужно загружать новые).
Подобный трюк использовался во всех старых консолях. Самый заметный и известный
В игре Super Mario Bros. тайлы для облаков и кустов идентичны, меняются только цвета палитры. Так же как и палитра Марио и Луиджи.
Как самая простая палитра была использована палитра NES, но была обнаружена ее недостаточность.
Вооружившись GIMP и встроенным колориметром в системе, расширил цветовой набор до 76 (4 черных цвета для резерва).
Не являясь человеком с идеальным цветовосприятием, я, конечно же, не смог сделать адекватную палитру из
Буду премного благодарен, если кто сможет найти или сделать более адекватную палитру на 256 цветов (с одним цветом для будущего альфа канала).
Если нужно будет использовать другую палитру, ее можно загрузить с SD карты.
В RAM sGPU есть целых 512 байт под это (256 цветов по два байта на цвет, прямо как в GameBoy Advance, но только в RGB565).
Экспортировать палитру из GIMP очень просто, достаточно экспортировать любое индексированное изображение (как raw data), и GIMP рядом с файлом изображения создаст еще один с расширением *.pal. Его и нужно поместить на SD карту (не забыв про размер имени до 8 символов).
Протокол
Протокол писался так, чтобы размер любой команды был как можно меньше. Строго заданные размеры и параметры команд, и полное отсутствие контроля корректности команды. Все для максимальной скорости выполнения команд (ведь все выполняется программно, если помните).
Любая команда начинается с байта ее кода. Конечный размер всей команды зависит от ее кода. Так для заполнения всего экрана одиночным цветом нужно только 3 байта, но для того чтобы нарисовать треугольник уже 15 байт.
Коды команд аккуратно разбиты по диапазонам (секциям), есть большое количество не использованных кодов. Всего в текущей реализации протокола доступно 255 команд из них использована даже не половина (есть свободное поле для творчества).
Сторона хоста
На стороне хоста sGPU выглядит как обычный экран, но только по UART интерфейсу.
Не смотря на большой входной буфер для команд sGPU, все равно есть риск его переполнения. Поэтому есть два варианта защиты (по факту 1):
- Программный ответ хосту. Медленный, но широко поддерживаемый вариант (можно использовать хоть USB <-->UART и всего 3 линии для обмена);
- Аппаратный ответ хосту. Значительно быстрее, так как используется GPIO, и хосту достаточно следить за состоянием этого пина и меньше шансов, что буфер будет переполнен.
Есть две версии библиотек под Arduino.
Версия библиотеки, полностью заточенная под Arduino (на C++), конечно хоть и обладает безумным количеством преимуществ, но у нее есть один огромный и побочный недостаток — она ну очень толстая и медленная (см. раздел тайлов). Иными словами пустой скетч,
#include
STMsGPU gpu;
void setup() {}
void loop() {}
съедает 1634 байт ROM и 217 байт RAM, и это без метода синхронизации с sGPU! Более того, если использовать STM32, то эти цифры будут уже в 10 (как минимум) раз выше — 16732 байта ROM и 3960 байт RAM!
Поэтому есть версия на С. К сожалению, пока заточенная под atmega328p (и ей подобным) и лишенная почти всех достоинств Arduino. Эти недостатки компенсируются как увеличенной скоростью работы, так и значительно меньшим размером: 922 байт ROM и 46 байт RAM (можно сделать и меньше конечно) только уже с синхронизацией.
Самое главное если вы использовали библиотеку для экранов ili9341 от Adafruit, то не придется переписывать почти ничего! Имеется почти полная совместимость со всеми методами.
Недостатки
Нет большого количества памяти, из-за этого невозможно сделать фрэймбуфер, так как требуемое количество памяти для одного кадра всего экрана будет занимать 180 килобайт. Из-за этого на текущий момент нет альфа канала для тайлов и спрайтов.
Медленно. Против FPGA (FT800, RA8875 и им подобным) простой контроллер на 72МГц не имеет ни малейшего шанса противостоять. Однозначно, что запуск FSMC на pro версии поможет ситуации, но не столь сильно как хотелось бы.
Исходники
Все исходные коды и даже прошивка для stm32 есть на Github (если вдруг у вас нет программатора, то можно прошить через STM32 Flash Loader Demonstrator по UART1).
Вот ссылка на репозиторий проекта (надеюсь не получу бан/mute аккаунта из-за трафика хабраэффекта, если такое все еще есть).
В исходниках вы найдете проект для sGPU (под IAR ARM версии 7.40), код для AVR и Arduino (тавтология та еще выходит), так же там есть описания по подключению и многое немногое другое.
В целом проект очень сырой, несмотря на уже имеющиеся возможности.
Не так много шагов осталось до консоли с аудио сопроцессором на YM2149F.
Если есть вопросы, задавайте, буду рад ответить.
Так же могу написать еще, осветив какие либо моменты куда подробнее (только дайте знать надо это или нет). В одну статью, как уже писал выше, все не впихнуть.