Week 8Home


This week we learned how to add input capabilities to microcontrollers.


The Goal:

This week, our goal was to add a sensor to a board we designed. I decided to go minimalist for this week, and I again just used the Hello World board from Week 4. While reading the ATTiny44A datasheet during week 6, I discovered that these microcontrollers have built-in temperature sensors. Since I want a temperature readout built in to my final project, I decided to write code that would enable my Hello World board to read the ambient temperature.

The Process:

Before I started, I went through the ADC section of the datasheet, which also describes the functionality of the temperature sensor. I could see that the main limit would be the sensitivity of this sensor, which only reads in ~1 degree celsius increments. I would therefore need to average several readings in order to get sensitivity on the order of 1 degree F, which is what I wanted.

I started with the code from my Week 6 project, in which I had created an interrupt-based framework that would allow the controller to respond to button presses and talk to the computer terminal.

The first step was to set up the Analog to Digital Converter (ADC) so that it would take as input the built-in temperature sensor. According to the datasheet, this required enabling the ADC (setting the ADEN bit in ADCSRA), setting the voltage reference to the built-in 1.1V reference (setting the REFS1 bit of ADMUX), and setting the input source to the temperature sensor (setting the MUX5 and MUX1 bits in ADMUX). Just to be safe, I also made sure that the ADC was not turned off by any power reduction mode (clear the PRADC bit in PRR). The final ADC setup step was to make sure the ADC is running at a good speed. The ADC isn't specified to over 1 MHz, and gives the best resolution in the range of 50-200 kHz, while the microcontroller is running at 20 MHz on this board. I could have reduced the overall CPU clock, but since the ADC has a built-in clock prescaler, this wasn't necessary. I set the prescaler to divide by 128 (ADPS0,1,2 bits of ADCSRA all set), giving an ADC clock of 156 kHz from the 20 MHz system clock.

The second overall step was to set up the interrupt handler. Since the ADC takes more than 1000 cpu clock cycles per conversion, it doesn't make sense to wait for the ADC result before performing other computations. Happily, the ADC has a built-in interrupt that is flagged whenever the ADC finishes a conversion. In order to use this, I enabled the ADC conversion complete interrupt (set ADIE bit in ADCSRA), and created an interrupt handler ISR(ADC_vect) to handle the processing of the raw value returned by the ADC.

The interrupt handler does most of the work of reading out and sending the temperature reading. First, it reads the ADC into an integer variable, t. Importantly, when reading values from two-word registers, one should always read the low byte first, and then the high byte. If the register is read in this order, the microcontroller will make sure the high byte isn't changed between the low and high byte reads. In order to increase precision, I set up an accumulation and averaging system. Each value read from the ADC is added to an accumulator variable, t. Importantly, this variable is a 'long' type, which for AVR GCC is a 32-bit number. Since the ADC has 10-bit precision, and since I add 100 of these readings together in the accumulation variable, the 16 bits of an int are not quite enough to protect against overflow. If fewer than 100 readings have been taken, the ADC interrupt handler starts another ADC conversion. If 100 readings have been taken, the handler converts the accumulated value to a reading in Fahrenheit and then sends it out to the serial port, resetting the accumulator. The conversion is not trivial. Firstly, since floating point is not supported in hardware, it is much better in terms of program space and speed if all constants and intermediate and final results remain integers. (AVR-GCC know how to do software floating point, but it takes up a lot of space: replacing the constant 18/10 with 1.8 increases program space by 20% of the overall program capacity). The order of operations is also important so that intermediate results do not overflow their variable size. The conversion formula basically subtracts an offset value (approximately converting from Kelvin to Centigrade), then multiplies by 1.8 and adds 32, finally dividing by the number of data points. In this manner, although I stick entirely to integer math, I am able to read out temperature in increments of 1 degree F instead of 1 degree C. Since I didn't have an accurate digital thermometer lying around I calibrated the reading roughly by comparing to my analogue wall-mounted thermostat. If I wanter better calibration, I could also do a two-point calibration, perhaps using melting ice as one point.

The final overall step is to control the initiation of ADC temperature readings. The easy way to do this is to manually start an ADC conversion at the desired point in the code (for instance a button press) by setting the ADSC bit in ADCSRA. The ADC then starts a conversion, the program execution continues, and the ADC raises an interrupt flag when done. I began with this implementation, but then decided to learn how to use the ADC noise reduction mode. In this mode, the all of the clocks except the ADC clock shut down, enabling the ADC to run, but stopping code execution and IO handling. This can greatly reduce noise during the ADC reading, increasing precision. Since my chip doesn't need to do a lot besides ADC measurements, and since my ADC readings are fairly sparse, it is acceptable to shut down the CPU during ADC readings.

To implement this, instead of manually initiating ADC conversions, I use the ADC Noise Reduction sleep mode. In order to enter this sleep mode, one must enable sleep modes by setting the SE bit in MCUCR, select the desired sleep mode (ADC noise reduction) by setting the SM0 bit in MCUCR, and then call the SLEEP command (cpu_sleep() in AVR-GCC). I first tried to simply replace my manual ADC conversion commands with these sleep commands, but this caused the microprocessor to spit out garbage. It turns out that since I was entering sleep from within the ADC conversion complete interrupt handler, I was effectively setting up a recursive code structure. Recursion is always dangerous, particularly when you don't realize you are doing it, and it is particularly hard to do well in a microcontroller that has very limited stack space. The garbage I was getting was the result of the stack overwriting legitimate data in memory. In order to fix this, I removed the actual SLEEP command from the interrupt handlers and instead had the handlers simply set a global flag. The main loop then checks for the flag and initiates the sleep mode when the flag is set.

When I first tried this fix, however, the main loop wouldn't initiate sleep, even when the flag was set. If I had the main loop print out the flag value, however, the problem disappeared and the main loop correctly initiated sleep. After some thought, I realized that the compiler was optimizing the flag variable out of my main loop. Because the flag could only be changed by interrupt handlers (which aren't in the program execution flow from the compiler's perspective), the compiler decided to replace the flag with a constant in the main loop. This is easily fixed by declaring the flag as 'volatile,' telling the compiler not to optimize it out. In fact, this 'volatile' declaration should be done for all variables that are used in both interrupt handlers and main code, so I updated all of my global variable declarations.


The Result:

In the end, I programmed the board so that it takes a temperature reading every time the user presses the button. I set up the button interrupt so that it triggers only on the falling edge of the signal, so only when the button is pressed and not when it is released. I also added some code to the serial interrupt handler so that the temperature is read when the user enters a capital 'T', allowing activation even when the button is inaccessible (in a fridge, perhaps).

Temperature dropping on a cold-pack

Temperature dropping after put in the freezer



Files