Введение
Модель дисплея называется H016IT01. Данный дисплей интересен прежде всего тем, что он является трансфлективным(transflective). Это означает, что изображение на нем должно быть видно даже под ярким солнцем. А также это чуть ли не единственная доступная модель с этой особенностью на известном китайском сайте.
Статья же увидела свет потому, что информации по контроллеру SSD1283A очень мало(как в русском, так и западном сегменте сети), и руководства я нигде не встречал. В сети можно найти даташит, однако там нет информации по инициализации и работе с дисплеем, а из полезного только описания регистров.
Хочу подчеркнуть, что данный материал конечно же не является истиной последней инстанции. Я привожу лишь свой опыт взаимодействия с устройством. Основная цель статьи проста — помочь всем тем, кто решил, хочет или захочет поработать с данным дисплеем, не более.
Дисплей имеет 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, а значит с дисплея мы ничего считать не можем, что нам и говорит даташит.
Для начала настроим 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);
После этого на дисплее должно поменяться изображение с белого на серый цвет телевизионных помех.
Рисуем прямоугольник
Контроллер 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);
Результат:
Рисуем буквы и цифры
Создадим перечень массивов с координатами для символов.
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);
В таком режиме дисплей работает быстро, глаз не замечает процесса отрисовки.
Для всех интересующихся, полный код проекта можно посмотреть по ссылке.
Комментарии (14)
AntonSor
03.01.2020 16:41Да-да, помню, на изиэлектрониксе его «раскуривали», но так и не «раскурили». И SSD1238 действительно контроллер для TFT
GarryC
04.01.2020 16:30-1И в 2020 году мы по прежнему будем видеть тексты вроде
GPIOA->MODER |= (GPIO_MODER_MODE5_1 | GPIO_MODER_MODE6_1 | GPIO_MODER_MODE7_1);
мда.AntonSor
04.01.2020 16:32-1Ваши предложения?
Vadimatorikda
04.01.2020 20:42+1Похоже имеют ввиду, что даже CubeMX научился генерировать define по имени пина, чтобы не было этих безымянных MODEX_Y.
master__v
04.01.2020 19:48void stm32f407_spi::stm32f407_spi_send(uint8_t data)
{
SPI1->DR = data;
while((SPI1->SR & SPI_SR_BSY)) continue;
}
Этого достаточно чтобы SPI заработал в нужном нам режиме и передавал данные на максимально возможной скорости.
К сожалению здесь максимально возможная скорость не будет достигнута
big_dig_dev Автор
04.01.2020 19:51Вы про бит TX is empty? С ним, к сожалению, с данным экраном работать не получается, хотя осциллограммы корректные.
Vadimatorikda
04.01.2020 20:40Тут (на хабре) некоторое время назад была статья про то, как достигать максимальной скорости из SPI (даже без DMA). Сколько помню, все свелось к тому, что нужно проверять флаг того, что данные передались из DR во внутренний сдвиговый и сам DR сейчас «пуст» (в него можно писать). И, по сути, за счет этого достигается эффект непрерывной передачи. Типа данные еще передаются, а мы уже заготовили в DR следующую пачку. Ну а вообще лучше DMA конечно. Но это чуть усложнит пример (ИМХО).
Vadimatorikda
04.01.2020 20:41А, и самое главное. Проверять перед отправкой, а не после. Чтобы по-максимуму использовать SPI время.
GarryC
05.01.2020 17:35Да, так оно и есть, я об этом писал. Ну и настроить SPI нужно правильно, многие дефолтные настройки делают его времянку 9-тактовой (а иногда и 10-тактовой) с рабочими 8 тактами и паузами между ними.
rubinstein
04.01.2020 19:48+1Зачем это шаманство с кучей массивов и указателями на массивы, когда можно делать так, как делают другие уже сто лет? Цифра ноль это код 48 в формате asc2. Далее делаем двумерный массив, где нулевой элемент это цифра ноль, первый элемент цифра один, второй элементы цифра два и так далее. При парсинге строки *char мы просто вычитаем из символа значение 48 и подставляем в массив.
chnav
Странно, мне казалось, что трансрефлективный дисплей по определению должен быть LCD, никак не OLED.
alex-open-plc
Именно так.
И вопрос автору: так TFT или OLED? Если OLED, то зачем ему подсветка?
big_dig_dev Автор
Все верно, TFT.