Подключение дисплея Nokia 1616 на примере LPC1343

Что за зверь

Как я уже писал в обзоре, дисплей с подсветкой, имеет разрешение 126х160 пикселей и глубину цвета 18 бит. Размеры примерно 28х36мм. шлейф имеет 12 контактных площадок (1-я и 12-я не подключены) с шагом 0.5мм. Логика вся работает от 3.3В и лучше не завышать. От 5В скорее всего сгорит. На подсветку надо около 7В, но есть и версии с 3В подсветкой.

По правильному на питание цифровой схемы надо подать 1.8В и на аналоговую 2.7В, но всё прекрасно работает и от одного источника 3.3В (уже год - не сгорело ничего), по даташитам это напряжение в пределах допустимого.

Работа с дисплеем аналогична работе с Nokia 6100 (статей на эту тему предостаточно) по тому глубоко вдваться не стану, а сконцентрируюсь на различиях.

Отмечу что мне попадалось несколько разновидностей дисплеев, но все они работают с данным кодом. Отличия в ориентации верх/низ и в напряжении подсветки, как уже поминалось.

Хочу так же вас предумпредить. Промывал от канифоли плату спиртом, в итоге просто слои рассеивателей и поляризаторов разошлись. Не давайте спиртного дисплею, он спиваетса быстро! :)

Как подключить

В моём варианте работает следующая схема включения:
Схема подключения дисплея Nokia 1616

Важно! на данной схеме учтены только "подключенные" выводы, крайние в счет не берутся. Таким образом 1-й вывод на схеме соответствует 2-му на дисплее. 10 вывод схемы - 11-му на дисплее.

Взаимодействие

Данный дисплей, как и большенство других цветных Nokia, для взаимодействия использует интерфейс SPI 9 бит. Возможно взаимодействие в режимах 0 и 3 (если память не изменяет). Успешно работает на скоростях 4Мб/с и 9Мб/с, больше проверять не стал. Линия данных на самом деле у дисплея двунаправленная и по ней можно считать такую информацию как ID контроллера в дисплее, но мы её быдем использовать только для записи.

Старший бит является признаком данных: для команды он равен 0, для данных 1. младшие 8 бит содержат либо сами данные, либо код команды.

Ну и естественно перед работой с дисплеем его надо сбросить. делаеся это выбором дисплея и подачей на вывод Reset низкого уровня в моём случае на 100мс. После снятия сброса надо так же выдержать паузу, что бы дисплей успел инициализироваться.

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

И так, получается следующий набор функций (для LPC1343)


void SPI_init() {	// Раздел 13.2 UM10375
    // Reset SSP (пункт 4)
    LPC_SYSCON->PRESETCTRL &= ~(1<<0);	// "маска" сброса SSP
    LPC_SYSCON->PRESETCTRL |= (1<<0);	// 1 в бит RST_SSP_0

    // Enable AHB clock to the SSP domain. (пункт 2)
    LPC_SYSCON->SYSAHBCLKCTRL |= (1<<11);

    // Divide by 1 (SSPCLKDIV also enables to SSP CLK) (пункт 3)
    LPC_SSP->CPSR = 0;	// отключим тактирование (а то мало ли)

    // Set P0.9 to SSP MOSI
    LPC_IOCON->PIO0_9	&= ~(7<<0);
    LPC_IOCON->PIO0_9	|= (1<<0);	// использовать как вывод MOSI
    LPC_IOCON->PIO0_9	|= IOCON_COMMON_MODE_PULLUP;

    // Set 2.11 to SSP SCK (0.6 and 0.10 can also be used)
    LPC_IOCON->SCKLOC	= 1;	// SCK на вывод 2.11
    LPC_IOCON->PIO2_11	&= ~(7<<0);	// сброс текущей функции порта ввода-вывода
    LPC_IOCON->PIO2_11	|= (1<<0);	// использовать как вывод SCK
    LPC_IOCON->PIO2_11	|= IOCON_COMMON_MODE_PULLUP;

    // Set P0.2/SSEL to GPIO output and high
    LPC_IOCON->PIO0_2	&= ~(7<<0);	// сброс текущей функции порта ввода-вывода
    LPC_IOCON->PIO0_2	|= (1<<0);	// использовать как вывод SSEL	(можно обычным GPIO как 0)
    LPC_IOCON->PIO0_2	|= IOCON_COMMON_MODE_PULLUP;
    LPC_GPIO0->DIR	|= 1<<2;
    LPC_GPIO0->DATA	|= 1<<2;

    // If SSP0CLKDIV = DIV1 -- (PCLK / (CPSDVSR X [SCR+1])) = (72,000,000 / (2 x [3 + 1])) = 9.0 MHz
    LPC_SSP->CR0	= ( (8<<0)	// Размер данных 1000 - 9 бит
					  | (0<<4)	// Формат фрейма 00 - SPI
					  | (0<<6)	// Полярность 0 - низкий уровень между фреймами
					  | (0<<7)	// Фаза 0 - по нарастанию
					  | (3<<8)	// Делитель частоты шины на бит
					  ) ;

    // Clock prescale register must be even and at least 2 in master mode
    LPC_SSP->CPSR = 2;	// пердделитель 2-254 (кратно 2)

    // Enable device and set it to master mode, no loopback (разрешаем работу)
    LPC_SSP->CR1	= ( (0<<0)	// 0 - Loop Back Mode Normal
					  | (1<<1)	// Разрешение работы 1 - разрешено
					  | (0<<2)	// Режим ведущий-ведомый 0 - мастер
					  );
}

void SPI_send(uint16_t value) {
	while ((LPC_SSP->SR & ((1<<1) | (1<<4))) != (1<<1));	// если буффер передачи не переполнен и устройство не занято
	LPC_SSP->DR = value;
}

// Вспомогательные макросы
#define LCD_send(x)	SPI_send(x)
#define LCD_command(cmd)	LCD_send(cmd)
#define LCD_data(data) LCD_send(0x0100|(uint8_t)(data))

void LCD_reset(void) {
	// Настройка для вывода Reset дисплея
	LPC_IOCON->PIO0_8	&= ~(7<<0);	// сброс текущей функции порта ввода-вывода
	LPC_IOCON->PIO0_8	|= IOCON_COMMON_MODE_PULLUP;
	LPC_GPIO0->DIR	|= 1<<8;
	LPC_GPIO0->DATA	|= 1<<8;
	// Настройка для вывода Select
	LPC_IOCON->PIO0_2	&= ~(7<<0);	// Временно отключаем спецфункцию вывода выбора SPI
	delayms(100);
	// Сброс дисплея
	LPC_GPIO0->DATA	&= ~(1<<2);	// ncs = 0
	LPC_GPIO0->DATA	&= ~(1<<8);	// nrst = 0
	delayms(100);
	LPC_GPIO0->DATA	|= 1<<8;	// nrst = 1
	LPC_GPIO0->DATA	|= 1<<2;	// ncs = 1
	delayms(100);
	// Возврат спецфункции
	LPC_IOCON->PIO0_2	|= (1<<0);	// использовать как вывод SSEL
}

В этих функциях по сути собран весь аппаратно-зависимый код. именно по этой причине функция LCD_reset включена сюда, хотя правильнее её описать в следующем разделе. Я использовал "фоновый вывод данных", так функция SPI_send сначала ожидает окончания вывода предидущего байта (9 бит), затем помещает очередной байт (9 бит) на вывод и возвращается не ожидая завершения операции вывода.

Важно: Линию выбора дисплея CS обязательно надо периодически освобождать, иначе вывода никакого не будет, получите просто белый экран. У меня используется полностью аппаратный контроль.

Инициализация дисплея

Идем по пути найменьшего сопротивления, и одалживаем у тов. Rossum'а код для работы с дисплеями Nokia из проекта NokiaSuperBreakout. Не беда что нашего дисплея нет в перечислении, берём тот что соответствует диспею 132х160 контроллер SPFD54124B.

Данный код настраивает дисплей на 16-битный индексный режим вывода, в итоге получаем формат BGR 5-6-5. по биту глубины на R и B мы теряем, но зато получаем возможность передавать только 2 бата на один пиксель, взамен 3-х в 18битном режиме.

Перед инициализацией надо не забыть сбросить дисплей (иногда критично).


const uint16_t init_lcd1616ph[] = {
		0xBA, 0x107, 0x115,	// Data Order
		0x25, 0x13F,		// Contrast
		0x11,				// Sleep Out
		0x13,				// Display Normal mode

		0x37,0x100,			// VSCROLL ADDR
		0x3A,0x105,			// COLMOD pixel format 4=12,5=16,6=18
		0x29,				// DISPON
		0x20,				// INVOFF
		0x13				// NORON
};

void LCD_init()
{
	const uint16_t *data = &init_lcd1616ph[0];
	uint16_t size = sizeof(init_lcd1616ph)/sizeof(init_lcd1616ph[0]);
	while(size--) {
		LCD_send(*data++);
	}
	LCD_command(0x2D);
	int i;
	for (i = 0; i < 32; i++)
		LCD_data(i<<1);
	for (i = 0; i < 64; i++)
		LCD_data(i);
	for (i = 0; i < 32; i++)
		LCD_data(i<<1);
	delay(100);
	m_lcdResetClip();
	EndDraw();
	SetFont(0);
}

Про m_lcdResetClip, EndDraw и SetFont потом.

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

Вывод графики

Принцип вывода у всех встреченных мной дисплеев один. Вначале передается команда выбора диапазона строк, затем диапазона столбцов, после чего попиксельно выводятся пиксели в получившееся окно. Таким образом для вывода изображения в области [ x1:y1, x2:y2 ] надо передать следующую последовательность:
[PASET] [y1] [y2] [CASET] [x1] [x2] [RAMWR] [pixel_1] [pixel_2] ... [pixel_N]

Однако, в отличии от 6100 дисплей для 1616 каждая из величин координат (x1, x1, y1, y2) имеет 2-хбайтовый размер, не смотря на то, что старший байт всегда нулевой. Контроллер дисплея сам будет переносить "курсор" вывода в окне на следующую строку вывода, при достижении правой границ окна. Направление заполнения слева-направо, сверху вниз. Удобно. Ну и немаловажный фактор: левый верхний угол имеет координату x:y = 2:1.

Таким образом получаем что на операцию вывода требуется передать 11 служебных байт плюс 2хN байт данных. Естественно получаем что выгоднее выводить прямогольными областями по несколько пикселей за раз.

Ну а теперь к ложке дегтя. К сожалению при передаче координат "вне экрана" предсказать поведение дисплея нельзя. Он вроде как и "закольцован" на размер в 256 байт, но артефакты попадаются порой очень странные. По этому у вас есть выбор: либо не передавать координаты вне экрана, либо делать програмное отсечение. Я выбрал второй вариант и вот что из этого получилось:


// Nokia1616
#define displayOffsetX	2
#define displayOffsetY	1
#define displayWidth	128
#define displayHeight	160

#define	RGB(r, g, b)	(((uint32_t)(r))|((uint32_t)(g)<<8)|((uint32_t)(b)<<16))
#define RECT_set(rect, l, t, r, b) { (rect).left = (l); (rect).top = (t); (rect).right = (r); (rect).bottom = (b); }

// Описание переменных
RECT	m_lcdBound;		// Установленная для вывода область
POINT	m_lcdOutput;		// Очередная позиция для вывода
RECT	m_lcdClip;		// Размеры области отсечения
uint8_t	m_lcdClipOutput;	// Активны ли отсечения в текущей итерации вывода

uint32_t BeginDraw(int16_t left, int16_t top, uint16_t width, uint16_t height)
{
	int16_t right = left + width - 1;
	int16_t bottom = top + height - 1;
	if( left >= m_lcdClip.left && top >= m_lcdClip.top && right <= m_lcdClip.right && bottom <= m_lcdClip.bottom ) {	// RectInRect
		// область вывода полностью видима, используем "быстрый" вывод без проверок отсечения
		m_lcdClipOutput = 0;
	} else {
		// область вывода частично либо полностью невидима на дисплее, используем вывод с проверкой отсечений
		m_lcdClipOutput = 1;
		m_lcdOutput.x = left;
		m_lcdOutput.y = top;
		RECT_set(m_lcdBound, left, top, right, bottom);
		if(left < m_lcdClip.left) left = m_lcdClip.left;	// ClipRect
		if(top < m_lcdClip.top) top = m_lcdClip.top;
		if(right > m_lcdClip.right) right = m_lcdClip.right;
		if(bottom > m_lcdClip.bottom) bottom = m_lcdClip.bottom;
	}
	if( left > right || top > bottom ) {	// IsRectValid
		// область не видна на дисплее, завершаем вывод
		EndDraw();
		return 0;
	}
	uint32_t count;
	// Устанавливаем "виртуальные" границы вывода
	LCD_command(0x2A);	//LCD_command(CASETP);
	count	 = right - left + 1;
	left	+= displayOffsetX;
	right	+= displayOffsetX;
	LCD_data(left>>8);
	LCD_data(left);
	LCD_data(right>>8);
	LCD_data(right);
	// Диапозон строк
	LCD_command(0x2B);	//LCD_command(PASETP);
	count	*= (bottom - top + 1);
	top		+= displayOffsetY;
	bottom	+= displayOffsetY;
	LCD_data(top>>8);
	LCD_data(top);
	LCD_data(bottom>>8);
	LCD_data(bottom);
	LCD_command(0x2C);	//LCD_command(RAMWR);
	// Возвращаем количество видимых пикселей
	return count;
}

void EndDraw()
{
	m_lcdClipOutput = 1;
	m_lcdBound.right = m_lcdBound.left - 1;	// SetRect
}

void NextPoint(uint32_t color)
{
	// Надо ли проверять отсечения при выводе
	if( m_lcdClipOutput ) {
		int16_t x = m_lcdOutput.x;
		int16_t y = m_lcdOutput.y;
		// учит.отсеч.
		//if(!m_lcdClip.width /* || !m_lcdClip.height */) return;
		if(m_lcdBound.right < m_lcdBound.left) { // IsRectValid вывод недоступен
			return;
		}
		// Смещение на следующую позицию вывода
		if( m_lcdOutput.x >= m_lcdBound.right ) {
			// Если в конце строки - переходим на следующую
			m_lcdOutput.x = m_lcdBound.left;
			if( m_lcdOutput.y >= m_lcdBound.bottom ) {
				// Если последняя точка, переходим в начало (или можно завершить вывод вызовом EndDraw)
				m_lcdOutput.y = m_lcdBound.top;
			} else {
				m_lcdOutput.y++;
			}
		} else {
			m_lcdOutput.x++;
		}
		if(!(x >= m_lcdClip.left && x <= m_lcdClip.right && y >= m_lcdClip.top && y <= m_lcdClip.bottom) ) {	// PtInRect
			return;
		}
	}
	// точка будет видимой
	uint8_t r = color;
	uint8_t g = color>>8;
	uint8_t b = color>>16;
	LCD_data((r&0xF8)|(g>>5));
	LCD_data(((g<<3)&0xE0)|(b>>3));
}

Данные функции являются "дисплей ориентированными" и могут быть изменены для работы с другим дисплеем (естественно не забыв и про функцию инифиализации).

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

В функции настройки отображения я прибавляю смещение начала координат, что позволяет мне не запоминать с каким именно дисплеем я работаю. С той же целью для цвета используется 32-битное значение, по 8 бит на компоненту, и 8 бит прозапас (канал прозрачности) и для выравнивания.

Функция вывода очередной точки NextPoint должна выполнить некоторые проверки и расчеты, тут нам позволит хорошо ускорить процесс использование фонового вывода на дисплей: пока по SPI передаются данные, мы проводим свои проверки для следующей точки.

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

Ну и для завершения картины средства для работы с отсечениями:


typedef struct _tagPOINT {
	int16_t x;
	int16_t y;
} POINT, *PPOINT;

typedef struct _tagRECT {
	int16_t left;
	int16_t top;
	int16_t right;
	int16_t bottom;
} RECT, *PRECT;

void m_lcdSetClip(const RECT *r)
{
	// Проверяем на видимость отсечения
	RECT_set(m_lcdClip, r->left, r->top, r->right, r->bottom);
	// Устанавливаем "абсолютное" ограничение для вывода c усечением до границ дисплея
	if(m_lcdClip.left	< 0) m_lcdClip.left = 0;	// ClipRect
	if(m_lcdClip.top	< 0) m_lcdClip.top = 0;
	if(m_lcdClip.right	>= displayWidth) m_lcdClip.right = displayWidth - 1;
	if(m_lcdClip.bottom	>= displayHeight) m_lcdClip.bottom = displayHeight - 1;
	// Is clip valid
	if(m_lcdClip.bottom < m_lcdClip.top || m_lcdClip.right < m_lcdClip.left) {	// IsRectValid
		// invalid clip region
		RECT_set(m_lcdClip, 0, 0, -1, -1);
	}
}

void m_lcdResetClip()
{
	RECT_set(m_lcdClip, 0, 0, displayWidth - 1, displayHeight - 1);
}

Примитивы

Самое простое пожалуй это закрасить область одним цветом.


void Fill(int16_t left, int16_t top, uint16_t width, uint16_t height, uint32_t color)
{
	uint32_t count;
	count = BeginDraw(left, top, width, height);	// Функция возвращает количество видивых пикселей в установленной области вывода.
	m_lcdClipOutput = 0;	// Принудительно меняем функцию вывода, выводим только необходимое количество пикселей
	while(count--) {
		NextPoint(color);
	}
	EndDraw();
}

void Clear(uint32_t color)
{
	Fill(0, 0, displayWidth, displayHeight, color);
}

void Pixel(int16_t x, int16_t y, uint32_t color)
{
	Fill(x, y, 1, 1, color);
}

А проверяется всё это дело так:


	SPI_init();
	LCD_reset();
	LCD_init();
	Clear(0x00000000);
	Fill(10, 20, 30, 40, 0x00FF00FF);

Файлы: usbdisplay_2011_07_27.zip

Следующая >