
РЕТРОКОМПЬЮТИНГ
Всё началось со смерти давнего друга. Но не волнуйтесь: этот друг теперь живее всех живых, хорошо себя чувствует и служит мне верой и правдой.
Это классический матричный принтер Epson MX-80 F/T III. Он был первым принтером, который я купил, где-то в 1982 или в 1983 году, и стоил он мне гораздо больше, чем я мог себе позволить. Даже после того, как он перестал быть моим основным принтером — сначала его заменило невероятно громкое лепестковое печатающее устройство, а потом лазерный принтер Epson, который я «приобрёл» на своей должности составителя обзоров продуктов — он продолжал служить мне в нишевых ситуациях.
Его основной задачей стала печать этикеток для моей библиотеки 35-миллиметровых цветных слайдов. У меня есть база данных, созданная на основе ПО Eagle компании Emerald Bay (это своего рода более удобная версия dBase), и мне было гораздо проще печатать этикетки программно на матричном принтере, чем возиться с листами этикеток на лазерном. Именно такой функциональности мне не хватает.

Несколько лет назад, поддавшись импульсу, я решил включить принтер и ещё раз попробовать пообщаться с ним из Linux. При включении я услышал громкий хлопок и треск, за которыми последовал едкий запах волшебного дыма.
Опытным любителям ретрокомпьютинга знакомы эти симптомы. Конденсатор RIFA, известный своей неспособностью сохранять влажность и ёмкость, решил умереть с почётом и спецэффектами.
Оказалось, отремонтировать принтер было довольно легко. Я смонтировал новый конденсатор RIFA (и заменил ещё несколько электролитических конденсаторов, которые показались мне немного раздувшимися), и принтер вернулся к жизни. Но почему-то это вызвало во мне ещё более сильное желание пользоваться этой сорокалетней машиной.

И мне это удалось, хоть пришлось и немного сжульничать. В этой статье я расскажу, как подошёл к созданию собственного интерфейса принтера. Однако это будет общее описание, я не буду вдаваться в технические подробности, и на то есть пара причин.
Во-первых, создаваемые мной проекты никогда не работают так, как мне хотелось. Вероятно, это связано с неопытностью и проблемами ПО. Возможно, теперь, набравшись опыта, я снова попробую взяться за код, но для меня это уже будет побочный квест.
Во-вторых, в конечном итоге я обнаружил почти готовое решение, на основании которого я написал ПО. Однако у него есть собственные проблемы, о которых я подробно расскажу в будущей статье.
И здесь мы плавно переходим к тайной третьей причине: эта статья будет прелюдией к новому проекту создания более функционального адаптера USB/serial-to-parallel, который меня очень радует. Мои первые попытки создания интерфейса позволили кое-чему научиться, и эти уроки я применю в новом проекте, поэтому посчитал, что стоит вернуться к ним в этой статье.
Любовь к матрице
Зачем я трачу столько времени на попытки найти применение устройству, которое старше родителей некоторых читателей?
Во-первых, я скучаю по созданию печатных копий текстов моих программ на матричном принтере. Есть что-то медитативное и приятное в ожидании того, когда принтер закончит жужжать и трещать, чтобы можно было приступить к чтению кода и поиску бага.
Чтение с бумаги отличается от чтения с экрана. Я журналист и редактор, и по-прежнему предпочитаю по возможности выполнять финальную вычитку в бумажном виде. Тебе бросаются в глаза некоторые недостатки, которые не замечаешь на экране. Кроме того, на бумаге можно чертить и делать рукописные заметки.
И давайте признаем: распечатка на длинном листе бумаги с перфорацией по бокам кажется настоящей работой с компьютером.
При кодинге я по-прежнему сохраняю привычку ограничивать длину строк 80 символами. В конце концов, это ещё и повышает их читаемость. Газетные и журнальные столбцы довольно узкие, потому что благодаря этому глазу приходится меньше перемещаться, а значит, и меньше утомляться. В моём VS Code настроено отображение линии в конце 80-го столбца. Я всегда подсознательно рассчитываю на то, что мне захочется распечатать код.
Вообще адаптеры USB-to-parallel можно купить — у меня есть такой. Но мне так и не удалось заставить работать эти чёртовы драйверы. А большинство драйверов, спроектированных для матричных принтеров, похоже, предполагают, что вы хотите работать с любым выводом, как с графическим. Честно говоря, в мире и так слишком много PostScript. Позже (гораздо позже) я узнал, как можно было бы всё это устроить, но тогда я уже закопался слишком глубоко.
Коммутация
Я начал эти проекты примерно семь лет назад. Моим первым шагом стало создание платы для 25-контактного разъёма D-sub, обычно используемого в параллельных интерфейсах. Я намеревался поэкспериментировать с микроконтроллером в качестве интерфейса между компьютером и принтером. Компьютер общается с интерфейсом через последовательный TTL, а микроконтроллер выполняет все необходимые преобразования в параллельный интерфейс плюс занимается сигналами принтера.

Вот шпаргалка по сигналам:
Centronics Сигнал DB25 Обычное Примечания 36-контактный состояние разъём Вывод хоста / Ввод принтера 1 1 /STROBE HIGH Кратковременный импульс вниз с подъёмом наверх для обозначения готовности данных. Импульс 0,5-500 мкс 2-9 2-9 D0-D7 -- Данные 14 14 /AUTOFEED HIGH Подтягивание вниз для использования автоперевода строки 31 16 /INIT HIGH Подтягивание вниз для инициализации принтера 36 17 /SELECT-IN HIGH Используется в некоторых стандартах для обозначения отправки адреса принтера Ввод хоста / Вывод принтера 10 10 /ACK HIGH Импульс вниз для подтверждения получения данных 11 11 BUSY LOW Высокий, когда принтер занят 12 12 PAPER END LOW Высокий, когда кончилась бумага 13 13 SELECT LOW Высокий, когда принтер онлайн 32 15 /ERROR HIGH Низкий, когда возник сбой 19-30, 33 18-25 GROUND Используется в витых парах с сигнальными линиями для обеспечения экранирования сигнала
На коммуникационную плату я добавил сдвиговый регистр (74HC595) для линий данных, чтобы уменьшить количество необходимых мне контактов микроконтроллера, а также несколько подтягивающих вверх резисторов там, где мне показалось нужным. Но микроконтроллера не было — всё находилось на макетной плате, потому что это была фаза экспериментов, на которой хотел проверить, смогу ли написать код.
Вся идея этого проекта, который получил амбициозное, но скучное название SmartParallel, заключалась в том, что интерфейс должен не просто преобразовывать сигналы из последовательных в параллельные. Мне хотелось, чтобы он работал в качестве промежуточного модуля, имеющего собственное мнение о том, как нужно обрабатывать входящие данные; а самое важное — он должен быть программируемым.
Добавив на макетную плату старый добрый ATmega328P, я вскоре убедился, что задача решаема, поэтому пришло время двигаться дальше.
Создаём печатные платы
Эта коммуникационная плата стала одной из моих первых попыток работы с компонентами с поверхностным монтажом (SMD), и она вселила в меня неоправданную уверенность в способности пайки крошечных компонентов.

Не все из них были крошечными. Резисторы имели формат 1812, который сегодня мне уже кажется громадным. Сдвиговый регистр 74HC595 имел формат SOIC-16 с шагом 1,27 мм между контактами. Теперь подобные вещи не вызывают у меня проблем, но в то время казались сложными.
Тем не менее, на следующей итерации мои амбиции ещё больше усилились. На новой печатной плате была не одна, а три интегральные схемы формата SOIC и микроконтроллер ATmega328PB в формате TQFP с шагом 0,8 мм между контактами. Светодиоды и резисторы имели размер 1206.
На тот момент я всё ещё паял вручную, время от времени пользуясь термофеном. Я совершенствовал навыки, и компоненты SOIC-16 и 1206 вызывали лишь небольшие затруднения. Однако при пайке TQFP постоянно возникали перемычки припоя. Прежде чем изготовить работающую плату, я поломал пару чипов.
Что касается коммутационной платы, то микроконтроллер управлял линиями данных через сдвиговый регистр, на этот раз 74LV595. Позже я расскажу, почему перешёл на семейство LV.

Кроме того, на плате был смонтирован 74LV541, используемый в качестве буфера между принтером и микроконтроллером с подтягивающими резисторами на нескольких важных линиях, в частности /ACK, PAPER END и /ERROR (все выводы от принтера). Также буферизировались сигналы BUSY и PAPER END (управляемые принтером) и /STROBE, /INIT и /AUTOFEED (управляемые микроконтроллером).
На плате была интегральная схема инвертора Шмитта 74LV14. Она тоже применялась в качестве буфера для сигналов /AUTOFEED, SELECT и /ERROR, но на этот раз выводы управляли светодиодами.
/AUTOFEED и /ERROR — это сигналы, активные при низком уровне, так что было логично инвертировать логику, зажигая светодиоды при понижении сигналов. Однако SELECT имеет высокий сигнал, когда принтер включён. Почему я инвертировал его? И в самом деле... К этому мы вернёмся позже.
Другими примечательными особенностями были последовательный порт, предоставленный UART ATmega, три управляемые пользователем светодиода, кнопка сброса и порт I2C для подключения дисплея.
На самом деле, эта плата имеет версию Rev 2. Очевидно, была ещё и Rev 1, но сведения о ней потерялись во времени. Я смутно помню её хлипкие провода.

Я разработал и Rev 3, где вернулся к компонентам с монтажом в отверстия, потому что меня утомила борьба с пайкой SMD. С тех пор я не только стал гораздо лучше справляться с поверхностным монтажом, но и открыл для себя радости производства и пайки сложных компонентов фабриками по изготовлению печатных плат.
Выбор
Давайте вернёмся к подробностям о сигнале SELECT. Во время первых экспериментов с параллельным интерфейсом я хотел понять поведение сигналов и их состояния по умолчанию, когда печать не выполняется.
Как и ожидалось, сигнал SELECT принтера становился высоким, когда машина включалась и была готова к печати. Но при тестировании соединений на единственной моей машине с параллельным портом (древнем Dell с Windows XP) я, к моему удивлению, увидел, что компьютер тоже поддерживает высокий сигнал на линии. Я рассудил так:
Принтер повышает сигнал на этой линии, когда он готов, и опускает в обратном случае. Таким образом он может сигнализировать, что включён.
Вероятно, у компьютера эта линия подтянута вверх, чтобы она не находилась в плавающем состоянии, если принтер отсоединён или отключён.
Так я пришёл к выводу, что правильно будет по умолчанию подтягивать этот сигнал вверх. На печатной плате я подавал сигнал через инвертор и пометил светодиод вывода как «Offline»; иными словами, он превратился в сигнал ошибки. Но мне никогда это не нравилось. В следующем проекте я попробую сделать иначе.
Небольшое признание
Был и ещё один сигнал, вызвавший во мне определённое смятение: /ACK.
Процесс печати символа очень прост: на стороне компьютера мы отправляем байт по восьми линиям данных, а затем на короткий промежуток времени опускаем сигнал /STROBE. Это сообщает принтеру, что ожидается байт. Принтер поднимает сигнал на линии BUSY, чтобы сообщить компьютеру, что не стоит его беспокоить, пока не закончена печать, выводит символ, а когда будет полностью готов к получению другого, даёт низкий импульс низкого сигнала /ACK, чтобы сообщить об этом компьютеру, а затем снова опускает сигнал линии BUSY.
Все остальные сигналы — это просто вишенка на торте. На самом деле, нам нужны линии данных плюс эти три сигнала. Но, как оказалось, один из этих трёх даже не требуется.
В первых версиях прошивки SmartParallel микроконтроллер ждал сигнала /ACK, прежде чем двигаться дальше. Из-за этого всё повисало. Я добавил тайм-аут и обнаружилось, что он всегда истекает. Изменение времени ожидания тайм-аута начало давать результаты, но я задумался, действительно ли сигнал /ACK настолько важен.
Оказалось, что ПО для общения с параллельными принтерами часто просто полностью игнорирует /ACK, а некоторые устройства даже не заморачиваются с его отправкой.

Я пробовал всевозможные тесты таймингов, настройки задержек и длины импульсов. Я даже, чёрт возьми, прочитал руководство. Epson MX-80 F/T III производили в эпоху, когда руководства были полезными, например, там были таблицы символов ASCII, примеры кода и расположение контактов параллельного интерфейса. В руководстве Epson также имелась схема таймингов, которую я по какой-то причине до сего момента не замечал.
Например, из схемы можно понять, что сигналы данных должны удерживаться в течение не менее 0,5 мкс перед отправкой импульса /STROBE и что сам импульс должен иметь длину не менее 0,5 мкс (обычно я использую 1 мкс, что нормально). Сигнал BUSY должен становиться высоким почти сразу же, когда начинается импульс /STROBE, а линии данных должны оставаться стабильными не менее 0,5 мкс после завершения импульса /STROBE.

