Настраиваемое ядро для Arduino

Тема в разделе "Глядите, что я сделал", создана пользователем DIYMan, 20 дек 2017.

  1. DIYMan

    DIYMan Guest

    Приветствую!

    Сразу скажу, что проект изначально планировался под Arduino Mega, плюс - порог вхождения в него, кмк, выше среднего. Что за проект, вкратце: надоело мне каждый раз писать код для опроса датчиков, парсинга настроечных параметров, приходящих по Serial, кооперативную работу без delay и т.п. Было решено разработать удобное настраиваемое ядро (про FLProg плз не надо - оно из другой оперы совсем), которое бы позволяло:

    1. Настраивать привязку и типы датчиков, используемых в прошивке, через софт под Windows;
    2. Умело бы собирать информацию с этих датчиков, не тормозя всю систему, с учётом времени на конвертацию некоторых типов датчиков и т.п.;
    3. Умело бы отдавать данные с датчиков в разном формате - текстовом, числовом, raw и т.п.;
    4. Умело бы использовать разные транспорты для передачи этих показаний куда либо, неважно куда - на дисплей, по TCP и т.п.;
    5. Отвязало бы меня от необходимости писать кучу рутинного кода, а сразу переходить непосредственно к написанию логики того или иного проекта;
    6. Умело бы работать с разными хранилищами настроек - EEPROM, внешняя I2C-память - как минимум;
    7. Ещё много всего бы умело :)

    В общем, родился некий прототип - Core, валяется здесь: https://drive.google.com/file/d/1hetCxTlOijFm2XRG67lACGUS72uTYMSz/view?usp=sharing

    Пока балуюсь только с одним датчиком - BH1750. Настройки ядра - в файле CoreConfig.h (можно директивами условной компиляции не тащить поддержку ненужных датчиков), в файле .ino видно, как можно загрузить тестовый конфиг прямо из массива в памяти (пока только это тестировал). Там же видно, как можно при помощи всяких форматтеров получать данные.

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

    На будущее - припиливание кучи всего, типа форматтеров в разные типы пакетов (например, для Modbus или MQTT), написание транспортов во внешний мир и т.п.

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

    Как-то так. Кмк, архитектура получается удобной - не без вопросов, конечно, но это только самое начало.
     
    sys, Securbond, ostrov и ещё 1-му нравится это.
  2. DIYMan

    DIYMan Guest

    Обновил ядро, поддерживаются датчики DHT (влажность), Si7021 (влажность), DS3231 (часы), BH1750 (освещённость). Теперь можно получать инфу по всякому - сразу списком всех показаний, список показаний по типу (например, только показания с датчиков влажности), список показаний по типу железки (например, только показания с DHT).

    Протестировал DS3231 и BH1750, остальное пока не тестировал, но код опроса датчиков проверенный, будет работать. Для DS3231 предусмотрел установку времени, сейчас буду писать парсер первой команды, приходящей в ядро из Serial - это как раз будет команда на установку времени в часах ;)

    Ядрышко удобное получается, сам в шоке :)
     
  3. DIYMan

    DIYMan Guest

    Ну всё - теперь вообще шоколадно: можно уже общаться с ядром из монитора порта. В тестовом скетче в конфиге у меня прописан DS3231, в мониторе порта ввожу

    GET=DATETIME

    и мне возвращается время. Ввожу

    SET=DATETIME|21.12.2017 15:50

    - и время устанавливается. Что нам это даёт? Удобное общение с железкой через конфигурационный софт ;) Собственно, в этом опыта уже достаточно - софт из проекта в подписи так и работает, и прошивка контроллера теплицы также общается посредством текстовых команд с ним.

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

    Поясню: вы прописали конфиг, указав на одном пине 5 датчиков DS18B20. Всё ок, видно, что с них идут показания, жизнь удалась. Но, допустим не дай Бог - один датчик поломатый вдруг стал, и его надо заменить. Ок, заменили - тоже не вопрос. Вопрос: где гарантия, что показания датчиков будут отдаваться в том же порядке, что и до замены? Правильный ответ - нет такой гарантии, т.к. адрес нового датчика - другой, и при сканировании шины этот адрес может откликнуться раньше, чем откликался поломанный датчик (такова особенность протокола 1-Wire в части поиска устройств на шине).

    По итогу имеем, что если раньше последовательность датчиков была 1,2,3,4,5, то при замене третьего датчика она вполне может стать 1,3,2,4,5. Что, естественно, в понятийной привязке датчика к сущности (температура в бане, температура снаружи бани и т.п.) - будет неприятным сюрпризом.

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

    Так что поддержка DS18B20 будет, но только после того, как будет ясно, как грамотно это сделать. И построим мы на этом ядре контрольные весы для пасеки с блекджеком и мёдом :)
     
  4. ИгорьК

    ИгорьК Гуру

    Вязать адрес датчика к показаниям, а не номер. Тогда все ОК.
     
  5. DIYMan

    DIYMan Guest

    Да это ясно, но реально много нюансов: мы ВСЕГДА оперируем номерами ПОКАЗАНИЙ датчиков в системе, в любой. Ибо внутреннее устройство железки может быть разное, система её железной идентификации - тоже разная. При таком раскладе нужен слой абстракции, и он - есть.

    Ясно, что придётся вязать индекс датчика в конфиге с его адресом на шине, и при опрашивании - проверять, что там творится, по факту обновлять адрес датчика на шине у себя внутри, тогда все привязки сохраняться. Я сейчас пока ещё не определился, как это грамотней с архитектурной точки зрения реализовать, ибо - с одной стороны юзер, которому надо в конфиге вот тупо указать "у меня на пине номер 8 будет висеть десять датчиков температуры", с другой стороны - обеспечить целостность индексации, ибо юзер сказал - 10 датчиков, юзер оперирует их порядковыми номерами, юзер настроил тот же MQTT-клиент, чтобы смотреть показания, и тут - ломается датчик, юзер его меняет, и все показания перепрыгивают на другие места :)

    Ясное дело, что универсальной архитектуры - не бывает, вот я и размышляю вслух, оно помогает, проверено :)

    И да - система должна корректно игнорировать одиннадцатый датчик, если он вдруг появился на шине, ибо - юзер сказал 10, и баста. Ну или - не игнорировать, а корректно добавлять в конфиг. Короче, нюансов хватает ;)

    А вообще самый простой вариант - это забить на адреса в первой версии ядра, и всё: юзер сказал будет 10 датчиков, значит, ядро настраивается на 10 датчиков, и всё. Так и сделаю пока, чтоб лоб не морщить - лень она такая :) :) :)
     
    Последнее редактирование модератором: 21 дек 2017
  6. ИгорьК

    ИгорьК Гуру

    :)
     
  7. DIYMan

    DIYMan Guest

    Короче - припилил DS18B20, работают на одной линии как миленькие ;). Сейчас на макетке трудятся: DS3231 - один штук, BH1750 - один штук, DS18B20 - два штука. При этом в основном скетче я, по сути, описал лишь конфиг, скормив его ядру:

    Код (C++):
    const byte unitTestConfig[] PROGMEM = { // тестовый конфиг в памяти

      // заголовок конфига
      CORE_HEADER1,
      CORE_HEADER2,
      CORE_HEADER3,

        //---------------------------------------
          SensorRecord, // данные о датчике
          BH1750, // датчик BH1750
          1, // длина данных
          BH1750Address1, // на первом адресе на шине I2C
        //---------------------------------------
          SensorRecord, // данные о датчике
          BH1750, // датчик BH1750
          1, // длина данных
          BH1750Address2, // на втором адресе на шине I2C
        //---------------------------------------
          SensorRecord, // данные о датчике
          Si7021, // датчик Si7021
          0,  // длина данных
        //---------------------------------------
          SensorRecord, // данные о датчике
          DHT, // датчик DHT
          2, // длина данных
          DHT_2x, // тип датчика - DHT21
          5, // пин датчика - 5
        //---------------------------------------
          SensorRecord, // данные о датчике
          DS3231, // датчик часов реального времени DS3231 на шине I2C
          0, // длина данных
        //---------------------------------------
          SensorRecord, // данные о датчике
          DS18B20, // датчик DS18B20
          1, // длина данных
          6, // пин датчика - 6
        //---------------------------------------
          SensorRecord, // данные о датчике
          DS18B20, // датчик DS18B20
          1, // длина данных
          6, // пин датчика - 6
        //---------------------------------------

      // окончание конфига
      CORE_HEADER1

    };
    Ессно, конфиг подобного формата не обязательно описывать в скетче - в будущем, как я упоминал, будет софт для настройки конфига датчиков, и тады уже всё это добро будет сохраняться в EEPROM, и грузиться оттуда же. Но, конечно же, должна быть возможность грузить конфиг прямо из флеша, что я и приделал.

    Думаю, конфиг получился удобным, хотя тут вопрос вкуса, конечно. Пойду-ка что ли подключу Si7021, а то в конфиге прописан - а проверять работу будет Пушкин Саша? Силу светлую сторону вижу в тебе я, о юный падаван :)

    Кстати, выхлоп тестового скетча:
    Код (Text):

    GET ALL SENSORS
        SENSOR DATA: 10 lux
        SENSOR DATA: -
        SENSOR DATA: -
        SENSOR DATA: -
        SENSOR DATA: 21.12.2017 21:42:59
        SENSOR DATA: 28,68*
        SENSOR DATA: 25,93*

    GET ALL HUMIDITY
        HUMIDITY: -
        HUMIDITY: -

    GET ALL BH1750
        BH1750: 10 lux
        BH1750: -

    GET DATETIME
        DATETIME: 21.12.2017 21:42:59

    GET ALL DS18B20
        DS18B20: 28,68*
        DS18B20: 25,93*
    RAM free: 1159

     
     
  8. DIYMan

    DIYMan Guest

    А вот как просто получить из ядра данные всех датчиков, которые отдают данные в виде температуры, в текстовом виде:

    Код (C++):
    CoreTextFormatProvider textFormatter;
    // теперь получаем показания всех датчиков влажности
        Serial.println(F("\nGET ALL HUMIDITY"));
        CoreDataList allHumidity = CoreDataStore.getByType(Humidity);

        // и печатаем их
        for(size_t i=0;i<allHumidity.size();i++)
        {
          CoreStoredData dataStored = allHumidity[i];
          // получаем данные, отформатированные в текстовом виде
          String data = textFormatter.format(dataStored,i,true); // последний параметр - выводить ли единицы измерения

          // и печатаем их
          Serial.print(F("\tHUMIDITY: "));
          Serial.println(data);        
        }
    А вот так - по типу железки:
    Код (C++):
    // теперь получаем показания всех датчиков DS18B20
        Serial.println(F("\nGET ALL DS18B20"));
        CoreDataList allDS18B20 = CoreDataStore.getBySensor(DS18B20);

        // и печатаем их
        for(size_t i=0;i<allDS18B20.size();i++)
        {
          CoreStoredData dataStored = allDS18B20[i];
          // получаем данные, отформатированные в текстовом виде
          String data = textFormatter.format(dataStored,i,true); // последний параметр - выводить ли единицы измерения

          // и печатаем их
          Serial.print(F("\tDS18B20: "));
          Serial.println(data);        
        }    
    Пойду пожму себе руку :D:D:D Я, всё-таки, местами молодец (про список мест - офицеры....) :D:D

    З.Ы. Забыл же ж о самом главном - всё работает неблокирующе, даже с учётом времени, необходимого на конвертацию показаний DS18B20. Скажу по секрету - в delay ничего не ждётся, от слова "почти совсем, ну разве только в одном местечке, когда настраиваем разрешение у DS18B20 и он запоминает свой скрачпад, но там без этого никак, от десяти миллисекунд один раз при старте никто не умирал" :cool::confused:
     
  9. IvanUA

    IvanUA Гуру

    Я к примеру не забивал на адреса датчиков, а наоборот именно адреса подвязывал к "помещениям".
    Подключаете 1-й датчик, и ядро вам выдает что появился новый датчик - его адрес (как МАС в LANe) такой то - в поле напротив датчика укажите его порядковый номер (номер помещения).
    Подключили следующий - процедура та же...
    Плюс если есть не подвязанный датчик, то система ему дает привязку типа - unknow-01.
    Все понятно, и замена (количество, добавление, и прочее) не вызывает каких либо проблем)))))
     
    Последнее редактирование: 21 дек 2017
    DIYMan нравится это.
  10. DIYMan

    DIYMan Guest

    Ну это я уже потом допилю :) Сейчас только-только ядро вырисовывается, всякие тонкости будем решать в процессе. И кстати - именно так я и собираюсь сделать в конфигурационном софте - когда юзер добавляет датчики DS18B20 - будет кнопка "Сканировать шину", и потом уже привязка номера к датчику ;)
     
    IvanUA нравится это.
  11. DIYMan

    DIYMan Guest

    Всё, победил я лень :) Допилил привязку датчика DS18B20 к индексу. При запуске, если у датчика нет привязки - сканируется шина, первый непривязанный датчик пихается в слот (автопривязка при первом старте, короче).

    Затем, если датчик не отвечает N раз - привязка к адресу сбрасывается. Как только появился новый адрес на шине - в этот слот пихается новый датчик. Привязки хранятся в EEPROM, ессно.

    Короче, получается забавно и удобно: прописал в конфиге датчики, потом ну давай их регистрировать, даже индексы назначать не надо: какой первым подцепил - такой и появится под первым индексом в системе :)

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

    Остальные плюшки с 1-Wire - будут реализовываться поэтапно, по мере написания софта, отладки и т.п. Мне пока достаточно на этом этапе, но жизнь, как обычно, быстро покажет, кто ел жёлтый снег :D:D:D, если вдруг окажется, что чего-то не хватает.

    И даже Si7021 так и не подключил ещё - лубоффь моя DS18B20 отняла у мну все силы :)
     
  12. DIYMan

    DIYMan Guest

    Ещё вот какая интересная фишка в голову пришла, не совсем про датчики, но тоже из той оперы: случается, бывает надо получить состояние пина, и в большом проекте ХЗ, в каком он режиме, на вход или на выход работает. Очевидно, что в зависимости от этого надо просто читать из разных регистрова PIN* или PORT*.

    Написал парочку функций для этого дела:
    Код (C++):
    int CoreClass::getPinMode(int p)
    {

      #if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
         const int max_pin = 69;
      #else
         const int max_pin = 19;
      #endif

       if (p > max_pin)
           return -1;

       uint8_t port = digitalPinToPort(p);
       uint8_t portBit  = digitalPinToBitMask(p);

       volatile uint8_t *mode = portModeRegister(port);

       return ((*mode & portBit) != 0);
    }
    //--------------------------------------------------------------------------------------------------------------------------------------
    int CoreClass::getPinState(int pin)
    {
      int mode = getPinMode(pin);
      if(mode == -1)
        return mode;

      uint8_t port = digitalPinToPort(pin);
      uint8_t portBit = digitalPinToBitMask(pin);

      if(mode == INPUT)
      {
        volatile uint8_t* pir = portInputRegister(port);
        return (*pir & portBit) == LOW ? LOW : HIGH;  
      }
      else
      {
        volatile uint8_t* por = portOutputRegister(port);
        return (*por & portBit) == LOW ? LOW : HIGH;      
      }
    }
    //--------------------------------------------------------------------------------------------------------------------------------------
     
    Шо это нам даёт, спросите вы? Всё просто - во-первых, можно безопасно читать состояние порта откуда угодно. Во-вторых - я тут задумываюсь в рамках парадигмы сделать тип датчика - состояние цифрового порта (естественно, такие "датчики" будут опрашиваться постоянно, а не через определённые интервалы времени). И, по идее - получается слепок состояния, к которому можно обращаться однотипно, как я показывал выше. Соответственно, пихание этого дела во внешний мир - тоже однотипно происходит, т.к. всё укладывается в понятие "датчик".

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

    Пойду прикручивать, всё равно ненужные в конкретном проекте части сделаны отключаемыми ;)
     
  13. DIYMan

    DIYMan Guest

    Всё, быренько прикрутил - теперь есть автослежение за статусом порта. В конфиге вот такая запись сейчас:

    Код (C++):
          SensorRecord, // данные о датчике
          DigitalPortState, // состояние цифрового порта
          1, // длина данных
          13, // пин датчика - 13
        //---------------------------------------
     
    В loop мигаю светодиодом:
    Код (C++):
    unsigned long curMillis = millis();

      // мигаем светодиодом раз в 1.2 секунды
      static unsigned long blinkMillis = millis();
      if(curMillis - blinkMillis > 1185)
      {
        blinkMillis = curMillis;
        static bool isLedOn = false;
        isLedOn = !isLedOn;
        digitalWrite(LED_BUILTIN,isLedOn);
      }
    И собираю состояние порта:
    Код (C++):
    CoreDataList catchList = CoreDataStore.getBySensor(DigitalPortState);
      for(size_t i=0;i<catchList.size();i++)
      {
        CoreStoredData dataStored = catchList[i];
        DigitalPortData dt = dataStored;
        if(dt.Pin == 13)
        {
          // наш тестовый пин
          static int state = -1;
          if(state == -1)
            state = dt.Value;

          if(state != dt.Value)
          {
            state = dt.Value;
           
            Serial.println(F("-----------------------------------------------------------------"));
            Serial.print(F("PIN 13 TRIGGERED, VALUE: "));
            if(dt.Value)
              Serial.println(F("HIGH"));
            else        
              Serial.println(F("LOW"));
          }
        }
      } // for
    Всё, теперь пофиг, кто откуда что там изменит - состояние пина безопасно прочитано, и находится в хранилище. Ясное дело, что для одного пина вышеприведённый код избыточен, это только для демонстрации, так сказать. Но фишка-то в том, что у нас есть хранилище с актуальным состоянием системы, которое (хранилище) можно пихнуть в любом формате в любую дыру. А получать состояние пина оттуда в логике - ну да, не всегда нужно. Зато юзер может всегда настроить удобно, чтобы состояние пина попадало в хранилище, следовательно, по какому-либо из протоколов окажется во внешнем мире, в какой-нибудь SCADA, например. Это тоже нужно, щитаю.
     
    Securbond нравится это.
  14. DIYMan

    DIYMan Guest

    Подключил Si7021 - заработало сразу :) Основные датчики из распространённых уже поддерживаются, потихоньку прикручу всякие другие. Ну и софт надо писать неспеша. За пару лет управимся, как обычно :)
     
  15. ИгорьК

    ИгорьК Гуру

    А если... Iskra JS? И ничего не писать...
    По сути Вы пишете то, что реализовано в JS по умолчанию.
     
    DIYMan нравится это.
  16. DIYMan

    DIYMan Guest

    Не, мне под Мегу надо, если буду когда-нибудь на Iskra JS делать что-либо - ясное дело, что буду пользоваться тем, что там. А вот под Мегой - надоело каждый раз одно и то же, хочется ядра нормального :) Вот и встрял.
     
  17. ИгорьК

    ИгорьК Гуру

    У искры то процессор помощнее будет... и интерфейсов хватает.
    Наверно, памяти поменьше для программ, но все же.
    Дык... если ядро сделать тож на тож и получится.
    Просто... мне это напоминает один чудесный проект "АрдуиноМегаСервер".

    См: http://forum.amperka.ru/threads/arduino-mega-server.6850/
     
    Последнее редактирование: 23 дек 2017
    DIYMan нравится это.
  18. DIYMan

    DIYMan Guest

    Не, я в ту степь не вступлю :) Я это так - чисто в свободное время, с прицелом на то, что буду какие-то проекты на Меге делать, вот и всё. Прекрасно отдаю себе отчёт, что к чему, что называется ;) И не собираюсь отдавать этому проекту всё свободное время - так, развить до состояния, чтоб себя устраивало (простенького софта не хватает для настройки, но даже без него жить можно) - и амба.

    Так что всё норм, у мну есть чем заняться - вон, с контроллером теплицы сейчас с обратной связью работаю, делов невпроворот, до весны надо финиш :)
     
  19. DIYMan

    DIYMan Guest

    Перенёс проект на гитхаб: https://github.com/Porokhnya/ArduinoCore Потихоньку развиваю, дошёл до костяка трансорта, расширил возможности настроек через конфиг. С датчиками пока финиш, сейчас надо продумывать асинхронку по работе с ESP, потом писать этот транспорт, потом - писать всякие форматтеры, типа форматтера payload пакета MQTT (думаю, на MQTT в качестве пихания чего-либо во внешний мир я пока и остановлюсь), потом - писать менеджер работы с MQTT (вычитка настроек из конфига, руление топиками и пр.).

    Вот не пришли ещё гироскопы с али, никак не дописать обратную связь по положению фрамуг для контроллера теплицы - приходится занимать себя всяким разным :D:p
     
  20. sslobodyan

    sslobodyan Гик

    Меня терзают смутные сомненья, что использование готовой РТОС даст еще большие преимущества, чем разработка собственного "ядра". Заводите таски по типам датчиков, структуры с описанием параметров датчиков (в т.ч. частота опроса) - и в бой. Каждый таск будет выбирать по системному времени "готовый к употреблению" датчик и его опрашивать. Помним, что таск в РТОС умеет красиво отдавать процессорное время другим нуждающимся, пока сам ждет ответа от датчика. Плюс в больших проектах появляется юзеринтерфейс, да еще и с несколькими вариантами управления (кнопки, крутилки, тачпады, уарт и т.д), который проще обрабатывать именно своими тасками в РТОС. Ну и плюс уже есть готовое решение по арбитражу доступа к ресурсам.