Предистория


Если дома есть Arduino, в гараже машина или мотоцикл, а то и хоть мотособака, в голове туманные представления о программировании — возникает желание измерить скорость движения или обороты двигателя, посчитать пробег и моточасы.


В данной статье я хочу поделиться своим опытом по изготовлению подобных поделок.


Картинка для привлечения внимания


// Сразу скажу, что код, указанный в статье не для копипаста, а для иллюстраций. 
// Я конкретно эти примеры не компилировал. У меня код ещё по проекту размазан.

Немного физики


Для измерения частоты вращения нам понадобится датчик положения колеса/вала/круга/итп. Датчик ставится как правило один. Возможно, что он будет срабатывать не один раз на каждый оборот. Например, у вас датчик Холла и 4 магнита на колесе. Таким образом, для правильного вычисления частоты нужно знать:


  • количество срабатываний датчика на один оборот К;
  • минимальная ожидаемая частота Мин.
  • максимальная ожидаемая частота Макс.

Вычисленная = ВычисляемЧастоту() / К;
Если (Частота < Мин) Частота = 0
Иначе Если (Частота < Макс) Частота = Вычисленная 

То есть, если частота меньше разумного минимума, то считаем, что она равна нулю, если больше максимума — игнорируем показания.


С количеством срабатываний понятно, но зачем ещё эти мины и максы? Давайте рассмотрим сначала варианты расчёта частоты.


Со скоростью всё проще, достаточно знать число ?, диаметр колеса, а частоту вращения мы уже знаем.


Болванка для кода


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


const byte fqPin = 2; // Для ATMega32 только 2 или 3.

volatile unsigned long counter = 0;

// Функция для обработки прерывания.
void ISR() {
  // Здесь код прерывания
  counter++; // Например
}

void setup() {
  Serial.begin(115200);
  // Подключаем функцию ISR на прерывание по появлению сигнала на ноге fqPin.
  attachInterrupt(digitalPinToInterrupt(fqPin), ISR, RISING);
}

void loop() {
  // Копируем данные. 
  noInterrupts();
  unsigned long cnt = counter;
  interrupts();
  // Здесь делаем что-то с полученными данными.
  // ...
  Serial.println(cnt);
  delay(1000);
}

Обратите внимание на модификатор volatile у переменной counter. Все переменные, которые будут изменяться в обработчике прерывания (ISR) должны быть volatile. Это слово говорит компилятору, что переменная может изменяться неожиданно и доступ к ней нельзя оптимизировать.


Функция ISR() вызывается каждый раз, когда появляется единица на ноге fqPin. Мы эту функцию не вызываем, это делает сам контроллер. Он это делает, даже когда основная программа стоит в ступоре на функции delay(). Считайте, что ISR() обслуживает событие, от вас не зависящее и данное вам свыше как setup() и loop(). Контроллер прерывает выполнение вашей программы, выполняет ISR() и возвращается обратно в ту же точку, где прерывал.


Обратите внимание, что в функции loop() мы отключаем прерывания вообще любые для того, чтобы прочитать переменную counter и сохранить её во временную переменную cnt. Потом, конечно же, включаем снова. Так мы можем потерять один вызов, конечно же, но с другой стороны, переменная unsigned long имеет 32 бита, а процессор ATMega32 8-битный, вряд ли он скопирует данные за один такт, а ведь в процессе копирования может случиться прерывание и часть данных изменится. По этой же причине мы копируем значение counter локально так как значение этой переменной при использовании в разных местах программы может быть разным опять же из-за изменения её в прерывании.


Тело функции ISR() должно быть максимально коротким, точнее, сама функция должна выполняться максимально быстро. Это важно, так как прерывается выполнение вашего кода, который может оказаться чувствительным к непредвиденным задержкам. Некоторые библиотеки отключают прерывания для выполнения чувствительных с задержкам операций, например для управления светодиодной лентой WS2812.


Считаем обороты за единицу времени.


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


Частота = ( Счётчик / Время ) / К

const byte fqPin = 2; // Для ATMega32 только 2 или 3.
const unsigned long interval = 1000000UL; // Интервал подсчёта в микросекундах
const int K = 1;

unsigned long oldMks = 0; // предыдущий момент времени

volatile unsigned long counter = 0;

// Функция для обработки прерывания.
void ISR() {
  // Здесь код прерывания
  counter++; // Например
}

void setup() {
  Serial.begin(115200);
  // Подключаем функцию ISR на прерывание по появлению сигнала на ноге fqPin.
  attachInterrupt(digitalPinToInterrupt(fqPin), ISR, RISING);
}

void loop() {
  // вычисляем текущий момент времени
  unsigned long mks=microseconds();
  // Получаем данные. 
  noInterrupts();
  unsigned long cnt = counter;
  counter = 0; // Сразу сбросим счётчик
  interrupts();
  // Выводим частоту в оборотах в секунду
  Serial.println( 1000000f * (float)cnt / (float)(mks-oldMks) / (float)K );
  // 1000000 микросекунд в секунде
  // далее по формуле выше.
  // mks-oldMks лучше, чем interval потому, что это реальное время от последнего
  // опроса счётчика, а interval -- предполагаемое.
  // Все целые переменные приводим в вещественные
  oldMks=mks; // Сохраняем время вычисления.
  // Спим пока копятся отсчёты до следующего вычисления
  delayMicroseconds(interval);
}

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


Для повышени точности на малой скорости можно увеличить число К, как это сделано, скажем, в автомобильной технике для датчика ABS. Можно увеличить время подсчёта. Делая и то и другое мы подходим ко второй проблеме — переполнению счётчика. Да, переполнение легко лечится увеличением количества бит, но арифметика процессора Arduino не умеет считать 64-битные числа столь быстро, как хотелось бы и как она это делает с 16-разрядными.


Увеличение времени расчёта тоже не очень хорошо тк нам надо знать частоту прямо сейчас, вот при нажатии на газ, а не через пару секунд. Да и через пару секунд мы получим скорее некое среднее значение. За это время можно несколько раз сделать врумм-врумм.


Есть другой метод. Он лишён вышеописанных недостатков, но, как водится, имеет свои.


Считаем интервал между отсчётами


Частота = 1 / ( Интевал * К )

Мы можем засечь время одного отсчёта и другого, вычислить разницу. Величина, обратная вычисленному интервалу и есть частота. Круто! Но есть минусы.


const byte fqPin = 2; // Для ATMega32 только 2 или 3.
const int K = 1;

volatile unsigned long interval;

// Функция для обработки прерывания.
void ISR() {
  // Здесь код прерывания
  static unsigned long oldTime; // Сохраняем предыдущее значение.
  unsigned long Time=microseconds();
  interval=Time-OldTime();
  oldTime=Time;
}

void setup() {
  Serial.begin(115200);
  // Подключаем функцию ISR на прерывание по появлению сигнала на ноге fqPin.
  attachInterrupt(digitalPinToInterrupt(fqPin), ISR, RISING);
}

