Перейти к содержанию

parovoZZ

Members
  • Постов

    169
  • Зарегистрирован

  • Посещение

Записи блога, опубликованные parovoZZ

  1. parovoZZ
    Казалось бы, ведь этого добра навалом?
    Какие задачи ставились при проектировании диммера:
    1. Компактный настолько, насколько это возможно. Должен влезать в монтажную коробку.
    2. Блок питания простой настолько, насколько это возможно
    3. Пружинные клеммники
    4. Конечно же, радиоинтерфейс
    5. Проводное управление. Зачем? Пусть будет.
    6. Полноценное подключение к сети. Двухпроводка не планировалась изначально.
    Устройство собрано на МК, а это значит, что можно организовать абсолютно любую функциональность устройства. Например, таймер работы - забыли выключить свет и он выключился через заданный промежуток времени самостоятельно. Плавное включение и выключение. Задержка выключения после подачи команды на выключение - не интересно же уходить в потёмках? Обратная связь в виде изменения яркости свечения на одну ступень. Ну и прочие эффекты.
    Схема здесь:
    https://easyeda.com/Parovozz/dimmer-v1-1-841
    Схема не по ГОСТ. Нарисована хрен знает когда. Так что звиняйте.
    Блок питания - классический конденсаторный. Такое решение крайне надёжное (применён X/Y конденсатор), но есть у него недостаток: заряд накопительного конденсатора в фильтре выпрямителя происходит только в отрицательный полупериод сети. При положительном периоде заряда нет и питаемся мы только той энергией, которая есть в этом конденсаторе. Есть и положительный момент - заряд и подпитка конденсатора происходит в течение всего отрицательного периода, пока напряжение в сети выше напряжения на конденсаторе. В классическом выпрямителе не так - заряд происходит только в пике полуволны.
    Емкость балластного конденсатора выбрана не слишком большой, чтобы удовлетворить требованиям по габариту решения. Поэтому и ток в цепи не высок - всего 40 мА. Такое решение накладывает сильные ограничения на потребляемый ток. Поэтому микроконтроллер всегда спит, когда ему делать нечего. Симистор открывается коротким импульсом, не превышающим 50 мкс. Но основной потребитель энергии - это, конечно же, радиотрансивер. В качестве него применяется набивший оскомину nrf24l01+. Он не позволяет задавать длительность преамбулы радиопакета сколь угодно длинной - всего 5 байт. Поэтому фокусы с его периодическим отключением (для сохранения энергии) не проходят. 
    Обязательно наличие предохранителей. Это системный предохранитель в виде PTC с начальным сопротивлением около 50 Ом, а также предохранитель цепи питания лампы - классический 5х20. Детектор нуля - резисторный. Количество резисторов и их типоразмер подобран из условия номинального напряжения на них.
    Обязательно включён вачдог с периодом 250 мс, обязательно включен монитор питания. Уровень напряжения монитора питания согласован с минимальным уровнем питания радиотрансивера. Нам не зачем работать, если радиотрансивер "отвалился" по питанию. Радиотрансивер же каждые 5 секунд проходит полную переинициализацию вне зависимости ни от чего.
    Большую головную боль доставили клеммники. Они должны быть пружинными, компактными и на 250 вольт. Такие нашёл (выводы у них расположены по диагонали, что и дало 250 вольт), но только под провод 1.5 мм2. Высота клеммников всего 12 мм и они отлично согласуются с высотой остальных компонентов, которые подбирались очень тщательно. Итоговые размеры 45х50. Скошенные края для удобства укладки проводов. Design =)
    Симистор в планарном исполнении, охлаждение с помощью полигона на плате. Устройство изначально планировалось под 1 ампер тока в нагрузке, но LED лампы гораздо скромнее в своих аппетитах... Симистор подобран чувствительным - ток управления всего 5-10 мА. Управление непосредственно с ноги МК через ограничительный резистор. TVS диод для снижения уровня импульсов, проникающих через емкость симистора.
    Устройство собрано на двух платах: одна плата силовая, вторая плата с МК и трансивером. МК - Attiny441/841. Платы крепятся друг к другу перпендикулярно. Монтаж преимущественно планарный, двухсторонний.
    По итогу эксплуатации выяснилось следующее. Каждая светодиодная лампа диммируется по-своему. Лампы из икеа (ледаре) разных лет выпуска так же работают по-разному. Более старшая не хочет зажигаться, если фаза напряжения диммирования начинается не со 180 градусов. А если открывать симистор сразу после перехода через ноль, то загорается сразу. Диммирование "вниз" - нет никаких проблем. Правда, на момент покупки (более 7 лет назад) она не позиционировалась как диммируемая. Новая ледаре не имеет таких проблем, она легче по весу, и...гудит. Очень понравились филаментные лампы. При диммировании ведут они себя как лампы накаливания. Итог такой - под каждую конкретную лампу необходимо подстраивать параметры.
     


    *провода для отладки.
  2. parovoZZ
    Многие видели промышленные изделия, на платах которых присутствует индикатор "здоровья". Т.е такой индикатор, который моргает и частота моргания напрямую зависит от состояния устройства - часто моргает или редко - заболел совсем или частично, не моргает - что-то сыграло в ящик. Ну или забыли в сеть включить).
    Давайте тоже создадим такой индикатор. Но пойдём ещё дальше - моргать наш индикатор будет не однократными, а двухкратными вспышками. На диаграмме логического анализатора это будет выглядеть примерно так:

    Для измерения периодов будет использовать таймер типа TCA. TCA - это тип таймера, а не собственно сам таймер, поэтому запись
    TCA.xxx.yy будет ошибочна. Порядковый номер таймера обозначается цифрой. В нашем случае это таймер TCA0. TCA таймер - это до боли знакомый по atmega таймер - 16 битный счетчик, 3 компаратора, логика счета вверх и вниз. Но есть и новшества - буферные регистры для компараторов и регистра периода.
    Режимов работы у таймера несколько - нормальный режим, режим односкатный ШИМ (счетчик считает вверх до максимума и обнуляется), двускатный ШИМ (счетчик досчитав до максимума начинает отсчет вниз). Для реализации нашей задумки мы как раз-таки и будем использовать последний режим. В этом режиме компараторы работают так, как показано на следующем графике:

    Идея понятна из графика - при счете вверх и совпадении значений регистров CNT и CMPx выход компаратора очищается, при счете вниз - устанавливается. Мы видим, что если выходы компараторов сложить логически по XOR, то на выходе мы получим именно то, что нужно. Складывать (ксорить) мы будем в блоке CCL - Custom Control Logic/
    Давайте настраивать наш таймер. Но для начала необходимо настроить главный делитель частоты. С помощью фьюза я выставил привычную для меня частоту в 16 МГц.

    Сразу после ресета, по умолчанию, делитель настраивается на коэффициент 6. Мы это исправим и выставим коэффициент деления 2 не забывая про регистр защиты CCP:
    CCP = CCP_IOREG_gc; CLKCTRL.MCLKCTRLB = CLKCTRL_PDIV_2X_gc | CLKCTRL_PEN_bm; // Прескалер главной тактовой частоты первый бит - это разрешение работы делителя. Если он равен нулю, то деление частоты не производится и равно 1.
    Переходим к настройке таймера TCA0. У таймера есть два фундаментальных режима работы - нормальный (single) и сдвоенный (split). В сдвоенном режиме 16-ти битный счетчик распадается на два 8-ми битных и каждый тикает независимо. также в этом режиме количество компараторов умножается на два. Но этот режим я сейчас разбирать не буду. Остановимся на нормальном режиме.
    Нормальный режим работы указывается определением SINGLE, как показано ниже. Теперь зададим делитель = 1024 и дадим команду на работу таймера:
    TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1024_gc | // Определяем значение прескалера TCA_SINGLE_ENABLE_bm; // и включаем таймер Далее необходимо задать режим работы таймера:
    TCA0.SINGLE.CTRLB = 1<<TCA_SINGLE_ALUPD_bp | // Установим бит автоматической блокировки обнолвения регистров компаратора (0 << TCA_SINGLE_CMP0EN_bp) | (0 << TCA_SINGLE_CMP1EN_bp) | (0 << TCA_SINGLE_CMP2EN_bp) | // пины выводов компараторов не включаем TCA_SINGLE_WGMODE_DSTOP_gc; // Режим работы таймера - двухскатный ШИМ с прерыванием OVF в вершине счета В нашем случае это двухскатный ШИМ с переполнением в максимуме. Бит ALUPD блокирует обновление регистров периода и компараторов тогда, когда это может вызвать уход таймера из под контроля. Т.е. если значение в регистре-буфере периода меньше, чем текущее значение таймера, то регистр периода обновится только тогда, когда значение счетчика станет меньше значения регистра-буфера периода. Также и с регистрами компаратора. Биты CMPхEN разрешают сопоставить свои выходы с физическими выходами МК. Но для физического появления сигнала на выводах, эти выводы необходимо определить на выход. Сопоставление выходов компараторов с физическими ногами задано жестко, но у МК многоножек (таких, как Attiny817) есть также и альтернативные ноги. У attinyxx14 таких альтернативных ног нет. Но свободное назначение не предусмотрено. Мы же записываем в эти биты нули, ибо выходы компараторов у нас будут соединены с модулем CCL внутри МК.
    Всё. Таймер у нас настроен и уже тикает. Осталось только наполнить регистры значениями, чтобы тикалось так, как нам хочется. Как обычно, чтобы не искать в коде значения констант, я их выношу в самый вверх в виде определений:
    #define TCA_period 4000 #define TCA_cmp0 3000 #define TCA_cmp1 2500 И где-то в коде заполняем регистры:
    TCA_Init(); TCA0.SINGLE.PER = TCA_period; TCA0.SINGLE.CMP0 = TCA_cmp0; TCA0.SINGLE.CMP1 = TCA_cmp1; С таймером на этом всё. Переходим к CCL/
    CCL. Configurable Custom Logic. Чтобы понять суть этого блока, давайте посмотрим на его блок схему.

    CCL состояит из двух таблиц истинности (LUT), далле фильтр/синхронизатор, детектор фронта и обе таблицы совими выходами могут быть подключены к Sequental Logic (не знаю, как правильно перевести. Дословно - последовательная логика. Почему? Либо они курят чего, либо я не вытягиваю предмет). Для чего фильтр? Тот, кто работал с логическими схемами в железе (155 серия, ага)), знает, что сигнал от входа к выходу в реальном железе распространяется не мгновенно, а с конечной скоростью. Так вот если даже на входе сигнал изменится одновременно, то внутренняя схема переключается с задержкой и из-за этого возникают иголки на выходе. Иголки по длительности совсем короткие, но весьма способны повлиять на работу дальнейшей схемы. Особенно этому подвержены дешифраторы, сложные логические схемы (про абсолютную помехуНЕустойчивость наших КМОП серий знают, наверное, все). В виду того, что LUT может быть подключен к внешним выводам МК как входами (через систему асинхронных событий), так и выходами, то для исключения таких вот иголок и введен фильтр. Работает просто - изменения сигнала привязываются к тактирующему сигналу. Детектор фронта, как утверждает даташит, детектирует только передний фронт. Если нужна реакция по заднему фронту - переверните логику работы LUT. В даташите так вот и написано. Sequental Logic может быть одним из триггеров: RS, D, и JK. D триггер представлен в двух вариациях - классический с входами Data и Clock и с защелкой (Gate). Если на входе защелки установлен "0", то триггер не при каких обстоятельствах не меняет своего состояния. Но всю эту кухню мы сегодня готовить не будем, а разберем LUT.
    LUT представляет из себя конфигурируемую логику с тремя входами и одним выходом. Реакция выхода на входное воздействие полностью программируется. В нашем случае мы используем всего два входа. Логика работы такая - если на этих двух входах разные состояния, то на выходе "1". В остальных случаях "0". XOR, одним словом.

    Реакцию выхода на входное воздействие написали, теперь подключим это входное воздействие. Подключаться будем к выходам компараторов таймера TCA0:
    CCL.LUT0CTRLB = CCL_INSEL0_TCA0_gc // W0 | CCL_INSEL1_TCA0_gc // WO1; Теперь остальные настройки - разрешим работу LUT и разрешим его выход подключить на физическую ножку:
    CCL.LUT0CTRLA = 0 << CCL_CLKSRC_bp | 1 << CCL_ENABLE_bp | 1 << CCL_OUTEN_bp; Бит CLKSRC определяет из какого источника будут тактироваться фильтр и детектор фронта. Здесь мы их не используем, поэтому смело ставим в ноль.
    Ножку выхода LUT также надо настроить на выход, иначе ничего вообще работать не будет (в буквальном смысле):
    PORTC.DIR |= PIN0_bm; Работу LUT разрешили, теперь надо разрешить работу самого блока CCL:
    CCL.CTRLA = 1 << CCL_ENABLE_bp | 0 << CCL_RUNSTDBY_bp; Последний бит определяет - будет ли блок работать в режиме StandBy или нет. Это напрямую влияет на энергопотребление, поэтому изначально все модули выключены (в серии atmega наоборот ((____ ).
    Ну вот и всё. Осталось написать самый главный код в самом главном цикле и запустить программу в свободное выполнение:
    while (1) { } Прошиваем МК и смотрим на подключенный к выводу PC0 светодиод. Т.к. его вы не видите, то специально для вас я подключил ЛА и выложил картинку в первом сообщении.
    ENJOY!
  3. parovoZZ
    USI as SPI
    Для работы с трансивером нам необходим интерфейс SPI. Извлекать его будем всё из того же модуля USI. Здесь кратенько. Линии данных у SPI однонаправленные, а это значит, что пин DO всегда подключен к концу сдвигового регистра, а DI - к началу. Таким образом организован режим передачи full duplex. Здесь нам не нужны никакие подтягивающие резисторы, т.к. линия ВСЕГДА либо в нуле, либо в единице. А когда мы не работаем по интерфейсу SPI, то нам и пофигу, что творится на линиях. Здесь только про режим master. Здесь нет пина SS - соответственно нет и проблем, которые он создает в аппаратном SPI (пин SS всегда должен быть сконфигурирован как выход, если мы работаем ТОЛЬКО как мастер. Как только на SS приходит низкий уровень, аппаратный SPI бросает всё и переключает себя в режим SLAVE). Трансивер по интерфейсу SPI способен работать со скоростями вплоть до 10 МГц. Поэтому наша задача организовать тактирование максимально энергоэффективно ни теряя понапрасну ни единого такта.
    Начнем с функции инициализации.
    //... Инициализация 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); } Здесь не нужны ни счетчик, ни его убогие флаги, ни флаги старта, стопа - всё это идет лесом.
    Далее идет функция отправки элементарного байта.
    //... Передать байт данных 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 раз!
    Так сложилось исторически, но функция отправки и получения байта отдельно
    //... Передать и получить байт данных uint8_t SPI_ReadByte(uint8_t data) { //... Отправляем байт SPI_WriteByte(data); //... Принятый байт возвращаем return USIDR; } Функция отправки массива байт
    //... Отправить несколько байт по 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(); } И напоследок макросы 
    #define nRF_SELECT() ClearBit(nRF_CSN_PORT, nRF_CSN) #define nRF_DESELECT() SetBit(nRF_CSN_PORT, nRF_CSN) #define nRF_CE_PORT PORTB // #define nRF_CE_DDR DDRB // #define nRF_CSN_PORT PORTB #define nRF_CSN_DDR DDRB #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  
  4. parovoZZ
    Как часто у вас бывало так, что необходимо хранить данные в ЭСППЗУ, но в текущем МК места не хватало? Приходится брать следующий по уровню МК, хотя во всем остальном он избыточен. В MSP430 на основе FRAM памяти это уже может быть вовсе и не проблемой. Изначально FRAM память поделена на сегменты. В старших моделях таких сегментов 3 с произвольной защитой каждого из них, а в кристалле 2433 всего два сегмента: под код программы и под информационный сегмент. Но вся прелесть в том, что мы можем сохранять свои данные не только в информационном сегменте (у нас он всего 512 байт), но и в сегменте программ.
    От слов к делу. Сколько будем хранить? У меня IAR с лицензионным ограничением линковщика на 8кБ. Вот давайте столько и запишем =)
    Подключаем обязательные файлы
    #include "io430.h" #include "stdint.h" Определяем константу с сохраняемым объемом:
    #define Write_Size 4000 Создадим массив с вышеприведенным количеством элементов. У нас же архитектура 16 бит? Вот столько и запишем в элемент массива:
    __persistent uint16_t EEPROM[Write_Size]; директива __persistent - это аналог __no_init с той разницей, что __no_init указывает компилятору не инициализировать переменную в области SRAM, а __persistent - во FRAM области.
    Инициализируем счетчик, отключаем вачдог и переводим пин со светодиодом (для визуального наблюдения) на выход и обнуляем регистр вывода:
    uint16_t count = 0; WDTCTL = WDTPW + WDTHOLD; P1DIR |= BIT0; P1OUT = 0; Далее идет очень хитрая штука. Называется она регистр PM5CTL0. В этом регистре есть бит LOCKLPM5, назначение которого блокировать регистры порта ввода-вывода. А такое необходимо, т.к. выход из режимов x.5 возможен только через ресет. По умолчанию этот бит установлен и вся работа с пинами заблокирована. Чтобы разблокировать, необходимо в него записать 0:
    PM5CTL0 &= ~LOCKLPM5; Дальше пишем цикл:
    while (1) { if (EEPROM[Write_Size - 1] == Write_Size - 1) { P1OUT |= BIT0; } else { SYSCFG0 = FRWPPW; for (uint16_t i = 0; i < Write_Size; i++) { EEPROM[i] = count++; } SYSCFG0 = FRWPPW | PFWP; } } Если в последнем элементе массива ожидаемое значение, то загорается светодиод, если нет, то уходим в подпрограмму записи элементов массива. Но прежде, чем его писать, необходимо разрешить запись в область FRAM памяти. Это делается с помощью записи разрешающей сигнатуры, которая определена в константе FRWPPW, в регистр SYSCFG0. По окончании записи и для защиты FRAM памяти от перезаписи необходимо поднять бит PFWP. В заголовочном файле вся эта кухня записана так:
    /* SYSCFG0 Control Bits */ #define PFWP_L (0x0001u) /* Program FRAM Write Protection */ #define DFWP_L (0x0002u) /* Data FRAM Write Protection */ #define FRWPPW (0xA500u) /* FRAM protection password */ Запускаем IAR, прошиваемся. Передергиваем питание и снова заходим в дебаггер с помощью кнопочки

    Запускаем окно Symbolic Memory, где переходим по адресу 0хС400 (можно подсмотреть в окне Watch по начальному адресу массива) или с помощью меню выбираем FRAM. Листаем область памяти до конца и видим такую картину

    Все наши 8000 байт записаны и лежат в недрах МК.
    Enjoy!
  5. parovoZZ
    Приехала ко мне прямо от ковбоев плата под звучным названием LAUNCHPAD. Что на ней имеется? Прежде всего, это целевой МК на основе FRAM памяти - MSP430FR2433. Также на плате присутствует программатор/отладчик. И самое вкусное - это аппаратная обвязка технологии EnergyTrace™ Technology.
    В какой среде писать программу? TI предлагает две "каропки" - это полностью бесплатная CCS на основе ECLIPSE, и второй вариант - IAR. Также присутствует онлайн-редактор с минимальными возможностями (необходим агент на компьютере и расширение в браузере). CCS - это крайне тормознутый монстр (по сравнению с MSVS), при этом в нем есть всё для комфортной работы, хотя несколько и непривычно после MSVS (необходимо нажимать Ctrl + Space, чтобы появились подсказки).
    Первая мысль после запуска IAR - да поотрубать всем головы, кто ЭТО придумал. Редактор кода убогий настолько, насколько это возможно в принципе. Это самый обыкновенный блокнот с минимальной подсветкой. Правда, можно прикрутить внешний редактор типа notepad++, но тогда все "фенечки" (которых и так нет) исчезнут (нельзя перейти к декларации переменной/функции, нет подсказок и прочее). Тем не менее, IAR очень быстро запускается и очень шустро работает. Лично я пока его и использую, несмотря на всю убогость редактора. Лицензионная политика у IAR такая - либо бесплатно всегда, но не более 8кБ, либо без ограничений, но 30 дней. MISRA в обоих случаях не доступна.
    Мало кто знает, но у MSP430 тоже есть свой фреймворк, который полностью повторяет "arduino". Называется он Energia. Но, т.к. я изучаю МК, а не фреймворки, пользоваться я ей не буду.
    Литература у TI для своих МК организована так (это не правило, но в большинстве случаев так) - есть даташит на целое семейство МК (в нашем случае это MSP430FR4xx and MSP430FR2xx family User's Guide) с общим описанием всевозможных модулей, а для отдельных МК из этого семейства прилагается ещё один даташит уже с подробным описанием (что есть и чего нет в МК описывается именно в этом даташите). Для MSP430FR2433 документ так и называется: MSP430FR2433 Mixed-Signal Microcontroller datasheet.
    Давайте уже поморгаем. Моргать в стиле "Hello, world" уже не интересно, поэтому будем моргать в стиле "Привет, ЗЕМЛЯНЕ!". Тем более, что у нас для этого есть аж пять таймеров со счетчиками в 16 бит.
    Первое, что делаем, выключаем вачдог (по-умолчанию он включен):
    WDTCTL = WDTPW | WDTHOLD; Затем необходимо определить как выход тот пин, который мы можем подключить к блоку сравнения таймера и на котором у нас сидит светодиод. Это первая нога первого порта:
    P1DIR |= BIT1; Бит BIT1 в заголовочнике определен как:
    #define BIT1 (0x0002) Затем нам необходимо подключить выход блока сравнения к пину, чтобы на нем генерировать ШИМ. MSP430FR2433 здесь особого простора не предоставляет - всё жестко привязано аппаратно внутри. Поэтому открываем даташит на МК и смотрим картинку:

    К пину 1 порта 1 (P1.1) можно подключить первый компаратор нулевого таймера А (TA0). Первое услови е мы уже выполнили - пин переключили на выход. Теперь необходимо определить альтернативную функцию для выбранного пина. Это делается с помощью пары регистров P1SEL0 и P1SEL1. В них, согласно таблицы, необходимо записать число 10 на то место в регистре, пин которого должен обладать альтернативной функцией (да, мозгодробительно))):
    P1SEL1 = BIT1; Если бы мы подключали выход второго компаратора таймера А к пину (а это второй пин первого порта), то писали бы так:
    P1SEL1 = BIT2; Теперь запускаем таймер. ШИМ-ить будем, как уже сказали, нулевым таймером Timer_A (это экземпляр типового таймера A3). Чтобы таймер заработал, ему необходимо подключить источник тактирования. Источников тактирования в MSP430 несколько: внутренние ACLK и MSCLK, а также внешние - TACLK и INCLK. Для разнообразия, мы будем использовать сразу два: ACLK и MSCLK. Первый генерирует частоту 32768 Гц, второй 1 МГц (по-умолчанию. Но возможно и перестроить). Для нулевого таймера Timer_A бы будем использовать частоту 1 МГц, поэтому подключим MSCLK. Счетчик в этом типе таймера может считать до определенного значения как вверх, так и вверх и вниз, а также только вверх до максимального совего значения (0xFFFFFFFF). Мы определим, что счетчик считает только вверх и сбрасывается при достижении значения 256. Также перед включением таймера необходимо сбросить его самого и его предделитель. За это отвечает бит TACLR. Теперь всё сказанное запишем в коде:
    TA0CCR0 = 256; TA0CTL = TASSEL__SMCLK | MC__UP | TACLR; Перед запуском таймера необходимо настроить компаратор. У компаратора есть несколько режимов работы. Их можно посмотреть в даташите на семейство:

    Среди них я выбрал режим Toggle/Reset. Это режим Mode2:
    TA0CCTL1 = OUTMOD_2; И определим значение регистра сравнения сразу после инициализации:
    TA0CCR1 = 255; Всё, таймер настроен и уже работает. Для модуляции ШИМ -а будем использовать другой таймер этого же типа - TA1.
    Чтобы промодулировать наш ШИМ генератор (создание эффекта плавного затухания и загорания светодиода), я буду использовать такую конструкцию, вызываемую в прерывании от первого таймера Timer_A:
    #if defined(__TI_COMPILER_VERSION__) || defined(__IAR_SYSTEMS_ICC__) #pragma vector=TIMER1_A1_VECTOR __interrupt void TIMER1_A1_ISR(void) { switch(__even_in_range(TA1IV, TA1IV_TAIFG)) { case TA1IV_TAIFG: if (dir) { TA0CCR1 = count; } else { TA0CCR1 = 256 - count; } if (!count) { dir = !dir; } count --; break; default: break; } } где:
    volatile uint8_t count; volatile uint8_t dir; Ну и запуск таймера TA1. Отличие здесь в разрешении прерывания, источнике тактирования и отсутствие работы компараторов:
    TA1CCR0 = 255; TA1CTL = TASSEL__ACLK | MC__UP | TACLR | TAIE; Чтобы зря не гонять процессор между прерываниями, я его буду останавливать. Исторически так сложилось, что MSP430 предлагает богатый набор режимов энергосбережения. Я буду использовать режим остановки процессора - LPM3:
    __bis_SR_register(LPM3_bits | GIE); __no_operation(); // Для внутрисхемной отладки Выход из режима энергосбережения или его смена в МК MSP430 устроено несколько иначе, чем в Atmel. Суть в том, что периферийное устройство будит процессор, запускает необходимый осциллятор для своей работы, после завершения своей работы останавливается осциллятор (если он больше никому не нужен), а процессор уходит в тот режим, из которого его разбудили. И здесь каких-то телодвижений со стороны программиста совершать не требуется. Другое дело, если мы хотим совершить какие-то действия вне прерывания после пробуждения. Тогда в обработчике прерывания нам необходимо вручную сменить режим работы.

    Ну и вишенка: EnergyTrace™ Technology.
    На плате установлен ШИМ стабилизатор на основе МК из семейства MSP430. Как оно устроено и как работает - всё есть в документах от TI. Суть в том, что такая технология позволяет измерять токи вплоть до сотни наноампер с приемлемой точностью. Из минусов - работает только в режиме дебага. Впрочем, запускаем наш код и смотрим на токопотребление:

    Мы видим, что с ростом скважности ток падает и наоборот. Ну оно и логично.
    Enjoy!
  6. parovoZZ
    Всем привет!
    В этом топике я покажу, как с помощью новейшей Attiny от Atmel/Microchip управлять светодиодами WS2812. Для начала давайте вспомним, а лучше подсмотрим в даташите на временные интервалы нуля и единицы.

    Где видим, что для передачи нуля нам необходим импульс длительностью от 200 и до 500 наносекунд, для передачи единицы - от 550 и до 850 наносекунд. Период же так и вообще может изменяться в широких пределах.
    Управлять светодиодами мы будем с помощью SPI. Но не напрямую, а через CCL. Почему? Всё просто - SPI содержит 8-ми битный регистр и как его не крути, целочисленная передача не получится - исходный бит надо будет "разбавлять" нулями либо единицами. А что если поступить наоборот - длительность исходного бита будет задавать период сигнала, а разбивать бит будем тактовой частотой SCK того же интерфейса SPI. Так мы сформируем "1", которую поймет драйвер светодиода. Да, T1H и T1L по длительности будут равны, но в параметры даташита мы укладываемся. Как же быть с нулем? Для этого мы воспользуемся помощью зрительного зала таймера TCA0. Пусть он нам разобьет тактовую частоту SCK ещё раз напополам и мы получим длительность T0H в четыре раза меньшую, чем период. Но всё равно укладываемся в даташит)) В результате манипуляций мы получим такую картину:

    Звиняюсь, но диаграммы отображают (не то, что там подписано....)
    желтая диаграмма - это RGB код для светодиодов, зеленая диаграмма - тактовая частота SCK, синий - меандр с таймера TCA0, фиолетовый - подготовленные импульсы для подачи на WS2812.
    Работать мы будем на тактовой частоте в 20 МГц, поэтому первое, что сделаем, отключим прескалер:
    CCP = CCP_IOREG_gc; CLKCTRL.MCLKCTRLB = 0; Затем подготовим блок SPI:
    //... Инициализация SPI inline void SPI_Init(void) { SPI_PORT.DIR |= (1<<MOSI_PIN) | (1<<SCK_PIN) | (0<<MISO_PIN) | (1<< PIN4_bp); // Все выводы, кроме MISO, выходы SPI0.CTRLA = (0<<SPI_DORD_bp) // Старший бит вперёд | (1<<SPI_MASTER_bp) // SPI мастер | (1<<SPI_CLK2X_bp) // Удвоенная скорость тактирования | SPI_PRESC_DIV64_gc // Делитель = 64 | (1<<SPI_ENABLE_bp); // Разрешаем работу SPI SPI0.CTRLB = (0<<SPI_BUFEN_bp) | (0<<SPI_BUFWR_bp) | (1<<SPI_SSD_bp) | SPI_MODE_0_gc; SPI0.DATA = 0; } //... Передать байт данных inline void SPI_WriteByte(uint8_t data) { SPI0.DATA = data; while(!(SPI0.INTFLAGS & SPI_IF_bm)); } Выставляем прескалер на 64 такта и взводим бит SPI_CLK2X, что в итоге нам даст частоту тактирования 625 кГц. Период тактирования при такой частоте будет равен 1.6 мкс - в даташит укладываемся.

    Далее настроим таймер:
    void TCA_Init(void) { TCA0.SINGLE.CTRLB = 1<<TCA_SINGLE_ALUPD_bp | // Установим бит автоматической блокировки обновления регистров компаратора (0 << TCA_SINGLE_CMP0EN_bp) | (0 << TCA_SINGLE_CMP1EN_bp) | (0 << TCA_SINGLE_CMP2EN_bp) | // пины выводов компараторов не включаем TCA_SINGLE_WGMODE_SINGLESLOPE_gc; // Режим работы таймера - односкатный ШИМ с прерыванием OVF в вершине счета } Режим работы таймера следующий - счетчик таймера считает от нуля и до значения в регистре PER, после чего самостоятельно обнуляется и цикл повторяется.
    Значение прескалера таймера и его режим работы я определю в отдельной функции:
    inline void TCA0_Mode(uint8_t mode) { TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | // Определяем значение прескалера - 1 mode << TCA_SINGLE_ENABLE_bp; // mode = 1 - таймер включен, mode = 0 - таймер выключен } Уже в основной функции настрою регистры:
    TCA_Init(); TCA0.SINGLE.PER = TCA_period; TCA0.SINGLE.CMP2 = TCA_cmp2; Компаратор использую CMP2 - далее будет понятно почему. Выход компаратора устанавливается в "1" при сбросе счетчика, а сбрасывается при равенстве значений в счетном регистре и регистре компаратора. Для того, чтобы получить частоту 625 * 2 = 1250 кГц, я определю следующие значений регистров:
    #define TCA_period 15 #define TCA_cmp2 8 Давайте подключать таблицу LUT CCL. Для этого открываем даташит и внимательно читаем, на какой вход что можно подключить. Напомню, что нам надо завести сигналы от:
    -MOSI SPI
    -SCK SPI
    -WO2
    Смотрим - на нулевой канал LUT мы можем подключить от SPI только сигнал SCK (скопирую из файла определений iotn817.h):
    /* LUT Input 0 Source Selection select */ typedef enum CCL_INSEL0_enum { CCL_INSEL0_MASK_gc = (0x00<<0), /* Masked input */ CCL_INSEL0_FEEDBACK_gc = (0x01<<0), /* Feedback input source */ CCL_INSEL0_LINK_gc = (0x02<<0), /* Linked LUT input source */ CCL_INSEL0_EVENT0_gc = (0x03<<0), /* Event input source 0 */ CCL_INSEL0_EVENT1_gc = (0x04<<0), /* Event input source 1 */ CCL_INSEL0_IO_gc = (0x05<<0), /* IO pin LUTn-IN0 input source */ CCL_INSEL0_AC0_gc = (0x06<<0), /* AC0 OUT input source */ CCL_INSEL0_TCB0_gc = (0x07<<0), /* TCB0 WO input source */ CCL_INSEL0_TCA0_gc = (0x08<<0), /* TCA0 WO0 input source */ CCL_INSEL0_TCD0_gc = (0x09<<0), /* TCD0 WOA input source */ CCL_INSEL0_USART0_gc = (0x0A<<0), /* USART0 XCK input source */ CCL_INSEL0_SPI0_gc = (0x0B<<0), /* SPI0 SCK source */ } CCL_INSEL0_t; На первый канал подключается только MOSI:
    /* LUT Input 1 Source Selection select */ typedef enum CCL_INSEL1_enum { CCL_INSEL1_MASK_gc = (0x00<<4), /* Masked input */ CCL_INSEL1_FEEDBACK_gc = (0x01<<4), /* Feedback input source */ CCL_INSEL1_LINK_gc = (0x02<<4), /* Linked LUT input source */ CCL_INSEL1_EVENT0_gc = (0x03<<4), /* Event input source 0 */ CCL_INSEL1_EVENT1_gc = (0x04<<4), /* Event input source 1 */ CCL_INSEL1_IO_gc = (0x05<<4), /* IO pin LUTn-N1 input source */ CCL_INSEL1_AC0_gc = (0x06<<4), /* AC0 OUT input source */ CCL_INSEL1_TCB0_gc = (0x07<<4), /* TCB0 WO input source */ CCL_INSEL1_TCA0_gc = (0x08<<4), /* TCA0 WO1 input source */ CCL_INSEL1_TCD0_gc = (0x09<<4), /* TCD0 WOB input source */ CCL_INSEL1_USART0_gc = (0x0A<<4), /* USART0 TXD input source */ CCL_INSEL1_SPI0_gc = (0x0B<<4), /* SPI0 MOSI input source */ } CCL_INSEL1_t; И на третьем канале от таймера TCA0 возможно подключить только WO2:
    /* LUT Input 2 Source Selection select */ typedef enum CCL_INSEL2_enum { CCL_INSEL2_MASK_gc = (0x00<<0), /* Masked input */ CCL_INSEL2_FEEDBACK_gc = (0x01<<0), /* Feedback input source */ CCL_INSEL2_LINK_gc = (0x02<<0), /* Linked LUT input source */ CCL_INSEL2_EVENT0_gc = (0x03<<0), /* Event input source 0 */ CCL_INSEL2_EVENT1_gc = (0x04<<0), /* Event input source 1 */ CCL_INSEL2_IO_gc = (0x05<<0), /* IO pin LUTn-IN2 input source */ CCL_INSEL2_AC0_gc = (0x06<<0), /* AC0 OUT input source */ CCL_INSEL2_TCB0_gc = (0x07<<0), /* TCB0 WO input source */ CCL_INSEL2_TCA0_gc = (0x08<<0), /* TCA0 WO2 input source */ CCL_INSEL2_TCD0_gc = (0x09<<0), /* TCD0 WOA input source */ CCL_INSEL2_SPI0_gc = (0x0B<<0), /* SPI0 MISO source */ } CCL_INSEL2_t; Пишем:
    CCL.LUT0CTRLB = CCL_INSEL0_SPI0_gc // Выбор канала SPI SCK | CCL_INSEL1_SPI0_gc; // Выбор канала SPI MOSI CCL.LUT0CTRLC = CCL_INSEL2_TCA0_gc; // Выбор канала W02 таймера TCA Ну и общие настройки, про которые уже писал:
    CCL.LUT0CTRLA = 0 << CCL_CLKSRC_bp // Блок CCL не тактируем | 1 << CCL_ENABLE_bp // Разрешение работы LUT0 | 1 << CCL_OUTEN_bp; // Разрешение выхода LUT0 CCL.CTRLA = 1 << CCL_ENABLE_bp // Разрешение работы CCL | 0 << CCL_RUNSTDBY_bp; // В стендбае не работаем Теперь необходимо запрограммировать таблицу истинности. Для этого смотрим на диаграмму выше и отмечаем входные состояния, при которых на выходе "1". В остальных случаях "0". У меня получилось следующее:
    CCL.TRUTH0 = 0xA8; // Таблица истинности Теперь запишем функцию трансляции байта на выход MOSI модуля SPI. Также не забываем, что нам необходимо останавливать таймер TCA0 по окончании пересылки байта и запускать его (со сбросом) в начале посылки, иначе он нам тут натикает совсем не то)))
    void SPI_out8bit(uint8_t data) { TCA0.SINGLE.CNT = 0; TCA0_Mode(start); SPI_WriteByte(data); TCA0_Mode(stop); } Всё, что нам осталось - это заслать байты в порядке очереди:
    SPI_out8bit(green[0]); SPI_out8bit(red[0]); SPI_out8bit(blue[0]); Где:
    uint8_t blue[1]; uint8_t red[1]; uint8_t green[1]; Как видите - никакого ассемблера, куча времени для расчета эффектов. Если подключить буферы в SPI (а мы это обязательно сделаем), ну или высший пилотаж - работать на прерываниях, то формировать узоры можно параллельно с пересылкой байтов в линию Также ещё можно поработать и с внешними интерфейсами. Но это уже совсем другая история.
    ENJOY!
  7. parovoZZ
    Управление периферией и потребляемой мощностью
    В даташите можно отыскать вот такую табличку:

    Разумеется, что держать включенной неиспользуемую периферию незачем. Мы практически постоянно используем модуль USI, периодически ADC и никогда таймеры. В МК есть специальный регистр, который позволяет управлять ТАКТИРОВАНИЕМ перечисленными модулями. Регистр называется PRR. По умолчанию при включении и сбросе вся периферия тактируется. Мы это исправим. Сразу после загрузки выключаем всё
    PRR = 0xFF; // Выключаем всю периферию Необходимо записать "1" чтобы выключить и "0" чтобы включить. Необходимо также помнить, что при отключенном тактировании ни один регистр модуля не доступен!
    Когда нам необходим модуль USI, включим его:
    PRR = (1<<PRTIM0) | (1<<PRTIM1) | (1<<ADC); С ADC посложнее. У ADC в регистре ADCSRA необходимо поднять бит ADEN, чтобы блок ADC включился в работу. И наоборот - для выключения ADC в ADEN необходимо писать "0". Для чего? Даже при отсутствии тактирования, блок ADC потребляет энергию.
    Также мы будем управлять частотой тактирования. Частота меняется с помощью предделителя
    CLKPR = (1<<CLKPCE); // Разрешить изменение прескалера тактовой частоты CLKPR = (1<<CLKPS1) | (1<<CLKPS0); // Устанавливаем тактовую частоту 1 МГц Измерение напряжения питания
    Обязательный пункт в нашей программе как для контроля состояния батареи питания, так и для расчета коэффициента при вычислении температуры. Т.к. ADC ест очень много, то измерять будем как можно реже. Обязательно сразу после загрузки. Примем некую переменную, которую будем декрементировать с каждым измерением температуры и влажности. Если переменная байт, то максимальный период будет составлять 255 измерений от датчика. При 10 минутном интервале измерения параметров напряжение будем снимать каждые 2 суток. Ну вот и хорошо.
    Обычно измерения на АЦП происходят следующим образом: шкала входного напряжения размечается от нуля и до референсного напряжения. Мы же перевернем всё с ног на голову и в качестве референсного напряжения будем использовать напряжение питания, а измерять будем внутренний источник референсного напряжения (в рассматриваемом МК он один и напряжение на нем примерно равно 1.1в). Также я с АЦП буду снимать только старшие 8 бит. При напряжении питания от 3-х вольт и ниже погрешность составит 11 мВ - этого более, чем достаточно. Функция инициализации
    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 мс для успокоения переходных процессов. Что ж, поставим инициализироваться нашему АЦП перед инициализацией трансивера. Производим замер
    void ADC_Start_Conversion (void) { ADCSRA = (1<<ADEN) | (1<<ADIE) | (0<<ADATE) | (0<<ADPS2) | (0<<ADPS1) | (0<<ADPS0) | (1<<ADSC); // Команда на запуск преобразования } Забираем данные
    uint8_t ADC_Get_Data (void) { return ADCH; } Все выключаем
    void ADC_Off (void) { ADCSRA = 0; } Т.к. результат измерения напряжения питания мы получаем тогда, когда уже вовсю идет загрузка в трансивер данных, то все действия будем производить в обработчике прерывания от АЦП. И пусть трансивер подождет!
    ISR(ADC_vect) { data[6] = ADC_Get_Data(); // Получаем результат ADC_Off(); // Запрещаем работу ADC PRR = (1<<PRTIM0) | (1<<PRTIM1) | (1<<ADC); // Тактируем только USI }  
  8. parovoZZ
    Получаем результат от датчика
    Определим макросы команд, чтобы нам было легче писать код
    #define SHT1x_Get_Temp 0b00000011 #define SHT1x_Get_Humidity 0b00000101 #define SHT1x_Write_Status 0b00000110 #define SHT1x_Status 0b00000001 Определим буфер в виде массива
    volatile uint8_t data[8]; Т.к. с одним из элементов буфера мы будем работать в прерывании, то массив у нас с квалификатором volatile.
    Пишем код получения данных:
    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.
     
  9. parovoZZ
    Итак,
    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 будем переключать до вызова данной функции.
    //.. Функция передачи 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.
    //.. Функция приемо-передачи 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:
    //.. Функция инициализации 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 МГц - нет.
  10. 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 осциллятором.
    Схема электрическая принципиальная

    Схемотехнические решения
    У нас присутствует шина 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. А это пахнет лишним энергопотреблением.
  11. 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. Но протокол общения несколько отличается от стандартного.

    Как видно по картинке из даташита, датчик никак не воздействует на линию SCK (SCL). Повторный старт и стоп отсутствуют. Признак старта также свой, особенный. Есть и приятная особенность, которой мы непременно воспользуемся - по окончании измерения датчик притягивает линию DATA к нулю, что позволяет МК усыпить самым крепким сном. У датчика присутствует квазиадрес, который интегрирован в старiие биты команды и всегда равен 0b000. В целом протокол весьма простой и всё должно быть ясно из картинки.
  12. 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 и сушим весла. Такой разряд батареи - это не то, чтобы немного - это даже слишком.
  13. parovoZZ
    На плате Xplained Mini присутствует светодиод на порту PC0 и кнопка на порту PC5. Будем на кнопочку давить и ожидать, что светодиодик погаснет=)
     Давайте писать код. Определим константы:
    uint8_t const maskLED = 0b00000001; uint8_t const maskSwitch = 0b00100000; uint8_t const Switch = 5; Теперь напишем функцию, которая читает кнопку:
    uint8_t GetSwith (PORT_t volatile *switchPort, uint8_t mask) { return (switchPort->IN & mask); } Здесь все просто - читаем регистр IN порта и по маске отсекаем всё лишнее.

    Теперь функция включения светика:
    void SetLEDPort (PORT_t volatile *ledPort, uint8_t value, uint8_t mask) { ledPort->OUT = ledPort->OUT & ~mask | value; } Здесь также по маске стираем значение порта и выставляем новое.

    Ну и главная функция:
    int main(void) { PORT_t volatile *ledPort = &PORTC; PORT_t volatile *switchPort = &PORTC; ledPort->DIR = maskLED; while (1) { uint8_t value = (GetSwith(switchPort, maskSwitch) >> Switch); SetLEDPort(ledPort, value, maskLED); } } Т.к. кнопка и светодиод физически висят на разных битах, то смещением вправо на 5 бит мы значение кнопки ставим на место светодиода.
    И самое важное - для программирования необходим всего один проводок, не считая нулевого. По нему же работает внутрисхемная отладка.

    Размер кода - 106 байт с оптимизацией O3.
    Скоро освоим таймер! А потом и АЦП.
    До новых ВСТРЕЧ!
  14. parovoZZ
    Трансивер nRF24l01+
    Что нужно для энергоэффективно работы с данным трансивером? Прочитать даташит. Если с первого раза не всё понятно, то можно поискать в интернете его перевод. Для уточнения нюансов опять смотрим даташит и только его. Далее. Необходимо скачать заголовочный файлик с адресами регистров. Подобный легко ищется на том же гитхабе.
    Вопросы, которые возникают естественным образом при первом знакомстве с данным транисивером:
    1. Как с ним общаться? У трансивера только один интерфейс - SPI. Максимальная частота тактирования - 10 МГц. Прежде, чем начать общение, необходимо ногу CSN трансивера прижать к земле. Здесь надо четко для себя уяснить один момент - по SPI трансивер общается всегда, когда подано на него питание. Первым же байтом трансивер всегда отдаёт значение регистра статуса.
    2. На какую частоту программировать? Т.к. антенны для этого трансивера используются вай-файные, то и спектр их настройки также вайфайный с не выроженным максимумом примерно посередине (где-то 45 канал). Но т.к. каналы вай-фай в любой точке пространства может быть абсолютно любым, то я для себя принял решение, что буду использовать 83 канал. Этот же канал как раз находится на границе разрешенных для бесплатного (гражданского) использования.
    3. Какие адреса труб задавать? Здесь обращаемся к даташиту. Он нам говорит, что адрес для pipe необходимо задавать так, чтобы трансивер не спутал его с преамбулой (преамбула в данном трансивере в виде меандра), но также не спутал с шумами. По умолчанию, в одном из регистров трансивера записан адрес С2С2С2С2xx. Его я и буду использовать.

    Весь даташит я пересказывать не буду - нет такой задачи. Покажу самые сливки.
    Открываем даташит и видим картинку с режимами работы трансивера:

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

    Итак, пишем код.
    Мы для себя четко уясним наше ТЗ - с трансивером общаемся как можно реже, пакет пересылаем без запроса подтверждения. Для градусника это не актуально. Не приняли пакет сейчас, примем в следующий раз. Температура окружающего воздуха всё равно так быстро не меняется. А если и меняются, то ничто не мешает это заложить в алгоритм и уменьшить интервал общения - все ж в наших руках!
    Сейчас вы удивитесь, как необходимо мало кода для конфигурирования этого трансивера в качестве передатчика:
    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 )! Значения всех остальных регистров на режим передачи не влияют никак и их значения нам фиолетово. А раз так, то и не будем тратить драгоценные такты на их запись!
    Формат сообщения
    Перед каждым, кто хочет собрать себе систему глупой хаты (дачи, усадьбы, гаража и проч.), непременно встает вопрос - как все устройства, входящие в такую систему, будут между собой общаться? В случае каких-то решений-полуфабрикатов вопрос стоит не так остро - разработчик предоставляет шаблон или вовсе готовый формат. В моем случае два решения - все сообщения свести к единому стандарту без оглядки на датчик и исполнительное устройство, либо же у каждого датчика будет своя посылка. В первом случае пакет сообщения будет выглядеть примерно так: {Адрес, команда/параметр, значение параметра}. Для выключателей, диммеров, термостатов, датчиков с одним каналом такое сообщение в самый раз. У нас же несколько каналов, которые содержат также дополнительную информацию. Для беспроводного общения чем короче пакет, тем больше вероятность его доставки. Но в таком случае общий объем передаваемой информации возрастает в связи с необходимостью каждый раз передавать адрес получателя, а также идентификатор передаваемого значения. Я же остановился на втором варианте - передавать всю информацию в одном пакете. Пакет выглядит так:
    Структура пакета: 0 - адрес, старший байт 1 - адрес, младший байт 2 - тип 3 - температура, старший байт 4 - температура, младший байт 5 - влажность 6 - напряжение питания 7 - доп. сообщение (ошибка) |__ 0 бит бит PORF - загрузка по сбросу питания 1 бит бит EXTRF - перезагрузка по ресету 2 бит бит BORF - перезагрузка по детектору питания 3 бит бит WDRF - перезагрузка по вачдогу 4 бит 5 бит ошибка измерения Т 6 бит ошибка измерения RH 7 бит ошибка записи в регистр статуса Итого всего 8 байт. Под адрес отведено 2 байта (ну а вдруг?), под тип датчика всего 1 байт. На приемной стороне необходимо применить тот алгоритм парсинга сообщения, тип которого указан в пакете.
×
×
  • Создать...