В стандартном EDK нет поддержки графического интерфейса. Есть только из коробки пиксельный/текстовый вывод и TUI для HII интерфейса. А хочется капельку красоты и человеческий GUI. Дак добавим же! Даже не ради чего-то конкретного, а просто JUST FOR FUN!

Все элементарно: берем EDK, хватаем красивый язык для описания GUI и какую-нибудь не менее красивую библиотеку для вывода. Соединяем все это вместе и плавно перемешиваем на медленном огне. Все, готово, можно есть. Ну что, попробуем?

Дисклеймер: в статье нет каких-либо тайных или сильно новых знаний, она просто показывает путь самурая по портированию проекта в EFI.

Выбор

В качестве библиотеки GUI будем использовать библиотеки под названием LVGL. Просто потому что она мне нравится. При своей минималистичности в ней богатый функционал и есть куча всего, и плюс все это еще и активно развивается. Можно даже прямо в браузере посмотреть какие крутые штуки делают на ней разработчики.

А еще она портирована на micropython, который, в принципе, подходит под “красивый язык”. Тут сразу может возникнуть вопрос: а почему не портировать его на более полный “python 3”? Ответ прост: сам micropython внутренне несколько по другому устроен по сравнению со своим старшим братом и требует сложной адаптации, которой бы не хотелось. Можно прочитать про это тут.

Едем дальше! Что там у нас в EDK?! Еще в недалеком в 2018 в EDK портировали micropython, но судя по github-у он с тех пор этот проект не сильно и развивался. Мы люди не гордые - возьмем что есть и пойдем дальше. Как говорится: “работает - не трожь!”.

Итак, есть Micropython под EFI и есть LVGL под micropython под Linux/микроконтроллеры. Надо их соединить.

Соединяем!

Первым делом перетащим существующий micropython на новый EDK, хотя бы потому, что там много чего поменялось за последнее время и есть более классный эмулятор EFI, гораздо лучше чем раньше. Тут все просто: берем старое, Ctrl+C, берем новый EDK, Ctrl+V и пытаемся собрать. Не собралось - подредактировали - собралось. Там всего пару мелочей вроде отсутствующих в новом EDK небезопасных AsciiStrnCpy/AsciiStrCat и прочие мелкие неприятности. Собралось - отлично - едем дальше!

Далее, добавляем сам LVGL в micropython. Так как сам LVGL написан на С, то логично, что для языка micropython нужна некоторая обертка. Для генерации этой обертки существует проект lv_micropython. Суть проекта сводится к следующему: запускается некоторый условный “компилятор” для нашей библиотеки и генерирует на его основе с-файл с обертками для micropython-функций, которые в дальнейшем линкуются к micropython. После сборки уже в самом интерпретаторе micropython-а можно получить доступ к этим функциям библиотеки через отдельный модуль. 

Было несколько попыток и мне так и не удалось запустить этот “компилятор” для текущих исходников micropython под EDK. Сделаем "workaround" и скомпилируем проект под Linux, а затем возьмем уже скомпилированный файл оберток “lv_mpy_example.c” и поместим  в существующий проект в нашем EDK.

Доступ к модулям micropython после компиляции указываются в mpconfigport.h. Необходимо добавить ссылку на структуру модуля mp_module_lvgl, описанную в “lv_mpy_example.c” в MICROPY_PORT_BUILTIN_MODULES:

extern const struct _mp_obj_module_t mp_module_lvgl;

#define MICROPY_PORT_BUILTIN_MODULES \
...
{ MP_OBJ_NEW_QSTR(MP_QSTR_lvgl), (mp_obj_t)&mp_module_lvgl },
...

Здесь так же указывается идентификатор строки MP_QSTR_lvgl. Все идентификаторы, которые используются micropython хранятся в файле “qstrdefs.generated.h” в виде 3х байтового хеша и самой строки. Этот файл генерируется автоматически скриптом genhdr.py. Поэтому для обновления строки и перед компиляция его как минимум один раз надо запустить.

Добавляем все новые исходные файлы LVGL и в inf файл модуля MicroPythonDxe.inf.

Компилируем. Запускаем в Shell наш свежескомпилированный micropython.efi. Вводим в строке интерпретатора “import lvgl”, нажимаем Enter и наш micropython принимает модуль без ошибок. Автодополнение по tab тоже работает. Супер, все сделано верно!

Игла в яйце, яйцо в утке, утка в зайце
 В смысле Lvgl в Micropython, а он в EFI
Игла в яйце, яйцо в утке, утка в зайце В смысле Lvgl в Micropython, а он в EFI

Lvgl есть, а GUI нет - непорядок

Сейчас у нас нет драйвера, который был знал как выводить Lvgl на экран. Напишем! Драйвер будет выполнен в качестве еще одного модуля micropython.

Как же написать драйвер? Тут все просто. Посмотрим как устроен уже существующий драйвер SDL под Linux и пример как работать с ним в нашем питоне.

Начнем с вывода на экран. Так, есть функция инициализации и есть callback, который должен выводить буфер lvgl на экран

void monitor_flush(struct _disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)

Окей, все понятно, пишем.

В EFI вывод на экран происходит через протокол EfiGopProtocol. Он реализует простейшие функции для работы с экранным буфером. Помним, что в системе EfiGopProtocol может быть   несколько штук (например, для нескольких экранов). Делаем так, чтобы наш вывод изображения был на все из них, дабы не искать куда же вывелось изображение. Проходимся по всем EfiGopProtocol-ам и выводим буфер через функцию Blt, который пришел нам на вход.

Код вывода
{
  EFI_STATUS Status;
  UINTN Index;
  EFI_GRAPHICS_OUTPUT_PROTOCOL   *GraphicsOutput;
  EFI_HANDLE *HndlBuf;
  UINTN HndlNum;

  //
  // Get all the GOP protocol handles
  //
  Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiGraphicsOutputProtocolGuid, NULL, &HndlNum,  &HndlBuf);
  if (EFI_ERROR(Status) || HndlNum == 0) {
    return;
  }
  
  for (Index = 0; Index < HndlNum; Index++) {

    Status = gBS->HandleProtocol (
                    HndlBuf[Index],
                    &gEfiGraphicsOutputProtocolGuid,
                    (VOID **) &GraphicsOutput
                    );
    if (EFI_ERROR(Status)) {
      continue;
    }

    //
    // Out buffer to screen for each GOP protocol
    //
    GraphicsOutput->Blt(
        GraphicsOutput,
        (EFI_GRAPHICS_OUTPUT_BLT_PIXEL*)color_p,
        EfiBltBufferToVideo,
        0,
        0,
        area->x1,
        area->y1,
        w,
        h,
        0
    );
  }

  gBS->FreePool(HndlBuf);
}

Далее сделаем из этого кода модуль для micropython:

Код
STATIC const mp_rom_map_elem_t efidirect_globals_table[] = {
        { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_efidirect) },
        { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&mp_init_efidirect_obj) },
        { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&mp_deinit_efidirect_obj) },
        { MP_ROM_QSTR(MP_QSTR_monitor_flush), MP_ROM_PTR(&PTR_OBJ(monitor_flush))}
};
         

STATIC MP_DEFINE_CONST_DICT (
    mp_module_efidirect_globals,
    efidirect_globals_table
);

const mp_obj_module_t mp_module_efidirect = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t*)&mp_module_efidirect_globals
};

Компилируем, стартуем. Запускаем тестовый скрипт:

Скрипт
import lvgl as lv
import efidirect as ed

# Screen size
scr_width = 800
scr_height = 600

# Initialize EFI driver
ed.init(w = scr_width, h = scr_height)
lv.init()

# Register EFI display driver
disp_buf1 = lv.disp_buf_t()
buf1_1 = bytearray(scr_width*scr_height*4)
disp_buf1.init(buf1_1, None, len(buf1_1)//4)
disp_drv = lv.disp_drv_t()
disp_drv.init()
disp_drv.buffer = disp_buf1
disp_drv.flush_cb = ed.monitor_flush
disp_drv.hor_res = scr_width
disp_drv.ver_res = scr_height
disp_drv.register()

scr = lv.obj()
btn = lv.btn(scr)
btn.align(lv.scr_act(), lv.ALIGN.CENTER, 0, 0)
label = lv.label(btn)
label.set_text("Hello World!")

# Load the screen
lv.scr_load(scr)

Смотрим на экран.. а там.. ничего нет! Ну как же так!

Главное, не падать духом. В Linux с SDL работает, а у нас нет. Где-то вышла промашка. Снова изучаем документацию и изучаем SDL драйвер и вот оно! 

Дело в том, в Lvgl необходим обработчик, который должен вызываться в цикле. Для SDL это сделали в отдельном потоке при инициализации библиотеки. У нас в EFI нет отдельного потока, поэтому, для простоты, будем вызывать цикл обработки прямо в нашем скрипте:

while(1):
    lv.task_handler()
    #Дельту времени пока задаем 1. 
    #По хорошему здесь должно быть честное вычисление дельты времени, но пока так 
    lv.tick_inc(1)

Запускаем и о чудо, it’s works!

Привет мир!
Привет мир!

Единственное “Но” - мы в цикле, у нас ничего нет, мы ничего не может сделать. Это конец, “перезагружаемся”.

Добавляем ввод

Полностью аналогично предыдущему пункту. Смотрим в SDL драйвер и делаем так же. Для ввода с клавиатуры в EFI используем EfiSimpleInputProtocol. Он реализует метод ReadKeyStroke, который и возвращает нам нажатую клавишу. Упаковываем его в в callback от Lvgl, не забыв преобразовать определение клавиш с EFI в LVGL: 

Код для клавиатуры
/**
 * ConvertEfiKeyToLvgl
 */
uint32_t
ConvertEfiKeyToLvgl(
    EFI_INPUT_KEY *Key
)
{
    switch (Key->UnicodeChar)
    {
    case CHAR_CARRIAGE_RETURN:
        return LV_KEY_ENTER;
    case CHAR_BACKSPACE:
        return LV_KEY_BACKSPACE;
    case CHAR_TAB:
        return LV_KEY_NEXT;  
    default:
        break;
    }

    switch (Key->ScanCode)
    {
    case SCAN_UP:
        return LV_KEY_UP;
    case SCAN_DOWN:
        return LV_KEY_DOWN;
    case SCAN_RIGHT:
        return LV_KEY_RIGHT;
    case SCAN_LEFT:
        return LV_KEY_LEFT;
    case SCAN_DELETE:
        return LV_KEY_DEL;
    case SCAN_HOME:
        return LV_KEY_HOME;
    case SCAN_END:
        return LV_KEY_END;
    default:
        break;
    }

    return Key->UnicodeChar;
}


bool keyboard_read(struct _lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
    EFI_STATUS Status;
    EFI_INPUT_KEY Key;

    Status = gST->ConIn->ReadKeyStroke(gST->ConIn, &Key);
    if (EFI_ERROR(Status)) {
        data->state = LV_INDEV_STATE_REL;
        return false;
    }

    data->key = ConvertEfiKeyToLvgl(&Key);
    data->state = LV_INDEV_STATE_PR;

    return false;
}

Для мыши есть протокол EfiSimplePointerProtocol. Действуем аналогично. 

Код для мыши
bool mouse_read(struct _lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
    EFI_STATUS Status;
    EFI_SIMPLE_POINTER_STATE State;
    STATIC lv_coord_t X = 0, Y = 0;
    
    if (gSimplePointer == NULL) {
        return false;
    }

    //
    // Get current the mouse state
    //
    Status = gSimplePointer->GetState(gSimplePointer, &State);
    if (EFI_ERROR(Status)) {
        return false;
    }

    //
    // And calculate the absolute coordinate
    //
    X += (lv_coord_t)(State.RelativeMovementX / (INT32)gSimplePointer->Mode->ResolutionX);
    Y += (lv_coord_t)(State.RelativeMovementY / (INT32)gSimplePointer->Mode->ResolutionY);

    X = MAX(X, 0);
    Y = MAX(Y, 0);

    //
    // Store the collected data
    //
    data->point.x = X;
    data->point.y = Y;
    data->state = State.LeftButton ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;

    return false;
}

Здесь стоит отметить только то, что если в случае с EfiSimpleInputProtocol, он уже есть в системной таблице gST, которая подается на вход программы, то EfiSimplePointerProtocol в gST нет, и его надо получить по хендлу ConsoleInHandle. 

Получение EfiSimplePointerProtocol
EFI_SIMPLE_POINTER_PROTOCOL *gSimplePointer = NULL;

...
  
/*
 * Init Function
 */
void input_init()
{
    EFI_STATUS Status;
    Status = gBS->HandleProtocol(gST->ConsoleInHandle, &gEfiSimplePointerProtocolGuid, &gSimplePointer);
    if (EFI_ERROR(Status)) {
        gSimplePointer = NULL;
    }
}

Обновим тестовый скрипт. Запускаем. На этот раз работает все так как и ожидалось!

Скрипт micropython
import lvgl as lv
import efidirect as ed
import uctypes

# Screen size
scr_width = 800
scr_height = 600

close_flag = False

# Click button handler
def event_handler(source,evt):
	global close_flag
	if evt == lv.EVENT.CLICKED:
		print("Clicked")
		close_flag = True

# Initialize EFI drivers
ed.init(w = scr_width, h = scr_height)
lv.init()

# Register display driver.

disp_buf1 = lv.disp_buf_t()
buf1_1 = bytearray(scr_width*scr_height*4)
disp_buf1.init(buf1_1, None, len(buf1_1)//4)
disp_drv = lv.disp_drv_t()
disp_drv.init()
disp_drv.buffer = disp_buf1
disp_drv.flush_cb = ed.monitor_flush
disp_drv.hor_res = scr_width
disp_drv.ver_res = scr_height
disp_drv.register()

# Regsiter mouse driver

indev_drv = lv.indev_drv_t()
indev_drv.init() 
indev_drv.type = lv.INDEV_TYPE.POINTER;
indev_drv.read_cb = ed.mouse_read
mouse_indev = indev_drv.register();

indev_drv = lv.indev_drv_t()
indev_drv.init() 
indev_drv.type = lv.INDEV_TYPE.KEYPAD;
indev_drv.read_cb = ed.keyboard_read
kb_indev = indev_drv.register();

kb_group = lv.group_create()
kb_indev.set_group(kb_group)

scr = lv.obj()

# First button
btn = lv.btn(scr)
btn.align(lv.scr_act(), lv.ALIGN.CENTER, 0, 0)
label = lv.label(btn)
label.set_text("Cancel")
btn.set_event_cb(event_handler)
# Set the group for 'tab' works
kb_group.add_obj(btn)

# Second button
btn = lv.btn(scr)
btn.align(lv.scr_act(), lv.ALIGN.CENTER, 0, 50)
label = lv.label(btn)
label.set_text("Hello World!")
kb_group.add_obj(btn)


# Load the screen

lv.scr_load(scr)

# Set the PLAY symbol as the cursor image
img = lv.img(lv.scr_act(), None)
img.set_src(lv.SYMBOL.PLAY)
mouse_indev.set_cursor(img)

while(1):
	lv.task_handler()
	lv.tick_inc(1)
	key = lv.indev_t.get_key(kb_indev)
	if close_flag:
		break

Запускаем. На этот раз работает все так как и ожидалось!

Теперь работает и мышь и клавиатура. И да, это все еще запускается под UEFI
Теперь работает и мышь и клавиатура. И да, это все еще запускается под UEFI

Можно заметить одно маленькое "фи". Если запустить наше приложение со скрипом скриптом в эмуляторе и одновременно отобразить диспетчер задач, то можно увидеть простой факт: приложение занимает слишком много процессорного времени. 

Да, 9% кажется мало, но вторая вкладка диспетчера задача говорит, что одно ядро процессора постоянно занято эмулятором на 100%. Необходимо чтобы обработчик LVGL не так часто выполнялся и процессор немного “отдохнул” от рутины, прежде чем начнет выполнять его заново. Другими словами, должна быть небольшая задержка в цикле обработки, который мы описали в скрипте. Функция задержка есть в EFI. Вызовем ее прямо из скрипта. Для этого на помощь нам приходит EDK модуль micropython-a "uefi":

HotFix
import Lib.Uefi.uefi as uefi

…

while(1):
	lv.task_handler()
	lv.tick_inc(1)
  # Delay 1ms
	uefi.bs.Stall(100)
  #
	key = lv.indev_t.get_key(kb_indev)
	if close_flag:
		break

Все! Можно приступать к созданию GUI приложений или даже игр (если кому-то захочется) без каких-либо проблем под EFI. И главное все это делать на micropython-е. Не думаю, что сильно кому-нибудь это понадобится, но вдруг найдутся такие отважные люди.  

Исходный код всего этого безобразия есть на Github.

Если кто хочет поиграться с уже скомпилированной версией micropython-а с LVGL под EFI - вам сюда. Тестовый py-файл здесь. Документацию по API можно найти на официальной сайте.

И напоследок, быстро накидал 2048.

P.S. Пока окончально дописал статью, немного отстал от жизни т.к. вышел следующий релиз LVGL за номером 8, в то время как статья базируется на LVGL 7.x. Но большой беды в этом нет, т.к. описанное почти полностью идентично если бы портировался свежий LVGL 8.

Комментарии (5)


  1. Rast1234
    14.10.2021 20:32
    +2

    Статья огонь, автор крут! Помимо игр, сразу в голову пришла uefi-шная версия gdisk, ей бы такой GUI не помешал. Да и просто загрузчик, но с гибко настраиваемым интерфейсом и возможностью что-то дописать на почти-питоне, вполне пригодился бы.


  1. ciuafm
    15.10.2021 16:31

    Как говорят ничего не понял, но очень интересно. Я так понял можно запустить модернизированный питон (с GUI) вместо операционки и ранить скрипты? А можно добавить больше деталей как это сделать с примерами? Например установка вашей 2048 на пустой комп? Можно ли зайти в интерактивный режим питона?

    Заранее спасибо


    1. Grazinpeo Автор
      15.10.2021 22:10
      +2

       Я так понял можно запустить модернизированный питон (с GUI) вместо операционки и ранить скрипты? 

      Да, все именно так

       А можно добавить больше деталей как это сделать с примерами? Например установка вашей 2048 на пустой комп? 

      Можно, например, запускаться через EFI Shell. Опишу для флешки, для жесткого диска аналогично

      • Скачать оболочку EFI Shell и переименовать в BOOTX64.efi.

      • Создать путь из директорий на флешке "\EFI\BOOT\" и закинуть туда "BOOTX64.efi" . Лучше если флешка будет в FAT32.

      • Закинуть на флешку исполняемые модули micropython и сам скрипт 2048.py

      • Запустить ПК перейти в загрузочное меню (что-нибудь типа "UEFI Boot Menu"), выбрать что-нибудь похожее на "Boot from USB".

      • Запустить Shell и вызвать команду "map". Посмотреть на каком "FS<X>" находится флешка (Например 'вызвать' "FS0:", и далее посмотреть по команде "ls" что там именно нужные файлы). К примеру, для "FS1", тогда:

        • Можно вручную запустить скрипт:

          • FS1:

          • micropython.efi 2048.py

        • Можно, чтобы каждый раз 2048 сам запускался при включении Shell-a. Добавить в корень Shell файл-скрипта "startup.nsh" с содержимым "FS1:\micropython.efi 2048.py" (Способ неидеальный для автоматической загрузки, но в принципе будет работать)

      Еще, есть возможность, вместо Shell-а, сразу запускать micropython.efi (закинуть его как \EFI\BOOT\BOOTX64.efi) и поместить в тот же каталог файл MicroPythonDxe.efi. А далее Micropython позволяет запускать файл по умолчанию boot.py и/или main.py в корне. Но этот способ я не пробовал.

      Можно ли зайти в интерактивный режим питона?

      Да, тоже что и выше, только не указывая файл


      1. ciuafm
        06.11.2021 13:20

        Большое спасибо за подробную инструкцию. К сожалению я не смог запустить ваш micropython.efi файл. На Dell 6510 он просто ничего не выводит. На смартбуке intel Atom выводит : ASSERT [micropython] e:\start\edk2\EmulatorPkg\Library\DxeEmuLib\DxeEmuLib.c(38) : GuidHob != ((void *) 0)

        Можете скомпилить micropython.efi без GUI, а то у меня лапки :-( ?


        1. Grazinpeo Автор
          22.11.2021 22:36
          +1

          Пересобрал, теперь должно работать