«Шаг 5.9. CRC, паразитное питание и всякая всячина. MikroC и MikroPascal. Сайт о микроконтроллерах AVR.RU», версия для печати. Исходный документ: https://avr.ru/beginer/c_and_pascal/step5_9

Шаг 5.9. CRC, паразитное питание и всякая всячина

Итак, казалось, все было отлично. Датчик выдавал нам пусть не особо точную, но все же температуру, потягивал потихонечку питание со своего выхода на +5V, и все радовались жизни.

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

Ну что ж. Разберемся со всем по порядку.

Точность.

Эта часть несколько раз переписывалась, и мы решили изменить практически все. Ох.

Мы практически полностью разделили функции. Так, теперь getTemp у нас только считывает температуру в глобальную переменную, а convertTemp - отдельно преобразует её - тоже в глобальную переменную, и так далее.

Мы даже засунули датчик в холодильник (оооооооооо, это было чудесно-удивительно!!!) и долго плясали с переходом на темную отрицательную сторону)

Итак, основная задача - чтобы работало по следующей формуле:

Расчёт температуры
Рис. 1. Расчёт температуры (/int/Files/Picture/beginners/devices/ds1820/temp_accurate.PNG)

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

struct tempMemory //структура памяти датчика
{
    int TEMP;   //значение температуры
    byte EEPh;  //старший байт энергонезав. памяти датчика
    byte EEPl;  //младший байт энергонезав. памяти датчика
    byte reserve1;  //два зарезервированных байта
    byte reserve2;
    byte countRemain;   //сколько "шагов" осталось до целого градуса
    byte countPerC; //сколько всего "шагов" - в ds1820 16
    byte CRC;   //значение CRC
}       ourSensor;

Вот так.

Соответственно, поменяется и считка памяти. В функции getTemp после подачи всяких команд чтение уже 9 байт будет выглядеть так:

  byte* currentByte;    //указатель, куда записываем текущий байт
  ...
  currentByte = (char*)&ourSensor;  //поставили указатель в начало структуры
  for(i = 0; i < TempMemoryCount; i++)   //считываем память датчика в структуру. Умничка записывает int
                                       //правильно - сначала младший байт, потом старший, как и надо
  {
    *currentByte = OWRead(bPortTemp, bPinTemp);
    CRC = getCRC(CRC, *currentByte);  //проверка CRC
    currentByte++;  //перевели указатель
  }

Вот и отлично, все получается. Самое главное - что правильно записывается int, то есть в памяти он хранится как сначала младший байт, а потом - старший, как и в памяти датчика. Да-да, AVR являются "остроконечниками (http://ru.wikipedia.org/wiki/%CF%EE%F0%FF%E4%EE%EA_%E1%E0%E9%F2%EE%E2)", как и вся x86 платформа. То есть порядок байт в памяти подчиняется простому мнемоническому правилу: старший адрес, старший байт.

Теперь о преобразовании температуры, нашей новой функции convertTemp. Во-первых, сначала надо отбросить значение "дробной части" - младшего бита нашего int-вого значения. Самое простое - это просто сдвинуть на один бит вправо, но нужно не забыть сохранить знак (помним о холодильнике!). А потом - просто использовать нашу формулу, и все будет хорошо. Напоминаю, что функция пишет все в глобальную переменную fTemp. (Сразу хочу предостеречь: Некоторые "умники" пытаются заменить сдвиг целочисленным делением на 2. Так делать нельзя! На отрицательных значениях результат такого сдвига делением ависит от реализации алгоритма и может "округлить" результат в неправильную сторону ><)

//перевод температуры в красивый вид
//данные получаем из структуры ourSensor, запись - в fTemp
void convertTemp()
{
  //разбираемся с целой частью - убираем младший бит, но сохраняем знак
  fTemp = (int)((ourSensor.TEMP & 0x8000) | (ourSensor.TEMP >> 1));  //без конкретного указания типа не работает(
  //разбираемся с дробной частью - как по даташиту
  fTemp = fTemp - 0.25 + (float)(ourSensor.countPerC - ourSensor.countRemain) / ourSensor.countPerC;
}

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

Краткий экскурс - crc.

CRC - циклический избыточный код; число, с помощью которого можно проверить целостность передачи или хранения информации. То есть после передачи мы подсчитываем по определённому алгоритму, использующему все прочитанные байты (иногда биты), и сравниваем с присланным значением. Ну или используем такое свойство crc, что при пропускании через алгоритм самого значения crc в качестве «нового байта» он выдает нам ноль.

//получение CRC
//на вход - считаемое уже crc - его мы и изменяем, новый байт
byte getCRC(byte CRC, byte NewByte)
{
   const byte poli = 0b10001100; //полином, с помощью которого высчитываем CRC8
   byte i;  //счётчик
   for(i = 0; i < 8; i++)
   {
      if((CRC ˆ NewByte) & 0x01) CRC = (CRC >> 1) ˆ poli; //младший бит CRC не равен пришедшему биту
														// - исключающее ИЛИ
      else CRC = CRC >> 1; //равны - просто сдвиг
      NewByte = NewByte >> 1;
   }
   return CRC;
}

Итак, подсчитали crc. Теперь о «сигнализации» ошибок: Можно, конечно, использовать байтовую переменную и обозначать ошибки типа «1», «2» и так далее. Но так же непонятно! На это есть перечисления - enum:

//перечисление ошибок
enum err { ER_Not_errs, //нет ошибок
           ER_Not_found, //датчик не найден
           ER_Bad_crc   //неверное значение CRC
           } errCurrent;

Таким образом, глобальная переменная errCurrent типа enum err (да-да, для перечислений нужно слово enum) имеет значения ER_Not_errs, ER_Not_found, ER_Bad_crc. При этом на самом деле ER_Not_errs соответствует 0, ER_Not_found - 1, а ER_Bad_crc - 2. Но так значительно удобнее) Итак, при измерении температуры мы сначала считаем, что ошибок нет - errCurrent равна 0, а в случае ошибок изменяем эту глобальную переменную. А уже в main-е определяем, печатаем ли мы температуру, или же вызываем функцию, обрабатывающую ошибки:

//функция передаёт сообщение об ошибке (errCurrent) через USART, который уже должен быть готов к работе
void sendErr()
{
  char sTemp[15];
  switch(errCurrent)
  {
   case ER_Not_found:   //датчик не найден
   {
    strcpy(sTemp, "Not found");
    break;
   }
   case ER_Bad_crc:  //crc не сходится
   {
    strcpy(sTemp, "Bad crc");
    break;
   }
  }
  UART1_Write_Text(sTemp);
}

А вот теперь о паразитном питании...

... нашем горе-печали. В попытках реализовать было, кстати, немножечко сведено с ума два датчика. Теперь они считают, что паразитное питание ВСЕГДА. ОМГ. Но это уже были пляски с транзистором - в даташите он присутствует на схеме. На самом деле он вообще не обязателен. Чтобы понять, откуда ноги растут, необходимо окунуться в историю. Хотя датчик DS1820 можно запитывать через линию 1-wire, тока, протекающего через резистор подтяжки, явно недостаточно. Казалось бы, простое решение: передавать в датчик энергию, просто переключая ножку процессора в логическую "1" и подавая, тем самым, положительный потенциал на линию 1-wire. Но это было не всегда приемлемо, ибо в старых микроконтроллерах и ток через ножку был ограничен, и напряжение логической "1" было около 4.5 вольт, а вовсе не 5. Именно из-за этого приходилось применять дополнительный транзистор, включенный как усилитель, подающий в нужный момент питание на вход 1-Wire полной мерой. Однако в современных контроллерах и нагрузочная способность выше, и напряжение ближе к напряжению питания. Поэтому-то при включении ds1820 по схеме с паразитным питанием можно обойтись и без транзистора. Достаточно просто подключить выход VCC датчика к земле (1 и 3 выводы закоротить на землю).

К сожалению, теоретическим выкладкам не суждено было стать реальностью. Проблема была в программе. А точнее, в библиотеке. После некоторого чесания маковки - то есть прыжков с осциллографом, переписывания нашей программки, ритуального питья чая со сгущенкой и прочих полезных действий - было выяснено что стандартная библиотека вместо состояния логической 1 переводила ножку в высокоимпедансное Z-состояние, когда микроконтроллер только "слушает", никак не влияя на линию. (ddrX = 0; portX = 0;) Логическая 1 в таком состоянии получалась исключительно из-за подтяжки линии резистором 4.7k к питанию... Поэтому датчики недополучали питания и все было печально(

Так уж получилось, что стандартная библиотека, строго соответствуя заветам 1-Wire, была предназначена исключительно для внешнего питания. В результате, было принято решение таки написать свою библиотечку 1-Wire, ибо встроенная, к несчастью, закрыта.

С другой стороны, чисто теоретически (хотя по-хорошему протокол на то и протокол, чтобы недопускать такие вещи), "мастер" может послать на линию эту самую "1", а датчик в это время - выдавать что-нибудь. И послать "0". И тогда ой-ей, короткое замыкание, быдыщ-быдыщ! Запомните, дети, нельзя соединять два выхода вместе, если на одном может быть установлена логическая 1, тогда как на другом логический 0. Иначе и ваша коллекция пополнится забавными датчиками, питающимися только per rectum - как у нас)

В общем, и пишем на свой страх и риск) Ну, на самом деле пришлось не писать вчистую, а переписать все с Паскаля - MayDay (мастер MayDay =ˆˆ=) уже давно написал вот тут (/ready/inter/1wire/specials). В результате получено следующее - 1wire.h:

//переименование типов
typedef unsigned char byte;
typedef unsigned int word;

//состояние устройства
const byte owPresent = 0;  //устройство есть на шине
const byte owNoPresent = 1;             //устройства нет на шине
const byte owShort = 2;  //короткое замыкание - горе-печаль

const byte maxWaiting = 30;   //максимальное время ожидания ответа

//задержечки
const word dlReset = 500;        //задержка для ресета (длина импульса сброса)
const byte dlWait = 9;  //задержка между проверками линии
const byte dlShort = 1; //таймслот
const byte dlCheck = 60;   //передача данных
const byte dlRead = 200;   //задержка при чтении бита с устройства

//"расстояние" между адресами
const byte deltaDdrPort = 1;
const byte deltaPinPort = 2;

//уровень
const byte isOff = 0;   //на шине установлен ноль
const byte isOn = 1;    //на шине установлена единица

//какие функции у нас есть в библиотечке
byte OWReset(byte*, byte);
void OWWrite(byte*, byte, byte);
byte OWRead(byte*, byte);

//ожидание ответа устройства.
//выдает 1, если ответ совпал с ожидаемым до таймаута, 0 - иначе
//port - указатель на порт, pin - номер вывода, где расположена шина
//respond - ожидаемый ответ.
byte OWCheckRespond(byte* portX, byte pin, byte respond)
{
  byte* pinX;   //порт входа
  byte bTime;   //счетчик времени

  pinX = portX - deltaPinPort; //определили порт входа
  bTime = 0; //счетчик на ноль

  while(bTime < maxWaiting) //пока не превышен лимит ожидания
  {
    Delay_us(dlWait); //ждем
    if(((*pinX >> pin) & 1) == respond) break;   //выходим из цикла
    bTime++; //прибавляем
  }

  return(bTime < maxWaiting);
}

//проверка наличия подчинённого устройства
//port - указатель на порт, pin - номер вывода, где расположена шина
//результат: состояние устройства (см. заголовок в константах)
byte OWReset(byte* portX, byte pin)
{
  //переменные
  byte* ddrX;  //порт направления - вот как это называется!
  byte* pinX;  //порт входа
  byte shift;   //сдвиг 1 << pin

  ddrX = portX - deltaDdrPort;    //а вот так! в памяти инфа о портах лежит так: pin-ddr-port. 
									//Этим и пользуемся, переходя указателем
  pinX = portX - deltaPinPort;

  shift = 1 << pin;

  //подаем импульс сброса
  *ddrX = *ddrX | shift;   //ногу на выход
  *portX = *portX | shift;  //даем питалово
  Delay_us(dlReset);      //задержка перед сбросом - будь готов!
  *portX = *portX & (˜shift);   //переводим лапу на низкий уровень
  Delay_us(dlReset);            //импульс сброса!!!

  *portX = *portX | shift;  //даем питалово
  *ddrX = *ddrX & (˜shift); //ногу на вход с подтяжкой

  //ждем приветствия
  if(OWCheckRespond(portX, pin, isOff))   //проверяем, отвечают ли нам на сброс
  {
    if(OWCheckRespond(portX, pin, isOn))   //проверяем, возвращается ли потом уровень на единицу
      {
         return(owPresent);      //ура, устройство есть на шине
      }
      else return(owShort);    //короткое замыкание
  }
  else return(owNoPresent);     //никто нам не ответил
}

// запись байта в устройство
// Port, Pin - номер порта и вывода с подключенным устройством
// Value - записываемый байт.
void OWWrite(byte* portX, byte pin, byte value)
{
  byte* ddrX; //порт назначения
  byte i;  //счётчик
  byte sregOld;  //хранит изначальное значение регистра состояния Sreg - мы запрещаем прерывания
  byte sendingBit;        //передаваемый бит
  byte shift; //сдвиг 1 << pin
  byte lineIn1, lineIn0; //значение порта, когда по одноваре 1, когда 0

  sregOld = Sreg;   //Sreg - регистр состояния
  Sreg.B7 = 0;   //7 разряд - разрешены(1)/запрещены(0) прерывания

  shift = 1 << pin;

  //вычисляем соответствующие порту адреса
  ddrX = portX - deltaDdrPort;      //порт направления

  *ddrX = *ddrX | shift; //вывод на выход
  *portX = *portX | shift; //переводим в 1
  delay_us(dlWait); //немного ждём

  lineIn1 = *portX | shift;   //Port.pin = 1;
  lineIn0 = *portX & (˜shift);   //Port.pin = 0;

  for(i = 0; i < 8; i++)  //процесс передачи данных
  {
    sendingBit = (value >> i) & 1;  //определяем записываемый бит
    *portX = lineIn0; //переводим линию в 0 - знак начала записи бита
    delay_us(dlShort);   //ждем один таймслот
    //!!!
    if(sendingBit == 1)  //если записываем единицу, то поднимаем линию
    {
       *portX = lineIn1;
    }
    delay_us(dlCheck);   //передаем положенное время
    *portX = lineIn1; //поднимаем в 1 !!!
    delay_us(dlShort);   //перерыв между битами
  }
  Sreg = sregOld;
}

//чтение байта из устройства
//Port, Pin - номер порта и вывода с подключенным устройством
//результат - считанный байт
byte OWRead(byte* portX, byte pin)
{
  byte *ddrX, *pinX; //соответствующий порт направления и порт входа
  byte i;  //счётчик
  byte sregOld;  //хранит изначальное значение регистра состояния Sreg - мы запрещаем прерывания
  byte lineIn1, lineIn0; //значение порта, когда по одноваре 1, когда 0
  byte rez;   //результат
  byte shift; //сдвиг 1 << pin

  sregOld = Sreg;   //запоминаем текущее значение регистра состояния
  Sreg.B7 = 0;   //запрещаем прерывания

  shift = 1 << pin;

  //вычисляем соответствующие порту адреса
  ddrX = portX - deltaDdrPort;      //порт направления
  pinX = portX - deltaPinPort;      //порт входов

  //устанавливаем соответствующий вывод в 1 на выход
  *portX = *portX | shift;
  *ddrX = *ddrX | shift;
  delay_us(dlRead);

  lineIn1 = *portX | (1 << pin); //portX.pin = 1;
  lineIn0 = *portX & (˜(1 << pin)); //portX.pin = 0;

  rez = 0;

  for(i = 0; i < 8; i++)  //начинаем читать
  {
    *portX = lineIn1;
    *ddrX = *ddrX | shift;  //переводим линию в состояние выхода, поднимаем в 1
    delay_us(dlShort);

    *portX = lineIn0;  //переводим линию в 0 - начало чтения
    delay_us(dlShort);   //небольшая задержка
    *ddrX = *ddrX & (˜shift); //начинаем слушать линию !!!
    *portX = lineIn1;  //с подтяжкой
    delay_us(dlWait); //ждем - устройство должно откликнуться
    rez = rez | (((*pinX >> pin) & 1) << i); //запоминаем таким хитрейшим образом результат
    delay_us(dlCheck);   //ждем перед следующим чтением байта
  }

  Sreg = sregOld;
  return rez;
}

