Простой цифровой термометр/гигрометр на AM2302 (DHT22), ATtiny13 и MAX7219

Цифровой датчик температуры и влажности AM2302 (DHT22) достаточно популярен в сегменте DIY, так как при невысокой стоимости (если рассматривать реплики, сделанные в Китае) он обеспечивает неплохую точность измерений и весьма прост в подключении (три провода, включая питание). Однако, большинство примеров использования этого датчика рассчитаны на Arduino и написаны на языке программирования С/С++. Это прекрасно подойдет, если вы хотите ознакомиться с функционалом датчика или «по-быстрому» прикрутить термометр к уже существующему устройству. Но если же вы хотите собрать именно термометр/гигрометр и только его, использование целой платы Arduino (или просто большого МК с парой десятков выводов) вполне справедливо может показаться излишним.

В данной статье пойдет речь о простом термометре/гигрометре (далее — просто термометре), выполненном на одном из самых «маленьких» микроконтроллеров — ATtiny13 с весьма скромными характеристиками — 1Кб программной памяти, 64 байтами ОЗУ и 5-ю (6-ю, если отключить вывод сброса) интерфейсными выводами. В статье предполагается, что читатель уже немного знаком с микроконтроллерами AVR и их программированием, но статья, в основном, ориентирована на новичков в этой области. Кстати, о языке программирования — программа термометра полностью написана на ассемблере.

Итак, начнем. Для отображения информации о температуре и влажности был выбран 8-разрядный 7-сегментный светодиодный индикатор, позволяющий отображать оба параметра сразу без необходимости переключения между ними. Такой индикатор имеет 16 выводов (8 сегментов + 8 разрядов), что явно «не под силу» небольшому контроллеру ATtiny13. К счастью, фирма Maxim выпускает микросхему MAX7219, специально предназначенную для таких случаев — внутри микросхемы содержится весь функционал динамической индикации на 8 разрядов плюс последовательный интерфейс, совместимый с SPI. Таким образом, с этой микросхемой весь наш индикатор можно подключить к МК с помощью всего трех проводов (не считая землю и питание). Вот это уже вполне подходит для контроллера с 5-ю интерфейсными выводами. К слову, стоимость одного комплекта из индикатора, микросхемы и печатной платы в сборе составила всего $1.3 на aliexpress.

В качестве датчика температуры и влажности используется, как было сказано выше, AM2302. Он подключается к МК с помощью только одного провода. Таким образом, из имеющихся в наличии 5-ти интерфейсных выводов МК используются только 4, и на оставшийся 5-й можно «повесить» какую-либо дополнительную функцию. Также, если у вас в наличии есть HVSP-программатор, можно отключить вывод сброса и использовать его как 6-й интерфейсный вывод, но это несколько затруднит обновление прошивки МК.

Итак, вся схема термометра представлена на рисунке ниже:
Принципиальная схема

Поскольку все интерфейсы для работы с внешними устройствами МК реализованы программно, то выбор выводов (пинов), к которым подключается тот или иной сигнал — чисто произвольный и сделан, скорее всего, по принципу «куда было удобнее вставить этот проводок на макетной плате». Так что смело можно выбирать и другие выводы, надо будет только в коде поправить их номер. Единственное ограничение — не стоит подключать датчик температуры к одному из выводов, используемых для программирования МК через SPI — это может создать конфликт, т.к. выходы двух устройств окажутся соединенными вместе, что недопустимо с электрической точки зрения.

Теперь, когда с подключением датчика и индикатора все ясно, приступаем к написанию непосредственно кода. И тут нас ожидает новый «вызов» — ATtiny13 не имеет на борту никаких последовательных интерфейсов, т.е. всю их логику придется реализовывать программно. К счастью, реализация SPI для MAX7219 не составляет особого труда, т.к. протокол синхронный, микросхема работает на частоте до 10Мгц, да и интерфейс в нашей схеме работает только на вывод. А вот общение с АМ2302 будет более сложной задачей, потому что он подключается только одним проводом, данные по которому передаются в обе стороны и скорость передачи полностью определяется самим датчиком. Тут следует сказать, что большинство библиотек для работы с АМ2302 идут по «простому пути» — запрещают прерывания и считывают всю информацию с датчика одним вызовом функции. Это простое и надежное решение, но оно вряд ли подойдет, если на МК возложены какие-либо другие функции реального времени (например, динамическая индикация или непрерывный анализ данных из других источников), потому как весь цикл чтения информации о температуре и влажности занимает от 4-х до 6-ти миллисекунд (в зависимости от передаваемых данных). Не смотря на то, что в данном термометре никаких других функций реального времени нет, было принято решение написать универсальный код, который бы считывал информацию с датчика «в фоновом режиме», т.е. на прерываниях.

Для максимального упрощения схемы ATtiny13 тактируется от встроенного RC-генератора, выдающего около 9.6Мгц. Это позволяет, вызывая прерывание каждые 128 тактов процессора, получить частоту опроса АМ2302 75КГц или 13.33 микросекунды между соседними опросами. По спецификации АМ2302 минимальная длительность импульса на его выходе составляет 26 микросекунд, что практически в два раза превышает интервал опроса и гарантирует стабильное чтение данных. Конечно, 128 тактов между двумя прерываниями не очень-то много для реализации алгоритма опроса, но AVR выполняет большинство команд за 1 такт, поэтому написать работающую программу при таких условиях вполне возможно, еще и останется время для выполнения основной программы.

АМ2302 по спецификации можно опрашивать не чаще, чем один раз в две секунды. Однако практика показывает, что он вполне способен отдавать результат и чаще — до нескольких раз в секунду, при условии, что после включения питания ему дадут 1–2 секунды (по спецификации — 2) на инициализацию. В данном термометре датчик опрашивается один раз в секунду, однако интервал опроса легко изменить на любое другое значение.

К сожалению, АМ2302 (возможно, тут сказывается его китайское происхождение) имеет достаточно большую погрешность результата — два последовательных запроса температуры могут вернуть разницу в 0.5 или даже более градусов, поэтому было решено программно усреднять данные последних 8-ми измерений, чтобы показания термометра не прыгали.

Теперь перейдем непосредственно к коду. Исходный asm и результирующий hex-файл размещен в приложении в конце статьи, здесь же я поясню основные моменты. Будет удобно открыть исходный код программы в другом окне и смотреть туда в процессе чтения статьи.

В начале программы идет два важных определения:

#define SKIPNEXT1W (PC + 2)
#define DS(var) Y + var - _dataStart


Первое позволяет осуществлять условный переход через следующую команду размером 16 бит (1 слово, большинство команд AVR), т.е. пропускать ее без введения дополнительной метки, например:

 inc     R16
        cpi     R16, 5
        brne    SKIPNEXT1W
        dec     R16
        ...


Второе позволяет обращаться к первым 64-м байтам оперативной памяти МК с помощью 16-битных команд. Здесь расскажу подробнее — обычно для чтения или записи в ОЗУ МК применяются команды lds/sts, которые занимают 2 слова (32 бита) и выполняются за 2 такта. Они позволяют адресовать до 64Кб (без расширений) ОЗУ. К сожалению, размер в 32 бита (4 байта) — это уже весьма много для МК с объемом программной памяти всего 1Кб. Поэтому, для экономии программной памяти в регистр Y МК при старте помещается адрес начала ОЗУ (0×60 для ATtiny13), больше в процессе работы программы этот регистр никто не меняет, а доступ к первым 64 байтам ОЗУ выполняется с помощью косвенной адресации со смещением по регистру Y, например:

 ldd     R16, Y + 6


Команды ldd/std также выполняются за 2 такта, но занимают только 16 бит (2 байта), т.е. по сравнению с командами lds/sts такой вид адресации позволяет экономить половину объема программной памяти. Для того, чтобы не высчитывать в каждой команде смещение какой-либо переменной вручную, в самом начале сегмента данных ставится метка _dataStart:

.dseg
_dataStart:
...
testVar:                .byte   1


А в команде используется макрос DS (сокращение от Data Segment):

 ldd     R16, DS (testVar)


Компилятор преобразует это в строку:

 ldd     R16, Y + testVar - _dataStart


Автоматически высчитывая нужное смещение. Следует отметить, что такой вид адресации ограничен возможностями самой команды ldd, а это первые 64 байта относительно базового регистра. Но, в случае с ATtiny13, которая имеет как раз 64 байта ОЗУ на борту, он позволяет адресовать всю память. Тем не менее, в других МК, имеющих больший объем ОЗУ, также возможно применять данный способ, размещая наиболее часто адресуемые переменные в первых 64-х байтах сегмента данных. Расплата за такой способ адресации — регистр Y (два 8-битных регистра R28 и R29), значение которого нельзя менять ни в какой точке программы.

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

Особенностью МК AVR является то, что первые 16 регистров R0 — R15 являются «неполноценными», с ними не работают команды, содержащие внутри себя операнд — например, ldi или subi. Т.е. чтобы даже загрузить значение, отличное от 0 в один из этих регистров, надо использовать дополнительный регистр:

 ldi     R16, 32
        mov     R0, R16


Поэтому, часто такие регистры используются как «переменные с быстрым доступом». Для этого компилятор имеет директиву .def, позволяющую присвоить регистру дополнительное символьное имя, например:

.def     R_TS = R0


В программе термометра регистр R0 всегда хранит состояние приемника данных АМ2302, регистр R1 используется для подсчета времени приема сигнала, R2 содержит принимаемые данные, R3 используется как счетчик таймера, увеличивающегося с частотой 100Гц, а R4 и R5 — как обратный счетчик таймера 75КГц, считающего от 749 до 0.

Сегмент данных МК поделен на 4 части — блок принятых с АМ2302 данных (5 байт), буфер для десятичной печати числа (4 байта), буфер для усреднения показаний термометра и гигрометра на 8 значений (8×2*2 = 32 байта) и стек МК (ему отделена вся оставшаяся память, т.е. 23 байта). В действительности, конечно, стек занимает меньше, и в памяти можно еще найти несколько байт для дополнительных функций, но увлекаться уже не стоит.

Теперь перейдем непосредственно к сегменту кода. Он традиционно начинается с таблицы прерываний, для ATtiny13 это 10 векторов, включая вектор сброса. Неиспользуемые прерывания сразу же содержат команду reti, используемые (а их два) — команду перехода на обработчик. Термометр использует два прерывания, обслуживаемые одним обработчиком — это прерывание по переполнению таймера и прерывание по равенству таймера значению OCRA. Можно было бы обойтись одним, однако такой метод на 2 команды короче (не надо изменять режим работы таймера с обычного на СТС).

Сразу после векторов прерываний идет таблица перевода цифр в коды для зажигания 7-сегментных индикаторов. Можно было бы воспользоваться встроенной в MAX7219 функцией декодирования, однако тогда было бы сложнее выводить на индикатор строковые сообщения.
За таблицей начинается программа инициализации термометра, выполняемая сразу после сброса МК. Она выполняет начальную установку указателя стека МК, сторожевого таймера watchdog (устанавливается на 4 секунды), занесение начальных значений в регистры МК, а также инициализацию портов ввода-вывода, MAX7219 и основного таймера МК. После этого программа ждет 2 секунды, пока инициализируется АМ2302 (демонстрируя простую анимацию из гаснущих знаков «минус» на дисплее) и переходит в свой основной цикл.

Основной цикл начинается с инициации запроса к АМ2302 посредством изменения состояния приемника данных в регистре R_TS (R0). Ближайшее прерывание таймера определит изменение состояния и начнет цикл опроса датчика. По его завершению в биты состояния регистра R_TS будет помещено значение TMS_NONE, а до этого момента основная программа может выполнять любые действия. В данном случае выполнять нечего, поэтому программа просто переводит МК в режим сна (sleep) и ждет окончания цикла опроса.

После завершения опроса бит 3 регистра состояния определяет, были ли данные получены успешно (значение 1) или же произошла ошибка (значение 0). В случае успешного получения данных программа проверяет их контрольную сумму и, по необходимости, передает управление обработчику ошибки. Обработчик ошибки считает количество ошибок, идущих подряд, и как только это значение станет равным трем, выводит на дисплей сообщение «Sn Error», сигнализирующее о неисправности сенсора или соединительной линии. Как только данные о температуре и влажности будут получены успешно, счетчик ошибок сбрасывается. Такой механизм позволяет игнорировать одиночные ошибки сенсора, которые время от времени имеют место в реальной жизни.

В случае успешного получения данных, предыдущие измерения, находящиеся в буфере усреднения данных, сдвигаются вверх, и новые данные добавляются в его начало. Параллельно вычисляются средние значения, которые будут показаны на дисплее. Тут следует отметить, что АМ2302 выдает отрицательную температуру не в дополнительном коде, привычном для обработки процессорами, а в виде абсолютного значения температуры и отдельного бита её знака. Для того чтобы складывать такие числа и вычислять их средние значения, используя обычные команды МК, данные надо перевести в дополнительный код.

Поскольку изначально буфер усреднения не инициализируется, средние значения температуры и влажности отображаются только после проведения восьми успешных измерений. До этого момента на дисплей выводятся текущие значения. На практике это означает, что в первые 8 секунд после включения термометра значения температуры и влажности могут прыгать в пределах градуса, после чего показания стабилизируются. Следует сказать, что усреднение из 8-ми последних значений очень благотворно влияет на показания термометра — теперь они в основном изменяются не более чем на 0.1 градуса в секунду.

Температура выводится на дисплей в формате » х.х»,» хх.х», «ххх.х»,»- х.х» или »-хх.х» в зависимости от ее значения. Влажность выводится в формате » х.х» или » хх.х». Для преобразования двоичного числа, находящегося в регистре Х в десятичную форму (в соответствии с кодами для 7-сегментного индикатора), применяется функция printDecX. Поскольку МК не имеет команды деления, функция основана на последовательном вычитании из исходного числа значений 1000, 100 и 10. Максимальное число, которое может вывести функция — 9999, если при её вызове в регистре Х окажется число больше, функция вернет ошибку переполнения, установив флаг переноса.

Для работы с MAX7219 применяется функция maxWriteWord, которая записывает значение из регистра XL МК в регистр MAX, номер которого задан в регистре XH. После вывода значений текущей температуры и влажности на дисплей, программа делает задержку в 1 секунду и повторяет основной цикл заново. Для реализации задержки используется функция wait100Hz, которая выполняет задержку на время R16×0.01c с использованием счетчика R_TICK100, увеличение которого происходит по прерыванию таймера.

Получение данных с датчика температуры выполняется с помощью функции am2302proc, которая вызывается из обработчика прерывания таймера. Функция представляет собой конечный автомат, состояние которого хранится в регистре R_TS (R0) МК. В зависимости от состояния функция ждет определенного уровня сигнала от датчика, инициируя передачу и последовательно получая все 40 бит передаваемой информации. Синхронизация происходит на каждом изменении уровня входного сигнала, поэтому особой точности от частоты прерываний таймера не требуется (что позволяет МК работать от встроенного генератора). Функция состоит из быстрого обработчика состояния простоя (TMS_NONE), позволяющего минимизировать нагрузку на процессор МК в то время, когда обмена данными с датчиком не происходит, обработчика таймаута, предназначенного для сброса автомата в исходное состояние, если ожидаемый сигнал не приходит длительное время (около 3 мс), и обработчиков каждого отдельного состояния автомата. Следует отметить, что данная функция не обладает помехозащищенностью — если даже импульсная помеха изменит уровень линии данных на короткий промежуток времени, но именно он попадет на операцию чтения из порта, функция прочитает неверные данные. Для компенсации этого в основной программе происходит проверка контрольной суммы прочитанных данных, поэтому отображение неверной информации практически исключено. Однако такая реализация может оказаться не самой лучшей, если вы захотите вынести датчик за пределы термометра и подключить его к МК соединительной линией большой длины.

На данный момент термометр собран на макетной плате и выглядит следующим образом:

Внешний вид термометра

В будущем планируется поместить термометр внутрь корпуса существующих электронных часов, организовав его питание от БП часов.

Текущая программа занимает около 75% программной памяти МК. Что можно добавить в программу? Возможно, кому-то пригодится изменение яркости свечения дисплея (это реализовано непосредственно в драйвере MAX7219) по внешней кнопке или датчику освещенности (используя встроенный в МК АЦП и свободный интерфейсный вывод), кому-то может пригодиться запоминание и отображение минимальной и максимальной температуры. Для небольших доработок место еще есть. Более крупные доработки могут потребовать смену МК на другой, имеющий на борту больше программной и оперативной памяти. Что касается интерфейсных выводов — на данный момент у МК есть один полностью незадействованный вывод и еще один можно получить, отключив RESET. Также два вывода из интерфейса SPI (DATA и CLK) можно использовать для других функций, т.к. пока на выводе CS не будет низкого уровня (конкретно для МАХ7219 важен переход с низкого уровня на высокий) сигналы на этих выводах значения не имеют. Т.е., в принципе, заменив МК на более мощный, например, ATtiny85, можно подключить к термометру Real Time Clock (RTC) и до четырех кнопок.

Моей же целью было именно создание простого термометра/гигрометра, поэтому, скорее всего, я оставлю его себе в таком виде.

Текст программы

// *********************************************
// *** Simple digital thermometer/hygrometer ***
// *********************************************
// ***         (c) SD, 14.03.2016            ***
// *********************************************

// Based on ATtiny13, AM2303 and MAX7219

// **************
// *** Clocks ***
// **************

// MCU clock frequency is 9.6MHz (internal oscillator)
// Timer frequency is 75KHz = 9.6MHz/128
// (13.3 us between interrupts)

#define SKIPNEXT1W (PC + 2)
#define DS(var) Y + var - _dataStart

// ************
// *** Pins ***
// ************

// MAX7219 output pins
.equ    MAX_DIN = 0
.equ    MAX_CS = 1
.equ    MAX_CLK = 4

// AM2302 input pin
.equ    AM2302_PIN = 3

// MAX7219 registers
.equ    MAX_DECODE = 0x09
.equ    MAX_INTENSITY = 0x0A
.equ    MAX_SCANLIMIT = 0x0B
.equ    MAX_SHUTDOWN = 0x0C
.equ    MAX_DISPTEST = 0x0F

// Temperature measurement state register
// Bits 0 - 2 define the byte number being received
// Bit 3 is set when there are valid data received
// Bits 4 - 7 define the current receiver state
.def    R_TS = R0

// Temperature measurement tick
.def    R_TT = R1

// Temperature data register
.def    R_TD = R2

// Temperature measurement states
.equ    TMS_NONE =                      0x00    // TMS_NONE - do nothing an wait until
                                                                        // somebody changes the state
.equ    TMS_START =                     0x10    // Start of the measurement cycle
.equ    TMS_ST_LOW =            0x20    // Initial low signal is being sent
                                                                        // (1 ms = 75 timer ticks)
.equ    TMS_WRSP_LOW =          0x30    // Initial low signal has been sent,
                                                                        // waiting for the response low signal
.equ    TMS_WRSP_HIGH =         0x40    // Response low signal has been received,
                                                                        // waiting for the response high signal
.equ    TMS_W1ST_BIT_LOW =      0x50    // Waiting for the first bit low signal
.equ    TMS_WBIT_HIGH =         0x60    // Waiting for the bit high signal
.equ    TMS_WBIT_LOW =          0x70    // Waiting for the bit low signal
.equ    TMS_WHIGH =                     0x80    // Waiting for the final high signal

// Timer 100Hz tick counter
// (counts upwards from 0 to 255)
.def    R_TICK100 = R3

// Timer 16bit 75KHz tick counter
// (counts downwords from 749 to 0)
.def    R_TICKL = R4
.def    R_TICKH = R5

// ************
// *** Data ***
// ************

.dseg
_dataStart:                                                     // Data start label

tempData:                       .byte   5               // Data, received from the AM2302 sensor
displayData:            .byte   4               // Decimal printing result

.equ    DATA_BUF_SIZE =         8               // AM2302 data buffer size in samples
                                                                        // (each sample is 4 bytes)

dataBuffer:                     .byte   DATA_BUF_SIZE*4

.cseg
.org    0

        // *** Interrupts ***

        // Reset Handler
        rjmp    start

        // IRQ0 Handler
        reti
        
        // PCINT0 Handler
        reti

        // Timer0 Overflow Handler
        rjmp    timerOvfl

        // EEPROM Ready Handler
        reti
        
        // Analog Comparator Handler
        reti

        // Timer0 CompareA Handler
        rjmp    timerCompA

        // Timer0 CompareB Handler
        reti

        // Watchdog Interrupt Handler
        reti

        // ADC Conversion Handler
        reti

// Table to convert decimal digit into 7-segment code
hexTable:
        .db             0b01111110, 0b00110000, 0b01101101, 0b01111001
        .db             0b00110011, 0b01011011, 0b01011111, 0b01110010
        .db             0b01111111, 0b01111011

start:
        cli
        ldi             R16, RAMEND
        out             (SPL), R16

        // Init watchdog (4s interval)
        wdr
        ldi             R16, (1 << WDCE) | (1 << WDE)
        out             (WDTCR), R16
        ldi             R16, (1 << WDE) | (1 << WDP3)
        out             (WDTCR), R16

        // Init registers
        ldi             YL, low (_dataStart)
        ldi             YH, high (_dataStart)
        clr             R_TS
        clr             R_TT
        clr             R_TICKL
        clr             R_TICKH
        clr             R_TICK100

        // Init ports
        out             (PORTB), R_TS
        ldi             R16, (1 << MAX_DIN) | (1 << MAX_CS) | (1 << MAX_CLK)
        out             (DDRB), R16

        // Init LED driver
        // Set all digits to "-"
        ldi             XL, 0b00000001
        ldi             XH, 1
init1:
        rcall   maxWriteWord
        cpi             XH, 9
        brne    init1

        // Set control registers
        ldi             XL, 0                                   // Decode
        rcall   maxWriteWord
        ldi             XL, 4                                   // Intensity
        rcall   maxWriteWord
        ldi             XL, 7                                   // Scan limit
        rcall   maxWriteWord
        ldi             XL, 1                                   // Shutdown
        rcall   maxWriteWord
        ldi             XH, 0x0F
        ldi             XL, 0                                   // Display test
        rcall   maxWriteWord

        // Init timer for 1 interrupt each 128 CPU cycles
        ldi             R16, 127
        out             (OCR0A), R16
        ldi             R16, 0b00000110
        out             (TIMSK0), R16
        ldi             R16, 0b00000001
        out             (TCCR0B), R16

        // First part of the initialization is done.
        // Enable interrupts
        sei

        // Wait 2 sec (while AM2302 initialize itself)
        // with little animation
        ldi             XH, 1
        ldi             XL, 0
init2:
        ldi             R16, 25
        rcall   wait100Hz
        rcall   maxWriteWord
        cpi             XH, 9
        brne    init2

        // R6 will contain the number of
        // measurement values received
        clr             R6

        // R7 will contain the number of
        // continious errors
        clr             R7

loop:
        // Reset watchdog timer
        wdr

        // Initiate measurement
        ldi             R16, TMS_START
        mov             R_TS, R16

loop1:
        // Wait for the TMS_NONE state
        // which indicates that the measurement
        // is done
        sleep

        mov             R16, R_TS
        andi    R16, 0xF0
        brne    loop1

        // Do we have the valid data?
        sbrs    R_TS, 3
loop_error1:
        rjmp    loop_error

        // Check control sum of the received data
        ldd             R16, DS (tempData)
        ldd             ZL, DS (tempData + 1)
        add             R16, ZL
        ldd             ZL, DS (tempData + 2)
        add             R16, ZL
        ldd             ZL, DS (tempData + 3)
        add             R16, ZL
        ldd             ZL, DS (tempData + 4)
        cp              R16, ZL
        brne    loop_error1

        // We have valid new measurement data,
        // reset error count
        clr             R7

        // Move up data in the buffer
        // and count the sum at the same time.
        // R12:R13 will contain the humidity value and
        // R14:R15 the temperature value
        clr             R12
        clr             R13
        clr             R14
        clr             R15
        ldi             ZL, low (dataBuffer + (DATA_BUF_SIZE - 2)*4)
        ldi             ZH, 0
buf1:
        ldd             R16, Z + 0
        ldd             R17, Z + 1
        std             Z + 4, R16
        std             Z + 5, R17
        add             R12, R16
        adc             R13, R17

        ldd             R16, Z + 2
        ldd             R17, Z + 3
        std             Z + 6, R16
        std             Z + 7, R17
        add             R14, R16
        adc             R15, R17

        subi    ZL, 4
        cpi             ZL, low (dataBuffer - 4)
        brne    buf1

        // Add new humidity value to the buffer
        // and to the sum
        ldd             R16, DS (tempData + 1)
        ldd             R17, DS (tempData)
        std             DS (dataBuffer + 0), R16
        std             DS (dataBuffer + 1), R17
        add             R12, R16
        adc             R13, R17

        // Add new temperature value to the buffer
        // and to the sum
        ldd             R16, DS (tempData + 3)
        ldd             R17, DS (tempData + 2)
        
        // Check for a negative value
        and             R17, R17
        brpl    buf2

        // Convert negative temperature to the 2's
        // complement form
        clr             ZL
        andi    R17, 0x7F
        neg             R16
        sbc             ZL, R17
        mov             R17, ZL

buf2:
        std             DS (dataBuffer + 2), R16
        std             DS (dataBuffer + 3), R17
        add             R14, R16
        adc             R15, R17

        // Divide the humidity and temperature
        // sum values by 8 (by shifting them right
        // three times)
        ldi             R16, 3
buf3:
        asr             R15
        ror             R14
        asr             R13
        ror             R12
        dec             R16
        brne    buf3

        // Do we have 8 full measurements?
        mov             R16, R6
        cpi             R16, 7
        
        // If so, use the average values from
        // the buffer
        breq    buf4

        // Otherwise use the latest measurement
        ldd             R12, DS (dataBuffer + 0)
        ldd             R13, DS (dataBuffer + 1)
        ldd             R14, DS (dataBuffer + 2)
        ldd             R15, DS (dataBuffer + 3)
        inc             R6

buf4:
        // Print out values

        // *** Humidity ***
        movw    X, R12
        rcall   printDecX

        ldi             XH, 1
        ldd             XL, DS (displayData + 3)
        rcall   maxWriteWord

        ldd             XL, DS (displayData + 2)
        ori             XL, 0x80
        rcall   maxWriteWord

        ldd             XL, DS (displayData + 1)
        rcall   maxWriteWord

        ldd             XL, DS (displayData)
        rcall   maxWriteWord

        // *** Temperature ***
        movw    X, R14

        // Check for a negative value
        and             XH, XH
        brpl    buf5

        // Calculate the absolute value
        clr             ZL
        neg             XL
        sbc             ZL, XH
        mov             XH, ZL

buf5:
        rcall   printDecX

        ldi             XH, 5
        ldd             XL, DS (displayData + 3)
        rcall   maxWriteWord

        ldd             XL, DS (displayData + 2)
        ori             XL, 0x80
        rcall   maxWriteWord

        ldd             XL, DS (displayData + 1)
        rcall   maxWriteWord

        // If temperature is negative
        // write the minus sign to the first digit
        // (temperatures of -100.0 and below
        // are not supported anyway)
        ldd             XL, DS (displayData)
        and             R15, R15
        brpl    SKIPNEXT1W
        ldi             XL, 1
        rcall   maxWriteWord

loop2:
        // Wait for 1 sec
        ldi             R16, 100
        rcall   wait100Hz

        // And repeat
        rjmp    loop

loop_error:
        // An error had occured.
        // Increment error count
        inc             R7

        // Do we have 3 or more errors in a row?
        mov             R16, R7
        cpi             R16, 3

        // No? Just do nothing
        brne    loop2

        // Prevent error count from growing
        dec             R7

        // Display error
        ldi             ZL, low (errText*2)
        ldi             ZH, high (errText*2)
        rcall   maxWrite8Bytes
        rjmp    loop2

errText:
        // "Sn Error"
        .db             0b00000101, 0b00011101, 0b00000101, 0b00000101
        .db             0b01001111, 0b00000000, 0b00010101, 0b01011011

// **********
// Waits given number (R16) of 100Hz ticks
// Uses: Z
wait100Hz:
        // Enable sleep
        ldi             ZL, 0b00100000
        out             (MCUCR), ZL
        
        mov             ZL, R_TICK100
w100:
        sleep
        mov             ZH, R_TICK100
        sub             ZH, ZL
        cp              ZH, R16
        brcs    w100
        ret

// Timer interrupt

timerOvfl:
timerCompA:
        push    R16
        in              R16, (SREG)
        push    R16
        push    ZL
        push    ZH

        // Receive AM2303 data
        rcall   am2302proc

        // Decrement current 75KHz tick
        ldi             R16, 1
        sub             R_TICKL, R16
        brcc    timerRet
        sub             R_TICKH, R16
        brcc    timerRet

        // Initialize 75KHz tick value
        ldi             ZL, low (750 - 1)
        ldi             ZH, high (750 - 1)
        movw    R_TICKL, Z

        // Increment current 100Hz tick
        inc             R_TICK100

timerRet:
        pop             ZH
        pop             ZL
        pop             R16
        out             (SREG), R16
        pop             R16
        reti

// **************
// *** AM2302 ***
// **************

amStart:
        // Send the start low signal.
        // Switch corresponding PORTB pin to output
        // (there is already 0 in the PORTB register)
        sbi             (DDRB), AM2302_PIN
        ldi             R16, TMS_ST_LOW
        rjmp    amSetState

amStartLow:
        // Initial start low signal is being sent.
        // Wait for 75 ticks
        cpi             R16, 75
        brne    amNone

        // Switch PORTB pin back to input
        cbi             (DDRB), AM2302_PIN
        ldi             R16, TMS_WRSP_LOW

        // Do not check AM2303 input pin at this tick
        // since it's possible that it has not recovered
        // from the low state yet.
        rjmp    amSetState

amWRespLow:
        // Waiting for the response low signal
        sbrc    ZH, AM2302_PIN
        ret

        ldi             R16, TMS_WRSP_HIGH
        rjmp    amSetState

amWRespHigh:
        // Waiting for the response high signal
        sbrs    ZH, AM2302_PIN
        ret

        ldi             R16, TMS_W1ST_BIT_LOW
        rjmp    amSetState

amW1StBitLow:
        // Waiting for the first bit low signal
        sbrc    ZH, AM2302_PIN
        ret

        // Get ready to receive the first bit
        ldi             R16, 1
        mov             R_TD, R16

        // Set new state and reset the byte counter
        ldi             ZL, TMS_WBIT_HIGH
        rjmp    amSetState2

amBitHigh:
        sbrs    ZH, AM2302_PIN
        ret

        // If the bit low signal was there too long
        // (longer than 5 ticks (5*13.3 = 66.5us)
        // something went wrong)
        cpi             R16, 6
        brcc    amResetState

        ldi             R16, TMS_WBIT_LOW
        rjmp    amSetState

am2302proc:
        // First, check for the TMS_NONE state.
        // In this case just do nothing to
        // not waste MCU cycles.
        mov             ZL, R_TS
        andi    ZL, 0xF0

        cpi             ZL, TMS_NONE
        breq    amNone

        // Increment receiver tick
        inc             R_TT

        // If we are waiting for too long,
        // something went wrong, reset the state
        breq    amResetState

        // Save the current tick into a more
        // convenient register
        mov             R16, R_TT

        // Get input signal
        in              ZH, (PINB)

        // Branch depending on the current state.
        // Check for TMS_WBIT_LOW first since it
        // has the longest service routine
        cpi             ZL, TMS_WBIT_LOW
        breq    amBitLow

        cpi             ZL, TMS_START
        breq    amStart

        cpi             ZL, TMS_ST_LOW
        breq    amStartLow

        cpi             ZL, TMS_WRSP_LOW
        breq    amWRespLow

        cpi             ZL, TMS_WRSP_HIGH
        breq    amWRespHigh

        cpi             ZL, TMS_W1ST_BIT_LOW
        breq    amW1StBitLow

        cpi             ZL, TMS_WBIT_HIGH
        breq    amBitHigh

        cpi             ZL, TMS_WHIGH
        breq    amWHigh

amResetState:
        // In case of an error, reset state to
        // the default TMS_NONE
        ldi             R16, TMS_NONE

amSetState:
        // Preserve the current byte number
        mov             ZL, R_TS
        andi    ZL, 0x07
        or              ZL, R16

amSetState2:
        mov             R_TS, ZL
        
        // Clear receiver tick counter
        clr             R_TT

amNone:
        ret     

amBitLow:
        sbrc    ZH, AM2302_PIN
        ret

        // The high bit signal was too long?
        cpi             R16, 8
        brcc    amResetState

        // Store input bit (inverted, since cpi produces
        // inverted result in the carry flag)
        cpi             R16, 4
        rol             R_TD

        // Initally we set R_TD to 1, so when all 8
        // bits are received, the carry flag will be set
        // indicating that a full byte has been received.
        // Otherwise, receive the next bit
        ldi             R16, TMS_WBIT_HIGH
        brcc    amSetState

        // We have the full byte. Invert it
        com             R_TD

        // Save it
        mov             ZL, R_TS
        andi    ZL, 0x07
        subi    ZL, low (-tempData)
        ldi             ZH, high (tempData)
        st              Z+, R_TD

        // Did we receive all 5 bytes?
        cpi             ZL, low (tempData + 5)
        ldi             R16, TMS_WHIGH
        breq    amSetState

        // OK, receive the next byte.
        // Increment the byte counter
        inc             R_TS

        // Initialize R_TD
        ldi             R16, 1
        mov             R_TD, R16

        ldi             R16, TMS_WBIT_HIGH
        rjmp    amSetState

amWHigh:
        sbrs    ZH, AM2302_PIN
        ret

        cpi             R16, 6
        brcc    amResetState

        // We received everything. Set
        // the state to TMS_NONE and set
        // the data validity bit
        ldi             R16, 0x08
        mov             R_TS, R16
        ret

// *********

/*
// Write data from Z
// Uses R16 - R19, X, Z
maxWriteData:
        lpm             XH, Z+
        tst             XH
        brne    SKIPNEXT1W
        ret
        lpm             XL, Z+
        rcall   maxWriteWord
        rjmp    maxWriteData

maxInit:
        .db             MAX_DECODE, 0
        .db             MAX_INTENSITY, 4
        .db             MAX_SCANLIMIT, 7
        .db             MAX_SHUTDOWN, 1
        .db             MAX_DISPTEST, 0
        .db             0, 0

maxTest:
        .db             0, 0b00011101, 0b00010101, 0b00010000, 0b00011100, 0b00111101, 0b00000101, 0b01110111
*/

// Writes 8 bytes from (Z) (program memory)
// to MAX7219
// Uses R16 - R19, X, Z
maxWrite8Bytes:
        ldi             XH, 0x01

mw8b1:
        lpm             XL, Z+
        rcall   maxWriteWord
        cpi             XH, 9
        brne    mw8b1
        ret

// Write word X (XL = data, XH = address) to MAX2719
// Uses R16 - R19, X
maxWriteWord:
        // Set all pins to zero
        in              R17, (PORTB)
        andi    R17, ~((1 << MAX_DIN) | (1 << MAX_CS) | (1 << MAX_CLK))
        out             (PORTB), R17

        ldi             R19, (1 << MAX_CLK)

        mov             R16, XH
        rcall   mww1

        mov             R16, XL
        rcall   mww1

        // Set LOAD(CS) to high thus writing all 16 bits into
        // MAX register
        sbi             (PORTB), MAX_CS
        
        // Increment MAX register number
        inc             XH
        ret

mww1:
        ldi             R18, 8

mww2:
        bst             R16, 7
        bld             R17, MAX_DIN
        out             (PORTB), R17

        lsl             R16
        dec             R18

        // Create clock impulse by toggling clock output twice
        out             (PINB), R19
        out             (PINB), R19

        brne    mww2
        ret

// *********

printDecX:
        ldi             ZH, low (1000)
        ldi             R16, high (1000)
        rcall   pdx

        // Change zero digit to empty space
        cpi             ZL, 0b01111110
        brne    SKIPNEXT1W
        ldi             ZL, 0
        std             DS (displayData), ZL

        ldi             ZH, 100
        ldi             R16, 0
        rcall   pdx

        // If this digit is zero and the first
        // digit is empty (i.e. it was zero too)
        // change this digit to empty space
        ldi             R16, 0b01111110
        eor             R16, ZL
        ldd             ZH, DS (displayData)
        or              R16, ZH
        brne    SKIPNEXT1W
        ldi             ZL, 0
        std             DS (displayData + 1), ZL

        ldi             ZH, 10
        ldi             R16, 0
        rcall   pdx
        std             DS (displayData + 2), ZL

        mov             ZL, XL
        rcall   pdx3
        std             DS (displayData + 3), ZL
        
        // Clear carry flag to indicate that
        // no error occurred
        clc
        ret

pdx:
        ldi             ZL, 0
pdx1:
        sub             XL, ZH
        sbc             XH, R16
        brcs    pdx2

        cpi             ZL, 9
        breq    pdxOverflow
        inc             ZL
        rjmp    pdx1

pdx2:
        add             XL, ZH
        adc             XH, R16

pdx3:
        subi    ZL, -low (hexTable << 1)
        ldi             ZH, high (hexTable << 1)
        lpm             ZL, Z
        ret

pdxOverflow:
        // Set carry flag to indicate error
        sec

        // Pop return address out of the stack
        // so we can return to the caller of printDecX
        pop             R16
        pop             R16
        ret



HEX-файл (fuses: H: FF, L:7A)

:020000020000FC
:100000000EC018951895C2C018951895BFC01895C0
:10001000189518957E306D79335B5F727F7BF8940D
:100020000FE90DBFA89508E101BD08E201BDC0E6DA
:10003000D0E00024112444245524332408BA03E1D9
:1000400007BBA1E0B1E015D1B930E9F7A0E011D1CB
:10005000A4E00FD1A7E00DD1A1E00BD1BFE0A0E05B
:1000600008D10FE706BF06E009BF01E003BF78949F
:10007000B1E0A0E009E181D0FCD0B930D9F7662425
:100080007724A89500E1002E8895002D007FE1F7E8
:1000900003FE66C00881E9810E0FEA810E0FEB8135
:1000A0000E0FEC810E17A9F77724CC24DD24EE2463
:1000B000FF24E1E8F0E00081118104831583C00E84
:1000C000D11E0281138106831783E00EF11EE450D6
:1000D000E53689F70981188109871A87C00ED11E74
:1000E0000B811A8111232AF4EE271F770195E10B6A
:1000F0001E2F0B871C87E00EF11E03E0F594E7949A
:10010000D594C7940A95D1F7062D073029F0C984F4
:10011000DA84EB84FC846394D601C0D0B1E0A88576
:10012000A8D0AF81A068A5D0AE81A3D0AD81A1D069
:10013000D701BB2322F4EE27A195EB0BBE2FAED047
:10014000B5E0A88596D0AF81A06893D0AE8191D05C
:10015000AD81FF200AF4A1E08CD004E60ED091CF4F
:100160007394072D0330C9F77A94E2E7F1E07BD06E
:10017000F4CF051D05054F00155BE0E2E5BFE32D5B
:100180008895F32DFE1BF017D8F308950F930FB742
:100190000F93EF93FF932BD001E0401A30F4501AE5
:1001A00020F4EDEEF2E02F013394FF91EF910F91E7
:1001B0000FBF0F911895BB9A00E232C00B34A9F51E
:1001C000BB9800E32DC0F3FD089500E429C0F3FFC0
:1001D000089500E525C0F3FD089501E0202EE0E636
:1001E00022C0F3FF08950630D0F400E719C0E02DD7
:1001F000E07FE030D1F0139491F0012DF6B3E037B9
:10020000A9F0E031C1F2E032C9F2E033E1F2E034CA
:10021000F1F2E03501F3E03621F3E038E9F000E0F7
:10022000E02DE770E02B0E2E11240895F3FD0895C4
:100230000830A8F70430221C00E690F72094E02D47
:10024000E770E05AF0E02192E53600E849F30394C4
:1002500001E0202E00E6E4CFF3FF08950630F8F623
:1002600008E0002E0895B1E0A59103D0B930E1F780
:10027000089518B31C7E18BB30E10B2F05D00A2F50
:1002800003D0C19AB395089528E007FB10F918BB75
:10029000000F2A9536BB36BBC1F70895F8EE03E090
:1002A00017D0EE3709F4E0E0ED83F4E600E010D07B
:1002B0000EE70E27FD810F2B09F4E0E0EE83FAE054
:1002C00000E006D0EF83EA2F0DD0E88788940895E8
:1002D000E0E0AF1BB00B20F0E93041F0E395F9CF3F
:1002E000AF0FB01FEC5EF0E0E491089508940F9119
:0402F0000F910895CD
:00000001FF

© Geektimes