feat(st7789): драйвер TFT-панелей ST7789V поверх SPI

Перенесён из KONOR_ds18b20/lib/st7789; в OpticalTester лежала побайтово
такая же копия.

Ядро не включает заголовки МК и не заводит кадрового буфера: примитивы
пишут пиксели потоком, поэтому драйвер работает на МК с единицами
килобайт ОЗУ. Платформа подключается через ST7789_Io: обязательны
write, set_command и delay_ms, остальное — по разводке платы.
This commit is contained in:
2026-08-23 01:15:11 +03:00
parent fe6598ca60
commit c7798c02db
5 changed files with 1106 additions and 0 deletions

65
c/st7789/README.md Normal file
View File

@@ -0,0 +1,65 @@
# st7789
Драйвер TFT-панелей на контроллере **ST7789V** (SPI, цвет RGB565).
Ядро на C99: не включает заголовки микроконтроллера, не трогает регистры,
не заводит кадровый буфер. Примитивы пишут пиксели в контроллер потоком,
поэтому драйвер живёт на МК с единицами килобайт ОЗУ.
```
приложение
st7789.c окно вывода, примитивы, текст
st7789_font.c шрифт 6x8, масштаб 1..4
ST7789_Io write, set_command, set_select,
set_reset, set_backlight, delay_ms
порт платы SPI + GPIO
```
## Состав
| Файл | Что делает | Зависимости |
|---|---|---|
| `st7789.h`, `st7789.c` | инициализация панели, поворот, заливка, пиксели, линии, текст | `stdint.h` |
| `st7789_font.h`, `st7789_font.c` | растровый шрифт 6x8 с масштабированием, символ градуса `0x7F` | `stdint.h` |
## Что нужно от платформы
Заполнить `ST7789_Io`. Обязательны три вызова:
```c
void write(void *ctx, const uint8_t *data, uint32_t size); /* SPI, MSB первым */
void set_command(void *ctx, uint8_t is_command); /* линия DC */
void delay_ms(void *ctx, uint32_t ms);
```
Необязательные: `set_select` (без него CS считается посаженным на землю),
`set_reset` (без него выполняется программный SWRESET), `set_backlight`.
## Быстрый старт
```c
ST7789_Display display;
ST7789_Config config;
ST7789_ConfigDefault(&config, 240, 320); /* геометрия конкретного модуля */
config.rotation = ST7789_ROTATION_90;
ST7789_Io io = { .write = spi_write, .set_command = dc_set,
.delay_ms = board_delay, .context = &board };
ST7789_Init(&display, &io, &config);
ST7789_FillScreen(&display, ST7789_BLACK);
ST7789_DrawText(&display, 4, 4, "SET", ST7789_WHITE, ST7789_BLACK, 2);
```
Геометрия зависит от модуля: у панелей 240x240 и 240x320 смещений нет,
у 135x240 видимая область сдвинута — значения берутся из документации модуля
и задаются полями `offset_x` / `offset_y`.
## Проверено в проектах
`KONOR_ds18b20`, `OpticalTester` — обе платы на STM32F103C8T6, порт занимает
около сорока строк поверх SPI1 и трёх выводов GPIO.

624
c/st7789/st7789.c Normal file
View File

@@ -0,0 +1,624 @@
/**
* @file st7789.c
* @brief Реализация драйвера ST7789V: инициализация панели и графика.
*
* Кадрового буфера нет: заливки и текст выводятся потоком через окно RAMWR,
* а посредником служит буфер на ST7789_CHUNK_PIXELS пикселей. Обращений к
* регистрам микроконтроллера в файле нет, поэтому он переносится между
* платформами без правок — меняется только порт, заполняющий ST7789_Io.
*/
#include "st7789.h"
#include "st7789_font.h"
#define ST_CMD_SWRESET 0x01U /**< Программный сброс контроллера. */
#define ST_CMD_SLPIN 0x10U /**< Переход в спящий режим. */
#define ST_CMD_SLPOUT 0x11U /**< Выход из спящего режима. */
#define ST_CMD_NORON 0x13U /**< Обычный режим вывода. */
#define ST_CMD_INVOFF 0x20U /**< Отключение инверсии. */
#define ST_CMD_INVON 0x21U /**< Включение инверсии. */
#define ST_CMD_DISPON 0x29U /**< Включение изображения. */
#define ST_CMD_CASET 0x2AU /**< Окно по столбцам. */
#define ST_CMD_RASET 0x2BU /**< Окно по строкам. */
#define ST_CMD_RAMWR 0x2CU /**< Запись в память изображения. */
#define ST_CMD_MADCTL 0x36U /**< Порядок доступа к памяти и поворот. */
#define ST_CMD_COLMOD 0x3AU /**< Формат пикселя. */
#define ST_MADCTL_MY 0x80U /**< Отражение по строкам. */
#define ST_MADCTL_MX 0x40U /**< Отражение по столбцам. */
#define ST_MADCTL_MV 0x20U /**< Обмен строк и столбцов. */
#define ST_MADCTL_BGR 0x08U /**< Порядок субпикселей BGR. */
/** Растр контроллера ST7789V, в который вписана видимая область панели. */
#define ST_PANEL_WIDTH 240U
#define ST_PANEL_HEIGHT 320U
#if ST7789_CHUNK_PIXELS < (ST7789_FONT_WIDTH * ST7789_FONT_MAX_SCALE)
#error "ST7789_CHUNK_PIXELS слишком мал для строки знакоместа шрифта"
#endif
/** Буфер потоковой передачи пикселей; драйвер блокирующий, поэтому общий. */
static uint8_t g_chunk[ST7789_CHUNK_PIXELS * 2U];
/**
* @brief Открывает транзакцию SPI, если вывод CS разведён.
*
* @param display Дисплей с заполненной таблицей вызовов.
*/
static void st_select(ST7789_Display *display)
{
if (display->io.set_select != 0) {
display->io.set_select(display->io.context, 1U);
}
}
/**
* @brief Закрывает транзакцию SPI.
*
* @param display Дисплей с заполненной таблицей вызовов.
*/
static void st_deselect(ST7789_Display *display)
{
if (display->io.set_select != 0) {
display->io.set_select(display->io.context, 0U);
}
}
/**
* @brief Передаёт код команды с активной линией DC.
*
* @param display Дисплей.
* @param command Код команды контроллера.
*/
static void st_write_command(ST7789_Display *display, uint8_t command)
{
display->io.set_command(display->io.context, 1U);
display->io.write(display->io.context, &command, 1U);
display->io.set_command(display->io.context, 0U);
}
/**
* @brief Передаёт блок данных команды.
*
* @param display Дисплей.
* @param data Передаваемые байты.
* @param size Количество байтов; ноль допустим.
*/
static void st_write_data(ST7789_Display *display, const uint8_t *data, uint32_t size)
{
if (size != 0U) {
display->io.write(display->io.context, data, size);
}
}
/**
* @brief Собирает байт MADCTL для текущего поворота.
*
* @param display Дисплей с заполненной конфигурацией.
* @return Значение регистра MADCTL.
*/
static uint8_t st_madctl(const ST7789_Display *display)
{
uint8_t value;
switch (display->config.rotation) {
case ST7789_ROTATION_90:
value = (uint8_t)(ST_MADCTL_MV | ST_MADCTL_MY);
break;
case ST7789_ROTATION_180:
value = (uint8_t)(ST_MADCTL_MX | ST_MADCTL_MY);
break;
case ST7789_ROTATION_270:
value = (uint8_t)(ST_MADCTL_MV | ST_MADCTL_MX);
break;
case ST7789_ROTATION_0:
default:
value = 0U;
break;
}
if (display->config.bgr != 0U) {
value = (uint8_t)(value | ST_MADCTL_BGR);
}
return value;
}
/**
* @brief Пересчитывает размеры изображения и смещения окна под поворот.
*
* При повороте на 90 и 270 градусов оси меняются местами, а смещение видимой
* области отсчитывается от противоположного края растра контроллера.
*
* @param display Дисплей с заполненной конфигурацией.
*/
static void st_apply_geometry(ST7789_Display *display)
{
const uint16_t width = display->config.width;
const uint16_t height = display->config.height;
const uint16_t offset_x = display->config.offset_x;
const uint16_t offset_y = display->config.offset_y;
const uint16_t slack_x = (uint16_t)(ST_PANEL_WIDTH - (width + offset_x));
const uint16_t slack_y = (uint16_t)(ST_PANEL_HEIGHT - (height + offset_y));
switch (display->config.rotation) {
case ST7789_ROTATION_90:
display->width = height;
display->height = width;
display->offset_x = offset_y;
display->offset_y = slack_x;
break;
case ST7789_ROTATION_180:
display->width = width;
display->height = height;
display->offset_x = slack_x;
display->offset_y = slack_y;
break;
case ST7789_ROTATION_270:
display->width = height;
display->height = width;
display->offset_x = slack_y;
display->offset_y = offset_x;
break;
case ST7789_ROTATION_0:
default:
display->width = width;
display->height = height;
display->offset_x = offset_x;
display->offset_y = offset_y;
break;
}
}
/**
* @brief Задаёт окно вывода и оставляет контроллер в режиме записи RAMWR.
*
* @param display Дисплей.
* @param x Левая граница окна в координатах изображения.
* @param y Верхняя граница окна.
* @param width Ширина окна, больше нуля.
* @param height Высота окна, больше нуля.
*/
static void st_set_window(ST7789_Display *display, uint16_t x, uint16_t y,
uint16_t width, uint16_t height)
{
const uint16_t x0 = (uint16_t)(x + display->offset_x);
const uint16_t y0 = (uint16_t)(y + display->offset_y);
const uint16_t x1 = (uint16_t)(x0 + width - 1U);
const uint16_t y1 = (uint16_t)(y0 + height - 1U);
uint8_t bounds[4];
bounds[0] = (uint8_t)(x0 >> 8U);
bounds[1] = (uint8_t)(x0 & 0xFFU);
bounds[2] = (uint8_t)(x1 >> 8U);
bounds[3] = (uint8_t)(x1 & 0xFFU);
st_write_command(display, ST_CMD_CASET);
st_write_data(display, bounds, sizeof(bounds));
bounds[0] = (uint8_t)(y0 >> 8U);
bounds[1] = (uint8_t)(y0 & 0xFFU);
bounds[2] = (uint8_t)(y1 >> 8U);
bounds[3] = (uint8_t)(y1 & 0xFFU);
st_write_command(display, ST_CMD_RASET);
st_write_data(display, bounds, sizeof(bounds));
st_write_command(display, ST_CMD_RAMWR);
}
/**
* @brief Передаёт в открытое окно заданное число пикселей одного цвета.
*
* @param display Дисплей.
* @param color Цвет RGB565.
* @param count Количество пикселей.
*/
static void st_push_color(ST7789_Display *display, uint16_t color, uint32_t count)
{
const uint8_t high = (uint8_t)(color >> 8U);
const uint8_t low = (uint8_t)(color & 0xFFU);
uint32_t filled = 0U;
uint32_t index;
while (filled < (uint32_t)ST7789_CHUNK_PIXELS) {
g_chunk[filled * 2U] = high;
g_chunk[(filled * 2U) + 1U] = low;
filled++;
}
while (count != 0U) {
index = (count > (uint32_t)ST7789_CHUNK_PIXELS) ? (uint32_t)ST7789_CHUNK_PIXELS : count;
display->io.write(display->io.context, g_chunk, index * 2U);
count -= index;
}
}
/**
* @brief Обрезает прямоугольник по границам экрана.
*
* @param display Дисплей.
* @param x Левая граница; корректируется по месту.
* @param y Верхняя граница; корректируется по месту.
* @param width Ширина; корректируется по месту.
* @param height Высота; корректируется по месту.
* @return 1, если после обрезки область видима, иначе 0.
*/
static uint8_t st_clip(const ST7789_Display *display, int16_t *x, int16_t *y,
uint16_t *width, uint16_t *height)
{
int32_t left = *x;
int32_t top = *y;
int32_t right = left + (int32_t)*width;
int32_t bottom = top + (int32_t)*height;
if ((*width == 0U) || (*height == 0U)) {
return 0U;
}
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
if (right > (int32_t)display->width) {
right = (int32_t)display->width;
}
if (bottom > (int32_t)display->height) {
bottom = (int32_t)display->height;
}
if ((right <= left) || (bottom <= top)) {
return 0U;
}
*x = (int16_t)left;
*y = (int16_t)top;
*width = (uint16_t)(right - left);
*height = (uint16_t)(bottom - top);
return 1U;
}
void ST7789_ConfigDefault(ST7789_Config *config, uint16_t width, uint16_t height)
{
if (config == 0) {
return;
}
config->width = width;
config->height = height;
config->offset_x = 0U;
config->offset_y = 0U;
config->rotation = ST7789_ROTATION_0;
config->invert = 1U;
config->bgr = 0U;
}
uint8_t ST7789_Init(ST7789_Display *display, const ST7789_Io *io,
const ST7789_Config *config)
{
/* Порядок команд соответствует рекомендациям Sitronix для ST7789V. */
static const uint8_t init_porch[] = { 0x0CU, 0x0CU, 0x00U, 0x33U, 0x33U };
static const uint8_t init_gamma_p[] = {
0xD0U, 0x04U, 0x0DU, 0x11U, 0x13U, 0x2BU, 0x3FU,
0x54U, 0x4CU, 0x18U, 0x0DU, 0x0BU, 0x1FU, 0x23U
};
static const uint8_t init_gamma_n[] = {
0xD0U, 0x04U, 0x0CU, 0x11U, 0x13U, 0x2CU, 0x3FU,
0x44U, 0x51U, 0x2FU, 0x1FU, 0x1FU, 0x20U, 0x23U
};
static const uint8_t init_power1[] = { 0xA4U, 0xA1U };
uint8_t value;
if ((display == 0) || (io == 0) || (config == 0)) {
return 0U;
}
if ((io->write == 0) || (io->set_command == 0) || (io->delay_ms == 0)) {
return 0U;
}
if ((config->width == 0U) || (config->height == 0U)
|| ((uint32_t)config->width + config->offset_x > ST_PANEL_WIDTH)
|| ((uint32_t)config->height + config->offset_y > ST_PANEL_HEIGHT)) {
return 0U;
}
display->io = *io;
display->config = *config;
display->ready = 0U;
st_apply_geometry(display);
if (display->io.set_reset != 0) {
display->io.set_reset(display->io.context, 1U);
display->io.delay_ms(display->io.context, 10U);
display->io.set_reset(display->io.context, 0U);
display->io.delay_ms(display->io.context, 120U);
}
st_select(display);
if (display->io.set_reset == 0) {
st_write_command(display, ST_CMD_SWRESET);
display->io.delay_ms(display->io.context, 120U);
}
st_write_command(display, ST_CMD_SLPOUT);
display->io.delay_ms(display->io.context, 120U);
value = 0x55U; /* 16 бит на пиксель для интерфейса и памяти. */
st_write_command(display, ST_CMD_COLMOD);
st_write_data(display, &value, 1U);
value = st_madctl(display);
st_write_command(display, ST_CMD_MADCTL);
st_write_data(display, &value, 1U);
st_write_command(display, 0xB2U); /* PORCTRL: длительности кадровых интервалов. */
st_write_data(display, init_porch, sizeof(init_porch));
value = 0x35U; /* GCTRL: уровни VGH 13.26 В и VGL -10.43 В. */
st_write_command(display, 0xB7U);
st_write_data(display, &value, 1U);
value = 0x19U; /* VCOMS: 0.725 В. */
st_write_command(display, 0xBBU);
st_write_data(display, &value, 1U);
value = 0x2CU; /* LCMCTRL: типовое управление ЖК. */
st_write_command(display, 0xC0U);
st_write_data(display, &value, 1U);
value = 0x01U; /* VDVVRHEN: VDV и VRH задаются командами. */
st_write_command(display, 0xC2U);
st_write_data(display, &value, 1U);
value = 0x12U; /* VRHS: 4.45 В. */
st_write_command(display, 0xC3U);
st_write_data(display, &value, 1U);
value = 0x20U; /* VDVS: 0 В. */
st_write_command(display, 0xC4U);
st_write_data(display, &value, 1U);
value = 0x0FU; /* FRCTRL2: частота кадров 60 Гц. */
st_write_command(display, 0xC6U);
st_write_data(display, &value, 1U);
st_write_command(display, 0xD0U); /* PWCTRL1: источники AVDD и AVCL. */
st_write_data(display, init_power1, sizeof(init_power1));
st_write_command(display, 0xE0U); /* PVGAMCTRL: положительная гамма. */
st_write_data(display, init_gamma_p, sizeof(init_gamma_p));
st_write_command(display, 0xE1U); /* NVGAMCTRL: отрицательная гамма. */
st_write_data(display, init_gamma_n, sizeof(init_gamma_n));
st_write_command(display,
(uint8_t)((display->config.invert != 0U) ? ST_CMD_INVON : ST_CMD_INVOFF));
st_write_command(display, ST_CMD_NORON);
display->io.delay_ms(display->io.context, 10U);
st_write_command(display, ST_CMD_DISPON);
display->io.delay_ms(display->io.context, 20U);
st_deselect(display);
display->ready = 1U;
ST7789_FillScreen(display, ST7789_BLACK);
ST7789_Backlight(display, 1U);
return 1U;
}
void ST7789_SetRotation(ST7789_Display *display, ST7789_Rotation rotation)
{
uint8_t value;
if ((display == 0) || (display->ready == 0U)) {
return;
}
display->config.rotation = rotation;
st_apply_geometry(display);
value = st_madctl(display);
st_select(display);
st_write_command(display, ST_CMD_MADCTL);
st_write_data(display, &value, 1U);
st_deselect(display);
}
void ST7789_Backlight(ST7789_Display *display, uint8_t on)
{
if ((display == 0) || (display->io.set_backlight == 0)) {
return;
}
display->io.set_backlight(display->io.context, (uint8_t)((on != 0U) ? 1U : 0U));
}
void ST7789_Sleep(ST7789_Display *display, uint8_t sleep)
{
if ((display == 0) || (display->ready == 0U)) {
return;
}
st_select(display);
st_write_command(display, (uint8_t)((sleep != 0U) ? ST_CMD_SLPIN : ST_CMD_SLPOUT));
st_deselect(display);
display->io.delay_ms(display->io.context, 120U);
}
void ST7789_FillScreen(ST7789_Display *display, uint16_t color)
{
if ((display == 0) || (display->ready == 0U)) {
return;
}
ST7789_FillRect(display, 0, 0, display->width, display->height, color);
}
void ST7789_FillRect(ST7789_Display *display, int16_t x, int16_t y,
uint16_t width, uint16_t height, uint16_t color)
{
if ((display == 0) || (display->ready == 0U)) {
return;
}
if (st_clip(display, &x, &y, &width, &height) == 0U) {
return;
}
st_select(display);
st_set_window(display, (uint16_t)x, (uint16_t)y, width, height);
st_push_color(display, color, (uint32_t)width * height);
st_deselect(display);
}
void ST7789_DrawRect(ST7789_Display *display, int16_t x, int16_t y,
uint16_t width, uint16_t height, uint16_t color)
{
if ((display == 0) || (width == 0U) || (height == 0U)) {
return;
}
ST7789_FillRect(display, x, y, width, 1U, color);
ST7789_FillRect(display, x, (int16_t)(y + (int16_t)height - 1), width, 1U, color);
ST7789_FillRect(display, x, y, 1U, height, color);
ST7789_FillRect(display, (int16_t)(x + (int16_t)width - 1), y, 1U, height, color);
}
void ST7789_DrawPixel(ST7789_Display *display, int16_t x, int16_t y, uint16_t color)
{
ST7789_FillRect(display, x, y, 1U, 1U, color);
}
void ST7789_DrawBitmap(ST7789_Display *display, int16_t x, int16_t y,
uint16_t width, uint16_t height, const uint16_t *pixels)
{
uint32_t total;
uint32_t sent = 0U;
uint32_t index;
if ((display == 0) || (display->ready == 0U) || (pixels == 0)) {
return;
}
if ((width == 0U) || (height == 0U) || (x < 0) || (y < 0)
|| (((int32_t)x + width) > (int32_t)display->width)
|| (((int32_t)y + height) > (int32_t)display->height)) {
return;
}
total = (uint32_t)width * height;
st_select(display);
st_set_window(display, (uint16_t)x, (uint16_t)y, width, height);
while (sent < total) {
uint32_t block = total - sent;
if (block > (uint32_t)ST7789_CHUNK_PIXELS) {
block = (uint32_t)ST7789_CHUNK_PIXELS;
}
for (index = 0U; index < block; index++) {
g_chunk[index * 2U] = (uint8_t)(pixels[sent + index] >> 8U);
g_chunk[(index * 2U) + 1U] = (uint8_t)(pixels[sent + index] & 0xFFU);
}
display->io.write(display->io.context, g_chunk, block * 2U);
sent += block;
}
st_deselect(display);
}
void ST7789_DrawChar(ST7789_Display *display, int16_t x, int16_t y, char symbol,
uint16_t color, uint16_t background, uint8_t scale)
{
const uint8_t *glyph;
uint16_t code = (uint16_t)(uint8_t)symbol;
uint8_t row;
uint8_t column;
uint8_t repeat;
uint16_t cell_width;
uint16_t index;
if ((display == 0) || (display->ready == 0U)) {
return;
}
if (scale == 0U) {
scale = 1U;
}
if (scale > ST7789_FONT_MAX_SCALE) {
scale = ST7789_FONT_MAX_SCALE;
}
if ((code < ST7789_FONT_FIRST_CHAR)
|| (code >= (ST7789_FONT_FIRST_CHAR + ST7789_FONT_GLYPHS))) {
code = ST7789_FONT_FIRST_CHAR;
}
glyph = ST7789_Font5x7[code - ST7789_FONT_FIRST_CHAR];
cell_width = (uint16_t)(ST7789_FONT_WIDTH * scale);
/* Знакоместо целиком за экраном не рисуется; частичное — обрезается фоном. */
if ((x >= (int16_t)display->width) || (y >= (int16_t)display->height)
|| ((x + (int16_t)cell_width) <= 0)
|| ((y + (int16_t)(ST7789_FONT_HEIGHT * scale)) <= 0)) {
return;
}
if ((x < 0) || (y < 0) || ((x + (int16_t)cell_width) > (int16_t)display->width)
|| ((y + (int16_t)(ST7789_FONT_HEIGHT * scale)) > (int16_t)display->height)) {
/* Частично видимый символ выводится через обрезаемые заливки строк. */
for (row = 0U; row < ST7789_FONT_HEIGHT; row++) {
for (column = 0U; column < ST7789_FONT_WIDTH; column++) {
const uint8_t bits = (column < ST7789_FONT_GLYPH_BYTES) ? glyph[column] : 0U;
const uint16_t pixel = (uint16_t)(((bits >> row) & 1U) != 0U ? color : background);
ST7789_FillRect(display, (int16_t)(x + (int16_t)(column * scale)),
(int16_t)(y + (int16_t)(row * scale)), scale, scale, pixel);
}
}
return;
}
st_select(display);
st_set_window(display, (uint16_t)x, (uint16_t)y, cell_width,
(uint16_t)(ST7789_FONT_HEIGHT * scale));
for (row = 0U; row < ST7789_FONT_HEIGHT; row++) {
index = 0U;
for (column = 0U; column < ST7789_FONT_WIDTH; column++) {
const uint8_t bits = (column < ST7789_FONT_GLYPH_BYTES) ? glyph[column] : 0U;
const uint16_t pixel = (uint16_t)(((bits >> row) & 1U) != 0U ? color : background);
for (repeat = 0U; repeat < scale; repeat++) {
g_chunk[index * 2U] = (uint8_t)(pixel >> 8U);
g_chunk[(index * 2U) + 1U] = (uint8_t)(pixel & 0xFFU);
index++;
}
}
for (repeat = 0U; repeat < scale; repeat++) {
display->io.write(display->io.context, g_chunk, (uint32_t)index * 2U);
}
}
st_deselect(display);
}
int16_t ST7789_DrawString(ST7789_Display *display, int16_t x, int16_t y,
const char *text, uint16_t color, uint16_t background,
uint8_t scale)
{
uint16_t step;
if ((display == 0) || (text == 0)) {
return x;
}
if (scale == 0U) {
scale = 1U;
}
if (scale > ST7789_FONT_MAX_SCALE) {
scale = ST7789_FONT_MAX_SCALE;
}
step = (uint16_t)(ST7789_FONT_WIDTH * scale);
while (*text != '\0') {
if (x >= (int16_t)display->width) {
break;
}
ST7789_DrawChar(display, x, y, *text, color, background, scale);
x = (int16_t)(x + (int16_t)step);
text++;
}
return x;
}
uint16_t ST7789_TextWidth(const char *text, uint8_t scale)
{
uint16_t count = 0U;
if (text == 0) {
return 0U;
}
if (scale == 0U) {
scale = 1U;
}
if (scale > ST7789_FONT_MAX_SCALE) {
scale = ST7789_FONT_MAX_SCALE;
}
while (text[count] != '\0') {
count++;
}
return (uint16_t)(count * ST7789_FONT_WIDTH * scale);
}

280
c/st7789/st7789.h Normal file
View File

@@ -0,0 +1,280 @@
/**
* @file st7789.h
* @brief Портируемый драйвер TFT-дисплеев ST7789V (SPI, RGB565).
*
* Библиотека не включает заголовки микроконтроллера и не обращается к
* регистрам: весь обмен идёт через таблицу обратных вызовов ST7789_Io,
* которую заполняет порт платы. Кадрового буфера нет — примитивы пишут
* пиксели в контроллер потоком, поэтому драйвер работает на МК с единицами
* килобайт ОЗУ.
*
* Порядок использования: заполнить ST7789_Io, взять ST7789_ConfigDefault(),
* поправить геометрию и поворот, вызвать ST7789_Init(), далее рисовать.
*/
#ifndef ST7789_H
#define ST7789_H
#include <stdint.h>
/** Ширина знакоместа встроенного шрифта при масштабе 1, пикселей. */
#define ST7789_FONT_WIDTH 6U
/** Высота знакоместа встроенного шрифта при масштабе 1, пикселей. */
#define ST7789_FONT_HEIGHT 8U
/** Максимальный целочисленный масштаб шрифта. */
#define ST7789_FONT_MAX_SCALE 4U
/** Код знака градуса во встроенном шрифте. */
#define ST7789_CHAR_DEGREE 0x7FU
/** Размер потокового буфера в пикселях; определяет расход ОЗУ (по 2 байта). */
#ifndef ST7789_CHUNK_PIXELS
#define ST7789_CHUNK_PIXELS 64U
#endif
/**
* @brief Собирает цвет RGB565 из восьмибитных компонент.
*
* @param r Красная компонента 0..255.
* @param g Зелёная компонента 0..255.
* @param b Синяя компонента 0..255.
* @return Значение цвета для примитивов драйвера.
*/
#define ST7789_RGB(r, g, b) ((uint16_t)((((uint16_t)(r) & 0xF8U) << 8) \
| (((uint16_t)(g) & 0xFCU) << 3) \
| ((uint16_t)(b) >> 3)))
#define ST7789_BLACK 0x0000U /**< Чёрный. */
#define ST7789_WHITE 0xFFFFU /**< Белый. */
#define ST7789_RED 0xF800U /**< Красный. */
#define ST7789_GREEN 0x07E0U /**< Зелёный. */
#define ST7789_BLUE 0x001FU /**< Синий. */
#define ST7789_YELLOW 0xFFE0U /**< Жёлтый. */
#define ST7789_CYAN 0x07FFU /**< Голубой. */
#define ST7789_MAGENTA 0xF81FU /**< Пурпурный. */
#define ST7789_GRAY 0x8410U /**< Серый 50 %. */
#define ST7789_DARKGRAY 0x4208U /**< Тёмно-серый. */
#define ST7789_ORANGE 0xFD20U /**< Оранжевый. */
#define ST7789_NAVY 0x000FU /**< Тёмно-синий. */
/**
* @brief Обратные вызовы платы, через которые драйвер работает с дисплеем.
*
* Обязательны только @c write, @c set_command и @c delay_ms. Остальные поля
* допускают значение 0, если сигнал на плате не разведён: без @c set_reset
* драйвер выполняет программный сброс командой SWRESET, без @c set_select
* дисплей считается постоянно выбранным (вывод CS посажен на общий провод).
*/
typedef struct {
/** Передаёт блок байтов по SPI: 8 бит, старший бит первым. */
void (*write)(void *context, const uint8_t *data, uint32_t size);
/** Задаёт уровень линии DC: 1 — команда, 0 — данные. */
void (*set_command)(void *context, uint8_t is_command);
/** Управляет выбором кристалла: 1 — выбран (CS низкий). */
void (*set_select)(void *context, uint8_t selected);
/** Управляет линией RES: 1 — сброс активен (низкий уровень). */
void (*set_reset)(void *context, uint8_t asserted);
/** Включает и выключает подсветку. */
void (*set_backlight)(void *context, uint8_t on);
/** Задержка в миллисекундах на время инициализации панели. */
void (*delay_ms)(void *context, uint32_t milliseconds);
/** Указатель платы, передаваемый во все обратные вызовы. */
void *context;
} ST7789_Io;
/** @brief Ориентация растра относительно нулевого поворота панели. */
typedef enum {
ST7789_ROTATION_0 = 0, /**< Портрет, шлейф снизу. */
ST7789_ROTATION_90, /**< Ландшафт, поворот на 90 градусов. */
ST7789_ROTATION_180, /**< Портрет, перевёрнутый. */
ST7789_ROTATION_270 /**< Ландшафт, поворот на 270 градусов. */
} ST7789_Rotation;
/**
* @brief Параметры конкретной панели.
*
* Модули на ST7789V отличаются видимой областью и её положением в растре
* контроллера 240x320: у панелей 240x240 и 240x320 смещений нет, у 135x240
* видимая область сдвинута. Значения берутся из документации модуля.
*/
typedef struct {
uint16_t width; /**< Видимая ширина при нулевом повороте, пикселей. */
uint16_t height; /**< Видимая высота при нулевом повороте, пикселей. */
uint16_t offset_x; /**< Смещение видимой области по X при повороте 0. */
uint16_t offset_y; /**< Смещение видимой области по Y при повороте 0. */
ST7789_Rotation rotation; /**< Требуемая ориентация изображения. */
uint8_t invert; /**< 1 — инверсия (обычные IPS-модули ST7789V). */
uint8_t bgr; /**< 1 — порядок субпикселей BGR вместо RGB. */
} ST7789_Config;
/**
* @brief Состояние дисплея; принадлежит вызывающему коду.
*
* Поля @c width и @c height учитывают поворот, поэтому примитивы принимают
* координаты в системе видимого изображения.
*/
typedef struct {
ST7789_Io io; /**< Копия обратных вызовов платы. */
ST7789_Config config; /**< Копия параметров панели. */
uint16_t width; /**< Ширина изображения с учётом поворота. */
uint16_t height; /**< Высота изображения с учётом поворота. */
uint16_t offset_x; /**< Смещение окна по X с учётом поворота. */
uint16_t offset_y; /**< Смещение окна по Y с учётом поворота. */
uint8_t ready; /**< 1 после успешной инициализации. */
} ST7789_Display;
/**
* @brief Заполняет конфигурацию значениями типового модуля на ST7789V.
*
* Инверсия включена, порядок RGB, смещения нулевые, поворот нулевой.
*
* @param config Конфигурация, принадлежащая вызывающему коду.
* @param width Видимая ширина панели при нулевом повороте.
* @param height Видимая высота панели при нулевом повороте.
*/
void ST7789_ConfigDefault(ST7789_Config *config, uint16_t width, uint16_t height);
/**
* @brief Инициализирует панель и включает вывод изображения.
*
* Выполняет аппаратный либо программный сброс, задаёт формат пикселя RGB565,
* гамму, ориентацию и включает подсветку, если её вывод разведён.
*
* @param display Состояние дисплея.
* @param io Обратные вызовы платы; копируются внутрь состояния.
* @param config Параметры панели; копируются внутрь состояния.
* @return 1 при успешной инициализации, 0 при неполной таблице вызовов.
*/
uint8_t ST7789_Init(ST7789_Display *display, const ST7789_Io *io,
const ST7789_Config *config);
/**
* @brief Меняет ориентацию изображения без повторной инициализации.
*
* @param display Инициализированный дисплей.
* @param rotation Требуемая ориентация.
*/
void ST7789_SetRotation(ST7789_Display *display, ST7789_Rotation rotation);
/**
* @brief Включает или выключает подсветку панели.
*
* @param display Инициализированный дисплей.
* @param on 1 — включить, 0 — выключить.
* @note При отсутствии обратного вызова платы функция ничего не делает.
*/
void ST7789_Backlight(ST7789_Display *display, uint8_t on);
/**
* @brief Переводит панель в спящий режим и обратно.
*
* @param display Инициализированный дисплей.
* @param sleep 1 — сон (SLPIN), 0 — рабочий режим (SLPOUT).
*/
void ST7789_Sleep(ST7789_Display *display, uint8_t sleep);
/**
* @brief Заливает весь экран одним цветом.
*
* @param display Инициализированный дисплей.
* @param color Цвет RGB565.
*/
void ST7789_FillScreen(ST7789_Display *display, uint16_t color);
/**
* @brief Заливает прямоугольник одним цветом.
*
* Область обрезается по границам экрана; нулевые размеры игнорируются.
*
* @param display Инициализированный дисплей.
* @param x Левая граница.
* @param y Верхняя граница.
* @param width Ширина области.
* @param height Высота области.
* @param color Цвет RGB565.
*/
void ST7789_FillRect(ST7789_Display *display, int16_t x, int16_t y,
uint16_t width, uint16_t height, uint16_t color);
/**
* @brief Рисует рамку прямоугольника толщиной один пиксель.
*
* @param display Инициализированный дисплей.
* @param x Левая граница.
* @param y Верхняя граница.
* @param width Ширина области.
* @param height Высота области.
* @param color Цвет RGB565.
*/
void ST7789_DrawRect(ST7789_Display *display, int16_t x, int16_t y,
uint16_t width, uint16_t height, uint16_t color);
/**
* @brief Закрашивает один пиксель.
*
* @param display Инициализированный дисплей.
* @param x Координата по горизонтали.
* @param y Координата по вертикали.
* @param color Цвет RGB565.
*/
void ST7789_DrawPixel(ST7789_Display *display, int16_t x, int16_t y, uint16_t color);
/**
* @brief Выводит готовый растр RGB565 построчно.
*
* @param display Инициализированный дисплей.
* @param x Левая граница области.
* @param y Верхняя граница области.
* @param width Ширина растра.
* @param height Высота растра.
* @param pixels Массив width*height пикселей, порядок — строками.
* @note Область должна целиком попадать на экран, иначе вывод отменяется.
*/
void ST7789_DrawBitmap(ST7789_Display *display, int16_t x, int16_t y,
uint16_t width, uint16_t height, const uint16_t *pixels);
/**
* @brief Выводит один символ встроенного шрифта с непрозрачным фоном.
*
* @param display Инициализированный дисплей.
* @param x Левая граница знакоместа.
* @param y Верхняя граница знакоместа.
* @param symbol Код символа 0x20..0x7F; прочие выводятся как пробел.
* @param color Цвет штриха.
* @param background Цвет фона знакоместа.
* @param scale Целочисленный масштаб 1..ST7789_FONT_MAX_SCALE.
*/
void ST7789_DrawChar(ST7789_Display *display, int16_t x, int16_t y, char symbol,
uint16_t color, uint16_t background, uint8_t scale);
/**
* @brief Выводит строку встроенным шрифтом с непрозрачным фоном.
*
* Символы за правой границей экрана отбрасываются, перевод строки не
* обрабатывается.
*
* @param display Инициализированный дисплей.
* @param x Левая граница первого знакоместа.
* @param y Верхняя граница строки.
* @param text Строка, завершённая нулём.
* @param color Цвет штриха.
* @param background Цвет фона знакомест.
* @param scale Целочисленный масштаб 1..ST7789_FONT_MAX_SCALE.
* @return Координата X сразу за последним выведенным знакоместом.
*/
int16_t ST7789_DrawString(ST7789_Display *display, int16_t x, int16_t y,
const char *text, uint16_t color, uint16_t background,
uint8_t scale);
/**
* @brief Считает ширину строки в пикселях для заданного масштаба.
*
* @param text Строка, завершённая нулём.
* @param scale Масштаб шрифта.
* @return Ширина в пикселях.
*/
uint16_t ST7789_TextWidth(const char *text, uint8_t scale);
#endif /* ST7789_H */

