Как стать автором
Обновить

Простенькие часики на MSP430

Время на прочтение 10 мин
Количество просмотров 71K
Начитавшись огромным количеством статей про Arduino/LaunchPad захотелось приобрести подобную игрушку. Выбор пал на MSP430, так как его цена намного более привлекательна для старта в мир микроконтроллеров.
После томительных 5 дней ожидания, волшебная коробочка оказалась в моих руках. Поиграв минут 10 со светодиодами, захотелось сделать что-нибудь более интересное… Например часики!



Под рукой оказался старенький Siemens A65, который стал донором для моего небольшого проекта. Вытаскиваем из него экранчик и думаем, как бы его подключить. После недолго гугления, я успешно попал на ветку форума РадиоКот, где обсуждались распиновки и инициализации экранов. Если кто сталкивался с задачей подключения экранчиков к микроконтроллеру то знает, что мало узнать схему подключения, так как в экране стоит контроллер, для общения с которым нужно знать команды. Например для включения экрана и отображения мусора из памяти, некоторым контроллерам нужно послать несколько десятков команд, а некоторым хватает и меньше 10. Так вот, зачастую даташиты на контроллеры не найти, и в таком случае помогает только считывание инициализации экрана во время его работы на телефоне. Но мне повезло, инициализацию и команды для моего экранчика (в моем случае LPH8731-3C c контроллером EPSON S1D15G14) не только разобрали, но и даже нашелся на него даташит.

И так, смотрим распиновку, припаиваем проводки и подключаем к микроконтроллеру.

Распиновка для LPH8731-3C

Распиновка для LPH8731-3C. (Взято с форума РадиоКот)
Где:
  • CS — Chip Select. Когда находится в состоянии Low, чип готов принимать информацию.
  • RESET — ножка для сброса контроллера. Сигналом сброса служит переход из High -> Low -> High (по спецификации контроллера минимальное время 5мс).
  • RS — Служит для определения типа передаваемых данных (в даташите и у меня обозначается как CD). Для отправки команды должен быть в состоянии Low, для передачи данных — High.
  • CLK — служит тактовым сигналом для передачи данных.
  • DAT — для передачи данных.
  • VDD — по спецификации от +1.6V до +3.6V.
  • GND — надеюсь вы сможете сами угадать?;)
  • LED_A — оба разъема для подачи питания на подсветку. Тут лучше давать напряжение через резистор (можно без него, но в моем случае один из светодиодов начинал перегреваться, от чего получался засвет на экране).
  • LED_K — это к GND.

Кстати, некоторые уже могли заметить, что тут для передачи данных используется SPI, так что CLK и DAT можно подключить к SPI пинам MSP430.


Заводим «шарманку»



Теперь надо разобраться, как же общаться с контроллером. Для контроллера экрана существует 2 типа принимаемых данных — команда или данные. Для выбора типа данных используется отдельный пин. В остальном же, процедура передачи данных одинакова.

Процедура передачи данных на контроллер, взятая из даташита. Тут почему-то не указано состояние пина RS/CD. Кстати, если во время передачи данных состояние CS изменится Low -> High, прием данных приостановится. А вот в конце передачи данных, дергать CS вверх не обязательно (но рекомендуется).

Немного злобного кода
Код приведен для CSS (TI's Code Composer Studio).
Тут он не полностью, а только кусками для примера. Комментарии на английском, так как мне больше нравится так:)

LPH87313C.h
/**********************************************************/
/*		Pins and outputs								  */
/**********************************************************/
// Chip Select line		pin1.0
#define LCD_CS BIT7
#define LCD_CS_DIR P1DIR
#define LCD_CS_OUT P1OUT

// Hardware Reset		pin1.1
#define LCD_RESET BIT6
#define LCD_RESET_DIR P1DIR
#define LCD_RESET_OUT P1OUT

// Command/Data mode line	pin1.4
#define LCD_CD BIT3
#define LCD_CD_DIR P1DIR
#define LCD_CD_OUT P1OUT

// SPI
#define SPI UCA0TXBUF


LPH87313C.с
void LCD_SendCmd(unsigned char Cmd)
{
	LCD_CS_OUT |= LCD_CS; // set CS pin to High
	LCD_CD_OUT &= ~LCD_CD; // set CD pin to Low
	LCD_CS_OUT &= ~LCD_CS;

	SPI = Cmd;
}

void LCD_SendDat(unsigned char Data)
{
	LCD_CD_OUT |= LCD_CD; // set CD pin to High

	SPI = Data;
}



Теперь мы знаем, как отправить данные на контроллер (ну или хотя бы имеем представление). К счастью в даташите не только описаны все команды, но и имеется даже пример первоначальной инициализации экрана. В целом ее можно разделить на 3 этапа: делаем ресет контроллера (hardware & software reset), задаем первоначальную настройку параметров, включаем дисплей.

А тут много отправляемых команд
В принципе большинство параметров задаются как есть, но некоторые вещи можно менять. Например то, в каком порядке будут записываться значения в память (сверху вниз — справа налево/ снизу вверх — справа налево/ и тд), контраст, а так же глубина цвета (256 цветов или 4096).

LPH87313C.с
void LCD_Init()
{
	// Set pins to output direction
	LCD_CS_DIR |= LCD_CS;
	LCD_RESET_DIR |= LCD_RESET;
	LCD_CD_DIR |= LCD_CD;

	LCD_CS_OUT &= ~LCD_CS;
	LCD_RESET_OUT &= ~LCD_RESET;
	LCD_CD_OUT &= ~LCD_CD;

	__delay_cycles(160000); //wait 100ms (F_CPU 16MHz)
	LCD_RESET_OUT |= LCD_RESET;
	__delay_cycles(160000);

	LCD_SendCmd(0x01); //reset sw
	__delay_cycles(80000);

	LCD_SendCmd(0xc6); //initial escape
	LCD_SendCmd(0xb9); //Refresh set
	LCD_SendDat(0x00);

	__delay_cycles(160000);

	LCD_SendCmd(0xb6); //Display control
	LCD_SendDat(0x80); //
	LCD_SendDat(0x04); //
	LCD_SendDat(0x0a); //
	LCD_SendDat(0x54); //
	LCD_SendDat(0x45); //
	LCD_SendDat(0x52); //
	LCD_SendDat(0x43); //

	LCD_SendCmd(0xb3); //Gray scale position set 0
	LCD_SendDat(0x02); //
	LCD_SendDat(0x0a); //
	LCD_SendDat(0x15); //
	LCD_SendDat(0x1f); //
	LCD_SendDat(0x28); //
	LCD_SendDat(0x30); //
	LCD_SendDat(0x37); //
	LCD_SendDat(0x3f); //
	LCD_SendDat(0x47); //
	LCD_SendDat(0x4c); //
	LCD_SendDat(0x54); //
	LCD_SendDat(0x65); //
	LCD_SendDat(0x75); //
	LCD_SendDat(0x80); //
	LCD_SendDat(0x85); //

	LCD_SendCmd(0xb5); //Gamma curve
	LCD_SendDat(0x01); //

	LCD_SendCmd(0xbd); //Common driver output select
	LCD_SendDat(0x00); //
	LCD_SendCmd(0xbe); //Power control
	LCD_SendDat(0x54); //0x58 before
	LCD_SendCmd(0x11); //sleep out
	__delay_cycles(800000);
	LCD_SendCmd(0xba); //Voltage control
	LCD_SendDat(0x2f); //
	LCD_SendDat(0x03); //

	LCD_SendCmd(0x25); //Write contrast
	LCD_SendDat(0x60); //

	LCD_SendCmd(0xb7); //Temperature gradient
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x00); //

	LCD_SendCmd(0x03); //Booster voltage ON
	__delay_cycles(800000);
	LCD_SendCmd(0x36); //Memory access control
	LCD_SendDat(0x48); //

	LCD_SendCmd(0x2d); //Color set
	LCD_SendDat(0x00); //
	LCD_SendDat(0x03); //
	LCD_SendDat(0x05); //
	LCD_SendDat(0x07); //
	LCD_SendDat(0x09); //
	LCD_SendDat(0x0b); //
	LCD_SendDat(0x0d); //
	LCD_SendDat(0x0f); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x03); //
	LCD_SendDat(0x05); //
	LCD_SendDat(0x07); //
	LCD_SendDat(0x09); //
	LCD_SendDat(0x0b); //
	LCD_SendDat(0x0d); //
	LCD_SendDat(0x0f); //
	LCD_SendDat(0x00); //
	LCD_SendDat(0x05); //
	LCD_SendDat(0x0b); //
	LCD_SendDat(0x0f); //

	LCD_SendCmd(0x3a); //interface pixel format
	LCD_SendDat(0x03); // 0x02 for 8-bit 0x03 for 12bit
	__delay_cycles(1600000);

	LCD_SendCmd(0x29); //Display ON
}



Наш адрес не дом и не улица, наш адрес такой...



Включив дисплей, мы увидим либо мусор, либо белый/черный экран. Все потому, что контроллер изменяет состояние матрицы относительно внутренней памяти, и включив его, он отобразит все, что он «помнит». Для отображения какой-либо информации (либо ее изменения) достаточно изменить память и контроллер обновит дисплей (обновляет он дисплей постоянно с определенной частотой, задаваемой в первоначальной настройке, по умолчанию частота обновления 85Hz). Например, для смены цвета пикселя нужно просто записать новое значение в память. Но для начала нужно задать адрес, куда записывать новое значение. Если в компьютере просто задается адрес ячейки памяти и записывается новое значение, то тут надо указать диапазон памяти, в который можно последовательно отправить данные.

Например, для заливки всего экрана нужно выбрать начало записываемой области (x0, y0) и конец (x101, y80). А если нужно поменять цвет только одного пиксела, то соответственно задаем область [x, y][x+1, y+1].

Выбрав область, мы можем теперь просто отправлять данные и они последовательно будут записываться в память (а как именно (слева на право, сверху вниз или наоборот) будет зависеть от первоначальной настройки). Например выбрав область 40х40px, нам нужно будет отправить последовательно 1600 значений (правда это не совсем так, но об этом по порядку), которые будут внесены в память и эта область будет полностью обновлена. А если продолжить отправлять значения, то обновление продолжится со следующего пиксела (в данном случае с первого).

Команды для задания области
LCD_SendCmd(0x2A); //задаем область по X (x0 - начальный, x1 - конечный)
LCD_SendDat(x0);
LCD_SendDat(x1);

LCD_SendCmd(0x2B); //задаем область по Y (y0 - начальный, y1 - конечный)
LCD_SendDat(y0+1); //у этого контроллера Y отсчитывается от 1, а не 0
LCD_SendDat(y1+1);

LCD_SendCmd(0x2C); // отправляем команду на начало записи в память и начинаем посылать данные



Вам письмо! Правда на Китайском...



Мы уже разобрались как включить дисплей и даже как выбрать область для рисования, но как цвет то отправлять? Дисплей может работать с 2 цветовыми палитрами:
  • 8bit (256 цветов)
  • 12bit (4096 цветов)

В случае 8 битного цвета все просто — достаточно отправлять 8 бит на каждый цвет (а именно R2R1R0G2G1G0B1B0, где R2R1R0 — 3 бита красного цвета и тд. На красный и зеленый по 3 бита, а на синий 2 бита).
А вот в случае 12 битного цвета все немого сложнее. Тут уже для каждого оттенка дается по 4 бита. Приведу картинку из даташита.

Как видите, для отправки одного цвета используется полтора байта. Если надо изменить только 1 пиксел, то отправляется 2 байта информации, где во втором байте D3-D0 не будут использоваться. А если надо изменить 2 пиксела, то достаточно отправить 3 байта (где D3-D0 второго байта будут началом, а D7-D0 третьего байта продолжением цвета для второго пиксела ).

Опять порция кода
Пример функции заливки всего экрана.
void LCD_Flush(unsigned char R, unsigned char G, unsigned char B)
{
	volatile int i = 4040;

	volatile char B0, B1, B2;
	B0 = ((R << 4) & 0xF0) + (G & 0x0F);
	B1 = ((B << 4) & 0xF0) + (R & 0x0F);
	B2 = ((G << 4) & 0xF0) + (B & 0x0F);

	LCD_SendCmd(0x2A);
	LCD_SendDat(0);
	LCD_SendDat(100);

	LCD_SendCmd(0x2B);
	LCD_SendDat(1);
	LCD_SendDat(80);

	LCD_SendCmd(0x2C);

	while (i--)
	{
		LCD_CD_OUT |= LCD_CD;
		SPI = B0;
		SPI = B1;
		SPI = B2;
	}
}



А где обещанные часики?



А теперь самое сложное — нарисовать часики. Как вы могли заметить, они стилизованы под сегментный индикатор, так что для отображения часов достаточно нарисовать 2 типа сегментов (вертикальные и горизонтальные) в разных местах.
Для начала надо определиться с дизайном. Спасибо великой программе от MS — Paint, очень уж она мне помогла с этим;).

Вот что у меня получилось. Каждый сегмент размером 12х4px (а вертикальные соответственно наоборот — 4x12px).

А теперь вспомним про выбор области для рисования. Можно же задать область 12х4 в нужном месте и отрисовать сегмент, не перерисовывая весь экран. Если повнимательнее взглянуть на сегмент, можно заметить, что он почти полностью заливается одним цветом, за исключением углов. Так что алгоритм рисования сегмента довольно простой: начинаем заполнение памяти с пустого цвета (к сожалению тут нет прозрачности, так что заполняем цветом фона), добавляем проверки для верхнего правого и нижнего левого угла, и последний пиксел тоже заполняем цветом фона. Точно так же и для вертикальных. А уж как нарисовать точечки я даже не буду рассказывать:).

Набор матерных слов на непонятном языке
Пример функции для отображения горизонтального сегмента. greenBright — константа «яркого» цвета (для активного сегмента), greenDim — константа цвета для неактивного сегмента. BG0, BG1, BG2 — константы битов для зарисовки фона.
А если заметите __delay_cycles — это необъяснимая магия, без которой не работает (хотя скорее всего не успевает хардварный SPI отправлять данные, так как они отправляются не за один такт (но намного быстрее, в отличии если реализовать отправку своими силами)).
void drawHorizontal(char type, unsigned char x, unsigned char y)
{
	volatile unsigned char i = 22, B2, B1, B0;
	if (type)
	{
		B0 = greenBright;
		B1 = 0;
		B2 = (greenBright << 4) & 0xF0;
	} else {
		B0 = greenDim;
		B1 = 0;
		B2 = (greenDim << 4) & 0xF0;
	}

	LCD_SendCmd(0x2A);
	LCD_SendDat(x);
	LCD_SendDat(x+11);

	LCD_SendCmd(0x2B);
	LCD_SendDat(y+1);
	LCD_SendDat(y+4);

	LCD_SendCmd(0x2C);

	__delay_cycles(4);

	LCD_CD_OUT |= LCD_CD;
	SPI = BG0;
	SPI = (BG1 << 4) & 0xF0;
	__delay_cycles(2);
	SPI = B2;

	while(i--)
	{
		if (i == 17)
		{
			SPI = B0;
			__delay_cycles(2);
			SPI = (BG0 >> 4) & 0x0F;
			__delay_cycles(2);
			SPI = (BG0 << 4) & 0xF0 + (BG1 << 4) & 0x0F;
			continue;
		}
		if (i == 4)
		{
			SPI = BG0;
			SPI = (BG1 << 4) & 0xF0;
			__delay_cycles(2);
			SPI = B2;
			continue;
		}
		SPI = B0;
		SPI = B1;
		SPI = B2;

	}

	SPI = B0;
	__delay_cycles(2);
	SPI = (BG0 >> 4) & 0x0F;
	__delay_cycles(2);
	SPI = (BG0 << 4) & 0xF0 + (BG1 << 4) & 0x0F;
}



Теперь надо превратить цифру в набор сегментов (например для отображения 1 надо нарисовать только правые вертикальные сегменты). Я это решил довольно просто — создал массив значений сегментов для различных цифр (от 0 до 9). Подставляя в него цифру, я получаю массив со значениями 1/0, которые управляли отрисовкой сегментов. Например 1 означала, что сегмент нужно отрисовывать, а 0 — что не надо (или отрисовать его «неактивным»). А зная что и где надо рисовать, сделать функцию не составит труда.
Простенький массивчик
/********************************************************************************************
* 	Array for Clock
*  	   	   ____
* 		 _|__1_|_
* 		|6|    |2|
* 		|_|____|_|
* 		 _|__7_|_
* 		|5|    |3|
* 		|_|____|_|
* 		  |__4_|
*
********************************************************************************************/
static const char HH[10][7] = {
		{1,1,1,1,1,1,0}, // 0
		{0,1,1,0,0,0,0}, // 1
		{1,1,0,1,1,0,1}, // 2
		{1,1,1,1,0,0,1}, // 3
		{0,1,1,0,0,1,1}, // 4
		{1,0,1,1,0,1,1}, // 5
		{1,0,1,1,1,1,1}, // 6
		{1,1,1,0,0,0,0}, // 7
		{1,1,1,1,1,1,1}, // 8
		{1,1,1,1,0,1,1}  // 9
};

Код рисования часов
void drawClock(char hh, char mm, char dots)
{
	volatile char h0, h1, m0, m1;
	h0 = hh / 10;
	h1 = hh - (h0 * 10);
	m0 = mm / 10;
	m1 = mm - (m0 * 10);



	drawHorizontal(HH[h0][0], 9, 25);
	drawHorizontal(HH[h1][0], 31, 25);
	drawHorizontal(HH[m0][0], 58, 25);
	drawHorizontal(HH[m1][0], 80, 25);

	drawVertical(HH[h0][5], 6, 29);
	drawVertical(HH[h0][1], 20, 29);

	drawVertical(HH[h1][5], 28, 29);
	drawVertical(HH[h1][1], 42, 29);

	drawVertical(HH[m0][5], 55, 29);
	drawVertical(HH[m0][1], 69, 29);

	drawVertical(HH[m1][5], 77, 29);
	drawVertical(HH[m1][1], 91, 29);

	drawHorizontal(HH[h0][6], 9, 38);
	drawHorizontal(HH[h1][6], 31, 38);
	drawHorizontal(HH[m0][6], 58, 38);
	drawHorizontal(HH[m1][6], 80, 38);

	drawVertical(HH[h0][4], 6, 42);
	drawVertical(HH[h0][2], 20, 42);

	drawVertical(HH[h1][4], 28, 42);
	drawVertical(HH[h1][2], 42, 42);

	drawVertical(HH[m0][4], 55, 42);
	drawVertical(HH[m0][2], 69, 42);

	drawVertical(HH[m1][4], 77, 42);
	drawVertical(HH[m1][2], 91, 42);

	drawHorizontal(HH[h0][3], 9, 51);
	drawHorizontal(HH[h1][3], 31, 51);
	drawHorizontal(HH[m0][3], 58, 51);
	drawHorizontal(HH[m1][3], 80, 51);

	drawDots(dots);
}



И вот мы подошли к концу статьи. Надеюсь я смог максимально подробно объяснить принцип их работы:) И на закуску небольшое видео, как они работают и мигают «точками».




P.S.
Возможно в этой статье присутствуют орфографические, грамматические и пунктуационные ошибки. Если вы найдете их, убедительно прошу отправить мне сообщение в личку, а не писать комментарий.
В статье не написано про реализацию часов как таковых потому что они не доделаны:) Изначально планировалось сделать небольшой гаджет, где бы использовался дисплей, внешняя flash память и внешние часы-календарь, но так как я случайно купил не те часы, все встало:) Еще одна из причин — хотелось использовать дисплей побольше, но не смог купить подходящий, например от китайской Nokla n95 8gb. Может кто подскажет где такой купить?
Если кому-то понадобится исходный код — обращайтесь, могу поделиться:) Если у кого есть вопросы по реализации вывода символов на экран (печать текста) — могу так же поделиться исходным кодом (я не стал о нем писать, тут вроде статья про часы:), да и на отдельный пост тоже не тянет). Так же могу поделиться библиотекой для работы с экраном от Siemens CX75 (на контроллере SSD-1286, есть даже даташит), писал для себя, но случайно спалил его.
Теги:
Хабы:
+30
Комментарии 17
Комментарии Комментарии 17

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн