Раритет из мира термопринтеров

—

от автора

Приветствую всех!

Думаю, все мы себе представляем, как работает термопринтер. Но неподвижная пластина и ряд нагревательных элементов не всегда были типичной конструкцией такого устройства.

Итак, в сегодняшней статье разберёмся, как устроен и работает термопринтер старого образца с подвижной головкой. Узнаем, как его подключить к микроконтроллеру и запустить. Традиционно будет много интересного.

❯ Суть такова

Давным-давно я уже писал пост о подключении термопечатающей головки к микроконтроллеру. И там я обмолвился, что существовали и более старые экземпляры, где головка не была неподвижной и которые сильно отличаются по управлению. Конечно, с моей стороны было бы неправильно упустить из виду такой экземпляр, тем более, что у меня он есть.

❯ Обзор оборудования

Так уж получилось, что мне в своё время достались остатки от мониторов пациента компании Criticare (модель мне неизвестна, но, судя по всему, это 506N3). Измерительное оборудование было утрачено, но осталась горсть плат, а также несколько термопринтеров.

Сама плата. Запустить её мне не удалось, при включении она просто выдаёт какую-то ошибку, попутно сообщая, что датчик пульсоксиметра не подключён. Тем не менее, распаивать её я не буду, когда-нибудь мы к ней ещё вернёмся.

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

А вот и термопринтер. Это STP211J-192 от Seiko/Epson. Как ясно из названия, разрешение по горизонтали у него 192 точки. Отчётливо видны два шаговых двигателя, печатающая головка, направляющая, червячный вал.

С обратной стороны ничего интересного.

Слева привод головки. Также тут находится концевой выключатель крайнего её положения.

Справа привод протяжки бумаги.

Из других устройств, где применялись такие термопринтеры, можно вспомнить VeriFone PrintPak. И если в модели 350 стоит самый обычный, то в более старом 300 — именно тот, что у нас. Мною весьма активно ищется такой аппарат, но пока что найти его не вышло.

❯ Что нужно, чтобы управлять таким принтером?

В отличие от ранее рассмотренных экземпляров, этот простой и дубовый как никогда.
Из оборудования у нас имеются два мотора, головка, а также датчик её положения. Всё, больше ничего нет. Сама по себе головка представляет сборку из восьми резисторов, которые и служат нагревателями, никакой управляющей логики в ней нет.

Таким образом, помимо драйвера двигателей, понадобятся также силовые ключи для управления головкой.

❯ Моторы

Поскольку шаговые двигатели тут униполярные, для управления ими было решено использовать ULN2804A. Восьми выходов как раз хватит для двух шаговиков, использующихся в принтере.

В даташите на принтер отыскались и последовательности включения двигателя. Так что проблем возникнуть не должно.

Помня об этом, подключаем моторы к ULNке. Выводы 1-8 соединяются с портами контроллера.

❯ ТПГ

В отличие от более совершенных моделей, где термопечатающая головка имела свой собственный драйвер и управлялась по последовательному интерфейсу, здесь применена обычная сборка из восьми нагревательных резисторов. Сама головка съёмная, в даташите даже описана процедура её замены.

Сопротивление этих резисторов отличается в зависимости от модели принтера и составляет от четырнадцати до восемнадцати ом.

Итак, схема для управления головкой получается примерно такая.

❯ Контроллер

Для управления решил взять всем известную Arduino — просто из-за пятивольтовых уровней и встроенного USB-UART. У меня нет ответной части к такому шлейфу, поэтому я припаял МГТФ прямо к контактам. Они там очень крупные, можно спокойно подпаяться, не боясь поплавить шлейф.

Собираем всё вместе. Термопринтер просто идеально подошёл по размерам на макетку. На ней же разместились преобразователь питания, две ULNки и плата Arduino. Термоголовка питается от пяти вольт, но брать их от USB нельзя, во время печати ток может составлять больше двух ампер. Всё, можно начинать эксперименты.

❯ Управление моторами

И для начала, конечно, разберёмся с приводами. Тут всё достаточно просто — шаг мотора головки сдвигает её на расстояние одного пикселя, шаг мотора протяжки бумаги прокручивает её на расстояние четверти пикселя. Функции для всего этого получились вот такие:

uint8_t currentPhase = 0; uint8_t paperCurrentPhase = 0;  void paperStep() {   switch (paperCurrentPhase) {     case 2:       digitalWrite(A0, LOW);       digitalWrite(A1, LOW);       digitalWrite(A2, HIGH);       digitalWrite(A3, HIGH);       break;     case 3:       digitalWrite(A0, LOW);       digitalWrite(A1, HIGH);       digitalWrite(A2, HIGH);       digitalWrite(A3, LOW);       break;     case 0:       digitalWrite(A0, HIGH);       digitalWrite(A1, HIGH);       digitalWrite(A2, LOW);       digitalWrite(A3, LOW);       break;     case 1:       digitalWrite(A0, HIGH);       digitalWrite(A1, LOW);       digitalWrite(A2, LOW);       digitalWrite(A3, HIGH);       break;   }   if (paperCurrentPhase == 3) paperCurrentPhase = 0;   else paperCurrentPhase++; }  void headStep(int8_t dir) {   if (dir == -1) {     switch (currentPhase) {       case 1:         digitalWrite(A4, LOW);         digitalWrite(A5, LOW);         digitalWrite(11, HIGH);         digitalWrite(12, HIGH);         break;       case 0:         digitalWrite(A4, LOW);         digitalWrite(A5, HIGH);         digitalWrite(11, HIGH);         digitalWrite(12, LOW);         break;       case 3:         digitalWrite(A4, HIGH);         digitalWrite(A5, HIGH);         digitalWrite(11, LOW);         digitalWrite(12, LOW);         break;       case 2:         digitalWrite(A4, HIGH);         digitalWrite(A5, LOW);         digitalWrite(11, LOW);         digitalWrite(12, HIGH);         break;     }   }   else if (dir == 1) {     switch (currentPhase) {       case 0:         digitalWrite(A4, LOW);         digitalWrite(A5, LOW);         digitalWrite(11, HIGH);         digitalWrite(12, HIGH);         break;       case 1:         digitalWrite(A4, LOW);         digitalWrite(A5, HIGH);         digitalWrite(11, HIGH);         digitalWrite(12, LOW);         break;       case 2:         digitalWrite(A4, HIGH);         digitalWrite(A5, HIGH);         digitalWrite(11, LOW);         digitalWrite(12, LOW);         break;       case 3:         digitalWrite(A4, HIGH);         digitalWrite(A5, LOW);         digitalWrite(11, LOW);         digitalWrite(12, HIGH);         break;     }   }   if (currentPhase == 3) currentPhase = 0;   else currentPhase++; }

В отличие от управления головкой, время выполнения тут не слишком критично, поэтому используются «медленные» digitalWrite. Для привода ТПГ также добавлена возможность задания направления.

❯ Инициализация

Отдельно стоит упомянуть про действия после запуска. Сразу после подачи питания МК не знает, где сейчас находится головка. Поэтому необходимо выставить её в нулевое положение — гнать влево, пока она не упрётся в концевой выключатель. Дальше необходимо сделать ещё несколько добавочных шагов, так как датчик срабатывает несколько раньше, чем головка упирается в крайнее положение. Если же ноль уже стоит, выводим головку из него и проверяем, не разомкнулся ли концевик. Если даже после существенного числа шагов он всё равно замкнут, значит, на моторы не подаётся питание или просто нет контакта.
Делается это всё примерно так:

void headInit() {   if (!digitalRead(10)) headReturn();   else {     for (int i = 0; i < 50; i++) {       headStep(1);       delay(10);     }     if (digitalRead(10)) {       Serial.println("Head drive error");       while (1);;     }     else headReturn();   } }  void headReturn() {   while (!digitalRead(10)) {     headStep(-1);     delay(10);   }   for (int i = 0; i < 6; i++) {     headStep(-1);     delay(10);   } }

Вообще, в даташите было сказано о двух шагах после касания концевика. Но в моём случае механизм имел достаточно сильный люфт, так что для уверенного возврата каретки число шагов увеличил до шести. Только тогда она стала нормально вставать в крайнее положение.

❯ Управление головкой

Теперь очередь нагревателей. Чтобы задать их состояние, используется следующая функция:

void headControl(uint8_t toHead) {   PORTD &= B00000011;   PORTB &= B11111100;   PORTD |= ((toHead << 2) & B11111100);   PORTB |= ((toHead >> 6) & B00000011); } 

Для удобства загрузки восьми бит сразу применена работа с портами через регистры.

❯ Печать символов

Как известно, головка печатает строку символов за один проход, как в матричном принтере. То есть за раз прожигается вертикальная линия из восьми точек. А это значит, что тот самый шрифт из предыдущего поста про термопринтер подойдёт как нельзя лучше, не придётся разбираться с преобразованием строки символов в восемь горизонтальных линий. Поэтому для печати символа необходимо всего лишь разбить его на пять столбцов, а потом последовательно прожечь их, каждый раз сдвигая головку на один шаг.

Делается это примерно так:

void printChar(char input) {   uint8_t vertical8dots = 0x00;   for (int i = 0; i < 5; i++) {     vertical8dots = pgm_read_byte(&FontTable[input][i]);     headControl(vertical8dots);     delay(3);     headControl(0x00);     headStep(1);     delay(10);   }   headDriveOff(); }

Задержка перед отключением головки определяет яркость печати. Не стоит пытаться изменить её поднятием напряжения, иначе головка может сдохнуть.

❯ Печать строки

Ну, где символы, там и строка. Делается это всё достаточно просто:

void printString(String toPrinter) {   int target = 0;   for (int i = 0; i < 20; i++) {     headStep(1);     delay(10);   }   if (toPrinter.length() > 18) target = 18;   else target = toPrinter.length();   for (int i = 0; i < target; i++) {     printChar(toPrinter[i]);     for (int n = 0; n < 3; n++) {       headStep(1);       delay(10);     }   }   headReturn();   headDriveOff(); }

Ничего сложного: прожигаем очередной символ, затем сдвигаем головку на некоторое число пикселей (в моём случае три) и так до конца строки. Затем возвращаем головку на место, и можно проматывать бумагу.

В итоге вся программа получилась такая:

#include "FontTable.h"  uint8_t currentPhase = 0; uint8_t paperCurrentPhase = 0;  void setup() {   for (int i = 2; i <= 9; i++) pinMode(i, OUTPUT);   pinMode(10, INPUT_PULLUP);   pinMode(11, OUTPUT);   pinMode(12, OUTPUT);   for (int i = 14; i <= 19; i++) pinMode(i, OUTPUT);   Serial.begin(115200);   headInit();   headDriveOff(); }  void paperStep() {   switch (paperCurrentPhase) {     case 2:       digitalWrite(A0, LOW);       digitalWrite(A1, LOW);       digitalWrite(A2, HIGH);       digitalWrite(A3, HIGH);       break;     case 3:       digitalWrite(A0, LOW);       digitalWrite(A1, HIGH);       digitalWrite(A2, HIGH);       digitalWrite(A3, LOW);       break;     case 0:       digitalWrite(A0, HIGH);       digitalWrite(A1, HIGH);       digitalWrite(A2, LOW);       digitalWrite(A3, LOW);       break;     case 1:       digitalWrite(A0, HIGH);       digitalWrite(A1, LOW);       digitalWrite(A2, LOW);       digitalWrite(A3, HIGH);       break;   }   if (paperCurrentPhase == 3) paperCurrentPhase = 0;   else paperCurrentPhase++; }  void headStep(int8_t dir) {   if (dir == -1) {     switch (currentPhase) {       case 1:         digitalWrite(A4, LOW);         digitalWrite(A5, LOW);         digitalWrite(11, HIGH);         digitalWrite(12, HIGH);         break;       case 0:         digitalWrite(A4, LOW);         digitalWrite(A5, HIGH);         digitalWrite(11, HIGH);         digitalWrite(12, LOW);         break;       case 3:         digitalWrite(A4, HIGH);         digitalWrite(A5, HIGH);         digitalWrite(11, LOW);         digitalWrite(12, LOW);         break;       case 2:         digitalWrite(A4, HIGH);         digitalWrite(A5, LOW);         digitalWrite(11, LOW);         digitalWrite(12, HIGH);         break;     }   }   else if (dir == 1) {     switch (currentPhase) {       case 0:         digitalWrite(A4, LOW);         digitalWrite(A5, LOW);         digitalWrite(11, HIGH);         digitalWrite(12, HIGH);         break;       case 1:         digitalWrite(A4, LOW);         digitalWrite(A5, HIGH);         digitalWrite(11, HIGH);         digitalWrite(12, LOW);         break;       case 2:         digitalWrite(A4, HIGH);         digitalWrite(A5, HIGH);         digitalWrite(11, LOW);         digitalWrite(12, LOW);         break;       case 3:         digitalWrite(A4, HIGH);         digitalWrite(A5, LOW);         digitalWrite(11, LOW);         digitalWrite(12, HIGH);         break;     }   }   if (currentPhase == 3) currentPhase = 0;   else currentPhase++; }  void lineFeed() {   for (int i = 0; i < 48; i++) {     paperStep();     delay(10);   }   paperDriveOff(); }  void headReturn() {   while (!digitalRead(10)) {     headStep(-1);     delay(10);   }   for (int i = 0; i < 6; i++) {     headStep(-1);     delay(10);   } }  void headInit() {   if (!digitalRead(10)) headReturn();   else {     for (int i = 0; i < 50; i++) {       headStep(1);       delay(10);     }     if (digitalRead(10)) {       Serial.println("Head drive error");       while (1);;     }     else headReturn();   } }  void headDriveOff() {   digitalWrite(A4, LOW);   digitalWrite(A5, LOW);   digitalWrite(11, LOW);   digitalWrite(12, LOW); }  void paperDriveOff() {   digitalWrite(A0, LOW);   digitalWrite(A1, LOW);   digitalWrite(A2, LOW);   digitalWrite(A3, LOW); }  void headControl(uint8_t toHead) {   PORTD &= B00000011;   PORTB &= B11111100;   PORTD |= ((toHead << 2) & B11111100);   PORTB |= ((toHead >> 6) & B00000011); }  void printChar(char input) {   uint8_t vertical8dots = 0x00;   for (int i = 0; i < 5; i++) {     vertical8dots = pgm_read_byte(&FontTable[input][i]);     headControl(vertical8dots);     delay(3);     headControl(0x00);     headStep(1);     delay(10);   }   headDriveOff(); }  void printString(String toPrinter) {   int target = 0;   for (int i = 0; i < 20; i++) {     headStep(1);     delay(10);   }   if (toPrinter.length() > 18) target = 18;   else target = toPrinter.length();   for (int i = 0; i < target; i++) {     printChar(toPrinter[i]);     for (int n = 0; n < 3; n++) {       headStep(1);       delay(10);     }   }   headReturn();   headDriveOff(); }  void loop() {   String inputString = Serial.readString();    if (inputString.length() > 0)   {     printString(inputString);     lineFeed();   } }

