Введение


Модель дисплея называется 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

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

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

Комментарии (14)


  1. chnav
    03.01.2020 16:03
    +1

    Странно, мне казалось, что трансрефлективный дисплей по определению должен быть LCD, никак не OLED.


    1. alex-open-plc
      03.01.2020 16:14

      Именно так.
      И вопрос автору: так TFT или OLED? Если OLED, то зачем ему подсветка?


      1. big_dig_dev Автор
        03.01.2020 16:48

        Все верно, TFT.


  1. AntonSor
    03.01.2020 16:41

    Да-да, помню, на изиэлектрониксе его «раскуривали», но так и не «раскурили». И SSD1238 действительно контроллер для TFT


  1. GarryC
    04.01.2020 16:30
    -1

    И в 2020 году мы по прежнему будем видеть тексты вроде

    GPIOA->MODER |= (GPIO_MODER_MODE5_1 | GPIO_MODER_MODE6_1 | GPIO_MODER_MODE7_1);
    мда.


    1. AntonSor
      04.01.2020 16:32
      -1

      Ваши предложения?


      1. Vadimatorikda
        04.01.2020 20:42
        +1

        Похоже имеют ввиду, что даже CubeMX научился генерировать define по имени пина, чтобы не было этих безымянных MODEX_Y.


      1. GarryC
        05.01.2020 17:33

        Я бы предпочел выражение вроде
        PortA(5,6,7)=PortOutput;


  1. master__v
    04.01.2020 19:48

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


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


    К сожалению здесь максимально возможная скорость не будет достигнута


    1. big_dig_dev Автор
      04.01.2020 19:51

      Вы про бит TX is empty? С ним, к сожалению, с данным экраном работать не получается, хотя осциллограммы корректные.


      1. Vadimatorikda
        04.01.2020 20:40

        Тут (на хабре) некоторое время назад была статья про то, как достигать максимальной скорости из SPI (даже без DMA). Сколько помню, все свелось к тому, что нужно проверять флаг того, что данные передались из DR во внутренний сдвиговый и сам DR сейчас «пуст» (в него можно писать). И, по сути, за счет этого достигается эффект непрерывной передачи. Типа данные еще передаются, а мы уже заготовили в DR следующую пачку. Ну а вообще лучше DMA конечно. Но это чуть усложнит пример (ИМХО).


      1. Vadimatorikda
        04.01.2020 20:41

        А, и самое главное. Проверять перед отправкой, а не после. Чтобы по-максимуму использовать SPI время.


        1. GarryC
          05.01.2020 17:35

          Да, так оно и есть, я об этом писал. Ну и настроить SPI нужно правильно, многие дефолтные настройки делают его времянку 9-тактовой (а иногда и 10-тактовой) с рабочими 8 тактами и паузами между ними.


  1. rubinstein
    04.01.2020 19:48
    +1

    Зачем это шаманство с кучей массивов и указателями на массивы, когда можно делать так, как делают другие уже сто лет? Цифра ноль это код 48 в формате asc2. Далее делаем двумерный массив, где нулевой элемент это цифра ноль, первый элемент цифра один, второй элементы цифра два и так далее. При парсинге строки *char мы просто вычитаем из символа значение 48 и подставляем в массив.