USB-polygon-17: Подключение экрана от Nokia 3410
Введение
К настоящему моменту описываемое в цикле статей устройство определяется операционными системами под видом флэшки с FAT32 на борту, якобы содержит некоторый набор файлов, поддерживает операции чтения и записи.
При этом центральный элемент устройства — AT90USB162 — имеет аппаратные возможности передачи информации по некоторым протоколам, например, SPI. Значит, есть возможность «записать» файл на устройство таким образом, что данные уйдут на передачу по выбранному порту.
В качестве приемника данных выбран экран от мобильного телефона Nokia 3410. Во-первых, потому что он есть. Во-вторых, потому что он должен принимать данные по SPI, так как укомплектован соответствующим контроллером. В-третьих, потому что мне интересно попробовать вывести произвольную информацию на ЖК-дисплей. В-четвертых, потому что мне хочется использовать ЖК-дисплей в некоей самоделке, а для этого надо отладить работу с ним.
Подключение экрана к устройству
Надо сказать, что сперва я пытался подключить экран от Nokia 1100, но получилось только зажечь светодиоды подсветки. Телефон был взят у друзей, сломанный напрочь, поэтому, возможно, его экран и в самом деле неработоспособен, а не я накосячил. По крайней мере, при попытках его подключения ни один пиксел даже не пискнул, а при некоторых ошибках с 3410 экран хотя бы местами помигивал полосочками.
Перед подключением порылся в интернетах в поисках распиновки и системы команд контроллера. И вот самое полезное, что нашел:
- Страничка pcd8544-based displays, на которой я узнал, что используемый дисплеем контроллер называется pcd8544, а значит, можно найти хоть какую-то документацию на него, типа datasheet в pdf. Кроме того, там приведена распиновка экрана, смонтированного в рамке и замечательно расписаны особенности подключения, правда, с упором на подключение к LPT-порту ПК.
- Страничка на сайте Д. Погребняка, на которой кроме описания подключения к контроллеру (и без того несложного) есть примеры кода для общения контроллера с экраном. Когда у меня с первого раза не получилось, я сверил свой код с приведенным на этой страничке и понял, что дело не в программе — код практически совпадал.
- Ныне недоступная страничка, найденная на РадиоЛоцмане, где я нашел подтверждение собственным практическим изысканиям, например, несоответствию числа точек по горизонтали и вертикали ранее найденным сайтам и описанию контроллера.
Стало быть, если повернуть рамку с экраном разъемом к себе, то слева направо контакты будут такие:
- VDD — питание, 2,7..3,3 В
- SCLK — синхронизация данных
- SI — вход данных (в datasheet — SDIN)
- D/C — данные/команда («1» – данные)
- CS — разрешение приема данных (выбор кристалла, в datasheet — SCE)
- GND — корпус, земля, общий
- Vout — контрастность, подключается к общему через конденсатор
- RES — сброс, активен нулем, для работы должен быть в «1»
Припаял к ним провода МГТФ. С имеющимся в наличии плоским шлейфом не сложилось: очень уж легкоплавкая изоляция у него, а паяльник у меня так себе. Получилось так:
Через отверстия в панели клавиатуры привязал жгут проводов нитками, и дополнительно (чтобы не шатались вправо-влево) подклеил синей (это важно!) изолентой к задней стороне экрана.
Информационные выводы подключил к порту B, так как на этом порту есть выводы, альтернативное назначение которых — аппаратный SPI: 2 контакт подсоединил к площадке разряда 1, 3 контакт — к площадке 2, 5 контакт — к площадке 0, то есть SCLK, MOSI и SS SPI-порта соответственно (если, конечно, активировать SPI-интерфейс). 4 контакт (D/C) прицепил к площадке 5, 8 контакт (сброс) — к площадке 4.
6 контакт (GND) пошел напрямую на корпус, который на устройстве занимает практически всю поверхность той стороны, где светодиоды.
7 контакт (Vout) посадил на корпус через электролит, какой нашел. А нашел как раз подходящий: 4,7 мкФ, 6,3 В.
1 контакт — питание — поначалу подпаял к площадке «+3,3», но оказалось, что напрасно. В datasheet на микроконтроллер я не нашел допустимой нагрузки на выход встроенного стабилизатора 3,3 В. Там лишь сказано, что этот стабилизатор может использоваться для питания внешних устройств и даже самого контроллера:
The AT90USB82/162 product includes an internal 5V to 3.3V regulator that allows to supply the USB pad (see Figure 7-1.) and, depending on the application, external components or even the microcontroller itself
Опять же, в datasheet на pcd8544 указано, что его потребление меньше миллиампера, однако я нигде не нашел потребления экрана с контроллером в сборе.
В общем, в результате подключения таким образом я видел лишь отдельные подмигивания некоторыми пикселами и горизонтальными полосками пикселов на экране после подачи питания или нажатия кнопки сброса, несмотря на то, что код инициализации и вывода изображения на экран практически соответствовал коду, который, по заявлениям его авторов, вполне рабочий.
Предположив, что дело все-таки в питании, я стрельнул у товарища лишний стабилизатор LM317 (точнее, какую-то ненужную и поломанную платку под разбор) и запитал экран от площадки «+5» (она подключена прямо к выводу питания USB) через этот самый стабилизатор. Подключал с учетом цоколевки LM317:
по такой типовой схеме:
с небольшими изменениями. Верхний резистор R1 нашел почти нужный — 220 Ом, нижний R2 рассчитал (с учетом опорного напряжения 1,2 В — в соответствии с datasheet) и взял ближайший к расчетному из имеющихся — 332 Ома. Получил на выходе примерно 3,2 В. Емкость на выходе C2 поставил 4,7 мкФ, а по входу C1 нахально не стал ставить совсем, предполагая, что USB-питание и так вполне стабильное. То есть, когда (если) буду разводить плату для подключения экрана — обязательно поставлю, а пока обойдусь.
И теперь все более-менее заработало. А выглядит это так:
Инициализация экрана, вывод изображения
Теория
Основной источник информации о контроллере экрана и способах управления им — «DATA SHEET PCD8544 48 x 84 pixels matrix LCD controller/driver», хотя он немножко врет. В моем случае, для экрана от Nokia 3410, размер экрана (как выяснилось опытным путем и было подтверждено третьим источником) — 96 × 65 точек, а не 84 × 48.
Из описания PCD8544 ясно, что обмен с ним происходит по последовательному интерфейсу, практически идентичному SPI slave в режиме 0, только в одном направлении. Правда, в описании нигде не встречается буквосочетание «SPI». Ну и ладно, главное, что данные идут по 8 разрядов, старшим вперед, защелкиваются по фронту сигнала синхронизации, воспринимаются в случае, если сигнал выбора CS в нуле.
Если сигнал D/C в нуле, то переданный экрану байт интерпретируется, как команда, иначе — записывается в «видеопамять», в ОЗУ, из которого берутся данные для отображения на экране.
Набор команд
Команда | D/C | Данные, 8 разрядов: DB7 .. DB0 | Описание |
---|---|---|---|
NOP | 0 | 0 0 0 0 0 0 0 0 | нет операции |
Набор команд | 0 | 0 0 1 0 0 PD V H | управляет потреблением, адресацией и расширенным набором команд |
Запись данных | 1 | D7 D6 D5 D4 D3 D2 D1 D0 | записывает данные в память экрана |
H = 0 | |||
Резерв | 0 | 0 0 0 0 0 1 X X | не используется |
Управление экраном | 0 | 0 0 0 0 1 D 0 E | устанавливает конфигурацию экрана |
Резерв | 0 | 0 0 0 1 X X X X | не используется |
Установка Y-адреса | 0 | 0 1 0 0 0 Y2 Y1 Y0 | устанавливает позицию по Y |
Установка X-адреса | 0 | 1 X6 X5 X4 X3 X2 X1 X0 | устанавливает позицию по X |
H = 1 | |||
Резерв | 0 | 0 0 0 0 0 0 0 1 | не используется |
Резерв | 0 | 0 0 0 0 0 0 1 X | не используется |
Управление температурой | 0 | 0 0 0 0 0 1 TC1 TC0 | устанавливает температурный коэффициент (TCx) |
Резерв | 0 | 0 0 0 0 1 X X X | не используется |
Система смещения | 0 | 0 0 0 1 0 BS2 BS1 BS0 | устанавливает смещение (BSx) |
Резерв | 0 | 0 1 X X X X X X | не используется |
Установка Vop | 0 | 1 VOP6 VOP5 VOP4 VOP3 VOP2 VOP1 VOP0 | записывает Vop в регистр |
Описание используемых символов:
Биты PD, V и H:
Бит | 0 | 1 |
---|---|---|
PD | кристалл активен | кристалл в режиме пониженного потребления |
V | горизонтальная адресация | вертикальная адресация |
H | использовать основной набор команд | использовать расширенный набор команд |
Подробнее об адресации и наборах команд чуть ниже.
Биты D и E:
Значение | Описание |
---|---|
00 | экран пуст |
10 | нормальный режим |
01 | все сегменты экрана включены |
11 | инверсный видеорежим |
Режимы «00» и «01» действительно очищают экран или зажигают все пикселы экрана. Вот только проделывают это данные режимы с самим экраном, а не с видеопамятью. То есть, после возвращения в нормальный или инверсный режимы мы получим то же изображение, что и до входа в режимы выключения/включения. Поэтому использовать режим «00» для стирания экрана не получится. Чтобы стереть изображение, нужно сформировать картинку в видеопамяти, содержащую одни нули, то есть, пройти по всем адресам (можно с автоматическим приращением адреса) и записать нули. Инверсный режим, как нетрудно догадаться или проверить, выводит данные белым по черному.
Биты TC1 и TC0:
Значение | Описание |
---|---|
00 | VLCD температурный коэффициент 0 |
01 | VLCD температурный коэффициент 1 |
10 | VLCD температурный коэффициент 2 |
11 | VLCD температурный коэффициент 3 |
Адресация и набор команд
Экран имеет размер 96 точек по горизонтали и 65 по вертикали. Этот массив разделен на банки — горизонтальные полосы по 8 точек высотой.
Строго говоря, на банки разделено ОЗУ экрана, причем, каждый разряд каждой ячейки памяти соответствует некоторому пикселу на экране. Поэтому, говоря о банках памяти в данном случае не будет большой ошибкой сказать и о банках экрана.
Таких банков, как несложно подсчитать, 8,125. Конечно, дробного числа банков быть не может. Банков 9, но самый нижний банк имеет не 8 полосок, а только одну.
Таким образом, за один раз на экран можно вывести 8 точек — ни больше, ни меньше, в виде вертикальной полоски. Чтобы изменить состояние одной точки, нужно знать состояние остальных точек в данной горизонтальной позиции данного вертикального банка, чтобы вывести их заново в прежнем виде.
Младший разряд байта данных отвечает за самую верхнюю точку вертикальной полоски, старший — за нижнюю. В девятом банке можно пользоваться только младшим битом: остальных пикселов там нет.
Стало быть, нужно сперва в режиме команд задать номер банка и номер позиции в нем (требуется две команды), затем вывести байт в режиме данных. Но для вывода порции байтов не нужно задавать адрес каждый раз, одна из координат будет увеличиваться автоматически в зависимости от режима адресации.
Команда, которая задает набор команд — расширенный или обычный — также задает режим энергопотребления и режим адресации. Режим пониженного потребления пока неинтересен. Режим адресации определяет, какая координата будет наращиваться автоматически при вводе новых данных. При режиме горизонтальной адресации следующий байт будет выводиться в этом же горизонтальном банке на одну позицию правее. При достижении последней позиции в банке байт будет выведен в первую позицию следующего банка. Этот вариант мне кажется удобнее вертикальной адресации, следовательно, команда включения расширенного набора команд будет 0x21
, а обычного — 0x20
.
В обычном режиме можно задать позицию выводимых на экран данных и режим экрана, в расширенном — прочие параметры.
Инициализация
Datasheet предупреждает, что неправильный сброс может повредить контроллер, а импульс сброса «Reset» нужно использовать обязательно. В результате обнуляются все внутренние регистры, кроме видеопамяти.
С параметрами импульса Reset в документе как-то напутано. Обозначения в таблице на странице 20 в строчках, ссылающихся на рисунок 16, не стыкуются с этим самым рисунком 16. Но, насколько я понял, при включении Reset может быть в единице (но может и сразу в нуле), максимум через 30 мс должен опуститься в ноль на минимум 100 нс.
После сброса контроллер экрана находится в режиме пониженного потребления, с горизонтальной адресацией, адрес выдаваемых байтов — верхний левый угол (банк 0, Х тоже 0), в общем, все по нулям, кроме бита PD.
Из-за зависимости вязкости жидких кристаллов от температуры управляющее напряжение VLCD должно увеличиваться при низких температурах для достижения оптимального контраста. Это делается при помощи битов TC1 и TC0. Прямого указания в документе не нашел, но похоже, в контроллере есть термодатчик, который устанавливает напряжение в зависимости от заданного VLCD, температурного коэффициента и температуры. Предположение о наличии термодатчика косвенно подтверждается наличием такого в экране от Nokia 1100, где его показания даже можно прочитать (там SPI двунаправленный).
Однако заморачиваться с температурным коэффициентом я не хочу: это может быть важно при использовании экрана при разных температурах, а я из комнаты выносить не собираюсь, хватит и нулевого значения.
Теперь о системе смещения. Ячейки ЖК-экрана должны управляться переменным напряжением, постоянное может их испортить. Для этого в матричной структуре ячеек на каждую подведены, скажем так, провода рядов и колонок, напряжение на которых меняется от кадра к кадру. Значения напряжений выбираются из определенного набора — шесть штук с определенным соотношением между ними:
Обозначение | Напряжение смещения |
---|---|
V1 | VLCD |
V2 | (n + 3) / (n + 4) |
V3 | (n + 2) / (n + 4) |
V4 | 2 / (n + 4) |
V5 | 1 / (n + 4) |
V6 | VSS |
Здесь n выбирается исходя из скорости переключения:
BS1 | BS2 | BS3 | n | Рекомендуемая скорость переключения |
---|---|---|---|---|
0 | 0 | 0 | 7 | 1 : 100 |
0 | 0 | 1 | 6 | 1 : 80 |
0 | 1 | 0 | 5 | 1 : 65 |
0 | 1 | 1 | 4 | 1 : 48 |
1 | 0 | 0 | 3 | 1 : 40 / 1 : 34 |
1 | 0 | 1 | 2 | 1 : 24 |
1 | 1 | 0 | 1 | 1 : 18 / 1 : 16 |
1 | 1 | 1 | 0 | 1 : 10 / 1 : 9 / 1 : 8 |
В datasheet есть и формула пересчета n исходя из скорости переключения, но она вполне соответствует данной таблице. А скорость переключения в datasheet указана 1 : 48, и при этом рядов пикселов тоже 48. Что-то мне подсказывает, что это неспроста. С учетом того, что у Nokia 3410 65 рядов, сильно подозреваю, что и mux rate тоже должно быть 1 : 65, тогда n = 5, а не 4, как рекомендовано в документе. Тогда команда для установки напряжения смещения будет 0x12
.
Раздел об установке значения VOP вообще ввел в легкий ступор обилием параметров, которые неизвестно где брать.
Там рассказывается, что действующее напряжение VLCD устанавливается программно в зависимости от выбранного материала жидких кристаллов. Устанавливается при помощи VOP по формуле
VLCD = a + (VOP · b),
где a = 3,06; b = 0,06. Допустим, но какое следует выставить VOP? Далее приведена формула для скорости переключения 1:48 (формула 2 в разделе 8.9), в результате которой получается, что
VLCD = 6,06 · Vth,
где Vth — пороговое напряжение используемого материала жидких кристаллов. Где его взять — неизвестно. Тогда я предположил, что в datasheet данные как-то между собой связаны, а в используемом ниже примере VOP установлено в значение «16». Тогда VLCD получается равным 4,02. Отсюда Vth получилось 0,(6633). Предполагая, что материалы в ЖК-экранах, где используется этот контроллер, сильно не отличаются (то есть, это пороговое напряжение можно использовать), а также предполагая скорость переключения Nokia 3410 равным 1:65, я пересчитываю VOP и получаю 28,8. Округляю до 29. Тогда команду установки VOP можно записать как 0x80 | 29
.
Забегая вперед отмечу, что попробовал свои расчетные параметры, параметры из примера в datasheet и параметры с упомянутой выше странички на сайте Д. Погребняка, но принципиальной разницы не заметил. По крайней мере, в комнатных условиях. Возможно, при перепаде температур и попытке высокоскоростной (насколько это возможно) смены кадров какие-то отличия могут проявиться.
Практика
Была у меня совсем неудачная попытка подключить экран от Nokia 1100, и остались от этой попытки кое-какие наработки. Тот экран отличался 9-битным SPI, то есть, признак D/C передавался не отдельным проводом, а старшим разрядом в посылке данных. Таким образом, было 2 варианта вывода данных: или реализовать SPI полностью программно, или отключить аппаратный SPI, сформировать старший бит и моргнуть синхроимпульсом, включить аппаратный SPI и передать оставшиеся 8 битов. Для начала решил попробовать чисто программное решение, от которого осталась функция out9bit(uint8_t DC, uint8_t data8)
. Из нее я сделал такую:
void out8bit(uint8_t data8) {
PORTB &= ~(1 << 0); // cs -> 0
for (uint8_t i = 0; i < 8; i++){
PORTB &= ~(1 << 1); // sclk -> 0
if (data8 & 0x80) {
PORTB |= (1 << 2); // data -> 1
} else {
PORTB &= ~(1 << 2); // data -> 0
}
data8 <<= 1;
PORTB |= (1 << 1); // sclk -> 1
}
PORTB |= (1 << 0); // cs -> 1
}
То есть, включаю CS нулем, опускаю SCLK в ноль, поднимаю или опускаю разряд данных в зависимости от старшего разряда передаваемых данных, сдвигаю данные влево для подготовки передачи следующего бита и поднимаю SCLK. Операции от падения до подъема SCLK повторяю, пока не передам байт, потом поднимаю CS.
Еще добавил функцию очистки экрана:
void scrClear(void)
{
PORTB &= ~(1 << 5); // d/c -> 0
out8bit(0x40); // Y = 0
out8bit(0x80); // X = 0
/* 96 * 65 ? 768 -> 864 */
PORTB |= (1 << 5); // d/c -> 1
for (uint16_t i = 0; i < (96 * 9); i++) out8bit(0x00);
PORTB &= ~(1 << 5); // d/c -> 0
}
Сперва здесь включается режим команд и задаются начальные координаты, потом включается режим данных и выводятся нулевые байты по всей поверхности экрана.
Пояснение по цифрам: экран имеет размер 96х65. Это значит, что по оси x 96 позиций, а по оси y 8 позиций по байту и еще одна позиция с одним пикселом, младшим в байте, итого — 9 позиций, стало быть, для очистки надо записать 96 × 9 = 864 нуля.
Затем добавил в текст программы после инициализации переменных в функции main()
перед SetupHardware()
сброс экрана:
PORTB &= ~(1 << 4);
_delay_ms(10);
PORTB |= (1 << 4);
Первая строчка вообще-то не нужна, порт и так инициализирован нулями, но… Опять же, задержка реализована плохонькой библиотечной функцией, надо бы это не так сделать. И указание номеров разрядов надо бы сделать константами, а не цифрами в коде. Но это попозже переделаю, пока проверить работоспособность надо.
Дальше инициализация экрана:
// screen init
PORTB &= ~(1 << 5); // d/c -> 0
out8bit(0x21); // расширенный режим команд
out8bit(0x12); // установка напряжения смещения
out8bit(0x80 | 29); // установка напряжения LCD
out8bit(0x20); // обычный режим команд
out8bit(0x0c); // нормальный режим отображения
scrClear();
Включаю режим команды, потом выдаю серию этих самых команд: включаю расширенный набор команд (0х21), задаю напряжение смещения (0х80 + 56), задаю режим температурной коррекции (0х04), устанавливаю схему смещения напряжения (0х13), возвращаю стандартный набор команд (0х20), и включаю вывод графической информации на дисплей (0х0с). Затем чищу экран, потому что иначе он будет покрыт случайной россыпью точек.
Теперь вывожу линию во второй сверху полоске пикселов:
// line on the top
PORTB &= ~(1 << 5); // d/c -> 0
out8bit(0x40);
out8bit(0x80);
PORTB |= (1 << 5); // d/c -> 1
for (uint8_t i = 0; i < 96; i++) {
out8bit(0x02);
}
Переключаю в режим команд, устанавливаю координаты в левый верхний угол и вывожу 96 точек подряд, заодно проверяя реальную ширину экрана.
Затем вывожу еще одну полоску внизу экрана, вторую снизу, это старший разряд седьмого банка, а младший разряд восьмого банка будет самой нижней полоской.
// line on the bottom
PORTB &= ~(1 << 5); // d/c -> 0
out8bit(0x47);
out8bit(0x80);
PORTB |= (1 << 5); // d/c -> 1
for (uint8_t i = 0; i < 96; i++) {
out8bit(0x80);
}
Теперь примерно посередине экрана хочу нарисовать свой логотип. Для этого сначала на листочке в клеточку рисую его карандашом и прикидываю, сколько вертикальных восьмипиксельных линий это займет, и какие биты в байтах установить:
Затем устанавливаю позицию начала рисунка (предварительно включив режим команд). Полторы строчки — используются два банка — по оси y начнем с банка 3. 17 точек в ширину, а ширина экрана 96, значит по оси x начнем с позиции [(96 / 2) – (17 / 2)], округленно 40, или 0х28. Потом ставлю режим вывода данных и выдаю верхнюю строчку. Затем устанавливаю координаты второй строчки (смещая банк на единицу) и выдаю вторую строчку. Получается такой код:
// position set
PORTB &= ~(1 << 5); // d/c -> 0
out8bit(0x43);
out8bit(0x80 | 0x28);
// data out
PORTB |= (1 << 5); // d/c -> 1
out8bit(0x00);
out8bit(0x00);
out8bit(0x00);
out8bit(0x80);
out8bit(0x40);
out8bit(0x20);
out8bit(0x10);
out8bit(0x88);
out8bit(0x44);
out8bit(0x22);
out8bit(0x31);
out8bit(0xc3);
out8bit(0x0c);
out8bit(0x30);
out8bit(0xc0);
out8bit(0x00);
out8bit(0x00);
PORTB &= ~(1 << 5); // d/c -> 0
out8bit(0x44);
out8bit(0x80 | 0x28);
PORTB |= (1 << 5); // d/c -> 1
out8bit(0x04);
out8bit(0x06);
out8bit(0x05);
out8bit(0x04);
out8bit(0x04);
out8bit(0x06);
out8bit(0x05);
out8bit(0x04);
out8bit(0x05);
out8bit(0x06);
out8bit(0x05);
out8bit(0x04);
out8bit(0x07);
out8bit(0x04);
out8bit(0x04);
out8bit(0x07);
out8bit(0x04);
PORTB &= ~(1 << 5); // d/c -> 0
Программа с дописанным кодом компилируется и прошивается. Но неожиданно при запуске ничего на экране не происходит. Хоть через FLIP, хоть через dfu-programmer. Кнопка Reset тоже не помогает. Однако выручает перезапуск питания: выдергиваю разъем и вставляю снова, и вот что вижу на экране:
Доработка под аппаратный SPI
Экран завелся.
Честно говоря, заводился он не настолько уж сразу. Сперва помаялся с питанием экрана.
До сих пор остается загадкой, почему сброс после программирования не запускает картинку на экране.
Пока разбирался с отсутствием изображения, добавил задержку в формирование импульса SCLK, из-за чего стирание стало занимать секунд десять. Стирание тоже сделал не сразу и любовался картинкой на замусоренном поле.
Потом рассчитывал, сколько пустых байтов нужно записать для стирания, чтобы и экран почистить, и слишком много времени на это не тратить.
Потом я ошибался в количестве и порядке пикселов для картинки.
Пока искал ошибки, подозревал, что неправильно подключал заголовочные файлы, и именованные константы обнулены. Бред, конечно, компилятор бы предупредил, но… В общем, заменил кучу констант на хардкод, цифрами.
Но теперь работает.
Однако хотелось бы упростить обмен по SPI — зря, что ли, он реализован в контроллере аппаратно? Сразу переходить на аппаратный вариант немножко страшновато — не хочется ломать то, что работает, поэтому ввожу флажок включенности SPI, для чего объявляю переменную
uint8_t isSpiOn = 0;
выше объявления функции out8bit()
, а саму эту функцию изменяю таким образом:
void out8bit(uint8_t data8) {
if (isSpiOn == 1) {
PORTB &= ~(1 << 0); // cs -> 0
SPDR = data8;
while (!(SPSR & (1 << SPIF))) ; // wait for transmit
PORTB |= (1 << 0); // cs -> 1
} else {
PORTB &= ~(1 << 0); // cs -> 0
for (uint8_t i = 0; i < 8; i++){
PORTB &= ~(1 << 1); // sclk -> 0
if (data8 & 0x80) {
PORTB |= (1 << 2); // data -> 1
} else {
PORTB &= ~(1 << 2); // data -> 0
}
data8 <<= 1;
PORTB |= (1 << 1); // sclk -> 1
}
PORTB |= (1 << 0); // cs -> 1
}
}
При установленном флажке включенности SPI функция записывает байт в буфер SPI и ждет, когда он выдастся как-нибудь сам, а при снятом флажке работает, как раньше.
Теперь после объявления переменных в функции main()
надо включить SPI и установить флажок следующим образом:
/* Enable SPI, Master, set clock rate fck/2 (4 MHz) */
SPCR = (1 << SPE) | (1 << MSTR) | (0 << SPR1) | (0 << SPR0);
SPSR = (1 << SPI2X);
isSpiOn = 1;
Биты SPR0
, SPR1
, SPI2X
задают частоту обмена, равную половине тактовой, так как тактовая частота на используемом устройстве — 8 МГц, а максимально разрешенная для экрана — 4 МГц.
Больше ничего менять не надо. Компилируется, прошивается и работает без изменений. При желании можно поиграться со скоростями SPI, задавая их при инициализации
Передача файла экрану
Результат, конечно, радует, но ничего выдающегося в этом нет. Как нет и ничего специфичного для USB-polygon. Интересно добавить возможность отображения на экране некоторых данных, задаваемых с ПК, который при этом думает, что пересылает файлы на флэшку.
Передавать данные из файла на экран скопом, как есть, пожалуй, не получится. Надо разделить их на собственно данные и команды, раз уж так реализован вывод на экран. Иначе трудно будет сделать позиционирование при выводе очередного элемента. Кроме того, можно при помощи команд с ПК попросить устройство выполнить некоторую типовую операцию (например, стирание экрана), состоящую из нескольких команд, а не передавать эти несколько команд поштучно.
Я решил, что буду заполнять файл парами байтов. Младший в паре будет командой, старший — данными. Скажем, команда 1
соответствует требованию выставить режим команд для передачи байтов, команда 2
— выставить режим данных для передачи байтов, команда 3
— прекратить обработку файла и дальнейшие данные игнорировать (включая и непосредственно следующий байт), команда 4
— выполнить операцию очистки экрана, при этом следующий байт игнорируется. Остальные команды (включая 0
) — ничего не делать, использовать для байта данных предыдущую команду. То есть, можно выдать серию байтов для отображения, а команду 2
выставить только перед первым из них, остальные предварять нулевым байтом-командой.
Оставляем файл MassStorage.c
, тут уже все сделано. Открываем Lib/fake_fs.c
. В нем расширим тип-перечисление операций при записи:
enum WriteType {None, ToFile, ToLed, ToSpi};
Еще объявляю переменную, разрешающую пересылку по SPI данных, полученных из «записываемого» файла:
uint8_t canSpiFromFile = 0;
Это нужно для ограничения данных для передачи: файл будет передаваться кусками, кратными сектору, а данных для передачи может быть иное количество. Поэтому в конце файла надо поставить метку-команду, запрещающую передачу, чтобы остаток файла — хвост — не передавался, когда очередные 16-байтовые порции хвоста придут в устройство.
Затем я решил не плодить лишние сущности (кроме типа записи, в которую я добавил новый тип, а мог тоже заменить): то, что связано с выводом байтов файла на светодиодную линейку, использовалось только для демонстрации и отладки, больше использоваться не будет, поэтому заменяю «LED» на «SPI», а именно:
вместо
static const char strToLED[] PROGMEM = "Write to LED ";
пишу
static const char strToSpi[] PROGMEM = "Write to SPI ";
Вместо
uint8_t * readToLed(uint8_t *data_buf, uint32_t size, uint32_t offset);
записываю
uint8_t * readToSpi(uint8_t *data_buf, uint32_t size, uint32_t offset);
И в таблице файлов строку
{"TO_LED TXT", SIZE_OF_COMMAND, readToLed},
заменяю на
{"TO_SPI TXT", SIZE_OF_COMMAND, readToSpi},
Далее вместо определения функции readToLed()
пишу несколько измененную
uint8_t * readToSpi(uint8_t *data_buf, uint32_t size, uint32_t offset) {
writeType = ToSpi;
memcpy_P(data_buf, (PGM_P)(&(strToSpi[0]) + offset), 16);
canSpiFromFile = 1;
return data_buf;
}
И, наконец, в функции process_data()
в переключателе switch
добавляю новый вариант:
case ToSpi:
if (BlockAddress >= FILES_AREA) {
for (uint8_t i = 0; i < 16; i++) {
if (!canSpiFromFile) {
break;
}
if ((i & 1) == 0) {
switch (data_buf[i]) {
case 1:
PORTB &= ~(1 << 5); // d/c -> 0
break;
case 2:
PORTB |= (1 << 5); // d/c -> 1
break;
case 3:
canSpiFromFile = 0;
break;
case 4:
scrClear();
break;
default:
;
}
} else {
if (canSpiFromFile) {
out8bit(data_buf[i]);
} else {
break;
}
}
}
}
break;
Небольшое отступление: казалось бы, if (canSpiFromFile) {
— перестраховка, ведь в самом начале for
есть условие if (!canSpiFromFile) {
, ан нет, так и задумано. Ведь в текущей паре байтов первый (командный) может оказаться равным «3», тогда надо запрет обработки байта данных применить немедленно.
Чтобы это заработало, надо еще в файл common.h
добавить строчки
extern void out8bit(uint8_t data8);
extern void scrClear(void);
В общем-то и все. Модифицированный код компилируется и прошивается, только результат работы пока не виден.
Формирую для пробы файл с данными в HEX-редакторе:
00000000: 04 00 01 43 01 98 02 3f 00 09 00 09 00 36 00 00 ...C...?.....6..
00000010: 00 1e 00 21 00 21 00 1e 00 00 00 3f 00 02 00 04 ...!.!.....?....
00000020: 00 08 00 04 00 02 00 3f 00 00 00 3f 00 25 00 25 .......?...?.%.%
00000030: 00 21 00 00 00 1e 00 21 00 21 00 1e 00 00 00 00 .!.....!.!......
00000040: 00 1e 00 21 00 29 00 1a 00 00 00 1e 00 21 00 21 ...!.).......!.!
00000050: 00 1e 00 00 00 3f 00 20 00 20 00 20 00 00 00 3f .....?. . . ...?
00000060: 00 05 00 05 00 01 03 00 00 .........
Первая пара байтов содержит команду 4
стирания экрана. Две следующие пары устанавливают координаты выдачи надписи. Потом идет пара байтов, первый их которых — команда 2
, выдавать данные на экран, а второй — первый байт для выдачи. Следующие пары байтов почти до конца содержат команду 0
и остальные данные, а в конце идет команда 3
(прекратить обработку), второй байт пары с нулевым значением и нулевой хвостовой байт просто так. Дальше, после тройки в командном байте, можно писать сколько угодно чего угодно. Называю получившийся файл «to_spi.bin».
Снова подключаю перепрошитое обновленной версией программы устройство к ПК. На экране ПК появляется окошко «Автозапуск», предлагающий открыть флэшку, а на экране, подключенном к устройству, уже знакомая картина:
Открываю «флэшку», вижу там файл TO_SPI.TXT
. Его открытие на чтение будет для устройства командой пересылать любую записываемую в дальнейшем информацию на экран. Открываю файл (блокнотом по умолчанию) и вижу надпись «Write to SPI ». Закрываю блокнот. Перетаскиваю файл «to_spi.bin» мышью в окно «флэшки», и изображение на экране устройства изменяется:
Замечательно! Подтверждена возможность передачи файла по линии связи, а заодно и использовать передаваемые в файле данные для управления самим устройством.
Заключение
Проверена работа измененного проекта в ОС Linux, а конкретно в Debian 8. Все работает точно так же, как и в Windows 7, кроме небольшой детали: чтобы начальная картинка сменилась надписью, после записи файла «to_spi.bin» нужно или подождать некоторое время, или дать команду sync
, потому что в Linux запись на флэшку по умолчанию идет не сразу, а через буфер, и данные сбрасываются на носитель в подходящий по мнению ОС момент или по команде.
Теперь стоит «причесать» код. Избавиться от жестко заданных чисел, переведя их в константы. Для этого в файл common.h
добавлю определения констант
// разряды порта B - SPI и управление экраном
#define BIT_DC (1 << 5)
#define BIT_RES (1 << 4)
#define BIT_SCLK (1 << 1)
#define BIT_MOSI (1 << 2)
#define BIT_SS (1 << 0)
// разряды порта С - кнопки
#define BT_1 (1 << 4)
#define BT_2 (1 << 5)
#define BT_3 (1 << 2)
#define BT_4 (1 << 6)
#define BT_5 (1 << 7)
И по тексту файлов MassStorage.c
и fake_fs.c
заменю PORTB &= ~(1 << 5);
на PORTB &= ~BIT_DC;
(аналогично остальные разряды порта С), а if ((bt_now & 0x30) == 0) {
на if ((bt_now & (BT_1 | BT_2)) == 0) {
(аналогично остальные кнопки).
Для интересу перед перекомпиляцией сохранил файлы .hex и .bin, после команды make
сравнил новые со старыми. Как и ожидалось, изменений нет.
Хорошо бы еще немного оптимизировать, удалить лишнее. И попробовать использовать устройство для связи с еще какими-нибудь штучками. На очереди SD-карта памяти (по SPI-интерфейсу, хочется попробовать совместить на одной шине дисплей и память) и энкодер, с которым все теоретически несложно, но практически еще не сделано.