Oled-дисплей WEH001202

Решил попробовать oled-дисплей WEH001202. Порадовало наличие SPI-протокола. Он, конечно, поддерживает и старые протоколы типа 6800 и 8080, но это пережитки прошлого. Использование этих протоколов для новых устройств считаю моветоном (хотя и этот SPI оказался не тем, что хотелось :D).
Работу с параллельными протоколами рассматривать не будем - там всё как в подобных дисплеях. Рассмотрим работу по SPI и графический режим.

Оглавление

Аппаратное подключение

По умолчанию дисплей настроен на работу по параллельным протоколам. Для перевода в работу по SPI необходимо перепаять на плате дисплея две перемычки (нулевые резисторы).
На рисунке 1 показано, как было до и после доработки. Дисплеи могут отличаться расположением резисторов. На рисунке 1 (справа) привёл пример немного отличающегося дисплея, не помню, откуда взял.


Рисунок 1. Фото перемычек до после доработки и ещё один вариант исполнения такого дисплея.

Перемычки на плате подписаны. В принципе понятно, что куда.
Дорабатываем (перепаиваем), подключаем в соответствии с даташитом на дисплей.
Я подключал к плате NUCLEO-F103RB. Схему подключения изобразил на рисунке 2.


Рисунок 2. Схема подключения дисплея.

WEH001202 основан на чипе WS0010, поэтому всю информацию по работе с дисплеем будем брать из даташита на этот чип. Аппаратно реализованный SPI контроллера не получилось использовать, поэтому подключил как удобнее было (как провода легли).

Соответствие пинов разъёма дисплея и разных названий.
Pin12 - DB5 – SCL – он же SCLK – тактовый сигнал
Pin13 - DB6 – SDO – он же MISO – данные дисплея в контроллер
Pin14 - DB7 – SDI – он же MOSI – данные от контроллера в дисплей
Pin15 – NC (используется только в режиме SPI) – CS – он же SS – это выбор чипа.

Особенности SPI-протокола

В целом SPI как SPI (диаграмма приведена на рисунке 3). Ничего особенного, если не считать, что надо передавать 10 бит, а при чтении получаем 18. В общем, как ни крути, аппаратным SPI не обойтись. Не видел ещё ни одного контроллера, где подобное решение было аппаратным.



Рисунок 3. Диаграмма SPI для записи и для чтения.

