Некоторые из нас неоднократно интересовались, как подключить термопринтер от какого-либо оборудования (кассы, терминала, торгового автомата или чего-то ещё). Количество постов на тематических форумах — хороший тому пример.
Тем не менее, подробного описания работы с такими железками я нигде не встречал.
Итак, в сегодняшней статье узнаем, как заставить работать термопечатающую головку со стандартным последовательным интерфейсом. Разберёмся, как подключить её и как ей управлять. Традиционно будет много интересного.
❯ О чём я?
Безусловно, китайской промышленностью уже выпускаются экземпляры, подключение которых к компьютеру или МК не вызывает ни малейших проблем.
В данной же статье разберём именно «голые» механизмы термопринтеров (thermal printer core), не имеющие какого-то готового интерфейса. Причин для их применения достаточно — возможно, у вас есть корпус, разработанный под конкретный механизм, или же, может, хочется собрать какой-то девайс, но старый кассовый аппарат на вторичке стоит в разы дешевле такого блока, ну, или же просто к вам в руки попала такая железка, которую хочется запустить.
К слову, когда-то давно товарищем BaurzhanD был написан пост про то, как подключить принтер от кассы к Arduino. Но тот принтер был матричным (к слову, было бы неплохо попробовать найти такой экземпляр поиграться), а сегодня речь пойдёт про куда более распространённые термопринтеры.
❯ Обзор оборудования
Где же взять экземпляр на опыты?
Разумеется, его можно пойти и купить. Искать следует нечто вроде thermal printer core или embedded thermal printer. Также его можно изъять из отслужившей своё кассовой техники. В (теперь уже) далёком две тысячи семнадцатом году появился закон 54-ФЗ, сделавший обязательным использование онлайн-касс, отчего сотни аппаратов старого образца, не подлежащих модернизации, отправились на свалку и просторы вторички. До сих пор такие экземпляры можно найти даром или по до смешного малой цене. Также можно найти просто неисправную кассу: хоть печатающая головка и не вечна, импортные экземпляры весьма надёжны и редко выходят из строя. Ну а если попадётся обычный термопринтер для COM-порта или встраиваемый экземпляр от какого-нибудь торгового автомата, то тут всё вообще просто — большинство из них имеют хорошо задокументированный протокол ESC/POS, так что разбирать их вообще не надо.
Вот для примера кассовые аппараты «Феликс-РК» и «Феликс-02К». К слову, их можно запустить в качестве термопринтера и без препарирования, как это сделать, расскажу чуть позже.
Терминалы оплаты. Примерно в то же время были выведены из эксплуатации устройства без поддержки бесконтактной оплаты, отчего их вполне можно приобрести на вторичке (и цена вполне молодёжная).
А вот и принтер. Он от помершего терминала VeriFone VX510. Именно с ним мы и будем проводить эксперименты.
Ещё один, от Hypercom Optimum T4220. По интерфейсу подключения он ничем не отличается от предыдущего (только слегка другая распиновка). Единственная разница в механизме — у VeriFone VX510 прижимной резиновый валик снимался нажатием на рычаг, тут же его удерживают просто две пружинки и для извлечения нужно за него потянуть. Практически во всех таких термопринтерах конструкция корпуса такова, что приводной вал закреплён на крышке отсека бумаги, соответственно, для заправки необходимо просто прижать бумагу крышкой, а дальше она встанет как надо сама, не надо будет проматывать её как в матричных принтерах.
Суровый отечественный принтер от какого-то кассового аппарата. По управлению он несколько отличается от предыдущих, так что рассмотрим его в другой раз.
❯ Про кассы
Вообще, для многих кассовых аппаратов есть свой софт, позволяющий печатать на них как на термопринтере. Тема подробно раскрыта вот в этом посте.
Вот для примера софт компании «Атол», поддерживающий немалое число девайсов, выпущенных нашей промышленностью.
А вот печать на кассовом аппарате «Феликс-02К».
С терминалами сложнее — для них нужен SDK, при помощи которого надо написать программу для печати. В своё время именно это довелось сделать товарищу vladkorotnev (именно его прошивку я тут запустил). Но если софта у вас нет (увы, для большинства терминалов всё именно так), то напечатать что-то будет тяжело.
Как устроен термопринтер
По сравнению с матричными собратьями, эти девайсы куда более просты и надёжны.
Для примера разберём ещё один такой модуль от терминала. Он в нерабочем состоянии: вышла из строя группа нагревательных элементов, отчего он стал печатать с полосами, так что разломать его не жалко. Деталей всего-ничего: головка, пластмассовый корпус, несколько направляющих, шестерни механизма протяжки, резиновый валик (на фото его нет), маленький шаговый мотор. Именно в простоте и заключается надёжность таких девайсов: в отличие от матричных с их иголками и кареткой, здесь только неподвижная головка и пара шестерёнок.
Термопечатающая головка (она же просто ТПГ). Она представляет собой керамическую пластину, на которой находится ряд тонкоплёночных резисторов, которые и осуществляют нагрев. Полосы золотистого цвета — ряды проводников, подающих питание на нагреватели. Они поделены на шесть групп, чтобы уменьшить броски тока при печати путём последовательного прожига каждого из шести участков строки. Под слоем смолы — бескорпусные микросхемы управления.
Обратная сторона. Керамическая основа приклеена к пластиковой корпусной детали. На шлейфике датчик наличия бумаги.
Конечно, у термопринтеров есть и свои неисправности (например, банальный износ нагревательных элементов от трения их об бумагу), но в остальном они по всем параметрам превосходят матричные. Они просты в управлении, очень надёжны, не требуют дефицитных картриджей.
К слову говоря, на заре термопечати экземпляры с подвижной кареткой всё же существовали. Вот один из них, головка печатает вертикальную линию в восемь точек. На данный момент такие устройства встречаются уже редко.
❯ Интерфейс
Теперь разберёмся с тем, как вообще управлять термопринтером. Сам по себе он состоит из нескольких компонентов: собственно, самой термопечатающей головки, обычного биполярного шагового двигателя, NTC-термистора для контроля перегрева, датчика наличия бумаги (то есть оптического концевика). И если последние три пункта, думаю, пояснений не требуют, то с головкой разберёмся поподробнее.
Термопечатающая головка — по сути сдвиговый регистр (один-единственный или группа из нескольких, в зависимости от модели). Она имеет всё те же линии LATCH, DATA, CLK, что и у типичного представителя таких устройств вроде 74HC595N. Каждой из шести групп точек соответствует свой регистр. Соответственно, алгоритм печати строки получается примерно следующий:
- Загоняем в принтер группу точек.
- Записываем их при помощи LATCH.
- Аналогичным образом записываем остальные группы.
- Последовательно прожигаем каждую из шести групп.
- Проматываем бумагу.
Одна строка представляет собой ряд из трёхсот восьмидесяти четырёх пикселей. Соответственно, за раз необходимо загнать сорок восемь байт (причём не с наскоку, а шестью блоками по восемь байт, чтобы правильно загрузить данные в шесть сдвиговых регистров). Есть также более старые экземпляры с вдвое или втрое меньшим разрешением.
❯ Что нужно для запуска
Конечно, у меня уже были эти кассовые аппараты. И я даже пользовался одним из них («Феликс-РК», с головкой под более широкую ленту) — печатал шпоры, заметки, списки и тому подобные документы. Но, конечно, хотелось запустить и «голый» экземпляр.
Итак, для того, чтобы это сделать, нам понадобится примерно следующее:
- Собственно, сам принтер. О нём детально расскажем чуть позже.
- МК с логическими уровнями в пять вольт. Разбираться с их преобразованием не хотелось, взял обычную Arduino Nano.
- Драйвер шагового двигателя. Как нетрудно догадаться, нужен он для протяжки бумаги.
- DC-DC преобразователь. Он будет создавать напряжение порядка семи вольт (точные его значения можно узнать из документации к принтеру), которое будет подаваться на нагреватели. Суровые отечественные термоголовки работают от двенадцати вольт.
Самое сложное тут — найти документацию на сам принтер. Мой экземпляр имел маркировку LTPA245S и был произведён Seiko/Epson. В поисках распиновки я даже начал реверсить схему терминала и даже определил некоторые выводы (на скриншоте то, к чему я успел прийти), но в итоге даташит таки был найден по запросу "ltpa245 technical reference".
Идём в документацию и смотрим напряжения питания. Управляющие цепи питаются от пяти вольт (по идее, подача трёх вольт позволит подключить девайс к трёхвольтовому МК), на нагреватели надо около восьми. Можно, конечно, и меньше, но тогда изображение будет бледным.
А вот и долгожданная распиновка. Vp и Vdd подключаем к питанию, LATCH, DAT, CLK — к цифровым пинам контроллера. Если у вас есть, чем обеспечить стабильное питание, DST1...DST6 можно соединить вместе и подключить к одному контакту МК. К слову, в терминале это было реализовано именно так. Разъёма для этого шлейфа под рукой не оказалось, так что я просто припаял к контактам тонкий провод МГТФ.
❯ Испытания
Теперь очередь программы. На просторах нашлась вот такая статья, где (на удивление) весьма доходчиво было описано подключение термоголовки. Скачиваем скетч, перепиливаем работу с драйвером на STEP/DIR, заливаем.
Вставляем бумагу, подаём питание, и, внезапно, всё оживает с первого раза. Нет, серая муть выше надписи никакого отношения к этому не имеет, это наследие экспериментов с печатью графики.
Для дальнейших экспериментов разместил всё на обломке макетки. Драйвер шагового двигателя заменён на EasyDriver.
❯ Совершенствованию нет предела
Устройство работало, но программа меня совершенно не устраивала (что по аккуратности кода, что по потреблению памяти), да и хотелось ещё реализовать печать картинок. Но на тот момент желания этим заниматься совершенно не было.
И вот спустя практически три года после сборки той схемы я таки поправил ту прошивку. А это значит, что самое время разобраться, как оно вообще работает и как этим пользоваться.
#include "FontTable.h"
#define TH_LATCH 2
#define TH_STROBE 4
#define TH_CLOCK 11
#define TH_DATA 10
#define FEED_BUTTON 12
#define STEP A0
#define DIR A1
#define MOT_EN A2
#define STEPS_REQUIRED 35
#define MOTOR_DELAY 500
#define HEATING_TIME 4000
#define COOLING_TIME 1000
#define LINE_INTERVAL 6
#define FORM_INTERVAL 15
#define LINE_FEED 0
#define FORM_FEED 1
#define PIXEL_FEED 2
#define WIDTH_SCALE 4
String currentPrinting = "";
int countDots = 1;
int count2 = 1;
int currentStb = 1;
void setup()
{
pinMode(STEP, OUTPUT);
pinMode(DIR, OUTPUT);
pinMode(MOT_EN, OUTPUT);
digitalWrite(MOT_EN, HIGH);
digitalWrite(DIR, HIGH);
pinMode(TH_LATCH, OUTPUT);
digitalWrite(TH_LATCH, HIGH);
pinMode(TH_CLOCK, OUTPUT);
digitalWrite(TH_CLOCK, HIGH);
pinMode(TH_DATA, OUTPUT);
digitalWrite(TH_DATA, HIGH);
pinMode(TH_STROBE, OUTPUT);
heatOff();
pinMode(FEED_BUTTON, INPUT_PULLUP);
Serial.begin(9600);
while (!Serial);;
delay(500);
Serial.println("--------input string (ENTER=Finish)------------");
Serial.println(" ширина шрифта =" + String(WIDTH_SCALE));
}
void loop()
{
while (digitalRead(FEED_BUTTON) == LOW) // прогон ленты по кнопке
{
paperFeed(FORM_FEED);
}
String inputString = Serial.readString();
if (inputString.length() > 0)
{
Serial.println("печатаем строку : '" + inputString + "' ");
printString(inputString);
paperFeed(LINE_FEED);
}
}
void printString(String target)
{
currentPrinting = target;
for (int jj = 0; jj < 8; jj++)
{
print384DotsRow(jj);
burn1Line384Dots();
paperFeed(PIXEL_FEED);
}
currentPrinting = "";
}
void print384DotsRow(uint8_t row)
{
currentStb = 1;
int nextSymbol = 0;
int len = currentPrinting.length();
unsigned char code = currentPrinting[nextSymbol];
unsigned char horizontalPosition = 0;
unsigned char vertical8dots;
countDots = 1;
int maxSymbols = min(round(64 / WIDTH_SCALE), len);
int symbolWidth = (int)(6 * WIDTH_SCALE);
while (countDots <= 384)
{
if ( fmod (count2, 6) == 0) // 6 точек - 1 символ (точек : 5 + 1 на разделитель)
{
nextSymbol++;
if (nextSymbol <= maxSymbols)
{
code = currentPrinting[nextSymbol];
}
else
{
code = char(' '); // оставшуюся пустую часть забиваем пробелами
}
// добавляем разделитель, т.е. пустую точку
for (int dd = 1; dd <= WIDTH_SCALE; dd++)
{
if (loadToPrinter_1Dot(0) == 1)
return;
}
count2++;
horizontalPosition = 0;
}
else
{
vertical8dots = pgm_read_byte(&FontTable[code][horizontalPosition]);
int vv = (vertical8dots >> row) & 0x01; //
for (int dd = 1; dd <= WIDTH_SCALE; dd++)
{
if (loadToPrinter_1Dot(vv) == 1)
return;
}
count2++;
horizontalPosition++;
}
}
return;
}
void sendToPrinter_LAT()
{
digitalWrite(TH_LATCH, LOW);
delayMicroseconds(1);
digitalWrite(TH_LATCH, HIGH);
delayMicroseconds(1);
}
void burn1Line384Dots()
{
digitalWrite(TH_STROBE, HIGH);
delayMicroseconds(HEATING_TIME);
digitalWrite(TH_STROBE, LOW);
delayMicroseconds(COOLING_TIME);
heatOff();
}
int loadToPrinter_1Dot(uint8_t val)
{
countDots++;
digitalWrite(TH_CLOCK, LOW);
delayMicroseconds(1);
if (val == 1)
{
digitalWrite(TH_DATA, HIGH);
}
else
{
digitalWrite(TH_DATA, LOW);
}
delayMicroseconds(1);
digitalWrite(TH_CLOCK, HIGH);
delayMicroseconds(1);
// если заполнились 64 точки , то надо просто защелкнуть LAT
if ( fmod (countDots, 64) == 0) // 64 новый STB
{
sendToPrinter_LAT();
if (currentStb == 7)
return 1;
currentStb++;
}
return 0;
}
void heatOff()
{
digitalWrite(TH_LATCH, HIGH);
digitalWrite(TH_STROBE, LOW);
}
void motorStep()
{
digitalWrite(MOT_EN, LOW);
for (int k = 0; k < STEPS_REQUIRED; k++) {
digitalWrite(STEP, LOW);
delayMicroseconds(MOTOR_DELAY);
digitalWrite(STEP, HIGH);
}
digitalWrite(MOT_EN, HIGH);
}
void paperFeed(uint8_t mode) {
int i;
switch (mode) {
case LINE_FEED:
for (i = 0; i < LINE_INTERVAL; i++) {
motorStep();
}
break;
case FORM_FEED:
for (i = 0; i < FORM_INTERVAL; i++) {
motorStep();
}
break;
case PIXEL_FEED:
motorStep();
break;
}
}
А вот и сама программа. В отдельном файле находится шрифт:
// font 8*5 CP866
const unsigned char PROGMEM FontTable[256][5] = {
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x00 0
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x01 1
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x02 2
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x03 3
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x04 4
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x05 5
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x06 6
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x07 7
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x08 8
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x09 9
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0A 10
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0B 11
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0C 12
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0D 13
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0E 14
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x0F 15
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x10 16
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x11 17
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x12 18
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x13 19
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x14 20
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x15 21
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x16 22
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x17 23
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x18 24
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x19 25
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1A 26
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1B 27
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1C 28
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1D 29
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1E 30
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x1F 31
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x20 32
{ 0x00, 0x00, 0x5F, 0x00, 0x00 }, // ! 0x21 33
{ 0x00, 0x07, 0x00, 0x07, 0x00 }, // " 0x22 34
{ 0x14, 0x7F, 0x14, 0x7F, 0x14 }, // # 0x23 35
{ 0x24, 0x2A, 0x7F, 0x2A, 0x12 }, // $ 0x24 36
{ 0x4C, 0x2C, 0x10, 0x68, 0x64 }, // % 0x25 37
{ 0x36, 0x49, 0x55, 0x22, 0x50 }, // & 0x26 38
{ 0x00, 0x05, 0x03, 0x00, 0x00 }, // ' 0x27 39
{ 0x00, 0x1C, 0x22, 0x41, 0x00 }, // ( 0x28 40
{ 0x00, 0x41, 0x22, 0x1C, 0x00 }, // ) 0x29 41
{ 0x14, 0x08, 0x3E, 0x08, 0x14 }, // * 0x2A 42
{ 0x08, 0x08, 0x3E, 0x08, 0x08 }, // + 0x2B 43
{ 0x00, 0x00, 0x50, 0x30, 0x00 }, // , 0x2C 44
{ 0x10, 0x10, 0x10, 0x10, 0x10 }, // - 0x2D 45
{ 0x00, 0x60, 0x60, 0x00, 0x00 }, // . 0x2E 46
{ 0x20, 0x10, 0x08, 0x04, 0x02 }, // / 0x2F 47
{ 0x3E, 0x51, 0x49, 0x45, 0x3E }, // 0 0x30 48
{ 0x00, 0x42, 0x7F, 0x40, 0x00 }, // 1 0x31 49
{ 0x42, 0x61, 0x51, 0x49, 0x46 }, // 2 0x32 50
{ 0x21, 0x41, 0x45, 0x4B, 0x31 }, // 3 0x33 51
{ 0x18, 0x14, 0x12, 0x7F, 0x10 }, // 4 0x34 52
{ 0x27, 0x45, 0x45, 0x45, 0x39 }, // 5 0x35 53
{ 0x3C, 0x4A, 0x49, 0x49, 0x30 }, // 6 0x36 54
{ 0x01, 0x71, 0x09, 0x05, 0x03 }, // 7 0x37 55
{ 0x36, 0x49, 0x49, 0x49, 0x36 }, // 8 0x38 56
{ 0x06, 0x49, 0x49, 0x29, 0x1E }, // 9 0x39 57
{ 0x00, 0x36, 0x36, 0x00, 0x00 }, // : 0x3A 58
{ 0x00, 0x56, 0x36, 0x00, 0x00 }, // ; 0x3B 59
{ 0x08, 0x14, 0x22, 0x41, 0x00 }, // < 0x3C 60
{ 0x14, 0x14, 0x14, 0x14, 0x14 }, // = 0x3D 61
{ 0x00, 0x41, 0x22, 0x14, 0x08 }, // > 0x3E 62
{ 0x02, 0x01, 0x51, 0x09, 0x06 }, // ? 0x3F 63
{ 0x3C, 0x42, 0x5A, 0x56, 0x0C }, // @ 0x40 64
{ 0x7E, 0x11, 0x11, 0x11, 0x7E }, // A 0x41 65
{ 0x7F, 0x49, 0x49, 0x49, 0x36 }, // B 0x42 66
{ 0x3E, 0x41, 0x41, 0x41, 0x22 }, // C 0x43 67
{ 0x7F, 0x41, 0x41, 0x22, 0x1C }, // D 0x44 68
{ 0x7F, 0x49, 0x49, 0x49, 0x41 }, // E 0x45 69
{ 0x7F, 0x09, 0x09, 0x09, 0x01 }, // F 0x46 70
{ 0x3E, 0x41, 0x49, 0x49, 0x7A }, // G 0x47 71
{ 0x7F, 0x08, 0x08, 0x08, 0x7F }, // H 0x48 72
{ 0x00, 0x41, 0x7F, 0x41, 0x00 }, // I 0x49 73
{ 0x20, 0x40, 0x41, 0x3F, 0x01 }, // J 0x4A 74
{ 0x7F, 0x08, 0x14, 0x22, 0x41 }, // K 0x4B 75
{ 0x7F, 0x40, 0x40, 0x40, 0x40 }, // L 0x4C 76
{ 0x7F, 0x02, 0x0C, 0x02, 0x7F }, // M 0x4D 77
{ 0x7F, 0x04, 0x08, 0x10, 0x7F }, // N 0x4E 78
{ 0x3E, 0x41, 0x41, 0x41, 0x3E }, // O 0x4F 79
{ 0x7F, 0x09, 0x09, 0x09, 0x06 }, // P 0x50 80
{ 0x3E, 0x41, 0x51, 0x21, 0x5E }, // Q 0x51 81
{ 0x7F, 0x09, 0x19, 0x29, 0x46 }, // R 0x52 82
{ 0x46, 0x49, 0x49, 0x49, 0x31 }, // S 0x53 83
{ 0x01, 0x01, 0x7F, 0x01, 0x01 }, // T 0x54 84
{ 0x3F, 0x40, 0x40, 0x40, 0x3F }, // U 0x55 85
{ 0x1F, 0x20, 0x40, 0x20, 0x1F }, // V 0x56 86
{ 0x3F, 0x40, 0x38, 0x40, 0x3F }, // W 0x57 87
{ 0x63, 0x14, 0x08, 0x14, 0x63 }, // X 0x58 88
{ 0x07, 0x08, 0x70, 0x08, 0x07 }, // Y 0x59 89
{ 0x61, 0x51, 0x49, 0x45, 0x43 }, // Z 0x5A 90
{ 0x00, 0x7F, 0x41, 0x41, 0x00 }, // [ 0x5B 91
{ 0x02, 0x04, 0x08, 0x10, 0x20 }, // 0x5C 92
{ 0x00, 0x41, 0x41, 0x7F, 0x00 }, // ] 0x5D 93
{ 0x04, 0x02, 0x01, 0x02, 0x04 }, // ^ 0x5E 94
{ 0x40, 0x40, 0x40, 0x40, 0x40 }, // _ 0x5F 95
{ 0x00, 0x01, 0x02, 0x04, 0x00 }, // ` 0x60 96
{ 0x20, 0x54, 0x54, 0x54, 0x78 }, // a 0x61 97
{ 0x7F, 0x48, 0x44, 0x44, 0x38 }, // b 0x62 98
{ 0x38, 0x44, 0x44, 0x44, 0x20 }, // c 0x63 99
{ 0x38, 0x44, 0x44, 0x48, 0x7F }, // d 0x64 100
{ 0x38, 0x54, 0x54, 0x54, 0x18 }, // e 0x65 101
{ 0x08, 0x7E, 0x09, 0x01, 0x02 }, // f 0x66 102
{ 0x0C, 0x52, 0x52, 0x52, 0x3E }, // g 0x67 103
{ 0x7F, 0x08, 0x04, 0x04, 0x78 }, // h 0x68 104
{ 0x00, 0x44, 0x7D, 0x40, 0x00 }, // i 0x69 105
{ 0x20, 0x40, 0x44, 0x3D, 0x00 }, // j 0x6A 106
{ 0x7F, 0x10, 0x28, 0x44, 0x00 }, // k 0x6B 107
{ 0x00, 0x41, 0x7F, 0x40, 0x00 }, // l 0x6C 108
{ 0x7C, 0x04, 0x18, 0x04, 0x78 }, // m 0x6D 109
{ 0x7C, 0x08, 0x04, 0x04, 0x78 }, // n 0x6E 110
{ 0x38, 0x44, 0x44, 0x44, 0x38 }, // o 0x6F 111
{ 0x7C, 0x14, 0x14, 0x14, 0x08 }, // p 0x70 112
{ 0x08, 0x14, 0x14, 0x18, 0x7C }, // q 0x71 113
{ 0x7C, 0x08, 0x04, 0x04, 0x08 }, // r 0x72 114
{ 0x48, 0x54, 0x54, 0x54, 0x20 }, // s 0x73 115
{ 0x04, 0x3F, 0x44, 0x40, 0x20 }, // t 0x74 116
{ 0x3C, 0x40, 0x40, 0x20, 0x7C }, // u 0x75 117
{ 0x1C, 0x20, 0x40, 0x20, 0x1C }, // v 0x76 118
{ 0x3C, 0x40, 0x30, 0x40, 0x3C }, // w 0x77 119
{ 0x44, 0x28, 0x10, 0x28, 0x44 }, // x 0x78 120
{ 0x0C, 0x50, 0x50, 0x50, 0x3C }, // y 0x79 121
{ 0x44, 0x64, 0x54, 0x4C, 0x44 }, // z 0x7A 122
{ 0x00, 0x08, 0x36, 0x41, 0x00 }, // { 0x7B 123
{ 0x00, 0x00, 0x7F, 0x00, 0x00 }, // | 0x7C 124
{ 0x00, 0x41, 0x36, 0x08, 0x00 }, // } 0x7D 125
{ 0x08, 0x04, 0x08, 0x10, 0x08 }, // ~ 0x7E 126
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // 0x7F 127
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // А 0x80 128
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Б 0x81 129
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // В 0x82 130
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Г 0x83 131
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Д 0x84 132
{ 0x40, 0x00, 0x40, 0x00, 0x40 }, // Е 0x85 133
{ 0x04, 0x04, 0xFF, 0x04, 0x04 }, // Ж 0x86 134
{ 0x24, 0x24, 0xFF, 0x24, 0x24 }, // З 0x87 135
{ 0x28, 0x7C, 0xAA, 0xAA, 0x82 }, // И 0x88 136
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Й 0x89 137
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // К 0x8A 138
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Л 0x8B 139
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // М 0x8C 140
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Н 0x8D 141
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // О 0x8E 142
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // П 0x8F 143
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Р 0x90 144
{ 0x00, 0x06, 0x05, 0x00, 0x00 }, // С 0x91 145
{ 0x00, 0x00, 0x05, 0x03, 0x00 }, // Т 0x92 146
{ 0x06, 0x05, 0x00, 0x06, 0x05 }, // У 0x93 147
{ 0x05, 0x03, 0x00, 0x05, 0x03 }, // Ф 0x94 148
{ 0x18, 0x3C, 0x3C, 0x3C, 0x18 }, // Х 0x95 149
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Ц 0x96 150
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Ч 0x97 151
{ 0x78, 0x48, 0x48, 0x78, 0x00 }, // Ш 0x98 152
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Щ 0x99 153
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Ъ 0x9A 154
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Ы 0x9B 155
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Ь 0x9C 156
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Э 0x9D 157
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Ю 0x9E 158
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // Я 0x9F 159
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // а 0xA0 160
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // б 0xA1 161
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // в 0xA2 162
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // г 0xA3 163
{ 0x22, 0x1C, 0x14, 0x1C, 0x22 }, // д 0xA4 164
{ 0x7E, 0x02, 0x02, 0x02, 0x03 }, // е 0xA5 165
{ 0x00, 0x00, 0xE7, 0x00, 0x00 }, // ж 0xA6 166
{ 0x4A, 0x95, 0xA5, 0xA9, 0x52 }, // з 0xA7 167
{ 0x7C, 0x55, 0x54, 0x45, 0x44 }, // и 0xA8 168
{ 0x00, 0x18, 0x24, 0x24, 0x00 }, // й 0xA9 169
{ 0x3E, 0x49, 0x49, 0x41, 0x22 }, // к 0xAA 170
{ 0x08, 0x14, 0x2A, 0x14, 0x22 }, // л 0xAB 171
{ 0x04, 0x04, 0x04, 0x04, 0x0C }, // м 0xAC 172
{ 0x00, 0x08, 0x08, 0x08, 0x00 }, // н 0xAD 173
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // о 0xAE 174
{ 0x00, 0x45, 0x7C, 0x45, 0x00 }, // п 0xAF 175
{ 0x00, 0x04, 0x0A, 0x04, 0x00 }, // ░ 0xB0 176
{ 0x44, 0x44, 0x5F, 0x44, 0x44 }, // ▒ 0xB1 177
{ 0x00, 0x41, 0x7F, 0x41, 0x00 }, // ▓ 0xB2 178
{ 0x00, 0x00, 0x7A, 0x00, 0x00 }, // │ 0xB3 179
{ 0x00, 0x78, 0x08, 0x0C, 0x00 }, // ┤ 0xB4 180
{ 0x00, 0xFC, 0x20, 0x3C, 0x20 }, // ╡ 0xB5 181
{ 0x0C, 0x1E, 0xFE, 0x02, 0xFE }, // ╢ 0xB6 182
{ 0x00, 0x18, 0x18, 0x00, 0x00 }, // ╖ 0xB7 183
{ 0x39, 0x54, 0x54, 0x49, 0x00 }, // ╕ 0xB8 184
{ 0x78, 0x10, 0x20, 0x7B, 0x03 }, // ╣ 0xB9 185
{ 0x38, 0x54, 0x54, 0x44, 0x00 }, // ║ 0xBA 186
{ 0x22, 0x14, 0x2A, 0x14, 0x08 }, // ╗ 0xBB 187
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // ╝ 0xBC 188
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // ╜ 0xBD 189
{ 0x00, 0x00, 0x00, 0x00, 0x00 }, // ╛ 0xBE 190
{ 0x00, 0x54, 0x70, 0x44, 0x00 }, // ┐ 0xBF 191
{ 0x7C, 0x12, 0x11, 0x12, 0x7C }, // └ 0xC0 192
{ 0x7F, 0x49, 0x49, 0x49, 0x31 }, // ┴ 0xC1 193
{ 0x7F, 0x49, 0x49, 0x49, 0x36 }, // ┬ 0xC2 194
{ 0x7F, 0x01, 0x01, 0x01, 0x01 }, // ├ 0xC3 195
{ 0x60, 0x3F, 0x21, 0x3F, 0x60 }, // ─ 0xC4 196
{ 0x7F, 0x49, 0x49, 0x49, 0x41 }, // ┼ 0xC5 197
{ 0x77, 0x08, 0x7F, 0x08, 0x77 }, // ╞ 0xC6 198
{ 0x22, 0x41, 0x49, 0x49, 0x36 }, // ╟ 0xC7 199
{ 0x7F, 0x10, 0x08, 0x04, 0x7F }, // ╚ 0xC8 200
{ 0x7E, 0x10, 0x09, 0x04, 0x7E }, // ╔ 0xC9 201
{ 0x7F, 0x08, 0x14, 0x22, 0x41 }, // ╩ 0xCA 202
{ 0x40, 0x3E, 0x01, 0x01, 0x7F }, // ╦ 0xCB 203
{ 0x7F, 0x02, 0x0C, 0x02, 0x7F }, // ╠ 0xCC 204
{ 0x7F, 0x08, 0x08, 0x08, 0x7F }, // ═ 0xCD 205
{ 0x3E, 0x41, 0x41, 0x41, 0x3E }, // ╬ 0xCE 206
{ 0x7F, 0x01, 0x01, 0x01, 0x7F }, // ╧ 0xCF 207
{ 0x7F, 0x09, 0x09, 0x09, 0x06 }, // ╨ 0xD0 208
{ 0x3E, 0x41, 0x41, 0x41, 0x22 }, // ╤ 0xD1 209
{ 0x01, 0x01, 0x7F, 0x01, 0x01 }, // ╥ 0xD2 210
{ 0x07, 0x48, 0x48, 0x48, 0x3F }, // ╙ 0xD3 211
{ 0x0E, 0x11, 0x7F, 0x11, 0x0E }, // ╘ 0xD4 212
{ 0x63, 0x14, 0x08, 0x14, 0x63 }, // ╒ 0xD5 213
{ 0x3F, 0x20, 0x20, 0x3F, 0x60 }, // ╓ 0xD6 214
{ 0x07, 0x08, 0x08, 0x08, 0x7F }, // ╫ 0xD7 215
{ 0x7F, 0x40, 0x7E, 0x40, 0x7F }, // ╪ 0xD8 216
{ 0x3F, 0x20, 0x3F, 0x20, 0x7F }, // ┘ 0xD9 217
{ 0x01, 0x7F, 0x48, 0x48, 0x30 }, // ┌ 0xDA 218
{ 0x7F, 0x48, 0x30, 0x00, 0x7F }, // █ 0xDB 219
{ 0x00, 0x7F, 0x48, 0x48, 0x30 }, // ▄ 0xDC 220
{ 0x22, 0x41, 0x49, 0x49, 0x3E }, // ▌ 0xDD 221
{ 0x7F, 0x08, 0x3E, 0x41, 0x3E }, // ▐ 0xDE 222
{ 0x46, 0x29, 0x19, 0x09, 0x7F }, // ▀ 0xDF 223
{ 0x20, 0x54, 0x54, 0x54, 0x78 }, // р 0xE0 224
{ 0x3C, 0x4A, 0x4A, 0x4A, 0x31 }, // с 0xE1 225
{ 0x7C, 0x54, 0x54, 0x28, 0x00 }, // т 0xE2 226
{ 0x7C, 0x04, 0x04, 0x0C, 0x00 }, // у 0xE3 227
{ 0x60, 0x3C, 0x24, 0x3C, 0x60 }, // ф 0xE4 228
{ 0x38, 0x54, 0x54, 0x54, 0x18 }, // х 0xE5 229
{ 0x6C, 0x10, 0x7C, 0x10, 0x6C }, // ц 0xE6 230
{ 0x00, 0x44, 0x54, 0x54, 0x28 }, // ч 0xE7 231
{ 0x7C, 0x20, 0x10, 0x08, 0x7C }, // ш 0xE8 232
{ 0x7C, 0x21, 0x12, 0x09, 0x7C }, // щ 0xE9 233
{ 0x7C, 0x10, 0x28, 0x44, 0x00 }, // ъ 0xEA 234
{ 0x40, 0x38, 0x04, 0x04, 0x7C }, // ы 0xEB 235
{ 0x7C, 0x08, 0x10, 0x08, 0x7C }, // ь 0xEC 236
{ 0x7C, 0x10, 0x10, 0x10, 0x7C }, // э 0xED 237
{ 0x38, 0x44, 0x44, 0x44, 0x38 }, // ю 0xEE 238
{ 0x7C, 0x04, 0x04, 0x04, 0x7C }, // я 0xEF 239
{ 0x7C, 0x14, 0x14, 0x14, 0x08 }, // Ё 0xF0 240
{ 0x38, 0x44, 0x44, 0x44, 0x00 }, // ё 0xF1 241
{ 0x04, 0x04, 0x7C, 0x04, 0x04 }, // Є 0xF2 242
{ 0x0C, 0x50, 0x50, 0x50, 0x3C }, // є 0xF3 243
{ 0x08, 0x14, 0x7C, 0x14, 0x08 }, // Ї 0xF4 244
{ 0x44, 0x28, 0x10, 0x28, 0x44 }, // ї 0xF5 245
{ 0x3C, 0x20, 0x20, 0x3C, 0x60 }, // Ў 0xF6 246
{ 0x0C, 0x10, 0x10, 0x10, 0x7C }, // ў 0xF7 247
{ 0x7C, 0x40, 0x7C, 0x40, 0x7C }, // ° 0xF8 248
{ 0x3C, 0x20, 0x3C, 0x20, 0x7C }, // ∙ 0xF9 249
{ 0x04, 0x7C, 0x50, 0x50, 0x20 }, // · 0xFA 250
{ 0x7C, 0x50, 0x20, 0x00, 0x7C }, // √ 0xFB 251
{ 0x00, 0x7C, 0x50, 0x50, 0x20 }, // № 0xFC 252
{ 0x28, 0x44, 0x54, 0x54, 0x38 }, // ¤ 0xFD 253
{ 0x7C, 0x10, 0x38, 0x44, 0x38 }, // ■ 0xFE 254
{ 0x48, 0x54, 0x34, 0x14, 0x7C } // 0xFF 255
};
Шрифт имеет размер 8*5 и по виду очень напоминает тот, что используется в дисплеях на HD44780.
Ну что же, разберёмся, как это работает.
Самой важной является, естественно, загрузка данных в ТПГ. Здесь это реализовано так:
int loadToPrinter_1Dot(uint8_t val)
{
countDots++;
digitalWrite(TH_CLOCK, LOW);
delayMicroseconds(1);
if (val == 1)
{
digitalWrite(TH_DATA, HIGH);
}
else
{
digitalWrite(TH_DATA, LOW);
}
delayMicroseconds(1);
digitalWrite(TH_CLOCK, HIGH);
delayMicroseconds(1);
// если заполнились 64 точки , то надо просто защелкнуть LAT
if ( fmod (countDots, 64) == 0) // 64 новый STB
{
sendToPrinter_LAT();
if (currentStb == 7) return 1;
currentStb++;
}
return 0;
}
Загружаем мы не массив байт, а просто одну-единственную точку: при печати текста закидывать значения в буфер головки напрямую оказывается удобнее, нежели вначале подготавливать строки, а потом их печатать.
Здесь мы увеличиваем счётчик числа загруженных точек, после чего отправляем значение на входе в ТПГ: опускаем CLOCK, выставляем нужное значение на выходе DATA, а затем вновь поднимаем CLOCK. Линейка нагревательных резисторов поделена на шесть промежутков по шестьдесят четыре точки, поэтому при заполнении очередной группы необходимо дёрнуть LATCH, чтобы принтер сохранил эти данные. Если места уже не осталось (то есть последняя группа забита), функция возвращает единицу, иначе — ноль.
После того, как все данные загружены, их необходимо прожечь: с поднятым LATCH активировать строб.
void burn1Line384Dots()
{
digitalWrite(TH_STROBE, HIGH);
delayMicroseconds(HEATING_TIME);
digitalWrite(TH_STROBE, LOW);
delayMicroseconds(COOLING_TIME);
heatOff();
}
void heatOff()
{
digitalWrite(TH_LATCH, HIGH);
digitalWrite(TH_STROBE, LOW);
}
Тут имеют значение два параметра: HEATING_TIME и COOLING_TIME. Первый из них определяет, собственно, время, которое потребуется на прожиг. Он напрямую влияет на яркость печати. Также это один из самых критичных параметров в программе: сама головка не имеет никаких защит и регулировок и при слишком большом значении просто перегреется и сдохнет. Второй параметр отвечает за время ожидания после отключения прожарки. Дело в том, что нагреватели не остывают мгновенно, поэтому, если сразу проматывать бумагу дальше, на изображении будут чёрные полосы. Поэтому необходимо дать им остыть.
Далее проматываем бумагу:
void motorStep()
{
digitalWrite(MOT_EN, LOW);
for (int k = 0; k < STEPS_REQUIRED; k++) {
digitalWrite(STEP, LOW);
delayMicroseconds(MOTOR_DELAY);
digitalWrite(STEP, HIGH);
}
digitalWrite(MOT_EN, HIGH);
}
void paperFeed(uint8_t mode) {
int i;
switch (mode) {
case LINE_FEED:
for (i = 0; i < LINE_INTERVAL; i++) {
motorStep();
}
break;
case FORM_FEED:
for (i = 0; i < FORM_INTERVAL; i++) {
motorStep();
}
break;
case PIXEL_FEED:
motorStep();
break;
}
}
Тут всё крайне просто — типичное управление STEP/DIR-драйвером. Значение STEPS_REQUIRED — число шагов, необходимых для того, чтобы промотать бумагу на расстояние одного пикселя. Для разных механизмов эти значения могут отличаться.
❯ Шрифт
По сути представленных ранее трёх функций уже достаточно, чтобы заставить головку прожигать нужный набор точек. Но, конечно, хочется чего-то поинтереснее, например, печати текста. Разберёмся и с этим.
Таблица со шрифтами представляет собой двумерный массив, где адресами являются коды символов и номера столбцов. Под каждый символ отведено пять байт, каждый из который кодирует вертикальную линию в восемь пикселей. Соответственно, для печати необходимо получить строку, далее разделить её на восемь линий, загнать каждую в принтер, а затем прожечь и промотать бумагу. Остановимся поподробнее на первых двух этапах:
void printString(String target)
{
currentPrinting = target;
for (int jj = 0; jj < 8; jj++)
{
print384DotsRow(jj);
burn1Line384Dots();
paperFeed(PIXEL_FEED);
}
currentPrinting = "";
}
void print384DotsRow(uint8_t row)
{
currentStb = 1;
int nextSymbol = 0;
int len = currentPrinting.length();
unsigned char code = currentPrinting[nextSymbol];
unsigned char horizontalPosition = 0;
unsigned char vertical8dots;
countDots = 1;
int maxSymbols = min(round(64 / WIDTH_SCALE), len);
int symbolWidth = (int)(6 * WIDTH_SCALE);
while (countDots <= 384)
{
if ( fmod (count2, 6) == 0) // 6 точек - 1 символ (точек : 5 + 1 на разделитель)
{
nextSymbol++;
if (nextSymbol <= maxSymbols)
{
code = currentPrinting[nextSymbol];
}
else
{
code = char(' '); // оставшуюся пустую часть забиваем пробелами
}
// добавляем разделитель, т.е. пустую точку
for (int dd = 1; dd <= WIDTH_SCALE; dd++)
{
if (loadToPrinter_1Dot(0) == 1)
return;
}
count2++;
horizontalPosition = 0;
}
else
{
vertical8dots = pgm_read_byte(&FontTable[code][horizontalPosition]);
int vv = (vertical8dots >> row) & 0x01; //
for (int dd = 1; dd <= WIDTH_SCALE; dd++)
{
if (loadToPrinter_1Dot(vv) == 1)
return;
}
count2++;
horizontalPosition++;
}
}
return;
}
Итак, для печати мы копируем строку в буфер, а затем восемь раз (для каждой из восьми горизонтальных линий, из которых состоит строка символов) вызываем функцию для загрузки данных в ТПГ. Далее очередная линия прожигается, а бумага — проматывается.
Теперь надо преобразовать эти самые символы в набор горизонтальных линий для печати. Для начала необходимо получить длину строки и рассчитать число символов на ленте. Если оно превышает максимальное число, которое может влезть, то лишние просто отбрасываются. Если же их меньше, то пустое пространство забивается пробелами. Далее из Flash считывается байт, представляющий собой одну вертикальную линию размером в восемь точек, соответствующий символу с нужным кодом. При помощи битовых операций получается нужное значение точки в данной строке, которое закидывается в ТПГ. Тут используется параметр WIDTH_SCALE, предназначенный для того, чтобы регулировать ширину шрифта. Всё просто: чему равно это значение, столько раз и будет загружен каждый бит, а также, соответственно, во столько же раз меньше символов уместится на одной строке ленты. После этого необходимо отправить пустую точку. Это разделитель, нужный для того, чтобы символы не стояли впритык.
Операция эта повторяется восемь раз, после каждого раза печатается очередная строка.
❯ Печать картинок
С текстом разобрались. Попробуем теперь напечатать какое-нибудь изображение.
Как ни странно, это куда проще: достаточно писать в ТПГ просто голые строки пикселей, не нужно заниматься преобразованием символов и подобными манипуляциями.
Делается это примерно так:
void loadLine(uint8_t * newLine) {
countDots = 1;
for (int i = 0; i < 48; i++) {
for (int j = 0; j < 8; j++) {
loadToPrinter_1Dot((newLine[i] >> j) & 1);
}
}
}
Думаю, пояснений это не требует — загружаем за раз сорок восемь байт, представляющих собой данные одной строки пикселей. Можно, к примеру, построчно получать картинку из последовательного порта, а можно печатать из массива в памяти контроллера. Последнее делается примерно так:
void printImageFromPROGMEM(uint8_t * image, int height) {
uint8_t temp[48];
for(int i = 0; i < height; i++) {
for(int j = 0; j < 48; j++) temp[j] = pgm_read_byte(&image[48*i + j]);
loadLine(temp);
burn1Line384Dots();
paperFeed(PIXEL_FEED);
}
paperFeed(FORM_FEED);
}
Конвертируем изображение при помощи того же LCDAssistant, загружаем в память МК и получаем примерно следующее:
Работает.
❯ Вот как-то так
Итак, как можно заметить, подключить термоголовку на самом деле не так уж и сложно, даже несмотря на все нюансы. Где применять такой девайс, разумеется, решать только вам. Но даже просто поиграться с данной железкой может оказаться очень интересно.
Такие дела.
Автор: Лев