Продолжаем серию публикаций, посвященных использованию среды ARM mbed для создания прототипа измерительного устройства. Сегодня говорим об основах работы с сенсорным вводом.
Содержание цикла публикаций:
- Обзор использованных программных и аппаратных решений.
- Начало работы с графическим контроллером FT800. Использование готовых mbed-библиотек для периферийных устройств.
- Подключение датчика HYT-271. Создание и публикация в mbed собственной библиотеки для периферийных устройств.
- Разработка приложения: Структура программы, работа с сенсорным экраном.
- Разработка приложения: Вывод изображений на дисплей, проблемы русификации.
- Печать деталей корпуса. Анализ ошибок проектирования и другие выводы.
По итогам предыдущих трех статей мы получили mbed-проект — программу, которая может быть скомпилирована в рабочую прошивку для любой отладочной платы с интерфейсами SPI и I2C и поддержкой в ARM mbed. В качестве испытуемых выступали отладочные платы SLSTK3400A от Silicon Labs, ATSAMD21-XPRO от Atmel и WIZwiki-W7500P от Wiznet.
Реализованная программа пока выполняет две задачи — опрос датчика температуры и относительной влажности HYT и вывод результатов измерений на TFT-дисплей от Riverdi. Проект доступен по ссылке.
Сегодня мы начинаем расширять базовый проект — вместо единственного рабочего экрана появится меню и несколько подразделов, между которыми можно будет переключаться с помощью сенсорного интерфейса.
1. Структура TFT-модуля
Итак, используемый TFT-модуль Riverdi состоит из собственно дисплея, графического контроллера серии FT8xx от FTDI и дополнительных компонентов — сенсорного контроллера, аудиоконтроллера и т.д. На сайте производителя можно найти полный список доступных TFT-модулей, а на сайте ЭФО — доступные со склада позиции и действующие цены.
Дисплеи различаются диагональю, разрешением, яркостью, типом подсветки, наличием крепежной или декоративной рамки, а также типом сенсорного экрана — выпускаются емкостные и резистивные TFT, а также дисплеи без поддержки сенсорного ввода. Встроенный графический контроллер соответствует возможностям дисплея: контроллер FT800 предназначен для резистивных дисплеев, FT801 — для емкостных, а более старшие модели FT81x отличаются поддержкой относительно большого разрешения и другими дополнительными функциями.
Я использую симпатичнейший модуль RVT43ULFNWC00 серии uxTouch диагональю 4.3'', выполненный в черной декоративной рамке. Модуль имеет ёмкостный сенсорный экран и, соответственно, встроенный графический контроллер FT801.
Мы уже говорили о порядке управления TFT-модулем Riverdi — управляющий контроллер подключается к микросхеме FT801 и обменивается с графическим контроллером простыми командами, то есть осуществляет чтение и запись определенных регистров FT801. В соответствии командами, полученными от управляющего контроллера, графический контроллер взаимодействует с дисплеем — осуществляет отрисовку и вывод изображений, реализует сенсорный ввод и работу аудиоканала.
В емкостных TFT-модулях, в отличии от моделей с резистивным экраном, установлен отдельный контроллер сенсорной панели. Впрочем, наличие этого дополнительного аппаратного блока никак не влияет на процесс управления TFT с точки зрения программиста.
2. Получение данных о касании
Графический контроллер FT801 поддерживает стандартный и расширенный режимы сенсорного ввода. В первом случае могут детектироваться только одиночные касания, а в расширенном режиме поддерживается мультитач. Используемый режим работы определяется значением в регистре REG_CTOUCH_EXTENDED:
- 0: extended mode — расширенный режим,
- 1: FT800 compatibility mode — стандартный single-touch режим, является режимом по умолчанию.
Я буду использовать только режим single-touch. Во-первых, для моего приложения его возможностей вполне достаточно, в во-вторых, так программа будет совместима с модулями на базе FT800.
При касании TFT-дисплея контроллер сенсорной панели передает на FT801 «сырые» данные об области касания. Эти данные записываются в регистр REG_CTOUCH_RAW_XY графического контроллера, после чего FT801 вычисляет координаты касания (x, y) и помещает результат в регистр REG_TOUCH_TAG_XY. Вычисление координат представляет собой матричные преобразования, в которых участвуют данные из REG_CTOUCH_RAW_XY и матрица координат, которая хранится в регистрах с REG_CTOUCH_TRANSFORM_A по REG_CTOUCH_TRANSFORM_F. К регистрам REG_CTOUCH_TRANSFORM нам ещё предстоит вернуться.
Для детектирования касания, в принципе, можно можно использовать данные из REG_TOUCH_TAG_XY, но для программиста предусмотрены более удобные инструменты.
Самый простой из этих инструментов — метки (TAG). Метка — это номер от 1 до 255, который может быть присвоен графическому объекту, т.е. прямоугольнику, точке, линии, кнопке, тексту и так далее. Во время касания в области отмеченного графического объекта, в регистре REG_TOUCH_TAG установится значение соответствующей метки. Таким образом, чтобы детектировать касание объекта, хост-контроллер должен периодически опрашивать регистр REG_TOUCH_TAG и сравнивать полученное значение с меткой, присвоенной интересующему объекту.
Контроллеры FT801 также поддерживают трекинг нажатия — автоматическое вычисление угла или линейного расстояния между точкой касания и точкой с заданными координатами. В этом случае кроме установки метки понадобится задать параметры её отслеживания (команда Track()), а для детектирования нажатия проверять регистр REG_TRACKER вместо REG_TOUCH_TAG.
Впрочем, давно пора перейти к примерам.
2.1. Работа с готовыми виджетами
Сначала рассмотрим виджеты — графические объекты, которые аппаратно реализованы на графическом контроллере. Три наиболее простых виджета для сенсорного интерфейса — кнопка, ряд кнопок и слайдер.
Для отрисовки виджетов служат простые команды, входящие в библиотеку FT800_2, о которой мы говорили во второй статье цикла. Для отрисовки показанных на рисунке элементов служат вот такие простые функции:
TFT.FgColor(0xC1004D);
TFT.Keys(27, 127, 271, 41, 29, 0, "123");
TFT.Button(26, 33, 120, 36, 27, OPT_FLAT, "Button");
TFT.Slider(244, 45, 161, 17, 0, 17, 100);
Чтобы элементы не только отображались, но и реагировали на касание, нужно добавить созданным объектам метки и опрашивать хранящий метку регистр.
Элементам объекта Ряд кнопок (Keys) метки присваиваются автоматически, поэтому для детектирования нажатия кнопок «1», «2» и «3» достаточно просто опрашивать регистр REG_TOUCH_TAG:
TFT.Keys(27, 127, 271, 41, 29, 0, "123");
char pressedButton = TFT.Rd8(REG_TOUCH_TAG);
Элементам типа Button метку нужно назначить вручную:
TFT.DL(TAG(1));
TFT.Button(26, 33, 120, 36, 27, OPT_FLAT, "Button");
char pressedButton = TFT.Rd8(REG_TOUCH_TAG);
При работе со слайдером используется трекинг — кроме установки метки, нужно выполнить команду Track, устанавливающую параметры трекинга, а опрашивать следует 32-разрядный регистр REG_TRACKER, а не 8-разрядный REG_TOUCH_TAG:
char sliderVal = 0;
TFT.Track(244, 45, 161, 17, 2);
...
while(1)
{
TFT.DL(TAG(2));
TFT.Slider(244, 45, 161, 17, 0, sliderVal, 255);
int pressedSlider = TFT.Rd32(REG_TRACKER);
sliderVal = (pressedSlider >> 16) * 255 / 65536;
}
Приведенный код позволит регистрировать положение слайдера (в данном случае оно изменяется от 0 до 255) и перерисовывать слайдер в соответствии с текущим положением бегунка.
2.2. Калибровка экрана
На самом деле для работы сенсорных элементов недостаточно создать дисплей-лист с описанием графических элементов и опрашивать соответствующий регистр.
Дело в том, что после включения и инициализации TFT-модуля все регистры графического контроллера сброшены в значения по умолчанию. Чуть выше мы говорили, что для вычисления координат нажатия используется матрица, хранящаяся в регистрах REG_CTOUCH_TRANSFORM. Так вот, эти регистры после инициализации тоже «пусты», а чтобы записать туда корректные значения, нужно выполнить калибровку сенсорного экрана.
Для калибровки служит инструкция графического сопроцессора CMD_CALIBRATE. Грубо говоря, по приходу этой инструкции графический контроллер FT8xx выводит на дисплей друг за другом три точки, на которые нужно нажать. В соответствии с координатами трех касаний в регистры REG_CTOUCH_TRANSFORM заносятся нужные значения.
DLstart();
DL(CLEAR_COLOR_RGB(64,64,64));
DL(CLEAR(1,1,1));
DL(COLOR_RGB(0xff,0xff,0xff));
Text((DispWidth/2), (DispHeight/2), 27, OPT_CENTER, "Please Tap on the dot");
Calibrate(0);
Flush_Co_Buffer();
WaitCmdfifo_empty();
Функцию калибровки следует вызывать по окончании инициализации TFT-дисплея. Выглядит всё это следующим образом:
С проектом, запущенным на видео можно ознакомиться по ссылке.
Конечно, странно было бы заставлять пользователя калибровать экран после каждого включения устройства. Поэтому имеет смысл единожды провести калибровку, считать содержимое регистров REG_CTOUCH_TRANSFORM_A… REG_CTOUCH_TRANSFORM_F, сохранить данные, а потом программно заносить их в регистры REG_CTOUCH_TRANSFORM после инициализации TFT-модуля.
Калибровочные данные можно хранить в какой-нибудь EEPROM, а если совесть позволяет действовать совсем топорно, то можно проводить «калибровку» примерно вот так:
void Display::Calibration()
{
char calibration[25] = {98, 99, 0, 0, 182, 254, 255, 255, 245, 142, 248, 255, 117, 254, 255, 255, 34, 98, 0, 0, 123, 154, 248, 255};
for (int i = 0; i < 24; i++) {
(*_TFT).Wr8(REG_TOUCH_TRANSFORM_A + i, calibration[i]);
}
}
3. Использование сенсорного интерфейса в собственном проекте
Моё конечное приложение — это главное меню, на котором отображаются текущие данные о температуре и относительной влажности, а также несколько подразделов:
Поскольку вопросов вывода изображений и русификации мы коснемся только в следующей статье, сегодня будем разбирать проект-полуфабрикат:
Для навигации между разделами служат два перечисления. Первое — это все метки, присвоенные графическим объектам:
typedef enum {
NONE_PRESS,
CURR_TEMP_PRESS,
CURR_HUM_PRESS,
MENU_PRESS,
} pressValues;
Второе — список экранов, между которыми мы переключаемся.
typedef enum {
MENU_SCREEN,
CURR_HUM_SCREEN,
CURR_TEMP_SCREEN,
} screenValues;
Логика работы приложения описывается следующим образом:
disp.activeScreen = MENU_SCREEN;
disp.pressedButton = NONE_PRESS;
// change active screen depending on pressed area
while(1) {
dataUpdate();
disp.pressedButton = disp.GetTouch();
// Main menu screen
if (disp.activeScreen == MENU_SCREEN) {
disp.MainMenu(SENSOR.humidity, SENSOR.temperature);
if (disp.pressedButton) {
wait_ms(150);
if (disp.pressedButton == CURR_TEMP_PRESS) {
disp.activeScreen = CURR_TEMP_SCREEN;
} else if (disp.pressedButton == CURR_HUM_PRESS) {
disp.activeScreen = CURR_HUM_SCREEN;
}
disp.pressedButton = NONE_PRESS;
}
// Any other screen
} else {
// You can back to main menu from any screen
if (disp.pressedButton == MENU_PRESS) {
disp.pressedButton = NONE_PRESS;
disp.activeScreen = MENU_SCREEN;
} else {
// Screen with current temperature / humidity
if (disp.activeScreen == CURR_TEMP_SCREEN) {
disp.CurrentTemperature(SENSOR.temperature);
} else if (disp.activeScreen == CURR_HUM_SCREEN) {
disp.CurrentHumidity(SENSOR.humidity);
}
}
}
}
Для отрисовки всех трех экранов используются только простые графические примитивы (LINES, POINTS, EDGE_STRIP_B и RECTS) и виджеты для вывода текста и чисел. Полагаю что после прочтения статьи Как перестать бояться и полюбить mbed [Часть 2] и просмотра исходников не должно остаться вопросов по отрисовке элементов меню и графиков, поэтому остановлюсь только на реализации элементов сенсорного ввода.
Я не использую готовые виджеты от FTDI, поскольку на некоторых кнопках главного меню будут расположены изображения и строки разного размера, а ещё потому что меня бесят закругленные углы кнопок. Поэтому мои кнопки представляют из себя не виджеты button, а прямоугольники, поверх которых выводятся текстовые строки и другие элементы.
Метки (TAG) для «обычных» графических объектов, например прямоугольников, устанавливаются так же как при использовании виджетов. Всем графическим объектам, описанным после вызова команды TAG(CURR_HUM_PRESS), присваивается метка CURR_HUM_PRESS. Закончить список отмеченных объектов можно либо вызовом TAG() с новым аргументом, либо командой TAG_MASK(0), которая запрещает присваивание меток. При использовании TAG_MASK(0), нужно не забыть разрешить присваивание меток (TAG_MASK(1)) перед следующим вызовом TAG().
Также стоит предусмотреть какую-нибудь визуализацию нажатия. В моём случае для этого делается вот что:
а) Если после отображения на TFT-дисплее предыдущего кадра зафиксировано касание кнопки, на новом кадре цвет кнопки менятся с темно-синего на голубой (см. код ниже),
б) После вывода кадра с голубой кнопкой выполняется задержка 150 миллисекунд (см. код выше).
void Display::MainMenu(float humidity, float temperature)
{
...
(*_TFT).DL(TAG_MASK(1));
(*_TFT).DL(TAG(CURR_HUM_PRESS));
(*_TFT).DL(COLOR_RGB(9, 0, 63));
// если кнопка уже была нажата, выбираем для её более светлый оттенок
if (pressedButton == CURR_HUM_PRESS) {
(*_TFT).DL(COLOR_RGB(75, 70, 108));
}
(*_TFT).DL(BEGIN(RECTS));
(*_TFT).DL(VERTEX2II(12, 62, 0, 0));
(*_TFT).DL(VERTEX2II(12 + 400, 62 + 93, 0, 0));
(*_TFT).DL(COLOR_RGB(255, 255, 255));
(*_TFT).Text(12 + 10, 62 + 5, 30, 0, "Current humidity (rH)");
// преобразование значения влажности в строку (32 -> "32%")
CreateStringTempHum(humidityStr, humidity, 0);
(*_TFT).Text(12 + 10, 62 + 45, 31, 0, humidityStr);
(*_TFT).DL(TAG_MASK(0));
...
}
Все кнопки главного меню формируются аналогично. По нажатию на кнопку Current temperature мы переходим на экран с графиком изменения температуры, по нажатию на Current humidity — на экран с графиком изменения относительной влажности.
С экранов с графиками можно вернуться в главное меню по нажатию на ссылку Back to main menu. Для создания такой ссылки используются те же инструменты — метка TAG(MENU_PRESS), после которой описываются графические объекты — текстовая строка и линия (подчеркивание).
(*_TFT).DL(TAG_MASK(1));
(*_TFT).DL(TAG(MENU_PRESS));
(*_TFT).DL(COLOR_RGB(0, 0, 0));
(*_TFT).Text(14, 240, 22, 0, "Back to main menu");
(*_TFT).DL(BEGIN(LINES));
(*_TFT).DL(LINE_WIDTH(8));
(*_TFT).DL(VERTEX2F(15 * 16, 260 * 16));
(*_TFT).DL(VERTEX2F(155 * 16, 260 * 16));
(*_TFT).DL(TAG_MASK(0));
Выглядит это следующим образом:
Исходный код проекта доступен на developer.mbed.org.
И этот проект, и демо-проект с розовыми виджетами, и рассмотренные в предыдущих статьях программы благополучно запускаются на платах SLSTK3400A от Silicon Labs, ATSAMD21-XPRO от Atmel и WIZwiki-W7500P от Wiznet. Требуется только изменить названия используемых GPIO и заменить целевую плату перед компиляцией. Как тут не полюбить mbed?
Заключение
В заключении традиционно благодарю читателя за внимание и напоминаю, что вопросы по применению продукции, о которой мы пишем на хабре, можно также задавать на email, указанный в моем профиле.
Автор: ЭФО