В уроке расскажу, как создавать программы для отображения стрелочных измерителей на круглом дисплее RP2040-LCD-1.28 в среде Arduino IDE. В качестве примера разработаем стрелочный вольтметр. Немного расскажу о работе с графикой на LCD-дисплее RP2040-LCD-1.28 и, в частности, о повороте графических объектов на плоскости.
Предыдущий урок Список уроков Следующий урок
Мне заказали разработку одного устройство. Задача примитивная.
Есть компрессор с аналоговым датчиком давления. Надо реализовать релейный регулятор для стабилизации давления. Измеренное значение ниже нормы – компрессор должен работать, давление выше нормы – компрессор надо выключить.
Естественно, необходимо задавать значение стабильного давления и отображать текущее. Казалось бы проще простого! LCD-дисплеи, LED-индикаторы, выбирай, какие нравятся!
Но заказчик – перфекционист в особо тяжелой форме. Захотел в качестве средства отображения использовать круглый LCD-дисплей RP2040-LCD-1.28. И даже нарисовал конкретную картинку.
Надо только стрелки заставить поворачиваться.
Таким образом, простая задача реализации релейного регулятора для компрессора превратилась в проблему отображения и поворота графических объектов. Хорошо, что в дисплее используется мощный 32х разрядный микроконтроллер.
Раз уж пришлось разбираться в этой задаче, я решил написать урок о реализации подобных устройств с графическим интерфейсом в виде стрелочного прибора.
LCD-дисплей RP2040-LCD-1.28.
Дисплей интересный. Картинка красивая, яркая. Я вывел первую попавшуюся иконку.
Даже на фотографии выглядит хорошо. В реальности еще эффектнее.
Цена для такого дисплея достаточно невысокая. На Али Экспресс это 1500 руб.
Я не буду подробно рассказывать об этом устройстве. Его параметры, установку в Arduino IDE, примеры программирования можно посмотреть по ссылке RP2040-LCD-1.28 - Waveshare Wiki.
Дам минимум информации, чтобы вы могли представить, о чем идет речь.
Итак, RP2040-LCD-1.28 это круглый цветной LCD-дисплей диаметром 1,28 дюйма (32,5 мм) и разрешающей способностью 240x240 пикселей.
- В устройстве используется микроконтроллер RP2040, аналог платы Raspberry Pi Pico.
- Это 32х разрядный, 2х ядерный микроконтроллер с тактовой частотой 133 мГц.
- Память: 264 кБайт SRAM и 2 мБайт FLASH.
- Множество периферии: 2 x SPI, 2 x I2C, 2 x UART, 4 x 12-bit ADC, 16 x PWM, датчик температуры, часы реального времени, акселерометр, гироскоп.
- Может программироваться в среде Arduino IDE.
Для подключения внешних устройств используются 2 разъема.
Работа с графическими объектами в RP2040-LCD-1.28.
Я выполнял действия в следующей последовательности.
По ссылке Demo code загрузил файл RP2040-LCD-1.28.zip
Из него разархивировал проект RP2040-LCD-1.28 -> Arduino -> RP2040-LCD-1.28.
В нем используются акселерометр и гироскоп, но нас интересуют стандартные библиотеки для работы с дисплеем. Работу с акселерометром и гироскопом я выбросил из проекта.
Теперь создаем собственно стрелочный прибор.
Первым делом необходимо вывести на дисплей шкалу прибора. Формализованная задача - вывести картинку разрешением 240x240.
Нарисовал шкалу прибора. Есть специальные программы для разработки шкал, в том числе и онлайн. Я не очень хороший дизайнер. Получилась такая картинка.
Ее надо разместить в файле ImageData.cpp в числовом виде.
const unsigned char scale[] = {
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
. . . . . . . . . . . . . . . . . . . . . . .
};
Цвет каждой точки кодируется 2 байтами в формате 65K RGB565. На красный цвет отводится 5 бит, на зеленый 6 бит и 5 бит для синего цвета.
В массиве пиксели передаются в последовательности слева направо, сверху вниз.
Существуют конвертеры графических файлов в код языка C/C++. Я использовал этот.
В результате конверсии появляется блок данных Image data, который надо копировать в массив. Массив для изображения шкалы я назвал scale. Всего должно быть 240*240*2 байтов.
В файле ImageData.h необходимо объявить этот массив для доступа к нему из основного файла проекта.
extern const unsigned char scale[];
Теперь о выводе на дисплей. Необходимо выполнить следующую последовательность действий.
Объявил указатель на объект (область памяти) , с которым будут производиться действия над графическими объектами.
uint16_t *BlackImage;
Выделил под него память 240*240*2 байт.
BlackImage = (uint16_t *)malloc(Imagesize);
Создал объект Image.
Paint_NewImage((uint8_t *)BlackImage, LCD_1IN28.WIDTH, LCD_1IN28.HEIGHT, 0, WHITE);
Теперь с этим объектом можно работать через функции стандартной библиотеки файла GUI_Paint.h .
Для загрузки изображения из массива scale можно использовать стандартную функцию
void Paint_DrawImage(const unsigned char *image, UWORD xStart, UWORD yStart, UWORD W_Image, UWORD H_Image);
В программе будет выглядеть так
Paint_DrawImage(scale, 0, 0, 240, 240);
Но я применил более быстрый вариант. Просто копировал массив scale в область памяти BlackImage.
for( int i=0; i < Imagesize; i++ ) *((unsigned char *)BlackImage + i) = scale[i]; // фоновая картинка
Собственно вывод изображения на дисплей производится функцией
LCD_1IN28_Display(BlackImage);
Приборная шкала появилась на экране.
Теперь надо формировать стрелку.
Поворот графических объектов на плоскости.
Стрелка – это картинка, которую нужно поворачивать относительно центра дисплея.
В двумерном пространстве поворот можно задать одним углом α.
Матрица поворота вычисляется по формуле:
При умножении любого вектора на матрицу длина вектора не изменяется. Для поворота изображения необходимо координаты каждой точки умножить на матрицу поворота.
Новые координаты точки вычисляются так;
Кроме поворота картинка стрелки должна быть с прозрачным фоном, иначе будет исчезать шкала и другие объекты на дисплее.
Я разработал функцию, которая выводит картинку на экран с поворотом и прозрачным фоном. Она расположена в конце основного файла.
void drawTurnImage(UWORD *Image, const unsigned char *image, float turn, uint16_t backGround );
- Image - указатель на область памяти дисплея. В нашем случае это BlackImage.
- image – указатель на массив (имя массива) с рисунком, который выводится на дисплей.
- turn – нормализованный коэффициент поворота. Значение 0 соответствует повороту на 0°, значение 1 – поворот на 360°.
- background – цвет, который принимается за фоновый и на дисплей не выводится.
Перед пересчетом координат точек разрешение картинки удваивается, после вычислений приводится к исходному разрешению. Это делается для того, чтобы не было пустых точек в повернутой картинке.
Создал картинку для стрелки.
И сформировал массив для нее в файле файле ImageData.cpp
const unsigned char arrow_0[] = {
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf
. . . . . . . . . . . .
};
Я нарисовал стрелку в исходном положении налево. Потом сообразил, что при значении аргумента turn=0 она должна “смотреть” вниз. Стрелку перерисовывать не стал, учел это смещение в функции drawTurnImage.
Создал переменную float voltage, в которой будет формироваться измеренное напряжение. С учетом этого стрелка рисуется следующей функцией.
drawTurnImage(BlackImage, arrow_0, MIN_TURN + (voltage / 6.) * (MAX_TURN - MIN_TURN), 65535); // стрелка
Начало и конец шкалы заданы константами:
#define MIN_TURN 0.125 // начало шкалы
#define MAX_TURN 0.8725 // конец шкалы
Решил продублировать измеренное значение в цифровом виде. Оказалось, что шрифт максимального размера в стандартной библиотеке 24 пикселя. Цифры получаются маленького размера, с трудом читаются, плохо выглядят.
Пришлось разработать функцию, которая выводит символ стандартного шрифта удвоенного размера.
void Paint_DrawChar2(UWORD Xpoint, UWORD Ypoint, const char Acsii_Char, sFONT* Font, UWORD Color_Foreground);
Вывод значения напряжения с двумя значащими разрядами после запятой производится следующим блоком.
sprintf(str, "%4f2", voltage + 0.005 );
Paint_DrawChar2(V_START_X, 186, str[0], &Font24, cl);
Paint_DrawChar2(V_START_X + 20, 186, str[1], &Font24, cl);
Paint_DrawChar2(V_START_X + 40, 186, str[2], &Font24, cl);
Paint_DrawChar2(V_START_X + 65, 186, str[3], &Font24, cl);
Цвет измеренного значения в цифровом виде изменяется в зависимости от самого значения, в соответствии с положением стрелки.
Для тестирования работы измерителя добавил в программу блок, который плавно наращивает значение переменной voltage. При достижении максимального значения переменная начинает уменьшаться.
Скетч такого варианта можно загрузить по ссылке.
При компиляции надо выбрать вариант оптимизации: Optimize: "Optimize (-O)". С предыдущим вариантом у меня не заработало.
Как я уже говорил, стрелка задана отдельной картинкой. Чтобы продемонстрировать это я заменил картинку стрелки на изображение рыбы.
Фоновый цвет – красный.
Для переключения на рыбу в качестве стрелки надо разрешить компиляции следующей строки.
//drawTurnImage(BlackImage, arrow_0, MIN_TURN + (voltage / 6.) * (MAX_TURN - MIN_TURN), 65535); // стрелка
drawTurnImage(BlackImage, fish_0, MIN_TURN + (voltage / 6.) * (MAX_TURN - MIN_TURN), 248); // рыба
Выглядит так.
Или в динамике.
Разработка вольтметра со стрелочной шкалой.
Большая часть проекта уже сделана. Осталось измерить напряжение и загрузить его в переменную voltage.
Для измерения напряжения будем использовать аналоговый вход A0 (GP26).
Подключил переменный резистор в качестве делителя 5 В.
Измерение напряжения производится стандартной для Ардуино функцией analogRead(). Производится 256 выборок, затем значение усредняется и приводится к диапазону 0…6 В.
// измерение напряжения
uint32_t avCode=0;
for( int i=0; i < 256; i++ ) avCode += analogRead(A0);
voltage= (float)avCode / (float)(4095. * 256) * MAX_VOLTAGE;
Serial.println(voltage);
Вот окончательный скетч проекта.
Это я изменяю напряжение на входе A0.
И вариант с рыбой в качестве стрелки.
Подведение итогов.
Не могу сказать, что я тщательно проработал заявленную тему. Скорее дал представление о ней и минимальные варианты практической реализации.
Тем не менее, все выше написанное может быть использовано в практических приложениях.
Что касается несделанного.
- Вычисления для поворота графических объектов требует достаточно много времени. Стрелка поворачивается не очень быстро. В связи с этим неплохо было бы ускорить вычисления, например, за счет перехода на вычисления координат точек в формате с фиксированной запятой. Вычисления с плавающей запятой требуют много времени.
- Можно использовать для вывода информации на дисплей второе ядро.
- Конечно, надо бы оформить работу с графикой библиотекой и дополнить ее другими полезными функциями.
Возможно дополню урок, когда закончу проект управления компрессором.
Я серьезно доработал проект в следующем уроке.
Здравствуйте Эдуард! Очень занимательный урок! Прочитал с удовольствием! А можно кроме аналогово входа подключить ещё энкодер?
Здравствуйте!
В проекте, который мне заказали, параметры должны устанавливаться энкодером. В ближайшие дни буду подключать.