Перенос LittlevGL на монохромный дисплей
Использование возможностей LvGL в проекте с низким уровнем ресурсов
Моя работа приближает меня к небольшим встроенным системам с минимальным пользовательским интерфейсом: несколько светодиодов или несколько цифровых дисплеев. Однако время от времени мне приходится работать с маленькими монохромными экранами, что вызывает потребность в рабочей, хотя и простой библиотеке графического интерфейса.
В этой области доступные ресурсы, мягко говоря, бесплодны. Большинство результатов, найденных в Интернете, относятся либо к проприетарным библиотекам, распространяемым поставщиками ЖК-дисплеев с несуществующей поддержкой, либо к сомнительным генераторам кода, которые не могут предоставить что-либо, кроме нескольких поддерживаемых драйверов, если вообще. Предыдущее решение моей компании представляло собой взломанный и модифицированный API, справочное руководство которого было давно потеряно во времени. Единственная надежная альтернатива запеканию собственного графического интерфейса - LittlevGL.
LittlevGL (или LvGL) - это графическая библиотека с открытым исходным кодом для встраиваемых систем. Он в основном ориентирован на цветные TFT-дисплеи, но в принципе может работать и с монохромными ЖК-дисплеями. Мой первый подход был обескуражен, поскольку его основной целью был цвет и размер скомпилированного результата (около 250 КБ двоичного кода для архитектуры PIC). Однако, немного поигравшись, я могу сказать, что очень доволен и хочу сделать небольшой урок о том, как его использовать.
Для справки: в моем проекте используется микроконтроллер PIC24, управляющий монохромным резистивным сенсорным ЖК-дисплеем с разрешением 240х128 пикселей; в принципе, однако, весь код абстрагирован от реального оборудования, и большинство объяснений в целом действительны. Все написанное здесь основано на обширной документации LittlevGL.
Монохромная конфигурация
LittlevGL полностью написан на C и использует макросистему для настройки своих параметров и функций. В корне репозитория есть конфигурация шаблона, которую нужно скопировать в файл с именем lv_conf.h
и включить во все другие источники. Вариантов много, но для простоты мы можем сосредоточиться лишь на некоторых из них:
/* Maximal horizontal and vertical resolution to support by the library.*/
#define LV_HOR_RES_MAX (240)
#define LV_VER_RES_MAX (128)
/* Color depth:
* - 1: 1 byte per pixel
* - 8: RGB233
* - 16: RGB565
* - 32: ARGB8888
*/
#define LV_COLOR_DEPTH 1
/* Enable anti-aliasing (lines, and radiuses will be smoothed) */
#define LV_ANTIALIAS 0
/*1: Enable the Animations */
#define LV_USE_ANIMATION 0
/* Enable GPU optimization */
#define USE_LV_GPU 0
Установка разрешения по горизонтали и вертикали является обязательной для любого дисплея; глубина цвета должна быть 1, так как мы работаем с монохромным, и, кстати, мы также отключаем сглаживание, графический процессор и анимацию (они не понадобятся в таком сокращенном графическом интерфейсе).
Самая важная настройка в этом случае - LV_COLOR_DEPTH
. Он настраивает размер для каждого пикселя, который в данном случае составляет всего 1 бит, что позволяет сэкономить столь необходимую оперативную память.
Инициализация библиотеки
Давайте посмотрим, как на самом деле инициализировать библиотеку. Следующий код управляет всеми необходимыми операциями:
static uint8_t gbuf[8*240]; static lv_disp_buf_t disp_buf; lv_disp_buf_init(&disp_buf, gbuf, NULL, 8*240); lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); /*Basic initialization*/ disp_drv.buffer = &disp_buf; /*Set an initialized buffer*/ disp_drv.flush_cb = my_flush_cb; /*Callback*/ disp_drv.set_px_cb = my_set_px_cb; /*Callback*/ disp_drv.rounder_cb = my_rounder; /*Callback*/ lv_disp_t * disp; disp = lv_disp_drv_register(&disp_drv); /*Register the driver and save the created display objects*/ /*Input device*/ lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); /*Basic initialization*/ indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = my_input_read; /*Register the driver in LittlevGL and save the created input device object*/ lv_indev_t * my_indev = lv_indev_drv_register(&indev_drv);
Давайте развернем его построчно.
gbuf
- это наш кадровый буфер, область памяти, в которой происходит рисование и графические операции. У меня экран 240х128 пикселей, с пиксельными линиями, сгруппированными по горизонтали в байты: итоговый общий размер будет 240 * 128/8 = 240 * 16.
Обратите внимание, что для буфера необязательно, чтобы он занимал весь экран одновременно: при необходимости я могу использовать меньшую область, и LvGL удобно разделит операции графического интерфейса, которые не помещаются там, на несколько меньших частей. Тем не менее, у меня есть достаточно свободной оперативной памяти.
lv_disp_buf_init
инициализирует структуру disp_buf
, зная, что мы используем единственный буфер (gbuf
) шириной 8 * 240 байт.
Структура disp_drv
содержит информацию, относящуюся к интерфейсу без драйвера дисплея; подробнее об этом позже. После того, как он правильно заполнен обратными вызовами, мы регистрируем его с помощью функции lv_disp_drv_register
.
Затем мы регистрируем устройство ввода: это LV_INDEV_TYPE_POINTER
, сенсорный экран или мышь (последнее здесь). my_input_read
просто возвращает текущие координаты касания, так что LvGL может знать, где пользователь щелкает:
bool my_input_read(lv_indev_drv_t * drv, lv_indev_data_t*data) { data->point.x = Touch_Coord[0]; data->point.y = Touch_Coord[1]; data->state = f_touch_detected ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL; return false; /*No buffering now so no more data read*/ }
В нем я заполняю структуру координатами, которые я читаю.
Время выполнения
Помимо загрузки, LvGL также требует, чтобы для работы периодически вызывались две функции: lv_task_handler
и lv_tick_inc
.
lv_task_handler
- вот как на самом деле работает LvGL. lv_tick_inc
- это то, как он узнает, что время прошло (в частности, миллисекунды).
Чтобы соответствовать этим требованиям, достаточно вызвать их обоих в основном цикле. Период, с которым вызывается lv_tick_inc
, должен быть передан функции как единственный аргумент.
while (1) { lv_task_handler(); lv_tick_inc(1); delay_ms(1); }
В качестве альтернативы они могут быть вызваны в подпрограммах прерывания, но в этом случае нужно позаботиться о том, чтобы lv_tick_inc
находился в обработчике с более высоким приоритетом из соображений параллелизма. Фактически, увеличение числа тиков прерывания является хорошей практикой, поскольку оно более точное; в предыдущем примере время, затраченное lv_task_handler
, могло бы сбить с пути миллисекундный период. Для целей этого примера этого достаточно.
После этих функций библиотека готова к использованию. Однако, прежде чем показывать пример, давайте углубимся в обратные вызовы драйвера дисплея.
Портирование LvGL
Один из самых привлекательных аспектов LvGL - это то, насколько хорошо он управляет интеграцией с драйвером дисплея. Нельзя ожидать найти общий API во встроенных системах, поэтому лучшим решением будет просто предоставить разработчику задачу по подключению оборудования и графического интерфейса: LVGL предоставляет несколько обратных вызовов, которые сигнализируют, когда и что показывать на экране; как это сделать, зависит от вас.
Некоторые из этих крючков имеют смысл только в цветной среде, поэтому для монохромного ЖК-дисплея нам нужно определить всего три функции:
flush_cb
: функция, которая сигнализирует вашему приложению обновить часть экрана некоторыми данными.rounder_cb
: обратный вызов с просьбой «округлить» область, которую необходимо обновить. Подробнее об этом позже.set_px_cb
: просит отрисовать пиксель в буфер
Здесь я покажу свои реализации, но имейте в виду, что они будут сильно отличаться в других настройках.
flush_cb
Возможно, самый важный обратный вызов для LvGL, он сигнализирует о том, что на самом деле должно быть отправлено на экран. LvGL оптимизирует рисование, обновляя только те области, которые изменились, а flush_cb - это способ сказать драйверу, что нужно продолжить.
Он получает 3 аргумента:
lv_disp_drv_t *drv
: указатель на текущую сконфигурированную структуру драйвера дисплеяlv_area_t *area
: набор координат, ограничивающих область экрана, которая должна быть обновленаlv_color_t *color_p
: указатель на буферную память, в которой отрисовываются графические объекты. Он содержит данные, которые должны быть выведены на дисплей.
Первый аргумент можно игнорировать; область сообщает вам, где вы должны обновить экран, а color_p содержит данные для очистки.
Это моя реализация обратного вызова:
void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { int row = area->y1, col; unsigned int address = (unsigned int)row * HW_COLUMNS + area->x1/8; uint8_t *buffer; int linelen = (area->x2 - area->x1)/8; buffer = (uint8_t*) color_p; for (row = area->y1; row <= area->y2; row++) { flush_one_row(address, buffer, linelen); buffer += linelen+1; address += HW_COLUMNS; } lv_disp_flush_ready(disp_drv); }
Обратите внимание, что:
color_p
- указатель наlv_color_t
буфер, но когдаLV_COLOR_DEPTH
равно 1, эта структура сворачивается в однобайтовое объединение; таким образом, его можно безопасно преобразовать вuint8_t*
для передачиcolor_p
будет указывать на область внутри буфера, который мы использовали для инициализации отображения LvGL (gbuf
). Однако нет никакой гарантии, где это будет: виджеты не отображаются вgbuf
в каком-либо конкретном порядке или позиции, это работает аналогично куче распределения.area
с другой стороны, сообщает мне, где именно промывка должна обновлять экран, поэтому я знаю адрес, по которому обновляется оперативная память ЖК-дисплея.- Когда функция завершена, должна быть вызвана очистка
lv_disp_flush_ready
. Здесь он используется в конце обратного вызова, но может сигнализировать о завершении более длительной процедуры (например, передача DMA или отложенное обновление).
rounder_cb
Это немного сложно. Я упоминал ранее, что в монохромных ЖК-дисплеях пиксели часто сгруппированы в байты; кроме этого, они могут принимать обновления в свою оперативную память только в специальных форматах (например, в определенных пакетах или путем записи на страницы памяти фиксированного размера). Из-за этого LvGL может использовать области, которые не имеют смысла в процессе обновления.
LvGL может захотеть, например, обновить прямоугольник, состоящий из координат x = (3,14) и y = (1,4). Если пиксели сгруппированы по байтам, невозможно отправить на экран нечетное число из 11 (14–3) бит. Решение состоит в том, чтобы округлить эту область до x = (0,16) и y = (1,4) (с учетом выравнивания пикселей по горизонтали).
rounder_cb
имеет именно такую функцию: LvGL уведомляет код следующего обновления экрана и просит округлить его до минимально возможного размера.
void my_rounder(struct _disp_drv_t * disp_drv, lv_area_t *a) { a->x1 = a->x1 & ~(0x7); a->x2 = a->x2 | (0x7); }
Чтобы округлить горизонтальный байт, мне просто нужно переместить левую координату x на меньшее кратное 8 (логические и с 0b11111000
), а правое - на большее кратное 8 (логическое или с 0b00000111
). Эти значения также будут отправлены на flush_cb
.
set_px_cb
Самый большой недостаток LvGL (для моих целей) заключается в том, что он изначально не управляет однобитными значениями пикселей. Он в основном используется для цветного дисплея, где каждый пиксель имеет ширину не менее одного байта, но было бы абсурдно использовать буфер в 8 раз больше, чем необходимо, с монохромным ЖК-дисплеем. Вместо того, чтобы интегрировать эту функциональность, LvGL делегирует ее вам.
Этот обратный вызов требует, чтобы драйвер установил или очистил один пиксель с учетом буфера и координат; Ваш код должен правильно изменить память, в конечном итоге учитывая выравнивание пикселей.
void my_set_px_cb(struct _disp_drv_t * disp_drv, uint8_t * buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y, lv_color_t color, lv_opa_t opa) { buf += buf_w/8 * y; buf += x/8; if(lv_color_brightness(color) > 128) {(*buf) |= (1 << (7 - x % 8));} else {(*buf) &= ~(1 << (7 - x % 8));} }
disp_drv
- это обычная структура драйвера дисплеяbuf
- это диапазон памяти, в котором LvGL хочет, чтобы вы обновили пиксель.buf_w
- ширина указанного диапазона; координата y указывает его высотуx
иy
- координаты пикселя для обновления.color
- в монохромной настройке - полный или чистыйopa
- значение непрозрачности, здесь не используется по очевидным причинам.
Все эти арифметические и побитовые операции с указателями предназначены для установки одного бита в правильный горизонтальный сегмент / байт.
Небольшой пример
LvGL предоставляет множество практических руководств, примеров и демонстраций. К сожалению, многие из них выйдут из строя или будут работать неправильно в монохромной среде: для меня либо экран, либо доступная память MCU были слишком малы. Это небольшой пример с кнопкой и меняющейся меткой:
lv_obj_t *label2, *label1; static void btn_event_cb(lv_obj_t * btn, lv_event_t event) { if(event == LV_EVENT_RELEASED) { lv_label_set_text(label1, "RELEASED"); } else if (event == LV_EVENT_PRESSED) { lv_label_set_text(label1, "CLICKED"); } } int main (void) { ConfigureOscillator(); InitializeSystem(); lv_init(); static lv_disp_buf_t disp_buf; lv_disp_buf_init(&disp_buf, gbuf, NULL, 8*240); lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.buffer = &disp_buf; disp_drv.flush_cb = my_flush_cb; disp_drv.set_px_cb = my_set_px_cb; disp_drv.rounder_cb = my_rounder; lv_disp_t * disp; disp = lv_disp_drv_register(&disp_drv); lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb =my_input_read; lv_indev_t * my_indev = lv_indev_drv_register(&indev_drv); lv_obj_t * scr = lv_disp_get_scr_act(NULL); lv_theme_t * th = lv_theme_mono_init(0, NULL); /* Set the mono system theme */ lv_theme_set_current(th); /*Create a Label on the currently active screen*/ label1 = lv_label_create(scr, NULL); lv_label_set_text(label1, ""); lv_obj_set_pos(label1,30, 30);// position, position); /*Create a button on the currently loaded screen*/ lv_obj_t * btn1 = lv_btn_create(scr, NULL); lv_obj_set_event_cb(btn1, btn_event_cb); /*Set function to be called when the button is released*/ //lv_obj_align(btn1, label2, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 20); /*Align below the label*/ lv_obj_set_pos(btn1, 30, 50); /*Create a label on the button (the 'label' variable can be reused)*/ label2 = lv_label_create(btn1, NULL); lv_label_set_text(label2, "Click me!"); lv_obj_set_size(btn1, 100, 20); while (1) { lv_task_handler(); lv_tick_inc(1); delay_ms(1); } }
После инициализации, как я уже объяснил, наиболее важными частями являются установка монотемы - единственной используемой в монохромной среде - и создание реальных виджетов. Событие кнопки работает из-за обратного вызова сенсорного ввода, который предоставляет координаты касания библиотеке, управляя всей системой ввода.
Дополнительные замечания
Как я уже упоминал во введении, сначала я не мог использовать LvGL из-за того, что он занимал память. В этом руководстве я использовал микроконтроллер PIC24EP512GP206 с 512 КБ флеш-памяти: только LvGL занимает половину этого объема.
С тех пор я поигрался с параметрами конфигурации и обнаружил, что многие неиспользуемые компоненты можно отключить для экономии места. Помимо всех тем и шрифтов, LvGL позволяет удалить каждый виджет с опциями макроса: если я не использую календарь на дисплее 128x64, я могу отключить его, установив LV_USE_CALENDAR
на 0, и он не займет драгоценную вспышку. объем памяти. В файле шаблона конфигурации есть все значения по умолчанию.
Выбрав только то, что я планирую использовать (и оставив прилично открытыми мои варианты), я достиг разумного двоичного вывода 150 КБ, и я с нетерпением жду возможности использовать LvGL во всей моей будущей работе!