Думаю, все мы себе представляем, как работает термопринтер. Но неподвижная пластина и ряд нагревательных элементов не всегда были типичной конструкцией такого устройства.
Итак, в сегодняшней статье разберёмся, как устроен и работает термопринтер старого образца с подвижной головкой. Узнаем, как его подключить к микроконтроллеру и запустить. Традиционно будет много интересного.
❯ Суть такова
❯ Обзор оборудования
Так уж получилось, что мне в своё время достались остатки от мониторов пациента компании 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();
}
Работает. Но тут я столкнулся уже с чисто конструктивными ограничениями: как бы я ни подкручивал общее число шагов, заставить строки быть точно на одном уровне не вышло. Люфт механизма всё же даёт о себе знать.
Получилось примерно так:
На видео заметен ещё один глюк: при печати в обратном направлении теряются первые два символа. Это косяк не алгоритма печати, а исключительно функции для вывода этой таблицы символов.
❯ Вот как-то так
Понятное дело, что в наши дни этот принтер — скорее игрушка, чем действительно рабочий девайс. И для реальных проектов давно уже существуют более простые в управлении принтеры. Тем не менее, запустить такое было реально интересно. А некоторое сходство с матричным принтером ещё больше добавляет крутизны этому девайсу.
Такие дела.
Автор: Лев