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

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

Содержание цикла публикаций:
  1. Обзор использованных программных и аппаратных решений.
  2. Начало работы с графическим контроллером FT800. Использование готовых mbed-библиотек для периферийных устройств.
  3. Подключение датчика HYT-271. Создание и публикация в mbed собственной библиотеки для периферийных устройств.
  4. Разработка приложения: Структура программы, работа с сенсорным экраном.
  5. Разработка приложения: Вывод изображений на дисплей, проблемы русификации.
  6. Печать деталей корпуса. Анализ ошибок проектирования и другие выводы.

По итогам предыдущих трех статей мы получили mbed-проект — программу, которая может быть скомпилирована в рабочую прошивку для любой отладочной платы с интерфейсами SPI и I2C и поддержкой в ARM mbed. В качестве испытуемых выступали отладочные платы SLSTK3400A от Silicon Labs, ATSAMD21-XPRO от Atmel и WIZwiki-W7500P от Wiznet.

40d5364d5c544c05906b68c5459faafe.png

Реализованная программа пока выполняет две задачи — опрос датчика температуры и относительной влажности HYT и вывод результатов измерений на TFT-дисплей от Riverdi. Проект доступен по ссылке.

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

1. Структура TFT-модуля
Итак, используемый TFT-модуль Riverdi состоит из собственно дисплея, графического контроллера серии FT8xx от FTDI и дополнительных компонентов — сенсорного контроллера, аудиоконтроллера и т.д. На сайте производителя можно найти полный список доступных TFT-модулей, а на сайте ЭФО — доступные со склада позиции и действующие цены.

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

Я использую симпатичнейший модуль RVT43ULFNWC00 серии uxTouch диагональю 4.3'', выполненный в черной декоративной рамке. Модуль имеет ёмкостный сенсорный экран и, соответственно, встроенный графический контроллер FT801.

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

c2a56bd8319e413c87485362058548e9.png

В емкостных 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 нам ещё предстоит вернуться.

4d870cfd1a9f4722b695e4f766cf76a2.png

Для детектирования касания, в принципе, можно можно использовать данные из 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);

b58f43985be04061a55c745f6aa522a6

Чтобы элементы не только отображались, но и реагировали на касание, нужно добавить созданным объектам метки и опрашивать хранящий метку регистр.

Элементам объекта Ряд кнопок (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 заносятся нужные значения.

В библиотеке FT800_2 предусмотрена стандартная функция для проведения калибровки
        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. Использование сенсорного интерфейса в собственном проекте
Моё конечное приложение — это главное меню, на котором отображаются текущие данные о температуре и относительной влажности, а также несколько подразделов:

fdaa5593678745f38dcb952798dd98ce.png

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

866f1a2a76be4c7baa7b38b24d665c5e.png

Для навигации между разделами служат два перечисления. Первое — это все метки, присвоенные графическим объектам:

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, указанный в моем профиле.

Комментарии (0)

© Habrahabr.ru