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

C++CProgramming microcontrollers
Tutorial

Введение


Модель дисплея называется 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 содержит цвет для текущего пикселя. Это означает что её нужно повторять для каждого пикселя текущего прямоугольника. Также у дисплея есть особенность, хоть сам он и обладает разрешением 130x130, его виртуальная координатная сетка имеет размеры 132x132, а координаты начинают отсчёт с точки 2x2.

Таким образом, если мы, например, хотим нарисовать квадрат чёрного цвета 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

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

Для всех интересующихся, полный код проекта можно посмотреть по ссылке.
Tags:stm32c++clcdssd1283Acortex-m4armmicrocontrollersembeddedпрограммирование микроконтроллеровspi
Hubs: C++ C Programming microcontrollers
+23
7.1k 83
Comments 14

Popular right now