Simple digital thermometer / hygrometer on the AM2302 (DHT22), ATtiny13 and MAX7219

    The digital temperature and humidity sensor AM2302 (DHT22) is quite popular in the DIY segment, because at a low cost (if we consider replicas made in China), it provides good measurement accuracy and is very easy to connect (three wires, including power). However, most examples of the use of this sensor are designed for Arduino and written in the C / C ++ programming language. This is perfect if you want to get acquainted with the sensor’s functionality or fasten the thermometer to an existing device. But if you want to assemble a thermometer / hygrometer and only it, using an entire Arduino board (or just a large MK with a couple of dozen conclusions) may rightly seem redundant.

    This article will discuss a simple thermometer / hygrometer (hereinafter referred to simply as a thermometer), made on one of the “smallest” microcontrollers - ATtiny13 with very modest characteristics - 1Kb of program memory, 64 bytes of RAM and 5th (6th if disable reset pin) with interface pins. The article assumes that the reader is already a little familiar with the AVR microcontrollers and their programming, but the article is mainly aimed at beginners in this field. By the way, about the programming language - the thermometer program is completely written in assembly language.

    So, let's begin. To display information on temperature and humidity, an 8-bit 7-segment LED indicator was selected, which allows displaying both parameters at once without the need to switch between them. Such an indicator has 16 pins (8 segments + 8 bits), which is clearly “beyond the power” of the small ATtiny13 controller. Fortunately, Maxim releases the MAX7219 chip, which is specially designed for such cases - inside the chip contains all the functionality of the dynamic display for 8 bits plus a serial interface that is compatible with SPI. Thus, with this chip, our entire indicator can be connected to the MK with just three wires (not counting the ground and power). This is already quite suitable for a controller with 5 interface pins. By the way, the cost of one set of indicator,

    As a temperature and humidity sensor, AM2302 is used, as mentioned above. It connects to the MK with just one wire. Thus, out of the available 5 interface outputs of MK, only 4 are used, and for the remaining 5th you can "hang" some additional function. Also, if you have an HVSP programmer available, you can disable the reset output and use it as the 6th interface output, but this will make it somewhat difficult to update the MK firmware.

    So, the whole diagram of the thermometer is presented in the figure below:
    Circuit diagram

    Since all interfaces for working with external MK devices are implemented in software, the choice of conclusions (pins) to which this or that signal is connected is purely arbitrary and made, most likely, according to the principle "where it was more convenient to insert this wiring on the breadboard". So you can safely choose other conclusions, it will only be necessary to correct their number in the code. The only limitation is that you should not connect the temperature sensor to one of the outputs used to program the MC through SPI - this can create a conflict, because the outputs of the two devices will be connected together, which is unacceptable from an electrical point of view.

    Now that everything is clear with the connection of the sensor and indicator, we proceed to write the code directly. And here a new “challenge” awaits us - ATtiny13 does not have any serial interfaces on board, i.e. all their logic will have to be implemented programmatically. Fortunately, implementing the SPI for the MAX7219 is not difficult, as the protocol is synchronous, the microcircuit operates at a frequency of up to 10 MHz, and the interface in our circuit works only for output. But communication with AM2302 will be a more difficult task, because it is connected with only one wire, the data on which are transmitted in both directions and the transmission speed is completely determined by the sensor itself. It should be said that most libraries for working with AM2302 follow the “simple path” - they prohibit interrupts and read all the information from the sensor with a single function call. This is a simple and reliable solution. but it is hardly suitable if any other real-time functions are assigned to the MK (for example, dynamic indication or continuous analysis of data from other sources), because the entire cycle of reading information about temperature and humidity takes from 4 to 6 milliseconds (depending on the transmitted data). Despite the fact that there are no other real-time functions in this thermometer, it was decided to write a universal code that would read information from the sensor “in the background”, i.e. on interruptions. Despite the fact that there are no other real-time functions in this thermometer, it was decided to write a universal code that would read information from the sensor “in the background”, i.e. on interruptions. Despite the fact that there are no other real-time functions in this thermometer, it was decided to write a universal code that would read information from the sensor “in the background”, i.e. on interruptions.

    To simplify the circuit as much as possible, ATtiny13 is clocked from the built-in RC-generator, generating about 9.6 MHz. This allows, by interrupting every 128 clock cycles of the processor, to obtain the polling frequency of AM2302 75KHz or 13.33 microseconds between adjacent polls. According to the AM2302 specification, the minimum pulse duration at its output is 26 microseconds, which is almost twice the polling interval and guarantees stable reading of data. Of course, 128 cycles between two interrupts is not very much for the implementation of the polling algorithm, but AVR executes most of the commands in 1 cycle, so it is quite possible to write a working program under such conditions, there will still be time for the main program.

    AM2302 according to the specification can be interrogated no more than once every two seconds. However, practice shows that he is quite capable of giving the result and more often - up to several times per second, provided that after turning on the power he will be given 1-2 seconds (according to the specification - 2) for initialization. In this thermometer, the sensor is polled once a second, however, the polling interval is easily changed to any other value.

    Unfortunately, AM2302 (perhaps its Chinese origin affects it) has a rather large error in the result - two consecutive temperature requests can return a difference of 0.5 or even more degrees, so it was decided to programmatically average the data of the last 8 measurements so that the thermometer would not jump .

    Now let's go directly to the code. The source asm and the resulting hex file are placed in the application at the end of the article, here I will explain the main points. It will be convenient to open the source code of the program in another window and look there while reading the article.

    At the beginning of the program, there are two important definitions:

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

    The first allows a conditional transition through the next 16bit instruction (1 word, most AVR instructions), i.e. skip it without entering an additional label, for example:

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

    The second allows you to access the first 64 bytes of the RAM memory using 16-bit instructions. Here I’ll tell you in more detail - usually for reading or writing to RAM RAM, the lds / sts commands are used, which take 2 words (32 bits) and are performed in 2 cycles. They allow you to address up to 64Kb (without extensions) of RAM. Unfortunately, the size of 32 bits (4 bytes) is already quite a lot for MK with a program memory of only 1Kb. Therefore, to save program memory, the address of the RAM start (0x60 for ATtiny13) is placed in the Y register of MK at startup, no one changes this register during the program operation, and access to the first 64 bytes of RAM is performed using indirect addressing with a shift in register Y , eg:

    	ldd	R16, Y + 6
    

    The ldd / std commands are also executed in 2 cycles, but only take 16 bits (2 bytes), i.e. Compared to lds / sts commands, this type of addressing saves half the amount of program memory. In order not to manually calculate the offset of any variable in each command, the _dataStart label is put at the very beginning of the data segment:

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

    And the team uses the DS macro (short for Data Segment):

    	ldd	R16, DS (testVar)
    

    The compiler converts this to a string:

    	ldd	R16, Y + testVar - _dataStart
    

    Automatically calculating the desired offset. It should be noted that this type of addressing is limited by the capabilities of the ldd command itself, and this is the first 64 bytes relative to the base register. But, in the case of ATtiny13, which has just 64 bytes of RAM on board, it allows you to address all the memory. However, in other MKs with a large amount of RAM, it is also possible to apply this method by placing the most frequently addressed variables in the first 64 bytes of the data segment. The payment for this method of addressing is register Y (two 8-bit registers R28 and R29), the value of which cannot be changed at any point in the program.

    Further, the program determines the bit numbers of port B (namely, the bits inside the byte, not the physical outputs of the microcircuit), to which external devices are connected. Since all protocols for interacting with devices are software, the bit numbers can be changed without any restrictions.

    A feature of MK AVR is that the first 16 registers R0 - R15 are "inferior", commands containing an operand inside them, for example, ldi or subi, do not work with them. Those. to even load a value other than 0 into one of these registers, you must use an additional register:

    	ldi	R16, 32
    	mov	R0, R16
    

    Therefore, often such registers are used as “quick access variables”. To do this, the compiler has a .def directive that allows you to assign an additional symbolic name to the register, for example:

    .def	R_TS = R0
    

    In the thermometer program, register R0 always stores the state of the AM2302 data receiver, register R1 is used to calculate the time of signal reception, R2 contains the received data, R3 is used as a counter of a timer increasing with a frequency of 100 Hz, and R4 and R5 as a countdown timer of 75 KHz, counting from 749 to 0.

    The MK data segment is divided into 4 parts - a block of data received from AM2302 (5 bytes), a buffer for decimal printing of a number (4 bytes), a buffer for averaging the readings of the thermometer and hygrometer by 8 values ​​(8 * 2 * 2 = 32 byte) and the MK stack (all the remaining n kneading, i.e., 23 bytes). In reality, of course, the stack takes less, and you can still find several bytes for additional functions in the memory, but you should not get carried away anymore.

    Now we go directly to the code segment. It traditionally begins with an interrupt table; for ATtiny13, these are 10 vectors, including a reset vector. Unused interrupts immediately contain the reti command, used (and two of them) - the transition to the handler command. The thermometer uses two interrupts serviced by one handler - this is an interrupt for timer overflow and an interrupt for equality of timer value to OCRA. One could do without one, however, such a method is 2 shorter commands (no need to change the timer mode from normal to STS).

    Immediately after the interrupt vectors, there is a table for converting numbers into codes for igniting 7-segment indicators. One could use the built-in decoding function in the MAX7219, but then it would be more difficult to display string messages on the indicator.
    Behind the table, the thermometer initialization program starts, executed immediately after the MK reset. It performs the initial installation of the MK stack pointer, the watchdog watchdog timer (set for 4 seconds), recording the initial values ​​in the MK registers, as well as initializing the I / O ports, MAX7219 and the main MK timer. After that, the program waits 2 seconds until AM2302 is initialized (showing a simple animation of the dying minus signs on the display) and goes into its main loop.

    The main cycle begins with the initiation of a request to AM2302 by changing the state of the data receiver in the register R_TS (R0). The next timer interruption will detect a change in state and begin the polling cycle of the sensor. Upon its completion, the TMS_NONE value will be placed in the status bits of the R_TS register, and until this moment the main program can perform any action. In this case, there is nothing to do, so the program simply puts the MK in sleep mode and waits for the end of the polling cycle.

    After the polling is completed, bit 3 of the status register determines whether the data was received successfully (value 1) or if an error occurred (value 0). If the data is successfully received, the program checks its checksum and, if necessary, transfers control to the error handler. The error handler counts the number of errors in a row, and as soon as this value becomes three, it displays the message “Sn Error”, indicating a malfunction of the sensor or connecting line. As soon as the temperature and humidity data are received successfully, the error counter is reset. This mechanism allows you to ignore the single sensor errors that occasionally occur in real life.

    In case of successful data acquisition, the previous measurements, which are in the data averaging buffer, are shifted up, and new data is added to its beginning. In parallel, the average values ​​are calculated, which will be shown on the display. It should be noted here that AM2302 gives out negative temperature not in an additional code, which is usual for processing by processors, but in the form of an absolute temperature value and a separate bit of its sign. In order to add such numbers and calculate their average values ​​using the usual MK commands, the data must be transferred to an additional code.

    Since the averaging buffer is not initially initialized, the average values ​​of temperature and humidity are displayed only after eight successful measurements. Up to this point, the current values ​​are displayed. In practice, this means that in the first 8 seconds after turning on the thermometer, the temperature and humidity values ​​can jump within a degree, after which the readings stabilize. It should be said that averaging from the last 8 values ​​has a very beneficial effect on the thermometer readings - now they basically change by no more than 0.1 degrees per second.

    The temperature is displayed in the format “x.x”, “xx.x”, “xxx.x”, “- x.x” or “-xx.x” depending on its value. Humidity is displayed in the format “xx” or “xx.x”. To convert a binary number in the X register to decimal form (in accordance with the codes for the 7-segment indicator), the printDecX function is used. Since the MK does not have a division command, the function is based on sequentially subtracting the values ​​1000, 100 and 10 from the initial number. The maximum number that the function can output is 9999; if it is called, the number will be higher in the X register, the function will return an overflow error by setting the flag transfer.

    To work with MAX7219, the maxWriteWord function is used, which writes the value from the XL MK register to the MAX register, whose number is specified in the XH register. After the current temperature and humidity are displayed, the program makes a delay of 1 second and repeats the main cycle again. To implement the delay, the wait100Hz function is used, which performs a delay of the time R16 * 0.01c using the R_TICK100 counter, the increase of which occurs when the timer is interrupted.

    Data is obtained from the temperature sensor using the function am2302proc, which is called from the timer interrupt handler. The function is a state machine whose state is stored in the register R_TS (R0) MK. Depending on the state, the function waits for a certain signal level from the sensor, initiating the transmission and sequentially receiving all 40 bits of the transmitted information. Synchronization occurs at each change in the input signal level, therefore, special accuracy is not required from the frequency of timer interruptions (which allows the MK to work from the built-in generator). The function consists of a quick idle state handler (TMS_NONE), which allows minimizing the load on the MK processor at a time when there is no data exchange with the sensor, a timeout handler designed to reset the machine to its original state, if the expected signal does not arrive for a long time (about 3 ms), and the handlers of each individual state of the machine. It should be noted that this function does not have noise immunity - even if the impulse noise changes the data line level for a short period of time, but it is he who gets into the read operation from the port, the function will read incorrect data. To compensate for this, the checksum of the read data is checked in the main program, so the display of incorrect information is practically excluded. However, such an implementation may not be the best if you want to move the sensor outside the thermometer and connect it to the MK with a long connecting line. that this function does not have noise immunity - even if the impulse noise changes the level of the data line for a short period of time, but it is he who gets into the read operation from the port, the function will read incorrect data. To compensate for this, the checksum of the read data is checked in the main program, so the display of incorrect information is practically excluded. However, such an implementation may not be the best if you want to move the sensor outside the thermometer and connect it to the MK with a long connecting line. that this function does not have noise immunity - even if the impulse noise changes the level of the data line for a short period of time, but it is he who gets into the read operation from the port, the function will read incorrect data. To compensate for this, the checksum of the read data is checked in the main program, so the display of incorrect information is practically excluded. However, such an implementation may not be the best if you want to move the sensor outside the thermometer and connect it to the MK with a long connecting line. therefore, the display of incorrect information is practically excluded. However, such an implementation may not be the best if you want to move the sensor outside the thermometer and connect it to the MK with a long connecting line. therefore, the display of incorrect information is practically excluded. However, such an implementation may not be the best if you want to move the sensor outside the thermometer and connect it to the MK with a long connecting line.

    At the moment, the thermometer is assembled on a breadboard and looks as follows:

    The appearance of the thermometer

    In the future, it is planned to place the thermometer inside the case of the existing electronic clock, organizing its power from the power supply of the clock.

    The current program occupies about 75% of the program memory MK. What can be added to the program? Perhaps someone will find it useful to change the brightness of the display luminescence (this is implemented directly in the MAX7219 driver) using an external button or a light sensor (using the built-in ADC and a free interface output), someone may need to remember and display the minimum and maximum temperatures. There is still room for minor modifications. Larger improvements may require changing the MK to another, which has more software and RAM on board. As for the interface outputs - at the moment the MK has one completely unused output and one more can be obtained by disabling RESET. Also, two outputs from the SPI interface (DATA and CLK) can be used for other functions, as until the CS pin is low (specifically for the MAX7219 it is important to switch from low to high) the signals at these pins do not matter. That is, in principle, replacing the MK with a more powerful one, for example, ATtiny85, you can connect up to four buttons to the Real Time Clock (RTC) thermometer.

    My goal was to create a simple thermometer / hygrometer, so most likely I will leave it to myself in this form.

    Program text
    
    // *********************************************
    // *** 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 file (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


    Also popular now: