В прошлой своей публикации я подключал ЖКИ дисплей от старого кассового аппарата. Напомню, что я приобрел 3 аппарата за смешные деньги, разобрал их, и в итоге стал обладателем милых сердцу электронных штучек: экраны, принтеры, мелочевки....;)
В комментариях люди интересовались подключением чекового принтера. Выкроив время из своей межвахты наконец то разобрался с принтером. Начал я с матричного. Модель MD910, ниже фото.
Ну поскольку Ардуинка, паяльник, и прочая-прочая давным давно ждут своего выхода на сцену решил я таки его неприкаенного подключить и что-нибудь напечатать. Что самое главное в нашей жизни? Это инструкция! Так вот озадачился я поиском datasheet-а на этот принтер. Нагуглил, правда самой первой редакции. Там нашлась схема распиновки выводов, тайминги, немного про устройство принтера. Не было параметров подключения светодиода оптопары и еще нескольких данных. На ум пришла идея спросить эту информацию в мастерских по ремонту кассовых аппаратов. Здесь, я Вам скажу меня ждало разочарование — эти парни лениво протянули, что у них никаких datasheet-ов, сервисных manual-ов и даже схем кассовых аппаратов у них нет, и вообще они тут делом заняты...;)
А если спросить эту информацию непосредственно у самих производителей (Citizen Business Machines)? Я так и сделал — написал им имейл — так мол и так, я радиолюбитель, сейчас хочу прикрутить этот принтер и печатать на нем листовки, будьте добры и любезны предоставьте datasheet. И Citizen Systems Europe мне через пару дней прислало заправшиваемую информацию!
Собрал платы подключения датчиков — а их там два: Dot Pulse и Reset Pulse. Спаял драйвера для управления двигателем и печатающей головкой.
Цифры обозначают к каким выводам принтера подключены эти точки. Поскольку на входы Ардуино подаются инвертированные сигналы (например 1 в случае, если выключатель разомкнут), то при написании программы необходимо учитывать этот момент.
Что касается драйверов для мотора и печатающей головки. В загашниках лежало несколько микросхем SMA4033 и STA471A, которые были выпаяны из неисправного матричного принтера Эпсон (типа FX800). Вот перипетии судеб микросхем — старый матричный принтер был разобран на запчасти, чтобы через несколько лет реинкарнироваться в облике нового принтера! ;)
Документация была найдена при первом же запросе Гугла (кстати, я их выложил на GitHub). Эти микросхемы представляют собой 4 транзистора Дарлингтона в едином корпусе, разница между ними (кроме напряжений питания) в наличии защитных диодов в SMA4033. Мне они очень понравились — отличные параметры, можно приклеить на радиатор и просто припаять проводки к выводам, корпус относительно массивный, так что легко выдерживает выпайку при помощи строительного фена! ;)
Схема подключения мотора. Используется только два канала из четырех микросхема SMA4033.
Схема подключения печатающей головки к микросхемам STA471A (коллекторы). Необходимо помнить, что печатающая голова состоит из 2 блоков по 4 иголки. Поэтому нам нужно 8 силовых выходов.
Общее для обоих микросхем. Выходные пины Ардуино через резисторы сопротивление (680 ом — 1к) подключены на базы транзисторов Дарлингтона.
#define b1stHead_D 8
#define b1stHead_B 9
#define b1stHead_A 10
#define b1stHead_C 11
#define b2ndHead_H 4
#define b2ndHead_F 5
#define b2ndHead_E 6
#define b2ndHead_G 7
#define Motor 13
#define Feed 12
#define DotPulse 3
#define ResetPulse 2
Как работает принтер?
Печатающая головка состоит из двух одинаковых частей. Каждая часть включает в себя четыре вертикально стоящие иголки. Однако четные и нечетные иголки чуть-чуть сдвинуты относительно друг друга, полагаю, чтобы они не сильно мешали друг-другу — ведь расстояние между ними совсем крошечное! Однако это немного усложняет алгоритм печати: вначале нужно напечатать нечетные точки, потом, дождавшись, когда головка сдвинется на 0.5 точки напечатать четные точки.
Что касается двух половинок. Левая часть печатающей головки печатает первую половину строки, правая — вторую. Чтобы напечатать строку из 8 пикселей высотой нужно сделать два прохода. Вот смотрите, за четыре первых такта печатается один первый столбец буквы A. Первый такт — печатаем точки A и C, потом 1 такт — головка сдвигается на половину точки, потом печатаются точки B и D, потом опять сдвигается на полточки. Потом опять за четыре такта печатается следующий столбец буквы A.
При этом в принтере всего 144 столбца, 72 для каждой части печатающей головки. Если будем использовать шрифт 8x8, то мы сможем напечатать 18 символов в каждой строке.
Мы выяснили каким образом печатает принтер. Теперь это дело нужно оформить в виде программного кода.
Что нам нужно, чтобы напечатать текст?
1) входная информация — строка из 18-ти символов;
2) шрифт
3) программа
Большинство растровых шрифтов устроено следующим образом: каждый байт (или несколько байтов) описывает одну строчку символа, следующий — следующую строчку и т.д. Это удобно для отображения на экране — поскольку используется построковое построение изображения. В случае печати, иголочки стоят вертикально. Поэтому нам был бы удобен шрифт, который описан по стоблцам. Однако, такого я не нашел. Пришлось исходить из того, что есть.
Кроме того, я решил печать организовать следующим образом: первый шаг — это рендеринг строки текста в буфер, второй шаг — это непосредственно печать подготовленной информации из буфера рендеринга. Это также дает некоторую уверенность в том, что тайминги печати будут соблюдены. Написание кода рендеринга было самой сложной частью программы.
Давайте посмотрим, как работает рендеринг
Здесь красным цветом выделены биты и байты экранного шрифта, зеленым цветом — биты и байты шрифта для принтера. Основная задача рендеринга — сконвертировать экранный шрифт в принтерный в буфер. Обработка выглядит примерно так: все D7-ые биты (всех 8-и байтов шрифта) необходимо превратить в биты D7-D0 (первый столбец), все D6-ые биты необходимо превратить в биты D7-D0 (второй столбец). Таким образом экранный шрифт для латинской буквы A (0x30, 0x78, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0x00) превратится в последовательность (0x3E, 0x7E, 0xC8, 0xC8, 0x7E, 0x3E, 0x00, 0x00) для буфера рендеринга. Надеюсь, что приведенный ниже кусок кода поможет понять алгоритм рендеринга.
С еще одной большой проблемой я боролся целый вечер: при попытке отработки кода рендеринга — в терминал выбрасывался мусор, если закомментировать этот участок, все работало без проблем. Мне показалось, что переполняется/переписывается оперативная память и поэтому возникает мусор в выдаче. Прочитав про то, что Ардуино хранит все переменные в ОЗУ я понял, что всему вина — это 2 килобайта данных шрифта. Пришлось хранить его непосредственно в теле программы (флэш) и обращаться через специальные функции. Все заработало. Здесь и здесь более подробно об этом.
// FontData=pgm_read_byte(&(CP1251Font[Address]));
// https://www.arduino.cc/reference/en/language/variables/utilities/progmem/
// к сожалению оперативная память всего 1К поэтому фонт размещен в программной памяти
// http://www.nongnu.org/avr-libc/user-manual/pgmspace.html
// длина печатаемой строки 18 символов (144 / 8) +1 байт на символ конца строки = 19 байт
void MD910_RenderPrintStr(char String2Print[19])
{
byte TmpVal[8], MaskBit[8];
byte i,j,k, Code, Column, Value;
word FontStart;
//***************************************************************
Column=0; // индекс на MD910_Buffer[]
for(j=0;j<=17;j++) // отрабатываем каждый символ
{
Code=byte(String2Print[j]);
FontStart=Code*8; // индекс, указывающий на начало данных шрифта в CP1251Font[]
for (k=0; k<=7; k++)
{
TmpVal[k]=pgm_read_byte(&(CP1251Font[FontStart+k])); // готовлю временный буфер (0)
};
for(i=0;i<=7;i++)
{
for (k=0; k<=7; k++)
{
MaskBit[k]=(TmpVal[k] & 128) >> k; // выделяю старший бит и сохраняю его в матрице масок (1)
};
Value=0;
for (k=0; k<=7; k++)
{
Value=(Value | MaskBit[k]); // матрица масок превращается в одну маску (2)
};
MD910_Buffer[Column]=Value; // столбец сформирован из общей маски (3)
for (k=0; k<=7; k++)
{
TmpVal[k]=TmpVal[k] << 1; // сдвигаю данные временного буфера (4)
};
Column++; // переходим к обработке следующей колонке
};
};
//***************************************************************
};
void MD910_PrintBuffer()
{
byte PinA, PinB, PinC, PinD, PinE, PinF, PinG, PinH;
Serial.println("Printing....");
//***********************************************************************************************************
DP_Count=0;
// печатаем верхнюю часть строки ***************************
if (RP_Status()==false)
{
for(byte j=0;j<=71;j++)
{
PinA=MD910_Buffer[j] & 0x80;
PinB=MD910_Buffer[j] & 0x40;
PinC=MD910_Buffer[j] & 0x20;
PinD=MD910_Buffer[j] & 0x10;
PinE=MD910_Buffer[j+72] & 0x80;
PinF=MD910_Buffer[j+72] & 0x40;
PinG=MD910_Buffer[j+72] & 0x20;
PinH=MD910_Buffer[j+72] & 0x10;
// 1 такт
// можно включать точки A,C и E,G (нечетные)
if (PinA>0) digitalWrite(b1stHead_A, HIGH); //
if (PinC>0) digitalWrite(b1stHead_C, HIGH); //
if (PinE>0) digitalWrite(b2ndHead_E, HIGH); //
if (PinG>0) digitalWrite(b2ndHead_G, HIGH); //
while (DP_Status()==true) {};
while (DP_Status()==false) {};
DP_Count++;
digitalWrite(b1stHead_A, LOW);
digitalWrite(b1stHead_C, LOW);
digitalWrite(b2ndHead_E, LOW);
digitalWrite(b2ndHead_G, LOW);
while (DP_Status()==true) {};
while (DP_Status()==false) {};
DP_Count++;
// 2 такт
// можно включать точки B,D и F,H (четные)
if (PinB>0) digitalWrite(b1stHead_B, HIGH); //
if (PinD>0) digitalWrite(b1stHead_D, HIGH); //
if (PinF>0) digitalWrite(b2ndHead_F, HIGH); //
if (PinH>0) digitalWrite(b2ndHead_H, HIGH); //
while (DP_Status()==true) {
};
while (DP_Status()==false) {
};
DP_Count++;
digitalWrite(b1stHead_B, LOW);
digitalWrite(b1stHead_D, LOW);
digitalWrite(b2ndHead_F, LOW);
digitalWrite(b2ndHead_H, LOW);
while (DP_Status()==true) {};
while (DP_Status()==false) {};
DP_Count++;
};
}
while (RP_Status()==false) {}; // ждем когда головка переместится в начало
while (RP_Status()==true) {}; // теперь ждем когда головка переместится в начало области печати
// печатаем нижнюю часть строки ***************************
if (RP_Status()==false)
{
for(byte j=0;j<=71;j++)
{
PinA=MD910_Buffer[j] & 0x08;
PinB=MD910_Buffer[j] & 0x04;
PinC=MD910_Buffer[j] & 0x02;
PinD=MD910_Buffer[j] & 0x01;
PinE=MD910_Buffer[j+72] & 0x08;
PinF=MD910_Buffer[j+72] & 0x04;
PinG=MD910_Buffer[j+72] & 0x02;
PinH=MD910_Buffer[j+72] & 0x01;
// 1 такт
// можно включать точки A,C и E,G (нечетные)
if (PinA>0) digitalWrite(b1stHead_A, HIGH); //
if (PinC>0) digitalWrite(b1stHead_C, HIGH); //
if (PinE>0) digitalWrite(b2ndHead_E, HIGH); //
if (PinG>0) digitalWrite(b2ndHead_G, HIGH); //
while (DP_Status()==true) {};
while (DP_Status()==false) {};
DP_Count++;
digitalWrite(b1stHead_A, LOW);
digitalWrite(b1stHead_C, LOW);
digitalWrite(b2ndHead_E, LOW);
digitalWrite(b2ndHead_G, LOW);
while (DP_Status()==true) {};
while (DP_Status()==false) {};
DP_Count++;
// 2 такт
// можно включать точки B,D и F,H (четные)
if (PinB>0) digitalWrite(b1stHead_B, HIGH); //
if (PinD>0) digitalWrite(b1stHead_D, HIGH); //
if (PinF>0) digitalWrite(b2ndHead_F, HIGH); //
if (PinH>0) digitalWrite(b2ndHead_H, HIGH); //
while (DP_Status()==true) {
};
while (DP_Status()==false) {
};
DP_Count++;
digitalWrite(b1stHead_B, LOW);
digitalWrite(b1stHead_D, LOW);
digitalWrite(b2ndHead_F, LOW);
digitalWrite(b2ndHead_H, LOW);
while (DP_Status()==true) {};
while (DP_Status()==false) {};
DP_Count++;
};
}
while (RP_Status()==false) {};
while (RP_Status()==true) {};
//***********************************************************************************************************
Serial.println("Done!");
}
Еще интересный сюрприз преподнес мне Power Bank от Xiaom, который я планировал использовать как источник питания. Просто он не включался от нагрузки в виде Ардуино, при насильном включении (нажав на кнопку) он включался, питал нагрузку пару секунд, потом отрубался. Причина — думаю одна: Ардуинка не так много потребляет, моторчики и печатающая головка (основной потребитель) тянет импульсами, но не постоянно (нагрузка скачет от десятков миллиампер до пары ампер)…
Пришлось городить блок питания из 12-ти вольтового 5-ти амперного блока питания для светодиодной ленты + 2 DC-DC конвертера на народных LM2596. В выходную цепь +5 вольт я включил по диоду Шоттки + резистор 2,5 ома для ограничения токов.
Поскольку принтер матричный, для печати используется картридж с лентой, да-да, как в старину. Я пытался попробовать восстановить родной картридж, замочил его в воде на недельку. Попробовал разобрать, чтобы пропитать поролон краской-мастикой. Поролон просто рассыпался… Ближайший картридж от меня находится в Самаре. :(
Решил тогда попробовать найти бумагу, чувствительную к ударам (когда ее покупал, продавец пару раз меня предупредил, что эта лента не подходит никуда ;) Пришлось ее заверять, что я все понял, и претензий потом от меня не будет...;) ). Нашел только ленту шириной 80 мм, пришлось резать на кусочки и уменьшать ширину до 57 мм, добрым старым ламповым способом при помощи шариковой ручки и линейки… Зато печатает! ;)
Как выглядит финальный результат. На 20 мм фанерке закреплен принтер и платы управления. При монтаже использован ШВВП 2*0.5мм, коннекторы WAGO и клеммник! ;) МГТФ кончился давным-давно… И его вообще не могу найти в продаже у себя в городе. :(
Видео с демонстрацией работы принтера.
Спасибо за внимание!
Автор: Baurzhan A. Duisaliev