И где же во всём этом /ACK? Принтер подтягивает линию вниз в течение 1 мс (максимум) после завершения импульса /STROBE. При моём тестировании оказалось, что это происходит немного быстрее. Линия /ACK остаётся низкой примерно 5 мкс, а BUSY опускается вскоре после этого.
В конечном итоге я решил полностью игнорировать /ACK. При отправке символов микроконтроллеру нужно знать три вещи:
Включён ли принтер? Об этом сообщает сигнал
SELECT.Есть ли какие-то ошибки? Эту информацию дают
/ERRORиPAPER END.Готов ли принтер к следующему символу? Для этого нужен сигнал
BUSY.
Обратная запитка
Ещё одна возникшая проблема заключалась в том, что когда принтер был включён, но на плату SmartParallel не подавалось питание, происходили произвольные сбросы принтера, а некоторые светодиоды на плате тускло горели.
Такая обратная запитка происходила через какие-то из трёх интегральных схем платы — сдвиговый регистр, буфер и/или инвертор.
Настал момент привлечь на помощь моего самого умного друга — эти ваши интернеты. И, разумеется, выяснилось, что такая обратная запитка — это хорошо известная проблема. Обычно она происходит через встроенные в чипы диоды защиты от электростатического разряда (ESD), предназначенные для обеспечения рассеяния произвольных переходных напряжений.
Большинство конструкций чипов состоит в разнообразных семействах, обозначающих используемые в их основе технологии. Я использовал высокоскоростное КМОП-семейство HC серии 74, потому что... да потому что это всё, что у меня было. Мне показалось, что больше подойдёт семейство LV с пониженным напряжением. Обычно в них имеется так называемая функциональность Ioff, поддерживающая частичное отключение питания, чтобы ограничить потребление тока, когда на чип не подаётся непосредственное питание. Поэтому в них имеется защита от обратной запитки. Это решило проблему.
Программное обеспечение
Любая электроника выглядит более внушающе, если поместить её в корпус, поэтому я засунул в него плату SmartParallel, блок питания, Raspberry Pi 3 и ЖК-дисплей на 2x16 символов. На нём ещё есть большая подсвеченная кнопка сброса. В основном она нужна, чтобы радовать меня, ведь я люблю нажимать на большие подсвеченные кнопки, особенно если они красные.

Углубляться в ПО особого смысла нет, так что расскажу о нём в целом.
В Pi выполняется серверный скрипт на Python (позже заменённый программой на Go), принимающий команды, отправляемые устройствами в сети, и занимающийся печатью. Я разработал протокол сообщений, потому что в основном думал о возможности программного использования принтера из любого источника, в том числе и с IoT-устройств.
Код ориентирован на строки. Он предполагает, что каждый поступающий на сервер пакет данных будет содержать одну строку текста. MX-80 — это 80-символьный принтер или 132-символьный в режиме сжатого текста, поэтому я использовал 254-символьный буфер, в который с лёгкостью умещались все печатные символы плюс большая доля непечатных управляющих символов, например, для выбора подчёркивания, полужирного начертания и завершающего нуля.
Если не считать обычные битвы с багами, я столкнулся с одной кажущейся банальной проблемой, которая всплывает в любых типах последовательного обмена данными — способом обработки возврата каретки и новой строки.
Эти команды (а также то, что их две) уходят корнями в историю пишущей машинки. Доходя до конца строки, печатающий нажимает клавишу возврата, выполняющую два действия — прокручивает валик, чтобы бумага сдвинулась на одну строку (linefeed, или LF); а если продолжать давить, то каретка возвращается справа в свою начальную позицию (carriage return, или CR).
Этот механизм в электрифицированном виде был перенесён в одно из первых периферийных компьютерных устройств — телетайп или телепринтер. По сути, это была электрическая пишущая машинка, которая взаимодействовала с компьютером и выводила на бумагу сессию ввода-вывода.
В Windows по-прежнему сохраняется это наследие, в ней для обозначения конца строки используются и возврат каретки (ASCII 13, или 0x0D), и переход на новую строку (10, или 0x0A). Но я начинал этот проект, рассуждая в терминологии Raspberry Pi с Linux. А в мире *nix в качестве обозначения конца строки обычно используется только переход на новую строку.
Кроме того, у принтера есть параметр /AUTOFEED. В зависимости от его настройки принтер может автоматически выполнять переход на новую строку, когда встречает возврат каретки. Если вы тоже отправляете переходы на новую строку, то будет выполняться двойной переход. В конечном итоге я определил отправляемую на SmartParallel команду, сообщающую, как реагировать на linefeed и carriage return.

Что дальше
В целом вся эта система по большей мере работала, но с парой незначительных глитчей, с которыми мне так и не удалось разобраться. А потом благодаря познавательному и развлекательному YouTube-каналу Veronica Explains я обнаружил готовое решение. В нём применяется довольно старое оборудование, находящееся в сети. Однако для объяснения этого потребуется отдельная статья, которую я скоро выпущу.
И это, в свою очередь, заставило меня заняться совершенно новым решением с участием Raspberry Pi Pico. Но это тоже тема для отдельной статьи.

