Saturday, 31 January 2015

Temperature controller for K-type thermocouple and MAX6675

After CNCPaul/LathePaul created such an awesome-looking heating chamber, the postie once again knocked on the door at Nerd Towers to deliver yet another eBay purchase - this time, it was a K-type thermistor and breakout module (basically just a MAX6675 on a board with pin headers)

We  had looked online at temperature controllers but they all seemed to be quite pricey for what they were - a cheap thermistor, a few 4x7 segment displays and a fancy box. Since we've currently got loads of 16x2 character displays and a boxful of PIC microcontrollers knocking about, it made sense (to us at least) to make our own temperature controller with a simple menu system.



We stuck an HD44780 character display onto a breadboard with a massive old PIC16F877A chip that just happened to be lying around. To this we also added the newly acquired temperature module and bundled some firmware onto the chip.

The LED on this breadboard indicates whether or not a relay would be activated, to switch the mains supply to our 300W cartridge heaters. As our target temperature in this example has yet to be met, the heater element is still switched on.

Here's the firmware for our controller. It's pretty basic. The most notable thing about it are the button press routines - we're using just three buttons, up/down/select. But, like an alarm clock, a "short press" will cause the values to increase/decrease by just one at a time, but a "long press" results in the numbers whizzing by in multiples of ten.

Define CONFIG = 0x3f32
Define CLOCK_FREQUENCY = 20
Define STRING_MAX_LENGTH = 30

AllDigital

definitions:
     Define LCD_BITS = 4 'no of pins to connect to character LCD
     Define LCD_DREG = PORTD '4-pin interface on this port
     Define LCD_DBIT = 4 '4-pin interface is on (0=0-3, 4=4-7)
     Define LCD_RSREG = PORTD
     Define LCD_RSBIT = 2
     Define LCD_EREG = PORTD
     Define LCD_EBIT = 3
     Define LCD_RWREG = PORTD
     Define LCD_RWBIT = 1

     Define SPI_CS_REG = PORTC
     Define SPI_CS_BIT = 1
     Define SPI_SCK_REG = PORTC
     Define SPI_SCK_BIT = 0
     Define SPI_SDI_REG = PORTC
     Define SPI_SDI_BIT = 2
     Define SPI_SDO_REG = PORTC
     Define SPI_SDO_BIT = 3
     Define SPICLOCK_STRETCH = 9

     Symbol led_pin = PORTA.0
     Symbol led_heater = PORTA.1
     Symbol heater_relay = PORTA.2
     Symbol button_up = PORTB.0
     Symbol button_down = PORTB.1
     Symbol button_ok = PORTB.2

     Symbol spi_sck = PORTC.0
     Symbol spi_cs = PORTC.1
     Symbol spi_si = PORTC.2
   
     ConfigPin PORTD = Output
     ConfigPin PORTB = Input
     ConfigPin led_pin = Output
     ConfigPin led_heater = Output
     ConfigPin heater_relay = Output
   
     ConfigPin spi_sck = Output
     ConfigPin spi_cs = Output
     ConfigPin spi_si = Input
   
     Dim state As Byte
     Dim return_state As Byte
   
     Dim b As Byte
     Dim target_temp As Word
     Dim current_temp As Word
     Dim tmp_temp As Word
     Dim overshoot As Word
     Dim last_temp As Word
     Dim temp_direction As Bit
     Dim button_press_count As Byte
               
initialise:

     SPIPrepare 'prepare the software SPI interface
     OPTION_REG.7 = 0 'enable pullups on portb (/RBPU)
     Lcdinit 0 'no cursor on the lcd display
     Low led_heater 'make sure we boot up with the heater off
     Low heater_relay 'heater indicator is off
   
     state = 0
     overshoot = 0
     temp_direction = 0
         
     SPIPrepare
     High spi_cs
     Low spi_sck
   
