Как подключить матричный принтер MD910 от кассового аппарата Миника

В прошлой своей публикации я подключал ЖКИ дисплей от старого кассового аппарата. Напомню, что я приобрел 3 аппарата за смешные деньги, разобрал их, и в итоге стал обладателем милых сердцу электронных штучек: экраны, принтеры, мелочевки…;)

В комментариях люди интересовались подключением чекового принтера. Выкроив время из своей межвахты наконец то разобрался с принтером. Начал я с матричного. Модель MD910, ниже фото.

fsq4hbgduua_ert7ctt6ekmohle.jpeg
3n5f2mjjzj9e0jmgge_lqfwwzpk.jpeg

Ну поскольку Ардуинка, паяльник, и прочая-прочая давным давно ждут своего выхода на сцену решил я таки его неприкаенного подключить и что-нибудь напечатать. Что самое главное в нашей жизни? Это инструкция! Так вот озадачился я поиском datasheet-а на этот принтер. Нагуглил, правда самой первой редакции. Там нашлась схема распиновки выводов, тайминги, немного про устройство принтера. Не было параметров подключения светодиода оптопары и еще нескольких данных. На ум пришла идея спросить эту информацию в мастерских по ремонту кассовых аппаратов. Здесь, я Вам скажу меня ждало разочарование — эти парни лениво протянули, что у них никаких datasheet-ов, сервисных manual-ов и даже схем кассовых аппаратов у них нет, и вообще они тут делом заняты…;)

А если спросить эту информацию непосредственно у самих производителей (Citizen Business Machines)? Я так и сделал — написал им имейл — так мол и так, я радиолюбитель, сейчас хочу прикрутить этот принтер и печатать на нем листовки, будьте добры и любезны предоставьте datasheet. И Citizen Systems Europe мне через пару дней прислало заправшиваемую информацию!

Собрал платы подключения датчиков —, а их там два: Dot Pulse и Reset Pulse. Спаял драйвера для управления двигателем и печатающей головкой.

Схемы подключения датчиков.
8zp3-gc1cv9mapr6k5fardqjvww.png
Цифры обозначают к каким выводам принтера подключены эти точки. Поскольку на входы Ардуино подаются инвертированные сигналы (например 1 в случае, если выключатель разомкнут), то при написании программы необходимо учитывать этот момент.


Что касается драйверов для мотора и печатающей головки. В загашниках лежало несколько микросхем SMA4033 и STA471A, которые были выпаяны из неисправного матричного принтера Эпсон (типа FX800). Вот перипетии судеб микросхем — старый матричный принтер был разобран на запчасти, чтобы через несколько лет реинкарнироваться в облике нового принтера! ;)
Документация была найдена при первом же запросе Гугла (кстати, я их выложил на GitHub). Эти микросхемы представляют собой 4 транзистора Дарлингтона в едином корпусе, разница между ними (кроме напряжений питания) в наличии защитных диодов в SMA4033. Мне они очень понравились — отличные параметры, можно приклеить на радиатор и просто припаять проводки к выводам, корпус относительно массивный, так что легко выдерживает выпайку при помощи строительного фена! ;)

Схемы драйверов мотора и печатающей головки
iy-hg4ttbbz7gom3pc68z1efhhy.png
Схема подключения мотора. Используется только два канала из четырех микросхема SMA4033.

j_rwjlhxtxf6dva6r6xvp_qrals.png
Схема подключения печатающей головки к микросхемам STA471A (коллекторы). Необходимо помнить, что печатающая голова состоит из 2 блоков по 4 иголки. Поэтому нам нужно 8 силовых выходов.

Общее для обоих микросхем. Выходные пины Ардуино через резисторы сопротивление (680 ом — 1к) подключены на базы транзисторов Дарлингтона.


Схема распиновки принтера
2uychmol08tuufh4imhegmp3g_0.jpeg


Как это все подключено к Ардуине?
#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



Как работает принтер?
6tvrro_jiyvfcbmkoav_zlzkj6i.png
Печатающая головка состоит из двух одинаковых частей. Каждая часть включает в себя четыре вертикально стоящие иголки. Однако четные и нечетные иголки чуть-чуть сдвинуты относительно друг друга, полагаю, чтобы они не сильно мешали друг-другу — ведь расстояние между ними совсем крошечное! Однако это немного усложняет алгоритм печати: вначале нужно напечатать нечетные точки, потом, дождавшись, когда головка сдвинется на 0.5 точки напечатать четные точки.

yjg7owhvqdcgrtcrrifilki44wm.png
Что касается двух половинок. Левая часть печатающей головки печатает первую половину строки, правая — вторую. Чтобы напечатать строку из 8 пикселей высотой нужно сделать два прохода. Вот смотрите, за четыре первых такта печатается один первый столбец буквы A. Первый такт — печатаем точки A и C, потом 1 такт — головка сдвигается на половину точки, потом печатаются точки B и D, потом опять сдвигается на полточки. Потом опять за четыре такта печатается следующий столбец буквы A.

7v9egqkw9yvrjfgvhn428zaimtw.png

При этом в принтере всего 144 столбца, 72 для каждой части печатающей головки. Если будем использовать шрифт 8×8, то мы сможем напечатать 18 символов в каждой строке.

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

Что нам нужно, чтобы напечатать текст?

1) входная информация — строка из 18-ти символов;
2) шрифт
3) программа

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

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

Давайте посмотрим, как работает рендеринг
ucrcai06p99ispwzodho5vlibzq.jpeg
Здесь красным цветом выделены биты и байты экранного шрифта, зеленым цветом — биты и байты шрифта для принтера. Основная задача рендеринга — сконвертировать экранный шрифт в принтерный в буфер. Обработка выглядит примерно так: все D7-ые биты (всех 8-и байтов шрифта) необходимо превратить в биты D7-D0 (первый столбец), все D6-ые биты необходимо превратить в биты D7-D0 (второй столбец). Таким образом экранный шрифт для латинской буквы A (0×30, 0×78, 0xCC, 0xCC, 0xFC, 0xCC, 0xCC, 0×00) превратится в последовательность (0×3E, 0×7E, 0xC8, 0xC8, 0×7E, 0×3E, 0×00, 0×00) для буфера рендеринга. Надеюсь, что приведенный ниже кусок кода поможет понять алгоритм рендеринга.

С еще одной большой проблемой я боролся целый вечер: при попытке отработки кода рендеринга — в терминал выбрасывался мусор, если закомментировать этот участок, все работало без проблем. Мне показалось, что переполняется/переписывается оперативная память и поэтому возникает мусор в выдаче. Прочитав про то, что Ардуино хранит все переменные в ОЗУ я понял, что всему вина — это 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 ома для ограничения токов.

pnguthplz4qu_zfrtxiq_vt6zk0.jpeg

Поскольку принтер матричный, для печати используется картридж с лентой, да-да, как в старину. Я пытался попробовать восстановить родной картридж, замочил его в воде на недельку. Попробовал разобрать, чтобы пропитать поролон краской-мастикой. Поролон просто рассыпался… Ближайший картридж от меня находится в Самаре. :(

tlycaheftavmz-jewcp1jvlnjmo.jpeg

Решил тогда попробовать найти бумагу, чувствительную к ударам (когда ее покупал, продавец пару раз меня предупредил, что эта лента не подходит никуда ;) Пришлось ее заверять, что я все понял, и претензий потом от меня не будет…;)). Нашел только ленту шириной 80 мм, пришлось резать на кусочки и уменьшать ширину до 57 мм, добрым старым ламповым способом при помощи шариковой ручки и линейки… Зато печатает! ;)

wtaznsebehqta7gcfxtphenkgfs.jpeg

Как выглядит финальный результат. На 20 мм фанерке закреплен принтер и платы управления. При монтаже использован ШВВП 2×0.5 мм, коннекторы WAGO и клеммник! ;) МГТФ кончился давным-давно… И его вообще не могу найти в продаже у себя в городе. :(

zfimakzhmyvsxnez4skzgd3xd1k.jpeg

Видео с демонстрацией работы принтера.

Исходный текст, шрифты, полезные утилиты, документация


Спасибо за внимание!

© Geektimes