Учебный курс AVR. Использования TWI модуля как ведущего I2C устройства. Работа на прерываниях. Ч5

21/11/2013 - 08:08 Pavel Bobkov

Введение

Все операции, выполняемые TWI модулем, завершаются установкой бита прерывания (TWINT бит регистра TWCR) и соответствующего статусного кода (старшие шесть разрядов регистра TWSR). Это позволяет организовать обмен данными по шине с помощью прерываний. 

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

Такой подход экономит время микроконтроллера, потому что не нужно ожидать установки бита прерывания в цикле, чтобы продолжить процесс обмена данными. 

В этой части мы рассмотрим работу ведущего TWI устройства на прерываниях. Код для этого материала основан на руководстве фирмы Atmel - AVR315: Using TWI module as I2C master. В интернете есть его перевод. Загуглите в Яндексе, если интересно. 

Я сделал свой аутентичный перевод этого руководства, скоро его выложу.

Обработчик прерывания TWI модуля

Благодаря наличию статусных кодов в TWI модуле, обработчик прерывания органично реализуется в виде автомата (state machine), где каждое состояние - это один статусный код.

Традиционный способ построения автоматов основан на использовании оператора ветвления- switch. Начальный код для обработчика прерывания TWI выглядит так.


//буфер для сообщения
volatile static uint8_t twiBuf[TWI_BUFFER_SIZE]; 

//сколько байт нужно передать
volatile static uint8_t twiMsgSize;

//статус модуля
volatile static uint8_t twiState = TWI_NO_STATE; 

...


#pragma vector=TWI_vect
__interrupt void TWI_ISR(void)
{
//берем статусный код модуля
uint8_t stat = TWSR & TWSR_MASK;

//обрабатываем его
switch (stat){

  // состояние START сформировано
  case TWI_START:

  // состояние повторный START сформировано
  case TWI_REP_START:

  // был передан пакет SLA+W и получено подтверждение
  case TWI_MTX_ADR_ACK:

  // был передан байт данных и получено подтверждение
  case TWI_MTX_DATA_ACK:

  //байт данных принят и передано подтверждение
  case TWI_MRX_DATA_ACK:

  //был передан пакет SLA+R и получено подтверждение
  case TWI_MRX_ADR_ACK:

  //был принят байт данных без подтверждения
  case TWI_MRX_DATA_NACK:

  //был потерян приоритет
  case TWI_ARB_LOST:

  // был передан пакет SLA+W и не получено подтверждение
  case TWI_MTX_ADR_NACK:

  // был передан пакет SLA+R и не получено подтверждение
  case TWI_MRX_ADR_NACK:

  // был передан байт данных и не получено подтверждение
  case TWI_MTX_DATA_NACK:

  // ошибка на шине из-за некорректных состояний СТАРТ или СТОП
  case TWI_BUS_ERROR:

  default:
  }
}


Теперь для состояний автомата нужно добавить какой-то код. Разберемся, что нужно делать в каждом состоянии.

Состояния TWI_START и TWI_REP_START по сути одинаковые. Передача данных только началась, нужно установить указатель на первый элемент массива twiBuf, загрузить его в регистр данных TWDR, инкрементировать указатель и сбросить флаг прерывания.

TWI_MTX_ADR_ACK - передан адресный пакет SLA+W и получено подтверждение. Можно продолжать передачу данных, если это не единственный байт сообщения. Загружаем байт данных в регистр TWDR, увеличиваем указатель и сбрасываем флаг прерывания. Если это единственный байт сообщения, формируем состояние СТОП, сбрасываем флаг прерывания и запрещаем его. И еще устанавливаем статус окончания передачи данных.

TWI_MTX_DATA_ACK - передан байт данных и получено подтверждение. Можно продолжать передачу данных, если это был не последний байт сообщения. Загружаем байт данных в регистр данных, увеличиваем указатель и сбрасываем флаг прерывания. Если это был последний байт сообщения, формируем состояние СТОП, сбрасываем флаг прерывания, запрещаем его и устанавливаем статус окончания передачи данных.

В четырех состояниях выполняются практически одинаковые действия, поэтому их можно объединить. Код для этой части обработчика будет таким.


case TWI_START:
case TWI_REP_START:
   ptr = 0; //индексная переменная для массива
case TWI_MTX_ADR_ACK:
case TWI_MTX_DATA_ACK:
   if (ptr < twiMsgSize){ //если не все передано

      //загружаем байт сообщения
      TWDR = twiBuf[ptr]; 

      //сбрасываем флаг TWINT
      TWCR = (1<<TWEN)|(1<<TWIE)|(1<<TWINT);

      //увеличиваем индексную переменную
      ptr++; 
   }
   else{ //если передано все

      //устанавливаем состояние, что данные переданы
      twiState = TWI_SUCCESS; 

      //формируем СТОП, запрещаем прерывания
      TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWSTO)|(0<<TWIE);
   }
break;


Продолжаем.

TWI_MRX_DATA_ACK - принят байт данных и передано подтверждение. В регистре данных TWDR лежит принятый байт, мы должны скопировать его в массив и увеличить указатель. Затем нужно проверить, какой этот байт по счету? Если он предпоследний, то для приема последнего байта нам не нужно формировать подтверждение, если нет, то нужно. Далее мы сбрасываем бит прерывания и устанавливаем бит подтверждения в зависимости от ситуации.

TWI_MRX_ADR_ACK - передан пакет SLA-R и получено подтверждение. Ведущий отозвался на свой адрес и можно получить от него данные. Проверяем, следующий байт будет последним или нет. Если да, то формировать подтверждение не нужно, если нет, то нужно. Далее мы сбрасываем бит прерывания и устанавливаем бит подтверждения в зависимости от ситуации.

Опять частично одинаковый код в разных состояниях, который мы можем объединить. Код для этой части обработчика будет таким.


case TWI_MRX_DATA_ACK: 
   twiBuf[ptr] = TWDR; //сохраняем байт данных
   ptr++;


case TWI_MRX_ADR_ACK:
   if (ptr < (twiMsgSize-1)){ //это предпоследний байт?
       //нет, формируем подтверждение
      TWCR = (1<<TWEN)|(1<<TWIE)|(1<<TWINT)|(1<<TWEA);
   }
   else {
      //да, подтверждение не формируем
     TWCR = (1<<TWEN)|(1<<TWIE)|(1<<TWINT); 
   }
   break;



TWI_MRX_DATA_NACK - принят байт без подтверждения. Значит это последний байт, который мы должны принять. Копируем его из регистра данных в свой массив. Устанавливаем какой-нибудь флаг, что прием данных окончен. Формируем состояние СТОП, сбрасываем флаг прерывания и запрещаем его.


case TWI_MRX_DATA_NACK:
   twiBuf[ptr] = TWDR;
   twiState = TWI_SUCCESS;
   TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWSTO);
   break;


TWI_ARB_LOST - потеря приоритета. Приоритет может быть потерян, если несколько ведущих устройств одновременно начали передачу данных. В этом случае можно сбросить флаг прерывания и сформировать состояние повторного старта. Состояние старта сформируется, когда шина освободится.


case TWI_ARB_LOST:
   TWCR = (1<<TWEN)|(1<<TWIE)|(1<<TWINT)|(1<<TWSTA);
   break;


Далее идут статусные коды, связанные с неполадками на шине.

TWI_MTX_ADR_NACK - передан пакет SLA+W и не получено подтверждение
TWI_MRX_ADR_NACK - передан пакет SLA+R и не получено подтверждение
TWI_MTX_DATA_NACK - передан байт данных и не получено подтверждение
TWI_BUS_ERROR - ошибка на шине из-за некорректных состояний СТАРТ или СТОП

Такие коды могут возникнуть, если ведомое устройство не отвечает или прекратило отвечать. В этом случае нужно сохранить ошибочный статусный код и запретить прерывание TWI модуля.


case TWI_MTX_ADR_NACK:
case TWI_MRX_ADR_NACK:
case TWI_MTX_DATA_NACK:
case TWI_BUS_ERROR:
default:

   //сохраняем статусный код
   twiState = stat; 

   //запрещаем прерывания модуля
   TWCR = (1<<TWEN)|(0<<TWIE);  


Полный код обработчика прерывания TWI модуля выглядит так.


//буфер для сообщения
volatile static uint8_t twiBuf[TWI_BUFFER_SIZE]; 

//сколько байт нужно передать
volatile static uint8_t twiMsgSize; 

//статус модуля
volatile static uint8_t twiState = TWI_NO_STATE; 

...

#pragma vector=TWI_vect
__interrupt void TWI_ISR(void)
{
   static uint8_t ptr;
   uint8_t stat = TWSR & TWSR_MASK;

   switch (stat){
      case TWI_START:
      case TWI_REP_START:
         ptr = 0;
      case TWI_MTX_ADR_ACK:
      case TWI_MTX_DATA_ACK:
         if (ptr < twiMsgSize){
            TWDR = twiBuf[ptr];
            TWCR = (1<<TWEN)|(1<<TWIE)|(1<<TWINT);
            ptr++;
        }
        else{
            twiState = TWI_SUCCESS;
            TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWSTO)|(0<<TWIE);
        }
        break;

   case TWI_MRX_DATA_ACK:
      twiBuf[ptr] = TWDR;
      ptr++;

   case TWI_MRX_ADR_ACK:
      if (ptr < (twiMsgSize-1)){
         TWCR = (1<<TWEN)|(1<<TWIE)|(1<<TWINT)|(1<<TWEA);
      }
      else {
         TWCR = (1<<TWEN)|(1<<TWIE)|(1<<TWINT);
      }
      break;

   case TWI_MRX_DATA_NACK:
      twiBuf[ptr] = TWDR;
      twiState = TWI_SUCCESS;
      TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWSTO);
      break;

   case TWI_ARB_LOST:
      TWCR = (1<<TWEN)|(1<<TWIE)|(1<<TWINT)|(1<<TWSTA);
      break;

   case TWI_MTX_ADR_NACK:
   case TWI_MRX_ADR_NACK:
   case TWI_MTX_DATA_NACK:
   case TWI_BUS_ERROR:
   default:
      twiState = stat;
      TWCR = (1<<TWEN);
   }
}


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


Как запускается обработчик прерывания

Обработчик прерывания есть, но как его запустить? Очень просто. Записать во внутренний буфер (twiBuf) сообщение и количество байт для передачи (twiMsgSize), сформировать состояние СТАРТ и разрешить прерывание TWI модуля. Когда модуль сформирует на шине состояние СТАРТ, установится бит TWINT и запустится обработчик прерывания. Всю дальнейшую работу по передаче сообщения выполнит он.

Функция передачи сообщения выглядит так. 


void TWI_SendData(uint8_t *msg, uint8_t msgSize)
{
   uint8_t i;

   /*ждем ,когда TWI модуль освободится*/
   while(TWCR & (1<<TWIE));

   /*сохр. количество байт для передачи
   и первый байт сообщения*/
   twiMsgSize = msgSize;
   twiBuf[0] = msg[0];

   /*если первый байт типа SLA+W, то
   остальное сообщение тоже сохраняем*/
   if (!(msg[0] & (TRUE<<TWI_READ_BIT))){
      for (i = 1; i < msgSize; i++){
         twiBuf[i] = msg[i];
      }
   }

   twiState = TWI_NO_STATE ;

   /*разрешаем прерывание и формируем состояние старт */
   TWCR = (1<<TWEN)|(1<<TWIE)|(1<<TWINT)|(1<<TWSTA);
}

Драйвер TWI модуля

Я объяснил принцип работы с TWI по прерываниям, теперь расскжу о драйвере, в котором объединено все выше описанное. 

Драйвер состоит из двух файлов twim.h и twim.c. В заголовочном файле twim.h определена частота микроконтроллера и размер передающего/приемного буфера TWI модуля, статусные коды для ведущего устройства и прототипы функций. В сишной файле twim.c описана реализация пользовательских функций и обработчик прерывания TWI модуля.

Пользовательские функции драйвера.

uint8_t TWI_MasterInit(uint16_t fr) - инициализирует TWI модуль и устанавливает частоту SCL сигнала. Возвращает TWI_SUCCESS, если требуемую скорость удалось установить и 0, если не удалось. Скорость задается в килогерцах (от 1 до 400). Вызывается в начале main функции.

Пример:


/*установить скорость обмена 100 кГц*/
TWI_MasterInit(100); 


void TWI_SendData(uint8_t *msg, uint8_t msgSize) - сохраняет сообщение и инициирует передачу/прием данных по TWI/I2C шине, формируя состояние СТАРТ и разрешая прерывания. В качестве параметров принимает указатель на буфер, в котором хранится сообщение, и количество байтов, которые нужно передать/принять. Первый байт сообщения для функции должен содержать адрес ведомого устройства и бит R/W.

Функция может вызываться после функции инициализации и глобального разрешения прерываний.

Пример:

uint8_t buf[8];
...

TWI_MasterInit(100);
__enable_interrupt();

/*подготавливаем сообщение*/
buf[0] = (DS1307_ADR<<1)|0;
buf[1] = 0;
buf[2] = 0;
buf[3] = 0;
buf[4] = 0;

/*отправляем его*/
TWI_SendData(buf, 5);


uint8_t TWI_GetData(uint8_t *msg, uint8_t msgSize) - получить принятые данные. Она просто копирует принятые ранее данные в буфер msg. Переменная msgSize определяет сколько байт нужно скопировать. Функция возвращает 0, если сообщение не было принято и 1 если сообщение получено.

Эта функцию нужно вызывать после функции TWI_SendData(..), которой был передан адресный пакет SLA-R.

Пример:

/*подготавливаем сообщение*/
buf[0] = (DS1307_ADR<<1)|1; //бит R/W установлен в 1
buf[1] = 0;

/*отправляем его*/
TWI_SendData(buf, 5);
//в байты buf[1]..buf[4] запишется принятое сообщение

/*копируем данные из буфера драйвера в свой*/
TWI_GetData(buf, 5);


uint8_t TWI_GetState(void) - возвращает статус TWI модуля.

TWI_NO_STATE - неопределенное состояние
TWI_SUCCESS - сообщение передано/получено
TWI_MTX_ADR_NACK - передан пакет SLA+W и не получено подтверждение
TWI_MRX_ADR_NACK - передан пакет SLA+R и не получено подтверждение
TWI_MTX_DATA_NACK - передан байт данных и не получено подтверждение
TWI_BUS_ERROR - ошибка на шине из-за некорректных состояний СТАРТ или СТОП

Пример использования драйвера ведущего TWI устройства смотрите в тестовых проектах. 

Файлы

DS1307-iar-3.rar
DS1307-proteus.rar
DS1307-AS6-3.rar
DS1307-winavr-3.rar
DS1307-cv-3.rar


Comments   

# foxit 2013-11-22 14:34
Спасибо.
Ждем проекты для winavr.

Интересует точность хода часов.
Как организовать коррекцию(ручну ю и автоматическую)?
Как привязать часы к времени UTC?
# Pashgan 2013-11-23 20:20
Quote:
Интересует точность хода часов.
Как организовать коррекцию(ручну ю и автоматическую)?
Как привязать часы к времени UTC?
Это уже не по теме TWI. С этим я не разбирался.
# Ferrocen 2013-12-17 19:14
Code:/*подготавливаем сообщение*/
buf[0] = (DS1307_ADR<<1)|1; //бит R/W установлен в 1
buf[1] = 0;

/*отправляем его*/
TWI_SendData(buf, 5);
//в байты buf[1]..buf[4] запишется принятое сообщение

/*копируем данные из буфера драйвера в свой*/
TWI_SendData(buf, 5);

Подправьте,пожалуйста, последнюю строку в соответствии с приемом данных.

PS: пардон за первых два "кривых" сообщения – проглядел теги Code: .
# Pashgan 2013-12-20 01:48
А что с ней не так?
# Ferrocen 2013-12-24 17:38
Как я понимаю должно быть:
Code:/*копируем данные из буфера драйвера в свой*/
TWI_GetData(buf, 5);
# Pashgan 2013-12-25 15:03
Да, точно. Спасибо. Поправил.
# Aleksei 2014-01-01 10:38
Может и глупый вопрос, но как правильно прочесть данные из регистра, хранящего 16-битное знаковое значение? Аппаратно предусмотрено два 8-битных регистра для хранения старшего и младшего байтов.
# JoJo 2014-01-01 13:51
Читаешь по очереди, потом склеиваешь.
Наверное так
int tmp = ((int)hi_reg
# JoJo 2014-01-01 13:52
Code:
int tmp = ((int)hi_reg<<8)|(low_reg);
# dmitr-panov 2014-01-12 03:58
упс - не туда
# dmitr-panov 2014-01-12 05:49
Code:
/*подготавливаем сообщение*/
buf[0] = (DS1307_ADR<<1)|0; //адресный пакет
buf[1] = 0; //адрес регистра
buf[2] = (5<<4)|5; //значение секунд
buf[3] = (5<<4)|9; //значение минут
buf[4] = 0; //значение часов

/*отправляем его*/
TWI_SendData(buf, 5);

А как отправить число месяц и год?
# Pashgan 2014-01-14 08:32
Задать адрес регистра DS1307 - 0x04. Записать в остальные байты буфера число месяц и год.
Code:
/*подготавливаем сообщение*/
buf[0] = (DS1307_ADR<<1)|0; //адресный пакет
buf[1] = 0x04; //адрес регистра дней
buf[2] = (1<<4)|4; //день - 14
buf[3] = (0<<4)|1; //месяц - 01
buf[4] = (1<<4)|4; //год - 14

/*отправляем его*/
TWI_SendData(buf, 5);
# dmitr-panov 2014-01-15 12:50
спасибо
# Plotnik 2014-01-19 07:59
Работаю в AS6,подключил 7сегм.индикатор 4разряда. Вывожу секунды-ds1307 считает так-1,2,3,4,5,6 ,7,8,9,16,17,18 ... до 87,т.е. в каждом десятке-пропуск ается по 7 секунд.Да и минуты "уходят" ...77 и более.Подключен о:"twi.h","indi cator_2f.h".Как -то странно...
# dmitr-panov 2014-01-20 03:31
ds1307 хранит время в двоично-десятич ном коде.
Я так делаю:
Code:
IND_Time(hour, min);

void IND_Time(unsigned char n1, unsigned char n2)
{
data[0]=number[n2&0xf]; //единицы
data[1]=number[n2>>4]; //десятки
data[2]=number[n1&0xf];//единицы
data[3]=number[n1>>4]; //десятки
}
# dim 2014-04-15 14:56
а будут примеры на AS или WinAvr?
а то IAR многим не знаком
# Pashgan 2014-04-16 01:36
Если потребность есть, могу сделать.
# dim 2014-04-16 12:28
Если не трудно, конечно)
Единственно не разобрался как часть библиотеки twim " Обработчик прерывания TWI модуля" переписать под winavr, поэтому спросил.
Учимся на твоих статьях!
# Pashgan 2014-04-16 15:52
Завтра залью.
# dim 2014-04-18 12:08
Благодарю, все проще, чем думалось
# Дмитрий Е 2014-06-10 06:23
Здравствуйте. Спасибо за статью. Запрограммирова л atmega328P на плате arduino. Все работает нормально опрашиваются данные с ds1307. Но через какое-то время контроллер шину SDA подтягивает в ноль и перестает идти тики SCL. Контроллер уходит в нокаут. У кого-нибудь была такая проблема?
# Дмитрий Е 2014-06-10 16:48
На одной печатной плате у меня установлены ds1307 и ds1621. C термометром плата общается на ура, а вот с часами от 20 до 150 опросов параметров и все контроллер обваливается... .
# Pashgan 2014-06-14 09:50
У меня такого не было. Я бы сделал вывод отладочной информации на комп и посмотреть в какой момент происходит "обвал".
# foton6 2015-05-10 16:46
Было(есть) такое. Иногда почему то не ставится бит TWINT и естественно прога дальше не может работать, покрайней мере та часть которая отвечает за TWI. Никак не пойму почему бит не ставится, сижу осцилом на шине, вроде все ок. Организjвал что-то типо WDT.
P.S.
Своевременный ответ^^
# DmAlex 2014-06-29 06:15
Спасибо за драйвер.
Но при компиляции у меня ошибки, правда я драйвер этот засунул в RTOS. Вроде работает, но глюки при сравнении значении с датчика
../GCC-RTOS.c:90:2: warning: implicit declaration of function 'TWI_SendData'
../GCC-RTOS.c:97:2: warning: implicit declaration of function 'TWI_GetData'
../GCC-RTOS.c:150:1: warning: implicit declaration of function 'TWI_MasterInit '
# DmAlex 2014-06-29 06:30
а точней, вытаскиваю данные с датчика
hourOutDS1306 = (((buf[3] & 0xF0) >> 4)*10)+(buf[3] & 0x0F);
затем сравниваю
if (hourOutDS1306
# loki_amorf 2014-11-22 16:54
Спасибо за познавательный код.
Павел, подскажите как можно модернизировать код чтобы при отсутствии связи с подчиненным устройством, контроллер не зависал. Какой-то тайм аут отсутствия ответа.
# Рустем 2014-12-18 04:31
как то так
Code: time=0;
while (!(TWCR & (1<<TWINT))&&time<255)time++; //ждем


а вообще конечно лучше через прерывание запускать тви
# Рустик 2014-12-18 04:02
Почему нигде нету такого кода для ведомого?
Был бы очень признателен, если бы кто нибудь ткнул носом :)
# _Артём_ 2014-12-18 07:41
Quoting Рустик:
Почему нигде нету такого кода для ведомого?
Был бы очень признателен, если бы кто нибудь ткнул носом :)


На сайте Атмела есть: AVR311: Using the TWI module as I2C slave on tinyAVR and megaAVR devices
# Рустик 2014-12-18 08:04
Спасибо!, посмотрю.
# Andreas 2015-01-31 12:59
Спасибо ОГРОМНОЕ !!! Очень помогло.
# loki_amorf 2015-02-12 21:37
Пробовал прикрутить библиотеку для работы с внешней I2C EEPROM, оказалось что средствами библиотеки не удается читать нулевой байт buf[0], функция TWI_GetData в buf[0] возвращает ранее переданный адрес. Для часов это конечно же неважно.
# loki_amorf 2015-02-13 07:16
Quoting loki_amorf:
Пробовал прикрутить библиотеку для работы с внешней I2C EEPROM, оказалось что средствами библиотеки не удается читать нулевой байт buf[0], функция TWI_GetData в buf[0] возвращает ранее переданный адрес. Для часов это конечно же неважно.

Потрите пожалуйста сообщение, все работает EEPROM читается, датчик температуры LM75 читается. Видимо заработался вчера вечером. :)
# Алексей_жиленков 2015-05-22 07:21
Почему в функции TWI_SendData в цикли
for (i = 1; i < msgSize; i++){
twiBuf = msg;
}
переменная i начинается с 1 ?
# Рустем 2015-05-22 07:28
наверное потому что 0 байт идет с адресом и направлением
# Алексей_жиленков 2015-05-22 07:39
то есть нулевой байт адреса нужно отправлять отдельно?
# Алексей 2015-08-27 09:33
Как можно понять когда передан последней байт по twi? При использовании прерывания twi.
# Григорий11 2016-01-26 17:57
Когда ptr == msgSize
# бухомуха 2015-12-19 14:39
ребята, какие вы умные
# ПавелCC 2016-11-17 14:05
Всем привет. я только начинающий поэтому прошу объяснить мне. Я использую код из этой статьи для подключения дисплея

и если ставлю строку /*ждем ,когда TWI модуль освободится*/
while(TWCR & (1
# ПавелCC 2016-11-17 14:07
[quote name="ПавелCC"] Всем привет. я только начинающий поэтому прошу объяснить мне. Я использую код из этой статьи для подключения дисплея

и если ставлю строку /*ждем ,когда TWI модуль освободится*/
Code: /*ждем ,когда TWI модуль освободится*/
while(TWCR & (1<<TWIE));
# ПавелCC 2016-11-17 14:10
Всем привет. я только начинающий поэтому прошу объяснить мне. Я использую код из этой статьи для подключения дисплея

и если ставлю строку
Code: /*ждем ,когда TWI модуль освободится*/
while(TWCR & (1<<TWIE));


то дисплей не включается, а если ее закоментить то все работает.

всем заранее спасибо.

У вас недостаточно прав для комментирования.