Как перестать бояться и полюбить mbed [Часть 5]

в 8:19, , рубрики: atmel, FT800, HYT, IST, mbed, SiLabs, Wiznet, Блог компании ЭФО, программирование микроконтроллеров, Промышленное программирование, Разработка для интернета вещей, Разработка робототехники

Продолжаем серию публикаций, посвященных использованию среды ARM mbed для создания прототипа измерительного устройства.

Сегодня я наконец-то заканчиваю описание программной части — остались вопросы связанные с выводом на TFT-дисплей изображений и кириллицы. Сделаем всё красиво.

Как перестать бояться и полюбить mbed [Часть 5] - 1

Содержание цикла публикаций:


Напомню, что речь идет о разработке прототипа устройства с сенсорным экраном, которое служит для высокоскоростного измерения относительной влажности и (заодно) температуры. Для написания программы для микроконтроллера используется онлайн IDE mbed, позволяющая создавать железонезависимый код, который одинаково работает на отладочных платах от SiLabs, Atmel, Wiznet, STM32, NXP и других производителей.

1. Вывод изображений на TFT


Общая схема управления TFT-дисплеем с помощью графического контроллера от FTDI уже была описана в предыдущих статьях. Как и другие связанные с отрисовкой процедуры, вывод изображений на TFT-дисплей аппаратно реализован на графическом контроллере FT801. От управляющего хост-контроллера требуется только передавать на FT801 простые управляющие команды и необходимые данные.

Графические контроллеры серии FT8xx позволяют работать с изображениями формата .jpeg или .png. Изображение можно не только выводить на экран, но и трансформировать — изменять размер, вращать или перемещать по экрану, а также использовать как заставку. Все эти операции выполняются на графическом контроллере по приходу соответствующих команд от управляющего МК.

Говоря о загрузке изображений на графический контроллер, следует сразу разделить два подхода к работе с изображениями.

Как перестать бояться и полюбить mbed [Часть 5] - 2

В первом случае файл .jpeg берется из памяти управляющего МК или с внешнего носителя и сразу записывается в память графического контроллера FT8xx. В этом случае для загрузки используется команда CMD_LOADIMAGE. После загрузки изображения в память FT8xx становятся доступны все функции для работы с изображением — трансформации картинки и её вывод на дисплей. Такой подход является оптимальным, если вам есть где хранить изображения, то есть используется USB Flash-память или SD-карта.

Во втором случае изображение предварительно сжимается по алгоритму Deflate. Полученный в результате кодирования bitmap занимает гораздо меньше места, поэтому может храниться не только на внешнем носителе, но во встроенной памяти управляющего МК. Изображение загружается на графический контроллер FT8xx в сжатом виде, а распаковка данных выполняется уже графическим контроллером. Для загрузки сжатого изображения служит команда CMD_INFLATE. После того как изображение распаковано, с ним можно работать точно так же, как и в первом случае.

Поскольку в моём проекте не предполагается использование внешней памяти, будем рассматривать только первый случай — работу со сжатыми изображениями, хранящимися в памяти управляющего микроконтроллера. В результате я хочу выводить на дисплей три изображения: иконки для температуры и относительной влажности на экране с главным меню и фотографию датчика HYT-271 на экране с описанием датчика.

Как перестать бояться и полюбить mbed [Часть 5] - 3

Как перестать бояться и полюбить mbed [Часть 5] - 4

1.1. Форматирование изображения, компрессия


Алгоритм Deflate изначально создавался для zip-архивов, но благодаря отсутствию патентов успешно используется и для других целей, в том числе для сжатия изображений. Вот, кстати, отличная статья о Deflate и png.

Для преобразования изображения FTDI предлагает несколько консольных утилит, например img_cvt. Также доступны графические оболочки вроде EVE Screen Editor, которые не только позволяют конвертировать изображения, но и вообще сильно упрощают жизнь при создании программ для графического контроллера. По сути, EVE Screen Editor — это эмулятор TFT-модулей Riverdi.

