Подключаем LCD экран к макетной плате LPCXpresso55S69

В рамках проекта All-Hardware довелось мне освоить работу с экраном на макетной плате LPC55S69-EVK фирмы NXP. Пикантность ситуации состоит в том, что штатно эта плата поставляется без экрана, так что в освоение работы также входил поиск экрана, который можно достать в наших краях, и его подключение.

Во второй части статьи, я расскажу о том, какие действия следует выполнить, чтобы повторить мой подвиг на практике. Но сначала я выскажу всё, что накипело за время работы. Правда, всё ниже сказанное является моим личным мнением и часто не совпадает с мнением руководства нашей компании. Но у инженера-программиста, лично прошедшего через это всё, вполне может быть собственное мнение… И вот оно.

Часть первая. Исследование

Конструктивные и схемотехнические особенности

Итак. В целом, с виду, всё должно получиться вполне себе симпатично. У макетной платы имеется разъём, совместимый с Arduino Uno. Причём он сделан забавно. Фактически разъёмы двухрядные. С Arduino Uno совместимы только внутренние ряды. Внешние же – сами по себе.

Что может быть проще? Сейчас возьмём дисплей, совместимый с Arduino, просто наденем его на плату и воспользуемся какой-нибудь библиотекой, коих на github вагон и маленькая тележка. Разумеется, у нас в наличии имелись подходящие дисплеи. Вот его разъём:

Смущает, что положение контактов D0 и D1 шины данных на этих двух платах не совпадает. Но если бы это было единственной проблемой! Само собой, для реальной работы, надо понять, что за порты контроллера подключены к соответствующим шинам. Сейчас мы возьмём описание на макетку… Ой! А чтобы взять описание на макетку на сайте производителя, надо зарегистрироваться.

Вот представьте себе: вы – инженер, получивший задание подобрать аппаратуру для нового изделия. Вы вошли на свой любимый поисковик. И он вам выдал то, что открыто. И вы выбрали из того, что выдало… Станете ли вы специально рыскать по закрытым сусекам, специально регистрируясь? Я бы не стал. Почему я тогда пишу про эту макетку? Я же говорю, мне руководство спустило задачу внедрить конкретную плату в проект, в котором важна широкая номенклатура. И плату мне эту выдали, так как она была куплена специально для расширения поддерживаемой номенклатуры. Иначе бы я и не нашёл её. Не люблю я лишний раз где-то регистрироваться, когда вокруг столько всего доступного без регистрации!

Ну, да ладно. На самом деле, требуемый документ можно скачать у продавцов. У того же mouser. Открываем его и… Нет. Это надо иллюстрировать. Таблицы, поясняющей цоколёвку разъёма, нет. Но зато есть рисунок.

Допустим, к портам PIO1_9 и PIO1_10 у меня особых претензий нет. Но что за порты контроллера такие LED_RED_ARD и далее по всем цветам? Ну, и ряд других ножек подобного вида. Такое чувство, что автор документа не до конца понимал, для чего он это делает. Поэтому просто скопировал имена цепей со схемы. Зачем эти цепи? Имена цепей – это абстракция от автора схемы, не более того. Нужны имена соответствующих им ножек контроллера! Это знают все. Кроме автора документа.

Ну хорошо, ищем схему макетки. В отличие от описания, схему нигде, кроме как у производителя, не скачаешь. А производитель требует регистрацию на каждый чих. Вообще, мне это всё напоминает те времена, когда справочник Шило был незаменимой, но и недоставаемой вещью. Я его достал уже когда от него не было проку, так как мелкая логика была заменена логикой программируемой. Но психологическая травма, полученная в детстве, заставила меня его взять, когда народ стал сдавать их в букинистические магазины. И та же травма не даёт теперь выкинуть этот томик, хоть место в шкафу я расчищаю регулярно. Но электронная промышленность страны, находившейся в таких условиях, накрылась медным тазом. Или инженеры NXP мечтают о том же? Что может быть секретного в схеме макетной платы? Почему её нельзя увидеть без регистрации? Китайцы украдут? Так бизнес-то в продаже контроллеров! Чем больше инженеров владеют макеткой, тем выше шанс, что контроллер пойдёт в изделие и будет закупаться в промышленных масштабах! Я не понимаю! Но продолжим….

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

