13 KiB
RP Pico Display Engine
Лёгкий C-движок вывода для RP2040/RP2350 и дисплеев ST7789 / ILI9341 (SPI + DMA) с явным контрактом кадра begin/end paint.
Возможности
- Режимы работы:
DISPLAY_MODE_SAFEиDISPLAY_MODE_RAW - 1 или 2 кадровых буфера (
buffer_count) - Неблокирующий и блокирующий захват кадра:
display_begin_paint_try()display_begin_paint_blocking()
- Безопасное завершение кадра через
display_end_paint() - Набор примитивов рендера и шрифт с поддержкой кириллицы
Структура репозитория
include/display/- публичные заголовки движка и рендераsrc/core/display.c- ядро дисплейного движкаsrc/render/- реализация примитивовinclude/Font/,src/Font/- данные шрифта и текстовый рендерExamples/Thermometr/- рабочий пример проекта на Pico SDK
Что должно быть установлено заранее
gitcmakeверсии3.18.4+- средство сборки, которое поддерживает CMake (
makeпо умолчанию на Unix-подобных системах) Pico SDKи переменная окруженияPICO_SDK_PATH, указывающая на него- ARM toolchain
arm-none-eabi-gcc
Поддерживаемые дисплеи и wiring по умолчанию
Поддерживаются два backend-контроллера:
DISPLAY_TYPE_ST7789DISPLAY_TYPE_ILI9341
Если не передавать compile definitions для дисплея, библиотека использует значения по умолчанию из src/core/display_driver.h:
- Контроллер:
DISPLAY_TYPE_ST7789 - SPI порт:
spi0 MOSI=19SCK=18CS=17DC=22RST=13BL=12
То есть без переопределения пинов и DISPLAY_TYPE прошивка рассчитана на ST7789, подключённый к этим GPIO у RP2040/RP2350.
Быстрый старт
1. Подключение исходников через git submodule (рекомендуется)
Практичный вариант интеграции: добавить этот репозиторий в проект как вендорную зависимость через git submodule и собрать display_engine из вашего основного CMakeLists.txt.
Подключать все примитивы сразу не обязательно: оставьте только нужные и раскомментируйте остальные позже.
Создайте новый проект и добавьте библиотеку как submodule:
mkdir pico_display_app
cd pico_display_app
git init
git submodule add https://github.com/stasenso/rp_pico_display_engine.git external/rp_pico_display_engine
git submodule update --init --recursive
mkdir -p src
Убедитесь, что переменная PICO_SDK_PATH выставлена:
export PICO_SDK_PATH=/absolute/path/to/pico-sdk
Создайте CMakeLists.txt в корне проекта:
cat > CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.18.4)
# EN: Change to `pico` for RP2040-based boards or keep `pico2` for Pico 2 / RP2350.
# RU: Замените на `pico` для плат на RP2040 или оставьте `pico2` для Pico 2 / RP2350.
set(PICO_BOARD pico2 CACHE STRING "Pico SDK target board")
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
project(my_app C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
pico_sdk_init()
set(DISPLAY_ENGINE_DIR ${CMAKE_CURRENT_LIST_DIR}/external/rp_pico_display_engine)
add_library(display_engine STATIC
${DISPLAY_ENGINE_DIR}/src/Font/font_data.c
${DISPLAY_ENGINE_DIR}/src/core/display.c
${DISPLAY_ENGINE_DIR}/src/core/display_transport.c
${DISPLAY_ENGINE_DIR}/src/core/display_driver.c
${DISPLAY_ENGINE_DIR}/src/render/context.c
${DISPLAY_ENGINE_DIR}/src/render/line.c
# EN: Uncomment these sources if/when you need the extra primitives.
# RU: Раскомментируйте эти исходники, если понадобятся дополнительные примитивы.
# ${DISPLAY_ENGINE_DIR}/src/render/grid.c
# ${DISPLAY_ENGINE_DIR}/src/render/sine_wave.c
# ${DISPLAY_ENGINE_DIR}/src/render/bezier.c
)
target_include_directories(display_engine PUBLIC
${DISPLAY_ENGINE_DIR}/include
${DISPLAY_ENGINE_DIR}/include/display
)
target_link_libraries(display_engine PUBLIC
pico_stdlib
hardware_spi
hardware_dma
hardware_timer
pico_multicore
)
# EN: Change the SPI bus, GPIO pins, and display controller here to match your wiring.
# RU: Здесь измените SPI-шину, GPIO-пины и контроллер дисплея под вашу схему подключения.
target_compile_definitions(display_engine PUBLIC
# EN: Select the display controller backend: ST7789 or ILI9341.
# RU: Выберите бэкенд контроллера дисплея: ST7789 или ILI9341.
DISPLAY_TYPE=DISPLAY_TYPE_ST7789
# EN: Select the SPI peripheral connected to the display: spi0 or spi1.
# RU: Выберите SPI-периферию, к которой подключён дисплей: spi0 или spi1.
DISPLAY_SPI_PORT=spi1
DISPLAY_PIN_MOSI=15 # DIN / SDA
DISPLAY_PIN_SCK=14 # CLK / SCL / SCK
DISPLAY_PIN_CS=13 # CS / SS
DISPLAY_PIN_DC=12 # DC / A0 / RS
DISPLAY_PIN_RST=11 # RST / RES / RESET
DISPLAY_PIN_BL=10 # BL / LED / LEDK
)
add_executable(my_app src/main.c)
target_link_libraries(my_app PRIVATE display_engine)
pico_add_extra_outputs(my_app)
EOF
Создайте src/main.c:
cat > src/main.c <<'EOF'
#include "pico/stdlib.h"
#include "display/display.h"
#include "display/render/context.h"
int main(void) {
stdio_init_all();
display_config_t cfg = {
.width = 320,
.height = 240,
.buffer_count = 1,
.mode = DISPLAY_MODE_SAFE
};
display_init(&cfg);
render_ctx_t rc;
while (1) {
uint16_t *buf = display_begin_paint_blocking();
render_begin(&rc, buf, 320, 240);
render_clear(&rc, RGB16(0, 0, 0));
display_end_paint();
sleep_ms(16);
}
}
EOF
Соберите проект:
cmake -S . -B build
cmake --build build
2. Как менять пины и тип дисплея
Задавайте compile definitions на цели display_engine:
DISPLAY_SPI_PORT(spi0илиspi1)DISPLAY_PIN_MOSI,DISPLAY_PIN_SCK,DISPLAY_PIN_CS,DISPLAY_PIN_DC,DISPLAY_PIN_RST,DISPLAY_PIN_BLDISPLAY_TYPE(DISPLAY_TYPE_ST7789илиDISPLAY_TYPE_ILI9341)
Если определения не заданы, используются такие значения по умолчанию из src/core/display_driver.h:
DISPLAY_TYPE=DISPLAY_TYPE_ST7789DISPLAY_SPI_PORT=spi0DISPLAY_PIN_MOSI=19DISPLAY_PIN_SCK=18DISPLAY_PIN_CS=17DISPLAY_PIN_DC=22DISPLAY_PIN_RST=13DISPLAY_PIN_BL=12
3. Режим SAFE с одним буфером в цикле
Подходит для простого цикла, где можно пропустить кадр, если буфер занят.
#include "display/display.h"
#include "display/render/context.h"
#define WIDTH 320
#define HEIGHT 240
void app_loop(void) {
display_config_t cfg = {
.width = WIDTH,
.height = HEIGHT,
.buffer_count = 1,
.mode = DISPLAY_MODE_SAFE
};
display_init(&cfg);
render_ctx_t rc;
while (1) {
uint16_t *buf = display_begin_paint_try();
if (!buf) {
continue;
}
render_begin(&rc, buf, WIDTH, HEIGHT);
render_clear(&rc, RGB16(0, 0, 0));
// draw...
bool ok = display_end_paint();
hard_assert(ok);
}
}
4. Режим SAFE с двумя буферами и возможным отложенным выводом
Режим SAFE + buffer_count = 2: пока DMA выводит один буфер, вы рисуете второй.
Если DMA занят в момент display_end_paint(), кадр может быть отложен (очередь на 1 pending-кадр).
#include "display/display.h"
#include "display/render/context.h"
#define WIDTH 320
#define HEIGHT 240
void app_loop(void) {
display_config_t cfg = {
.width = WIDTH,
.height = HEIGHT,
.buffer_count = 2,
.mode = DISPLAY_MODE_SAFE
};
display_init(&cfg);
render_ctx_t rc;
while (1) {
uint16_t *buf = display_begin_paint_try();
if (!buf) {
// DMA/очередь заняты, делайте другую логику
continue;
}
render_begin(&rc, buf, WIDTH, HEIGHT);
render_clear(&rc, RGB16(8, 16, 8));
// draw...
bool accepted = display_end_paint();
hard_assert(accepted); // false только при нарушении контракта
}
}
5. Заголовки примитивов и шрифтов + пример вывода
Минимальный набор:
#include "display/display.h"
#include "display/render/context.h"
#include "display/render/line.h"
#include "Font/font_data.h" // draw_string(), draw_char(), get_char_width()
Опциональные заголовки (раскомментируйте при необходимости):
// #include "display/render/grid.h"
// #include "display/render/sine_wave.h"
// #include "display/render/bezier.h"
Пример рендера примитивов и текста:
render_ctx_t rc;
render_begin(&rc, buf, 320, 240);
render_clear(&rc, RGB16(9, 19, 9));
render_line(&rc, 0, 0, 319, 239, RGB16(255, 0, 0));
// render_grid(&rc, 20, 20, 40, RGB16(12, 26, 13)); // раскомментируйте, если нужна сетка
// render_sine_wave(&rc, 960, 100, 2.0f, 0, 120, phase, RGB16(0, 255, 0)); // раскомментируйте при необходимости
int px[4] = {20, 80, 140, 220};
int py[4] = {200, 120, 220, 160};
// render_bezier(&rc, px, py, 4, RGB16(255, 255, 0)); // раскомментируйте при необходимости
draw_string(&rc, 16, 16, L"Проверка кириллицы", RGB565(255, 255, 255));
Сборка примера (Pico SDK)
Если хотите собрать готовый пример из этого репозитория без создания собственного проекта:
cd Examples/Thermometr
mkdir -p build && cd build
cmake ..
cmake --build .
В этом примере целевая плата задаётся прямо в Examples/Thermometr/CMakeLists.txt. Чтобы переключить плату, измените строку:
set(PICO_BOARD pico2 CACHE STRING "Pico SDK target board")
Используйте pico для Pico на RP2040 или pico2 для Pico 2 на RP2350. Конфигурация дисплея в этом же файле:
- Контроллер:
DISPLAY_TYPE_ILI9341 - SPI порт:
spi1 MOSI=15SCK=14CS=13DC=12RST=11BL=10
Также есть Examples/EngineDemo/CMakeLists.txt, где по умолчанию выбрано:
- Плата:
Pico 2/RP2350(PICO_BOARD=pico2) - Контроллер:
DISPLAY_TYPE_ST7789 - SPI порт:
spi1 MOSI=15SCK=14CS=13DC=12RST=11BL=10
То есть получившийся .uf2 нужно заливать именно в ту плату, которая выбрана в CMakeLists.txt конкретного примера, а дисплей подключать к перечисленным там пинам.
Контракт API (важно)
- Каждый успешный
display_begin_paint_*()должен завершатьсяdisplay_end_paint() - Нельзя открывать второй
begin, пока не закрыт первый - Не вызывайте
display_submit()вручную внутри открытой paint-секции
Подробные сценарии описаны в SCENARIOS.ru.md.