RomeoGolf

Чт 03 Май 2018

USB-polygon-17: Подключение экрана от Nokia 3410

Введение

К настоящему моменту описываемое в цикле статей устройство определяется операционными системами под видом флэшки с FAT32 на борту, якобы содержит некоторый набор файлов, поддерживает операции чтения и записи.

При этом центральный элемент устройства — AT90USB162 — имеет аппаратные возможности передачи информации по некоторым протоколам, например, SPI. Значит, есть возможность «записать» файл на устройство таким образом, что данные уйдут на передачу по выбранному порту.

В качестве приемника данных выбран экран от мобильного телефона Nokia 3410. Во-первых, потому что он есть. Во-вторых, потому что он должен принимать данные по SPI, так как укомплектован соответствующим контроллером. В-третьих, потому что мне интересно попробовать вывести произвольную информацию на ЖК-дисплей. В-четвертых, потому что мне хочется использовать ЖК-дисплей в некоей самоделке, а для этого надо отладить работу с ним.

Подключение экрана к устройству

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

Перед подключением порылся в интернетах в поисках распиновки и системы команд контроллера. И вот самое полезное, что нашел:

  1. Страничка pcd8544-based displays, на которой я узнал, что используемый дисплеем контроллер называется pcd8544, а значит, можно найти хоть какую-то документацию на него, типа datasheet в pdf. Кроме того, там приведена распиновка экрана, смонтированного в рамке и замечательно расписаны особенности подключения, правда, с упором на подключение к LPT-порту ПК.
  2. Страничка на сайте Д. Погребняка, на которой кроме описания подключения к контроллеру (и без того несложного) есть примеры кода для общения контроллера с экраном. Когда у меня с первого раза не получилось, я сверил свой код с приведенным на этой страничке и понял, что дело не в программе — код практически совпадал.
  3. Ныне недоступная страничка, найденная на РадиоЛоцмане, где я нашел подтверждение собственным практическим изысканиям, например, несоответствию числа точек по горизонтали и вертикали ранее найденным сайтам и описанию контроллера.

Стало быть, если повернуть рамку с экраном разъемом к себе, то слева направо контакты будут такие:

  1. VDD — питание, 2,7..3,3 В
  2. SCLK — синхронизация данных
  3. SI — вход данных (в datasheet — SDIN)
  4. D/C — данные/команда («1» – данные)
  5. CS — разрешение приема данных (выбор кристалла, в datasheet — SCE)
  6. GND — корпус, земля, общий
  7. Vout — контрастность, подключается к общему через конденсатор
  8. 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:

Цоколевка LM317

по такой типовой схеме:

Схема 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-интерфейсу, хочется попробовать совместить на одной шине дисплей и память) и энкодер, с которым все теоретически несложно, но практически еще не сделано.


Теги: