В данной статье рассматриваются способы вывода текста на дисплей ST7735 с помощью микроконтроллера STM32F103C8. В качестве шрифтов используются два шрифта с кириллицей 8х8 и 8х16, а также шрифт Cybercafe 8х16 без кириллицы.
Статья является продолжением предыдущей "Работа с SPI дисплеями Nokia_5110 и ST7735", где я рассматривал подключение дисплея к микроконтроллеру, его инициализацию, и затрагивал вопрос о максимально быстрой работе с дисплеем по SPI интерфейсу. В этот раз рассматривается вывод текста на дисплей ST7735, сжатие шрифтов, а так же отрисовка простых графических примитивов.
Содержание:
Шрифт Terminus (добавлено 22 мая 2023г.)
Конвертация векторных шрифтов
Дизайнерские шрифты (добавлено 16 июня 2023г.)
Все примеры и готовые прошивки можно скачать с портала GitLab
Инициализация дисплея ST7735 уже рассматривалась в прошлогодней статье: "11) инициализацию дисплея ST7735", но тогда код заливки дисплея (не инициализации) содержал ошибку, из-за чего цвет отображался не всегда корректно. Я тогда ошибку видел, но моей заботой тогда было разобраться с максимально быстрой работой SPI интерфейса. Но впоследствии, из-за ошибки возникли вопросы к последовательности инициализации, а точнее к корректной установке цветового режима. Напомню, что последовательность инициализации я брал "из интернета", а именно, с сайта "MicroTechics: Подключение дисплея на базе ST7735 к микроконтроллеру STM32". Забегая вперед скажу, что она вполне рабочая. Под спойлером я привел код функции инициализации для напоминания:
показать код инициализацииvoid st7735_init() { // hardware reset // hardware reset gpio_reset(GPIOB,RST); delay_ms(10); gpio_set(GPIOB,RST); delay_ms(10); // init routine chip_select_enable(); st7735_send(LCD_C,ST77XX_SWRESET); delay_ms(150); st7735_send(LCD_C,ST77XX_SLPOUT); delay_ms(150); st7735_send(LCD_C,ST7735_FRMCTR1); st7735_send(LCD_D,0x01); st7735_send(LCD_D,0x2C); st7735_send(LCD_D,0x2D); st7735_send(LCD_C,ST7735_FRMCTR2); st7735_send(LCD_D,0x01); st7735_send(LCD_D,0x2C); st7735_send(LCD_D,0x2D); st7735_send(LCD_C,ST7735_FRMCTR3); st7735_send(LCD_D,0x01); st7735_send(LCD_D,0x2C); st7735_send(LCD_D,0x2D); st7735_send(LCD_D,0x01); st7735_send(LCD_D,0x2C); st7735_send(LCD_D,0x2D); st7735_send(LCD_C,ST7735_INVCTR); st7735_send(LCD_D,0x07); st7735_send(LCD_C,ST7735_PWCTR1); st7735_send(LCD_D,0xA2); st7735_send(LCD_D,0x02); st7735_send(LCD_D,0x84); st7735_send(LCD_C,ST7735_PWCTR2); st7735_send(LCD_D,0xC5); st7735_send(LCD_C,ST7735_PWCTR3); st7735_send(LCD_D,0x0A); st7735_send(LCD_D,0x00); st7735_send(LCD_C,ST7735_PWCTR4); st7735_send(LCD_D,0x8A); st7735_send(LCD_D,0x2A); st7735_send(LCD_C,ST7735_PWCTR5); st7735_send(LCD_D,0x8A); st7735_send(LCD_D,0xEE); st7735_send(LCD_C,ST7735_VMCTR1); st7735_send(LCD_D,0x0E); st7735_send(LCD_C,ST77XX_INVOFF); st7735_send(LCD_C,ST77XX_MADCTL); st7735_send(LCD_D,0xC0); st7735_send(LCD_C,ST77XX_COLMOD); st7735_send(LCD_D,0x05); st7735_send(LCD_C,ST7735_GMCTRP1); st7735_send(LCD_D,0x02); st7735_send(LCD_D,0x1C); st7735_send(LCD_D,0x07); st7735_send(LCD_D,0x12); st7735_send(LCD_D,0x37); st7735_send(LCD_D,0x32); st7735_send(LCD_D,0x29); st7735_send(LCD_D,0x2D); st7735_send(LCD_D,0x29); st7735_send(LCD_D,0x25); st7735_send(LCD_D,0x2B); st7735_send(LCD_D,0x39); st7735_send(LCD_D,0x00); st7735_send(LCD_D,0x01); st7735_send(LCD_D,0x03); st7735_send(LCD_D,0x10); st7735_send(LCD_C,ST7735_GMCTRN1); st7735_send(LCD_D,0x03); st7735_send(LCD_D,0x1D); st7735_send(LCD_D,0x07); st7735_send(LCD_D,0x06); st7735_send(LCD_D,0x2E); st7735_send(LCD_D,0x2C); st7735_send(LCD_D,0x29); st7735_send(LCD_D,0x2D); st7735_send(LCD_D,0x2E); st7735_send(LCD_D,0x2E); st7735_send(LCD_D,0x37); st7735_send(LCD_D,0x3F); st7735_send(LCD_D,0x00); st7735_send(LCD_D,0x00); st7735_send(LCD_D,0x02); st7735_send(LCD_D,0x10); st7735_send(LCD_C,ST77XX_NORON); delay_ms(10); st7735_send(LCD_C,ST77XX_DISPON); delay_ms(100); chip_select_disable(); }
Ошибка была быстро найдена, но чтобы быть уверенным, я подключил дисплей к плате Arduino, в которую предварительно залил пример "graphicstest" из библиотеки Adafruit для работы с дисплеем ST7735, и логическим анализатором снял последовательность инициализации дисплея ST7735, чтобы сравнить ее со своей. И картинка получилась следующей.
Вначале идет команда сброса - "SWRESET":
Затем поступает команда выхода из спящего режима - "SLPOUT"
Промежутки между командами "SWREST", "SLPOUT" составляют что-то около 500 мс, хотя по документации достаточно выдерживать 120 мс.
Можно предположить, что тут ребята подстраховались, т.к. железо на котором запускается код Arduino может быть довольно разнообразным. С другой стороны, характеристики дисплеев купленных на али могут вас удивить, как в лучшую, так в худшую сторону.
Итак, после очередных 500 мс начинается главная последовательность инициализации:
В начале идут команды установки частоты работы дисплея FRMCTR1 и FRMCTR2. Первая устанавливает частоту дисплея в "нормальном" режиме, вторая - в спящем. Мы, кстати, спящий режим не используем.
Далее идет еще одна команда установки частоты кадров FRMCTR3 для работы в "Partial Mode". Затем идет команда настройки режима инверсии дисплея INVCTR. Параметр 0х07 устанавливает "Column Inversion" режим инверсии для Normal, Idle и Partial режимов работы дисплея.
Далее идут команды управления питанием: PWCTR1, PWCTR2 и PWCTR3, PWCTR4, PWCTR5, VMCTR1.
Затем командой 0x20 выключается режим инверсии дисплея:
До этого момента, у моя последовательность инициализации совпадала с логом библиотеки Adafruit, но дальше идут небольшие расхождения.
Команда MADCTL c параметром =0xC8 устанавливается цветовой режим BGR. В то время, как в моей последовательности инициализации, команда MADCTL передается параметр =0хС0.
Если посмотрим документацию, то увидим, установленный бит D3 задает цветовой режим BGR:
Далее командами CASET и RASET устанавливается рабочая область равная всему дисплею, т.е. 128х160 пикселей. В моей последовательности инициализации этих команд вообще нет.
Далее идут команды установки гамма-коррекции: GMCTRP1 (позитивная) и GMCTRN1 (негативная), с вереницей "мистических" аргументов, смысл которых мне лично малопонятен. Все аргументы совпадают с моей вариантом инициализации дисплея ST7735.
В завершении инициализации передаются команды переключения в нормальный режим и включения дисплея.
Итак, полученная последовательность инициализации дисплея ST7735, от моей отличается только BGR режимом цвета, и наличием команд CASET и RASET.
После инициализации дисплея ST7735, демо-программа Adafruit очищает дисплей заливкой черного цвета. Давайте взглянем на начало протокола заливки.
Здесь дважды передаются команды установки цветового режима RGB. Затем идут команды CASET и RASET:
После этого передается команда 0х2С для записи данных в графическую память дисплея, и собственно сами данные:
Команда 0х2С интересна в том плане, что для передачи данных у нас есть линия DC. Но данными могут быть не только данные графической памяти, но и параметры команд передаются как данные.
В результате анализа лога инициализации, я ничего сверхъестественного не нашел, кроме того, что полная заливка дисплея на Arduino занимает ~1200 мс, но не об этом речь. А вот команда MADCTL привлекла мое внимание.
Первая функция которая нам понадобится для работы с дисплеем ST7735 - это функция заливки прямоугольной области дисплея. С ее помощью мы будем очищать дисплей, закрашивать его в фоновый цвет, рисовать прямоугольники, вертикальные и горизонтальные линии, а также простые точки. На базе этой функции, мы потом будем писать вывод текста на экран дисплея.
Прототип этой функции уже был написан в прошлой статье, поэтому сейчас скачиваем проект для дисплея ST7735:
$ git clone git@gitlab.com:flank1er/stm32_bare_metal.git
Переходим в каталог проекта:
$ cd stm32_bare_metal/25_st7735_90fps/
Очищаем проект:
$ make clean
В модуле "src/st7735.c" нам нужна будет функция "void st7735_fill(uint16_t value)"
Во-первых, меняем список параметров функции на следующий:
void st7735_fill(uint8_t x0, uint8_t x1, uint8_t y0, uint8_t y1, uint16_t color)
Далее, через команды CASET и RASET устанавливаем рабочую область дисплея:
chip_select_enable(); st7735_send(LCD_C,ST77XX_CASET); st7735_send(LCD_D,0); st7735_send(LCD_D,x0); st7735_send(LCD_D,0); st7735_send(LCD_D,x1); st7735_send(LCD_C,ST77XX_RASET); st7735_send(LCD_D,0); st7735_send(LCD_D,y0); st7735_send(LCD_D,0); st7735_send(LCD_D,y1);
И здесь нужно пояснить следующее. Границы устанавливаются ВКЛЮЧИТЕЛЬНО. Это означает, что если мы, например, захотим вывести точку c координатам {x0,y0}, то х1 должен быть равен х0, а y1 должен быть равен y0. Если мы хотим вывести квадрат размером два на два, то х1 должен быть равен х0+1, а y1 должен быть равен y0+1. Аналогично, если мы хотим вывести квадрат размером 50х50, то х1 должен быть равен х0+49, а y1 должен быть равен y0+49.
Далее следует подсчитать количество пикселей заданной области. Для квадрата 50х50, оно будет 50*50=2500. Однако,в параметрах функции, рабочая область задется не ее шириной и высотой, а начальными и конечными точками. Поэтому, количество пикселей будем посчитывать так:
uint16_t len=(uint16_t)((uint16_t)(x1-x0+1)*(uint16_t)(y1-y0+1));
Цикл заливки будет следующим:
gpio_set(GPIOB,DC); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI for (uint16_t i=0; i < len; i++) { while (!(SPI1->SR & SPI_I2S_FLAG_TXE)); SPI1->DR=color; } while (!(SPI1->SR & SPI_I2S_FLAG_TXE) || (SPI1->SR & SPI_I2S_FLAG_BSY)); chip_select_disable(); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_8b|SPI_NSS_Soft|SPI_BaudRatePrescaler_4); SPI1->CR1 |= CR1_SPE_Set; // enable SPI
Здесь после цикла заливки добавлен цикл ожидания передачи последнего пикселя (выделено красным), и только после этого CS-линия отключает передачу. Без данного цикла ожидания, вывести простую точку из одного пикселя не получится, а, скажем, квадрат 2х2 будет состоять всего из трёх пикселей.
Используя функцию "st7735_fill()" мы можем для удобства ввести макросы для рисования прямоугольников, с заданием начальной точки, ширины и высоты:
#define st7735_rectangle(a,b,c,d,e) st7735_fill(a,a+b-1,c,c+d-1,e)
Макрос рисования точки:
#define st7735_point(x,y,c) st7735_fill(x,x,y,y,c)
Макрос заливки всего экрана:
#define st7734_clear(x) st7735_fill(0,0x7f,0,0x9f,x)
Макрос рисования вертикальной линии:
#define st7735_line_vertical(x,y,l,d,c) st7735_fill(x,x+d-1,y,y+l-1,c)
Макрос рисования горизонтальной линии:
#define st7735_line_vertical(x,y,l,d,c) st7735_fill(x,x+l-1,y,y+d-1,c)
Пока это все. Для тестирования данного кода, главный цикл в "main.c" пусть будет таким:
st7735_init(ST7735_BLACK); for(;;) { gpio_set(GPIOC,LED); st7735_clear(ST77XX_BLACK); delay_ms(3000); println("begin"); st7735_rectangle(0,50,5,50,ST77XX_RED); st7735_rectangle(30,50,55,50,ST77XX_GREEN); st7735_rectangle(70,50,105,50,ST77XX_BLUE); st7735_point(30,130,ST77XX_WHITE); st7735_line_horizont(5,154,118,2,ST7735_WHITE); st7735_line_horizont(5,5,118,2,ST7735_WHITE); st7735_line_vertical(5,5,150,1,ST7735_WHITE); st7735_line_vertical(123,5,150,1,ST7735_WHITE); println("end"); gpio_reset(GPIOC,LED); delay_ms(3000); }
Результат выполнения программы выглядит так:
Рамка из белых линий рисуется поверх цветных квадратов. Белая точка в один пиксел в нижнем левом углу.
Время выполнения отрисовки составляет 4 мс:
11:11:27.183-> begin 11:11:27.187-> end 11:11:33.194-> begin 11:11:33.198-> end
Из всех команд, что использовались при инициализации дисплея ST7735, более всего мое внимание привлекла MADCTL, которая управляет ориентацией дисплея и переключает цветовой режим RGB/BGR.
Если в монохромных дисплеях я использовал специальный алгоритм для отзеркаливания изображения дисплея, который требовал экранного буфера для своей работы, то тут все делается аппаратно командой MADCTL. Причем, ориентацию можно менять без сброса дисплея, т.е. ты часть текста можем написать по горизонтали, часть по вертикали. Один текст слева на право, другой справа на лево.
Давайте еще раз взглянем на документацию:
Здесь, три бита MY, MX, MV управляют ориентацией дисплея. MY - это зеркальное отображение по оси Y, MX - это зеркальное отображение по оси X, MV - это смена X и Y местами, т.е. смена портретной ориентации на альбомную и обратно.
Для управления экраном командой MADCTL введем новую функцию:
void st7735_set_madctl(uint8_t value) { chip_select_enable(); st7735_send(LCD_C,ST77XX_MADCTL); st7735_send(LCD_D,value); chip_select_disable(); }
Для проверки функции, составим такой главный цикл:
for(;;) { gpio_set(GPIOC,LED); delay_ms(3000); st7735_clear(ST77XX_BLACK); println("begin"); st7735_set_madctl(0xC0); st7735_rectangle(0,50,5,50,ST77XX_RED); println("end"); gpio_reset(GPIOC,LED); delay_ms(3000); st7735_set_madctl(0xA8); st7735_rectangle(0,50,5,50,ST77XX_RED); }
Здесь, дважды выводится один и тот же графический примитив - квадрат красного цвета, но в первый раз он выводится в "нормальных" настройках, а второй выводится в альбомной ориентации экрана и в BGR режиме. В итоге, на дисплее получается два квадрата, причем при выводе второго, первый меняет цвет с красного на синий. Смена цвета происходит "по щелчку", т.е. мгновенно. Полезная функция. Также, "по щелчку" меняется цветовая гамма при инвертировании дисплея: команды 0x20 и 0x21.
Про конвертацию шрифта восемь на восемь пикселей я рассказывал в предыдущей статье: "Шрифты для дисплея Nokia 1202", однако, в случае использования данного шрифта на дисплее ST7735, мне пришлось и шрифт переконвертировать, и драйвер вывода текста полностью переписывать с нуля. Но, опять же, давайте по порядку.
В частности, это приводит к тому, что шрифт 8х8 который на дисплее Nokia 1202 смотрелся как "жирный", на дисплее ST7735 смотрится как "мелкий". Хотя на фотографии масштаб трудно передать, но все же:
if (f.is_open()) { f.seekg (0, f.end); uint32_t length_file = (uint32_t)f.tellg(); f.seekg (0, f.beg); std::cout << "file size: " << length_file << std::endl; f.seekg(32,f.cur); // skip 32 bytes for (int z=0; z<256; z++) { std::cout << std::hex; uint8_t data; for(int i=0; i<8;i++) { f.read((char*)&data,sizeof(data)); std::cout << "0x" << (int)data << ", "; } std::cout << std::endl; } f.close(); }
Самый простой способ вывода текста - это вывод по одной букве. Для этого нам нужен небольшой буфер в ОЗУ и при этом не требуется никаких DMA. Такой код вполне можно использовать на ATmega8/Arduino и прочих 8-битниках с небогатой периферией.
Первое что нам потребуется - это небольшой буфер:
#define CHAR_LEN 64 // 64=(8 x 8 ) i.e. width * height #define MAX_CHAR 1 // #define BUF_LEN MAX_CHAR*CHAR_LEN // =64 volatile uint16_t buf[BUF_LEN];
В данном случае буфер выделен всего под один символ. Спецификатор "volatile" объявлен для того, что бы оптимизация GCC на "-Os" и "-O2" не "убивала" наш буфер.
Далее нам потребуется функция, которая будет декодировать символ наш буфер:
void cp866_char_to_buf(uint8_t x_pos, uint8_t ch, uint16_t fg_color, uint16_t bg_color) { uint16_t c = (((uint16_t)ch) * FONT_CP866_8_CHAR_WIDTH); // FONT_CP866_8_CHAR_WIDTH=8 for (uint8_t i=0; i < FONT_CP866_8_CHAR_WIDTH; i++) { uint8_t data=CP866_8x8[c+i]; uint16_t index= (i*FONT_CP866_8_CHAR_WIDTH); // FONT_CP866_8_CHAR_WIDTH=8 for (uint8_t j=0; j<FONT_CP866_8_CHAR_WIDTH;j++) { buf[index++]=(data & 0x80) ? fg_color : bg_color; data = data << 1; } } }
В принципе, ничего сложного.
Теперь нам потребуется функция, которая будет выводить полученный буфер на дисплей по заданным координатам:
void st7735_send_char(uint8_t x, uint8_t y, uint8_t ch, uint16_t fg_color, uint16_t bg_color) { cp866_char_to_buf(x,ch, fg_color,bg_color); chip_select_enable(); // 8x8 area st7735_send(LCD_C,ST77XX_CASET); st7735_send(LCD_D,0); st7735_send(LCD_D,x); st7735_send(LCD_D,0); st7735_send(LCD_D,x+7); st7735_send(LCD_C,ST77XX_RASET); st7735_send(LCD_D,0); st7735_send(LCD_D,y); st7735_send(LCD_D,0); st7735_send(LCD_D,y+7); st7735_send(LCD_C,ST77XX_RAMWR); #ifdef HW_SPI gpio_set(GPIOB,DC); // switch SPI to 16-bit mode SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI for (uint8_t i=0;i<BUF_LEN;i++) { uint16_t pixel=buf[i]; while (!(SPI1->SR & SPI_I2S_FLAG_TXE)); SPI1->DR=pixel; } while (!(SPI1->SR & SPI_I2S_FLAG_TXE) || (SPI1->SR & SPI_I2S_FLAG_BSY)); chip_select_disable(); // switch SPI to 8-bit mode SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_8b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI #else chip_select_disable(); #endif }
Фактически - это слегка измененная функция заливки, которая заливает квадрат 8х8 содержимым буфера. В самом начале происходит вызов функции "cp866_char_to_buf()". Возможно ее следовало бы объявить как "inline", т.к. она нигде больше не вызывается, но впоследствии, она будет вызываться также из функции печати строки одним массивом разом. Вариант с программным SPI я вырезал для простоты восприятия.
Имея функцию печати символа, мы всегда можем получить функцию печати строки:
void cp866_print_str(char *str, uint8_t x, uint8_t y, uint16_t fg_color, uint16_t bg_color) { uint8_t i=0; while (*str) { st7735_send_char(x + i*8,y,*str++,fg_color,bg_color); ++i; } }
Соответственно, имея функцию печати строки, мы можем получить функцию печати целого числа:
void cp866_print_num(uint32_t num, uint8_t x, uint8_t y, uint16_t fg, uint16_t bg){ uint8_t n[numlen]; uint8_t *s=n+(numlen-1); *s=0; // EOL do { *(--s)=('0' + num%10); num = num/10; } while (num>0); cp866_print_str((char*)s,x,y,fg,bg); }
Для проверки работы написанного кода, в главном цикле напишем следующее:
for(;;) { gpio_set(GPIOC,LED); st7735_clear(ST77XX_BLACK); delay_ms(3000); println("begin"); for (uint8_t i=0; i<16;i++) { cp866_print_str("\xe2\xa5\xaa\xe3\xe9\xa8\xa9\x20\xe1\xe7\xa5\xe2\x3a\x20",10,(i*8),ST7735_WHITE, ST7735_BLACK); cp866_print_num(count++,120,(i*8),ST7735_WHITE, ST7735_BLACK); } println("end"); gpio_reset(GPIOC,LED); delay_ms(3000); }
Результат работы:
Давайте откроем монитор последовательного порта, и оценим время вывода текста на экран:
15:04:32.288-> begi15:04:32.290-> n 15:04:32.311-> end 15:04:37.464-> begin 15:04:37.487-> end 15:04:42.639-> begin 15:04:42.661-> end15:04:42.662->
Итого имеем где-то 23 мс для заполнения экрана. В данном случае не весь экран заполняется текстом, а лишь только 17 символов. Если заполнять дисплей текстом полностью, по 20 символов, то время вывода страницы на дисплей составит около 28 мс:
14:14:46.342-> b14:14:46.343-> egin 14:14:46.370-> end 14:14:52.377-> begin 14:14:52.404-> end14:14:52.405-> 14:14:58.412-> begin 14:14:58.440-> end 14:15:04.446-> be14:15:04.447-> gin 14:15:04.474-> end
Это дает нам где-то 36 FPS. В принципе, неплохая отдача при минимуме вложений.
Мы можем немного выиграть в скорости вывода текста, если будем выводить строку не побуквенно, а целой строкой разом. Отпадет необходимость перед каждой буквой посылать команды CASET и RASET, и постоянно переключать SPI-интерфейс между 16-битным и 8-битным режимами. Однако в качестве расплаты за быстродействие придется пожертвовать оперативной памятью, т.к. для этого потребуется буфер не для одного символа, а для целой строки.
В частности, для 20-символьной строки размер буфера составит 2560 байт:
#define CHAR_LEN 64 // 64=(8 x 8 ) i.e. width * height #define MAX_CHAR 20 // =160/8 #define BUF_LEN MAX_CHAR*CHAR_LEN // =1280 volatile uint16_t buf[BUF_LEN];
Главная сложность при таком подходе - правильно сформировать буфер строки так, что бы буквы следовали рядами друг за другом, а не "сначала одна буква, потом другая, потом следующая и т.д.". Т.е. как-то так:
Здесь хитрая математика, в которую мне не очень хочется вдаваться. Готовая функция преобразования строки в содержимое буфера у меня получилась такой:
void cp866_char_to_buf(uint8_t x_pos, uint8_t ch, uint8_t len, uint16_t fg_color, uint16_t bg_color) { if ((x_pos + FONT_CP866_8_CHAR_WIDTH) > LCD_X) return; uint16_t c = ((uint16_t)ch - FONT_CP866_8_START_CHAR) * (FONT_CP866_8_CHAR_WIDTH * FONT_CP866_8_CHAR_HEIGHT); for (uint8_t i=0; i < FONT_CP866_8_CHAR_WIDTH; i++) { uint8_t data=CP866_8x8[c+i]; uint16_t index= x_pos + (i*len*8); for (uint8_t j=0; j<8;j++) { buf[index++]=(data & 0x80) ? fg_color : bg_color; data = data << 1; } } }
Функция печати строки, или печати полученного буфера, не сильно отличается от предыдущего примера:
void st7735_print_str(char* str, uint8_t len, uint8_t x, uint8_t y, uint16_t fg_color, uint16_t bg_color) { for(uint8_t i=0; i<len;i++) { cp866_char_to_buf((i*8),str[i], len,fg_color,bg_color); } chip_select_enable(); // 8x8 area st7735_send(LCD_C,ST77XX_CASET); st7735_send(LCD_D,0); st7735_send(LCD_D,x); st7735_send(LCD_D,0); st7735_send(LCD_D,(x+len*8-1)); st7735_send(LCD_C,ST77XX_RASET); st7735_send(LCD_D,0); st7735_send(LCD_D,y); st7735_send(LCD_D,0); st7735_send(LCD_D,(y+len*8-1)); st7735_send(LCD_C,ST77XX_RAMWR); #ifdef HW_SPI gpio_set(GPIOB,DC); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI for (uint16_t i=0; i<(CHAR_LEN * len); i++) { while (!(SPI1->SR & SPI_I2S_FLAG_TXE)); SPI1->DR=buf[i]; } while (!(SPI1->SR & SPI_I2S_FLAG_TXE) || (SPI1->SR & SPI_I2S_FLAG_BSY)); chip_select_disable(); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_8b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI #else chip_select_disable(); #endif }
И перейдем сразу к результату, время выполнения предыдущего примера уменьшилось на 4 мс:
11:20:19.178-> beg11:20:19.178-> in 11:20:19.198-> en11:20:19.198-> d 11:20:24.350-> beg11:20:24.351-> in 11:20:24.370-> en11:20:24.371-> d 11:20:29.522-> begi11:20:29.523-> n 11:20:29.542-> end11:20:29.543-> 11:20:34.695-> begi11:20:34.696-> n 11:20:34.715-> end11:20:34.716-> 11:20:39.867-> begin11:20:39.868-> 11:20:39.887-> end11:20:39.888-> 11:20:45.039-> begin11:20:45.040-> 11:20:45.059-> end
Если полностью заполнить дисплей текстом, что затраченное на отрисовку текста время составит 24 мс, т.е. также на 4 мс меньше чем в предыдущем случае, когда строка печаталась по одной букве:
09:55:59.218-> begin 09:55:59.242-> end 09:56:05.249-> begin 09:56:05.273-> end 09:56:11.281-> begin 09:56:11.305-> end
Т.о. имеем быстродействие в 42 fps. Чтобы его еще ускорить, следует использовать DMA.
Для того, чтобы кардинально увеличить скорость вывода текста на дисплей, следует использовать DMA.
Работает это так. Процесс разделяется на две стадии: 1) преобразование строки в буфер дисплея (функция "cp866_char_to_buf()"); 2) вывод буфера на дисплей при помощи DMA как бы "в фоне".
Данный способ не избавляет от задержек (блокировок) в выполнении программы. Если функция "cp866_char_to_buf()" будет работать медленно, то SPI будет простаивать. Если же она будет работать быстро, то придется ждать, когда SPI освободится.
Первое что понадобится - это сразу два буфера. В один будет происходить конвертация строки, другой в это время будет выводиться на дисплей.
#define CHAR_LEN 64 // 64=(8 x 8 ) i.e. width * height #define MAX_CHAR 20 // =160/8 #define BUF_LEN MAX_CHAR*CHAR_LEN // =1280 uint16_t buf[BUF_LEN]; uint16_t buf2[BUF_LEN];
Настройка DMA для работы с SPI будет выглядеть следующим образом.
Для начала следует включить тактирование DMA модуля:
#ifdef HW_SPI RCC->APB2ENR |= RCC_APB2Periph_SPI1; // enable SPI1 RCC->AHBENR |= RCC_AHBENR_DMA1EN; // enable DMA1 #endif
Далее идет конфигурация SPI и DMA:
#ifdef HW_SPI // --- SPI1 setup ---- SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_8b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); //SPI1->CR2 |= SPI_CR2_TXDMAEN; // enable DMA request SPI1->CR1 |= SPI_CR1_SPE; // --- DMA1 Setup -------------------- DMA1_Channel3->CCR |= DMA_CCR1_MSIZE_0; // =0x0400 (Memory Size is 16-Bit) DMA1_Channel3->CCR |= DMA_CCR1_PSIZE_0; // =0x0100 (Peripheral Size is 16-Bit) DMA1_Channel3->CCR |= DMA_CCR1_DIR; // =0x0010 (Read from Memory) DMA1_Channel3->CCR |= DMA_CCR1_MINC; // =0x0080 (Memory Increment Enable) DMA1_Channel3->CCR |= DMA_CCR1_PL; // =0x3000 (Priority Level is Very High) #endif
Здесь SPI пока настраивается на работу в 8-битном режиме без DMA, т.к. в этом режиме посылаются команды на дисплей ST7735. DMA настраивается на работу в 16-битном режиме, но сам DMA пока не включается.
Разрешаем прерывание DMA:
NVIC_SetPriority(DMA1_Channel3_IRQn,1); // set priority for DMA1_Channel3 IRQ NVIC_EnableIRQ(DMA1_Channel3_IRQn); // enable DMA1_Channel3 IRQ
Далее работает функция вывода текста посредством DMA. Ее можно разбить на несколько блоков. В первом блоке текст преобразуется в буфер, для вывода на дисплей:
void st7735_print_str_DMA(char* str, uint8_t len, uint8_t x, uint8_t y, uint16_t fg_color, uint16_t bg_color) { bool isDone=is_done_DMA; if (isDone || (buf_num != 0)) { buf_num=0; for(uint8_t i=0; i<len;i++) cp866_char_to_buf((i*8),str[i], len,fg_color,bg_color, buf); } else { buf_num=1; for(uint8_t i=0; i<len;i++) { cp866_char_to_buf((i*8),str[i], len,fg_color,bg_color, buf2); } }
Глобальная переменная "buf_num" хранит номер буфера который использовался в последний раз. Он считается пока "занятым", поэтому вызывается функция преобразования текста cp866_char_to_buf() в "свободный" буфер.
Далее ждем пока SPI шина освободится. После завершения работы DMA срабатывает прерывание, в обработчике которого сбрасывается значение глобальной переменной "is_done_DMA":
while (!is_done_DMA) {};
После того как SPI шина освободилась, посылаем команды CASET и RASET, настраиваем работу SPI и DMA, и в конце запускаем DMA:
chip_select_enable(); // 8x8 area st7735_send(LCD_C,ST77XX_CASET); st7735_send(LCD_D,0); st7735_send(LCD_D,x); st7735_send(LCD_D,0); st7735_send(LCD_D,(x+len*8-1)); st7735_send(LCD_C,ST77XX_RASET); st7735_send(LCD_D,0); st7735_send(LCD_D,y); st7735_send(LCD_D,0); st7735_send(LCD_D,(y+len*8-1)); st7735_send(LCD_C,ST77XX_RAMWR); #ifdef HW_SPI gpio_set(GPIOB,DC); is_done_DMA=false; // DMA setup DMA1_Channel3->CCR&=~(DMA_CCR1_EN); //Disable DMA DMA1_Channel3->CPAR=(uint32_t)(&SPI1->DR); //Address of uses peripheral interface if (buf_num == 0) DMA1_Channel3->CMAR=(uint32_t)&buf; //Address of buffer with data else DMA1_Channel3->CMAR=(uint32_t)&buf2; //Address of buffer with data DMA1_Channel3->CNDTR=(len * CHAR_LEN); // Size of data DMA1_Channel3->CCR|=DMA_CCR1_TCIE; // Enable of IRQ DMA1->IFCR |= DMA_IFCR_CGIF3; // Clear all interrupt flags // SPI setup SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 |= (SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR2 |= SPI_CR2_TXDMAEN; // enable DMA request SPI1->CR1 &= ~SPI_CR1_SPE; // clear // Enable SPI+DMA SPI1->CR1 |= CR1_SPE_Set; // enable SPI DMA1_Channel3->CCR|=DMA_CCR1_EN; //Включить DMA #else chip_select_disable(); #endif }
На этом работа функции отправки данных заканчивается.
После завершения работы DMA вызывается обработчик прерывания DMA, который отключает DMA и настраивает SPI на обычный 8-битный режим работы без DMA:
void DMA1_Channel3_IRQHandler(void) { DMA1->IFCR |= DMA_IFCR_CGIF3; // Clear all interrupt flags DMA1_Channel3->CCR &= ~(DMA_CCR1_EN); // Disable DMA1 chip_select_disable(); SPI1->CR1 &= CR1_SPE_Reset; // Disable SPI for setup SPI1->CR2 &= ~SPI_CR2_TXDMAEN; // Disable DMA request SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_8b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); // toggel to 8-bit SPI mode SPI1->CR1 |= SPI_CR1_SPE; SPI1->CR1 |= CR1_SPE_Set; // Enable SPI after setup is_done_DMA=true; }
Давайте посмотрим на скорость вывода текста:
17:39:33.537-> begin 17:39:33.548-> end 17:39:39.554-> b17:39:39.555-> egin 17:39:39.566-> end 17:39:45.573-> be17:39:45.574-> gin 17:39:45.585-> end 17:39:51.591-> beg17:39:51.592-> in 17:39:51.603-> end
Получаем 12 мс на печать текста на всю площадь дисплея. Это около 83 fps. Если отбросить печать цифр, и печатать только текст, то время сократится до 10 мс, т.е. 100 fps. По всей видимости частое переключение режимов приводит к ощутимым задержкам, и возможно имеет смысл производить конкатенацию строк, дабы выводить их одим махом.
Честно говоря, шрифт 8х8 на дисплее ST7735 выглядит мелковатым и трудно читаемым. Поковырявшись еще в консольных шрифтах, я также обнаружил шрифты размером 8х14 и 8х16. Наиболее интересным мне показался последний шрифт, который в ширину имеет 8 пикселов, а в высоту 16. Его глифы выглядят примерно так:
На экране обычного монитора, шрифты 8x8 и 8x16 визуально смотрятся так:
Если смотреть издалека, то шрифт 8х16 выглядит более читаемым, и я подумал, что для дисплея ST7735 он более подойдет.
Сконвертировать еще один шрифт несложно, но полный набор в 256 глифов будет занимать 4096 байт флеш-памяти. Два таких шрифта будут занимать уже 8192 байт. А если нам понадобятся более крупные шрифты, то счет уже пойдет на десятки килобайт. И тут самое время задуматься о сжатии данных шрифтов, т.к. если посмотреть на изображения глифов, то в глаза бросаются "пустые" строки, последовательность которых можно попытаться как-то закодировать.
Кодирование последовательностей одинаковых данных называется RLE-сжатем, при котором последовательность одинаковых байт заменяется парой: "число повторов + значащий байт".
Данный тип сжатия не должен сказаться на производительности вывода текста. Декодирование осуществляется по довольно простому алгоритму и требует только 16 байт в ОЗУ под раскодированный символ.
В целом, для полного набора шрифта CP866_8x16 получилось 2608 байт + 512 байт таблица смещений. Т.е. сжатие дало экономию около одного килобайта, или 25 процентов.
Лучше всего сжимаются сжимаются символы псевдографики, там, где имеются одинаковые вертикальные последовательности. Например, следующие глифы:
сжались следующим образом:
0xf7, 0x18, 0xf1, 0xf8, 0xf8, 0x18, // Символ 180 0xB4 ┤ lenght: 6 offset: 1994 0xf5, 0x18, 0xf1, 0xf8, 0x18, 0xf1, 0xf8, 0xf8, 0x18, // Символ 181 0xB5 ╡ lenght: 9 offset: 2000 0xf7, 0x36, 0xf1, 0xf6, 0xf8, 0x36, // Символ 182 0xB6 ╢ lenght: 6 offset: 2009 0xf7, 0x0, 0xf1, 0xfe, 0xf8, 0x36, // Символ 183 0xB7 ╖ lenght: 6 offset: 2015 0xf5, 0x0, 0xf1, 0xf8, 0x18, 0xf1, 0xf8, 0xf8, 0x18, // Символ 184 0xB8 ╕ lenght: 9 offset: 2021 0xf5, 0x36, 0xf1, 0xf6, 0x6, 0xf1, 0xf6, 0xf8, 0x36, // Символ 185 0xB9 ╣ lenght: 9 offset: 2030 0xf0, 0x36, // Символ 186 0xBA ║ lenght: 2 offset: 2039 0xf5, 0x0, 0xf1, 0xfe, 0x6, 0xf1, 0xf6, 0xf8, 0x36, // Символ 187 0xBB ╗ lenght: 9 offset: 2041
Единственный символ, который дал избыточность кодирования - это:
Он "сжался" в последовательность из 19 байт:
0x0, 0x80, 0xc0, 0xe0, 0xf1, 0xf0, 0xf1, 0xf8, 0xf1, 0xfc, 0xf1, 0xf8, 0xf1, 0xf0, 0xe0, 0xc0, 0x80, 0xf4, 0x0, // Символ 16 0x10 ► lenght: 19 offset: 177
Все остальные глифы сжались либо в 16 байт, либо меньше.
Для начала приведу значения именованных констант для шрифта 8х16:
#define FONT_CP866_16_LENGTH 256 // full cp866 code page #define FONT_CP866_16_START_CHAR 0 #define FONT_CP866_16_CHAR_WIDTH 8 #define FONT_CP866_16_CHAR_HEIGHT 2 #define FONT_CP866_16_ARRAY_LENGTH 2608 // compressed
Для декомпрессии символа нужен будет буфер на 16 байт, для полученного результата:
volatile uint8_t sym_buf[(FONT_CP866_16_CHAR_WIDTH*FONT_CP866_16_CHAR_HEIGHT)];
Функция декомпрессии символа в буфер у меня получилась такой:
void cp866x16_decompress_sym(uint8_t sym) { uint16_t idx=CP866_8x16_ID[sym]; uint8_t i=0; while (i<(FONT_CP866_16_CHAR_WIDTH*FONT_CP866_16_CHAR_HEIGHT)) { uint8_t s=CP866_8x16[idx]; if (s >= 0xf0) { s &= 0x0f; if (s==0) s=16; for (uint8_t j=0; j<s;j++) { uint8_t value=CP866_8x16[idx+1]; sym_buf[i+j]=value; } i += s; idx +=2; } else { sym_buf[i]=s; ++idx; ++i; } } }
По идее, данная функция должна выполняться "на лету" и не должна сказываться на скорости вывода текста.
Остальные функции аналогичны уже рассмотренным ранее, на примере вывода шрифта 8х8. Функция декодирования символа в буфер дисплея:
void cp866_16_char_to_buf(uint8_t x_pos, uint8_t ch, uint8_t len, uint16_t fg_color, uint16_t bg_color, uint16_t* buffer) { if ((x_pos + FONT_CP866_16_CHAR_WIDTH) > LCD_X) return; cp866x16_decompress_sym(ch); for (uint8_t i=0; i < (FONT_CP866_16_CHAR_WIDTH * FONT_CP866_16_CHAR_HEIGHT); i++) { uint8_t data=sym_buf[i]; uint16_t index= x_pos + (i*len*8); for (uint8_t j=0; j<8;j++) { buffer[index++]=(data & 0x80) ? fg_color : bg_color; data = data << 1; } } }
Здесь красным выделены отличия от варианта для шрифта 8х8.
Функция печати символа:
void cp866x16_send_char(uint8_t x, uint8_t y, uint8_t ch, uint16_t fg_color, uint16_t bg_color) { cp866_16_char_to_buf(x,ch, 1, fg_color,bg_color,buf); chip_select_enable(); // 8x8 area st7735_send(LCD_C,ST77XX_CASET); st7735_send(LCD_D,0); st7735_send(LCD_D,x); st7735_send(LCD_D,0); st7735_send(LCD_D,x+7); st7735_send(LCD_C,ST77XX_RASET); st7735_send(LCD_D,0); st7735_send(LCD_D,y); st7735_send(LCD_D,0); st7735_send(LCD_D,y+15); st7735_send(LCD_C,ST77XX_RAMWR); #ifdef HW_SPI gpio_set(GPIOB,DC); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI for (uint8_t j=0;j<128;j++) { uint16_t color; color=buf[j + x ]; while (!(SPI1->SR & SPI_I2S_FLAG_TXE)); SPI1->DR=color; } while (!(SPI1->SR & SPI_I2S_FLAG_TXE) || (SPI1->SR & SPI_I2S_FLAG_BSY)); chip_select_disable(); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_8b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI #else chip_select_disable(); #endif }
Здесь отличия лишь в нескольких константах.
Функции вывода строки и вывода целого числа, аналогичны варианту для шрифта 8х8, поэтому их приводить не буду.
В главный цикл поставим следующее:
for(;;) { gpio_set(GPIOC,LED); st7735_clear(ST77XX_BLACK); delay_ms(3000); println("begin"); for (uint8_t i=0; i<8;i++) { cp866x16_print_str_from_char("\xe2\xa5\xaa\xe3\xe9\xa8\xa9\x20\xe1\xe7\xa5\xe2\x3a\x20", 12, (i*16), ST7735_GREEN, ST7735_BLACK); cp866x16_print_num_from_char(count++,120,(i*16),ST7735_GREEN, ST7735_BLACK); } println("end"); gpio_reset(GPIOC,LED); delay_ms(3000); }
Результат работы прошивки выглядит так:
Это уже вполне читаемый шрифт.
Если замерить скорость вывода текста:
09:16:32.123-> begin09:16:32.124-> 09:16:32.138-> end 09:16:38.145-> begin 09:16:38.160-> end 09:16:44.167-> begin 09:16:44.182-> end
Получается 15 мс. Это 66 fps. Скорость работы не уменьшилась, а даже увеличилась. Парадокс))
Если текст печатать не по буквам, а единой строкой, то придется размер буфера строки увеличить в два раза:
uint16_t buf[2560]; // 5120 bytes
В функции печати строки будет пара изменений:
void cp866x16_print_str(char* str, uint8_t len, uint8_t x, uint8_t y, uint16_t fg_color, uint16_t bg_color) { for(uint8_t i=0; i<len;i++) { cp866_16_char_to_buf((i*8),str[i], len,fg_color,bg_color, buf); } chip_select_enable(); // 8x8 area st7735_send(LCD_C,ST77XX_CASET); st7735_send(LCD_D,0); st7735_send(LCD_D,x); st7735_send(LCD_D,0); st7735_send(LCD_D,(x+len*8-1)); st7735_send(LCD_C,ST77XX_RASET); st7735_send(LCD_D,0); st7735_send(LCD_D,y); st7735_send(LCD_D,0); st7735_send(LCD_D,(y+len*16-1)); st7735_send(LCD_C,ST77XX_RAMWR); #ifdef HW_SPI gpio_set(GPIOB,DC); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI for (uint16_t i=0; i<(CHAR_LEN * len); i++) { while (!(SPI1->SR & SPI_I2S_FLAG_TXE)); SPI1->DR=buf[i]; } while (!(SPI1->SR & SPI_I2S_FLAG_TXE) || (SPI1->SR & SPI_I2S_FLAG_BSY)); chip_select_disable(); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_8b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI #else chip_select_disable(); #endif }
Здесь вдвое увеличивается область печати, и константа CHAR_LEN имеет значение уже не 64, а 128.
Если посмотреть на скорость работы:
10:24:33.554-> begin 10:24:33.567-> end10:24:33.568-> 10:24:39.574-> begin 10:24:39.587-> e10:24:39.588-> nd 10:24:45.594-> begin 10:24:45.608-> end
То получаем те же 13-14 мс. Т.е. с точки зрения выигрыша производительности, данный метод ничего не дает.
Если посмотреть на быстродействие с использованием DMA, то на отрисовку дисплея затрачивается около 13 мс:
11:50:51.992-> begin 11:50:52.005-> end 11:50:58.012-> begin 11:50:58.025-> end 11:51:04.031-> begin 11:51:04.044-> end
Т.е. какого-то серьезного выигрыша по скорости, в данном случае нет.
Драйвер шрифта 8х16 позволяет выводить и другие шрифты данного размера. Я на скорую руку преобразовал шрифт "Cybercafe", и добавил его в прошивку. Шрифт не содержит кириллицу, только латинские глифы, псевдографику и какие-то непонятные символы. Выглядит так:
На этом хотелось бы поставить не точку, но запятую. В целом, мне понравился вывод текста на функции одного символа. Минимум ресурсов при максимуме результата. Вывод текста все-таки не такая сложная задача, чтобы для этого требовалось DMA. Но работу с DMA надо было рассмотреть, т.к. в будущем это обязательно пригодится. Все исходники и готовые прошивки можно скачать с портала GitLab по ссылке https://gitlab.com/flank1er/stm32_bare_metal. Для данной статьи это проекты:
В следующий раз хотелось бы рассмотреть использование крупных шрифтов на 24 и 32 пикселя, а также конвертацию из TTF шрифтов.
В настоящее время существует кириллистический растровый шрифт высокого разрешения Terminus, который можно свободно использовать в своих проектах. В википедии можно прочитать следующее:
В шрифт включены наборы символов для размеров 6×12, 8×14, 8×16, 10×18, 10×20, 11×22, 12×24, 14×28 и 16×32 пикселей[2]. Доступно нормальное и полужирное (за исключением 6×12) начертание.
Шрифт моноширинный, без засечек. Лицензия OFL - свободная. Доступны кодировки KOI8-R и CP1251, кодировка CP866 не поддерживается, поэтому пришлось выбрать KOI8-R. Для нее доступны следующие шрифты:
PSF_KOI8_RV = ter-k14n.psf ter-k14b.psf ter-k16n.psf ter-k16b.psf PSF_KOI8_R = ter-k12n.psf ter-k18n.psf ter-k18b.psf ter-k20n.psf ter-k20b.psf ter-k22n.psf ter-k22b.psf ter-k24n.psf ter-k24b.psf ter-k28n.psf ter-k28b.psf ter-k32n.psf ter-k32b.psf
Здесь цифра - это размер шрифта по вертикали. Каждый шрифт доступен в обычном начертании (суффикс n) и в полужирном (суффикс b).
Для примеров, я возьму шрифты на 16пх, 24пх и 32пх. Всего шесть шрифтов, т.к. каждый шрифт в двух вариантах: "нормальный" и "полужирный".
Если говорить про "нормальный" шрифт на 16пх, то его начертание выглядит так:
Я не уверен, в чем причина, но строчные буквы "съехали" вверх со смещением 0x20. Я подумал, что с этим делать, и решил, что проще будет из кода строчных букв вычитать число 32.
На экране дисплея шрифт выглядит следующим образом:
Если взять полужирный шрифт, то в сравнении с нормальным шрифтом, он выглядит так:
На экране дисплея ST7735 полужирный шрифт Terminus 8x16 выглядит так:
В случае использования шрифта 12 на 24 пикселя, пришлось переделывать систему сжатия шрифта, т.к. предыдущее сжатие по алгоритму RLE было рассчитано на одно-байтовые последовательности данных.
Согласно стандарту PSF, если число пикселей по горизонтали больше восьми, но меньше шестнадцати, то данные кодируются парой байт. При этом "лишние", младшие(LSB), биты забиваются нулями. Т.о., один символ шрифта 12*24 будет занимать: 2*24 (горизонталь * вертикаль) =48 байт. Полный набор глифов из 256 символов, будет занимать (48*256)= 12288 байт.
Например, нулевой глиф, который в шрифте Terminus выглядит следующим образом:
Будет кодироваться последовательностью из 48 байт выделенных красным:
00000000 72 b5 4a 86 00 00 00 00 20 00 00 00 01 00 00 00 |r.J..... .......| 00000010 00 01 00 00 30 00 00 00 18 00 00 00 0c 00 00 00 |....0...........| 00000020 00 00 00 00 00 00 00 00 7f c0 40 40 40 40 40 40 |..........@@@@@@| 00000030 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 |@@@@@@@@@@@@@@@@| 00000040 40 40 40 40 7f c0 00 00 00 00 00 00 00 00 00 00 |@@@@............|
В этой последовательности, младшие 4 бита всегда нули:
00000000 72 b5 4a 86 00 00 00 00 20 00 00 00 01 00 00 00 |r.J..... .......| 00000010 00 01 00 00 30 00 00 00 18 00 00 00 0c 00 00 00 |....0...........| 00000020 00 00 00 00 00 00 00 00 7f c0 40 40 40 40 40 40 |..........@@@@@@| 00000030 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 |@@@@@@@@@@@@@@@@| 00000040 40 40 40 40 7f c0 00 00 00 00 00 00 00 00 00 00 |@@@@............|
Так же нужно обратить внимание, что здесь повторяющиеся последовательности состоят из 16-битных данных. На этом принципе я и решил построить алгоритм сжатия. Он получился даже проще чем предыдущий вариант.
Для примера, приведенная выше последовательность для нулевого глифа: "0x0, 0x0, 0x0, 0x0, 0x7fc0, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x7fc0, 0x0, 0x0, 0x0, 0x0, 0x0"
будет кодироваться следующим образом: "0x4000, 0x17fc, 0xd404, 0x17fc, 0x5000"
В целом, полный набор из 256 глифов сжимается с 12кБ до 4.5кБ, вместе с таблицей смещений. Т.е. коэффициент сжатия достигает 2.5 раза, а не на 25%, как в случае с 16-пиксельным шрифтом.
Набор глифов шрифта Terminus-k24 с нормальным и полужирным начертанием выглядит так:
В данном случае кодировка KOI8-R корректная, без смещений.
Для декомпрессии произвольного глифа нам понадобится буфер на 24 полуслова(16 бит).
static volatile uint16_t sym16_buf[(TERMINUS_K12x24N_CHAR_WIDTH*TERMINUS_K12x24N_CHAR_HEIGHT)];
Здесь именованные константы "TERMINUS_K12x24N_CHAR_WIDTH" равна 12, а "TERMINUS_K12x24N_CHAR_HEIGHT" равна 2.
Функция декомпрессии глифа у меня вышла такой:
void KOI8Rx24_decompress_sym(uint8_t sym, uint8_t font) { uint16_t idx; idx=(!font) ? TERMINUS_K12x24N_ID[sym] : TERMINUS_K12x24B_ID[sym]; uint8_t i=0; while (i<(TERMINUS_K12x24N_CHAR_WIDTH*TERMINUS_K12x24N_CHAR_HEIGHT)) { uint16_t s,j; s=(!font) ? TERMINUS_K12x24N[idx] : TERMINUS_K12x24B[idx]; j=(s >> 12); s &= (uint16_t)0x0fff; if (j == 0) j=16; for (uint8_t k=0;k<j;k++) sym16_buf[i++]=s; ++idx; } }
Через параметр функции "font" можно задавать шрифт: нормальный или полужирный:
#define REGULAR 0x0 #define BOLD 0x1
Остальные функции типовые. Преобразование глифа в дисплейный буфер:
void KOI8Rx24_char_to_buf(uint8_t x_pos, uint8_t ch, uint8_t len, uint8_t font, uint16_t fg_color, uint16_t bg_color, uint16_t* buffer) { if ((x_pos + TERMINUS_K12x24N_CHAR_WIDTH) > LCD_X) return; KOI8Rx24_decompress_sym(ch, font); for (uint8_t i=0; i < (TERMINUS_K12x24N_CHAR_WIDTH*TERMINUS_K12x24N_CHAR_HEIGHT); i++) { uint16_t data=sym16_buf[i]; data = data <<4; uint16_t index= (i*len*TERMINUS_K12x24N_CHAR_WIDTH); for (uint8_t j=0; j<TERMINUS_K12x24N_CHAR_WIDTH;j++) { buffer[index++]=(data & (uint16_t)0x8000) ? fg_color : bg_color; data = data << 1; } } }
Функция вывода буфера на дисплей ST7735 в данном случае примет следующий вид:
void KOI8Rx24_send_char(uint8_t x, uint8_t y, uint8_t ch, uint8_t font, uint16_t fg_color, uint16_t bg_color) { KOI8Rx24_char_to_buf(x,ch, 1, font, fg_color,bg_color,buf); chip_select_enable(); // 8x8 area st7735_send(LCD_C,ST77XX_CASET); st7735_send(LCD_D,0); st7735_send(LCD_D,x); st7735_send(LCD_D,0); st7735_send(LCD_D,x+11); st7735_send(LCD_C,ST77XX_RASET); st7735_send(LCD_D,0); st7735_send(LCD_D,y); st7735_send(LCD_D,0); st7735_send(LCD_D,y+23); st7735_send(LCD_C,ST77XX_RAMWR); #ifdef HW_SPI gpio_set(GPIOB,DC); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_16b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI for (uint16_t j=0;j<CHAR_LEN;j++) { uint16_t color; color=buf[j]; while (!(SPI1->SR & SPI_I2S_FLAG_TXE)); SPI1->DR=color; } while (!(SPI1->SR & SPI_I2S_FLAG_TXE) || (SPI1->SR & SPI_I2S_FLAG_BSY)); chip_select_disable(); SPI1->CR1 &= CR1_SPE_Reset; // disable SPI for setup SPI1->CR1 = (SPI_Mode_Master|SPI_DataSize_8b|SPI_NSS_Soft|SPI_BaudRatePrescaler_2); SPI1->CR1 |= CR1_SPE_Set; // enable SPI #else chip_select_disable(); #endif }
Здесь именованные константа "CHAR_LEN" равняется 24*12=288.
Для демонстрации, в главном цикле модуля "main.c" я задал печать на дисплей ST7735 обоими шрифтами.
for(;;) { gpio_set(GPIOC,LED); st7735_clear(ST77XX_BLACK); delay_ms(3000); println("begin"); for (uint8_t i=0; i<5;i++) { if (i%2) { KOI8Rx24_print_str_from_char("\xd3\xde\xc5\xd4\x3a", 10,(i*24),ST7735_GREEN,ST7735_BLACK, REGULAR); KOI8Rx24_print_num(count,80, (i*24),ST7735_GREEN,ST7735_BLACK, REGULAR); } else { KOI8Rx24_print_str_from_char("\xd3\xde\xc5\xd4\x3a", 10,(i*24),ST7735_GREEN,ST7735_BLACK, BOLD); KOI8Rx24_print_num(count,80, (i*24),ST7735_GREEN,ST7735_BLACK,BOLD); } ++count; } println("end"); gpio_reset(GPIOC,LED); delay_ms(3000); }
Результат выглядит так:
Здесь первая, третья и пятая строка выведены полужирным шрифтом, а вторая и четвертая - нормальным. Время обновления дисплея - около 9 мс:
13:39:59.088-> begin 13:39:59.097-> end 13:40:05.105-> begin 13:40:05.114-> end
Функции вывода полной строки и с использованием DMA я приводить не буду, т.к. они типовые, готовый код можно посмотреть в репозитории. Время обновления дисплея с использованием DMA зависит от оптимизации компилятора. При "-O2" оно составляет 7 мс, но в этом случае не всегда корректно работает функция печати числа. При "-Os" она не работает совсем, а при отключенной оптимизации, т.е. "-O0", все работает корректно, но время обновления дисплея ST7735 составляет те же 9 мс. В данном случае следовало бы переписать функцию вручную на ассемблере.
Шрифт размером 16х32 опять заставляет менять алгоритм сжатия, т.к. в этом варианте нет свободных 4 бита, куда можно было бы записать индекс, а кодировать двухбайтные последовательности таким же двухбайтным индексом, при том, что необходимо лишь 5 бит под индекс для последовательности из 32 элементов, ну как-то совсем накладно.
И в данном случае, алгоритм сжатия будет самый замороченный.
Общий принцип используемого варианта RLE-сжатия описан в википедии: Кодирование длин серий
Возьмём строку, состоящую из большого количества неповторяющихся символов:
ABCABCABCDDDFFFFFF алфавит целых чисел можно разделить на две части: положительные и отрицательные числа. Положительные числа используют для записи количества повторов одного символа, а отрицательные — для записи количества неодинаковых символов, следующих друг за другом. Посчитаем символы с учётом вышесказанного: * сначала друг за другом следуют 9 не одинаковых символов: «ABCABCABC»; * затем записаны 3 символа «D»; * наконец записаны 6 символов «F». Сжатая строка запишется в виде: -9ABCABCABC3D6F Исходная строка состоит из 18 символов, а сжатая — из 15. Размер данных уменьшился в 18/15=1.2 раза.
Именно этот вариант RLE кодирования я использовал для сжатия шрифта 16х32, чтобы избежать большого количества индексов.
Про индекс.
Для сжатия требуется 5-битный индекс, который позволит индексировать последовательность из 32 элементов. При этом 0 равняется 32, т.к. ноль повторов быть не может.
Еще один бит явлется флагом отрицательного числа.
Остается два бита, которые можно использовать по своему усмотрению. Например, для комбинации со "словарным" методом сжатия.
Т.к. имеется два шрифта Regular и Bold, которые требуется сжать, можно определить самые часто встречающиеся значения и закодировать их повторы с помощью лишних двух бит индекса.
Я сделал частотный анализ для шрифта Regular, и самым часто встречающимся значеним является ноль:
$ grep -Eo "0x0," ./regular.txt |wc -l
450
Далее идут "0x300c" и "0x180":
$ grep -Eo "0x300c," ./regular.txt |wc -l
114
$ grep -Eo "0x180," ./regular.txt |wc -l
98
Для полужирного шрифта на перовом месте также ноль:
$ grep -Eo "0x0," ./bold.txt |wc -l
450
А вот а втором и третьем месте идут значения "0x701c" и "0х380":
$ grep -Eo "0x701c," ./bold.txt |wc -l
117
$ grep -Eo "0x380," ./bold.txt |wc -l
117
Т.о., в обоих шрифтах самым часто встречающимся значеним является ноль. Если закодировать последовательность нолей специальным индексом, то получится экономия на 450*2=900 байт.
В данном случае ноль будет кодироваться хадкорно двумя установленными младшими битами в индексе. Чтобы кодировать другие словарные последовательности, нужно будет создавать отдельную таблицу со "словарем", и отдельный индекс который бы указывал на позицию в словаре. Но пока обойдемся без этого.
Т.о. структура выглядит так:
1 11111 11
где старший бит обозначенный синим определяет, является ли последовательность повторяющийся или нет, далее идут пять битов индекса обозначенные зеленым. Два младших бита могут принимать значение 00 или 11 (3). Если младшие биты сброшены в ноль, то за индексом идут данные, если же они установлены в единицы, то индекс кодирует количество нулей.
Индекс декодируется следующей структурой:
if ((index & 0x03) == 0x03) { ... } else if (index & 0x80) { ... } else { ... }
Т.е. сначала проверяются младшие биты, затем проверяется старший бит, после чего отрабатывается оставшийся вариант.
Приведу пару примеров кодирования символов. Сперва простой случай, символ "0х0":
В оригинальном файле шрифта Он кодируется следующей последовательностью:
$ hexdump -C ter-k32n.psf |head 00000000 72 b5 4a 86 00 00 00 00 20 00 00 00 01 00 00 00 |r.J..... .......| 00000010 00 01 00 00 40 00 00 00 20 00 00 00 10 00 00 00 |....@... .......| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 3f fc 3f fc |............?.?.| 00000030 30 0c 30 0c 30 0c 30 0c 30 0c 30 0c 30 0c 30 0c |0.0.0.0.0.0.0.0.| * 00000050 3f fc 3f fc 00 00 00 00 00 00 00 00 00 00 00 00 |?.?.............|
В данном случае имеются только повторяющиеся последовательности:
0x6: 0x0, 0x2: 0x3ffc, 0x10: 0x300c, 0x2: 0x3ffc, 6: 0x0,
где индекс записанный с двоеточием означает повторяющуюся последовательность. В закодированном виде данный символ будет выглядеть так:
0x1b, 0x8, 0xfc, 0x3f, 0x40, 0xc, 0x30, 0x8, 0xfc, 0x3f, 0x1b,
Напомню, что индекс записывается умноженный на 4. Т.е. восемь означает два.
Следующий пример будет посложнее:
Он кодируется следующей последовательностью:
$ hexdump -C ter-k32n.psf |head 00000060 00 00 00 00 00 00 00 00 00 00 00 00 1f f8 3f fc |..............?.| 00000070 70 0e 60 06 60 06 60 06 6e 76 6e 76 60 06 60 06 |p.`.`.`.nvnv`.`.| 00000080 60 06 60 06 6f f6 67 e6 60 06 60 06 60 06 70 0e |`.`.o.g.`.`.`.p.| 00000090 3f fc 1f f8 00 00 00 00 00 00 00 00 00 00 00 00 |?...............|
В упрощенной сжатой записи он будет выглядеть так:
0x6: 0x0, !0x3: 0x1ff8, 0x3ffc, 0x700e, 0x3: 0x6006, 0x2: 0x6e76, 0x4: 0x6006, !0x2: 0x6ff6, 0x67e6, 0x3: 0x6006, !0x3: 0x700e, 0x3ffc, 0x1ff8, 6: 0x0,
Где индекс с двоеточием - это повторяющиеся данные, а индекс записаный со знаком восклицания - НЕповторяющиеся данные.
Путем сжатия получится последовательность из 33 байт:
0x1b, 0x8c, 0xf8, 0x1f, 0xfc, 0x3f, 0xe, 0x70, 0xc, 0x6, 0x60, 0x8, 0x76, 0x6e, 0x10, 0x6, 0x60, 0x88, 0xf6, 0x6f, 0xe6, 0x67, 0xc, 0x6, 0x60, 0x8c, 0xe, 0x70, 0xfc, 0x3f, 0xf8, 0x1f, 0x1b,
В целом исходный шрифт весит 64*256=16384 байт. Путем сжатия он уменьшается до 5.5 килобайт. Т.е. удалось добиться сжатия в три раза.
Готовая функция декомпрессии символа у меня вышла такой:
void KOI8Rx32_decompress_sym(uint8_t sym, uint8_t font) { uint16_t idx; idx=(!font) ? TERMINUS_K16x32N_ID[sym] : TERMINUS_K16x32B_ID[sym]; uint8_t i=0; while (i<32) { uint8_t index = (!font) ? TERMINUS_K16x32N[idx++] : TERMINUS_K16x32B[idx++]; if ((index & 0x03) == 0x03) { index=(index >> 2); if (index == 0) index=0x20; for (uint8_t j=0; j<index;j++) { sym32_buf[i++]=0x0; } } else if (index & 0x80) { index &=0x7f; index = index>>2; if (index == 0) index=0x20; for(uint8_t j=0;j<index;j++){ uint16_t a1=(!font) ? TERMINUS_K16x32N[idx++] : TERMINUS_K16x32B[idx++]; uint16_t a2=(!font) ? TERMINUS_K16x32N[idx++] : TERMINUS_K16x32B[idx++]; a1=((a2<<8) + a1); sym32_buf[i++]= a1; } } else { index=(index >> 2); if (index == 0) index=0x20; uint16_t a1=(!font) ? TERMINUS_K16x32N[idx++] : TERMINUS_K16x32B[idx++]; uint16_t a2=(!font) ? TERMINUS_K16x32N[idx++] : TERMINUS_K16x32B[idx++]; a1=((a2<<8) + a1); for(uint8_t j=0;j<index;j++){ sym32_buf[i++]= a1; } } } }
Остальные функции типовые, их можно посмотреть в репозитории. Вывод с помощью буфера целой строки и DMA я в данном случае не реализовывал, т.к. они дают незначительный прирост производительности, а для двух буферов в STM32F103C8 уже не хватает ОЗУ.
В главном цикле модуля "main.c" пусть будет следующий код:
for(;;) { gpio_set(GPIOC,LED); st7735_clear(ST77XX_BLACK); delay_ms(3000); println("begin"); KOI8Rx32_print_str_from_char("\xd3\xde\xc5\xd4\x3a",25,0,ST7735_GREEN,ST7735_BLACK,BOLD); KOI8Rx32_print_num(count++,110,0,ST7735_GREEN,ST7735_BLACK,BOLD); KOI8Rx32_print_str_from_char("count:",10,32,ST7735_RED,ST7735_BLACK,BOLD); KOI8Rx32_print_num(count++,110,32,ST7735_RED,ST7735_BLACK,BOLD); KOI8Rx32_print_str_from_char("\xfe\xe9\xf3\xec\xef\x3a",10,64,ST7735_BLUE,ST7735_BLACK,BOLD); KOI8Rx32_print_num(count++,110,64,ST7735_BLUE,ST7735_BLACK,BOLD); KOI8Rx32_print_str_from_char("NUMBER:",0,96,ST7735_YELLOW,ST7735_BLACK,BOLD); KOI8Rx32_print_num(count++,110,96,ST7735_YELLOW,ST7735_BLACK,BOLD); if (count >996) count=0; println("end"); gpio_reset(GPIOC,LED); delay_ms(3000); }
Результат работы программы выглядит следующим образом. Полужирный шрифт:
Нормальный шрифт:
На данном этапе уже можно создать какой-нибудь простенький экран для вывода данных. Например:
Также полезным будет масштабирование текста. В частности масштабирование в два раза даст шрифт 32х64, т.е. по высоте строка будет занимать половину дисплея, что смотрится достаточно крупно.
Масштабирования для ST7735 намного проше, чем аналогичная операция для Nokia 5110, в результате чего преобразование будет выполняется "на лету". У меня функция получилась следующего вида:
void KOI8Rx32_char_to_buf_2X(uint8_t x_pos, uint8_t ch, uint8_t len, uint8_t font, uint16_t fg_color, uint16_t bg_color, uint16_t* buffer) { if ((x_pos + (TERMINUS_K16x32N_CHAR_WIDTH*2)) > LCD_X) return; KOI8Rx32_decompress_sym(ch, font); for (uint8_t i=0; i < (TERMINUS_K16x32N_CHAR_WIDTH*TERMINUS_K16x32N_CHAR_HEIGHT); i++) { uint16_t data=sym32_buf[i]; uint16_t index= x_pos + (i*len*TERMINUS_K16x32N_CHAR_WIDTH*4); for (uint8_t j=0; j<TERMINUS_K16x32N_CHAR_WIDTH;j++) { uint16_t color=(data & (uint16_t)0x8000) ? fg_color : bg_color; buffer[index+(TERMINUS_K16x32N_CHAR_WIDTH*2)]=color; buffer[index+(TERMINUS_K16x32N_CHAR_WIDTH*2+1)]=color; buffer[index++]=color; buffer[index++]=color; data = data << 1; } } }
Аналогично можно сделать масштабирование в три и четыре раза. Генерируемый шрифт выглядит так:
В принципе, пикселизации практически не заметно.
В настоящее время практически все шрифты рисуются в векторе, и поставляются они в соответствующих форматах: TTF, OTF и пр. В операционных системах, эти шрифты "на лету" преобразовываются в растровые, после чего отрисовываются на дисплее. Это довольно "дорогая" операция в плане вычислительных ресурсов и поэтому во встраиваемых системах конвертацию выполняют предварительно с помощью какой-либо компьютерной программы, а в прошивку включают уже готовый растровый шрифт.
Таким образом возникает вопрос выбора такой программы, которая бы сконвертировала векторный шрифт в растровый. Но для грамотного выбора, нужно хотя бы немного разбираться в процессе конвертации. К сожалению, разные программы будут по разному конвертировать один и тот же шрифт. Причина этого в том, что произвольную кривую, которая задается множеством вещественных чисел, нельзя однозначно перенести на дискретную плоскость, где каждый пиксель имеет определенный физический размер. В википедии есть статья по алгоритму Брезенхэма, в которой о проблеме подробно рассказывается.
Но это еще полбеды. Полагаю, что все знают, что в отображении шрифтов есть функция "сглаживания", и что с отключенным сглаживанием шрифты смотрятся "с зазубринами", т.е. очень плохо.
На картинке один и тот же шрифт, но слева буква со сглаживанием, а справа без:
Полутона, т.е. сглаживание, сконвертировать никак не получится. Поэтому следует ожидать, что идеальные ровные линии изображенные на превью шрифта, после конвертации обернутся "заборчиком" с пикселизацией. Одним словом, шрифт для конвертации следует выбирать очень осторожно, просто чтобы не потратить свое время впустую.
В частности, для первого эксперимента я выбрал "жидко-кристаллический" шрифт "Digital-7":
У шрифта есть сайт: www.styleseven.com, где он заявлен как "freeware". Хотя, строго говоря, freeware может быть только ПО.
Данный шрифт достаточно узкий, благодаря чему мне удалось сконвертировать его в растровый шрифт размером 32х15 пикселей. Это означает, что его можно кодировать и печатать на функциях для шрифта "Terminus 32х16". Остальные шрифты, которые привлекли мое внимание, выглядели более квадратными, и требовали опять менять алгоритм сжатия.
Первым делом я пытался сконвертировать шрифты программой matrixFont, про которую упоминал в предыдущей статье.
Она в принципе неплохая, конвертируемый шрифт можно посмотреть визуально, есть редактор, при помощи которого можно поправить какие-то косяки. Но я не смог добиться что-бы результат выдавался в виде шрифта нужной мне высоты, т.е. 32 пикселя.
При импорте нужно задать размер шрифта (подчеркнуто красным), и в результате получается шрифт какой-то непонятной величины (размер холста):
Методом перебора значений, я сумел добиться на выходе шрифта высотой 32 пикселя и шириной 17:
Но позвольте спросить, какая взаимосвязь между шрифтом размера 28 и шрифтом 32х17 ?? Кроме того, мне нужен было размер 32х16 пикселей.
Также я подозревал, что с начертанием шрифтов тоже будут косяки. Если взять например букву "Х", то она сконвертировалась так:
В графическом редакторе глиф сопоставимого размера выглядел по другому:
Вроде мелочь, но неприятно. Несмотря на эти недочеты, программа matrixFont дает вполне приемлемый результат, и если бы я пользовался Windows, я бы остановил свой выбор на ней.
В поисках альтернативы, я покопался на гитхабе, и нашел проект: STM32-LCD_Font_Generator
Он написан на питоне, и имеет только текстовый интерфейс. Тем не менее, мой выбор пал именно на него.
Его использование очень простое:
$ python3 ./stm32-font.py --font ../Fonts/digital7.ttf --size 32
В результате чего появляется новый файл с результатом "FontDigital7Mono32.h", в котором содержится таблица сгенерированных символов. Причем есть визуальное отображение результата. Например та же буква "Х" получилась так:
// @5632 'x' (15 pixels wide) 0x00, 0x00, /* | | */ 0x00, 0x00, /* | | */ 0x00, 0x00, /* | | */ 0x00, 0x00, /* | | */ 0x00, 0x00, /* | | */ 0x00, 0x00, /* | | */ 0x00, 0x00, /* | | */ 0x00, 0x00, /* | | */ 0x30, 0x18, /* | ## ## | */ 0x70, 0x38, /* | ### ### | */ 0x38, 0x38, /* | ### ### | */ 0x38, 0x70, /* | ### ### | */ 0x1C, 0x70, /* | ### ### | */ 0x1C, 0xE0, /* | ### ### | */ 0x0E, 0xE0, /* | ### ### | */ 0x0E, 0xC0, /* | ### ## | */ 0x06, 0xC0, /* | ## ## | */ 0x06, 0x80, /* | ## # | */ 0x00, 0x00, /* | | */ 0x06, 0x80, /* | ## # | */ 0x06, 0xC0, /* | ## ## | */ 0x0E, 0xC0, /* | ### ## | */ 0x0E, 0xE0, /* | ### ### | */ 0x1C, 0xE0, /* | ### ### | */ 0x1C, 0x70, /* | ### ### | */ 0x38, 0x70, /* | ### ### | */ 0x38, 0x38, /* | ### ### | */ 0x70, 0x38, /* | ### ### | */ 0x30, 0x18, /* | ## ## | */ 0x00, 0x00, /* | | */ 0x00, 0x00, /* | | */ 0x00, 0x00, /* | | */
Во-первых, здесь имеется горизонтальная прорезь в середине буквы, которую matrixFont "потерял". Во-вторых все шрифты, которые я прогонял через данную программу преобразуются именно в заданное число пикселей по высоте. Ну и еще, в результате получился шрифт в 15 пикселей по ширине, что позволило использовать функции написанные для Terminus 32х16.
Шрифт имеет только заглавные латинские буквы. Визуально он мне не очень нравится, но, имхо, имеет право на существование. На дисплее ST7735 шрифт смотрится как-то так:
На мой взгляд, шрифт так же без проблем масштабируется в два раза без потери качества.
Размер массива шрифта - немного меньше двух килобайт. Количество символов - 69 (латинская раскладка без строчных букв).
В интернете есть достаточное количество привлекательных свободно-распространяемых шрифтов, сделанных небольшими командами или одиночными дизайнерами. Они смотрятся достаточно футуристично и как-бы сказать, незаезженно для глаза.
Многие шрифты, на мой взгляд, выглядят достаточно технично, т.е. пригодными для использования в технике. Для примера небольшой скрин:
И я подумал, что было бы неплохо попытаться сконвертировать в матричный формат и посмотреть как они будут выглядеть на дисплее ST7735. В итоге, я отобрал пять шрифтов (два жидко-кристальных и три футористичных), и приступил к работе.
Первое, что нужно заметить, набор символов у всех пяти шрифтов одинаковый - это латинский шрифт содержащий только заглавные буквы. Всего 69 символов:
Таким шрифтом можно написать какой-то заголовок или напечатать режим работы устройства, цифры можно выводить, но для смысловой информации я бы их не использовал.
Следующий момент на который я обратил внимание - шрифты достаточно квадратные. При высоте в 32 пикселя, для примера, шрифт BladeRunner занимает 40 пикселей в ширину.
Если подсчитать, то 40 пикселей это 5 байт. Один символ высотой 32 пикселя - это 32*5 = 160 байт. Весь набор - это 160*69 = 11040 байт. Итого 11 килобайт.
Для сжатия шрифта я решил использовать словарное сжатие. Если посмотреть кодирование символа "!", то тут сплошные повторяющиеся строки:
// @96 '!' (23 pixels wide) 0x00, 0x00, 0x00, /* | | */ 0x00, 0x00, 0x00, /* | | */ 0x00, 0x00, 0x00, /* | | */ 0x00, 0x00, 0x00, /* | | */ 0x00, 0x00, 0x00, /* | | */ 0x00, 0x00, 0x00, /* | | */ 0x00, 0x00, 0x00, /* | | */ 0x00, 0x60, 0x00, /* | ## | */ 0x00, 0x60, 0x00, /* | ## | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0x00, 0x00, /* | | */ 0x00, 0x00, 0x00, /* | | */ 0x00, 0x00, 0x00, /* | | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */ 0x00, 0xF8, 0x00, /* | ##### | */
Здесь всего три варианта строк, которые следует поместить в словарь, а сами строки заменить индексами словаря. Тогда символ будет закодирован 32 цифрами которые можно будет сжимать по RLE-алгоритму.
В качестве варианта RLE алгоритма я решил выбрать предыдущий вариант, когда индексом кодируется число повторяющихся значений и число неповторяющихся значений.
Напомню, что предыдущий вариант RLE - сжатия был самым "замороченным" и я решил его еще более "заморочить". Смысл в следующем.
Если посмотреть на фрагмент шрифта, то можно увидеть большое число повторяющихся последовательностей с небольшим по значению индексом (менее шестнадцати):
0 0 0 0 0 0 0 0 4 104 67 55 9 26 32 6 6 20 0 98 11 34 42 59 3 37 37 75 75 0 0 0 0 0 0 0 0 0 0 0 0 10 7 41 5 1 1 1 5 18 0 18 5 1 1 1 1 5 48 12 10 0 0 0 0 0 0 0 0 0 0 0 0 20 13 2 2 2 2 2 13 19 0 19 2 2 2 2 2 2 13 20 0 0 0 0 0 0 0 0 0 0 0 0 0 10 7 64 4 4 4 4 4 184 30 12 8 3 3 3 3 8 169 12 10 0 0 0 0 0 0 0 0 0 0 0 0 10 7 64 4 4 4 4 4 31 7 31 4 4 4 4 4 4 65 12 10 0 0 0 0 0 0 0 0 0 0 0 0 0 87 5 1 1 1 1 5 14 7 31 4 4 4 4 4 4 23 57 0 0 0 0 0 0 0 0 0 0 0 0 0 10 7 25 8 3 3 3 3 27 88 31 4 4 4 4 4 4 65 12 10 0 0 0 0 0 0 0 0 0 0 0 0 10 7 25 8 3 3 3 8 24 7 14 5 1 1 1 1 5 48 12 10 0 0 0 0 0 0 0 0 0 0 0 0 10 7 64 4 4 4 4 4 21 0 21 4 4 4 4 4 4 23 57 0 0 0 0 0 0 0 0 0 0 0 0 0 10 7 41 5 1 1 1 5 14 7 14 5 1 1 1 1 5 48 12 10 0 0 0 0 0 0 0 0 0 0 0 0 10 7 41 5 1 1 1 5 14 7 31 4 4 4 4 4 4 65 12 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 2 0 0 0 0 0 0 0 0 0 0 0 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 2 2 0 0 0 0 0 0 0 0 0 0 0 2 2 2 2 2 0 0
И т.к. теперь имеется словарь, его можно отсортировать по частотности вхождения "слов" в исходный текст. Отсюда следует возможность кодировать часть самых встречаемых последовательностей одним байтом, где часть битов будет кодировать число последовательностей, а остальная часть битов будет кодировать индекс последовательности в словаре.
11111111
В общем виде парсер индекса работает по следующему алгоритму:
// если двухбайтное число if (index == 0xf0) { ... } else { uint8_t k = (index >> 4); // если неповторяющаяся последовательность if (k == (uint8_t)0x0f) { ... // повторяющаяся последовательность со значением более или равному 16 } else if (k == 0x0 ) { ... } else { // повторяющаяся последовательность со значением менее 16 ... }
Для пример, следующая строка:
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x1, 0x0, 0x1, 0x3, 0x3, 0x3, 0x3, 0x3, 0x0,
сжимается в следующую последовательность:
0x90, 0xe3, 0xf3, 0x1, 0x0, 0x1, 0x53, 0x10,
Пример посложнее. Строка:
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x16f, 0x5d, 0x5d, 0x5d, 0x9e, 0x9e, 0x3b, 0x3b, 0x11, 0x164, 0x9c, 0x9c, 0x58, 0x2e, 0x2e, 0x2e, 0x15c, 0x154, 0x93, 0x93, 0x95, 0x95, 0x0,
сжимается в следующую в следующую последовательность:
0x90, 0xf0, 0x6f, 0x3, 0x5d, 0x2, 0x9e, 0x2, 0x3b, 0xf1, 0x11, 0xf0, 0x64, 0x2, 0x9c, 0xf1, 0x58, 0x3, 0x2e, 0xf0, 0x5c, 0xf0, 0x54, 0x2, 0x93, 0x2, 0x95, 0x10,
Здесь исходная строка состоит из 32 значений, четыре из которых двухбайтные числа. Вторая строка состоит из 28-и однобайтных чисел.
Предыдущий жидко-кристальный шрифт меня не очень впечатлил и я решил попробовать сконвертировать еще пару шрифтов. Выбор пал на DS_Digital и PixelLCD. Выглядят они примерно так. Шрифт DS_Digital:
Шрифт PixelLCD:
В сравнении со шрифтом Digital_7, DS_Digital выглядит как жирный, а PixelLCD еще жирнее)
Оба шрифта трех-байтные по ширине, первый 22 пикселя, второй 23 пикселя. По высоте оба шрифта имеют 32 пикселя. Словарь шрифта DS_Digital составил 213 слов, а PixelLCD - 205. Т.е. в обоих случаях индекс словаря не превышает одного байта.
Размер шрифта DS_Digital составил 1136+69*2+213*3=1913 байт. Размер шрифта PixelLCD составил 1127+69*2+205*3=1880 байт.
На дисплее ST7735 шрифт DS_Digital выглядит так:
Шрифт увеличенный в два раза получился таким:
На мой взгляд, у увеличенного шрифта межбуквенный интервал получился большим, его можно было сделать поменьше.
Тоже самое для шрифта PixelLCD. Нормальный шрифт:
Увеличенный в два раза шрифт:
Т.о. получился набор из трех жидко-кристальных шрифтов: тонкого(Digital_7), нормального (DS_Digital) и жирного (PixelLCD).
Последние три шрифта, это дизайнерские шрифты: Nulshock, DuneRise, BladeRunner. Выглядят они так:
Шрифт "Nulshock":
Шрифт "DuneRise":
Шрифт "BladeRunner"
Последние два шрифта, как не трудно догадаться, были разработаны для печати названий одноименных х/ф.
Не следует обольщаться красивыми изогнутыми линиями шрифтов, для конвертации следует выбирать скорее "прямоугольные" шрифты, а не такие. Мне просто было интересно как эта красота будет смотреться на TFT дисплее ST7735.
Все три шрифта пятибайтные, по ширине имеют 36, 38 и 40 пикселов, при высоте в 32 пиксела. Т.е.
Словарь шрифтов занимает 383, 300 и 467 пятибайтных слов. Сжатые шрифты занимают: Nulshock - 3390 байт, RuneRise - 2926 байт, BladeRunner - 4210 байт.
Шрифт Nulshock, на мой взгляд, получился неплохо:
На увеличенном шрифте, на скруглениях, вылазит пикселизация:
При этом глифы без скруглений, например "H" или "F" выглядят нормально.
Шрифт "DuneRaise" выглядит оригинально и тоже довольно неплохо:
Хуже всего получился шрифт BladeRunner, из-за того, что его глифы имеют наклон:
Также неказисто будут выглядеть любые наклонные шрифты.
Все примеры и готовые прошивки можно скачать с портала GitLab