Как перестать бояться и полюбить mbed [Часть 5] - 5

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

Используемый мной контроллер FT801 позволяет выводить на TFT-дисплей изображения девяти форматов: черно-белые L1, L4 и L8, а также RGB332, ARGB2, ARGB4, RGB565, PALETTED и ARGB1555, о них можно подробнее почитать здесь. Разные форматы позволяют получить разное соотношение качества изображений и размера кодирующего изображение бинарного файла.

Как перестать бояться и полюбить mbed [Часть 5] - 6
(на рисунке слева направо L1, L4, L8, RGB332, ARGB2, ARGB4, RGB565, PALETTED, ARGB1555)

По идее, наилучшее качество можно получить при использовании формата ARGB1555, однако на практике лучше попробовать разные варианты и подобрать наиболее подходящий. Фотография датчика действительно лучше всего выглядит после преобразования в ARGB1555 или RGB565, однако в какой-то момент мне не хватило встроенной памяти МК и пришлось отказаться от этих форматов в пользу RGB332. Смотрится более-менее прилично, а занимает 1865 байт вместо 7067 и 7816 у ARGB1555 и RGB565 соответственно.

С иконками получилось интереснее. Исходные файлы — .png с прозрачным фоном размером 37x77 и 53x67 точек. Прозрачность иконок можно сохранить только с кодированием типа ARGB, т.е. выбирать приходится из форматов ARGB2, ARGB4, и ARGB1555. Из них наиболее симпатичное сглаженное изображение дает не ARGB2, и, к моему удивлению, не ARGB1555, а ARGB4.

Как перестать бояться и полюбить mbed [Часть 5] - 7
(на рисунке слева направо ARGB2, ARGB4, ARGB1555)

После того как выбран подходящий формат, нужно сохранить проект в Screen Editor. В папке, куда Screen Editor был установлен, появится директория images, где для каждого из использованных изображений создан файл .binh. Он то нам и нужен.

1.2. Загрузка изображения в память графического контроллера


Получив бинарное представление изображения, возвращаемся из Screen Editor в mbed IDE, где в коде программы сохраняем все полученные bitmap как массивы. Иконка для влажности, например, выглядит вот так.

const unsigned char hum_icon[]={
120,156,165,86,203,109,195,48,12,205,177,40,250,25,193,35,120,4,141,160,17,60,130,71,200,181,55,143,224,123,46,26,193,11,20,208,4,133,54,168,71,96,31,165,248,43,74,145,82,18,8,20,138,79,228,35,41,193,244,73,117,218,86,250,175,250,251,65,243,115,200,203,133,88,236,83,232,153,130,92,171,145,29,109,82,135,108,214,168,44,117,172,29,209,1,59,22,35,21,197,82,91,165,125,100,45,121,254,188,147,33,131,94,110,255,37,17,177,52,248,189,149,209,247,155,136,85,2,82,195,238,124,109,186,213,102,132,156,83,189,80,152,91,222,111,130,213,207,226,94,92,60,93,152,186,137,150,185,185,98,53,193,178,229,51,223,53,194,121,237,217,255,235,133,215,248,229,115,250,195,126,11,109,68,228,33,79,159,63,75,251,184,135,224,228,162,202,247,167,188,83,58,238,251,178,106,156,119,162,51,219,60,36,121,164,59,35,237,113,189,77,6,107,40,121,163,49,85,46,121,110,200,215,194,39,199,199,74,59,152,116,247,176,19,83,34,242,85,172,111,28,57,226,140,76,185,74,185,58,6,117,130,151,46,136,186,100,215,9,54,93,128,85,240,27,78,182,49,83,255,189,54,2,227,255,96,37,30,146,106,33,103,85,88,171,49,142,113,123,45,234,145,191,17,194,228,249,59,138,233,74,34,113,187,172,204,236,254,182,76,194,253,91,170,100,211,188,128,154,252,219,167,197,26,49,39,147,190,41,216,9,239,185,5,131,150,253,192,65,161,7,206,91,135,240,250,101,84,249,232,103,49,197,95,24,13,226,26,68,183,56,103,196,58,91,255,63,65,221,118,198,
};