У меня есть многострадальная статья, написанная в 2017-м году, но до сих пор не увидевшая свет, где я рассматриваю принципы работы библиотеки mcucpp Константина Чижова. Там я гляжу, насколько оптимально компилятор построит ассеблерный код для подряд идущих битов, для разрыва, для двух нибблов на разных портах… После чего – останавливаюсь со словами, что более извратно не сделает ни один схемотехник. Но та статья – не про мои наработки… Моё там – только проверка. Поэтому мне не дали добро на её публикацию. Может, в комментариях кто убедит руководство, что стоит выложить… Но тем не менее. Когда я её писал, то и представить не мог, что всё-таки найдутся схемотехники, которые раскидают биты шины данных настолько хитро. Не факт, что аппарат для перемешивания шариков «Спортлото» смог бы сделать так же!

Но и это не всё! Как уже отмечалось выше, на трёх битах шины данных висят линии светодиода. Чтобы во время работы с параллельной шиной была дискотека, наверное. Почему нельзя было повесить их на другие ряды разъёма? Видно же, что разъём вдвое шире, чем его ардуиновская часть.

Всего, сказанного выше, я бы не сказал, кабы не шелкография. На плате шелкографическим способом нанесена маркировка параллельной шины. Это не я придумал, что там такая шина есть, это конструктор отметил…

Программная поддержка

Теперь перейдём к программной поддержке. Здесь всё намного лучше. В состав BSP входит сразу две библиотеки (двоичный код знаменитого emWin и исходные коды littlevgl). К этим библиотекам прилагаются примеры прикладных программ.

Сервис All-Hardware создан для того, чтобы дать программистам пощупать работу с оборудованием, не покупая это оборудование. Само собой, программистам желательно пользоваться штатными библиотеками и примерами. Если я предложу работать с экраном через какую-то свою библиотеку, пользователи скажут про меня примерно то же, что я говорил про разработчиков аппаратуры в прошлом разделе. Не надо изобретать никакого велосипеда при наличии готовых решений!

Но вот беда – все готовые решения рассчитаны на дисплей Adafruit TFT LCD shield w/Cap Touch. Он подключается через порт SPI (сенсорный экран мы пока не рассматриваем – всё равно удалённо на него не понажимать). То есть, имеющийся у нас дисплей с параллельной шиной – хорош, но не годится. Мало того, что его сложно внедрить, так ещё и он будет абсолютно не совместим с готовыми программными решениями из комплекта поставки BSP.

Выбираем экран

Где быстро добыть Adafruit TFT LCD shield w/Cap Touch в наших краях, я не нашёл. Но мы сделали хитрее. Мы запросили на Яндекс Маркете дисплей с контроллером ILI9341 (как у Адафрутовского) и интерфейсом SPI. Был выдан большой список. Вариант с разъёмом для Arduino и интерфейсом SPI в списке был единственный. Много вариантов было с коротким разъёмом SPI, но конструктивно же проще надевать на готовый разъём, чем что-то там городить. Ну, мы его и заказали.

Он ехал из самого Красноярска. Доставка была СДЭКом до квартиры, что в условиях самоизоляции – очень полезно. Он приехал даже на день раньше заявленного срока… А внутри посылки был точно такой же дисплей, как и показанный мною на фото в начале статьи. С параллельной шиной. Вот и верь после этого людям!

Так как у проекта сроки всё-таки должны быть конечными, мы приняли решение больше не экспериментировать с заказами, а взять что-нибудь с шиной SPI, из имеющегося в наличии. А в наличии был дисплей от Raspberry Pi фирмы WaveShare. Вообще, у него есть своё достоинство – он широко распространён.

К слову, уже позже выяснилось, что фирма WaveShare выпускает также и SPI дисплеи и с разъёмом Arduino, но они по контактам не полностью совместимы с Адафрутовскими. На другой ножке расположен сигнал C/D и есть ножка Reset_n. Зато они имеются на Ali Express у множества продавцов. Если кто-то будет искать дисплей для себя – имейте в виду. Но нам было некогда ждать доставки с Ali Express, так что дальше будет идти работа с дисплеем от Raspberry Pi.

