Jump to content

Solar water heating controller

Recommended Posts

I wanted to try out myself how well solar energy really works. Toward

this aim I built a solar collector system for waterheating. A small boiler with a

heat exchanger is used for heat storage and a ordinary heating pump

for the water circulation. Temperature is measured at the collector,

at the entrance and outlet of the heat exchanger and inside the heat

storage boiler. As a controller for the pump I used an MSP430

launchpad. There were several problems I had to solve.


First, temperature must be measured with a resolution of at least 1

degree. My preferred temperature sensor would have been a Pt100. This

sensors are accurate and linear, but not very sensitive. With a 8 bit

AD converter, a resolution of not more than 4 degree is possible, not

good enough. Therefore I was forced to use NTC sensors. These are more

sensitive, but not very accurate and the resistance changes with

temperature not linearly, but exponentially. When I tried this out, I

had to realize that the exponential function of C uses too much

memory. Therefore, I had to find a replacement and I used a rational

approximation that uses much less space and works satisfactorily

accurate. Further, as the NTC sensors are not accurate enough, I had to measure

the value of each and correct by software.


To supervise the working of the controller I transmit the temperature

values, the state of the pump and what action is taken to a PC that

can be connected to the launchpad. This is actually not needed for the

working of the controller, but it is nice to have a mean to see what

is going on.



Here is how the program works:


After initialization we enter an infinite loop, that does the actual



- First all 4 temperature values are measured. According to these values

we decide what to do.

- If the boiler temp is too high, we switch the pump of for security


- If the pump is off and the collector temp is higher than the boiler

temp by some margin and the collector temp is above some minimal

value, we switch the pump on.

- If the pump is on, we compare the entry and outlet temp of the heat

exchanger. If these differ less than some value, that is, practically

no heat is put into the boiler, we switch the pump off.

- Finally we toggle a LED every 1 sec to indicate that the controller is

working and wait 1 minute before we start the next cycle.


To make the temperature measurement more reliable, I measure the temp

8 times and take the average.

To minimize current consumption I put the CPU to sleep during wait.


This controller now works for more than half an year to my full

satisfaction. I get approx. 0.75 kW / m^2 heating power on a sunny day. I only regret, that we do not have more sunny days

here in Switzerland.










And here is the program that uses up nearly every bit of the 2K memory:


/* Solar collector controller 
* Daniel Huber 1.7.2011    daniel_huber@sunrise.ch 
* Measures Pin 1.3,1.4,1.5,1.7 with average 
* Calculates temperatures from measured values 
* If boiler temp too high -> do nothing 
* If collector temp > boiler+ TDelCol switch on pump, wait 1 min. 
* If temp of heatexchanger input < heat exchanger output+TDelEx switch off pump, repeat 
* send values to RS232 
#include "msp430g2231.h" 
#include "stdbool.h" 
#include "stdlib.h" 
#include "math.h" 
#include "string.h" 
#define         Tcm             110                            // max temp at collector 
#define         Tcmin           30                              // min temp at collector 
#define         Tbm             90                              // max temp at boiler 
#define         TDelCol         5                               // min. temp between Collector and Boiler 
#define         TDelEx          2                               // min. temp between heat exchanger in and out 
#define       TXD               BIT1                            // TXD on P1.1 
#define       Bit_time         104                            // 9600 Baud, SMCLK=1MHz (1MHz/ 9600)=104 
#define       LEDR              BIT0                           // LED red 
#define       LEDG              BIT6                          // LED green 
#define       CHCOLLECT    INCH_4                      // measure channel collector 
#define       CHHEATEXIN   INCH_5                       // measure channel heat 
#define       CHHEATEXOUT  INCH_3                    // measure channel heat 
#define       CHBOILER         INCH_7                    // measure channel boiler 
                                                                       // pump on/off 
#define PumpOn  {P1OUT |= BIT0;PumpOnFl=true; } 
#define PumpOff {P1OUT &= ~BIT0;PumpOnFl=false; } 

unsigned char BitCnt;                                        // Bit count, used when transmitting byte 
unsigned int  TXByte;                                       // value sent over UART when Transmit() 
is called 
int  i,j;                                                           // 'for' loop variable 
int     TCol,TExIn,TExOut,TBoiler,channel;        // Measured ADC Value 
bool    PumpOnFl;                                            // pump on flag 
unsigned int ADCValue;                                   // aux. value to return value from 
short state;                                                    // state variable 
char msg[3]; 
float cor,R; 

// Function Definitions 
void Transmit();                                               //transmits a byte 
int Single_Measure();                                       //measures with avarage 
void Single_Measure0();                                   //measure single value 
void TransmitRecord(/*int TCol,int TExIn,int TExOut,int TBoiler,bool PumpOnFl*/); // transmit one record 
void delay1();                                                 //delays 1 sec 

int  P2T(int P);                                                // ADC value to degree 
void main(void) { 
       WDTCTL = WDTPW + WDTHOLD+WDTCNTCL;   // Stop WDT: PWD,Hold, Counter Reset 
       BCSCTL1 = CALBC1_1MHZ;                       // Set range 
       DCOCTL = CALDCO_1MHZ;                       // SMCLK = DCO = 1MHz 
//    BCSCTL3 |= LFXT1S1;                              // sets LFXT1Sx to 0b10, VLO mode, bit not set-> 32KHz 
       BCSCTL3 |= XCAP_2;                              // set 10pF for cristal oscillator 
       P1SEL |= TXD;                                       // Connect TXD to timer pin 
       P1DIR |= TXD | LEDR | LEDG;                // use TX 
       PumpOff;                                              // Pump off 
       __bis_SR_register(GIE);                       // interrupts enabled 

          msg[0]='\0';                                        // empty string 
          cor=1.01*10.12;channel=CHCOLLECT;  //correction factor: 
          TCol=P2T(Single_Measure());               // collector temp 
          TExIn=P2T(Single_Measure());             // heat exchanger in temp 
          cor=0.89*10.21;channel=CHHEATEXOUT; // 0.937 takes care of R20 difference 
          TExOut=3+P2T(Single_Measure());        // heat exchanger out temp, correct for NTC tolerance 
          TBoiler=P2T(Single_Measure());// boiler temp 
          if (TBoiler > Tbm && state!=0) {PumpOff;state=0;strcat(msg,"st00");} 
                 case(0): if(TBoiler                  case(1): if((TCol>TBoiler+TDelCol) && TCol>=Tcmin) {PumpOn; state=2;strcat(msg,"12");} break; 
                 case(2): if(TExIn           } 
                P1OUT ^= LEDG;             // toggle LED at P1.6  

// transmit one record 
void TransmitRecord(){                                  
unsigned int k; 
                       case(0):  k='SS'; break; 
                       case(1):  k='tt'; break; 
                       case(2):  k=TCol; break; 
                       case(3):  k=TExIn; break; 
                       case(4):  k=TExOut; break; 
                       case(5):  k=TBoiler; break; 
                       case(6):  if(PumpOnFl)k=1; else k=0; break; 
                       case(7):  if (strlen(msg)>0){k=256*msg[1]+msg[0]; break;} else {i++;} 
                       case(8):  k='EE'; break; 
                       case(9):  {k='nn'; i=-2;} 
               TXByte = (k & 0x00FF);                  // Set TXByte 
               Transmit();                                   // Send 
               TXByte = k >> 8;                           // Set TXByte to the upper 8 bits 
               TXByte = TXByte & 0x00FF; 

// averaged single measurement 
int Single_Measure(/*int channel*/){ 
   int ADCAvg = 0; 
   for (i = 0; i < 8; i++){                         // add up values 
      ADCAvg += ADCValue; 
   ADCAvg >>= 3;                                 // divide by 8 
   return ADCAvg; 

* Reads ADC channel once, using AVCC as reference. 
void Single_Measure0(/*int channel*/) { 
   ADC10CTL0 &= ~ENC;                          // Disable ADC 
   ADC10CTL0 = ADC10SHT_3 + ADC10ON + ADC10IE; // 64 clock ticks, ADC On, enable ADC interrupt 
   ADC10CTL1 = ADC10SSEL_3 +channel;   // Set 'chan', SMCLK 
   ADC10CTL0 |= ENC + ADC10SC;              // Enable and start conversion 
   _BIS_SR(CPUOFF + GIE);                      // sleep CPU 

* Transmits the value currently in TXByte. The function waits till it is 
*   finished transmiting before it returns. 
void Transmit() { 
       TXByte |= 0x100;                                // Add stop bit to TXByte (which is logical 1) 
       TXByte = TXByte << 1;                         // Add start bit (which is logical 0) 
       BitCnt = 0xA;                                     // Load Bit counter, 8 bits + ST/SP 
       CCTL0 = OUT;                                    // TXD Idle as Mark 
       TACTL = TASSEL_2 + MC_2;                // SMCLK, continuous mode 
       CCR0 = TAR;                                      // Initialize compare register 
       CCR0 += Bit_time;                             // Set time till first bit 
       CCTL0 =  CCIS0 + OUTMOD0 + CCIE;        // Set signal, intial value, enable interrupts 
       while ( CCTL0 & CCIE );                     // Wait for previous TX completion 

* ADC interrupt routine. Pulls CPU out of sleep mode. 
#pragma vector=ADC10_VECTOR 
__interrupt void ADC10_ISR (void) 
       ADCValue = ADC10MEM;                    // Saves measured value. 
       __bic_SR_register_on_exit(CPUOFF);      // Enable CPU so the main while loop continues 

* Timer interrupt routine. This handles transmitting and receiving 
#pragma vector=TIMERA0_VECTOR 
__interrupt void Timer_A (void) { 
               CCR0 += Bit_time;                   // Add Offset to CCR0 
               if ( BitCnt == 0)                        // If all bits TXed 
                       TACTL = TASSEL_2;       // SMCLK, timer off (for power consumption) 
                       CCTL0 &= ~ CCIE ;           // Disable interrupt 
                       CCTL0 |=  OUTMOD2;      // Set TX bit to 0 
                       if (TXByte & 0x01) 
                       CCTL0 &= ~ OUTMOD2;      // If it should be 1, set it to 1 
                       TXByte = TXByte >> 1; 
                       BitCnt --; 

 * function to get temperature from measured ADC value 
int  P2T(int P){ 
       #define P0 0x3FF 
       (159.444F+R*(29.4008F-0.21077F * R))/(1+R*(0.59504F+0.0155797F * R)) 

//put CPU to sleep for 1 sec 
void delay1(){ 
  IE1 |= WDTIE;                        // Watchdog Interrupt Enable 
                                     //Intervall(WDTTMSEL),Timer= "0"(WDTCNTCL),
                                     //SourceClk=ACLCK(WDTSSEL),Sel:00=Clk/32768 01=Clk/8192 10:Clk/512 11=Clk/64 
  _BIS_SR(LPM3_bits + GIE);            // put CPU to sleep LPM1_bits 

// delay interrupt routine, wakes up CPU 
#pragma vector=WDT_VECTOR 
__interrupt void WATCHDOG_ISR (void){           // interrupt routine for delay 
                                     //Watchdog stop(WDTHOLD),Counter=0, this resets register(exeption: hold bit) 
//        IE1 &= ~WDTIE;                        // Watchdog Interrupt Disable 
       __bic_SR_register_on_exit(LPM3_bits); // clear LPM3 bits so the main while loop continues 


Link to post
Share on other sites
  • 2 weeks later...

Welcome Dan,


awsome project, couple of comments:

1) do you have any kind of storage tank?


2) Do you use the exchanger sensor as a cut off switch, or do you try to keep this constant?


3) is the demand signalled with temp alone?


4)did you do all that brazing and bending? Why copper ( not that i object) ? Quite impressive, share your tips please :)

Link to post
Share on other sites

Hi Kenemon,


the storage was a problem as I did not have the necessary space. Therefore, I needed another solution. The original installation had a small electrical boiler. I installed a second boiler in serie with the original one. Thereby, the electrical boiler receives water preheated by the solar collector. If there is not enough sunshine, the water temperature will be boosted by electricity during night time. If the sun shines long enough, no electricity is used. Of course I can not store heat for a long time, it is used on a daily basis.


For cut off, I monitor the temperature difference over the heat exchanger. If this becomes too small, it means that no heat is delivered to the boiler or even drained and I switch off the pump.


I heat the boiler as soon as the collector is warmer than the boiler. For security reasons, I also have a max. boiler temp. where the pump is switched off, but I have not yet reached it.


I soldered where appropriate because it is easier to do. Only where I had steel I did braze.

It is important to have copper sheet metal with a coating that absorbs visible light, but does not re- radiate infrared heat. The copper sheet metal is soldered to copper tubing because of heat flux. The auxiliary tubing was first done with PETX tubing (cross linked PET with a aluminium layer), but it turned out that with a temperature of 120 degree C, these started to leek at the connectors. Therefore, I did all the tubing inside the collector in copper. The tubing from the collector to the basement is done in PETX, isolated with PET. This seems to work well. The only problem I had was, that the water immediately at the outlet is so hot, that the PET isolation melted. Therefore, I isolated the first meter using glass wool. The isolation of the collector at the back side is done in rock wool. The front is covered with a polycarbonat sheet. This is much cheaper than glass and does not break. In addition it can be bent easily using a hot air gun.


cheers, Daniel

Link to post
Share on other sites

I am still taking it in... My gut brainless thought now is that you should recon a used or secondhand expansion tank to eliminate the potential disaster you have forseen with the proximity to the pump. It would be a shame to lose that pump (they aint cheap) or get juiced. I hope you have a GFI inline.... Otherwise, I want one, but they are tricky when it snows here :)

I would need to cycle water at night, and on cold days, or use PEG, much like a hot tub.... Still admiring your copper work!

How expensive was all that copper?

Link to post
Share on other sites

Hi kenemon,


I actually use an expansion tank, otherwise the system would crack (see pictures). At first thought it seems ridiculous that the tank consist of an upside down PET bottle. But if you dig in, you will find that these PET bottles are rated for an unbelievable 15bar. I have some experiences with these bottles, I used them to build "water rockets" for the kids. These consists of an upside down bottle, approx. half filled with water. You then pump them up with air (we used 7bar from a compressor or a bicycle pump) and then you let go. These "rockets" fly approx. 30m high and are fun for the whole neighborhood.

An additional benefit is that the bottles are transparent, you can actually see how the water expands, what is not little (good lecture in physics!). One bottle works, but next time when I empty the system, I will add a second bottle, because the pressure changes still a bit too much for my taste. However, at this time the system is filled with antifreeze (car supply) what makes it a bit of a mess to empty. Therefore, I wait until I have to empty it anyway.


In Switzerland you pay ridiculous prices for copper at this time. However, I am living not too far from Germany, where you can get it much cheaper. Most of the copper I could buy from surplus material and I payed something over SFr. 100.- for one collector. The tubing from the collector is done in PEX-Aluminium tubing, what is also much cheaper in Germany, I bought it for approx. 1 Euro/m.


What do you mean by GFI? Ground Fault Interrupter? The main voltage part is separated from the low voltage part by an opto. Therefore, there is no direct connection between both sub-systems.


By PEG, you main antifreeze? I need this dearly, two weeks ago we had -15 degree Celsius, but everything worked perfectly. As soon as the sun comes out, the boiler starts to heat. I do not think that circulating the water would prevent it from freezing with such low temperatures.


cheers, Daniel

Link to post
Share on other sites

No Worries Dan,


you seem to have thought it through adequately. Things are a little more complicated here, with city inspectors snooping around from time to time. I saw your expansion device, but i did not realize they could handle that type of pressure. It is just an excuse for inspectors here to hastle you for something... They also are sticklers for GFI's, even when closer than 2 feet to a kitchen sink! I just saw that potential moisture near your pump condom. Seems like your system works great. Yes I was speaking of antifreeze (PEG), they use it here allot to prevent disasters with radiant heat systems.... I am jealous of your free hot water- Nice Job!



Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Create New...