loop:
     Select Case state
           Case 0 'first boot up
           Lcdcmdout LcdClear
           Lcdout "Temperature"
           Lcdcmdout LcdLine2Home
           Lcdout "Controller"
         
           'read the last set temperature value (we'll automatically go back to this)
           Read 2, b
           target_temp.LB = b
           Read 3, b
           target_temp.HB = b
                     
           If target_temp.LB = 0xff And target_temp.HB = 0xff Then
                 'we've just read two blank values from eeprom, so choose
                 'any old default value - why not aim for, say, 100?
                 target_temp = 100
           Endif
         
           'read the last set overshoot value
           Read 4, b
           overshoot.LB = b
           Read 5, b
           overshoot.HB = b
         
           If overshoot.LB = 0xff And overshoot.HB = 0xff Then
                 'we've just read two blank values from eeprom, so choose
                 'any old default value - why not, say, ten?
                 overshoot = 10
           Endif
         
           'read the last overshoot value from eeprom (this is how many degrees before
           'reaching target temp we need to shut off for, to allow for overshooting)
         
           WaitMs 1000
           state = 1
         
           Case 1 'show the target and current temperature(s)
           Gosub get_current_temp
           Lcdcmdout LcdClear
           Lcdout "Target temp: "
           If target_temp < 100 Then Lcdout " "
           If target_temp < 10 Then Lcdout " "
           Lcdout #target_temp
         
           Lcdcmdout LcdLine2Home
           Lcdout "Actual temp: "
           If current_temp < 100 Then Lcdout " "
           If current_temp < 10 Then Lcdout " "
           Lcdout #current_temp
           state = 2
         
         
           Case 2 'monitor the current temp and the target temp
           Gosub get_current_temp
           Lcdcmdout LcdLine2Home
           If current_temp = 999 Then
                 'something has gone wrong
                 Low heater_relay
                 Low led_heater
                 Lcdout "**** ERROR! ****"
               
           Else
                 If current_temp <> last_temp Then
                       Lcdout "Actual temp: "
                       If current_temp < 100 Then Lcdout " "
                       If current_temp < 10 Then Lcdout " "
                       Lcdout #current_temp
                 Endif
                           
                 If last_temp <= current_temp Then
                       'temperature is rising
                       temp_direction = 1
                 Else
                       'temperature is falling
                       temp_direction = 0
                 Endif

                 If current_temp > target_temp Then
                       'we're over the target temperature so shut the heaters off
                       Low heater_relay
                       Low led_heater
                 Else
                       If temp_direction = 1 Then
                             'while the temperature is rising, we want to keep the heaters on
                             'until we cross the threshold of (target_temp-overshoot_value)
                             'and then turn them off and allow the block to continue heating
                             'even for a very short time
                             tmp_temp = target_temp - overshoot
                             If current_temp > tmp_temp Then
                                   'threshold exceeded, turn off the heater(s)
                                   Low led_heater
                                   Low heater_relay
                             Else
                                   'still not quite there, keep the heaters on
                                   High led_heater
                                   High heater_relay
                             Endif
                       Else
                             'if the temperature is falling, the bottom threshold
                             'is our target temperature minus the overshoot value; once we
                             'fall below that, turn the heaters back on
                             tmp_temp = target_temp - overshoot
                             If current_temp < tmp_temp Then
                                   'we've cooled down too much, turn the heaters on again
                                   High heater_relay
                                   High led_heater
                             Else
                                   'we're cooling down, but are still pretty close to the target
                                   'temperature, so allow a little more cooling
                                   Low heater_relay
                                   Low led_heater
                             Endif
                       Endif
                 Endif
         
                 last_temp = current_temp
           Endif
         
           'now while the temperature is being monitored, check for button presses
           '(on a button press, turn off the heater(s) just for good measure, and
           'go to a new state just for handling changing the settings
           If button_up = 0 Or button_down = 0 Or button_ok = 0 Then
                 Low heater_relay
                 Low led_heater
                 WaitMs 50
               
                 While button_up = 0 Or button_down = 0 Or button_ok = 0
                       'wait for the button to be released
                       WaitMs 50
                 Wend
                 state = 3
           Endif
           WaitMs 1000
         
         
           Case 3 'press buttons to change menu option
           Lcdcmdout LcdClear
           Lcdout "> Change target"
           Lcdcmdout LcdLine2Home
           Lcdout " Edit overshoot"
           state = 4
         
         
           Case 4
           If button_up = 0 Or button_down = 0 Then
                 WaitMs 50
                 While button_up = 0 Or button_down = 0
                       'wait for the button to be released
                       WaitMs 50
                 Wend
                 state = 5
           Endif
           If button_ok = 0 Then
                 'debounce the button press
                 WaitMs 50
                 While button_ok = 0
                       WaitMs 50
                 Wend
                 state = 10
           Endif
         
           Case 5 'press buttons to change menu option
           Lcdcmdout LcdClear
           Lcdout " Change target"
           Lcdcmdout LcdLine2Home
           Lcdout "> Edit overshoot"
           state = 6
         
         
           Case 6
           If button_up = 0 Or button_down = 0 Then
                 WaitMs 50
                 While button_up = 0 Or button_down = 0
                       'wait for the button to be released
                       WaitMs 50
                 Wend
                 state = 3
           Endif
           If button_ok = 0 Then
                 'debounce the button press
                 WaitMs 50
                 While button_ok = 0
                       WaitMs 50
                 Wend
                 state = 11
           Endif


           Case 10 'change the target temperature value
           Gosub show_target_temp
           return_state = 10
           state = 12
         
         
           Case 11 'change the overshoot value
           Gosub show_overshoot
           return_state = 11
           state = 12

         
           Case 12 'button presses to alter target temperature/overshoot value
           If button_up = 0 Then
                 WaitMs 50
                 button_press_count = 0
                 While button_up = 0
                       'wait for the button to be released
                       button_press_count = button_press_count + 1
                       WaitMs 100
                       If button_press_count >= 5 Then
                             'this is a "long press" so increase the value by 10 every loop
                             If return_state = 10 Then
                                   target_temp = target_temp + 10
                                   If target_temp > 500 Then target_temp = 500
                                   target_temp = target_temp / 10
                                   target_temp = target_temp * 10
                                   Gosub show_target_temp
                             Endif
                             If return_state = 11 Then
                                   overshoot = overshoot + 10
                                   If overshoot > 500 Then overshoot = 500
                                   overshoot = overshoot / 10
                                   overshoot = overshoot * 10
                                   If overshoot >= target_temp Then overshoot = target_temp - 1
                                   Gosub show_overshoot
                             Endif
                             WaitMs 200
                             'stop the counter from rolling over
                             button_press_count = 5
                       Endif
                 Wend
                 If button_press_count < 5 Then
                       'this is a "short" press so increase the value by just one
                       If return_state = 10 Then target_temp = target_temp + 1
                       If return_state = 11 Then overshoot = overshoot + 1
                 Endif
               
                 If target_temp > 500 Then target_temp = 500
                 If overshoot > 500 Then overshoot = 500
                 If overshoot >= target_temp Then overshoot = target_temp - 1
                 state = return_state
           Endif
         
         
           If button_down = 0 Then
                 WaitMs 50
                 button_press_count = 0
                 While button_down = 0
                       'wait for the button to be released
                       button_press_count = button_press_count + 1
                       WaitMs 100
                       If button_press_count >= 5 Then
                             'this is a "long press" so decrease the value by 10 every loop
                             If return_state = 10 Then
                                   b = target_temp Mod 10
                                   If b = 0 Then
                                         If target_temp > 10 Then target_temp = target_temp - 10 Else target_temp = 0
                                   Else
                                         target_temp = target_temp / 10
                                         target_temp = target_temp * 10
                                   Endif
                                   Gosub show_target_temp
                             Endif
                             If return_state = 11 Then
                                   b = overshoot Mod 10
                                   If b = 0 Then
                                         If overshoot > 10 Then overshoot = overshoot - 10 Else overshoot = 0
                                   Else
                                         overshoot = overshoot / 10
                                         overshoot = overshoot * 10
                                   Endif
                                   Gosub show_overshoot
                             Endif
                             WaitMs 200
                             'stop the counter from rolling over
                             button_press_count = 5
                       Endif
                 Wend
                 If button_press_count < 5 Then
                       'this is a "short" press so increase the value by just one
                       If return_state = 10 Then
                             If target_temp > 1 Then target_temp = target_temp - 1 Else target_temp = 0
                       Endif
                       If return_state = 11 Then
                             If overshoot > 1 Then overshoot = overshoot - 1 Else overshoot = 0
                       Endif
                 Endif

                 state = return_state
           Endif
         
           If button_ok = 0 Then
                 WaitMs 50 'simple debounce
                 While button_ok = 0
                       'wait for button to be released
                 Wend
                 If return_state = 10 Then
                       'write the new target temp to eeprom
                       b = target_temp.LB
                       Write 2, b
                       b = target_temp.HB
                       Write 3, b
                 Else
                       'write the new overshoot value to eeprom
                       b = overshoot.LB
                       Write 4, b
                       b = overshoot.HB
                       Write 5, b
                 Endif
                 state = 1
           Endif

     EndSelect
Goto loop
End


get_current_temp:
     'read the value from the thermistor
     'in some libraries, they suggest performing a "dummy read"
     'to force the device to re-sample the temperature
     Low spi_cs
     WaitMs 2
     High spi_cs
     WaitMs 220
     'the device should now have a new temperature in it's buffer
   
     'enable CS so we can read data back from the device
     SPICSOn
     'give the device time to settle
     WaitMs 50
   
     'get the data as two bytes
     SPIReceive b
     current_temp.HB = b
     SPIReceive b
     current_temp.LB = b

     SPICSOff
         
     'now according to the datasheet, bit 15 is a dummy, bits 0-2 are status
     'so the actual temperature we need is in bits 14-3
   
     b = current_temp.LB
     b = b And 00000100b
     If b > 0 Then
           'bit 2 is normally low, and goes high when the thermistor is not connected
           current_temp = 999
     Else
           'bit-shift three places to the right
           current_temp = ShiftRight(current_temp, 3)
           'and mask out to get only the lower 12 bits
           current_temp = current_temp And 0x0fff
     Endif
   
     If current_temp = 0 Or current_temp = 0x0fff Then
           'something has gone wrong here
           current_temp = 999
     Else
           'the temperature reported has a resolution of 0.25 deg C
           'so we need to divide our final answer by 4 (or multiply by 0.25)
           'to get the actual temperature in degrees
           current_temp = current_temp / 4
     Endif
   