Вот так вот. Зато теперь понятно, откуда рога растут) Этот файлик 1wire.h просто добавляем в проект - Project - Add File To Project А в самом начале нашего проектного файла .c пишем строчку #include "1wire.h" Получили следующее:

//считка показателей температуры с датчика DS18s20 (внешнее питание) по протоколу 1-Wire (к порту C.0)\
// и отсылка значения через UART (RXD - D.0, TXD - D.1). Используется микросхема ATmega32

#include "1wire.h"   //подключили библиотечку
//переименование типов - теперь в библиотеке

//объявление констант
byte ddrUART at ddrD;   //порт, к которому подключён UART
byte portUART at portD;
const byte pinRXDNumber = 0;  //номер вывода RXD
const byte pinTXDNumber = 1;  //номер вывода TXD
const byte UARTFrequency = 19200; //частота передатчика
const byte ddrUARTInit = (0xFF | (1 << pinTXDNumber)) & (˜(1 << pinRXDNumber));
const byte portUARTInit = 0xFF;  //заодно подтяжка для RXD

byte* bPortTemp = (byte*) &portC; //порт, к которому подключён датчик температуры
sbit ddrTemp at ddrC.B0;  //конкретный порт назначения
sbit portTemp at portC.B0; //конкретный порт вывода
const byte bPinTemp = 0; //номер вывода, к которому присоединяется датчик

const byte TempMemoryCount = 9;  //количество байт в памяти датчика

struct tempMemory //структура памяти датчика
{
    int TEMP;   //значение температуры
    byte EEPh;  //старший байт энергонезав. памяти датчика
    byte EEPl;  //младший байт энергонезав. памяти датчика
    byte reserve1;  //два зарезервированных байта
    byte reserve2;
    byte countRemain;   //сколько "шагов" осталось до целого градуса
    byte countPerC; //сколько всего "шагов" - в ds1820 16
    byte CRC;   //значение CRC
}       ourSensor;

float fTemp;   //значение температуры

//задержки
const word ConvertDelay = 1000;   //для конвертации - для нормальных датчиков достаточно и 1 мс за глаза
const byte DelayTime = 400; //задержка перед тем, как снова считать температуру
//const word WelcomeDelay = 1;  //для приветствия - датчику же нужна энергия для начала работы! <- 
								//прыжки с бубном у исходной библиотеки

//перечисление ошибок
enum err { ER_Not_errs, //нет ошибок
           ER_Not_found, //датчик не найден
           ER_Bad_crc   //неверное значение CRC
           } errCurrent;

//получение CRC
//на вход - считаемое уже crc - его мы и изменяем, новый байт
byte getCRC(byte CRC, byte NewByte)
{
   const byte poli = 0b10001100; //полином, с помощью которого высчитываем CRC8
   byte i;  //счётчик
   for(i = 0; i < 8; i++)
   {
      if((CRC ˆ NewByte) & 0x01) CRC = (CRC >> 1) ˆ poli; //младший бит CRC не равен пришедшему биту
														// - исключающее ИЛИ
      else CRC = CRC >> 1; //равны - просто сдвиг
      NewByte = NewByte >> 1;
   }
   return CRC;
}

//перевод температуры в красивый вид
//данные получаем из структуры ourSensor, запись - в fTemp
void convertTemp()
{
  //разбираемся с целой частью - убираем младший бит, но сохраняем знак
  fTemp = (int)((ourSensor.TEMP & 0x8000) | (ourSensor.TEMP >> 1));  //без конкретного указания типа не работает(
  //разбираемся с дробной частью - как по даташиту
  fTemp = fTemp - 0.25 + (float)(ourSensor.countPerC - ourSensor.countRemain) / ourSensor.countPerC;
}

//функция считывает память датчика, расположенного на выводе PortTempNumber порта bPortTemp,
//в структуру ourSensor
void getTemp()
{
  byte bCheck; //проверка, считывается ли информация с датчика
  int i; //счётчик
  byte CRC = 0; //проверка CRC
  byte* currentByte;    //указатель, куда записываем текущий байт

  //константы для работы с датчиком
  const byte SkipROM = 0xCC;  //Команда ROM - "Пропуск ROM"
  const byte ConvertTemp = 0x44;        //Функциональная команда - "Конвертировать температуру"
  const byte ReadMemory = 0xBE;         //Функциональная команда - "Чтение памяти"

  bCheck = OWReset(bPortTemp, bPinTemp);
  if(bCheck)   //если ошибка - не сумели прочитать
  {
    errCurrent = ER_Not_found;  //отправим по UASRT ошибку
    return;
  }
  OWWrite(bPortTemp, bPinTemp, SkipROM);     // Посылаем команду ROM - "Пропуск ROM"
  OWWrite(bPortTemp, bPinTemp, ConvertTemp); // Посылаем функциональную команду "Конвертировать температуру"

  VDelay_ms(ConvertDelay); //задержка для конвертации температуры

  bCheck = OwReset(bPortTemp, bPinTemp);     // Перезагружаем
  OWWrite(bPortTemp, bPinTemp, SkipROM);     // Посылаем команду ROM - "Пропуск ROM"
  OWWrite(bPortTemp, bPinTemp, ReadMemory);  // Посылаем функциональную команду "Чтение памяти"

  currentByte = (char*)&ourSensor;  //поставили указатель в начало структуры
  for(i = 0; i < TempMemoryCount; i++)   //считываем память датчика в структуру. Умничка записывает int
                                       //правильно - сначала младший байт, потом старший, как и надо
  {
    *currentByte = OWRead(bPortTemp, bPinTemp);
    CRC = getCRC(CRC, *currentByte);  //проверка CRC
    currentByte++;  //перевели указатель
  }
  
  if(CRC)   //если CRC не сошлось - ошибка!
  {
      errCurrent = ER_Bad_crc;
      return;
  }
}

//функция передаёт сообщение об ошибке (errCurrent) через USART, который уже должен быть готов к работе
void sendErr()
{
  char sTemp[15];
  switch(errCurrent)
  {
   case ER_Not_found:   //датчик не найден
   {
    strcpy(sTemp, "Not found");
    break;
   }
   case ER_Bad_crc:  //crc не сходится
   {
    strcpy(sTemp, "Bad crc");
    break;
   }
  }
  UART1_Write_Text(sTemp);
}

void main()
{
  //константы для непосредственной передачи значения с датчика по UART
  const byte TempLen = 15;   //длина строки со значением температуры
  char sTemp[TempLen];  //строка со значением температуры
  char sTextBeforeTemp[] = "Temp = ";   //текст-объяснение перед непосредственно самим значением температуры

  //инициализируем ошибку - нет ошибки
  errCurrent = ER_Not_errs;
  //инициализация портов
  ddrUART = ddrUARTInit; //TXD - на вывод, RXD - на вход, остальные - на выход
  portUART = portUARTInit;

  //инициализируем передатчик на определённой частоте
  UART1_Init(UARTFrequency);

  while(1)
  {
    errCurrent = ER_Not_errs; //сначала считаем, что без ошибок
    getTemp();   //записали в структуру ourSensor память датчика
    if(errCurrent == ER_Not_errs)   //если без ошибок
    {
      convertTemp();  //конвертируем температуру
      UART1_Write_Text(sTextBeforeTemp);
      sprintf(sTemp, "%.3f", fTemp);   //записываем в sTemp температуру с 3 знаками после .
      UART1_Write_Text(sTemp);
    }
    else sendErr();
    UART1_Write(0x0A);  //переходим на новую строку
    delay_ms(DelayTime); //небольшая задержка
  }
}

 

Вот так. Зато работает и при внешнем, и при паразитном питании)
Вот программка на Си (/int/Files/Dounload/c_and_pascal/step5/step5.9 - program.rar), а тут - прошивка (/int/Files/Dounload/c_and_pascal/step5/step5.9 - hex.rar).




Автор - Moriam (http://forum.avr.ru/member.php?u=69506)  совместно с котом MayDay (http://forum.avr.ru/member.php?u=2)
Обсудить на форуме (http://forum.avr.ru/mikroc-mikropascal-t35819.html)

Все права защищены © AVR.RU, 2021.