110
c/st7789/st7789_font.c Normal file
View File

@@ -0,0 +1,110 @@
/**
* @file st7789_font.c
* @brief Таблица знакогенератора 5x7 для кодов 0x20..0x7F.
*
* Глиф хранится пятью байтами столбцов слева направо; бит 0 каждого байта —
* верхняя строка. Шестой столбец знакоместа драйвер оставляет пустым, поэтому
* шаг символа равен ST7789_FONT_WIDTH. Код 0x7F занят знаком градуса, все
* остальные позиции совпадают с ASCII.
*/
#include "st7789_font.h"
const uint8_t ST7789_Font5x7[ST7789_FONT_GLYPHS][ST7789_FONT_GLYPH_BYTES] = {
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 0x20 пробел */
{ 0x00, 0x00, 0x5F, 0x00, 0x00 }, /* 0x21 '!' */
{ 0x00, 0x07, 0x00, 0x07, 0x00 }, /* 0x22 '"' */
{ 0x14, 0x7F, 0x14, 0x7F, 0x14 }, /* 0x23 '#' */
{ 0x24, 0x2A, 0x7F, 0x2A, 0x12 }, /* 0x24 '$' */
{ 0x23, 0x13, 0x08, 0x64, 0x62 }, /* 0x25 '%' */
{ 0x36, 0x49, 0x55, 0x22, 0x50 }, /* 0x26 '&' */
{ 0x00, 0x05, 0x03, 0x00, 0x00 }, /* 0x27 ''' */
{ 0x00, 0x1C, 0x22, 0x41, 0x00 }, /* 0x28 '(' */
{ 0x00, 0x41, 0x22, 0x1C, 0x00 }, /* 0x29 ')' */
{ 0x14, 0x08, 0x3E, 0x08, 0x14 }, /* 0x2A '*' */
{ 0x08, 0x08, 0x3E, 0x08, 0x08 }, /* 0x2B '+' */
{ 0x00, 0x50, 0x30, 0x00, 0x00 }, /* 0x2C ',' */
{ 0x08, 0x08, 0x08, 0x08, 0x08 }, /* 0x2D '-' */
{ 0x00, 0x60, 0x60, 0x00, 0x00 }, /* 0x2E '.' */
{ 0x20, 0x10, 0x08, 0x04, 0x02 }, /* 0x2F '/' */
{ 0x3E, 0x51, 0x49, 0x45, 0x3E }, /* 0x30 '0' */
{ 0x00, 0x42, 0x7F, 0x40, 0x00 }, /* 0x31 '1' */
{ 0x42, 0x61, 0x51, 0x49, 0x46 }, /* 0x32 '2' */
{ 0x21, 0x41, 0x45, 0x4B, 0x31 }, /* 0x33 '3' */
{ 0x18, 0x14, 0x12, 0x7F, 0x10 }, /* 0x34 '4' */
{ 0x27, 0x45, 0x45, 0x45, 0x39 }, /* 0x35 '5' */
{ 0x3C, 0x4A, 0x49, 0x49, 0x30 }, /* 0x36 '6' */
{ 0x01, 0x71, 0x09, 0x05, 0x03 }, /* 0x37 '7' */
{ 0x36, 0x49, 0x49, 0x49, 0x36 }, /* 0x38 '8' */
{ 0x06, 0x49, 0x49, 0x29, 0x1E }, /* 0x39 '9' */
{ 0x00, 0x36, 0x36, 0x00, 0x00 }, /* 0x3A ':' */
{ 0x00, 0x56, 0x36, 0x00, 0x00 }, /* 0x3B ';' */
{ 0x08, 0x14, 0x22, 0x41, 0x00 }, /* 0x3C '<' */
{ 0x14, 0x14, 0x14, 0x14, 0x14 }, /* 0x3D '=' */
{ 0x00, 0x41, 0x22, 0x14, 0x08 }, /* 0x3E '>' */
{ 0x02, 0x01, 0x51, 0x09, 0x06 }, /* 0x3F '?' */
{ 0x32, 0x49, 0x79, 0x41, 0x3E }, /* 0x40 '@' */
{ 0x7E, 0x11, 0x11, 0x11, 0x7E }, /* 0x41 'A' */
{ 0x7F, 0x49, 0x49, 0x49, 0x36 }, /* 0x42 'B' */
{ 0x3E, 0x41, 0x41, 0x41, 0x22 }, /* 0x43 'C' */
{ 0x7F, 0x41, 0x41, 0x22, 0x1C }, /* 0x44 'D' */
{ 0x7F, 0x49, 0x49, 0x49, 0x41 }, /* 0x45 'E' */
{ 0x7F, 0x09, 0x09, 0x09, 0x01 }, /* 0x46 'F' */
{ 0x3E, 0x41, 0x49, 0x49, 0x7A }, /* 0x47 'G' */
{ 0x7F, 0x08, 0x08, 0x08, 0x7F }, /* 0x48 'H' */
{ 0x00, 0x41, 0x7F, 0x41, 0x00 }, /* 0x49 'I' */
{ 0x20, 0x40, 0x41, 0x3F, 0x01 }, /* 0x4A 'J' */
{ 0x7F, 0x08, 0x14, 0x22, 0x41 }, /* 0x4B 'K' */
{ 0x7F, 0x40, 0x40, 0x40, 0x40 }, /* 0x4C 'L' */
{ 0x7F, 0x02, 0x0C, 0x02, 0x7F }, /* 0x4D 'M' */
{ 0x7F, 0x04, 0x08, 0x10, 0x7F }, /* 0x4E 'N' */
{ 0x3E, 0x41, 0x41, 0x41, 0x3E }, /* 0x4F 'O' */
{ 0x7F, 0x09, 0x09, 0x09, 0x06 }, /* 0x50 'P' */
{ 0x3E, 0x41, 0x51, 0x21, 0x5E }, /* 0x51 'Q' */
{ 0x7F, 0x09, 0x19, 0x29, 0x46 }, /* 0x52 'R' */
{ 0x46, 0x49, 0x49, 0x49, 0x31 }, /* 0x53 'S' */
{ 0x01, 0x01, 0x7F, 0x01, 0x01 }, /* 0x54 'T' */
{ 0x3F, 0x40, 0x40, 0x40, 0x3F }, /* 0x55 'U' */
{ 0x1F, 0x20, 0x40, 0x20, 0x1F }, /* 0x56 'V' */
{ 0x3F, 0x40, 0x38, 0x40, 0x3F }, /* 0x57 'W' */
{ 0x63, 0x14, 0x08, 0x14, 0x63 }, /* 0x58 'X' */
{ 0x07, 0x08, 0x70, 0x08, 0x07 }, /* 0x59 'Y' */
{ 0x61, 0x51, 0x49, 0x45, 0x43 }, /* 0x5A 'Z' */
{ 0x00, 0x7F, 0x41, 0x41, 0x00 }, /* 0x5B '[' */
{ 0x02, 0x04, 0x08, 0x10, 0x20 }, /* 0x5C '\' */
{ 0x00, 0x41, 0x41, 0x7F, 0x00 }, /* 0x5D ']' */
{ 0x04, 0x02, 0x01, 0x02, 0x04 }, /* 0x5E '^' */
{ 0x40, 0x40, 0x40, 0x40, 0x40 }, /* 0x5F '_' */
{ 0x00, 0x01, 0x02, 0x04, 0x00 }, /* 0x60 '`' */
{ 0x20, 0x54, 0x54, 0x54, 0x78 }, /* 0x61 'a' */
{ 0x7F, 0x48, 0x44, 0x44, 0x38 }, /* 0x62 'b' */
{ 0x38, 0x44, 0x44, 0x44, 0x20 }, /* 0x63 'c' */
{ 0x38, 0x44, 0x44, 0x48, 0x7F }, /* 0x64 'd' */
{ 0x38, 0x54, 0x54, 0x54, 0x18 }, /* 0x65 'e' */
{ 0x08, 0x7E, 0x09, 0x01, 0x02 }, /* 0x66 'f' */
{ 0x0C, 0x52, 0x52, 0x52, 0x3E }, /* 0x67 'g' */
{ 0x7F, 0x08, 0x04, 0x04, 0x78 }, /* 0x68 'h' */
{ 0x00, 0x44, 0x7D, 0x40, 0x00 }, /* 0x69 'i' */
{ 0x20, 0x40, 0x44, 0x3D, 0x00 }, /* 0x6A 'j' */
{ 0x7F, 0x10, 0x28, 0x44, 0x00 }, /* 0x6B 'k' */
{ 0x00, 0x41, 0x7F, 0x40, 0x00 }, /* 0x6C 'l' */
{ 0x7C, 0x04, 0x18, 0x04, 0x78 }, /* 0x6D 'm' */
{ 0x7C, 0x08, 0x04, 0x04, 0x78 }, /* 0x6E 'n' */
{ 0x38, 0x44, 0x44, 0x44, 0x38 }, /* 0x6F 'o' */
{ 0x7C, 0x14, 0x14, 0x14, 0x08 }, /* 0x70 'p' */
{ 0x08, 0x14, 0x14, 0x18, 0x7C }, /* 0x71 'q' */
{ 0x7C, 0x08, 0x04, 0x04, 0x08 }, /* 0x72 'r' */
{ 0x48, 0x54, 0x54, 0x54, 0x20 }, /* 0x73 's' */
{ 0x04, 0x3F, 0x44, 0x40, 0x20 }, /* 0x74 't' */
{ 0x3C, 0x40, 0x40, 0x20, 0x1C }, /* 0x75 'u' */
{ 0x1C, 0x20, 0x40, 0x20, 0x1C }, /* 0x76 'v' */
{ 0x3C, 0x40, 0x30, 0x40, 0x3C }, /* 0x77 'w' */
{ 0x44, 0x28, 0x10, 0x28, 0x44 }, /* 0x78 'x' */
{ 0x0C, 0x50, 0x50, 0x50, 0x3C }, /* 0x79 'y' */
{ 0x44, 0x64, 0x54, 0x4C, 0x44 }, /* 0x7A 'z' */
{ 0x00, 0x08, 0x36, 0x41, 0x00 }, /* 0x7B '{' */
{ 0x00, 0x00, 0x7F, 0x00, 0x00 }, /* 0x7C '|' */
{ 0x00, 0x41, 0x36, 0x08, 0x00 }, /* 0x7D '}' */
{ 0x08, 0x04, 0x08, 0x10, 0x08 }, /* 0x7E '~' */
{ 0x00, 0x06, 0x09, 0x09, 0x06 }, /* 0x7F знак градуса */
};

27
c/st7789/st7789_font.h Normal file
View File

@@ -0,0 +1,27 @@
/**
* @file st7789_font.h
* @brief Знакогенератор 5x7 для драйвера ST7789V.
*
* Таблица не зависит от аппаратуры и подключается вместе с драйвером. Для
* замены шрифта достаточно предоставить другой массив того же формата:
* пять байтов столбцов на символ, бит 0 — верхняя строка растра.
*/
#ifndef ST7789_FONT_H
#define ST7789_FONT_H
#include <stdint.h>
/** Число описанных глифов: коды 0x20..0x7F. */
#define ST7789_FONT_GLYPHS 96U
/** Байтов на глиф: пять столбцов растра. */
#define ST7789_FONT_GLYPH_BYTES 5U
/** Первый описанный код символа. */
#define ST7789_FONT_FIRST_CHAR 0x20U
/** Растр символов 5x7; шестой столбец знакоместа рисует драйвер. */
extern const uint8_t ST7789_Font5x7[ST7789_FONT_GLYPHS][ST7789_FONT_GLYPH_BYTES];
#endif /* ST7789_FONT_H */