Чтобы загрузить это изображение в память графического контроллера FT801, понадобится отправить с управляющего МК три команды:

#define IMAGE_ADDR_HUMIDITY 29696
...
    (*_TFT).WrCmd32(CMD_INFLATE);
    (*_TFT).WrCmd32(IMAGE_ADDR_HUMIDITY);
    (*_TFT).WrCmdBufFromFlash(hum_icon, sizeof(hum_icon));

CMD_INFLATE — команда, сообщающая FT801, что в его память будет записано сжатое по алгоритму Deflate изображение.
IMAGE_ADDR_HUMIDITY — начальный адрес в памяти графического контроллера RAM_G, по которому будет доступно изображение.
hum_icon — массив, хранящий изображение.

После такой операции иконка «Влажность» будет доступна для вывода на экран пока память графического контроллера не будет очищена программно или в результате сброса.

1.3. Захват изображения


Следующий шаг после загрузки — «захват» изображения.

StartDL();
...
    (*_TFT).DL(BITMAP_HANDLE(0));
    (*_TFT).DL(BITMAP_SOURCE(IMAGE_ADDR_HUMIDITY));
    (*_TFT).DL(BITMAP_LAYOUT(ARGB4, 60, 38));
    (*_TFT).DL(BITMAP_SIZE(NEAREST, BORDER, BORDER, 30, 38));
...
FinishDL();

Командой BITMAP_HANDLE мы присваиваем каждому изображению (объекту Bitmap) указатель — номер от 0 до 31, по которому в дальнейшем можно будет обратиться к изображению.
Команда BITMAP_SOURCE указывает на адрес в памяти RAM_G графического контроллера FT8xx (см. п. Загрузка изображения).
Команда BITMAP_LAYOUT сообщает графическому контроллеру формат изображения и его размеры, а команда BITMAP_SIZE определяет размер выводимого изображения. Изменяя её аргументы можно, например, обрезать картинку справа или слева.

Захват изображения, также как и его загрузку в память графического контроллера, достаточно выполнить один раз после инициализации FT8xx. Однако важно понимать, что команды захвата изображения, в отличии от команд загрузки, являются так называемыми командами дисплей-листа, то есть их можно использовать только после команды начала дисплей-листа и до команды окончания дисплей-листа.
* О том, что такое дисплей-лист и с чем его едят, можно почитать во второй статье данного цикла.

1.4. Вывод изображения


После того как выполнены загрузка и захват изображения, его можно вывести на TFT-дисплей. Для этого используются команды группы BITMAP, например для вывода иконки «Влажность» поверх одной из кнопок главного меню я выполняю три команды:

StartDL();
...
// отрисовка прямоугольника (кнопки)
...
    (*_TFT).DL(BEGIN(BITMAPS));
    (*_TFT).DL(VERTEX2II(12 + 255, 62 + 10, 0, 0));
    (*_TFT).DL(END());
...
FinishDL();

В первых двух аргументах команды VERTEX2II указываются координаты для вывода на экран, а третий аргумент является указателем на иконку «Влажность», который был задан в BITMAP_SOURCE и BITMAP_HANDLE — «0».

Вывод иконки Температура и фотографии датчика выполняются абсолютно так же

StartDL();
...
    (*_TFT).DL(BEGIN(BITMAPS));
    (*_TFT).DL(VERTEX2II(12 + 260, 62 + 93 + 12 + 10, 1, 0));
    (*_TFT).DL(END());
...
FinishDL();

StartDL();
...
    (*_TFT).DL(BEGIN(BITMAPS));
    (*_TFT).DL(VERTEX2II(360, 140, 2, 0));
    (*_TFT).DL(END());
...
FinishDL();

Ссылка на полный исходный код проекта приводится ниже.

Использование пользовательских шрифтов


В статье, посвященной началу работы с графическим контроллером серии FT8xx, упоминалась поддержка стандартных виджетов — процедур для вывода относительно сложных графических объектов, которые аппаратно реализованы на графических контроллерах от FTDI. Среди виджетов есть текстовая строка, в mbed-библиотеке FT800_2 выводу строки на дисплей соответствует функция Text().

    (*_TFT).Text(22, 67, 27, 0, "Current humidity (rH)");

Аргументы функции — координаты первого символа строки (22, 67), номер используемого шрифта (27), дополнительные опции (0) и, собственно, текстовая строка. С координатами всё понятно, дополнительные опции также относятся только к положению строки на экране, поэтому поговорим о шрифтах.

Номер используемого шрифта — это число от 0 до 31, причем номера с 0 до 15 зарезервированы под пользовательские шрифты, а номера с 16 по 31 соответствуют шестнадцати встроенным шрифтам. Если в вашем приложении достаточно выводить только первые 128 ASCII символов и вам достаточно стандартного начертания этих символов, то можно остановиться на этом месте и не читать статью дальше — просто используйте шрифты с номерами 16-31.

Как перестать бояться и полюбить mbed [Часть 5] - 8

Если же вам нужны нестандартные начертания цифр и латиницы или требуется вывод символов, выходящих за пределы стандартного набора ASCII (например, кириллицы), то придется разбираться с загрузкой собственных шрифтов.

Для графических контроллеров FT8xx пользовательские шрифты — это почти те же bitmap, что и изображения, поэтому создание нового шрифта во многом повторяет процесс вывода изображений.

2.1. Форматирование шрифта, компрессия


На первом этапе нужно скачать или создать нужный шрифт (формат .ttf вполне подойдет) и получить бинарный файл для загрузки в графический контроллер FT8xxx. Для этого можно использовать либо консольную утилиту, либо тот же EVE Screen Editor.

Как перестать бояться и полюбить mbed [Часть 5] - 9

В Screen Editor шрифт импортируется также, как файлы изображений — в окно Content добавляется файл шрифта, а в окне Properties устанавливаются параметры его компрессии: формат, размер и charset.

Формат выбирается из трех опций — L1, L4 и L8. Разница, как и при конвертации изображений, в соотношении качества отрисовки и размера бинарного файла. Размер шрифта просто определяет ширину символов в пикселях, а наибольшего внимания заслуживает поле charset.

Для графических контроллеров FTDI шрифты по умолчанию состоят из 128 ASCII символов.

 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~
// отсчет не с нулевого, а с 32-го символа

Если вы используете только эти символы и добавляете новый шрифт только чтобы изменить их начертание — отлично, конвертируйте шрифт не изменяя charset. А если нужно добавить кириллицу или какой-то другой не входящий в ASCII символ, то charset придется заменить. В моём приложении понадобится вся кириллица, цифры, некоторые знаки препинания и математические символы, знаки градуса и процента, а также несколько латинских букв. В итоге измененный charset выглядит следующим образом:

 0123456789АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя.,:-°±%<>rHYTICS
// отсчет не с нулевого, а с 32-го символа

Теперь, также как при работе с изображением, сохраняем проект Screen Editor, заходим в папку, где установлен EVE Screen Editor и в директории /fonts находим бинарный файл .binh.

2.2. Загрузка в память графического контроллера


Загрузка нового шрифта повторяет процесс загрузки изображения — сохраняем бинарное представление шрифта как массив и загружаем на FT8xx по определенному заранее начальному адресу в память графического контроллера с помощью команды CMD_INFLATE.

#define FONT_ADDR_ROBOTO_REGULAR_30   16992
...
    (*_TFT).WrCmd32(CMD_INFLATE);
    (*_TFT).WrCmd32(FONT_SET_ROBOTO_REGULAR_30);
    (*_TFT).WrCmdBufFromFlash(font_RobotoRegular30, sizeof(font_RobotoRegular30));

2.3. Захват и установка шрифта


