Files
templates/c/st7789/st7789.c
Andrey Kruchinkin c7798c02db feat(st7789): драйвер TFT-панелей ST7789V поверх SPI
Перенесён из KONOR_ds18b20/lib/st7789; в OpticalTester лежала побайтово
такая же копия.

Ядро не включает заголовки МК и не заводит кадрового буфера: примитивы
пишут пиксели потоком, поэтому драйвер работает на МК с единицами
килобайт ОЗУ. Платформа подключается через ST7789_Io: обязательны
write, set_command и delay_ms, остальное — по разводке платы.
2026-08-23 01:15:11 +03:00

625 lines
22 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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);
}