Top.Mail.Ru

Часть 11. STM32 и C++. Драйвер дисплея HX8347D.

В этой статье расскажу, как пишутся драйвера для дисплеев. В принципе, любых, независимо от интерфейса. Сегодня рассмотрим HX8347G. Дисплей довольно тормозной, и тормознутость GFX_TFT дает довольно термоядерное сочетание медлительности. Но для примера подойдёт.

Дисплей рассчитан на 8-ми битный интерфейс. Питание от 5 вольт, но контроллер дисплея и подсветка работают от 3.3 вольт. На плате дисплея стоят два преобразователя уровня и стабилизатор напряжения. Вход 3.3 вольта не задействован. Т. е. рассчитан только на Ардуину. Что бы запустить его под STM, я исключил из схемы стабилизатор, закоротив вход с выходом, и закоротил выводы +5 и +3.3 вольта (На картинке отмечено красными квадратиками). Таким образом у меня получилось исключить из работы преобразователи уровня. На данный момент они работают просто как буферные элементы. Можно конечно выпаять их и запаять перемычки. Такие микрухи в жизни пригодятся.

Дисплей размером 2.8. Очень много дисплеев на разных контроллерах имеют такой форм-фактор, но мне, как назло, попалось это чудо.

Конечно, там есть ещё разъём нераспаянный, который рассчитан сразу на STM, но повторюсь, я ленивый, да и плата уже была изготовлена под него. Выводов у него не так чтобы много:

  • D0-D7 - данные;
  • RST - сброс;
  • CS - выбор дисплея;
  • RS - выбор регистра = "0", выбор данных = "1";
  • WR - запись;
  • RD - чтение;

Все выводы инверсные, кроме RS, активный уровень "0". Алгоритм записи/чтения весьма прост и туп.

Запись команды:

  1. Активировать CS;
  2. Выдать на шину записываемый регистр;
  3. Дёрнуть вниз/вверх WR;
  4. Выдать на шину команду;
  5. Дёрнуть WR;
  6. Поднять CS.

Чтение регистра:

  1. Активировать CS;
  2. Выдать на шину читаемый регистр;
  3. Дёрнуть WR;
  4. Перевести шину на ввод;
  5. Дёрнуть RD;
  6. Считать с шины байт;
  7. Перевести шину на вывод;
  8. Поднять CS.

Запись данных в память дисплея (данные содержат цвет точки и являются 16-ти разрядными):

  1. Активировать CS;
  2. Выдать на шину команду записи в память дисплея (0x22)
  3. Дёрнуть WR;
  4. Выдать на шину старший байт цвета;
  5. Дёрнуть WR;
  6. Выдать на шину младший байт цвета;
  7. Дёрнуть WR;
  8. Повторять пункты 4-7, пока не заполнится нужное количество пикселей. Адрес пикселя инкрементируется автоматически;
  9. Поднять CS.

Чтение делается точно так же, только в пунктах 5 и 7 дёргается RD и байты не пишутся, а читаются. Теперь сам код...

У нас есть конструктор, который ничего не делает, просто инициализирует наш класс:

HX8347::HX8347():GFX_TFT(240,320)

Как видите, он ссылается на уже известную нам библиотеку GFX_TFT.

Более интересна инициализация дисплея. Так как там инициализируются все пины управления дисплеем:

void Init(GPIO_TypeDef *GPIOx_Data, uint16_t Data_Pin, // Порт данных, младший пин
          GPIO_TypeDef *GPIOx_Reset, // Порт сброса
          uint16_t TFT_Reset, // Пин сброса
          GPIO_TypeDef *GPIOx_CS, // Порт выбора кристалла
          uint16_t TFT_CS, // Пин выбора кристалла
          GPIO_TypeDef *GPIOx_RS, // Порт выбора Регистра/Данных
          uint16_t TFT_RS, // Пин выбора Регистра/Данных (Register/DataSelection)
          GPIO_TypeDef *GPIOx_WR, // Порт строба записи
          uint16_t TFT_WR, // Пин строба записи
          GPIO_TypeDef *GPIOx_RD, // Порт строба чтения
          uint16_t TFT_RD); // Пин строба чтения

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

Далее всё как обычно. С помощью ранее написанных библиотек инициализируются порты.

_SetSpeed(Speed_WeryHigh);                                     // Устанавливаем скорость порта
_SetEdge(High);
_SetPin(_GPIOx_Reset, _TFT_Reset, Output, No_Pull);            // Все выводы на выход и в высокое состояние
_SetPin(_GPIOx_CS, _TFT_CS, Output, No_Pull);
_SetPin(_GPIOx_RS, _TFT_RS, Output, No_Pull);
_SetPin(_GPIOx_WR, _TFT_WR, Output, No_Pull);
_SetPin(_GPIOx_RD, _TFT_RD, Output, No_Pull);
_SetEdge(Low);

И далее идёт нудная инициализация всех регистров, список которых я приводить не буду. Он слишком длинный. Но для образца кусочек, остальное можно посмотреть в коде:

// Инициализация
/* TE signal ON */
writeReg(0x2D, 0x1D); // Cycle control 1 (GDON)
writeReg(0x2E, 0x83); // Cycle control 2 (GDOF)
writeReg(0xE4, 0x02); // Power saving 1 (EQVCI_M1)

/* Power Voltage Setting */
writeReg(0xE5, 0x26); // Power saving 2 (EQGND_M1)
writeReg(0xE6, 0x26); // Power saving 3 (EQVCI_M0)
writeReg(0xE7, 0x02); // Power saving 4 (EQGND_M0)
writeReg(0xE8, 0x6E); // Source OP control_Normal (OPON_N)
writeReg(0xE9, 0x46); // Source OP control_IDLE (OPON_I)

После того, как инициализация прошла, если нам повезёт, и дисплей окажется тот, на который мы рассчитывали, дальше с ним можно делать что угодно. Работают все функции класса Print и Adafruit_GFX на основе которой была написана GFX_TFT, описанная в части 8.

Сейчас просто посмотрим, что же мы дописали, чтобы это всё завелось.

Мы всего-навсего переписали функции, вместо которых в GFX_TFT стоят заглушки. Таким образом, мы имеем свои функции, которые занимаются основными операциями, зависящими от конкретного контроллера дисплея:

virtual void fillScreen(uint16_t Color);
void setWindow(uint16_t sx,uint16_t sy,uint16_t width,uint16_t height);

virtual void  drawPixel(uint16_t x, uint16_t y, uint16_t color);
virtual void  setRotation(uint8_t x);

void drawRGBBitmap(uint16_t x, uint16_t y, const uint16_t *bitmap, uint16_t w, uint16_t h); // В данном случае пока не реализована

fillScreen() - вынесена сюда, так как она зависит от способа вывода. Есть дисплеи, которые заполняют экран попиксельно, с помощью функции drawPixel(). Есть дисплеи, у которых можно указать окно вывода, а потом в цикле забить это окно нужным цветом командой записи в память. Этот дисплей - второго типа.

setWindow() - как раз и есть эта функция, задающая окно вывода.

Осталась функция setRotation(). Есть дисплеи, поворот экрана у которых, осуществляется пересчётом координат. Есть дисплеи, которым достаточно в специальных регистрах изменить некоторые биты, и он станет выводить информацию из ОЗУ в другом порядке, что и даёт эффект поворота экрана. Нам же достаточно изменить эти биты и изменить некоторые переменные, отвечающие за размерность матрицы. Для примера листинг этой функции:

void HX8347::setRotation(uint8_t x)
{
  WR_REG(MADCTL);
  rotation = x % 8;
  switch (rotation)
  {
    case 0: // Portrait
      WR_DATA(0x00);
      _width = WIDTH;
      _height = HEIGHT;
      break;
    case 1: // Landscape (Portrait + 90)
      WR_DATA(MAD_MV |MAD_MX);
      _width = HEIGHT;
      _height = WIDTH;
      break;
    case 2: // Portrait
      WR_DATA(MAD_MX | MAD_MY);
      _width = WIDTH;
      _height = HEIGHT;
      break;
    case 3: // Landscape (Portrait + 90)
      WR_DATA(MAD_MY | MAD_MV);
      _width = HEIGHT;
      _height = WIDTH;
      break;
  }
}

Первая строка пишет команду в регистр, отвечающий за способ вывода на матрицу, а в конструкции switch на каждый вид поворота изменяются в нужном нам регистре биты, отвечающие за способ вывода и, соответственно, изменяются переменные на новые значения ширины и высоты. Так как экран у нас не квадратный. Позже эти переменные будут использоваться при расчёте местоположения пикселя.

Ну и самые сложные функции - это функции непосредственно отвечающие за запись/чтение байт в необходимые регистры. Сложности большой нет, но на разных дисплеях могут использоваться разные протоколы: параллельный 8/16-битный вывод, FSMC, SPI, I2C. Поэтому для каждого дисплея эти функции свои:

void    WR_REG(uint8_t regval);                         // Запись в регистр
void    WR_DATA(uint8_t data);                          // Запись данных в регистр указанный предыдущей командой
uint8_t RD_REG(uint8_t Reg);                            // Чтение регистра
uint8_t RD_DATA(void);                                  // Чтение данных
void    writeReg(uint8_t Reg, uint8_t Data);            // Запись в нужный регистр нужных данных

Для примера вот так реализована запись регистра, остальное можно посмотреть в коде:

void HX8347::WR_REG(uint8_t regval)
{
  _DigitalWriteBit(_GPIOx_CS, _TFT_CS, IO_Level::Low);       // Выбираем экран
  _DigitalWriteBit(_GPIOx_RS, _TFT_RS, IO_Level::Low);       // Выбираем запись команды

  _DigitalWrite(_GPIOx_Data, regval, _FirstPin, _PinMask);  // Выдаём команду (регистр в который будет писаться команда) 

  _DigitalWriteBit(_GPIOx_WR, _TFT_WR, IO_Level::Low);       // Дёргаем WR вниз/вверх
  _DigitalWriteBit(_GPIOx_WR, _TFT_WR, IO_Level::High);
}

Ну и сама работа с дисплеем на паре примеров:

#include "STM32.h"
#include "HX8347_8bit.h"

HX8347  myTFT;

int main(void)
{
  FLASH_DATAEEPROM_Unlock();
  SystemClock_Config(Quartz_None);
  myTFT.Init(GPIOA, GPIO_PIN_0, GPIOB, GPIO_PIN_12, GPIOB, GPIO_PIN_0, GPIOB, GPIO_PIN_1, GPIOB, GPIO_PIN_13, GPIOB, GPIO_PIN_14);
  myTFT.setRotation(1);
  myTFT.fillScreen(COLOR_BLACK);
  myTFT.setTextColor(COLOR_WHITE, COLOR_BLACK);
  myTFT.setTextSize(2);
  myTFT.print("Hello");
  myTFT.fillCircle(120, 120, 50, COLOR_BLUE);

  while (1)
  {
  }
}

Данная библиотека проверена на STM32L151CBT6. Как обычно, ссылка на Яндекс диск и WorkDevel. Пример находится в папке WorkDevel\Developer\Tests\MK\L1xx\L151_HX8347.

Подписаться
Уведомление о
guest
0 комментариев
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x