void loop() {
  // Получаем данные. 
  noInterrupts();
  unsigned long cnt = interval;
  interrupts();
  // Выводим частоту в оборотах в секунду
  Serial.println( 1000000f / ( (float)K * (float)(cnt) );
  // 1000000 микросекунд в секунде
  // далее по формуле выше.
  // Все целые переменные приводим в вещественные
  // Спим, чтобы не заливать экран потоком данных
  // Четверть секунд -- хорошее время.
  delay(250);
}

Что делать, если наше колесо крутится еле-еле и измеренный интервал превышает разумные пределы? Выше я предложил считать частоты ниже разумного минимума за ноль.


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


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


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


Комбинированный метод


Можно попробовать объединить достоинства обоих методов.


Частота = Счётчик / ИнтервалИзмерения / К

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


const byte fqPin = 2; // Для ATMega32 только 2 или 3.
const int K = 1;

volatile unsigned long counter; // Количество отсчётов.
volatile unsigned long mks;     // Время последнего отсчёта.

unsigned long oldTime;  // Время последнего отсчёта в предыдущем вычислении.

// Функция для обработки прерывания.
void ISR() {
  // Здесь код прерывания
  mks=microseconds(); // Момент последнего отсчёта
  counter++;  // Количество отсчётов
}

void setup() {
  Serial.begin(115200);
  // Подключаем функцию ISR на прерывание по появлению сигнала на ноге fqPin.
  attachInterrupt(digitalPinToInterrupt(fqPin), ISR, RISING);
}

void loop() {
  unsigned long rpm;
  // Получаем данные. 
  noInterrupts();
  unsigned long cnt = counter;
  counter = 0;
  unsigned long tmr = mks;
  interrupts();
  // Выводим частоту в оборотах в секунду
  if (cnt > 0) {
    rpm = 1000000UL / ((tmr - oldTime) / counter) / K;
    oldTime = tmr;
  } else {
    rpm = 0;
  }
  Serial.println( rpm );
  delay(250);
}

Обратите внимание, что за интервал считается не время опроса, как в первом примере, а время от последнего отсчёта до предыдущего последнего отсчёта в прошлом опросе. Это заметно поднимает точность вычисления.


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


Если использовать кооперативную многозадачнось, то можно сделать подсчёт, скажем раз 100мс, а вывод на дисплей раз в 250мс. Очень короткий интервал опроса снизит чувствительность к низким частотам.


Как говорят в рекламе, "но это ещё не всё".


Ошибки дребезга


Для устрашения вас предположу, что измеряем частоту вращения двигателя от индуктивного датчика зажигания. То есть, грубо говоря, на высоковольтный провод намотан кусок кабеля и мы измеряем индукцию в нём. Это довольно распространённый метод, не правда ли? Что же здесь сложного может быть? Самая главная проблема — современные системы зажигания, они дают не один импульс, а сразу пачку.


Примерно так:



Но даже обычная система зажигания даёт переходные процессы:



Старинные же кулачковые контактные вообще показывают замечательные картинки.


Как с этим бороться? Частота вращения не может вырасти мгновенно, не даст инерция. Кроме того, в начале статьи я предложил ограничить частоту сверху разумными рамками. Отсчёты, что происходят слишком часто можно просто игнорировать.


МинимальныйИнтервал = ( 1 / ( K * МаксимальнаяРазумнаяЧастота) )

const byte fqPin = 2; // Для ATMega32 только 2 или 3.
const int K = 1;
const unsigned long maxFq = 20000; // rpm (оборотов в минуту)
const unsigned long minInterval = 1000000UL / ( K * maxFq ); // минимальный интервал в мкс 

volatile unsigned long counter; // Количество отсчётов.
volatile unsigned long mks;     // Время последнего отсчёта.

unsigned long oldTime;  // Время последнего отсчёта в предыдущем вычислении.

// Функция для обработки прерывания.
void ISR() {
  // Здесь код прерывания
  static unsigned long oldTmr; // сохраняем старый таймер
  unsigned long tmr=microseconds();
  if (tmr - oldTmr > minImterval) {
    mks=microseconds(); 
    counter++;
    oldTmr=tmr;  
  }
}

void setup() {
  Serial.begin(115200);
  // Подключаем функцию ISR на прерывание по появлению сигнала на ноге fqPin.
  attachInterrupt(digitalPinToInterrupt(fqPin), ISR, RISING);
}

void loop() {
  unsigned long rpm;
  // Получаем данные. 
  noInterrupts();
  unsigned long cnt = counter;
  counter = 0;
  unsigned long tmr = mks;
  interrupts();
  // Выводим частоту в оборотах в секунду
  if (cnt > 0) {
    rpm = K * 1000000UL / ((tmr - oldTime) / counter);
    oldTime = tmr;
  } else {
    rpm = 0;
  }
  Serial.println( rpm );
  delay(250);
}

Другой вид помех — это пропадание отсчётов. Из-за той же инерции у вас не может измениться частота в два раза за одну миллисекунду. Понятно, что это зависит от того, что вы собственно измеряете. Частота биения крыльев комара может, вероятно и за миллисекунду упасть до нуля.


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


Особенности измерения скорости движения и скорости вращения.


При измерении скорости вращения бензинового двигателя надо обязательно учесть величину К, которая совсем не очевидна. Например, вы намотали провод на кабель свечи и ожидаете, что там будет одна искра на один оборот. Это совсем не так. Во-первых, у 4-тактного двигателя вспышка происходит один раз на два оборота, у 2-тактного один раз на оборот коленвала. Во-вторых, для упрощения системы зажигания коммутатор подаёт искру на неработающие в данный момент цилиндры, типа на выпуске. Для получения правильного К надо почитать документацию на двигатель или подсмотреть показания эталонного тахометра.


При измерении скорости движения частота обновления дисплея не имеет большого значения, особенно, если вы рисуете цифры, а не двигаете стрелку. Даже обновление информации раз в секунду не вызовет отторжения. С оборотами двигателя всё наоборот, индикатор должен откликаться гораздо быстрее на изменение оборотов.


Вывод информации


Типичная обида начинающего разработчика автомобильной и мотоциклетной электроники "стрелки дёргаются, цифры нечитабельны" лечится простым способом — надо обманывать клиента. Вы что думаете, автомобильный тахометр всегда показывает вам правду? Конечно же нет! Хотя вам этот обман нравится и вы хотите, чтобы ваш прибор дурил голову так же.


Стрелки


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


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


Вот пример с нелинейным выводом показаний:


dispRPM(unsigned int rpm) {
  static unsigned int lastRpm;
  if (rpm > lastRpm) {
    // Увеличивается
    unsigned int disp = rpm - (lastRpm-rpm)/5; // быстро вверх
    outputRPM(disp); // Поворот стрелки
    lastRpm=disp;
  } else {
    // Уменьшается
    unsigned int disp = rpm - (lastRpm-rpm)/2; // медленно вниз
    outputRPM(disp); // Поворот стрелки
    lastRpm=disp;
  }
}

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


Цифры


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


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


Я предлагаю менять частоту вывода информации на дисплей в зависимости от степени изменения величины. Если обороты меняются, скажем, на 5% от последнего подсчёта, а не показа — можно затупить и показывать раз в 300-500мс. Если на 20%, то показывать раз в 100мс.


Можно огрубить шкалу и показывать только две значащие цифры


С учётом мототематики, можно довольно точно показывать обороты холостого хода как описано чуть выше и огрублять вывод на оборотах от двух холостых. На высоких оборотах для гонщиков важнее делать блинкеры типа "передачу вниз", "передачу вверх" и "ты спалишь движок". То есть держать двигатель около максимального крутящего момента и не дать ему крутиться выше максимальных разрешённых оборотов. Блинкеры замечательно делаются с помощью SmartDelay когда можно унаследовать от этого класса свой с заданной ногой контроллера и частотой мигания, там есть методы для переопределения и они вызываются раз в заданное время.


Идеи по отображению цифр тоже приветствуются в комментариях.


Вывод


Если наступить на все грабли, можно стать опытным разработчиком.

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


  1. Vanellope
    06.01.2018 05:30

    Применительно к тахометру, частота импульсов будет 166.6 Гц для 10 тысяч(!) об/мин. Для микроконтроллера даже с 1 MIPS просто много времени для обработки сигналов и подавления дребезга. Достаточно проверять состояние входа по прерыванию таймера раз в 1 мс. Сигнал с датчика через компаратор прогонять.


    1. nwwind Автор
      06.01.2018 12:18

      Для дребезга достаточно конденсатор поставить или LC фильтр лучше.
      Есть готовые микросхемки, я интересовался, но по памяти не скажу прямо сейчас.

      Я напоролся на дребезг с CDI Suzuki Djebel, там не просто искра горит какое-то время как на графике в статье, а идут подряд несколько вспышек с чётким интервалом. Прерывание успевало отработать.


  1. hexus7
    07.01.2018 13:17

    Я глушил подобранным RC/LC фильтром. Достаточным для исключения ложных сработок без специальной обработки дребезга.


    1. nwwind Автор
      07.01.2018 20:15

      Схемку можно?
      Будет ли это универсальным решением?
      Нет ли готовой схемки индукционного датчика, что от провода работает, намотанного на высоковольный?


      1. hexus7
        07.01.2018 20:31

        О, прости, я брал не с индукционного. Хотя, думаю, подобными цепочками можно практически наглухо многое заглушить (благо делитель уже был с некоторой индукционностью, добавил 1 конденсатор тупо).
        Я делал блинкер под Yamaha V-Max 1200 (на чем-то копеечном, типа tiny25/45), там есть отвод на тахометр, первый график как раз с него снят напрямую. Судя по графику — дублирует выход с CDI до силовой части (коммутатора) — типичные такие управляющие в 0.


        1. nwwind Автор
          07.01.2018 20:45

          Ага, ясно.
          Хорошо, когда есть источник нормальных данных (более-менее).


          1. strvv
            08.01.2018 00:34

            в принципе с низковольтного выхода катушки можно снимать через сопротивление в 60-100кОм, чтобы не спалить вход, у приборок с высоковольтным (12вольт, Открытый коллектор) и низковольтным (с эбу, 5Вольт) входами связь через этот резистор. (вот схемы с драйва, первая с фазы генератора, вторая с катушки и эбу)
            далее просто RC цепочка в виде ФНЧ (т.е. к этому резистору достаточно небольшую емкость, получим интегрирующую цепочку) с частотой среза около 500 гц — 15тыс. оборотов :) или около 83м/сек — более чем достаточно для учёта.
            потом, т.к. на нижних скоростях 1-2 кмч, или оборотах ниже 200 об/мин будет неустойчивая работа — можно просто организовать по подсчёту импульсов. будет надежно считать высокие скорости, погрешность появится при малых скоростях и маленьком же окне счета, и на нижних частотах. жена как отдаст комп — скину ods или скорректирую в xls.


            1. strvv
              08.01.2018 00:42

              к 60 кОм достаточно 5нФ! емкость на землю. будет срез выше 530гц. сразу помехи от зажигания уйдут — там частоты, насколько я помню, у многоискровых в районе 1-2кГц, не ниже.


              1. nwwind Автор
                09.01.2018 12:07

                Отлично. Спасибо.
                Куплю на днях осциллограф — поиграю.


  1. AVKinc
    07.01.2018 20:15

    Все дело в том, что Ардуино это среда разработки.
    А измеряет частоту (или период и ли еще что-то) конкретный контроллер.
    И если мы занимаемся подобными вещами, нужно понимать как это делается в железе.
    То есть совершенно четко представлять как это делается на конкретных таймерах/счетчиках (прерывания) конкретной Меги (скорее всего Меги).
    И тогда сразу становится все ясно и никаких заморочек.
    Есть отличная книга Евстифеева «Микроконтроллеры семейства Mega»


    1. nwwind Автор
      07.01.2018 20:19

      Для тахометра достаточно ATTiny на самом деле. Как видно, здесь нет вычислительных задач.
      Ну худой конец, ATmega32 более чем годится.
      Если мы будем рассуждать о производстве, то не надо там атмегу совсем, есть варианты. Я же статейку писал для очумелых ручек, что делают что-то своими руками и с программированием знакомы чуть лишь.


      1. strvv
        08.01.2018 00:36

        более того, если использовать аналоговое, стрелочное отображение — достаточно любой одновибратор, например 555 и милиамперметр.


        1. nwwind Автор
          09.01.2018 12:07

          Амперметры слишком нежные. В боевых приборках давно уже сервы стоят.


  1. SadAngel
    07.01.2018 20:19

    Проще взять PSoC там есть модули для подавления дребезга:
    www.cypress.com/documentation/component-datasheets/debouncer
    подщета импульсов
    www.cypress.com/documentation/component-datasheets/quadrature-decoder-quaddec
    и много интересного.(ядро Cortex M0, 48 MHz).

    Arduino уже давно пора оставить истории.


    1. nwwind Автор
      07.01.2018 20:20

      Ардуино очень хорошо для тех, кто только начинает. Там всё просто, уютно и готово.


      1. SadAngel
        07.01.2018 20:28

        Мопробуйте PSoC и больше никогда не захотите работать с Arduino (90% всего настраиваеться через GUI, пєнормальная IDE, а не блокнот). PSoC и для начинающих подходить (не копипастеров кода)


        1. nwwind Автор
          07.01.2018 20:48

          Интересная игрушка.
          У меня на cortex5 такая подобная есть, только ещё с fpga на борту.


          1. SadAngel
            07.01.2018 21:04

            В PSoC нет FPGA, а просто блоки похожие на те что есть FPGA, такие блокие есть во всех PSoC: 3, 4, 5. В 5-м PSoC Cortex M3. Сейчас в разработке PSoC 6 ( много памяти, два ядра Cortex M0 и Cortex M4, есть BLE)


  1. strvv
    07.01.2018 20:20

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


    1. nwwind Автор
      07.01.2018 20:21

      Если обороты постоянные, то да.


      1. strvv
        08.01.2018 00:19

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


  1. woooody
    09.01.2018 04:43

    Эх, вспомнил молодость, как делал цифровой спидометр + тахометр на девятку. Тогда ж еще и ардуины не было — писалось все под PIC16F873 на ассемблере.
    Скорость на 3-символьном 7-сегментном LED дисплее. Тахометр — линейная шкала (1 диод — 200 об/мин вроде бы)
    Датчик скорости выдает 6 имп/1 метр. Рассчеты показали, что скорость в км/ч равна количеству импульсов за период 0.6 секунд. Кстати очень удачная частота для обновления скорости на цифровом дисплее.
    А вот тахометр я сделал иначе — считал время (такты) между импульсами на входе. От чего он получился более «живой».

    Однако проект не пошел под двум причинам:
    1. Не успел я сотворить хороший конвертер 12-5в (ну не было зарядок для телефонов и прочих импульсных преобразователей в таком количестве, как сейчас)
    2. Низкая яркость LED индикаторов — на солнце не видно + надо было как-то решать вопрос яркости ночью.
    3. не успел сделать человеческое сохранение одометра в NVM.


    1. nwwind Автор
      09.01.2018 11:51

      Считать импульсы хорошо, но вдруг у тебя окажется 6.123 импульса на метр? Или 6 импульсов на 98см? Это всё же гадания.
      А так — у тебя первый вариант получился.
      Кстати, если больше ничего не делать — так нормально. Но если требуется ещё что-то считать/рисовать, оно не работает тк не угадаешь, сколько времени займут другие задачи и время уплывает неконтролируемо.


  1. nwwind Автор
    09.01.2018 11:47

    habrahabr.ru/post/319336 я открыл для себя FRAM.
    А дисплеи OLED нынче стали дешевле заметно и они для машины самое то.