«Захват» пользовательского шрифта выполняется теми же командами, что и захват изображения, разница заключается в том что после команд BITMAP_HANDLE, BITMAP_SOURCE, BITMAP_LAYOUT и BITMAP_SIZE нужно ещё установить новый шрифт через вызов SetFont().

    (*_TFT).DL(BITMAP_HANDLE(3));
    (*_TFT).DL(BITMAP_SOURCE(FONT_ADDR_ROBOTO_REGULAR_30));
    (*_TFT).DL(BITMAP_LAYOUT(L4, 16, 33));
    (*_TFT).DL(BITMAP_SIZE(NEAREST, BORDER, BORDER, 32, 33));

    (*_TFT).SetFont(3, FONT_SET_ROBOTO_REGULAR_30);

2.4. Использование шрифта


Теперь среди пользовательских шрифтов под номером 3 значится загруженный нами шрифт Roboto Regular. Если бы при конвертации не был изменен charset этого шрифта, то для смены встроенного шрифта номер 27 на Roboto Regular нужно было бы всего лишь сменить

    (*_TFT).Text(22, 67, 27, 0, "Current humidity (rH)");

на

    (*_TFT).Text(22, 67, 3, 0, "Current humidity (rH)"); // выведет на экран ерунду

Однако мы собираемся выводить нестандартные для контроллера FT8xx символы, поэтому вместо явного указания строки («Current humidity (rH)») придется каждый раз лепить эту строку из отельных символов.

Рассмотрим строку «Относительная влажность». Задача состоит в том, чтобы каждому символу этой строки поставить в соответствие его номер в charset.

Если компилятор поддерживает кириллицу, то каждому символу начиная с прописных букв АБВ и заканчивая строчными эюя (с пропуском букв ё и Ё) будет соответствовать значение от 0xC0 до 0xFF. Значит чтобы сопоставить символы строки с номерами этих символов в charset нужно вычесть из кода каждого символа фиксированное значение. Например, если буква А в charlist занимает 32-ую позицию (0x20), а следующие за А буквы идут в том же порядке что и в таблице CP1251, то из кода каждого символа строки «Относительная влажность» (кроме пробела) нужно будет вычесть значение 0xA0.

Кодировка кириллицы CP1251

image

Однако компилятор может и не поддерживать кириллицу, mbed-овский как раз не поддерживает. Это значит, что компилятор не воспринимает кириллицу как коды с 0xC0 до 0xFF, поэтому мне не остается ничего кроме как использовать юникод, точнее UTF-8.

Каждый символ, не входящий в основную ASCII таблицу — кириллица, знаки ° и ± — представляется как двухбайтный код UTF-8. Я беру код каждого символа и ставлю ему в соответствие номер в своём charset.

Для латинских букв, которые тоже есть в charlist, нужно также заменить юникод на номер в charset, разница лишь в том что код латинских букв и других знаков ASCII типа точки, запятой и процента состоит из одного, а не двух байт.

Символ конвертируемой строки Код UTF-8 Порядковый номер в моём charset
АБВ… ноп 0xD090… 0xD0BF 43… 90
рст… эюя 0xD180… 0xD18F 91… 96
° 0xC2B0 112
± 0xC2B1 113
пробел 0x20 32
0… 9 0x30… 0x39 33… 43
. 0x2E 108
, 0x2C 109
: 0x3A 110
и так далее

Для выполнения такого преобразования строки создана соответствующая функция.

Функция преобразования строки CreateStringRussian