Return


show_target_temp:
     Lcdcmdout LcdClear
     Lcdout "Target temp: "
     If target_temp < 100 Then Lcdout " "
     If target_temp < 10 Then Lcdout " "
     Lcdout #target_temp
Return


show_overshoot:
     Lcdcmdout LcdClear
     Lcdout "Overshoot: "
     If target_temp < 100 Then Lcdout " "
     If target_temp < 10 Then Lcdout " "
     Lcdout #overshoot
Return

Our firmware allows the user to press a couple of buttons to change two different parameters:
Firstly, and most obviously, the target temperature to reach. When this is connected up, we're expecting to heat up a block of aluminium. The basic idea is to take a temperature reading and if we're below a certain threshold, turn the heaters on, until the block is "up to temperature"

We also added in another user-defined parameter: something we've called "overshoot".
This is because it's quite possible - as sometimes happens with heated elements like soldering irons for example - that the target temperature is exceeded. By how much can sometimes be predicted, so our variable "overshoot" means that the heaters will cut out a little bit before the ideal temperature has been reached.

So if the temperature of the block continues to rise, even when the heater(s) are turned off, it should - ideally - only reach, and not exceed, the target temperature.

We also use the "overshoot" value for monitoring a falling temperature, to give us a "safe zone" during which the heaters remain inactive. If we simply said "if the temperature is below this threshold, turn on the heaters" it's quite possible that the heaters will come on for a second or two, the temperature could rise by a single degree and the heaters get turned off - a few seconds later, the temperature drops a little, and the heaters are fired up again.

To avoid this "bouncing around" on a single value, our overshoot parameter allows us a "safety zone" of, say, 10 degrees (the user can make this wider or narrower as required). If our target temperature is 180 degrees, and our overshoot value is 10, this means:

When we first switch on, the heaters come on.
The heaters stay on until the temperature has climbed to 170 degrees.
When 170 degrees is exceeded, the heaters are turned off.
The temperature can continue to climb (if it climbs too high - i.e. the target temperature has been overshot by a long way, the user can edit the overshoot parameter to make it a larger value)
Eventually the temperature of the block will start to fall. Even if it falls below 180 degrees, we don't yet switch on the heaters
When the temperature of the block has fallen below 170 degrees (our target value minus the overshoot value)  and the temperature is falling, we turn the heaters back on.

By monitoring the "direction" of the temperature (either rising or falling) and by switching the heaters off slightly before the target temperature is reached, we should be able to maintain a fairly stable temperature in our heater block.

During testing, everything appeared to work as it should - immerse the thermistor into a cup of cold water and the temperature fell sharply. At the same time, the heater LED indicator lit up. We set our target temperature quite low (28 degrees) and our overshoot value to just 3 degrees, then held the thermistor in a closed fist.

The temperature continued to rise and at 25 degrees (three degrees before the target temperature) the heater indicator went out. After releasing the probe, the temperature continued to fall to ambient room temperature (and as the temperature fell below 25, the heater indicator once again came on to show that the heaters would normally have been activated).

Next time we're at the space, we should be able to drill and tap a small hole for the thermistor to be embedded into the heater block (hopefully avoiding drilling all the way through to the main chamber)  mount a relay in place of/as well as our heater indicator LED, and actually try our temperature controller out for real!