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:
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:
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:
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:
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:
And the team uses the DS macro (short for Data Segment):
The compiler converts this to a string:
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:
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:
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:
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.
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:
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:
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