Работа с ЖК дисплеями серии МТ-10Т11

30.09.2018

Российская компания МЭЛТ выпускает различные виды ЖК дисплеев среди которых есть серия MT-10T11. Это семисегментные дисплеи с интерфейсом I2C отображающие 10 символов высотой 8 мм. Серия включает дисплеи как с подсветкой различного цвета, так и без подстветки. Причем есть версии с питанием от 5В и 3.3В. Конечно, такие дисплеи менее популярны, чем знакосинтезирующие (особенно, 2 строки по 16 символов), но благодаря большому размеру символов и шине I2C их использование бывает целесообразно. К сожалению, документация МЭЛТ часто бывает неполной и содержит ошибки. А поддержка на их форумене не всегда может дать точные и правильные ответы. С МТ-10Т11 как раз такая ситуация, поэтому рассмотрю подробно работу с этим дисплеем с точки зрения программиста. Тонкости схемотехники и работы с шиной I2C затрагивать не буду. Дисплей выглядит так

Данный дисплей построен с использованием LCD контроллера PCF8576C. Описание на этот контроллер можно без труда найти, но использовать его несколько труднее. Во первых, значения загружаемые в управляющие регистры и память зависят от использованного индикатора (собственно "стекла") и схемы его подключени к контроллеру (как минимум). Во вторых, большая часть документации посвещена схемотехнике и режимам работы с индикатором, которые не нужны для программирования. МЭЛТ, кроме краткого листа с габаритными размерами и цоколевкой, предлагает и пример программы работы с дисплеем. Приведу его полностью

    //Пример программы для сегментных индикаторов MT10T11, MT10T12.

    void main(void) {
        byte i;
        I2C_Start(0x70);    // Сформировать на I2C шине комбинацию START
                            // и передать SLAVE адрес устройства с битом операции записи
        I2C_SendByte(0xCE); // 11001110b - Выдать команды настройки индикатора
        I2C_SendByte(0xE0); // 11100000b - Выдать команды настройки индикатора
        I2C_SendByte(0xF8); // 11111000b - Выдать команды настройки индикатора
        I2C_SendByte(0xF0); // 11110000b - Выдать команды настройки индикатора
        I2C_SendByte(0x00); // 00000000b - Всё, команды выданы, дальше пойдут данные
                            // изображения, их записывать с адреса 0
        for(i=0; i<10; i++) {   //Цикл вывода байтов изображения
            I2C_SendByte(Logo10[i]); // Вывод очередного байта в индикатор
        }
        I2C_Stop();     // Завершить цикл передачи, сформировав на I2C шине комбинацию STOP
    }

    //Данные для заполнения индикатора
    const byte Logo10[10]={0xC0,0xB3,0xA7,0x80,0xF9,0x80,0xB8,0x80,0xF8,0x40};

Кстати, этот пример появился далеко не сразу, а после длительного обсуждения на форуме МЭЛТ и сначала содержал несколько ошибок. Как видно, никаких особых подробностей нет. Более того, описан далеко не весь доступный функционал. Поэтому давайте разбираться подробно.

Шина I2C

Начну с того, как дисплей на этом контроллере подключается к нашему устройству. Не важно к какому, Arduino или одному из микроконтроллеров. В данном случае не обойтись без небольшого экскурса в схемотехнику и внутреннюю структуру LCD контроллера. Не пугайтесь, совсем немного. Как это часто бывает, адрес устройства на шине I2C можно немного изменять с помощью специально предназначенных для этого выводов. У PCF8576 для этого есть вывод SA0 (название в данном случае не важно, но для полноты картины). Это позволяет задавать базовый адрес дисплея 70h или 72h, в зависисти от того, куда этот вывод подключен. Так как нам этот вывод не доступен (возможность конфигурирования адреса МЭЛТ нам не предоставил), то получаем, что базовый адрес дисплея всегда 70h (0x70).

Но это еще не все. Для построения больших и/или сложных дисплеев микросхемы можно каскадировать. При этом ВСЕ они будут иметь один и тот же адрес на шине I2C! Конкретная микросхема при этом должна выбираться специальной командой DEVICE SELECT (будет рассмотрена позже). Для задания этого дополнительного адреса микросхемы имеют специальные выводы A0, A1 и A2, которые в нашем случае задают его нулевым. Казалось бы, зачем тогда это упоминать? Дело в том, что при записи символов в память дисплея используются два указателя, указатель позиции и указатель контроллера (это по документации). Подробнее я это опишу далее, а сейчас скажу, что фактически это не два указателя, а один. И при записи символа в память дисплея у нас может увеличиться не только указатель позиции, но и указатель контроллера. А подтверждение при обмене по шине контроллер дисплея выдает в том случае, когда совпадают И адрес на шине, И дополнительный адрес. В случае каскадирования контроллеров подтверждение выдаст одни из них, но у нас контроллер один. Поэтому, когда мы записываем символ в последнюю позицию дисплея, получаем ситуацию, когда дисплей перестает отвечать, пока снова не получит команду DEVICE SELECT. МЭЛТ об этом не говорит ни слова, а на форумах подобные вопросы задаются.

Дисплей работает с шиной I2C в стандартном режиме (100 кГц). Кроме того, PCF8576 может удерживать сигнал SCL на низком уровне, если не успевает отработать команду. Это надо учитывать, так как выдавать следующий байт на шину можно только тогда, когда SCL вернется к высокому уровню. После подачи питания дисплею требуется минимум 1 мс для выполнения своей аппаратной инициализации. Но лучше подождать подольше, например, 10 мс и только потом начинать обмен с дисплеем. Если все суммировать, то получаем:

  • Адрес дисплея на шине I2C - 70h
  • Дополнительный (логический) адрес, аппаратно - 0. Но нужно учитывать, что бит ACK при обмене по шине выдается только при совпадении адреса и дополнительного адреса. Нужно использовать команду DEVICE SELECT.
  • Используется стандартный режим шины (100 кГц).
  • Дисплей может удерживать сигнал SCL если не успевает.
  • После подачи питания требуется полождать минимум 1 мс, прежде чем начинать работу с дисплеем.

Организация памяти символов

Теперь поговорим о том, как организована память символов. Сама память физически представляет собой массив 40х4. Вот картинка из документации на PCF8576

А вот ее логическая организация зависит мультиплексирования. А это уже определяется тем, как к контроллеру подключен индикатор. Все варианты мультиплексирования рассматривать не буду, так как МТ-10Т11 использует только мультиплексирование 1:2. При этом память разделена на два банка. Вот это в примере МЭЛТ полностью отстутствует. Первый банк (bank 0) занимает битовые строки 0 и 1, а второй (bank 1) 2 и 3. При этом существует возможность не только переключать банки памяти, но и записывать в один, а отображать другой. Это позволяет, например, выводить в один банк медленно формируемую строку символов, отображая при этом информацию другого банка, а потом мгновенно вывести результат просто переключив банки.

Сами символы хранятся в банках памяти и отображаются слева направо. Самая левая позиция на дисплее (самый старший сивол) имеет позицию 0. Адрес (не номер!) позиции, куда записывается символ, хранится в указателе позиции (data pointer). Указатель позиции автоматически увеличивается после записи символа, что бы указывать на следующую позицию. Для принудительного задания позиции записываемого символа используется команда LOAD DATA POINTER. Формирование адреса позиции из ее номера, так же, зависит от мультиплексирования. Для нашего случая, мультиплексирование 1:2, каждая позиция занимает 4 бита. То есть, адрес должен увеличиваться на 4. Это делается автоматически при записи, а вот при самостоятельном задании позиции должно учитываться программистом при загрузке указателя. Например, третий слева символ будет соответствовать адресу 8 в памяти, а пятый адресу 16 (0Fh).

PCF8576 не имеет встроенного знакогенератора. Поэтому каждый символ в памяти хранится в виде виде байта каждый бит которого соответствует одному из 7 сегментов или десятичной точке (DP). Единичное значение бита соответствует включенному (темному) состоянию сегмента, а нулевое выключенному (прозрачному). Какой бит какому сегменту соответствует зависит от мультиплексирования. В нашем случае это будет выглядеть так

Например, что бы вывести на дисплей цифру 6 нужно записать в память байт BEh, если не нужна десятичная точка в этой позиции. Если же точка нужна, то нужно дополнительно установить нулевой бит. Вот пример таблицы, которую можно использовать для формирования символов (десятичная точка не выводится)

        пробел      00h                     минус       10h
          0         EEh                      8          FEh
          1         44h                      9          F6h
          2         DAh                      A          FCh
          3         D6h                      b          3Eh
          4         74h                      C          AAh
          5         B6h                      d          5Eh
          6         BEh                      E          BAh
          7         C4h                      F          B8h

Кроме того, можно заставить мигать либо весь дисплей, либо избранные сегменты. Мигающего курсора нет. Это реализуется двумя разными режимами, причем мигание отдельных сегментов реализованы автоматическим переключением банков памяти. Об этом не говорится в примере МЭЛТ. Подробности расскажу далее, при описании команд. На этом описание организации памяти заканчивается.

Команды

После начала цикла обмена по шине, последовательности START, дисплей находится в режиме приема команд. Может быть передана одна или несколько команд, каждая из которых занимает 1 байт. Старший бит каждой команды задает, будут ли еще команды, или эта команда последняя. Если старший бит установлен, то следующим байтом будет послана следующая команда. Если бит сброшен, то это последняя команда и дальше пойдут байты данных с символами для отображения. Вот как выглядит общий формат байта команды в документации на PCF8576

При дальнейшем описании команд я больше не буду говорить про старший бит, поскольку его функция жестко задана. Контроллер, а значит и дисплей, поддерживает всего 5 команд:

MODE SET - установка режима

биты 6 и 5
10 - фиксированное значение, код команды
бит 4 - LP
0 - нормальный режим, 1 - режим низкого энергопотребления. Рекомендую задавать 0, так как в режиме низкого энергопотребления, в частности, снижается тактовая частота. Если Вас интересует экономия питания, или этого требуют схемотехники, то прочитайте документацию, так как описание этого режима, формул, и всех ограничений выходит за рамки статьи.
бит 3 - E
Состояние дисплея. 0 - отображение выключено, 1 - отображение включено.
бит 2 - Bias
Для МТ-10Т11 должен быть 1. Влияет на формирование уровней напряжения управления индикатором.
биты 1 и 0 - M
Для МТ-10Т11 должно быть 10. Задает мультипексирование и определяется индикатором.

В примере МЭЛТ в качестве этой команды передается CEh, что задает нормальный режим энергопотребления и включает отображение информации на дисплее. Вы можете изменять значение бита 3 для включения и выключения отображения информации на дисплее. Можете попробовать изменять и бит 4 для управления энергопотреблением, но тут нельзя гарантировать корректность работы индикатора, так как схема МТ-10Т11 неизвестна. Эта команда может быть выдана в любое время.

DEVICE SELECT - выбор устройства

биты с 6 по 3
1100 - фиксированное значение, код команды
биты с 2 по 0 - A
Для МТ-10Т11 нужно всегда передавать 000, так как здесь не используется каскадирование контролллеров.

Как я уже говорил, эту команду нужно выдавать не только при инициализации дисплея, но и после записи в последнюю позицию (самую правую). Дело в том, что после записи символа внутренний указатель data pointer автоматически увеличивается, а после записи в поледнюю позицию он переполняется и снова начинает указывать на самую левую (нулевую) позицию. Но это переполнение инкрементирует внутренний указатель контроллера. А это приводит к тому, что наш дисплей просто перестает отвечать на запись символов в память, что исключает изменение отображаемой информации. Если Вы каждый раз обновляете всю отображаемую информацию, то рекомендуется просто передавать эту команду после начала цикла обмена по шине I2C (последовательность START). Все равно после START нужно передать, как минимум, одну команду. Из примера МЭЛТ следует, что надо каждый раз повторять последовательность инициализации, но это совершенно не требуется. Достаточно просто передавать DEVICE SELECT.

LOAD DATA POINTER - загрузка указателя данных

бит 6
0 - фиксированное значение, код команды
биты с 5 по 0 - P
Адрес памяти для записи следующего символа. От 000000 до 100111 (27h, 39).

Обратите внимание, это адрес в памяти, а не номер позиции символа! В нашем случае каждая позиция занимает 4 бита, так как каждый байт символа хранится в двух битовых плоскостях по 4 бита в каждой. Если Вы укажете неверное значение адреса, то будете частично перезаписывать другие символы. Контроллер никак не отслеживает корректность задаваемого адреса. Самая левая (нулевая) позиция будет иметь адрес 0, вторая слева 4, третья 8, и так далее.

BANK SELECT - выбор банка

биты с 6 по 2
11110 - фиксированное значение, код команды
бит 1 - I
Выбор банка памяти для сохранения записываемых символов. 0 - битовые плоскости 0 и 1. 1 - битовые плоскости 2 и 3.
бит 0 - O
Выбор банка памяти для отображения. Значения аналогичны биту I.

Теперь видно, что действительно можно задать разные банки для занесения информации и отображения. Не уверен, что эта возможность часто бывает нужна, но она есть. А в примере МЭЛТ об этой возможности не упоминается. Банки памяти используются и для одного из режимов мигания дисплея, о чем скажу в описании последней команды.

BLINK SELECT - выбор мигания

биты с 6 по 3
1110 - фиксированное значение, код команды
бит 2 - AB
Выбор режима мигания. 0 - обычное мигание (весь дисплей), 1 - альтернативное мигание
биты 1 и 0 - BF
Задает частоту мигания. 00 - мигания нет, 01 - 2 Гц, 10 - 1 Гц, 11 - 0.5 Гц.

Альтернативный режим мигания осуществляется автоматическим переключением бита O (выбор отображаемого банка памяти). Следовательно, сначала нужно заполнить оба банка. Если второй банк пустой, то получаем режим обычного мигания. Если информация в банках памяти различается только в каких то позициях, то получим переключение отображаемых символов только в этих позициях. Если требуется отличная от указанных частота мигания, то использовать аппаратную поддержку мигания уже не получится. Обычный режим мигания можно реализовать командой MODE SET с переключением бита 3 (E). Режим альтернативного мигания можно реализовать только изменением информации в памяти или ручным переключением банков памяти.

На этом закончу описание дисплея и контроллера. Пора переходить к практике.

Примеры процедур работы с дисплеем

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

i2c_Start(int addr)
Выдача последовательности START на шину (начало цикла обмена). Параметр addr задает базовый адрес устройства, который сохраняется внутри модуля i2c. Модуль сам управляет битом R/W в выдаваемом на шину адресе.
i2c_Stop()
Выдача последовательности STOP на шину (конец цикла обмена).
i2c_write(char b)
Передача байта b на шину. Предполагается, что эта процедура сама следит за состоянием линии SCL и начинает передачу только при его высоком уровне.
delay(int t)
Зажержка на t мс.

Сначала приведу общие определения, которые будут использоваться в процедурах далее

    #define LP_NORMAL           0       // Режим обычного энергопотребления
    #define LP_POWER_SAVING     1       // Режим пониженного энергопотребления
    #define ENABLED             1       // Отображение включено
    #define DISABLED            0       // Отображение выключено
    #define BANK0               0       // Банк в битовых плоскостях 0 и 1
    #define BANK1               1       // Банк в битовых плоскостях 2 и 3
    #define BLINK_NORMAL        0       // Нормальное мигание
    #define BLINK_ALTERNATE     1       // Альтернативный режим мигания
    #define BLINK_OFF           0       // Нет мигания
    #define BLINK_2Hz           1       // Частота мигания 2 Гц
    #define BLINK_1Hz           2       // Частота мигания 1 Гц
    #define BLINK_05Hz          3       // Частота мигания 0.5 Гц
    #define SEQ                 1       // Будет еще одна команда
    #define LAST                0       // Это последняя команда

Поскольку процедуры реализующие разные команды могут быть вызваны как в цепочке, так и по отдельности, нам как то нужно управлять старшим битом в байте команды. Для это используется первый параметр в каждой процедуре (кроме mt10t11_Init). В дальнейшем я не буду уделять отдельного внимания этому параметру.

mt10t11_ModeSet - установка режимов работы дисплея.

    void mt10t11_ModeSet(int s, int lp, int e) {
    
    // Параметры:
    //  s  - Последняя/не последняя команда в последовательности
    //  lp - Режим энергопотребления
    //  e  - Отображение включено/отключено
    
        i2c_write(s << 7 | 10 << 5 | lp << 4 | e << 3 | 0b110);
    }

Что либо особенного добавить к ранее описанному сложно. Сам байт команды формируется из предопределенных значений бит и задаваемых вызывающей программой (программистом) значений. Должен отметить, что такое формирование байта команды излишне и дается тут лишь для наглядности. В Ваших реальных программах, скорее всего, не потребуется такой универсальной обработки всех параметров, как и всех возможностей контроллера дисплея. Вероятно, подобные процедуры просто сведутся к макросам, а то и просто вызовам i2c_write с фиксированным значением. Это позволит существенно уменьшить объем генерируемого кода, что очень важно для микроконтроллеров. В приведенном примере компилятор сгенерирует несколько дополнительных инструкций логических операций (OR) и сдвигов. В Ваших программах вполне возможно избежать этого.

mt10t11_DeviceSelect - выбор устройства

    void mt10t11-DeviceSelect(int s) {
        i2c_write(s << 7);
    }

mt10t11_BankSelect - выбор банков памяти

    void mt10t11(int s, int i, int o) {
    
    // Параметры:
    //  i - Банк для записи данных
    //  o - Банк для отображения)
    
        i2c_write(s << 7 | 0b11110 << 2 | i << 1 | o);
    }

mt10t11_BlinkSelect - выбор режимов мигания

    void mt10t11_BlinlSelect(int s, int ab, int bf) {
    
    // Параметры:
    //  ab - режим мигания
    //  bf - частота мигания
    
        i2c_write(s << 7 | 0b1110 << 3 | ab << 2 | bf);
    }

mt10t11_SetPos - задать позицию для записи

    void mt10t11_SetPos(int s, int pos) {
    
    // Параметры:
    //  pos - номер позиции
    
        i2c_write(s << 7 | pos << 2);
    }

Обратите внимание, фактически, это передача команды LOAD DATA POINTER. Но поскольку для МТ-10Т11 одна позиция символа всегда занимает 4 бита, то мы можем защититься от ошибок и вычислять загружаемое значение прямо в процедуре из передаваемого номера позиции. Да и работать с дисплеем в результате будет проще.

mt10t11_Init - начальная инициализация дисплея

    void mt10t11_Init() {
        delay(10);      // На всякий случай, если нас вызвали сразу
                        // после включения питания
                        
        i2c_Start(0x70);
        mt10t11_ModeSet(SEQ,LP_NORMAL,ENABLED);
        mt1ot11_DeviceSelect(SEQ);
        mt10t11_BankSelect(SEQ,BANK0,BANK0);
        mt10t11_SetPos(SEQ,0);             
        mt10t11_BlinkSelect(LAST,BLINK_NORMAL,BLINK_OFF);
        i2c_Stop();
    }

Практически полностью соответствует примеру МЭЛТ при этом демонстрируя использование описанных ранее процедур. Собственно, на этом можно было бы и остановиться, поскольку все подробно описано. Но не хватает общих примеров использования возможностей дисплея.

Примеры управления дисплеем

Настало время собрать воедино описание контроллера PCF8576, замечания о возможностях дисплея и использование описанных выше процедур. Сначала пример вывода символов в три левых и три правых позиции дисплея. Этого нет в примере МЭЛТ.

    mt10t11_Init();
    i2c_Start(0x70);
    mt10t11_DeviceSelect(SEQ);
    mt10t11_SetPos(LAST,0);         // Самая левая позиция дисплея
    i2c_write(0x44);                // '1'
    i2c_write(0xDA);                // '2'
    i2c_write(0xD6);                // '3'
    i2c_Stop();
    i2c_Start(0x70);
    mt10t11_SetPos(LAST,7);
    i2c_write(0x74);                // '4'
    i2c_write(0xB6);                // '5'
    i2c_write(0xBE);                // '6'
    i2c_Stop();

Обратите внимание, что не требуется каждый раз выполнять полную инициализацию дисплея, как следует из примера МЭЛТ. Вызов mt10t11_DeviceSelect, в данном случае, лишний. Но представьте, что этот фрагмент кода вызывается несколько раз, например, в цикле (не считая инициализации, конечно). В этом случае у нас бы возникли проблемы, так как после вывода цифры 6 у нас получается неверное значение внутреннего указателя дополнительного адреса (логического адреса контроллера). Для демонстрации того, как поступать в таких случаях я и добавил вызов этой функции.

Так же, обратите внимание на вызов i2c_Stop и i2c_Start между выводом в левую часть дисплея и выводом в правую часть. Дело в том, что нет возможности переключить контроллер из режима передачи данных в режим передачи команд иным способом. Это замедляет и усложняет управление дисплеем, но по другому никак не получится.

Теперь пример работы с банками памяти.

    mt10t11_Init();
    i2c_Start(0x70);
    mt10t11_DeviceSelect(SEQ);
    mt10t11_SetPos(LAST,0);
    i2c_write(0x44);
    i2c_Stop();
    i2c_Start(0x70);
    mt10t11_BankSelect(SEQ,BANK1,BANK0);        // Отображаем банк 0 заполняем банк 1
    mt10t11_SetPos(LAST,9);
    i2c_write(0xDA);
    i2c_Stop();
    delay(5000);                                // Пауза 5 секунд
    i2c_Start(0x70);
    mt10t11_BankSelect(LAST,BANK0,BANK1);
    i2c_Stop();

Разбор этого примера не должен вызывать сложностей. После инициализации дисплея и для отображения, и для ввода, оказывается выбран нулевой банк (битовые плоскости 0 и 1). Мы выводим в нулевую позицию цифру 1, которая сразу отображается. Затем мы переключаем ввод на первый банк (битовые плоскости 2 и 3), при этом отображаемым остается нулевой банк. Устанавливаем самую правую позицию и выводим туда цифру 2. Но она не появляется на экране. После 5 секундной задержки мы снова переключаем банки, теперь ввод в нулевой банк, а отображение первого банка. У нас на дисплее пропадает цифра 1 и появляется цифра 2 в последней позиции.

Наконец, примеры мигания. Сначала мигания всего дисплея, затем мигания со сменой символов во всех позициях и, наконец, символа только в одной позиции.

    mt10t11_Init();
    i2c_Start(0x70);
    mt10t11_DeviceSelect(SEQ);
    mt10t11_SetPos(LAST,0);
    i2c_write(0x44);                // '1'
    i2c_write(0xDA);                // '2'
    i2c_write(0xD6);                // '3'
    i2c_Stop();
    i2c_Start(0x70);
    mt10t11_BankSelect(SEQ,BANK1,BANK0);
    mt10t11_SetPos(LAST,0);
    i2c_write(0x74);                // '4'
    i2c_write(0xB6);                // '5'
    i2c_write(0xBE);                // '6'
    i2c_Stop();
    i2c_Start(0x70);
    mt10t11_BlinkSelect(LAST,BLINK_NORMAL,BLINK_1Hz);
    
    // Теперь у нас на дисплее мигают цифры 123 с частотой 1 Гц
    
    delay(5000);
    i2c_Start(0x70);
    mt10t11_BlinkSelect(LAST,BLINK_ALTERNATE,BLINK_1Hz);
    i2c_Stop();
    
    // Теперь у нас на экране цифры 123 сменяются цифрами 456, и обратно, раз в секунду
    
    delay(5000);
    i2c_Start(0x70);
    mt10t11_BankSelect(SEQ,BANK1,BANK0);
    mt10t11_SetPos(LAST,0);
    i2c_write(0x44);                // '1'
    i2c_write(0);                   // пробел
    i2c_write(0xD6);                // '3'
    i2c_Stop();
    
    // Теперь у нас на дисплее мигает только цифра 2.

Тут все должно быть знакомо и понятно. Скажу только, что мы задали альтернативный режим мигания один раз, а получили два разных эффекта. Мигание лишь одного символа мы получили просто изменив содержание первого банка. Настройки режима мигания у нас сохранились. Использование аппаратного мигания не плохо помогает, например, при реализции режима установки значений часов.

Заключение

Как видно, даже такой простой дисплей умеет довольно многое. При этом управление простое. Что помешало МЭЛТ подготовить хоть какую то удобоваримую документацию на свой дисплей, не знаю. Но надеюсь, что для Вас работа с этим дисплеем теперь не составит труда.


Вы можете обсудить данную статью или задать вопросы автору на форуме