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

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



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

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



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

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

    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 заносятся нужные значения.

    В библиотеке 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-дисплея. Выглядит всё это следующим образом:


    С проектом, запущенным на видео можно ознакомиться на developer.mbed.org.

    Конечно, странно было бы заставлять пользователя калибровать экран после каждого включения устройства. Поэтому имеет смысл единожды провести калибровку, считать содержимое регистров 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, указанный в моем профиле.
    • +18
    • 5,3k
    • 9
    ЭФО
    63,00
    Поставки электронных компонентов
    Поделиться публикацией

    Комментарии 9

      +1
      Можно ли Вас попросить рассказать подробнее о том, как загрузить и вывести кириллицу на FT800?
        0
        Это тема следующей статьи.

        Собственно, текст практически готов, планирую выкладывать на хабр в пятницу или в следующий понедельник. Если у вас с кириллицей не получается что-то конкретное — пишите мне на xk@efo.ru, подскажу, а если дело не срочное и просто хотите почитать обзор, то потерпите недельку, пожалуйста.
          0
          Статья о выводе кириллицы опубликована: https://habrahabr.ru/company/efo/blog/311816/
            0
            Благодарю.
          0
          есть ли возможность диагностировать силу нажатия?
          т.е. если сильно нажали на «плюсик», — то быстрее его инкрементировать
            0
            Вообще, такой функции TFT-модули не имеют, давление на дисплей не измеряется.

            Однако если использовать модель с резистивным экраном, то в регистрах с «сырыми» данными можно найти значения, которые отображают величину сопротивления. Эта величина естественным образом связана с силой нажатия, т.е. в принципе вы можете самостоятельно измерить сопротивление без нажатия / с небольшим нажатием / с сильным нажатием, а потом использовать эти данные.
            Но во-первых это таки будет резистивный экран, а во-вторых градуировкой придется заниматься самостоятельно, без готовых примеров или библиотечных функций.
              0
              Функция pressure есть, например, в adafruite для многих чипов с простой градуировкой (надо указать ширину и высоту экрана и чип)
              Ваш вариант с задержкой тоже хороший, но на мой взгляд требует больше кода для имплементации.
              Спасибо Вам за хорошие примеры и последовательные материалы.
              +1

              Более простым (и распространенным) способом является измерение времени удержания кнопки, когда оно превышает некоторый порог, то увеличивается шаг инкремента; когда кнопку отпустили — сбрасывается на стандартное.

                0
                Это вопрос дизайна UI и удобства пользователя. Но как говорят, «на вкус и цвет товарищей нет».

                Поэтому не соглашусь с Вами, что способ с задержками проще, так, как в любом случае требует больше кода, что на ардуине, например, будет очень критично.

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

                Про популярность — вчера были популярны кнопки, сегодня сенсорные экраны. Это вопрос спроса и он есть:
                3D Touch

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое