Цель - разработка экономичного беспроводного монитора температуры и относительной влажности. Что мы имеем: МК 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 и сушим весла. Такой разряд батареи - это не то, чтобы немного - это ну овердохера.
При делении 3 на 4700 столько и получится. Но делать хоть какой-то вывод о потреблении тока датчиком из этого умозаключения не верно. DS18B20 действительно плох своим рабочим диапазоном напряжений - это его засада. А вот ток лучше взять из даташита, нежели вычислять таким способом.
Датчик температуры и влажности 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. Но протокол общения несколько отличается от стандартного. Как видно по картинке из даташита, датчик никак не воздействует на линию SCK (SCL). Повторный старт и стоп отсутствуют. Признак старта также свой, особенный. Есть и приятная особенность, которой мы непременно воспользуемся - по окончании измерения датчик притягивает линию DATA к нулю, что позволяет МК усыпить самым крепким сном. У датчика присутствует квазиадрес, который интегрирован в старiие биты команды и всегда равен 0b000. В целом протокол весьма простой и всё должно быть ясно из картинки.
Теперь пару слов о Выбор МК Я никак не черепил по поводу общения с датчиком SHT1x с помощью аппаратного I2C (TWI у атмела), но особенности протокола общения с датчиком так и говорят - ну позвони мне, позвони! Позвони мне ради бога! Позвони мне по USI! Какие МК мы знаем с USI на борту? Attinyx313A, attinyx5, attinyx4A. Первый МК реально многоножка - мне столько не надо. Второй хорош, но не PicoPower (10 мкА в PowerDown!). Остался последний, на который и пал выбор. С включенным вачдогом при питании от 3-х вольт потребляет он чуть более 4-х микроампер, с выключенным вачдогом - около 100 наноампер. Такой расклад наводит на мысль о внешнем будильнике. Наноамперные таймеры в природе существуют, но сейчас не об этом. Давайте посчитаем, сколько мы будем жить в режиме глубокого сна от батарейки емкостью 250 мА/ч. Примем, что кушаем мы 4 микроампера. Делим одно на другое, что надо перемножаем и получаем: 5 лет. Вот и хорошо. Но можно ещё дольше. К слову сказать, новые attiny 0-ой и 1-ой серии показывают вообще чудеса - около 1.7 uA с включенным LowPower осциллятором.
Схемотехнические решения У нас присутствует шина 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. А это пахнет лишним энергопотреблением.
Итак, USI В сети не так уж и много русскоязычного материала по данному модулю, с англоязычным дела не лучше. Я через яндекс нашел всего пару статей - одна из них на сайте уважаемого DiHalt, вторая просто очень хороший перевод даташита с комментариями редактора. Изучение USI рекомендую начать с этого материала, а за подробностями уже обратится к даташиту. Второе - необходимо обязательно скачать материалы апноута с сайта микрочипа - там есть примеры кода. Здесь не буду пересказывать сказанное, а лишь покажу сливки. Блок-схема USI: 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 кГц. Но необходимо тестировать на минимальном напряжении питания. Может не хватить тока через подтягивающие резисторы для перезарядки емкостей).
Теперь напишем функцию-алгоритм работы с датчиком 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 программирования=)). Для экономии процессорного времени весь код я оформил в линейном виде - мне здесь важно быстро отстреляться и залечь в сон.
Ну и самое простое - инициализация 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: На все отосланные байты датчик ответил подтверждением. А вот ответ: Если посчитать по формуле из даташита, то получится 40-41% отн. влажности. Сразу же после получения ответа формируем новый запрос и получаем ответ по температуре: Здесь в кадр попал задний фронт линии SDA, который сформировал датчик. По нему мы видим, что с момента готовности результат измерений и до первого такта на линии SCK прошло всего 110 мкс. Здесь уже показывать не буду, но скажу, что на измерение RH (8 бит) у датчика ушло 14 мс, а на измерение температуры (12 бит) - 63 мс. Результат в разы лучший, чем у DS18B20.
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
Управление периферией и потребляемой мощностью В даташите можно отыскать вот такую табличку Разумеется, что держать включенной неиспользуемую периферию незачем. Мы практически постоянно используем модуль 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 }
Формат сообщения Перед каждым, кто хочет собрать себе систему глупой хаты (дачи, усадьбы, гаража и проч.), непременно встает вопрос - как все устройства, входящие в такую систему, будут между собой общаться? В случае каких-то решений-полуфабрикатов вопрос стоит не так остро - разработчик предоставляет шаблон или вовсе готовый формат. В моем случае два решения - все сообщения свести к единому стандарту без оглядки на датчик и исполнительное устройство, либо же у каждого датчика будет своя посылка. В первом случае пакет сообщения будет выглядеть примерно так: {Адрес, команда/параметр, значение параметра}. Для выключателей, диммеров, термостатов, датчиков с одним каналом такое сообщение в самый раз. У нас же несколько каналов, которые содержат также дополнительную информацию. Для беспроводного общения чем короче пакет, тем больше вероятность его доставки. Но в таком случае общий объем передаваемой информации возрастает в связи с необходимостью каждый раз передавать адрес получателя, а также идентификатор передаваемого значения. Я же остановился на втором варианте - передавать всю информацию в одном пакете. Пакет выглядит так: Код (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 байт. На приемной стороне необходимо применить тот алгоритм парсинга сообщения, тип которого указан в пакете.
Тут нет опечатки? Что-то ток в power-down получается может быть в полтора раза выше, максимального тока в рабочем режиме. Странно как-то.
Трансивер 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. Его я и буду использовать. Весь даташит я пересказывать не буду - нет такой задачи. Покажу самые сливки. Открываем даташит и видим картинку с режимами работы трансивера: Нас интересует исключительно передача. Смотрим на картинку и видим - подаем на трансивер напряжение питания и ждем 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 )! Значения всех остальных регистров на режим передачи не влияют никак и их значения нам фиолетово. А раз так, то и не будем тратить драгоценные такты на их запись!