void Display::CreateStringRussian(const string rustext)
{
// CHANGED ASCII:
//  0123456789АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя.,:-°±%<>rHYTICS
    int len = rustext.length();
    int j = 0;
    for (int i = 0; i < len; i ++) {
        uint16_t res = uint8_t(rustext[i]);
        if (res > 0x7F) {
            res = res << 8 | uint8_t(rustext[i + 1]);
            // АБВ ... ноп
            if ((res >= 0xD090) && (res <= 0xD0BF)) {
                char offset = (char)(res - 0xD090);
                russianStr[j] = 32 + 11 + offset;
            // рст ... эюя
            } else if ((res >= 0xD180) && (res <= 0xD18F)) {
                char offset = (char)(res - 0xD180);
                russianStr[j] = 32 + 59 + offset;
            }
            // Degree sign
            else if (res == 0xC2B0) {
                russianStr[j] = 32 + 79;
            }
            // Plus-minus sign
            else if (res == 0xC2B1) {
                russianStr[j] = 32 + 80;
            }
            i++;
        } else {
            // Space
            if (res == 0x20) {
                russianStr[j] = 32;
            } 
            // Numbers
            else if (res >= 0x30 && res <= 0x39) {  
                russianStr[j] = 32 + 1 + (res - 0x30);
            } 
            // .
            else if (res == 0x2E) {
                russianStr[j] = 32 + 75;
            }
            // ,
            else if (res == 0x2C) {
                russianStr[j] = 32 + 76;
            }
            // :
            else if (res == 0x3A) {
                russianStr[j] = 32 + 77;
            }
            // -
            else if (res == 0x2D) {
                russianStr[j] = 32 + 78;
            }
            // %
            else if (res == 0x25) {
                russianStr[j] = 32 + 81;
            } 
            // <
            else if (res == 0x3C) {
                russianStr[j] = 32 + 82;
            }
            // >
            else if (res == 0x3C) {
                russianStr[j] = 32 + 83;
            }
            // "r"
            else if (res == 0x72) {
                russianStr[j] = 32 + 84;
            }
            // "H"
            else if (res == 0x48) {
                russianStr[j] = 32 + 85;
            }
            // "Y"
            else if (res == 0x59) {
                russianStr[j] = 32 + 86;
            }
            // "T"
            else if (res == 0x54) {
                russianStr[j] = 32 + 87;
            }
            // "I"
            else if (res == 0x49) {
                russianStr[j] = 32 + 88;
            }
            // "C"
            else if (res == 0x43) {
                russianStr[j] = 32 + 89;
            }
            // "S"
            else if (res == 0x53) {
                russianStr[j] = 32 + 90;
            }
        }
        j++;
    }
    russianStr[j] = 0;
}

Таким образом, чтобы вывести на TFT-дисплей строку «Относительная влажность» (или любую другую строку на русском языке) нужно сначала выполнить её преобразование, а затем использовать стандартный вывод строки, не забыв указать номер шрифта в качестве третьего аргумента.

    CreateStringRussian("Относительная влажность");
    (*_TFT).Text(15, 15, 3, 0, russianStr);

3. Итоговый результат


Исходный код готового проекта доступен на developer.mbed.org. К теме сегодняшней статьи относятся следующие файлы проекта:

  • /pictures.h — конвертированные изображения и шрифты
  • /TFT/display.ImagesAndFonts.cpp — загрузка и захват изображений и шрифтов, установка шрифтов
  • /TFT/display.StringsTransform.cpp — преобразования строк
  • /TFT/display.Draw_MainMenu.cpp — формирование экрана главного меню, где выводятся иконки Температура и Влажность
  • /TFT/display.Draw_AboutSensor.cpp — формирование экрана с описанием датчика, где выводится его фото

Таким образом я наконец-то заканчиваю описание создания проекта в онлайн IDE ARM mbed. Мы рассмотрели всё начиная с написания mbed-овского Hello Word до довольно объемной программы, использующей две библиотеки периферийных устройств — HYT для одноименного датчика и FT800_2 для TFT-модуля от Riverdi.

Волшебство в том, что полученная программа может быть скомпилирована в рабочую прошивку для любой из поддерживаемых в mbed отладочных плат.

В последней статье данного цикла поделюсь историей создания корпуса для этого девайса.

Заключение


В заключении традиционно благодарю читателя за внимание и напоминаю, что вопросы по применению продукции, о которой мы пишем на хабре, можно смело задавать на email, указанный в моем профиле.

Автор: ЭФО

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js