Пробуем что-то напечатать… и оно даже работает! К слову говоря, шрифт очень сильно напоминает тот, что выдаёт матричный принтер. Справа на фото как раз такая распечатка — сходство весьма сильное.

И я даже записал видео с этим:

❯ Двунаправленная печать

А что, если реализовать печать как в матричном принтере — при каждом проходе каретки? Официально этот механизм такое не поддерживает, но ничего не мешает это попробовать.
Для того, чтобы такое реализовать, необходимо поменять алгоритм печати: будем прожигать строку не посимвольно, а всю разом. Для этого создадим массив, куда запишем все символы вместе с пробелами сразу, а потом будем его печатать. Получилось примерно следующее:

void printString(String toPrinter) {   uint8_t index = 0;   uint8_t toHead[192];   for (int i = 0; i < 192; i++) toHead[i] = 0x00;   int target = 0;   for (int i = 0; i < 20; i++) {     headStep(1);     delay(10);   }   if (toPrinter.length() > 18) target = 18;   else target = toPrinter.length();   for (int i = 0; i < target; i++) {     for (int n = 0; n < 5; n++) {       toHead[index] = pgm_read_byte(&FontTable[toPrinter[i]][n]);       index++;     }     index += 3;   }   for (int i = 0; i < 146; i++) {     headControl(toHead[i]);     delay(3);     headControl(0x00);     headStep(1);     delay(10);   }   headMoveDirection = -1;   headDriveOff(); }

Как оказалось, эффективная ширина печати намного меньше 192 пикселей, а без отступов по краям распечатка выглядит так себе. Тем не менее, размер массива в 192 байта я оставил, для совместимости с другими модификациями этого принтера (ну, или если кому-то захочется печатать без полей).

Запускаем и убеждаемся, что всё работает как надо. Как нетрудно догадаться, алгоритм печати в обратном направлении совершенно идентичен:

void printStringReversed(String toPrinter) {   uint8_t index = 0;   uint8_t toHead[192];   for (int i = 0; i < 192; i++) toHead[i] = 0x00;   int target = 0;   if (toPrinter.length() > 18) target = 18;   else target = toPrinter.length();   for (int i = 0; i < target; i++) {     for (int n = 0; n < 5; n++) {       toHead[index] = pgm_read_byte(&FontTable[toPrinter[i]][n]);       index++;     }     index += 3;   }     for (int i = 148; i >= 0; i--) {     headControl(toHead[i]);     delay(3);     headControl(0x00);     headStep(-1);     delay(10);   }   headReturn();   headDriveOff();   headMoveDirection = 1; }

Чтобы сделать печать максимально простой, добавил отдельную функцию, где бы выбиралось нужное направление:

void processPrinting(String input) {   if (headMoveDirection == 1) printString(input);   else if (headMoveDirection == -1) printStringReversed(input);   lineFeed(); }

Работает. Но тут я столкнулся уже с чисто конструктивными ограничениями: как бы я ни подкручивал общее число шагов, заставить строки быть точно на одном уровне не вышло. Люфт механизма всё же даёт о себе знать.

Получилось примерно так:

На видео заметен ещё один глюк: при печати в обратном направлении теряются первые два символа. Это косяк не алгоритма печати, а исключительно функции для вывода этой таблицы символов.

❯ Вот как-то так

Понятное дело, что в наши дни этот принтер — скорее игрушка, чем действительно рабочий девайс. И для реальных проектов давно уже существуют более простые в управлении принтеры. Тем не менее, запустить такое было реально интересно. А некоторое сходство с матричным принтером ещё больше добавляет крутизны этому девайсу.

Такие дела.


Возможно, захочется почитать и это:


ссылка на оригинал статьи https://habr.com/ru/articles/752870/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *