Использование External Memory Interface для работы с экранчиком

AVR
Сразу оговорюсь — это не отображение видеопамяти экрана в адресное пространство. Хотя такого функционала очень хочется, но его не поддерживают используемые мной контроллеры экранчиков. Да и сомневаюсь, что такое будут встраивать в дешевые LCD/OLED экранчики.

Итак, спросите вы, а зачем же тогда упоминания о внешней памяти? Пойдем по порядку.


Введение

Про работу с обычными текстовыми LCD-экранчиками все уже в курсе, благо DiHalt в свое время выложил цикл статей по работе с такими экранчиками.
AVR. Учебный курс. Подключение к AVR LCD дисплея HD44780
AVR. Учебный Курс. Библиотека для LCD на базе HD44780

Вот так, например, выглядит диаграмма записи данных для контроллера HD44780:

тайминг записи контроллера HD44780
А вот кусочек кода из учебной библиотеки, который выполняет эту запись:

;-----------------------------------------------------------------------------------------
; Запись данных в дисплей. Код данных в R17
DATA_WR:	CLI						; Запрет прерываний
			RCALL	BusyWait		; Ждем готовности
			
			SBI		CMD_PORT,RS		; Идут данные!
WR_END:		CBI		CMD_PORT,RW		; Запись!	
			SBI		CMD_PORT,E		; Поднять строб	
			
			RCALL	PortOut			; Порт настроить на выход!
			PUSH	R17				; Сохраним данные которые будем выводить в стеке
			ANDI	R17,0xF0		; Отдавим по маске данным младшую тетраду.	

			IN		R16,DATA_PORT	; Возьмем из порта данных старое значение
			ANDI	R16,0x0F		; Отдавим ему старшую тетраду

			PUSH	R16				; Сохраним результа в стеке. Пригодится

			OR		R16,R17			; Склеим младшую тетраду из порта со старшей тетрадой данных 

			OUT		DATA_PORT,R16	; Выдадим этого мутанта в порт.

			RCALL	LCD_Delay		; Подождем	
			CBI		CMD_PORT,E		; Бросим строб вниз - данные ушли в индикатор	

			RCALL	LCD_Delay		; Подождем	 
			SBI		CMD_PORT,E		; Поднимем строб	
			POP		R16				; Достанем из стека младшую тетраду из порта
			POP		R17				; И данные которые мы выводим
	
			SWAP	R17				; Поменяем тетрады местами у байта данных 
			ANDI	R17,0xF0		; Отдавим младшую тетраду

			OR		R16,R17			; Склеим младшую тетраду из порта с старшей тетрадой данных (бывшая младшая)
			OUT		DATA_PORT,R16	; Выдадим в порт

			RCALL	LCD_Delay		; Подождем	
			CBI		CMD_Port,E		; Бросим строб	

			RCALL	PortIn			; Порт вернем в прежнее состояние - на вход
			SEI						; Разрешим прерывания
			RET						; Возврат

;=========================================================================================


Порядка 20 команд, несколько вызовов функций задержек. В общем достаточно долго.
Конечно, это происходит из-за того, что используется минималистический четырехбитный интерфейс, для заполнения экрана нужно будет вывести около 40 байт (две строки плюс немного управляющих кодов), поэтому на потери времени в этом участке никто внимания особо не обращает.

Проблемы

Но когда у тебя графический дисплей, особенно если не черно-белый, то тут с такими накладными расходами смириться не получится.

Попал мне в руки OLED-дисплейчик UMOH-8171N-O. В принципе ничего сверхобычного — 128х64 точки, 16 градаций яркости.

Внутри у него контроллер SSD1325. Смотрим в даташит на предмет общения с ним:
тайминг записи контроллера SSD1325

Опаньки! Те же грабли. Опять дергать ножками в ритме вальса. Но только теперь, для заполнения экрана надо будет передать 128*64*4/8 = 4096. Четыре килобайта! Только ножками и буду дрыгать.

Да, есть некоторые послабления — не надо ждать освобождения контроллера, выдерживать паузы — он работает достаточно шустро и при 8 МГц можно лить данные без задержек. Но все-равно, адЪ и сотона.

Сразу назревает резонный вопрос, а почему именно такой протокол передачи широко распространен?
Заглянем в даташит на древний микропроцессор Motorola MC6800:
тайминг записи MC6800

Надо же, совпадает. А раз совпадает, значит такой интерфейс разрабатывался для подключений этого экранчика к процессору 6800 в качестве периферии. Ну а если периферия — то работать с ним одно удовольствие — все эти замысловатые тайминги процессор берет на себя. И с точки зрения программиста — плюнул байт в порт, с моей стороны все вылетело, дальше ловите сами.
Ну а надпись поперек картинки как бы намекает, что процессор чуть старше динозавров.

Конечно, хочется использовать такие вкусности. Вспоминаем, что у старших мег (atmega64, atmega128 и всякие производные от них) есть интерфейс работы с внешней памятью (EMI — External Memory Interface). Заглянем в даташит:

тайминг atmega128

Кажется похоже, но неудача. Во-первых, две разных линии сигнала о записи и чтения, нет отдельного строба E и еще по мелочам. Можно, конечно, сколхозить переходник-преобразователь, но смысл его приближается к нулю.

К счастью, у контроллера SSD1325 есть еще один режим обмена (вообще-то есть еще режим последовательной записи, но его в данной статье не рассматриваем). Вот он более-менее похож на меговский интерфейс. Похож, но не до конца, поэтому будем их сводить.

timing 8080

Теория

Как вообще работает такая шина? У первых процессоров вообще и память и периферия сидели на одной шине, выбор происходил отдельным пином IO/MEM. Процессор кидает на шину адрес: «А подать мне Васю Пупкина». Устройство, у которого совпадает адрес с заказанным, напрягается и готовится работать. Потом процессор делает строб на линии чтения или записи, в зависимости от того, что он хочет произвести. Устройство, соответственно, должно либо прочитать уровни с шины данных, либо выставить свои. И так далее.

Тут два ключевых момента. Первый — дешифратор адреса. Если несколько устройств на линии, они должны как-то распознавать свой адрес. Схемка несложная, даже есть отдельные микросхемы-дешифраторы, но нам это пока ни к чему: у нас на шине одно устройство — экранчик.
Во-вторых, чтобы экономить ножки процессора, объединяется шина данных (у меги — 8 бит) с младшими битами шины адреса (у меги — 16 бит, младшие 8 объединены с данными). Поэтому на обозначениях пинов порта А написано AD0-AD7 (Address+Data), а на пинах порта С — просто А (только Address). Чтобы разрулить такую коллизию, весь процесс обмена по шине разбит на два этапа — сначала выставляется адрес, потом, по стробу ALE устройства должны быть готовы, что попрут данные и адрес будет затерт. Для работы с тупой памятью используется дополнительная защелка, которая и хранит пропавшие биты адреса. Вот примерно по такой схеме:

защелка atmega128

Вообще-то строб ALE так и расшифровывается — Address Latch Enabled.

Но опять же, поскольку у нас всего одно устройство на шине, и стробы чтения/записи будут гарантированно после того, как данные на шине выставлены, то мы может просто забить на такие условности. Да, втупую теряется половина времени, но это уже не наши проблемы.

Практика: железо

Итак, пытаемся подключить экранчик к меге.
У экранчика кроме линий питания (а ему кроме основного питания надо еще и внешние 9-14 вольт) есть такие линии:
D0-D7 — данные. С ними все понятно, соединяем их с пинами PA0-PA7
WR, RD — стробы чтения и записи. С ними тоже все понятно — соединяем с PG0 и PG1
RESET — его надо дергать при инициализации. Прицепим на любой свободный пин. У меня оказался свободен PF4
CS — Chip Select. Контроллер экранчика активен, когда на этом пине низкий уровень. Ну и посадим его на землю, экранчик-то у нас один.

Осталась линия D/C — признак данных/команды.
Казалось бы, сунуть на свободный пина, наподобие резета, но зачем? У нас же еще есть половина адресной шины не задействована.

Посмотрим подробнее на порт С — старшие биты шины адреса. Что на них выставляется? Ясный пень, адрес. А откуда он берется? Ну конечно же, когда мы инициируем запись или чтение мы же указываем адрес. Соответственно, мы можем выставить любые значения на этих пинах. Ну и дальше, особо не задумываясь, подключаем D/C к PC6. Почему именно к 14 биту? Мне на макетке было удобно туда воткнуть, никакого сакрального смысла здесь нет.

Поскольку экранчик представляет собой стекляшку с тоненьким шлейфиком, для экспериментов сделал плату-переходник, на которой заодно поместил и преобразователь в 12 вольт на LM2733. Сейчас ЛУТом 0.3/0.2 никого не удивишь, поэтому подробно рассказывать не буду. Схема обвеса такая:
Схема переходничка

Мега128 у мена распаяна на макетке, так что получилось такое переплетение:
комплекс

экранчик

Практика: софт

«А теперь пилот Иванов попробует поднять всю эту хрень в воздух.» (с) Анекдот

Для начала, чтобы было с чем сравнивать, я приведу кусочек кода до использования EMI:

void LCD_writeCommand(const uint8_t Com) {
	LCD_DATA_DDR = 0xff;
	LCD_DATA_PORT = Com;
	LCD_RW_PORT |= (_BV(LCD_R) | _BV(LCD_W));
	LCD_DC_PORT &= ~_BV(LCD_DC);
	LCD_RW_PORT &= ~_BV(LCD_W);
	LCD_DC_PORT |= _BV(LCD_CS);
	LCD_CMD_PORT |= _BV(LCD_W);
	LCD_DC_PORT &= ~_BV(LCD_CS);
}


И вот во что это разворачивалось после компиляции:
void LCD_writeCommand(const uint8_t Com) {
	LCD_DATA_DDR = 0xff;
 4de:	9f ef       	ldi	r25, 0xFF	; 255
 4e0:	9a bb       	out	0x1a, r25	; 26
	LCD_DATA_PORT = Com;
 4e2:	8b bb       	out	0x1b, r24	; 27
	LCD_RW_PORT |= (_BV(LCD_R) | _BV(LCD_W));
 4e4:	e5 e6       	ldi	r30, 0x65	; 101
 4e6:	f0 e0       	ldi	r31, 0x00	; 0
 4e8:	80 81       	ld	r24, Z
 4ea:	83 60       	ori	r24, 0x03	; 3
 4ec:	80 83       	st	Z, r24
	LCD_DC_PORT &= ~_BV(LCD_DC);
 4ee:	ae 98       	cbi	0x15, 6	; 21
	LCD_RW_PORT &= ~_BV(LCD_W);
 4f0:	80 81       	ld	r24, Z
 4f2:	8e 7f       	andi	r24, 0xFE	; 254
 4f4:	80 83       	st	Z, r24
	LCD_DC_PORT |= _BV(LCD_CS);
 4f6:	af 9a       	sbi	0x15, 7	; 21
	LCD_CMD_PORT |= _BV(LCD_W);
 4f8:	e2 e6       	ldi	r30, 0x62	; 98
 4fa:	f0 e0       	ldi	r31, 0x00	; 0
 4fc:	80 81       	ld	r24, Z
 4fe:	81 60       	ori	r24, 0x01	; 1
 500:	80 83       	st	Z, r24
	LCD_DC_PORT &= ~_BV(LCD_CS);
 502:	af 98       	cbi	0x15, 7	; 21
}
 504:	08 95       	ret


Дальше лезем в даташит на мегу. Для того, чтобы включить EMI, надо выставить бит SRE в регистр MCUCR:

MCUCR |= _BV(SRE);


Для наших целей хватит. Там еще можно настроить задержки и использование части порта С. Задержки нужны, если устройства/память на шине тормозные. А про порт С — Потом.

А теперь, если мы запишем байт по адресу 0x4000 что произойдет:

1. Выставляется шина адреса. Нас интересует один пин — A14 (который, не забываем, соединен с линией D/C экранчика). На нем у нас единичка (сли перевести 0x4000 в двоичный, то как раз увидим, что единичка в 14 бите). Это означает, что для экранчика передаваемый байт будет данными.
2. Щелкает строб ALE, на шине AD0:AD7 выставляются данные. Экранчик по-прежнему спит. Пока не щелкнут признаки записи/чтения ему все-равно, что происходит на шине.
3. Линия записи опускается в ноль. Контроллер экранчика приготовился
4. Линия записи поднимается обратно — контроллер экранчика захватывает данные с шины.

На первый взгляд кажется расхождение с даташитом на экранчик — на рисунке данные захватываются по фронту линии CS. Но покурив его детальней, проясняется, что захват данных идет фронту любой линии — CS, WR, RD. Так что все нормально.

Теперь как записать команду? А очень просто — адрес ячейки должен содержать 0 в четырнадцатом бите. Но внимание, нельзя просто указать адрес 0х0000 — в этом случае мы попадем в реальный адрес 0, чем можем порушить работу кристалла. Придется поставить в 1 какой-нить из битов старших адресов. Например, 0х8000.

Итак, ваяем программу:

#include <avr/io.h>
#include <util/delay.h>
#include <avr/pgmspace.h>
#include "font.h"

// адреса ячеек памяти для команд и данных. 
#define  LCD_C  (*(volatile uint8_t*)(0x8000))
#define  LCD_D  (*(volatile uint8_t*)(0x4000))

#define LCD_CMD_PORT 	PORTF
#define LCD_CMD_DDR 	DDRF
#define LCD_RESET 		PF4

// функции пересылки байта. Компилятор их сам сделает inline
void LCD_writeCom(const uint8_t Com) {
	LCD_C = Com;
}

void LCD_writeData(const uint8_t data) {
	LCD_D = data;
}

uint8_t LCD_readData() {
	return LCD_D;
}


// Инициализация контроллера. Выдрана из даташита
void LCD_init(void) {

	LCD_CMD_DDR |= _BV(LCD_RESET);
	LCD_CMD_PORT &= (~_BV(LCD_RESET));
	_delay_ms(100);
	LCD_CMD_PORT |= (_BV(LCD_RESET));
	_delay_ms(100);


	// Re-map
	LCD_writeCom(0xA0); // Set Re-map
	LCD_writeCom(0x40); // [0]:MX, [1]:Nibble, [2]:H/Vaddress [4]:MY, [6]:Com Split Odd/Even "1000010"

	// Display ON/OFF
	LCD_writeCom(0xAF); // AF=ON, AE=Sleep Mode
	LCD_writeCom(0x86); // Set Current Range84h:Quarter, 85h:Half,86h:Full
	// Contrast Control
	LCD_writeCom(0x81); // Set Contrast Control
	LCD_writeCom(0x38); // 0 ~ 127
}

// функция установки текущей точки
void LCD_gotoXY(uint8_t x, uint8_t y) {
	LCD_writeCom(0x15);
	LCD_writeCom(x>>1);
	LCD_writeCom(63);

	LCD_writeCom(0x75);
	LCD_writeCom(y);
	LCD_writeCom(63);
}

// очистка экрана
void LCD_clear(void) {
	LCD_writeCom(0x24);
	LCD_writeCom(0x0);
	LCD_writeCom(0x0);
	LCD_writeCom(63);
	LCD_writeCom(63);
	LCD_writeCom(0x0);

	_delay_ms(1);
}

// вывод символа на экран
void LCD_putcXY(uint8_t x, uint8_t y, uint8_t c, uint8_t attr) {
	uint8_t dx, dy, i, j, mask, data;
	uint8_t sym[6];

	dx = x;
	dy = y;

	// считаем символ в память
	for (j = 0; j < 6; j++) {
		if (j == 5) {
			sym[j] = 0;
		} else {
			if (c < 0x90) {
				sym[j] = pgm_read_byte(&font_5x8[c - 0x00][j]);
			} else {
				sym[j] = pgm_read_byte(&font_5x8[c - 0x30][j]);
			}
			sym[j] ^= attr;
		}
	}

	mask = 1;
	for (i=0; i<8; i++) {
		LCD_gotoXY(dx, dy);

		data = 0;
		if (sym[0] & mask) data |= 0x0f;
		if (sym[1] & mask) data |= 0xf0;
		LCD_writeData(data);

		data = 0;
		if (sym[2] & mask) data |= 0x0f;
		if (sym[3] & mask) data |= 0xf0;
		LCD_writeData(data);

		data = 0;
		if (sym[4] & mask) data |= 0x0f;
		if (sym[5] & mask) data |= 0xf0;
		LCD_writeData(data);

		dy++;
		mask = mask << 1;
	}

}


// вывод строки на экран
void LCD_putsXY(uint8_t x, uint8_t y, char s[]) {
	uint8_t i, dx;

	dx = x;

	for (i = 0; s[i] && i < 25; i++, dx += 6) {
		LCD_putcXY(dx, y, s[i], 0);
	}

}


// рисуем горизонтальную линию
void LCD_drawHLine(uint8_t x1, uint8_t y1, uint8_t x2,  uint8_t c) {
	uint8_t i,_c;

	_c = (c<<4) + c;
	LCD_gotoXY(x1,y1);
	for(i = (x2-x1+1)>>1; i>0; i--) {
		LCD_writeData(_c);
	}
}


int main() {

	//включаем режим работы с памятью
	MCUCR |= _BV(SRE);

	LCD_init();
	LCD_clear();

	LCD_putsXY(0,20,"for");
	LCD_putsXY(0,30,"we.easyelectronics.ru");
	
	while(1);

}

И прицеплю архив с этими файлами.

И вот теперь, во что превратил компилятор процедуру записи:
void LCD_writeCom(const uint8_t Com) {
	LCD_C = Com;
 522:	80 ea       	ldi	r24, 0xA0	; 160
 524:	80 93 00 80 	sts	0x0000, r24


Выводы


С использованием EMI можно подключить ту периферию, которая поддерживает работу с шиной типа «8080-совместимые». У моего экранчика этот режим выбирался замыканием нужных ног на землю/питание.

Финты ушами

При использовании EMI, если нам не надо задействовать большие объемы внешней памяти, мы можем освободить часть пинов порта С. Это определяется битами XMM2:XMM0 в регистре XMCRB:

XMM bits

В данной ситуации можно было бы подключить линию D/C к пину PC1 (или PC0) и соответственно освободить пины PC2:PC7.

Файлы в топике: testLCD.zip

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

RSS свернуть / развернуть
прикольно
0
Интересно. ЩАс попробую на вот этом
0
Да, 8 бит 8080 — это оно
0
… можно вопрос, у меня примерно такой-же дисплэй, хочу его опробовать. С кодом вроде все ясно а вот с обвесом: надо ли там какие нибудь особые источники питания, резисторы, кандеры, или без замарочек подсаединять напрямую все линии управления и данных к микроконтроллеру?
0
Сегодня переделал вывод на TFT экран таким методом. Работает, что не может не радовать:)
Особенно порадовало компиляция команды записи данных в две команды ассемблера.
0
  • avatar
  • PRC
  • 05 июля 2011, 00:57
Хай всем!!!
есть вопросик по данной теме — может кто сталкивался с ними и знает чт эт за чудо)
вобщем, в наличии пара-тройка китайских плееров Daewoo International MPF-2400, фотка разобранного.
интересует отсюда экранчик — что эт за чудо и как его возможно использовать?
узнал тольк разрешение… 320х240 =\

0
Во первых, не вижу ни одного линка.
Во вторых, скорее всего там TFT-экран без контроллера (точнее, со стандартным TFT-интерфейсом, какой-то контроллер там есть явно). Юзать можно, облегчается стандартным интерфейсом (разве что надо узнать распиновку, но она вродже тоже часто стандартна). Но дляэтого нужен МК с контроллером TFT, внешний контроллер TFT или самопальный на ПЛИС. Хотя, в DSO Nano STM32F103 и без контроллера успевает рулить таким экраном. Можешь его и собрать, кстати, кроме экрана и контроллера заряда там все детали довольно доступные или заменяемые на доступные.
0
блин… вставлял же…
s2.ipicture.ru/Gallery/Viewfull/5099107.html
угу, ясненьк, получается на меге из распространённых такой экран не завести…
кст, еще один вот нарыл, плеер Explay T9, с тач скрином даже… фотка
s2.ipicture.ru/Gallery/Viewfull/5100582.html
на второй плеер даж схему нашел, вот кусочек с дисплеем:
s2.ipicture.ru/Gallery/Viewfull/5100592.html

что можно по второму сказать? (ну кроме тача резистивного) )
p.s. сейчас воспользоволся предпросмотром — кнопка добавить ссылку не работает, вставляеш а в сообщении после отправки ссылок нету =(
0
УМВР. Но большинство не учиытвает, что кнопка превращает в ссылку выделенный текст.
Первая картинка: да, почти наверняка голый TFT. Кстати, насколько я вижу, от него оторвана отражающая пленка сзади.
Вторая: явный резистивный тач и почти наверняка голый TFT.
Третья: да, голый TFT.
на меге из распространённых
Не знаю. Но однозначно, либо нужна мега из быстрых, либо со встроенным контроллером. На эти матрицы нужно гнать непрерывный поток данных — выставляем на D* пиксель, дергаем строб, выставляем следущий, дергаем, заполнили строку — дергаем другой строб. Итого гоним байтики с частотой W*H*F, где W и H разрешение, а F — частота обновления (60Гц обычно, причем допуск на нее достаточно мал).
0
да, пленка снята, крепится не к дисплею а к основной платке, которая прижимает его… но она есть)
ясно, Спасибо Вам огромное!
а если мне не так важен fps, к примеру хватило бы с головой 5-10, что бы выводить данные текстового формата (никакой анимации или видео… скорей даж без картинок. может быть растровая графика)
в наличии имеются
atmega8
atmega32
atmega128
at90usb162
из них как я предполагаю по кол.пинов тольк 128я подойдет, хватит ли её для подобных целей?
0
А фиг там. TFT работает только на штатной частоте. Так что имеет смысл тока если ты будешь обновлять картинку в памяти контроллера 10 раз в секунду, но контроллер должен все равно гнать ее на 60Гц.
из них как я предполагаю по кол.пинов тольк 128я подойдет, хватит ли её для подобных целей?
Еще не факт что хватит частоты, да и заниматься будет тока выводом на дисплей. Лучше купить дешевый ARM с встроенным контроллером. Можно и самопальный контроллер на ПЛИС и ОЗУ, но это не дешевле.
0
о как даж… не предполагал такой засады)
чтож, видимо таки пора и arm изучать…
м, не подскажете с чего начать?
и какой arm наиболее дешёвый для таких целей подойдет ?(ну вначале на поиграться, а там уже посмотрим чт дальш будет) )
0
Попробуй собрать DSO Nano например)
АРМ любой с LCD контроллером (TFT, а не тупой сегментной стекляшки, как в STM8L), ченить из STM32/LPC13/17, мож ченить из тексасовских, если они доступны.
Можно попытаться поколупать железо плееров. Например, на вогоплеер доки нашли, там довольно мощный SoC на основе MIPS.
0
)) как вариант) я кст думал заказать с dealextreme подобный… потом не сложилось)
а по поводу книжек или статеек чего нить для начинающего? :3
кст, на C или Cpp програмируется arm?
Благодарен Вам очч за оперативные и разьяснительные ответы! :)
0
насчет книг-статей хз, программируется на чем угодно. ЕМНИП для ARM'а и фрипаскаль есть.
0
окк, еще раз Спасибо! :)
0
Вопрос, возможно, идиота: почему 4 КБ памяти? сколько будет на 320х240 RGB TFT?
0
  • avatar
  • cs134
  • 21 октября 2012, 21:40
16 градаций яркости монохрома — это 4 бита. Соответственно 128*64/2 = 4096 байт

320*240*2байта(16бит) = 153600 байт
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.