Суперэкономичный беспроводной монитор Т и RH

Тема в разделе "Глядите, что я сделал", создана пользователем parovoZZ, 1 янв 2019.

  1. parovoZZ

    parovoZZ Гуру

    Цель - разработка экономичного беспроводного монитора температуры и относительной влажности.
    Что мы имеем: МК Attiny24A, датчик SHT10 от SENSIRION, популярный трансивер nRF24l01+ и источник питания в виде пары батареек LR41. Работа будет весьма насыщенной и объемной, т.к. мы будем использовать модуль USI сразу в двух режимах, жонглировать регистрами (с) и заниматься прочими непристойными вещами. Но давайте сначала разберем и проанализируем ошибки первого моего прототипа такого устройства, но на датчике DS18B20.

    Первая детская неожиданность
    Первый прототип состоял из озвученного DS18B20 в металлическом корпусе и МК atmega 328p на плате ProMini с кварцем на 8 МГц. Т.е. это была версия, адаптированная под питание 3.3в. Сперва разберем
    Схемотехнические ошибки
    Разумеется, что на плате была проведена операция стабилизатороэктомия, а также светодиодоэктомия. Но для контроля работы интерфейса SPI был оставлен ещё один светодиод - на линии данных. Какой бы небольшой ток он не потреблял, но первый путь утечки тока присутствовал. Как известно, для работы датчика DS18B20 необходим подтягивающий резистор. Был поставлен первый попавшийся под руку и он же рекомендуемый - сопротивлением 4700 Ом (у монтажников-слаботочников отобрал =) ). Это был второй путь утечки тока абсолютно на ровном месте. Как не сложно догадаться, величина этого тока составляла до 0,64 мА при питании напряжением три вольта. Третье. Я никак не выключал свои периферийные устройства. Трансивер nRF24L01+ в режиме PowerDown потребляет до 0.9 мкА, а датчик DS18B20 до 1 мкА, что в сумме дает 2 мкА. Четвертое. Для такого класса устройств DS18B20 вообще не подходит. Абсолютно не интересный диапазон питающих напряжений (хоть у меня он и работал при 2.7в, но это не показатель. Ведь напряжение у литиевой батарейки может проседать до 2 вольт без снижения отдаваемого тока), очень длительный период измерения - до 750 мс, потребляемый ток при этом доходит до 1.5 мА.
    Программные ошибки
    Я работал на внешнем кварце 8 МГц при этом никак не снижая частоту. У меня был включен BOD, который останавливал работу МК при напряжении на батарее 2.7в. И вроде как я его программно выключал, но в устройствах подобного класса он не нужен вообще. Тем более, что он и кушает также прилично. Я работал с датчиком DS18B20 в абсолютно дичайшем режиме - все необходимые паузы были организованы с помощью delay_us(), Один только сброс в 480 мкс поднимал с пола озвученные выше 0.6 мА и заставлял МК нопить. Ну и реализация всего протокола общения с этим датчиком реализована в таком же ключе. Это сегодня я бы попробовал реализовать этот протокол через USI или более экономичным способом. А тогда мне было важно всё это дело запустить, ну а оптимизация потом... Работа с трансивером nRF24L01+ была организована по образцу из какого-то проекта. А так как я тогда ещё не умел жонглировать регистрами (с), то кусок кода был выдран из проекта полностью. А трансивер там работал с подтверждением приема, ну и в нагрузку ещё проверялся буфер приемника, хотя никаких данных с пакетом подтверждения я не отправлял (прим. ну разумеется - я ж не умел ещё тогда жонглировать регистрами (с)). Пакеты с данными слались раз в 8-9 секунд (максимальный период вачдога у МК atmega). Для монитора окружающей температуры так часто не нужно. Сам код был написан самым неоптимальным образом несмотря на глубокий сон везде, где только можно.
    Результат ошибок
    Всё это вкупе приводило к тому, что литиевая батарейка 123A за почти полтора года проседала с 3.0 до 2.7 вольт. Ну а дальше привет BOD и сушим весла. Такой разряд батареи - это не то, чтобы немного - это ну овердохера.
     
    Un_ka, vvr, Daniil и ещё 1-му нравится это.
  2. ИгорьК

    ИгорьК Оракул Модератор

    При делении 3 на 4700 столько и получится. Но делать хоть какой-то вывод о потреблении тока датчиком из этого умозаключения не верно.

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

    Пушной звер Оракул Модератор

    есть MAX31820 если очень надо.
     
    ИгорьК нравится это.
  4. parovoZZ

    parovoZZ Гуру

    Датчик температуры и влажности SHT10
    Датчик из семейства SHT1x от SENSIRION. Типовые и интересные нам характеристики:
    Ток потребления при измерении от 0,55 до 1 мА
    Ток в режиме PowerDown - от 300 до 1500 нА
    Минимальное напряжение питания - 2.4в
    Время измерения - до 280 мс

    Датчики внутри семейства отличаются точностью. Диапазон рабочих температур у всех датчиков составляет от -40 до 123.8 (кто спрашивал про датчик для бани?). И, как и у всех датчиков, точность по краям диапазона хуже, чем в нормальных условиях. Мой датчик самый дешевый и стоит чуть более 5 баксов на элсисоси. Но есть и одна печалька у этого семейства датчиков - в формуле преобразования температуры присутствует коэффициент, зависящий от напряжения питания. Правда, на конечный результат влияет не сильно - на сотые доли. Зато, в отличие от BME280 (его я тоже раскурю, но чуть позже - когда будем делать метеодатчик), не надо передавать калибровочные коэффициенты размером по 2 байта. В этом датчике эти коэффициенты применяются при вычислении результата измерения самим датчиком. Даташит уверяет, что калибровочные коэффициенты подогнаны под напряжение питания 3.3в, а вот коэффициент под это напряжение не приводится - мол, рассчитывайте сами(. Калибровочные коэффициенты можно отключить для ускорения времени измерения. Точность измерения: по умолчанию датчик настроен на 12bit RH / 14bit Temp, но можно переключить в режим, в котором точность понижается до 8bit RH / 12bit Temp, а вот время измерения в последнем случае сокращается почти в 4 раза. Я не буду бежать за сверхточными показаниями (тем более, что с этим датчиком их не получить). Мне хватит единиц в показаниях относительной влажности и десятых в показаниях температуры.
    Внутри датчика есть нагреватель. Включать и выключать его необходимо вручную. Как утверждает даташит, нагреватель исключительно проверки работоспособности датчика. Т.к. он потребляет 8 мА, то на данный момент использовать его не буду. Теперь поговорим о

    Интерфейс и протокол общения
    Подключается данный датчик к 2-х проводной линии интерфейса I2C. Но протокол общения несколько отличается от стандартного.
    SHT1x_protocol.png
    Как видно по картинке из даташита, датчик никак не воздействует на линию SCK (SCL). Повторный старт и стоп отсутствуют. Признак старта также свой, особенный. Есть и приятная особенность, которой мы непременно воспользуемся - по окончании измерения датчик притягивает линию DATA к нулю, что позволяет МК усыпить самым крепким сном. У датчика присутствует квазиадрес, который интегрирован в старiие биты команды и всегда равен 0b000. В целом протокол весьма простой и всё должно быть ясно из картинки.
     
    Последнее редактирование: 16 янв 2019
    vvr, ИгорьК, Daniil и ещё 1-му нравится это.
  5. parovoZZ

    parovoZZ Гуру

    Теперь пару слов о
    Выбор МК
    Я никак не черепил по поводу общения с датчиком SHT1x с помощью аппаратного I2C (TWI у атмела), но особенности протокола общения с датчиком так и говорят - ну позвони мне, позвони! Позвони мне ради бога! Позвони мне по USI! Какие МК мы знаем с USI на борту? Attinyx313A, attinyx5, attinyx4A. Первый МК реально многоножка - мне столько не надо. Второй хорош, но не PicoPower (10 мкА в PowerDown!). Остался последний, на который и пал выбор. С включенным вачдогом при питании от 3-х вольт потребляет он чуть более 4-х микроампер, с выключенным вачдогом - около 100 наноампер. Такой расклад наводит на мысль о внешнем будильнике. Наноамперные таймеры в природе существуют, но сейчас не об этом. Давайте посчитаем, сколько мы будем жить в режиме глубокого сна от батарейки емкостью 250 мА/ч. Примем, что кушаем мы 4 микроампера. Делим одно на другое, что надо перемножаем и получаем: 5 лет. Вот и хорошо. Но можно ещё дольше.
    К слову сказать, новые attiny 0-ой и 1-ой серии показывают вообще чудеса - около 1.7 uA с включенным LowPower осциллятором.
     
    Un_ka, vvr, ИгорьК и 2 другим нравится это.
  6. parovoZZ

    parovoZZ Гуру

    Схема электрическая принципиальная
    Вот так вот, мать её.
    AirMonitoring_Scheme.png
     
    ИгорьК, Daniil и DetSimen нравится это.
  7. parovoZZ

    parovoZZ Гуру

    Схемотехнические решения
    У нас присутствует шина I2C, а это значит, что для нее необходимы резисторы подтяжки. Одновременно, мы хотим избежать чрезмерной утечки тока, когда какая-либо линия шины находится в нуле. Поэтому приняты следующие решения - номинал резистора подтяжки увеличить настолько, насколько это возможно и, мало того, этот резистор вообще выключать (разумеется, по возможности) кода линия находится в нуле. И на роль таких резисторов идеально подходят встроенные резисторы - они достаточно высокого номинала и очень просто подключаются/отключаются к линиям шины. Даташит нас заверяет, что сопротивление этих резисторов при напряжении питания 5.5в лежит в диапазоне от 20к до 50к. Для напряжения 2.7в приведен график, по которому легко вычисляется типовое значение в 34к. В любом случае, нас это устраивает.
    Следующая задача - как их включать? И ждет нас большое разочарование - модуль USI при работе в режиме I2C отключает резисторы подтяжки на пинах SCK и DATA. Решение в лоб - данные пины объединить с соседними и манипулировать уже их резисторами. На схеме это пины PA3 и PA7. Резисторы подтяжки включаются регистром PORTx тогда, когда в регистре DDRx соответствующие пины сконфигурированы на вход. Выключаются они либо переводом пина на выход (при этом пин сразу переходит в "1", т.к. бит в регистре PORTx не сбрасывается), либо записью "0" в регистр PORTx, либо глобально битом PUD в регистре MCUCR. Т.к. половина операций с пинами SCK и DATA у нас будет происходить с помощью регистра PORTx, то и управлять резисторами подтяжки мы будем им же. Одновременно мы также экономим процессорное время.
    Обязательно задействуем линию прерывания от трансивера nRF24L01+ для сигнализации окончания отправки пакета. Почему не таймер? Да, мы знаем точное время, необходимое для отправки пакета и составляет оно порядка трех сотен микросекунд. Такую паузу мы не сможем обеспечить вачдогом, а если отмерять любым другим таймером, то нам придется МК держать в режим IDLE, а не в PowerDown. А это пахнет лишним энергопотреблением.
     
    ИгорьК нравится это.
  8. parovoZZ

    parovoZZ Гуру

    Итак,
    USI
    В сети не так уж и много русскоязычного материала по данному модулю, с англоязычным дела не лучше. Я через яндекс нашел всего пару статей - одна из них на сайте уважаемого DiHalt, вторая просто очень хороший перевод даташита с комментариями редактора. Изучение USI рекомендую начать с этого материала, а за подробностями уже обратится к даташиту. Второе - необходимо обязательно скачать материалы апноута с сайта микрочипа - там есть примеры кода. Здесь не буду пересказывать сказанное, а лишь покажу сливки. Блок-схема USI:
    USI.png

    USI as I2C
    В этом режиме у ас задействованы пины DI/SDA и USCK/SCL. Первое обозначение для SPI, второе для I2C. У пина SDA необходимо вручную менять режим работы в зависимости от направления передачи данных. При этом в режиме входа пин работает как самый обычный, а вот в режиме выхода у нас подключается драйвер с открытым стоком. Да - в драйверах интерфейса I2C отсутствует верхний драйвер - он заменен на подтягивающий резистор для исключения КЗ на линии.
    Как видно из блок-схемы, пин SDA подключается либо в начало, либо в конец сдвигового регистра. Для сдвига битов в этом регистре он может тактироваться от 4-х разных источников. Что это за источники? Это строб USICLK, это внешнее тактирование (в режиме работы SLAVE - здесь я его не разбираю), можно тактироваться от таймера T0 (у меня, к сожалению, пока не взлетело - поэтому тоже не разбираю), а также строб USITC. В чем разница между USICLK и USITC спросите вы? Для тактирования по стробу USICLK в этот бит необходимо записывать попеременно то "0", то "1". В USITC необходимо писать всегда "1" и такая запись будет формировать противоположный фронт. Для подсчета сделанных тактов у нас в наличии имеется 4-х битный счетчик. Счетчик при переполнении взводит флаг USIOIF, а также умеет генерировать прерывание.
    Также присутствует очень важный бит аппаратного детектора старта, который нам подложит подлянку. Бит зовется USISIF. Дело в том, что этому аппаратному детектору все равно, кто формирует старт - мы или кто-то на шине. Реакция у него всегда одна - он прижимает линию SCL к нулю. Чтобы линию поднять обратно, необходимо сбросить этот флаг. В статье на сайте у DiHalt-а автор не смог победить это и потому обходил данный момент с помощью костылей. Странно (
    Открываем редакторы и пишем код.
    Для начала напишем функцию приемо-передачи битов. Функция одна, а вот порт SDA будем переключать до вызова данной функции.
    Код (C++):
     //.. Функция передачи
    uint8_t SHT_USI_Transfer (uint8_t temp)
    {
        USISR = temp;                    // Помещаем настройки в регистр USISR - количество циклов счетчика до переполнения.

        temp =    (0<<USISIE)|(0<<USIOIE)|            // Прерывания запрещены
                (1<<USIWM1)|(0<<USIWM0)|        // USI как I2C.
                (1<<USICS1)|(0<<USICS0)|(1<<USICLK)|    // Программно тактируем шину
                (1<<USITC);                //

        SCK_PORT |= (1<<Pullup_SCK);                // Включили подтяжку на линии SCK

        do
        {
            //USICR = temp;                // Формируем положительный фронт на линии SCK
            //while(!(SCK_PIN & (1<<SCK)) );        // Ждем поднятия линии SCK
            USICR = temp;                // Формируем отрицательный фронт на линии SCK
        }while( !(USISR & (1<<USIOIF)) );            // Крутимся в цикле до переполнения счетчика
       
        SCK_PORT &= ~ ((1<<SCK) | (1<<Pullup_SCK));        // Прижимаем SCK к нулю, выключаем подтяжку

        temp  = USIDR;                    // Читаем принятые данные
        USIDR = 0xFF;                    // Отпускаем линию DATA
        DATA_DDR |= (1<<DATA);                // Пин DATA как выход

        return temp;                    // Возвращаем принятые данные
    }
    Сперва включаем подтяжку на линии SCK - напомню, что подтяжку мы включаем на соседнем пине, который электрически соединен с пином SCK. Далее мы в цикле каждый раз пишем "1" в бит USITC. При этом на выходе у нас формируется меандр, который тактирует счетчик и двигает данные в сдвиговом регистре. На выполнение итерации цикла уходит 8 тактов процессора. Таким образом, при тактовой частоте в 500 кГц мы имеем скорость передачи битов 62.5 кГц. При разборе режима SPI я покажу самую скорострельную работу тактирования. А тактироваться показанным выше способом имеет смысл только в одном случае - если раскомментировать код внутри цикла. Как видно из него, мы ждем поднятия потенциала на линии SCK. Т.к. у нас резисторы заранее неизвестного номинала, то и наверняка сходу мы не сможем правильно оценить 100% рабочую частоту. Но для устройства с экономичным питанием правильным будет тактироваться с максимальной эффективностью, т.е. CLK/2, а частоту тактирования шины регулировать прескалером осциллятора (в нашем случае можно смело выставлять делитель на 64. Можно и на 32 - тогда получим частоту 125 кГц. Но необходимо тестировать на минимальном напряжении питания. Может не хватить тока через подтягивающие резисторы для перезарядки емкостей).
     
    vvr, ИгорьК и DetSimen нравится это.
  9. parovoZZ

    parovoZZ Гуру

    Теперь напишем функцию-алгоритм работы с датчиком SHT1x.
    Код (C++):
      //.. Функция приемо-передачи
    uint8_t SHT_USI_Start_Transceiver (uint8_t *data, uint8_t measurement)
    {
        uint8_t USISR_8bit =    (1<<USISIF) | (1<<USIOIF) | (1<<USIPF) | (1<<USIDC) |
                        (0x0<<USICNT0);
        uint8_t USISR_1bit =    (1<<USISIF) | (1<<USIOIF) | (1<<USIPF) | (1<<USIDC) |
                        (0xE<<USICNT0);
                       
        //.. Формируем старт  
        SCK_PORT |= (1<<SCK) | (1<<Pullup_SCK);            // Отпускаем линию SCK и включаем подтяжку

        DATA_PORT &= ~((1<<DATA) | (1<<Pullup_DATA));        // Прижимаем DATA к нулю, выключаем подтяжку

        SCK_PORT &= ~((1<<SCK) | (1<<Pullup_SCK));        // Прижимаем SCK к нулю, выключаем подтяжку

        SCK_PORT |= (1<<SCK) | (1<<Pullup_SCK);            // Отпускаем линию SCK и включаем подтяжку
                                // Здесь сработал аппаратный детектор старта,
        USISR = (1<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC);    // который прижимает линию SCK к нулю. Чтобы отпустить
                                // линию SCK, необходимо очистить флаг USISIF
        DATA_PORT |= (1<<DATA) | (1<<Pullup_DATA);        // Отпускаем DATA, включаем подтяжку
        SCK_PORT &= ~((1<<SCK) | (1<<Pullup_SCK));        // Прижимаем SCK к нулю, выключаем подтяжку

        //.. Отправить команду
        USIDR = *data;                    // Загрузили в регистр USIDR данные

        SHT_USI_Transfer (USISR_8bit);            // Отправляем данные

        DATA_DDR &= ~(1<<DATA);                // Проверяем сигнал ACK от датчика
        if (SHT_USI_Transfer(USISR_1bit) & (1<<NACK_BIT))
                return(1);            // Если NACK - то выходим

        if (measurement)                    // Если команда на измерение
        {
            //.. Ожидание измерения
            GIMSK = (1<<PCIE0);                // Разрешаем прерывание на пинах PCINT0..PCINT7
            PCMSK0 = (1<<PCINT6);            // Выделяем маской прерывание только на PCINT6 / DATA

            Sleep(SLEEP_MODE_PWR_DOWN);            // Уходим в глубокий сон

            GIMSK = 0;                // Запрещаем прерывания на пинах      
        }
        else                        // Если команда на запись регистра
        {                        // статуса
            USIDR = *(++data);                // Записываем в датчик настройки
            SHT_USI_Transfer (USISR_8bit);      

            DATA_DDR &= ~(1<<DATA);            // Проверяем сигнал ACK от датчика
            if (SHT_USI_Transfer(USISR_1bit) & (NACK_BIT<<1))
                {return(1);}
                    else
                    {return(0);}
        }
       
        *(data++) = SHT_USI_Transfer (USISR_8bit);        // Читаем старший байт данных  
                                // Формируем квитанцию ACK
        DATA_DDR |= (1<<DATA);                // Линия DATA на выход
        DATA_PORT &= ~((1<<DATA) | (1<<Pullup_DATA));        // Прижимаем DATA к нулю, выключаем подтяжку

        SCK_PORT |= (1<<SCK) | (1<<Pullup_SCK);            // Отпускаем линию SCK и включаем подтяжку
        SCK_PORT &= ~((1<<SCK) | (1<<Pullup_SCK));        // Прижимаем SCK к нулю, выключаем подтяжку      
       
        DATA_DDR &= ~(1<<DATA);                // Линия DATA на вход
        Pullup_PORT |= (1<<Pullup_DATA);            // Включаем подтяжку на линии DATA

        *data = SHT_USI_Transfer (USISR_8bit);            // Принимаем младший байт данных      
                                // Формируем квитанцию NACK
        DATA_PORT |= (1<<DATA) | (1<<Pullup_DATA);        // Отпускаем DATA, включаем подтяжку

        SCK_PORT |= (1<<SCK) | (1<<Pullup_SCK);            // Отпускаем линию SCK и включаем подтяжку
        SCK_PORT &= ~((1<<SCK) | (1<<Pullup_SCK));        // Прижимаем SCK к нулю, выключаем подтяжку

        return(0);
    }
    Данная функция является универсальной - она позволяет как запрашивать результат у датчика, так и конфигурировать его. Датчик SHT1x. как и любое другое I2C устройство, различает команды на запись и чтение с помощью последнего бита. Но в этой функции этого делать не будем (чтобы не накручивать лишние такты МК в режиме Active). а признак чтения или записи будем формировать снаружи (мы же знаем,что делает та или иная команда?).
    Первое, что необходимо сделать - сформировать старт. Смотрим на картинку в самом верху и понимаем - старт необходимо формировать полностью вручную. Я не буду уж совсем углубляться в brain fuck программирование (хотя и можно =)), поэтому я беру текущее значение порта, накладываю маску по "или" и записываю обратно. Хотя, можно банально переписать текущее состояние всех битов в регистре и обойтись одной только записью. Этим мы сэкономим одну команду (а сэкономленные команды - это сэкономленные электроны в источнике питания. Хм. Надо подумать). Как я говорил чуть выше, как только мы сформируем стандартный старт, у нас сразу же срабатывает аппаратный детектор старта. Он нам залочит линию SCK на уровне нуля и мы не сможем ничего делать, пока не сбросим соответствующий бит. Сбрасываем. Формируем ещё один импульс и переходим к отправке команды - записываем ее в регистр USIDR. Дальше вызываем функцию передачи битов. После этого нам необходимо получить сигнал ACK от датчика. Для этого переводим пин SDA на вход и с помощью всё той же функции отправки битов формируем один импульс. Для формирования одного импульса нам необходимо в счетчик записать число 14. Счетчик переживет два такта и дальше уйдет на сброс, тем самым сформируется импульс на линии SCK. Пару слов о ручном управлении линией SDA. Несмотря на то, что на пин SDA прицеплен драйвер ОС, пин может управляться как от регистра PORTA, так и от сдвигового регистра. Логика управления - "И". Т.е. если где-то затесался нолик, то и на выходе будет ноль. Поэтому необходимо следить, чтобы старший бит в регистре USIDR был единицей, если мы хотим вручную управлять пином.
    Команду мы отправили и теперь нам надо отправить настройки или же получить результат измерения. Если мы настраиваем датчик, то всё аналогично предыдущей операции - отправляем данные и ждем подтверждения. Выходим из функции - в ней нам делать больше нечего. С получением данных поступаем по-другому. Т.к. для конвертации данных датчику необходимо время, то мы уйдем в глубокий сон. Датчик в процессе измерения держит линию SDA в единице, а по окончании оного прижимает её к нулю. Этим мы и воспользуемся. Но перед этим хочу напомнить, что регистр PINx всегда прицеплен к пину, если только мы специально его не отцепляем - в каком бы режиме пин не находился. Поэтому нам достаточно разрешить прерывание по этому пину, вызвать обработчик прерывания по изменению логического уровня на пине и разбудить МК. Для этого в регистре GIMSK мы разрешаем формировать прерывание от целой грядки пинов, а чтобы исключить ненужные - в регистре PCMSK0 мы накладываем маску (прим. если пинов с прерываниями много, то после поднятия флага прерывания нам необходимо вручную проверить, какой из пинов вызвал прерывание).
    После того, как датчик закончил измерение и разбудил нас, нам необходимо считать данные. Датчик ВСЕГДА отдает два байта! Какие бы мы настройки в него не пихали. На каждый принятый байт нам необходимо сформировать квитанцию ACK. Делаем это вручную и без фанатизма (brain fuck программирования=)). Для экономии процессорного времени весь код я оформил в линейном виде - мне здесь важно быстро отстреляться и залечь в сон.
     
    Un_ka, ИгорьК и DetSimen нравится это.
  10. parovoZZ

    parovoZZ Гуру

    Ну и самое простое - инициализация USI в режим I2C
    Код (C++):
     //.. Функция инициализации
    void SHT_USI_Init (void)
    {  
        SCK_DDR |= (1<<SCK);                // Пин SCK как выход
        DATA_DDR |= (1<<DATA);                // Пин DATA как выход

        SCK_PORT &= ~ ((1<<SCK) | (1<<Pullup_SCK));        // Прижимаем SCK к нулю, выключаем подтяжку
        DATA_PORT |= (1<<DATA) | (1<<Pullup_DATA);        // Отпускаем DATA, включаем подтяжку
     
        USIDR =     0xFF;
        USICR =    (0<<USISIE)|(0<<USIOIE)|            // Прерывания выключены
            (1<<USIWM1)|(0<<USIWM0)|            // USI в качестве I2C
            (1<<USICS1)|(0<<USICS0)|(1<<USICLK)|        // Шину тактируем сами
            (0<<USITC);

        USISR =    (1<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC)|    // Очищаем флаги,
                (0x0<<USICNT0);            // и сбрасываем счетчик
    }
    Здесь все просто. Но то ли лыжи не едут, то ли я....Инициализация в режим I2C занимает значительное время. И если при тактировании 1 МГц модуль успевает перейти в режим к моменту работы с ним, то при тактировании 8 МГц - нет.

    Получаем результат от датчика
    Определим макросы команд, чтобы нам было легче писать код
    Код (C++):
    #define SHT1x_Get_Temp    0b00000011
    #define SHT1x_Get_Humidity    0b00000101
    #define SHT1x_Write_Status    0b00000110
    #define SHT1x_Status    0b00000001
    Определим буфер в виде массива
    Код (C++):
    volatile    uint8_t    data[8];
    Т.к. с одним из элементов буфера мы будем работать в прерывании, то массив у нас с квалификатором volatile.
    Пишем код получения данных:
    Код (C++):
        SHT_USI_Init();              
                         
        data[3] = SHT1x_Write_Status;            // Записываем в датчик настроечные данные
        data[4] = SHT1x_Status;            // 8bit RH / 12bit Temp

        data[7] &= 0b00011111;            // Обнуляем биты с ошибками от датчика SHT1x
                            // Понижаем тактовую частоту для работы с I2C
        CLKPR = (1<<CLKPCE);            // Разрешить изменение прескалера тактовой частоты
        CLKPR = (1<<CLKPS2);            // Устанавливаем частоту 500 кГц
                         
        data[7] |= (SHT_USI_Start_Transceiver(&data[3], 0)<<7);
                            //
        data[4] = SHT1x_Get_Humidity;            // Первый байт RH - нули
                            // Нули никуда передавать не будем,
        data[7] |= (SHT_USI_Start_Transceiver(&data[4], 1)<<6);    // но функция пишет два байта,
                            // поэтому под первый байт подаем
        WDT_int_125mS;                // элемент массива, который впоследствии
                            // перепишется
        data[3] = SHT1x_Get_Temp;

        data[7] |= (SHT_USI_Start_Transceiver(&data[3], 1)<<5);
    Забегая вперед, скажу, что в элементе data[7] у нас живут биты с ошибками. Но о них позже.
    Подключаем ЛА и смотрим. Сформированный нами старт, загрузка настроек в датчик и запрос измерения RH:
    SHT1x_res1.png
    На все отосланные байты датчик ответил подтверждением. А вот ответ:
    SHT1x_res2.png
    Если посчитать по формуле из даташита, то получится 40-41% отн. влажности. Сразу же после получения ответа формируем новый запрос и получаем ответ по температуре:
    SHT1x_res3.png
    Здесь в кадр попал задний фронт линии SDA, который сформировал датчик. По нему мы видим, что с момента готовности результат измерений и до первого такта на линии SCK прошло всего 110 мкс. Здесь уже показывать не буду, но скажу, что на измерение RH (8 бит) у датчика ушло 14 мс, а на измерение температуры (12 бит) - 63 мс. Результат в разы лучший, чем у DS18B20.
     
    b707, vvr, KindMan и ещё 1-му нравится это.
  11. Daniil

    Daniil Гуру

    Любопытное решение) Это на время отладки?
    Спасибо.
    Самого ждет блок USI)
     
    ИгорьК нравится это.
  12. parovoZZ

    parovoZZ Гуру

    USI as SPI
    Для работы с трансивером нам необходим интерфейс SPI. Извлекать его будем всё из того же модуля USI. Здесь кратенько. Линии данных у SPI однонаправленные, а это значит, что пин DO всегда подключен к концу сдвигового регистра, а DI - к началу. Таким образом организован режим передачи full duplex. Здесь нам не нужны никакие подтягивающие резисторы, т.к. линия ВСЕГДА либо в нуле, либо в единице. А когда мы не работаем по интерфейсу SPI, то нам и пофигу, что творится на линиях. Здесь только про режим master. Здесь нет пина SS - соответственно нет и проблем, которые он создает в аппаратном SPI (пин SS всегда должен быть сконфигурирован как выход, если мы работаем ТОЛЬКО как мастер. Как только на SS приходит низкий уровень, аппаратный SPI бросает всё и переключает себя в режим SLAVE). Трансивер по интерфейсу SPI способен работать со скоростями вплоть до 10 МГц. Поэтому наша задача организовать тактирование максимально энергоэффективно ни теряя понапрасну ни единого такта.
    Начнем с функции инициализации.
    Код (C++):
        //... Инициализация SPI
    void SPI_Init(void)
    {
        SPI_DDR |= (1<<MOSI) | (1<<SCK) | (0<<MISO);        // Все выводы, кроме MISO, выходы
       // SPI_PORT |= (0<<MOSI) | (0<<SCK) | (0<<MISO);

        USICR = (1<<USIWM0) | (1<<USICLK);
    }
    Здесь не нужны ни счетчик, ни его убогие флаги, ни флаги старта, стопа - всё это идет лесом.
    Далее идет функция отправки элементарного байта.
    Код (C++):
        //... Передать байт данных
    void SPI_WriteByte(uint8_t data)
    {
        uint8_t clk0 = (1<<USIWM0) | (1<<USITC);
        uint8_t clk1 = (1<<USIWM0) | (1<<USITC) | (1<<USICLK);

        //... Копируем байт в регистр USIDR
        USIDR = data;

        USICR = clk0;            // Режим SPI, тактируем шину сами
        USICR = clk1;
        USICR = clk0;
        USICR = clk1;
        USICR = clk0;
        USICR = clk1;
        USICR = clk0;
        USICR = clk1;
        USICR = clk0;
        USICR = clk1;
        USICR = clk0;
        USICR = clk1;
        USICR = clk0;
        USICR = clk1;
        USICR = clk0;
        USICR = clk1;
    }
    Здесь в регистр USIDR пишем наш байт и начинаем дергать за веревочку. Как писал выше, при тактировании от USICLK нам необходимо самим писать в этот бит то "0", то "1". Нам надо 8 тактов, поэтому за веревочку дергаем аж 16 раз!
    Так сложилось исторически, но функция отправки и получения байта отдельно
    Код (C++):
        //... Передать и получить байт данных
    uint8_t SPI_ReadByte(uint8_t data)
    {
        //... Отправляем байт
        SPI_WriteByte(data);

        //... Принятый байт возвращаем
        return USIDR;
    }
    Надо бы ее или присовокупить к функции отправки байта, ну или заинлайнить.
    Функция отправки массива байт
    Код (C++):
        //... Отправить несколько байт по SPI. cmd - команда, data - данные для отправки
    void SPI_WriteArray(uint8_t cmd, uint8_t num, uint8_t *data)
    {
        nRF_SELECT();
       
        //... Отправим команду
        SPI_WriteByte(cmd);

        //... Затем данные
        while(num--)
        {
            SPI_WriteByte(*data++);
        }

        nRF_DESELECT();
    }
    Ну тоже есть простор для оптимизации)
    И напоследок макросы
    Код (C++):

    #define nRF_SELECT()            ClearBit(nRF_CSN_PORT, nRF_CSN)
    #define nRF_DESELECT()            SetBit(nRF_CSN_PORT, nRF_CSN)
    Код (C++):
    #define    nRF_CE_PORT        PORTB                //
    #define    nRF_CE_DDR        DDRB                //
    #define    nRF_CSN_PORT        PORTB
    #define    nRF_CSN_DDR        DDRB
    Код (C++):
    #define Bit(bit)  (1<<(bit))

    #define ClearBit(reg, bit)       reg &= (~(1<<(bit)))
    //пример: ClearBit(PORTB, 1); //сбросить 1-й бит PORTB

    #define SetBit(reg, bit)          reg |= (1<<(bit))  
    //пример: SetBit(PORTB, 3); //установить 3-й бит PORTB
     
    timon, b707, DetSimen и ещё 1-му нравится это.
  13. ИгорьК

    ИгорьК Оракул Модератор

    Классный проект! Обязательно буду давать на него ссылку при случае :)
     
  14. SergeiL

    SergeiL Гуру

    А это уже сделано, проверннно? Или в планах?
     
  15. parovoZZ

    parovoZZ Гуру

    Управление периферией и потребляемой мощностью
    В даташите можно отыскать вот такую табличку
    PRR.png
    Разумеется, что держать включенной неиспользуемую периферию незачем. Мы практически постоянно используем модуль USI, периодически ADC и никогда таймеры. В МК есть специальный регистр, который позволяет управлять ТАКТИРОВАНИЕМ перечисленными модулями. Регистр называется PRR. По умолчанию при включении и сбросе вся периферия тактируется. Мы это исправим. Сразу после загрузки выключаем всё
    Код (C++):
    PRR = 0xFF;                // Выключаем всю периферию
    Необходимо записать "1" чтобы выключить и "0" чтобы включить. Необходимо также помнить, что при отключенном тактировании ни один регистр модуля не доступен!
    Когда нам необходим модуль USI, включим его:
    Код (C++):
    PRR = (1<<PRTIM0) | (1<<PRTIM1) | (1<<ADC);
    С ADC посложнее. У ADC в регистре ADCSRA необходимо поднять бит ADEN, чтобы блок ADC включился в работу. И наоборот - для выключения ADC в ADEN необходимо писать "0". Для чего? Даже при отсутствии тактирования, блок ADC потребляет энергию.
    Также мы будем управлять частотой тактирования. Частота меняется с помощью предделителя
    Код (C++):
    CLKPR = (1<<CLKPCE);                // Разрешить изменение прескалера тактовой частоты
    CLKPR = (1<<CLKPS1) | (1<<CLKPS0);            // Устанавливаем тактовую частоту 1 МГц
    Измерение напряжения питания
    Обязательный пункт в нашей программе как для контроля состояния батареи питания, так и для расчета коэффициента при вычислении температуры. Т.к. ADC ест очень много, то измерять будем как можно реже. Обязательно сразу после загрузки. Примем некую переменную, которую будем декрементировать с каждым измерением температуры и влажности. Если переменная байт, то максимальный период будет составлять 255 измерений от датчика. При 10 минутном интервале измерения параметров напряжение будем снимать каждые 2 суток. Ну вот и хорошо.
    Обычно измерения на АЦП происходят следующим образом: шкала входного напряжения размечается от нуля и до референсного напряжения. Мы же перевернем всё с ног на голову и в качестве референсного напряжения будем использовать напряжение питания, а измерять будем внутренний источник референсного напряжения (в рассматриваемом МК он один и напряжение на нем примерно равно 1.1в). Также я с АЦП буду снимать только старшие 8 бит. При напряжении питания от 3-х вольт и ниже погрешность составит 11 мВ - этого более, чем достаточно. Функция инициализации
    Код (C++):
    void ADC_Init (void)
    {
        ADMUX =    (0<<REFS1) | (0<<REFS0) | ((0b100001)<<MUX0);            // Замеряем напряжение на внутреннем ИОН 1.1в
                                        // относительно опорного - напряжения питания
        ADCSRA =    (1<<ADEN) |                        // Разрешаем работу ADC
            (1<<ADIE) |                        // Разрешаем прерывание
            (0<<ADATE) |                        // Старт преобразования вручную
            (0<<ADPS2) | (0<<ADPS1) | (0<<ADPS0);                // Прескалер = 2

        ADCSRB =    (1<<ADLAR);                        // Работаем только со старшими 8-ю битами
    }
    Даташит нас предупреждает, что именно при таком способе измерения необходимо сделать паузу в 1 мс для успокоения переходных процессов. Что ж, поставим инициализироваться нашему АЦП перед инициализацией трансивера. Производим замер
    Код (C++):
     void ADC_Start_Conversion (void)
    {
        ADCSRA =    (1<<ADEN) |                      
            (1<<ADIE) |                      
            (0<<ADATE) |                      
            (0<<ADPS2) | (0<<ADPS1) | (0<<ADPS0) |              
            (1<<ADSC);                        // Команда на запуск преобразования              
    }
    Забираем данные
    Код (C++):
     uint8_t ADC_Get_Data (void)
    {
         return ADCH;
    }
    Все выключаем
    Код (C++):
     void ADC_Off (void)
    {
        ADCSRA =    0;
    }
    Т.к. результат измерения напряжения питания мы получаем тогда, когда уже вовсю идет загрузка в трансивер данных, то все действия будем производить в обработчике прерывания от АЦП. И пусть трансивер подождет!
    Код (C++):
    ISR(ADC_vect)
    {
        data[6] = ADC_Get_Data();            // Получаем результат
        ADC_Off();                // Запрещаем работу ADC
        PRR = (1<<PRTIM0) | (1<<PRTIM1) | (1<<ADC);    // Тактируем только USI
    }
     
    b707, DetSimen и ИгорьК нравится это.
  16. parovoZZ

    parovoZZ Гуру

    Формат сообщения
    Перед каждым, кто хочет собрать себе систему глупой хаты (дачи, усадьбы, гаража и проч.), непременно встает вопрос - как все устройства, входящие в такую систему, будут между собой общаться? В случае каких-то решений-полуфабрикатов вопрос стоит не так остро - разработчик предоставляет шаблон или вовсе готовый формат. В моем случае два решения - все сообщения свести к единому стандарту без оглядки на датчик и исполнительное устройство, либо же у каждого датчика будет своя посылка. В первом случае пакет сообщения будет выглядеть примерно так: {Адрес, команда/параметр, значение параметра}. Для выключателей, диммеров, термостатов, датчиков с одним каналом такое сообщение в самый раз. У нас же несколько каналов, которые содержат также дополнительную информацию. Для беспроводного общения чем короче пакет, тем больше вероятность его доставки. Но в таком случае общий объем передаваемой информации возрастает в связи с необходимостью каждый раз передавать адрес получателя, а также идентификатор передаваемого значения. Я же остановился на втором варианте - передавать всю информацию в одном пакете. Пакет выглядит так:
    Код (C++):

     

        Структура пакета:
        0 - адрес, старший байт
        1 - адрес, младший байт
        2 - тип
        3 - температура, старший байт
        4 - температура, младший байт
        5 - влажность
        6 - напряжение питания
        7 - доп. сообщение (ошибка)  
        |__
            0 бит    бит PORF - загрузка по сбросу питания
            1 бит    бит EXTRF - перезагрузка по ресету
            2 бит    бит BORF - перезагрузка по детектору питания
            3 бит    бит WDRF - перезагрузка по вачдогу
            4 бит
            5 бит    ошибка измерения Т
            6 бит    ошибка измерения RH
            7 бит    ошибка записи в регистр статуса                      

    */
    Итого всего 8 байт. Под адрес отведено 2 байта (ну а вдруг?), под тип датчика всего 1 байт. На приемной стороне необходимо применить тот алгоритм парсинга сообщения, тип которого указан в пакете.
     
  17. Asper Daffy

    Asper Daffy Иксперд

    Тут нет опечатки? Что-то ток в power-down получается может быть в полтора раза выше, максимального тока в рабочем режиме. Странно как-то.
     
  18. parovoZZ

    parovoZZ Гуру

    Трансивер nRF24l01+
    Что нужно для энергоэффективно работы с данным трансивером? Прочитать даташит. Если с первого раза не всё понятно, то можно поискать в интернете его перевод. Для уточнения нюансов опять смотрим даташит и только его. Далее. Необходимо скачать заголовочный файлик с адресами регистров. Подобный я выкладывал здесь:
    http://forum.amperka.ru/threads/nrf24l01-побеждаем-модуль.3205/page-97#post-203735
    Вопросы, которые возникают естественным образом при первом знакомстве с данным транисивером:
    1. Как с ним общаться? У трансивера только один интерфейс - SPI. Максимальная частота тактирования - 10 МГц. Прежде, чем начать общение, необходимо ногу CSN трансивера прижать к земле. Здесь надо четко для себя уяснить один момент - по SPI трансивер общается всегда, когда подано на него питание. Первым же байтом трансивер всегда отдаёт значение регистра статуса.
    2. На какую частоту программировать? Т.к. антенны для этого трансивера используются вай-файные, то и спектр их настройки также вайфайный с не выроженным максимумом примерно посередине (где-то 45 канал). Но т.к. каналы вай-фай в любой точке пространства может быть абсолютно любым, то я для себя принял решение, что буду использовать 83 канал. Этот же канал как раз находится на границе разрешенных для бесплатного (гражданского) использования.
    3. Какие адреса труб задавать? Здесь обращаемся к даташиту. Он нам говорит, что адрес для pipe необходимо задавать так, чтобы трансивер не спутал его с преамбулой (преамбула в данном трансивере в виде меандра), но также не спутал с шумами. По умолчанию, в одном из регистров трансивера записан адрес С2С2С2С2xx. Его я и буду использовать.

    Весь даташит я пересказывать не буду - нет такой задачи. Покажу самые сливки.
    Открываем даташит и видим картинку с режимами работы трансивера:
    nrf.png
    Нас интересует исключительно передача. Смотрим на картинку и видим - подаем на трансивер напряжение питания и ждем 100 мс. В этот период времени с трансивером все так же можно общаться по SPI, но общение будет не продуктивным - трансивер ничего записывать не будет. Готов ли трансивер к работе или нет, проверяется просто - записываем в любой регистр любое не дефолтное значение и затем этот же регистр читаем - если считали то, что записали, значит трансивер вышел на режим. Если нет - ждем ещё. Если всё хорошо, то трансивер переходит в режим Power Down - режим, в котором выключено абсолютно всё, кроме SPI. Для перевода трансивера в один из режимов (в нашем случае это передатчик), его необходимо включить. После того, как мы его включили и до, собственно, начала передачи (для этого пин CE необходимо поднять как минимум на 10 vrc)? необходимо выдержать паузу 1.5 мс для запуска осциллятора. В этот период времени трансивер также можно конфигурировать, в том числе и радиочасть, загружать данные, читать из него.

    Итак, пишем код.
    Мы для себя четко уясним наше ТЗ - с трансивером общаемся как можно реже, пакет пересылаем без запроса подтверждения. Для градусника это не актуально. Не приняли пакет сейчас, примем в следующий раз. Температура окружающего воздуха всё равно так быстро не меняется. А если и меняются, то ничто не мешает это заложить в алгоритм и уменьшить интервал общения - все ж в наших руках!
    Сейчас вы удивитесь, как необходимо мало кода для конфигурирования этого трансивера в качестве передатчика:
    Код (C++):
         uint8_t Buf[5];

        //.. CONFIG
        Buf[0] =    (1<<nRF_MASK_RX_DR) | (0<<nRF_MASK_TX_DS) | (1<<nRF_MASK_MAX_RT) |    // маски прерываний от событий
            (0<<nRF_PRIM_RX) |                                              // Режим передатчика
            (1<<nRF_EN_CRC) | (0<<nRF_CRCO) |                // Проверка CRC разрешена, 1 байт CRC
            (1<<nRF_PWR_UP);                                               // Запускаем трансивер
        SPI_WriteArray(nRF_WR_REG(nRF_CONFIG), 1, Buf);                // Отправляем команду. Пин CSN удерживается внутри функции

        Buf[0] = channel;                                                                        // Установка частоты канала передачи
        SPI_WriteArray(nRF_WR_REG(nRF_RF_CH), 1, Buf);            // см. Settings.h

        //.. RF_SETUP  Настройки радиоканала
        Buf[0] = (0<<nRF_RF_DR) | ((0x03)<<nRF_RF_PWR0);            // Скорость передачи 1 Mbps, мощность: 0dbm
        SPI_WriteArray(nRF_WR_REG(nRF_RF_SETUP), 1, Buf);

        //.. FEATURE  Опции
        Buf[0] = (1<<nRF_EN_DYN_ACK);                                                // Разрешаем отправку пакетов
        SPI_WriteArray(nRF_WR_REG(nRF_FEATURE), 1, Buf);            // не требующих подтверждения

        //.. TX_ADDR  Адрес канала удаленного приемника
        Buf[0] = 0xC2;                              
        Buf[1] = 0xC2;
        Buf[2] = 0xC2;
        Buf[3] = 0xC2;
        Buf[4] = 0xC2;
        SPI_WriteArray(nRF_WR_REG(nRF_TX_ADDR), 5, Buf);    // Адрес канала для передачи
    Первым делом включаем трансивер. Затем пишем канал передачи, скорость и мощность. Далее обязательная опция - разрешить отправку пакетов с данными, но без запроса автоподтверждения (флаг ACK в пакете). Далее формируем адрес удаленного pipe, на который будут отправляться наши данные. Всё! Наша ракета проверена, заправлена и объявлена подготовка к старту, чтобы из космоса транслировать байты в космос ( ээээ 0_o )! Значения всех остальных регистров на режим передачи не влияют никак и их значения нам фиолетово. А раз так, то и не будем тратить драгоценные такты на их запись!
     
    Ghosty, DetSimen, ImrDuke и ещё 1-му нравится это.