Сводный список записей блога

--->>>> Сводный список записей блога <<<<---

02 августа 2022

Графика для микроконтроллера - 3 уровня абстракции.

 


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

Но по сути - это все отображение дискретной информации, ограниченной возможностями устройства отображения.
Светодиод может гореть, может не гореть, может мигать. 7-сегментный дисплей может отобразить любую цифру и определенное количество букв. Алфавитно-цифровой дисплей может отобразить любой текст в соответствии с "вшитым" в него знакогенератором. Если дисплей поддерживает загрузку пользовательских символов - тут немножко легче. Можно попытаться добавить недостающие символы. Даже нарисовать какое то изображение при помощи псевдографики.
Но есть такие задачи, когда таких средств  отображения недостаточно.

И тогда на сцену выходят графические дисплеи.
Их много разных. Есть черно-белые, есть цветные. Есть маленькие, есть огромные.

Использование таких дисплеев - сплошные плюсы. Можно отобразить картинку. Можно нарисовать график. Можно вывести текст любым шрифтом и в любом виде. 
Всё, до чего дотянется фантазия разработчика (но в пределах возможностей примененного дисплея).

Но если есть плюсы, то должны быть и минусы. 

Это и различные интерфейсы связи дисплея и МК (хотя если без экзотики, то это параллельный 4-, 8- или 16-битный или последовательный - SPI или I2C- интерфейс).
Это различные требования к скорости передачи данных и тайминги.
Это различные форматы представления данных в видеобуфере дисплея.
Это различная глубина цвета.
Это огромное количество пикселей, которыми нужно управлять и, в отдельных случаях, еще и держать значения всех пикселей в ОЗУ микроконтроллера.

*Тут и далее под словом "дисплей" я подразумеваю стекло с контроллером и подсветкой, как единое устройство.

Так, у монохромных дисплеев пикселю на экране обычно соответствует 1 бит в видеобуфере.
И организация этого видеобуфера может быть различная....
Это может быть горизонтальная организация памяти - одному байту видеобуфера соответствует строчка из 8 пикселей, вертикальная - один байт видеопамяти соответствует столбику из 8 пикселей. Позиция старшего бита в этом столбике/строчке тоже может быть различная у разных дисплеев.
Некоторые дисплеи вообще имеют 16-битную организацию видеобуфера.
А дисплеи с разрешением 128*64 пикселя на контроллере ks0107/ks0108 - это вообще два независимых контроллера 64*64 пикселя со своими сигналами выбора кристалла CS1 и CS2.  И эти контроллеры просто подключены к одному стеклу.

Минус такой организации видеобуфера - это невозможность адресации к одному пикселю. Запись байта в дисплей управляет сразу 8 пикселями. А половина простых дисплеев просто не умеет отдавать наружу содержимое видеобуфера. Пример - тот же "дисплей от Нокии". Часть дисплеев позволяет читать видеобуфер. Часть дисплеев позволяет читать видеобуфер только при параллельном подключении. 

Соответственно, нужно либо как то оптимизировать создание графики под примененный дисплей так, что бы можно было отправлять данные прямо в дисплей, либо делать какие то буферы в оперативной памяти.
Можно вообще организовать в оперативной памяти копию видеобуфера, выполнять отрисовку изображения в этой копии, а потом отправлять этот видеобуфер в дисплей.
Для буфера 128*64 пикселя нужно 1024 байта ОЗУ, для "нокиевского" разрешения 84*48 пикселей - всего 504 байта...

Совершенно противоположная ситуация с цветными дисплеями. Там каждый пиксель в видеобуфере представлен минимум одним байтом. В доступных радиолюбителям недорогих дисплеях контроллер обычно поддерживает до 6 бит на цвет. Т.е. пиксель - это 18 бит. RGB666.

Для облегчения взаимодействия с микроконтроллерами, которые с максимальной скоростью оперируют данными с размерностью, кратной 8 битам, дисплеи поддерживают слегка усеченную 16-битную цветовую модель RGB565, когда для зеленого цвета оставлено 6 бит, а красный и синий цвета усечены до 5 бит (хотя внутри дисплея данные по прежнему хранятся в в 18-битном формате). 
Плюс в большинстве цветных дисплеев реализован так называемый оконный вывод. Когда несколькими командами определяется область экрана (окно, страница) и дальше в дисплей отправляются сплошным потоком данные про цвет пикселей и эти данные будут заполнять исключительно эту область. 
Размер области может быть любой, хоть 1х1 пиксель.

Ну и в большинстве доступных дисплеев простой команды присвоения определенному пикселю нужного нам цвета - нет. (Ну или может я чего то не знаю, но я в даташитах этого не нашел)...
Т.е. что бы изменить цвет пикселя, нужно определить окно размерами 1х1 пиксель по нужным нам координатам и записать туда этот наш цвет.
определение окна - это отправка команды задания левой и правой границ окна (1 байт команды, 2 слова/4 байта координат), потом отправка команды задания верхней и нижней границ окна - тоже 5 байт, потом отправка команды перевода дисплея в режим записи цветовых данных в заданное окно - 1 байт, ну и наконец то - отправка 2 байт цвета. Итого - 13 байт.

Что бы прочитать значение пикселя - нужно выполнить все почти то же самое. Определить окно, а потом дать команду чтения цветовых данных из окна. И читать данные. 
Хотя чтение данных нужно гораздо реже. Например, при отрисовке примитивов, которые требуют информацию об исходном цвете пикселя - спрайты с прозрачностью, сглаживание линий...
Альтернативный вариант - рисовать примитивы в буфере, потом этот буфер выплевывать в дисплей. Но тут есть ограничение по размеру ОЗУ у недорогих микроконтроллеров. Та же Мега328 - это 2 килобайта ОЗУ. В принципе, буфер для 32*16 16-битных пикселей можно разместить. Как раз 1 килобайт на буфер, 1 - на всю остальную программу.

С учетом всего вышесказанного можно поступить просто - для каждого вида дисплея писать свои методы формирования картинки и вывода ее на экран. Да, это будет эффективно (особенно для монохромных дисплеев) и быстро. Но - неудобно.

А можно слегка пожертвовать скоростью и сделать универсально.
Создается графическая библиотека (да, я знаю, что это очередной велосипед, в интернете куча таких библиотек), которая совершенно не в курсе, какой у нас дисплей. Ну вот абсолютно не в курсе. 
Для этой библиотеки нужна одна единственная функция от нас - присвоить пикселю по координатам X и Y значение цвета Color.
А как мы это делать будем - библиотеку не волнует.

И мы получаем верхний слой абстракции - графическая библиотека. Программа скармливает библиотеке высокоуровневые команды рисования графических примитивов. А библиотека при помощи функции рисования пикселя эти примитивы и отрисовывает. Всё.

Следующий этап - преобразовать команду установки цвета пикселя в что то, что понятно конкретно взятому дисплею. Это средний слой абстракции - драйвер управления дисплеем.
Драйвер уже знает, что у нас за дисплей, какая у него организация видеобуфера, цветной дисплей или монохромный....
Для монохромных дисплеев, с учетом возможности адресации индивидуально любого пикселя, а не набора из 8/16 пикселей, драйвер организует у себя копию видеобуфера.
Отправка этой копии в дисплей может выполняться как автоматически, по факту изменения данных, или по таймеру, так и отдельной командой. Это уже как автор драйвера решит.

Для дисплея на контроллере ST7920, из-за его особенностей, я реализовал отслеживание изменений по таймеру и автоматическую отправку данных в дисплей. Для остальных монохромников и OLED-ов - обновление данных у меня в драйверах выполняется по отдельной команде.

Для цветных дисплеев организовывать полноценный видеобуфер в ОЗУ в недорогих МК слишком накладно или совсем невозможно (128*128 16-битных пикселей - это 32 кб, 320*240  - уже 150 кб), соответственно, драйвер должен отправлять пиксель непосредственно в дисплей.
А это для цветных дисплеев, как писалось выше, несколько накладно по объему отправляемых в дисплей данных. К сожалению, рисование графических примитивов - линий, не закрашенных окружностей, возможно только попиксельно.
А вот закрашенные прямоугольники, окружности, вывод текста и небольших изображений (спрайтов) прекрасно оптимизируется при помощи оконных функций дисплея.
Для таких случаев, когда дисплей поддерживает оконные функции, драйвер дисплея должен предоставить графической библиотеке еще две функции - функцию задания координат окна вывода с последующим включением режима записи цветовых данных в это окно и функцию отправки цветовых данных одного пикселя.

Т.е. драйвер дисплея - это некий транслятор хотелок графической библиотеки в телодвижения, понятные дисплею. И тут  мы плавно подходим к физическому подключению дисплея к микроконтроллеру. По идее, драйверу должно быть абсолютно все равно, как подключен дисплей к МК, по SPI,  или по I2C, или по параллельному интерфейсу. Задача драйвера -  отправить в дисплей данные так что бы дисплей понял, что данные отправляются именно ему, какие это данные - команда или информация про цвет пикселя.
В некоторых дисплеях есть пин выбора дисплея (CS - ChipSelect), в некоторых нету.... В некоторых есть пин выбора типа информации (D/C - Data/Command) - данные или команда.. В некоторых дисплеях еще есть пин сброса дисплея (RST). Драйвер должен об этом знать и уметь сказать микроконтроллеру, какой уровень нужно установить на этих пинах. 
Так же драйвер должен уметь приказать микроконтроллеру отправить один или несколько байтов в дисплей.

Но драйвер не обязан знать, к каким пинам микроконтроллера и по какому интерфейсу подключен дисплей!
Драйверу должны быть предоставлены функции установки нужного уровня на служебных пинах CS, D/C, RST и функция отправки данных в дисплей.

А уже физическая реализация этих функций - ложится на нижний слой абстракции - драйвера физического подключения дисплея.

Я обычно описываю функции отправки данных и управления служебными пинами в отдельной паре файлов - HAL.h/HAL.c.
Эти файлы в моих проектах описывают и реализуют всё физическое взаимодействие с абсолютно всей периферией микроконтроллера - как внешней, так и внутренней. При этом HAL.h предоставляет основной программе абстрагированные от физической периферии функции управления и работы с периферией. Как, например, процедура установки необходимого уровня на пине дисплея. А о том, к какому порту МК подключен  нужный нам пин дисплея, знает только код процедуры в HAL.c.

Тем самым достигается легкая переносимость кода между разными МК и даже семействами МК, лёгкая смена дисплея без муторной переделки кода. При переводе устройства на другой тип дисплея с таким же разрешением нужно поменять только драйвер управления дисплеем и, возможно, переписать драйвера физического подключения.

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

Резюме.

Разделение прикладной программы и дисплея тремя уровнями абстракции:

  • верхний слой абстракции - графическая библиотека
  • средний слой абстракции - драйвер управления дисплеем
  • нижний слой абстракции - драйвера физического подключения дисплея

позволяет: 

  • работать с графикой высокого уровня, не задумываясь об физике дисплея
  • легко менять дисплеи, не затрагивая основной функционал программы
  • легко переносить код работы с дисплеем на другие микроконтроллеры и даже на другие семейства микроконтроллеров

Но за все нужно платить, такое абстрагирование может отрицательно влиять на быстродействие работы с графикой.


Следующая статья - Сжатие изображений или Как запихнуть большую картинку в маленький микроконтроллер

1 комментарий:

  1. Спасибо, как всегда, очень позновательно.
    Терпеливо жду ваших новых устройств.

    ОтветитьУдалить

======= !!! ВНИМАНИЕ !!! ======================================================================
Гугл умный и боится спама. Поэтому иногда ваши комментарии Гугл отправляет мне на премодерацию. Отправлять или нет - решаю не я, а алгоритмы Гугла. Если ваш комментарий не появился сразу, значит я получу уведомление и опубликую ваш комментарий через некоторое время. Я стараюсь это делать достаточно оперативно.