Суперэкономичный беспроводной монитор Т и 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-му нравится это.