В этой статье расскажу, как пишутся драйвера для дисплеев. В принципе, любых, независимо от интерфейса. Сегодня рассмотрим 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". Алгоритм записи/чтения весьма прост и туп.
Запись команды:
- Активировать CS;
- Выдать на шину записываемый регистр;
- Дёрнуть вниз/вверх WR;
- Выдать на шину команду;
- Дёрнуть WR;
- Поднять CS.
Чтение регистра:
- Активировать CS;
- Выдать на шину читаемый регистр;
- Дёрнуть WR;
- Перевести шину на ввод;
- Дёрнуть RD;
- Считать с шины байт;
- Перевести шину на вывод;
- Поднять CS.
Запись данных в память дисплея (данные содержат цвет точки и являются 16-ти разрядными):
- Активировать CS;
- Выдать на шину команду записи в память дисплея (0x22)
- Дёрнуть WR;
- Выдать на шину старший байт цвета;
- Дёрнуть WR;
- Выдать на шину младший байт цвета;
- Дёрнуть WR;
- Повторять пункты 4-7, пока не заполнится нужное количество пикселей. Адрес пикселя инкрементируется автоматически;
- Поднять 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.
Тема на форуме - перейти.