В прошлой своей публикации я подключал ЖКИ дисплей от старого кассового аппарата. Напомню, что я приобрел 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 и клеммник! ;) МГТФ кончился давным-давно… И его вообще не могу найти в продаже у себя в городе. :(



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




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

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


  1. lonelymyp
    05.04.2018 15:08

    Насколько я знаю, обычная термобумага, для кассовых термопринтеров, вполне себе чувствительна к «ударам», не обязательно искать какую-то особенную.


  1. BaurzhanD Автор
    05.04.2018 15:12

    Да, я кстати пробовал её в деле. Она чувствительна, но текст не совсем хорошо видно.


  1. PoliTeX
    05.04.2018 16:17

    Можно воспользоваться «копиркой», если влезет.


  1. ice938
    05.04.2018 16:20

    Зачем рендер?
    Берем символ:
    1.старшие биты строк, его составляющих, отправляем на печать
    2.сдвигаем строки влево
    3.повторяем 1,2 до конца символа

    Или тот же алгоритм, но сразу к буферу из 9 символов для правой полустроки и тоже для левой. И не надо тратить ресурсы на поворот символов


    1. BaurzhanD Автор
      05.04.2018 18:59

      Рендер был нужен, чтобы упростить написание/отладку/тестирование программы (человек писавший на Паскале и читавший Кнута...). Ну и вторая мысль была, что Ардуинка может не справится с выполнением программы в жестких временных рамках. Хотя, понимаю, что дури вычислительной у нее в районе 16 MIPS, что примерно равно 486-му процессору.
      Ваше предложение дельное, будет время — попробую реализовать Вашу идею.


      1. ice938
        05.04.2018 22:03

        Насколько я помню, сдвиг- это одна из наименее затратных операций. Вот получение одного бита- не помню, что по ресурсам требует… Но должно быть не много, битовая операция.
        Или вообще сделать через AND

        for (k=0; k<=7; k++)
               {
                if(TmpVal & 0x80)  // TmpVal (строка символа) AND 0b10000000
                       {
                          digitalWrite(b1stHead_A, HIGH); // если результат равен 0x80
                        }
                else  {
                          digitalWrite(b1stHead_A, LOW); // если результат равен 0
                        };
                TmpVal=TmpVal << 1; сдвигаем влево
               };


        1. BaurzhanD Автор
          06.04.2018 11:33

          Я согласен с Вами. And/Or/Xor/Not и сдвиги выполняются буквально за 1 такт на AVR. Единственное, мне было лень разбираться, какого размера код выдает компилятор при реализации этих операций на языке высокого уровня.


          1. ice938
            06.04.2018 11:55

            Это да, компилятор может такого наворотить… =) Но чаще он все же более-менее адекватный код выдает


  1. jaiprakash
    05.04.2018 17:07

    Когда-то думал купить новый термопринтер (запчасть кассовика) занедорого, но так и не придумал зачем…


  1. golf2109
    05.04.2018 17:18

    Вы конечно молодец. Очень полезное занятие для саморазвития. Правда сейчас повсеместно применяются термопринтеры в силу их несравненно большей надежности и большего времени наработки на отказ. Как специалист отремонтировавший не одну сотню этого «матричного ужаса» огорчу — скажу, что сначала у Вас сломается пару иголок, которые поменять весьма проблематично, а затем накроется двигатель…


    1. BaurzhanD Автор
      05.04.2018 19:01
      +3

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


  1. lopatoid
    05.04.2018 22:57
    +1

    Чтобы Power Bank от Xiaomi сам не отключался, надо у USB замкнуть D+ и D-


    1. kAIST
      05.04.2018 23:34

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


      1. lopatoid
        06.04.2018 00:13
        +1

        Ну вот если если замкнуть D+ и D- то он больше от малых токов не отключается. Проверено.


  1. serafims
    06.04.2018 11:19

    Кстати, можно купить кассовый аппарат типа Элвес Микро, накопать на него схему, и сделать какое-то готовое устройство, оно сразу будет с дисплеем и клавой… Какой-нибудь детский кассовый аппарат, совсем как настоящий) Только придется выдернуть процессор, который в нем (обычно там что-то на -51 архитектуре) и внимательно изучать схему…


  1. BaurzhanD Автор
    06.04.2018 11:30

    Я живу в г.Уральске, Казахстан. Обычный провинциальный город, с населением 300 тыс. За ненужный кассовый аппарат на OLX просят от 2000 руб (10000 тенге). Эти аппараты мне достались по счастливой случайности, новый хозяин бывшего здания банка решил избавиться от мусора самым простым способом: устроил распродажу барахла, типа все по 100 рублей (мониторы, кассы, и т.д.). Больше таких акций у нас не было за последние 100 лет! ;-)