Придётся дёргать ногами вручную :(

По времянке. Судя по даташиту, период клока 300 нс. Соответственно, это 3.333 Мгц – это и будет максимальный битрейт.
У меня получилась вот такая диаграмма.


Рисунок 4. Осцилограмма получившегося протокола. канал1- SCL, канал 2 – SDI

По осцилограмме видно, что она «немного» отличается от идеальной. Суть простая: вначале выставляем данные, потом дёргаем клок. На осцилограмме нет ещё одного нужного канала - выбора чипа. Он постоянно 0 во время передачи, и только после окончания передачи/приёма байта выставляется в 1.

Программное описание протокола

Для общения с дисплеем написал 3 процедурки. Не стал заморачиваться и писать универсальную процедуру с параметрами, думаю, так удобнее будет:

Вывод команды

void spi_com (uint8_t dat){	//Вывод команды в дисплей
uint8_t i;			//для счётчика
uint16_t data=dat;		//RS=0 RW=0 по этому просто приравниваем чтоб разрядность соответствовала
lcd_scl_s;			//SCL=1 подготовка к передаче
lcd_sdi_s;			//SDI=1
lcd_cs_c;			//CS=0 выбор чипа (начало передачи)

for (i=0;i<10;i++){		//счётчик на 10 (будем 10бит передавать)
	lcd_scl_c;		//SCL=0 это защёлка. 

	if (data & 0x200) lcd_sdi_s//если не ноль выставляем бит SDI=1
	else lcd_sdi_c		//если ноль соответственно SDI=0

	lcd_scl_s;		//SCL=1 это защёлка данных тут дисплей принял один бит

	data<<=1;		//сдвигаем дату
	}
lcd_cs_s;			//CS=1 концовочка выбор чипа убираем и дисплей смотрит чё принял

Вывод данных

void spi_dat (uint8_t dat){	//Вывод данных в дисплей
uint8_t i;			//для счётчика
uint16_t data=0x200 | dat;	//добавляем бит команды RS=1
lcd_scl_s;			//SCL=1 подготовка к передаче
lcd_sdi_s;			//SDI=1
lcd_cs_c;			//CS=0 выбор чипа (начало передачи)
for (i=0;i<10;i++){		//счётчик на 10 (будем 10бит передавать)
	lcd_scl_c;		//SCL=0 это защёлка. 
	if (data & 0x200) lcd_sdi_s//если не ноль выставляем бит SDI=1
	else lcd_sdi_c		//если ноль соответственно SDI=0
	lcd_scl_s;		//SCL=1 это защёлка данных тут дисплей принял один бит
	data<<=1;		//сдвигаем дату
	}
lcd_cs_s;			//CS=1 концовочка выбор чипа убираем и дисплей смотрит чё принял
}

Чтение статуса

uint8_t spi_in (){		//Чтение из дисплея
uint8_t i;			//для счётчика
uint8_t in;			//для приёма данных

uint16_t data=0x100;		//при приёме надо передавать 10 бит
				//в них сигналы RS RW и 8бит пустых данных
				//RS=0 RW=1 (чтение) 8бит данных 0x00
uint16_t bites=0x200;		//это масочка для проверки битов на вывод
lcd_scl_s;			//SCL=1 подготовка к передаче
lcd_sdi_s;			//SDI=1
lcd_cs_c;			//CS=0 выбор чипа (начало передачи)
for (i=0;i<18;i++){		//приём состоит из 18бит первые 10 это просто команда на чтение
				//следующие 8бит это данные
	lcd_scl_c;		//готовим клок
	if (data & bites) lcd_sdi_s//если не ноль выставляем бит SDI=1
	else lcd_sdi_c;		//если ноль соответственно SDI=0
	
	if (bites) bites>>=1;	//если не прокрутился полностью крутим
	else bites=0x200;	//если прокрутился то возврящаем на 10бит
	
	in<<=1;			//приёмный байт поворачиваем для подготовки нового бита
	if (lcd_sdo) in|=1;	//приписываем принятый бит
	lcd_scl_s;		//защёлкиваем клок
	}
lcd_cs_s;			//CS=1 концовочка выбор чипа убираем и дисплей смотрит чё принял
return in;			//выплёвываем принятый байт
}

Пины назначаются с помощью дефайнов в начале программы.
Писал пример под STM32, поэтому получилось вот так:

#define lcd_scl_s GPIOA->BSRR = GPIO_BSRR_BS0;  //клок
#define lcd_scl_c GPIOA->BSRR = GPIO_BSRR_BR0;

#define lcd_sdo   GPIOA->IDR  & GPIO_IDR_IDR1   //прин прима данных с дисплея PORTA (1бит)

#define lcd_sdi_s GPIOA->BSRR = GPIO_BSRR_BS4;  //вывод данных в контроллер
#define lcd_sdi_c GPIOA->BSRR = GPIO_BSRR_BR4;

#define lcd_cs_s  GPIOB->BSRR = GPIO_BSRR_BS0;  //выбор чипа 
#define lcd_cs_c  GPIOB->BSRR = GPIO_BSRR_BR0;

Для работы процедур необходимо подправить дефайны под свою схему, контроллер или что там ещё можно поменять.
По скорости особо не заморачивался - написал как написалось.тактовая контроллера была 8Мгц. Тактовая протокола получилась (можно посмотреть на рис. 4) 100кГц - в принципе, нормально для большинства случаев.

Инициализация и настройки

Для инициализации надо послать в дисплей 7 байт с настройками. Это минимальный набор, достаточный для включения и работы дисплея. Сейчас побитно все байты разберём:

  1. 0  0  1  1   N  F  FT1  FT0
  2. N – количество строк в дисплее 0 – однострочный, 1 - двухстрочный
    F – размер шрифта. В даташите написано, что шрифт может быть 5х8 и 5х10. Эксперименты показали, что изменения шрифта не происходит. Меняется положение курсора. Он становится на второй строке, где-то ближе к середине. Я думаю, что это для отдельных каких-то вариантов дисплея.
    FT1, FT0 – это переключение набора символов. Там их 4. В третьем наборе есть русские символы.

  3. 0  0  0  0   1  D   C   B
  4. D – включение-выключение дисплея; 1-включен.
    C – включение-выключение курсора; 1-включен. Мигает весь символ.
    B – подсветка позиции курсора; 1-включен. Палка под символом.

  5. 0  0  0  0   0  0  0  1 - команда очистки дисплея.
  6. 0  0  0  0   0  0  1  0 – перевод курсора в начало дисплея.
  7. 0  0  0  0   0  1 I/D  SH
  8. I/D – смещение курсора, увеличение/уменьшение.
    SH – включение сдвига.

  9. 0  0  0  1   G/C PWR  1 
  10. 1  - выбор режима ну и включение какого-то внутреннего источника питания
    C/G – выбор режима; 1-графический 0-текстовый
    PWR – включение внутреннего источника. Всегда 1, в общем (если не надо спячки).

  11. 0  0  0  1   S/C  R/L    0  0  - настройки режима.

S/C – выбор: или дисплей сдвигается при выводе , или курсор. 0 – значит, курсор.
R/L – выбор: сдвиг вправо или влево.

Для инициализации дисплея сделана небольшая процедура:

Инициализация дисплея в текстовом режиме с включеной подсветкой знакоместа (для наглядности), что получилось смотрите на рис.5 а

void lcd_init (){               // процедура инициализации дисплея
lcd_scl_s; lcd_sdi_s; lcd_cs_s;	//ноги вверх !
//выдаём дисплею 5 заветных байт
	lcd_wait_BF();        	//ждём готовности на всякий случай
	spi_com (0x38);	//N-1 F-0 FT1,0-0  т.е дисплей 2строчный , шрифт 5х8, набр символов 0
	lcd_wait_BF();          //ждём готовности
	spi_com (0x0E);	//D-1 C-0 B-1  т.е дисплей Вкл. Курсор Вкл. Знакоместо - подсвечивается
	lcd_wait_BF();          //ждём готовности
	spi_com (0x01);	// это не команда очистки дисплея не путать !
	lcd_wait_BF();          //ждём готовности
        	spi_com (0x02);         //возврат в начало
	lcd_wait_BF();          //ждём готовности
	spi_com (0x06);	//I/D-1 SH-0 т.е сдвиг курсора вправо при выводе, скрол экрана выключен
//тут настройка закончена можно перелючать режимы
	spi_com (0x17);	//настройка смещения курсора в право 
	spi_com (0x14);	//включение текстового режима
}
    

Вот ещё несколько настроек:
Настроечные байты по порядку: 0x38, 0x0E, 0x01, 0x02, 0x05, 0x17, 0x14 (см. рисунок 5.б).
Задано смещение дисплея, а не курсора (как в предыдущем варианте). НО ! когда дисплей смещается как бы в отрицательную сторону, курсор переходит на другую строку - в общем, надо там хитрить, чтоб курсор не уходил в минус, или учитывать это смещение. Или изначально уже писать символы в смещённое пространство и там уже двигать.

Причём вообще работать в таком режиме не очень удобно потому, что точное положение, в котором сейчас находится дисплей, непонятно. Нельзя узнать, насколько в данный момент сдвинут дисплей. Можно прочитать только текущее положение курсора (вместе со статусом). Поэтому, если всё сглючит (у меня было такое), контроллер думает, что всё порядке, а изображение всё сдвинуто. И никак это не проконтролировать. Выкручивался тем, что инициализировал дисплей время от времени.

Настроечные байты по порядку: 0x3C, 0x0E, 0x01, 0x02, 0x06, 0x17, 0x14 рисунок 5.в
В этой настройке выбрал шрифт не 5х8, а 5х10. Увеличения шрифта не произошло! Но непонятно, зачем курсор идёт по нижнему ряду посередине почти - вроде как ненужный режим.


Рисунок 5. Опыты с настройками.

Помимо трёх наборов символов WEH001202 позволяет (собственно, как и большинство подобных дисплеев) подгрузить произвольные символы. Где-то натыкался, мол, в этом дисплее, в отличии от других, подгружаются 16 символов. Нет. Их тут тоже 8.
Для подгрузки написана вот такая процедурка:

void load_sym (const uint8_t *pnt, uint8_t num,uint8_t count){ //num-это порядковый номер символа (0-7) начиная						//с которого будем подгружать символы
//count – количество символов
spi_com (0x40+(num<<3));			//тут мы выдаём в дисплей адрес CGRAM по которому символ 
//хранится. 0x40-команда +номер символа *8.
for (uint8_t i=0;i<(count*8);i++){			//и цикл с передачей данных
  spi_dat (*pnt++);
}
}

Применяется это так:
Создаём массив, в котором хранятся нарисованные символы. Я их формировал с помощью программульки Symbols8x5 - про неё попозже напишу.
Примеры программы. Всё тут выписывать не буду, результат на рис 6


Рисунок 6. Вывод подгруженных дополнительных символов.

С обычным символьным режимом всё понятно.
Самое интересное - это графический режим.

Графический режим

Для перевода дисплея в графический режим достаточно установить бит C/G, не обязательно при этом делать всю инициализацию. Можно переключать режимы прямо на лету: типо spi_com (0x1F) и всё, дисплей переходит в графический режим, потом опа - spi_com (0x17) - и он переключается обратно.

Если закрасить дисплей полностью, получится вот что:


Рисунок 7.Разделение на символы в графическом режиме.

По рисунку видно, что полностью дисплей не закрасился.
Вкратце по командам:
Для указания на адрес, грубо говоря, по оси Х - команда 0x80. Чтобы переместить курсор на нужную позицию, надо выдать команду spi_com(0x80 + X); где Х – это смещение соответственно. В пикселях слева направо.
Для перемещения курсора по вертикали выдаём команду spi_com(0x40+Y), где Y = 0 - верхняя строка, Y = 1 -нижняя.
Любая запись данных (spi_dat) выдаёт 8 бит на дисплей. Младшие биты вверху, старшие внизу.

Для демонстрации создал программульку небольшую, выводящую логотип. Таблицу логотипа сгенерировал с помощью программы logos_8 (потом про неё тоже расскажу - делал её для преобразования картинок в массив, удобовставляемый в код). Вот результат.


Рисунок 8. Пример работы с графическим дисплеем.

Самое хорошее в графическом дисплее то, что нет привязанности к шрифтам. Можно подгружать любые символы, сделать нормальную удобоваримую таблицу для работы с русскими шрифтами, чтобы не хранить строки в каких-то непонятных символах, а в нормальной раскладке.

Для этого есть у меня программулька, которая преобразует картинку в шрифт; очень удобно редактировать и вставлять в код. Сейчас сделаем шрифт, подгрузим и посмотрим. Программа называется fonts5x8.
Весь текст проекта смотреть не будем (там ничего особенного), разберём только некоторые моменты.
В начале программы вставляем таблицу с шрифтом – например, вот такой (нарисовал по-быстрому, особо не заморачиваясь с дизайном. Обычный шрифт)

static const uint8_t sym[]={
0,0,0,0,0,//0
0,0,191,0,0,//1
0,6,0,6,0,//2
40,124,40,124,40,//3

……

…..

252,32,120,132,120,//158
216,36,36,252,0,//159
};

Получилось 160 символов. По 5 байт, итого 800байт на шрифт :) круто конечно, но когда памяти полно, можно раскошелиться - зато преобразование при выводе минимальное. Накидал вот такую процедурку для вывода символов. Всё красиво.

void print_sym (uint8_t sy){//вывод одного символа
uint8_t ddt;
if (sy>0xAF) sy=sy-64;		//это смещение т.к в раскладке Русские буквы смещены
sy=sy-32;			//это смещение т.к. чтоб отбросить управляющие символы
for (uint8_t q=0;q<5;q++){	//вывод 5 байт. Т.е прорисовка символа
	ddt=sym[sy*5+q];
	spi_dat(ddt);
	}
}

Сначала проверяем, если это русские буквы строчные, то смещаем их на 64. Если посмотреть раскладку ascii-кодов, это будет понятно, так как там видно, что они стоят отдельно и между ними и остальными буквами ненужные символы. Вооот. Потом вычитаем 32 - это чтобы убрать управляющие символы, которых в таблице нет, и вуаля!

Потом добавляем процедурку вывода строки

void print_str(uint8_t *dat){	//выводим строку символов
while (*dat) {
	print_sym(*dat++);
	}
}

Тоже всё просто, даже нечего объяснять - обычный вывод. Зато можно не задумываясь писать print_str(«бла бла бла”) - дисплей всё поймёт. Пример кода здесь. Результат на рисунке 9.


Рисунок 9.Результат вывода собственного шрифта в графическом режиме.

Ну вот и всё, собственно. Напоследок бегущая строчка а-ля спектрум :) не хватает ещё снежка на заднем фоне :) музыку взял из «Shock Megademo Better Quality Part 2 ZX Spectrum».

Приложения

  1. Даташит на дисплей WEH001202
  2. Даташит на чип дисплея WS0010
  3. Программа преобразования для создания доп. символов symbol8x5
  4. Пример кода с подгрузкой доп. символов
  5. Пример кода с загрузкой и выводом логотипа
  6. Программа для преобразования картинок в спрайты logos_8
  7. Программа для преобразования картинки в шрифт fontes8x5
  8. Пример кода с выводом на дисплей символов в графическом режиме
  9. Пример кода с выводом на дисплей бегущей строки

Программы для компа написаны в Processing. Программы для контроллеров написаны в IAR 7.50

Автор: Сергей Меньшиков , e-mail для связи -

Обсуждение на форуме.