Запускаем трансфлективный TFT дисплей на SSD1283A с помощью STM32

—

от автора

Введение

Модель дисплея называется H016IT01. Данный дисплей интересен прежде всего тем, что он является транфлективным(transflective). Это означает, что изображение на нем должно быть видно даже под ярким солнцем. А также это чуть ли не единственная доступная модель с этой особенностью на известном китайском сайте.

Статья же увидела свет потому, что информации по контроллеру SSD1283A очень мало(как в русском, так и западном сегменте сети), и руководства я нигде не встречал. В сети можно найти даташит, однако там нет информации по инициализации и работе с дисплеем, а из полезного только описания регистров.

Хочу подчеркнуть, что данный материал конечно же не является истиной последней инстанции. Я привожу лишь свой опыт взаимодействия с устройством. Основная цель статьи проста — помочь всем тем, кто решил, хочет или захочет поработать с данным дисплеем, не более.

image

Дисплей имеет 8 выводов:

1) GND
2) VCC — 5 или 3.3V
3) CS — SPI Chip Select
4) RST — «0» — выключает дисплей, «1» — включает.
5) A0/DC — Data Command(«0» — команда, «1» — данные)
6) SDA — SPI MOSI
7) SCK — SPI SCK
8) LED — вывод подцветки, как и VCC, от 3.3 до 5V

Программировать дисплей нужно по SPI, в этом мне поможет плата discovery на stm32f407.

SPI

Хоть я и взаимодействовал с SSD1283A по SPI, стоит заметить, что контроллером предусмотрен и параллельный интерфейс, но данная модель дисплея его не поддерживает. SPI у него тоже не обычный, на нем всего одна линия данных SDA. По сути, это линия MOSI, а значит с дисплея мы ничего считать не можем, что нам и говорит даташит.

image

Для начала настроим SPI, для этого затактируем SPI1 и GPIO, настроим ноги SDA и SCK как альтернативную функцию(MISO я тоже сделал, но это не обязательно). Режим работы настраиваем как однонаправленный передатчик мастер.

BIT_BAND_PER(RCC->AHB1ENR ,RCC_AHB1ENR_GPIOAEN) = true;   if(SPI_NUM::_1 == spi)   {     /*!<-------------Enable spi clocking--------------->!*/      BIT_BAND_PER(RCC->APB2ENR, RCC_APB2ENR_SPI1EN) = true;            /*!<----PA5 - SCK, PA6 - MISO, PA7 - MOSI---->!*/      GPIOA->MODER |= (GPIO_MODER_MODE5_1 | GPIO_MODER_MODE6_1 | GPIO_MODER_MODE7_1);      GPIOA->OSPEEDR |= (GPIO_OSPEEDR_OSPEED5_1 | GPIO_OSPEEDR_OSPEED7_1);      GPIOA->AFR[0] |= (GPIO_AFRL_AFSEL5_0 | GPIO_AFRL_AFSEL5_2 | GPIO_AFRL_AFSEL6_0 | GPIO_AFRL_AFSEL6_2 | GPIO_AFRL_AFSEL7_0 | GPIO_AFRL_AFSEL7_2);            /*!<-----customize SPI------>!*/      SPI1->CR1 |= (SPI_CR1_BIDIMODE | SPI_CR1_BIDIOE | SPI_CR1_SSM | SPI_CR1_SSI /*| SPI_CR1_DFF*/ | SPI_CR1_MSTR);       BIT_BAND_PER(SPI1->CR1 ,SPI_CR1_SPE) = true;   }

Затем напишем простую функцию передачи байта по SPI:

void stm32f407_spi::stm32f407_spi_send(uint8_t data) {   SPI1->DR = data;   while((SPI1->SR & SPI_SR_BSY)) continue; }

Этого достаточно чтобы SPI заработал в нужном нам режиме и передавал данные на максимально возможной скорости.

Инициализация дисплея

Теперь самое время инициализировать дисплей. Для этого нужно отправить определенную последовательность команд. Команда состоит из 3 байт, где один байт — номер команды, а два других — данные. Также нужно переключать пин A0 при посылке байта команды в «0», а при данных в «1». Для удобства я сделал inline функции для переключения состояния пинов RST, A0 и CS дисплея.

   enum class DC : uint8_t  {   COMMAND,   DATA };  #pragma inline=forced  inline void tft_lcd_rst(bool rst) {BIT_BAND_PER(GPIOA->ODR , GPIO_ODR_OD2) = rst;} #pragma inline=forced  inline void tft_lcd_dc(DC dc) {BIT_BAND_PER(GPIOA->ODR , GPIO_ODR_OD3) = static_cast<bool>(dc);} #pragma inline=forced  inline void tft_lcd_cs(bool cs) {BIT_BAND_PER(GPIOA->ODR , GPIO_ODR_OD4) = cs;}

Тогда посылка команды будет выглядеть так:

 void tft_lcd::tft_lcd_send(uint8_t addr, uint16_t data) {   this->tft_lcd_dc(DC::COMMAND);   stm32f407_spi_send(addr);   this->tft_lcd_dc(DC::DATA);   stm32f407_spi_send(static_cast<uint8_t>(data >> 8));   stm32f407_spi_send(static_cast<uint8_t>(data));  }

Для инициализации нужно подать следующую последовательность команд, которую я подсмотрел в ардуино библиотеках для данного экрана:

Команды для инициализации

static constexpr uint8_t TFT_DELAY = 0xFF;   static constexpr t_tft_regs tft_regs[]=   {     { 0x10, 0x2F8E },     { 0x11, 0x000C },     { 0x07, 0x0021 },     { 0x28, 0x0006 },     { 0x28, 0x0005 },     { 0x27, 0x057F },     { 0x29, 0x89A1 },     { 0x00, 0x0001 },     { TFT_DELAY, 100 },     { 0x29, 0x80B0 },     { TFT_DELAY, 30 },     { 0x29, 0xFFFE },     { 0x07, 0x0223 },     { TFT_DELAY, 30 },     { 0x07, 0x0233 },     { 0x01, 0x2183 },     { 0x03, 0x6830 },     { 0x2F, 0xFFFF },     { 0x2C, 0x8000 },     { 0x27, 0x0570 },     { 0x02, 0x0300 },     { 0x0B, 0x580C },     { 0x12, 0x0609 },     { 0x13, 0x3100 },   };

где TFT_DELAY означает простой Sleep указанное количество mS. Если покопаться в даташите, такие данные для некоторых адресов покажутся странными, поскольку по многим адресам запись идёт в зарезервированные биты.

Для инициализации подадим «0» на CS, перезагрузим дисплей(пин RST), и пройдёмся по таблице команд.

  tft_lcd_rst(false);   delay(5, mS);   tft_lcd_rst(true);   delay(200, mS);        this->tft_lcd_cs(false);   delay(5, mS);   /*!<--------Init display---------->!*/     for(uint8_t i = 0; i < sizeof(tft_regs)/sizeof(tft_regs[0]) ;i++)   {     (TFT_DELAY != tft_regs[i].address) ? (this->tft_lcd_send(tft_regs[i].address, tft_regs[i].value)): (delay(tft_regs[i].value, mS));   }    delay(5, mS);   this->tft_lcd_cs(true); 

После этого на дисплее должно поменяться изображение с белого на серый цвет телевизионных помех.

image

Рисуем прямоугольник

Контроллер SSD1283A позволяет рисовать изображения прямоугольниками, для чего используются 4 команды. Команда 0x44 содержит координату конца и начала прямоугольника по оси абсцисс в старшем и младшем байте данных соответственно. Команда 0x45 есть тоже самое для оси ординат. Команда 0x21 содержит координату начальной отрисовки, в старшем байте для y, в младшем для x. Команда 0x22 содержит цвет для текущего пикселя. Это означает что её нужно повторять для каждого пикселя текущего прямоугольника. Также у дисплея есть особенность, хоть сам он и обладает разрешением 130×130, его виртуальная координатная сетка имеет размеры 132×132, а координаты начинают отсчёт с точки 2×2.

Таким образом, если мы, например, хотим нарисовать квадрат чёрного цвета 20 на 20, начальная точка которого находится в позиции (30, 45) то нужно передавать следующую последовательность команд:

0x44 0x3320 (30+20+2-1, 30+2)
0x45 0x422F (45+20+2-1, 45+2)
0x21 0x2F20
0x22 0x0000, причем эту команду нужно передать 400(20*20) раз.

Тогда функция отрисовки прямоугольника будет выглядеть так(при условии, что координаты уже сдвинуты на 2):

 void tft_lcd::draw_rect(const t_rect& rec) {   this->tft_lcd_send(0x44, ((rec.x1 - 1) << 8) | rec.x0);   this->tft_lcd_send(0x45, ((rec.y1 - 1) << 8) | rec.y0);   this->tft_lcd_send(0x21, (rec.y0 << 8) | rec.x0);      for(uint16_t i = 0; i < ((rec.x1 - rec.x0) * (rec.y1 - rec.y0)); i++)   {     this->tft_lcd_send(0x22, rec.col);   } } 

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

   t_rect rect = {0x02, 0x02, 0x84, 0x84, static_cast<uint16_t>(COLOUR::MAGENTA)};   this->draw_rect(rect); 

Результат:

image

Рисуем буквы и цифры

Создадим перечень массивов с координатами для символов.

Массивы координат букв и цифр

 namespace rect_coord_lit { const t_rect_coord comma[] = {{0, 20, 5, 25}, {3, 25, 5, 28}}; const t_rect_coord dot[] = {{0, 20, 5, 25}}; const t_rect_coord space[] = {{0, 0, 0, 0}}; const t_rect_coord _0[] = {{0, 0, 15, 5},{0, 5, 5, 25},{5, 20, 15, 25},{10, 5, 15, 20}}; const t_rect_coord _1[] = {{10, 0, 15, 25}}; const t_rect_coord _2[] = {{0, 0, 15, 5},{10, 5, 15, 15},{0, 10, 10, 15},{0, 15, 5, 25},{5, 20, 15, 25}}; const t_rect_coord _3[] = {{0, 0, 15, 5},{10, 5, 15, 25},{0, 10, 10, 15},{0, 20, 10, 25}}; const t_rect_coord _4[] = {{0, 0, 5, 15},{5, 10, 10, 15},{10, 0, 15, 25}}; const t_rect_coord _5[] = {{0, 0, 15, 5},{0, 5, 5, 15},{0, 10, 15, 15},{10, 15, 15, 25},{0, 20, 10, 25}}; const t_rect_coord _6[] = {{0, 0, 15, 5},{0, 5, 5, 25},{5, 10, 10, 15},{5, 20, 10, 25},{10, 10, 15, 25}}; const t_rect_coord _7[] = {{0, 0, 15, 5},{10, 5, 15, 25}}; const t_rect_coord _8[] = {{0, 0, 15, 5},{0, 5, 5, 25},{5, 20, 15, 25},{10, 5, 15, 20},{5, 10, 10, 15}}; const t_rect_coord _9[] = {{0, 0, 15, 5},{0, 5, 5, 15},{0, 20, 15, 25},{10, 5, 15, 20},{5, 10, 10, 15}}; const t_rect_coord a[] = {{0, 10, 5, 25},{5, 5, 10, 10},{5, 15, 10, 20},{10, 10, 15, 25}}; const t_rect_coord b[] = {{0, 0, 5, 25},{5, 10, 15, 15},{10, 15, 15, 20},{5, 20, 15, 25}}; const t_rect_coord c[] = {{0, 5, 15, 10},{0, 10, 5, 20},{0, 20, 15, 25}}; const t_rect_coord d[] = {{0, 10, 10, 15},{0, 15, 5, 20},{0, 20, 10, 25}, {10, 0, 15, 25}}; const t_rect_coord e[] = {{0, 5, 15, 8}, {0, 12, 15, 15}, {0, 8, 5, 25}, {10, 8, 15, 12}, {5, 20, 15, 25}}; const t_rect_coord f[] = {{5, 5, 10, 25},{5, 0, 15, 5},{0, 10, 15, 15}}; const t_rect_coord g[] = {{0, 5, 5, 20}, {5, 5, 10, 10}, {5, 15, 10, 20}, {10, 5, 15, 30}, {0, 25, 10, 30}}; const t_rect_coord h[] = {{0, 0, 5, 25},{5, 10, 15, 15},{10, 15, 15, 25}}; const t_rect_coord i[] = {{5, 3, 10, 8},{5, 10, 10, 25}}; const t_rect_coord j[] = {{5, 3, 10, 8},{5, 10, 10, 30}, {0, 25, 5, 30}}; const t_rect_coord k[] = {{0, 0, 5, 25},{5, 15, 10, 20}, {10, 10, 15, 15}, {10, 20 , 15, 25}}; const t_rect_coord l[] = {{5, 0, 10, 25}}; const t_rect_coord m[] = {{0, 10, 4, 25},{7, 10, 10, 25}, {13, 10, 17,25}, {0, 5 , 12, 10}}; const t_rect_coord n[] = {{0, 10, 5, 25},{10, 10, 15, 25}, {0, 5 , 10, 10}}; const t_rect_coord o[] = {{0, 5, 5, 25}, {10, 5, 15, 25}, {5, 5, 10, 10}, {5, 20, 10, 25}}; const t_rect_coord p[] = {{0, 5, 5, 30}, {5, 5, 15, 10}, {5, 15, 15, 20}, {10, 10, 15, 15}}; const t_rect_coord q[] = {{0, 5, 5, 20}, {5, 5, 15, 10}, {5, 15, 15, 20}, {10, 10, 15, 30}}; const t_rect_coord r[] = {{0, 10, 5, 25},{5, 5, 15, 10}}; const t_rect_coord s[] = {{3, 5, 15, 10}, {0, 8, 5, 13}, {3, 13, 12, 17}, {10, 17, 15, 22}, {0, 20, 12, 25}}; const t_rect_coord t[] = {{5, 0, 10, 25},{0, 5, 15, 10},{10, 20, 15, 25}}; const t_rect_coord u[] = {{0, 5, 5, 25},{10, 5, 15, 25},{5, 20, 10, 25}}; const t_rect_coord v[] = {{0, 5, 5, 15}, {10, 5, 15, 15}, {1, 15, 6, 20}, {9, 15, 14, 20}, {5, 20, 10, 25}}; const t_rect_coord w[] = {{0, 5, 4, 20},{7, 5, 10, 20}, {13, 5, 17, 20}, {4, 20 , 7, 25}, {10, 20 , 13, 25}}; const t_rect_coord x[] = {{0, 5, 5, 10},{10, 5, 15, 10}, {0, 20, 5, 25}, {10, 20 , 15, 25}, {5, 10 , 10, 20}}; const t_rect_coord y[] = {{0, 5, 5, 20}, {5, 15, 10, 20}, {10, 5, 15, 30}, {0, 25, 10, 30}}; const t_rect_coord z[] = {{0, 5, 15, 10}, {10, 10, 15, 13}, {5, 12, 10, 18}, {0, 17, 5, 20}, {0, 20, 15, 25}}; } 

Создадим таблицу, где соотнесем сам символ(в ascii), количество его прямоугольников и его координаты:

Таблица указателей на координаты

 typedef struct { 	char lit;         uint8_t size; 	const t_rect_coord *rec_coord; }t_rect_coord_table;  #define LITERAL_COORD(x)  sizeof(x)/ sizeof(x[0]), x    const t_rect_coord_table rect_coord_table[] =  {   {',', LITERAL_COORD(rect_coord_lit::comma)},   {'.', LITERAL_COORD(rect_coord_lit::dot)},   {'.', LITERAL_COORD(rect_coord_lit::dot)},   {' ', LITERAL_COORD(rect_coord_lit::space)},   {'0', LITERAL_COORD(rect_coord_lit::_0)},   {'1', LITERAL_COORD(rect_coord_lit::_1)},   {'2', LITERAL_COORD(rect_coord_lit::_2)},   {'3', LITERAL_COORD(rect_coord_lit::_3)},   {'4', LITERAL_COORD(rect_coord_lit::_4)},   {'5', LITERAL_COORD(rect_coord_lit::_5)},   {'6', LITERAL_COORD(rect_coord_lit::_6)},   {'7', LITERAL_COORD(rect_coord_lit::_7)},   {'8', LITERAL_COORD(rect_coord_lit::_8)},   {'9', LITERAL_COORD(rect_coord_lit::_9)},   {'a', LITERAL_COORD(rect_coord_lit::a)},    {'b', LITERAL_COORD(rect_coord_lit::b)},   {'c', LITERAL_COORD(rect_coord_lit::c)},   {'d', LITERAL_COORD(rect_coord_lit::d)},   {'e', LITERAL_COORD(rect_coord_lit::e)},   {'f', LITERAL_COORD(rect_coord_lit::f)},   {'g', LITERAL_COORD(rect_coord_lit::g)},   {'h', LITERAL_COORD(rect_coord_lit::h)},   {'i', LITERAL_COORD(rect_coord_lit::i)},   {'j', LITERAL_COORD(rect_coord_lit::j)},   {'k', LITERAL_COORD(rect_coord_lit::k)},   {'l', LITERAL_COORD(rect_coord_lit::l)},   {'m', LITERAL_COORD(rect_coord_lit::m)},   {'n', LITERAL_COORD(rect_coord_lit::n)},   {'o', LITERAL_COORD(rect_coord_lit::o)},   {'p', LITERAL_COORD(rect_coord_lit::p)},   {'q', LITERAL_COORD(rect_coord_lit::q)},   {'r', LITERAL_COORD(rect_coord_lit::r)},   {'s', LITERAL_COORD(rect_coord_lit::s)},   {'t', LITERAL_COORD(rect_coord_lit::t)},   {'u', LITERAL_COORD(rect_coord_lit::u)},   {'v', LITERAL_COORD(rect_coord_lit::v)},   {'w', LITERAL_COORD(rect_coord_lit::w)},   {'x', LITERAL_COORD(rect_coord_lit::x)},   {'y', LITERAL_COORD(rect_coord_lit::y)},   {'z', LITERAL_COORD(rect_coord_lit::z)} }; 

Тогда функции отрисовки одного символа будут выглядеть вот так:

 void tft_lcd::draw_lit(char ch, const t_rect_coord *rect_coord, uint16_t colour, uint16_t x0, uint16_t y0, uint8_t size) {   t_rect rec = {0};   rec.col = colour;   uint8_t ctr = size;   uint8_t i = 0;    while(ctr--)   {     rec.x0 = x0 + rect_coord[i].x0;     rec.y0 = y0 + rect_coord[i].y0;     rec.x1 = x0 + rect_coord[i].x1;     rec.y1 = y0 + rect_coord[i].y1;     i++;     this->draw_rect(rec);   }       }                          void tft_lcd::draw_char(char ch, uint16_t colour, uint16_t x0, uint16_t y0) {   x0 += 2;   y0 +=2;      for(const auto &field : rect_coord_table)   {     if(field.lit == ch)     {       draw_lit(ch, field.rec_coord, colour, x0, y0, field.size);     }   } } 

В функции draw_char представлен конечный автомат, где цикл for проходит по каждому полю таблицы координат, и ищет совпадение по символу. При найденном совпадении данные передаются в функцию draw_lit, которая отрисовывает нужное количество прямоугольников по заданным координатам.

Отрисовка строки будет выглядеть следующим образом:

 void tft_lcd::draw_string(char *ch, COLOUR colour, uint16_t x0, uint16_t y0) {   while(*ch)   {     this->draw_char(*ch, static_cast<uint16_t>(colour), x0, y0);     x0+= 20;     ch++;   } } 

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

   this->draw_string("123456", COLOUR::GREEN, 5, 0);   this->draw_string("habr,.", COLOUR::WHITE, 5, 30);   this->draw_string("abcdef", COLOUR::RED, 5, 60);   this->draw_string("stwxyz", COLOUR::YELLOW, 5, 90); 

image

В таком режиме дисплей работает быстро, глаз не замечает процесса отрисовки.

Для всех интересующихся, полный код проекта можно посмотреть по ссылке.


ссылка на оригинал статьи https://habr.com/ru/post/482610/