Часть вторая. Начинаем работу

Шаг 0: подключаем аппаратуру

Если вы пользователь сервиса All-Hardware, то можете пропустить этот шаг. Всё уже сделано на нашей стороне. Но если вы подключаете плату у себя, рассказываю, как это сделать.

Хорошо быть программистом! Намотал проводов, а дальше – забота конструкторов. У меня на столе результат коммутации выглядел так:

Шаг 1: отключаем работу с сенсорным экраном

Итак. Входим в MCUXpresso IDE и выбираем File->New->Import SDK Example.

Далее — выбираем свою плату:

И для удобства изложения я выберу проект litegl_example->litegl_demo. На нём мне будет проще показывать проблемы для шага 3. Но вообще, конечно, в реальной жизни можно выбрать и littlegl_terminal – он попроще при опытах.

Собираем, запускаем… Не работает. Это нормально. Фатальную ошибку нам выдают после попытки открытия сенсорного экрана. А у нас сенсорный экран отличается от используемого на Адафрутовской плате. В рамках данной статьи мы просто должны убрать поддержку сенсорного экрана. Как минимум, для проекта All-Hardware цели разобраться с ним не было, а чисто по-человечески – наверное, уже видно, что с продукцией NXP я не подружился. Мне больше нравятся открытые вещи. Поэтому работу с ним я не осваивал.

Но меньше лирики! Вот место, непосредственно приводящее к краху программы:

А чтобы оно не вызывалось, просто убираем общий вызов инициализации сенсорного экрана из функции AppTask(). На скриншоте ниже, соответствующий вызов закомментирован:

Шаг 2: заменяем инициализацию

Запускаем получившуюся программу. Она уже не вылетает с ошибкой, но и экран ничем не заполняется. Дело в том, что библиотека рассчитана на контроллер экрана ILI9341, а фактически стоит, как я понял, ILI9488 (или совместимый с ним). Эти контроллеры достаточно близки по набору команд, но процесс инициализации у них несколько различается. Сейчас мы будем править функцию FT9341_Init() в файле fsl_ili9341.c. Исходно она выглядит так (я привожу скриншоты, чтобы вы могли ориентироваться также в дереве файлов):

Мы полностью удалим её тело. Вместо него впишем следующий код:

static const uint8_t ILI9488_regValues[] = {

      0x01, 0,
      0xB1,  1, 0xA0,
      0xB4,  1, 0x02,
      0xC0,  2, 0x17, 0x15,
      0xC1,  1, 0x41,
      0xC5,  3, 0x00, 0x0A, 0x80,
      0x36,  1, 0x48,
      0x3A,  1, 0x55,
      0xE9,  1, 0x00,
      0xF7,  4, 0xA9 ,0x51, 0x2C, 0x82,
      0x11,  0,
      TFTLCD_DELAY, 120,
      0x29,  0,
    // */
    };
    SDK_DelayAtLeastUs(ILI9341_RESET_CANCEL_MS * 1000U, SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY);
    int i = 0;
    while(i < sizeof(ILI9488_regValues))
    {
       uint8_t r = ILI9488_regValues[i++];
       uint8_t len = ILI9488_regValues[i++];
       uint8_t d;
       if(r == TFTLCD_DELAY) 
       {
          SDK_DelayAtLeastUs(len * 1000U, SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY);
       } else 
       {
          writeCommand(r);
          for (d=0; d

Чтобы этот код заработал, добавим в заголовочный файл fsl_ili9341.h следующие объявления:

#define ILI9488_NOP 0x00
#define ILI9488_SOFT_RESET 0x01
#define ILI9488_READ_ID 0x04
#define ILI9488_READ_NUM_DSI_ERR 0x05
#define ILI9488_READ_STATUS 0x00
#define ILI9488_READ_PWR_MODE 0x0A
#define ILI9488_READ_MADCTL 0x0B
#define ILI9488_READ_PIXEL_FMT 0x0C
#define ILI9488_READ_IMG_MODE 0x0D
#define ILI9488_READ_SIGNAL_MODE 0x0E
#define ILI9488_READ_SELF_DIAG_RES 0x0F
#define ILI9488_SLEEP_IN 0x10
#define ILI9488_SLEEP_OUT 0x11
#define ILI9488_PARTIAL_MODE_ON 0x12
#define ILI9488_NORMAL_MODE_ON 0x13
#define ILI9488_INVERSION_OFF 0x20
#define ILI9488_INVERSION_ON 0x21
#define ILI9488_ALL_PIXEL_OFF 0x22
#define ILI9488_ALL_PIXEL_ON 0x23
#define ILI9488_OFF 0x28
#define ILI9488_ON 0x29
#define ILI9488_COL_ADDR_SET 0x2A
#define ILI9488_PAGE_ADDR_SET 0x2B
#define ILI9488_MEM_WRITE 0x2C
#define ILI9488_MEM_READ 0x2E
#define ILI9488_PARTIAL_AREA 0x30
#define ILI9488_VSCROLL_DEF 0x33
#define ILI9488_TEARING_EFFECT_LINE_OFF 0x34
#define ILI9488_TEARING_EFFECT_LINE_ON 0x35
#define ILI9488_MEM_ACCESS_CTRL 0x36
#define ILI9488_VSCROLL_START_ADDR 0x37
#define ILI9488_IDLE_MODE_OFF 0x38
#define ILI9488_IDLE_MODE_ON 0x39
#define ILI9488_INTERFACE_PIXEL_FMT 0x3A
#define ILI9488_MEM_WRITE_CONTINUE 0x3C
#define ILI9488_MEM_READ_CONTINUE 0x3E
#define ILI9488_WRITE_TEAR_SCAN_LINE 0x44
#define ILI9488_READ_TEAR_SCAN_LINE 0x45
#define ILI9488_WRITE_BRIGHTNESS_VAL 0x51
#define ILI9488_READ_BRIGHTNESS_VAL 0x52
#define ILI9488_WRITE_CTRL_VAL 0x53
#define ILI9488_READ_CTRL_VAL 0x54
#define ILI9488_WRITE_CONTENT_ADAPT_BRIGHTN_CTRL_VAL 0x55
#define ILI9488_READ_CONTENT_ADAPT_BRIGHTN_CTRL_VAL 0x56
#define ILI9488_WRITE_CABC_MIN_BRIGHTN 0x5E
#define ILI9488_READ_CABC_MIN_BRIGHTN 0x5F
#define ILI9488_READ_AUTO_BRIGHTN_CTRL_SELF_DIAG_RES 0x68
#define ILI9488_READ_ID1 0xDA
#define ILI9488_READ_ID2 0xDB
#define ILI9488_READ_ID3 0xDC
#define TFTLCD_DELAY 0xFF

Шаг 3: Правим функцию установки координат

Возможно, этот шаг требуется только пользователям дисплеев от WaveShare и не требуется для владельцев дисплеев с чистым SPI разъёмом. Нет возможности проверить. Но на имеющемся дисплее, после того как в инициализацию внесены изменения, экран начал что-то отображать. Правда, это что-то сосредоточено в его верхней части. Сразу после запуска программы, видно, как там что-то мелькает, а затем – получается вот такая картинка:

При трассировке видно, что в верхней части экрана последовательно отображается то, что вообще-то должно разъехаться по всей его высоте. После массы опытов я доказал, что проблемный код сосредоточен в теле функции DEMO_FlushDisplay():

Там явно задаются координаты для вывода, а вывод всегда идёт с координат 0,0. Почему? После долгих опытов, я сделал следующее предположение.

Вот формат команды COLADDR из документации на контроллер ILI9488:

Обратите внимание, что аргументы передаются через младший байт, а биты 23:8 – отмечены, как XX.

Но из этого же документа видно, что при использовании SPI, это не важно. Теперь смотрим схему дисплея от WaveShare. И видим, что по шлейфу данные уходят в параллельном виде. Да-да! Именно так! Именно в параллельном! Но они этого не скрывают, схема доступна свободно! Вот разъём для шлейфа:

В параллельный вид всё приводится при помощи двух регистров сдвига:

А стробы формируются счётчиком и прочей логикой:

Когда мы посылаем данные при помощи DMA, сигнал CS не сбрасывается на протяжении всей посылки. Есть мнение (я не стал вглядываться в детали такой сложно читаемой схемы, так что только мнение), что если сигнал CS сбросить после восьми бит, уйдёт восьмибитная посылка. Иначе – шестнадцати битная.

В итоге, в нашей функции вместо четырёх байт с координатами, в физический шлейф уйдёт два 16 битных слова, координаты в которых будут отформатированы неверно.
Зная эту теорию, переписываем проблемную функцию так:

static void DEMO_FlushDisplay(int32_t x1, int32_t y1, int32_t x2, int32_t y2, const lv_color_t *color_p)
{
    uint8_t data[4];
    const uint8_t *pdata = (const uint8_t *)color_p;
    uint32_t send_size   = (x2 - x1 + 1) * (y2 - y1 + 1) * LCD_FB_BYTE_PER_PIXEL;

    /*Column addresses*/
    DEMO_SPI_LCD_WriteCmd(ILI9341_CMD_COLADDR);
    DEMO_SPI_LCD_WriteData (x1 >> 8);
    DEMO_SPI_LCD_WriteData (x1);
    DEMO_SPI_LCD_WriteData (x2 >> 8);
    DEMO_SPI_LCD_WriteData (x2);

    /*Page addresses*/
    DEMO_SPI_LCD_WriteCmd(ILI9341_CMD_PAGEADDR);
    DEMO_SPI_LCD_WriteData (y1 >> 8);
    DEMO_SPI_LCD_WriteData (y1);
    DEMO_SPI_LCD_WriteData (y2 >> 8);
    DEMO_SPI_LCD_WriteData (y2);

    /*Memory write*/
    DEMO_SPI_LCD_WriteCmd(ILI9341_CMD_GRAM);
    DEMO_SPI_LCD_WriteMultiData(pdata, send_size);

    lv_flush_ready();
}

И вот результат:

Шаг 4: Чиним полярность тактового сигнала и цвета

Что меня сильно смущает в картинке — это цвета. Тем более, что когда я стал писать демку для проекта All-Hardware, она у меня получалась какая-то дальтоническая. Выбирал синий или зелёный цвета — всё было хорошо. А выбирал красный — получал чёрный. Ерунда какая-то!
Но я уже привык решать проблему с цветами не аналитически, а опытным путём. Я всегда выдаю на экран настроечную таблицу. Сказывается опыт работы на телевизионном заводе. В целевой библиотеке функция DEMO_FlushDisplay() получает на вход блоки, шириной в экран и высотой 40 строк. Это я выяснил при её трассировке для третьего шага. Ну и замечательно. Вставим туда код, который переписывает буфер на бегущую единицу. Сделаем на экране 16 вертикальных полос. Первая будет иметь цвет 0x0001, вторая — 0x0002, третья — 0x0004… И так — до 0x8000. И посмотрим, какие фактические цвета это даст. Вот код, реализующий эту доработку, внедрённый в функцию:

    {
        uint16_t *pdata16 = (uint16_t *)color_p;
        int i;
        uint32_t send_size16   = (x2 - x1 + 1) * (y2 - y1 + 1);
        int wordsPerColor = (x2 - x1 + 1) / 16;
        for (i=0;i

Результат меня озадачил. Вот он:

То, что зелёный цвет разорван — это нормально. Типичный случай переставленных байтов. В библиотеке littlevgl не только это учтено, но даже этот режим включён по умолчанию. Удивительно другое. Слева — четыре зелёных полосы. Вот так выглядит в двоичном виде запись цветов по схеме 565: BBBBBGGGGGGRRRRR (R и B можно менять местами через настройки контроллера дисплея, к этому сейчас не цепляемся). Разбиваем 16 бит на байты. Получаем BBBBBGGG GGGRRRRR. Переставляем байты местами. Получаем GGGRRRRR BBBBBGGG. То есть, слева должно быть не четыре, а три зелёных полосы. А справа — не две, а тоже три. А у нас — четыре и две! Налицо сдвиг битов при передаче.

Вообще, в сдвиге виновато наложение особенностей схемы от WaveShare на особенности передачи данных в библиотеке, но во время опытов я выявил и ошибку в самой библиотеке. Именно поэтому я выше сказал, что в программной части всё намного лучше. Сначала я хотел написать, что в программной части всё идеально. Увы. Только намного лучше, чем в аппаратной.

На коротких дорожках проблема не проявляется, но на длинных проводах у меня бывали сбои. Оказывается, программисты включили тактирование данных отрицательным фронтом сигнала SCK. Всё бы ничего, но и описание регистра сдвига 74HC4094 (кстати, именно «94», хотя на схеме выше фирма WaveShare написала «49») и в документации на ILI9341 (который стоит у Адафрута) написано, что тактировать надо положительным перепадом. Настройка полярности производится тут:

Меняем:

BOARD_LCD_SPI.Control(ARM_SPI_MODE_MASTER | ARM_SPI_CPOL1_CPHA0 | ARM_SPI_DATA_BITS(8), BOARD_LCD_SPI_BAUDRATE);

на:

    BOARD_LCD_SPI.Control(ARM_SPI_MODE_MASTER | ARM_SPI_CPOL0_CPHA0 | ARM_SPI_DATA_BITS(8), BOARD_LCD_SPI_BAUDRATE);

(то есть, CPOL1 на CPOL0). Всё бы ничего, но экран от Малины перестаёт работать вообще. И экспериментировать с параметром бесполезно. Или экран не работает вообще, или данные сдвинуты на один бит. Тем не менее, правильно — именно так, как я написал. Просто сейчас сделаем ещё одну правку. Чтобы понять её, мне пришлось потратить сутки. Пришлось много тыкаться осциллографом в мелкую логику на дисплее. Также я таки разрисовал логику работы аппаратуры WaveShare и понял, что при положительной полярности тактового сигнала, восьмибитные данные никогда не будут защёлкнуты на выходе регистра сдвига. Чип селект будет снят раньше. А при отрицательной же полярности 16 битные данные защёлкиваются на 1 такт раньше. Вот такое противоречие.

Какой выход? Правильно: привести всё к единому решению. Будем посылать команды тоже в 16 битном виде. Для этого в том же файле переписываем две функции. Они настолько тривиальны, что я просто приведу их текст, как их следует переделать:

static void DEMO_SPI_LCD_WriteCmd(uint8_t Data)
{
    GPIO_PortClear(BOARD_LCD_DC_GPIO, BOARD_LCD_DC_GPIO_PORT, 1u << BOARD_LCD_DC_GPIO_PIN);
    uint8_t temp[2];
    temp[0] = 0;
    temp [1] = Data;
    BOARD_LCD_SPI.Send(temp, 2);
    SPI_WaitEvent();
}

static void DEMO_SPI_LCD_WriteData(uint8_t Data)
{
    GPIO_PortSet(BOARD_LCD_DC_GPIO, BOARD_LCD_DC_GPIO_PORT, 1u << BOARD_LCD_DC_GPIO_PIN);
    uint8_t temp[2];
    temp[0] = 0;
    temp [1] = Data;
    BOARD_LCD_SPI.Send(temp, 2);
    SPI_WaitEvent();
}

Настроечная таблица становится правильной — по три зелёных полосы слева и справа:

Убираем вспомогательный код, получаем правдоподобные цвета интерфейса:

Заключение

Мы познакомились с частным мнением автора о том, что макетная плата LPC55S69-EVK собрала в себе максимум примеров, показывающих, как не надо делать. А также научились выбирать для неё жидкокристаллических дисплей и выводить на него изображение, пользуясь штатными библиотеками из среды разработки, внося в них как можно меньшее количество изменений.

 

Источник

All-Hardware, плата LPC55S69-EVK, системное программирование

Читайте также