Начитавшись огромным количеством статей про Arduino/LaunchPad захотелось приобрести подобную игрушку. Выбор пал на MSP430, так как его цена намного более привлекательна для старта в мир микроконтроллеров.
После томительных 5 дней ожидания, волшебная коробочка оказалась в моих руках. Поиграв минут 10 со светодиодами, захотелось сделать что-нибудь более интересное… Например часики!
Под рукой оказался старенький Siemens A65, который стал донором для моего небольшого проекта. Вытаскиваем из него экранчик и думаем, как бы его подключить. После недолго гугления, я успешно попал на ветку форума РадиоКот, где обсуждались распиновки и инициализации экранов. Если кто сталкивался с задачей подключения экранчиков к микроконтроллеру то знает, что мало узнать схему подключения, так как в экране стоит контроллер, для общения с которым нужно знать команды. Например для включения экрана и отображения мусора из памяти, некоторым контроллерам нужно послать несколько десятков команд, а некоторым хватает и меньше 10. Так вот, зачастую даташиты на контроллеры не найти, и в таком случае помогает только считывание инициализации экрана во время его работы на телефоне. Но мне повезло, инициализацию и команды для моего экранчика (в моем случае LPH8731-3C c контроллером EPSON S1D15G14) не только разобрали, но и даже нашелся на него даташит.
И так, смотрим распиновку, припаиваем проводки и подключаем к микроконтроллеру.
Распиновка для 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 вверх не обязательно (но рекомендуется).
Тут он не полностью, а только кусками для примера. Комментарии на английском, так как мне больше нравится так:)
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), задаем первоначальную настройку параметров, включаем дисплей.
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 в нужном месте и отрисовать сегмент, не перерисовывая весь экран. Если повнимательнее взглянуть на сегмент, можно заметить, что он почти полностью заливается одним цветом, за исключением углов. Так что алгоритм рисования сегмента довольно простой: начинаем заполнение памяти с пустого цвета (к сожалению тут нет прозрачности, так что заполняем цветом фона), добавляем проверки для верхнего правого и нижнего левого угла, и последний пиксел тоже заполняем цветом фона. Точно так же и для вертикальных. А уж как нарисовать точечки я даже не буду рассказывать:).
А если заметите __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.
Возможно в этой статье присутствуют орфографические, грамматические и пунктуационные ошибки. Если вы найдете их, убедительно прошу отправить мне сообщение в личку, а не писать комментарий.
Если кому-то понадобится исходный код — обращайтесь, могу поделиться:) Если у кого есть вопросы по реализации вывода символов на экран (печать текста) — могу так же поделиться исходным кодом (я не стал о нем писать, тут вроде статья про часы:), да и на отдельный пост тоже не тянет). Так же могу поделиться библиотекой для работы с экраном от Siemens CX75 (на контроллере SSD-1286, есть даже даташит), писал для себя, но случайно спалил его.
Автор: karech
Здравствуйте.
Вот решил на старости заняться.
Помалу вникаю, но с трудом.
Если не сложно, скиньте исходный код и схему подключения.
Да и на Siemens CX75, что есть.
С уважением, Владимир