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

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



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

Суть такова


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

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




Так уж получилось, что мне в своё время достались остатки от мониторов пациента компании 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();
}

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

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


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

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


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

Такие дела.



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


Комментарии (20)


  1. dlinyj
    09.08.2023 09:28
    +4

    Когда-то дома у меня валялся кассовый аппарат с точно таким же принтером. Я думал-думал, как его запустить, да не стал морочиться. Но у вас получилось. Очень круто, спасибо за статью!


    1. MaFrance351 Автор
      09.08.2023 09:28
      +3

      Интересно, что за кассовый аппарат.
      Было бы интересно посмотреть на то, как этот принтер работает "по заводу".


  1. marks
    09.08.2023 09:28
    +3

    Вот тоже интересный термопринтер, не такой специализированный:




    1. MaFrance351 Автор
      09.08.2023 09:28
      +2

      Интересная шняга. По виду, кстати, очень похож на портативные сублимационные фотопринтеры.
      Правда, этот картридж C-11 наверняка уже сейчас не достать, разве что обычную термобумагу нарезать и туда вставить...


      Я когда-то думал купить принтер по типу GWP-80, который тоже портативный, но полноразмерный А4 и печатает на факсовой ленте. Но он дорогой слишком.


      1. marks
        09.08.2023 09:28
        +2

        Продается на ибее. Во всяком случае, продавался, когда я искал. У многих компаний запасы накопились что принтеров таких, что бумаги. На барахолке, где я его купил, было сразу штук пять принтеров и гора этой бумаги. Кто-то не выбрасывает, а продает.


        1. MaFrance351 Автор
          09.08.2023 09:28
          +2

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


  1. serafims
    09.08.2023 09:28
    +2

    Любопытно, не знал о таком. Думал, что сразу появились ТПГ на всю ширину, так видел даже дубовые отечественные от кассовых аппаратов.
    Любопытно, можно ли самому сделать термобумагу. Молоком пропитать, к примеру).


    1. MaFrance351 Автор
      09.08.2023 09:28
      +1

      Там наверняка ещё куча нюансов типа длительности и температуры нагрева. И принтеру придётся на пределе работать.


  1. grishkaa
    09.08.2023 09:28

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


    1. MaFrance351 Автор
      09.08.2023 09:28

      А не окажется, что из-за размера пикселей в головке эти меры будут несущественными?


      1. grishkaa
        09.08.2023 09:28

        Ну как минимум удвоить кажется целесообразным, иначе между пикселями видны зазоры, если присмотреться.


        1. MaFrance351 Автор
          09.08.2023 09:28
          +1

          Тогда можно и печать картинок реализовать. Вначале нечётные строки печатаем, потом чётные.


          1. tormozedison
            09.08.2023 09:28

            А если применить микрошаг, можно и по горизонтали плотность печати увеличить.


            1. MaFrance351 Автор
              09.08.2023 09:28

              Одна проблема — головка люфтит нещадно...


              1. tormozedison
                09.08.2023 09:28

                А есть способ люфт уменьшить?


                1. MaFrance351 Автор
                  09.08.2023 09:28

                  Заменой пластиковых деталей, возможно. Они же изношены достаточно...


                  Или же сделать программную компенсацию этого (например, делать обычный шаг, а потом в обратную сторону "добивать" несколькими микрошагами).


  1. tormozedison
    09.08.2023 09:28

    С четырьмя подвижными головами, правда, не термо.

    https://habr.com/ru/articles/445850/


    1. MaFrance351 Автор
      09.08.2023 09:28

      Крутой аппарат.
      Аналогичный принтер у меня в составе терминала Tranz 460. Про него тоже писал некогда пост.


  1. Dr_Faksov
    09.08.2023 09:28

    Похож на принтер, встраиваемый в калькуляторы.


    1. MaFrance351 Автор
      09.08.2023 09:28

      В калькуляторах, правда, принтер барабанный, похожий по своей конструкции на АЦПУ.