Thermostatic Faucet
2016-04-13 | By Sergey Sokol
Overview
When I lived in Japan, my apartment had an electronic interface for the shower that made the hot water heater output exactly the water temperature I wanted. I was a big fan of this ease of use and thought that it could be applied to sink faucets as well. Upon returning to the United States, I realized that while I could use instant water heaters, the gas or electrical infrastructure simply wasn't there to retrofit these installations into current construction, especially the bathrooms. Also, in the few cases the infrastructure was available, the interfaces had been clunky, with definite room for improvement. As I was mulling this over, I realized valves could be installed on the cold and hot water lines at the point of usage and electronically controlled to automatically provide consistent water temperatures. This setup could be used in new or old construction without any infrastructure changes, would increase safety as the temperature would never go above the desired point, and could even save water by optimizing the process of replacing the cold stagnant water in hot water lines.
Approach
In order to reduce the difficulty of a retrofit, the design must be simple to install. As there will be both daily users and guests, the interface must be intuitive and able to accommodate both regular users and first time users. The interface for a shower or bath is much more simple than the interface required for bathroom or kitchen sinks. For the faucet, the valves and control logic will be installed at the point of use, going between the copper tubing and the braided metal hose. With this setup, the end user will have the flexibility of using their current faucet or obtaining a specific faucet for this without controls. Plugging into the outlet under the sink that is generally provided for garbage disposals will provide power. The controller itself will be a simple microcontroller-based PID controller, with a single thermocouple measuring output water temperature and two motors turning the valves. For the prototype, the interface will be a simple potentiometer to control both flow and temperature. In future incarnations I anticipate incorporating a flowmeter to give greater control, but at the moment, the flow will be roughly estimated with the main focus on maintaining the proper temperature.
Challenges and Design Considerations
The biggest challenge I anticipate is powering the controller for the shower. Under sink plugs are very common but finding a similar power source has been, and unfortunately continues to be, a nearly insurmountable challenge. A different yet related problem is what to do when a power outage occurs. Being able to use the system when the power is off and also turning the water off if the power cuts out when the water is already on are both concerns.
Another large, though more secondary, challenge is to keep the costs minimal. Thermostatic faucets are not an original idea. However, even those products designed specifically for new construction costs thousands of dollars for a single, simple faucet. To be competitive with these, a significantly lower price point needs to be met. Though it may seem counter-intuitive, the easiest portion of this project is likely the actual PID control algorithm. It is a fairly simple control problem that has been solved, perfected, and well documented over the years.
End Result
The implementation to create a proof of concept went much as anticipated. The interface between the motors and the valves were a bit of a challenge, but with the help of a mechanical engineer, a crude but effective rig was setup. We selected multi-turn valves to make certain that the motors would be able to turn them effectively. While this is effective in making sure the motors are never under any undue strain, it made the response time a bit sluggish. Also, while the valves are fully opened after four turns, the water comes out at nearly full flow after one turn. As anticipated, the flow had to be adjusted manually, setting the parameters in the software to consider one turn as one hundred percent flow.
The PID parameters needed tweaking, but it was a fairly simple matter of adjusting those parameters to get a critically damped response. The response time, while slower than ideal, was acceptable. The concern at first was there would need to be another thermocouple on the hot water input, but the PID worked well as a single input, dual output system. There also needs to be some sort of feedback to indicate the full closure of the valves to both ensure that the valve is entirely closed and also to limit the time the motor is stalling while attempting to close the valve. This will likely be done by using a half effect sensor to detect when the current consumed by the motor increases.
Finding a power source for the shower unit has thus far been unsuccessful, a concerning stumbling block. Also, the current prototype is still so far from manufacturability that the second challenge of being cost effective is not even a goal at the moment.
Moving Forward
The basic concept of a thermostatic faucet is not only possible, as shown previously by large manufacturers, but also within the realm of anyone with reasonable electronic and mechanical ability. To move beyond a proof of concept, the device needs a considerable amount of work. The main items identified are:
1) Solution to power concerns
2) Flow feedback
3) Position and current sensing feedback of motors
4) More elegant mechanical interface between motors and valves
Other than the power, all other concerns can be overcome with time and effort, leaving power the only potential problem that cannot be overcome with previous construction. At this time, it is difficult to decide whether the project is worth the time and expense of pursuit and may be temporarily shelved until a satisfactory solution is found for the power.
Code - main.c:
- #include "general.h"
- #include "DS18B20.h"
- #include "DRV8823.h"
- #include "Display.h"
- #define BUTTON_PIN BIT4
- #define MOTOR_MIN 0
- #define MOTOR_MAX 4000
- #define GAIN_T 6 //Temperature regulator gain
- #define TI_T 2.5 //Temperature rerulator integration time (in seconds)
- #define GAIN_R 1 //Rate regulator gain
- #define TI_R 5.0 //Rate rerulator integration time (in seconds)
- #define STEP 0.2 //Regulation step (in seconds)
- int16_t temperature, position; //Current values of the temperature and position
- int32_t set_temperature, set_position; //Set values of temperature and position
- char strtemp[9]={0,0,0,',',0,0xDF,'C',' ',0};//String for temperature value to be displayed
- char strpos[5]={0,0,0,'%',0}; //String for position value to be displayed
- int32_t motor_hot_pos, motor_cold_pos; //Current positions of motors (in steps)
- volatile int32_t set_motor_hot_pos, set_motor_cold_pos; //Set positions of motors (in steps)
- volatile int32_t set_motor_hot_pos_prev, set_motor_cold_pos_prev; //Previous set positions of motors for regulator
- uint8_t on_off; //Displays the state of the device: on/off
- int16_t delta, delta_prev; //Difference between set and current values of temperature
- float rate; //Rate between cold and hot valves position
- uint8_t correct_rate; //Displays if the rate is correct
- DS18B20 sensor(2,3); //Define DS18B20 sensor at P2.3
- Display display(2,5); //Define display with reset pin at P2.5
- DRV8823 motor(1,5,1,3,2,0,2,1); //Define motor driver
- ////////////////////////////////////////////////////////////////////////////////
- void read_temperature (void)
- {
- do
- temperature = sensor.GetData10();//Read current temperature
- while (temperature == 850); //850 is the default value that is got when sensor is not ready
- if (temperature > 1000) //If temperature is higher than 100 degress (for Fahrenheit)
- {
- strtemp[0] = (temperature/1000) 0x30;
- strtemp[1] = ((temperature/100)) 0x30;
- strtemp[2] = ((temperature/10)) 0x30;
- strtemp[4] = (temperature) 0x30;
- }
- else //For other temperatures
- {
- strtemp[0] = ' ';
- strtemp[1] = (temperature/100) 0x30;
- strtemp[2] = ((temperature/10)) 0x30;
- strtemp[4] = (temperature) 0x30;
- }
- // There is no need to display negative temperatures
- display.setCursor(5,1);
- display.write(strtemp); //Display the current temperature
- sensor.ConversionStart();
- }
- ////////////////////////////////////////////////////////////////////////////////
- void display_position (void)
- {
- //Position is calculated as average position of cold and hot valves
- //Division by 20 is required to convert steps in percents
- position = (motor_cold_pos motor_hot_pos)/2/20;
- if (position == 100)
- {
- strpos[0] = (position/100) 0x30;
- strpos[1] = ((position/10)) 0x30;
- }
- else
- {
- strpos[0] = ' ';
- strpos[1] = (position/10) 0x30;
- }
- strpos[2] = (position) 0x30;
- display.setCursor(13,1);
- display.write(strpos); //Display the current position
- }
- ////////////////////////////////////////////////////////////////////////////////
- void read_setpoints (void)
- {
- ADC10CTL0 |= ENC ADC10SC; //Start of ADC conversion
- while (ADC10CTL1 & ADC10BUSY); //Wating for result
- set_temperature = ADC10MEM; //Save the result
- ADC10CTL0 &= ~ENC; //Reset ENC bit to allow changing channel
- ADC10CTL1 ^= INCH0; //Select channel 1
- __delay_cycles (100); //Delay to swicth between channels
- ADC10CTL0 |= ENC ADC10SC; //Start of ADC conversion
- while (ADC10CTL1 & ADC10BUSY); //Wating for result
- set_position = ADC10MEM; //Save the result
- ADC10CTL0 &= ~ENC; //Reset ENC bit to allow changing channel
- ADC10CTL1 ^= INCH0; //Select channel 1
- set_temperature = ((((1023-set_temperature)*500)/1023)/5)*5 100; //Calculate set temperature in centigrades with 0.5 degrees resolution
- set_position = ((1023-set_position)*100)/1023; //Calculate set position in percents
- /*set_motor_hot_pos = (set_position)*40;
- set_motor_cold_pos = (set_position)*40;*/
- //Display set values (is implementd the same as for current values except they are displayed in the line 2)
- if (set_temperature > 1000)
- {
- strtemp[0] = (set_temperature/1000) 0x30;
- strtemp[1] = ((set_temperature/100)) 0x30;
- strtemp[2] = ((set_temperature/10)) 0x30;
- strtemp[4] = (set_temperature) 0x30;
- }
- else
- {
- strtemp[0] = ' ';
- strtemp[1] = (set_temperature/100) 0x30;
- strtemp[2] = ((set_temperature/10)) 0x30;
- strtemp[4] = (set_temperature) 0x30;
- }
- if (set_position == 100)
- {
- strpos[0] = (set_position/100) 0x30;
- strpos[1] = ((set_position/10)) 0x30;
- }
- else
- {
- strpos[0] = ' ';
- strpos[1] = (set_position/10) 0x30;
- }
- strpos[2] = (set_position) 0x30;
- display.setCursor(5,2);
- display.write(strtemp); //Display the set temperature
- display.setCursor(13,2);
- display.write(strpos); //Display the set position
- }
- ////////////////////////////////////////////////////////////////////////////////
- void init (void)
- {
- WDTCTL = WDTPW WDTHOLD; // Stop watchdog timer to prevent time out reset
- __delay_cycles(1000000); //Delay 1 s
- //Basic clock module initialization
- DCOCTL = CALDCO_8MHZ; //DCO setting = 8MHz
- BCSCTL1 = CALBC1_8MHZ; //DCO setting = 8MHz
- BCSCTL3 |= LFXT1S1; //Set VLOCLK = 12 kHz as ACLK source
- BCSCTL2 |= DIVS_3; //Set SMCLK divider = 8 (SMCLK frequency = 8 MHz / 8 = 1 MHz)
- while (~IFG1&OFIFG) //While oscollator fault flag is set
- {
- IFG1 &= OFIFG; //Reset oscillation fault flag
- __delay_cycles(8000); //Delay 1 ms
- }
- //I/O initialization
- P2REN |= BUTTON_PIN; //Switch on internal resistor
- P2OUT |= BUTTON_PIN; //Setup resistor as pull-up
- P2IES = BUTTON_PIN; //Allow high-to-low transition interrupt
- P2IE = BUTTON_PIN; //Enable external interrupts
- //Timer 0 initialization
- TA0CTL = TASSEL_2 MC_1 ID_3 TAIE;//Set timer 0 frequency f_timer0 = 1 MHz/8 = 125 kHz and enable timer overflow interrupt;
- TA0CCR0 = 25000; //Set timer 0 period T_timer = 0.2 sec (25000/125000 = 0.2 s)
- //ADC initialization
- ADC10CTL0 = ADC10SR ADC10ON; //VREF = VCC; ADC ON
- ADC10CTL1 = ADC10SSEL1 ADC10DIV_7; //Channel 0 (R2), clock source ADC10SC (~5MHz), clock divider = 8
- ADC10AE0 = BIT0 BIT1; //Configure P1.0 and P1.1 as analog inputs
- //Display initialization
- display.begin(); //Start display
- display.setCursor(1,1); //Set cursor to (1,1) position
- display.write("Press the button");//and write text "Press teh button"
- display.setCursor(1,2); //The same with
- display.write("Set:");//the second line
- //Configure DS18B20 sensor
- sensor.SetResolution(9); //Set DS18B20 resolution as 9 bits (0.5 centigrades)
- sensor.ConversionStart();
- //Enable motor driver
- motor.begin(); //Start motor driver
- motor.enable(); //Enable motor driver
- motor.stop(HOT); //Stop both motors
- motor.stop(COLD);
- //Variables initialization
- read_setpoints(); //Read and display the setpoints
- on_off = 0; //The device is off initially
- correct_rate = 0; //The rate is incorrect initially
- //Enable interrupts
- __bis_SR_register (GIE); //Enable interrupts
- }
- ////////////////////////////////////////////////////////////////////////////////
- #pragma vector=TIMER0_A1_VECTOR//Timer 0 overflow interrupt vector
- {
- read_setpoints(); //The setpoints are read and displayed in any mode
- if (on_off) //If the device is on
- {
- read_temperature(); //Read and display current temperatue
- display_position(); //Read and display current position of valves
- delta = temperature - set_temperature;//Calculate the error
- //Calculate control impact for hot valve by means of PI law
- set_motor_hot_pos = set_motor_hot_pos_prev - GAIN_T * (delta - delta_prev) - int32_t(GAIN_T * STEP * delta / TI_T);
- //Limit the values of control impact for hot valve
- if (set_motor_hot_pos <>
- set_motor_hot_pos = MOTOR_MIN;
- if (set_motor_hot_pos > set_position*2*20)
- set_motor_hot_pos = set_position*2*20;
- //Save current values for the next step
- delta_prev = delta;
- set_motor_hot_pos_prev = set_motor_hot_pos;
- //Set the setpoint for the cold motor and calculate the rate between hot and cold valves positions
- set_motor_cold_pos = set_position*20*2 - set_motor_hot_pos;
- //Limit the values of control impact for cold valve
- if (set_motor_cold_pos <>
- set_motor_cold_pos = MOTOR_MIN;
- if (set_motor_cold_pos > set_position*2*20)
- set_motor_cold_pos = set_position*2*20;
- }
- TA0CTL &= ~TAIFG; //Clear interrupt flag
- }
- ////////////////////////////////////////////////////////////////////////////////
- #pragma vector=PORT2_VECTOR//Port2 change interrupt vector
- __interrupt void PORT2_ISR (void)//Interrupt subroutine
- {
- uint16_t count_10ms = 0;
- if (~P2IN & BUTTON_PIN) //If button is pressed
- {
- __delay_cycles(160000); //Debounce delay
- if (~P2IN & BUTTON_PIN)
- {
- while (~P2IN & BUTTON_PIN)//While button is pressed
- {
- count_10ms ; //Increment the counter
- __delay_cycles(80000); //Delay for 10 ms
- if (!on_off) //If device is off
- {
- if (count_10ms > 100) //If duration of pressing is more than 1 second
- {
- motor.enable(); //Enable motor driver
- motor.step(COLD,CLOSE);//perform step in close direction for "cold" motor
- motor.step(HOT,CLOSE);//perform step in close direction for "hot" motor
- }
- }
- }
- motor.stop(COLD); //Stop "cold" motor
- motor.stop(HOT); //Stop "hot" motor
- motor.reset(); //Reset the motor driver
- __delay_cycles(160000); //Debounce delay
- if (P2IN & BUTTON_PIN) //If button is released
- {
- if (count_10ms < 100)="" if="" there="" was="" a="" short="">
- {
- if (on_off) //If motor was on
- {
- display.setCursor(1,1);
- display.write("Press the button");
- correct_rate = 0;
- }
- else //If motor was off
- {
- display.setCursor(1,1);//Set cursor to (1,1) position
- display.write("Cur:");//and write text "Cur:"
- motor.enable(); //Enable motor driver
- }
- on_off ^= 1; //Toggle the state of the device
- }
- }
- }
- }
- P2IFG &= ~BUTTON_PIN; //Clear interrupt flags
- }
- ////////////////////////////////////////////////////////////////////////////////
- int main( void )
- {
- init(); //Init all peripherals and modules
- while (1)
- {
- if (on_off) //If device is on
- {
- //If the difference between set and current values is more than thershold
- //then perform one step and calculate the current position of the valve
- if (set_motor_cold_pos - motor_cold_pos > 10)
- {
- if (motor_cold_pos <>
- {
- motor.step(COLD,OPEN);
- motor_cold_pos ;
- }
- }
- else if (motor_cold_pos - set_motor_cold_pos > 10)
- {
- if (motor_cold_pos > MOTOR_MIN)
- {
- motor.step(COLD,CLOSE);
- motor_cold_pos --;
- }
- }
- if (set_motor_hot_pos - motor_hot_pos > 10)
- {
- if (motor_hot_pos <>
- {
- motor.step(HOT,OPEN);
- motor_hot_pos ;
- }
- }
- else if (motor_hot_pos - set_motor_hot_pos > 10)
- {
- if (motor_hot_pos > MOTOR_MIN)
- {
- motor.step(HOT,CLOSE);
- motor_hot_pos --;
- }
- }
- }
- else //If device is off
- {
- if (motor_cold_pos > 0) //Close cold valve if its position is above 0
- {
- motor.step(COLD,CLOSE);
- motor_cold_pos --;
- }
- else //Then stop the valve
- {
- motor.stop(COLD);
- }
- //The same with the hot valve
- if (motor_hot_pos > 0)
- {
- motor.step(HOT,CLOSE);
- motor_hot_pos --;
- }
- else
- {
- motor.stop(HOT);
- }
- if ((motor_cold_pos == 0) && (motor_hot_pos == 0))
- {
- motor.reset();
- }
- }
- }
- }