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

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

6l0azdcdjiqg-gz4uo4ftpmbqcg.jpeg

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

Суть такова


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

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


ikp1aofujagl9bdgrvfyppxnll4.png

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

93v8uthz-gspg9qic5e8_juxkjm.jpeg

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

jgkpcozk5dbamnkigakb9zucvns.jpeg

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

gdezvedmgxkyx6u59mwd8k90pzw.jpeg

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

hfyayrht1pqggtz5ajupyniocqe.jpeg

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

rq8emcoejxw7wdny-k-ejredxzq.jpeg

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

nwxlze69c0al9neuu-bdta7od4c.png

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

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


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

Моторы


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

53ifhxpzp3wqrkcqucl3dv2fwp8.png

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

1722spjyayt1yd0b0wbvc-ciju0.jpeg

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

ТПГ


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

y4fqj5iyhlo8jzrmibb9hzgzx6y.png

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

qla3teplj23urbjd379msjzgtjc.jpeg

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

Контроллер


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

ihq3gaxkyf59qcjetgixrdoigha.jpeg

Собираем всё вместе. Термопринтер просто идеально подошёл по размерам на макетку. На ней же разместились преобразователь питания, две 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();
  }
}


fm0m__syd87dcwl6hft64z_u8hm.jpeg

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


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


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

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();
}


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

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


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

Такие дела.


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

© Habrahabr.ru