September 8, 2021

Closed Loop Espresso Part 2: Firmware Estimation and Control

I've made a lot of progress on the software and control side of the espresso machine, and learned some good lessons about designing the real version of the machine.

This is the first post in a series about the firmware/software, more to come soon.

To "spill the beans", as people at work say, here's a demo of the current state of the machine:


Here's what's going on while making the espresso in the video:
  • After the "Start" button is pressed, water is pumped through the heater and back into the tank at a constant flow rate, and the water/group heaters start heating.
  • Once both temperatures have converged to their setpoint, flow is switched from from the tank to the group.  At first, the water is flushed through the group to the drip tray, to purge air from the group.
  • Once the air is purged, the valve to the drip tray closes and "preinfusion" starts.  The group is filled up at a constant flow rate.
  • Once the desired shot pressure is reached (indicating the group is full and puck is saturated with water), the machine switches over to pressure control, and holds a constant pressure for the remainder of the shot.
  • Once the desired output weight is reached (for now estimated by integrating a pump displacement/leak model), flow to the group is controlled to zero and excess pressure is vented to the drip tray, so the drips through the puck stop quickly.
On to the subject of this post, firmware-level estimation and control

Flow Estimation
Originally I was planning on finding a flow sensor, but so far haven't found a sensor that's both affordable and any good - the impeller type sensors for all-in-one coffee machines have pretty terrible accuracy.  Fortunately I have enough instrumentation to estimate the flow pretty well.  Here's the basic model I'm using for flow estimation:



There's an "ideal" pump with constant displacement \(D\), spinning at angular velocity \(\dot{\theta}\), with the flow characteristic:

\[F_{ideal} = \dot{\theta} D\]

There's leak path between the outlet and inlet of the pump, with a nonlinear resistance which behaves quadratically.  The pressure, \(\Delta P\) across the resistor is:

\[\Delta P = C_{1}F_{leak} + C_{2}F_{leak}^{2}\]

Solving the quadratic equation:

\[F_{leak} = \frac{-C_{1} + \sqrt{C_{1}^{2} + 4C_{2}\Delta P}}{2 C_{2}}\]

\(\Delta P\) and \(\dot{\theta}\) are both measured, and the output flow is estimated by just adding up \(F_{ideal}\) and \(F_{leak}\).

This is (of course) in SI units, so \(\dot{\theta}\) is in cubic meters, \(D\) is in cubic meters per radian, \(\Delta P\) is in Pascals, and all the flows are in cubic meters per second.

Pump Characterization

The pump displacement is listed on the datasheet, and one could probably get a decent guess at the leak coefficients by squinting at the pressure/flow curves in the datasheet, but I measured these parameters in-place.

I measured the pump displacement by just pumping water into a cup on a scale, and measuring the weight in the cup vs pump angle.  Averaging over a minute of pumping, I got a result that was nearly identical to the datasheet value (hooray!).

I measured the leak coefficients \(C_{1}\) and \(C_{2}\) by blocking off the output of the pump with the needle valve, and varying the speed of the pump.  Blocking the output sets \(F_{leak} = F_{ideal}\), so the coefficients are found by fitting a curve to \(\Delta P\) vs \(\dot{\theta}\).

Looks pretty quadratic to me:

Flow Control

Flow control falls right out of the flow equations above.  Knowing \(\Delta P\) across the pump and have a desired flow rate \(F_{des}\), the desired pump velocity \(\dot{\theta}_{des}\) is set to:
\[\dot{\theta}_{des} = \frac{F_{des} + F_{leak}}{D}\]
Then the closed-loop velocity control is handled on the motor drive.  

Pressure Control

Pressure control was a bit more involved.  I started out by measuring the pressure frequency response of the system with the puck simulator installed and blocked off for zero flow, by applying a chirp torque signal and measuring the resulting pressure

The time series data can be turned into a bode plot by taking the ratios of the FFT's of the input and output.  I've fit a 2nd order model to it, which is what I'd expect with the output blocked - the dynamics should be dominated by the inertia of the pump motor and magnetic coupling, and the spring constant of the magnetic coupling + fluid.  When the output isn't blocked off, a third pole shows up, but that didn't matter too much for designing the controller.


I designed a pressure controller by loop shaping a PI + lead controller.  I was able to push it to ~90 rad/s (14.3 hz crossover) on the hardware before things got crunchy.


Here's the closed-loop step response (compared to the simulated step response given the 2nd order fit of the dynamics and the controller above).  There's a little periodic wiggle in the pressure (I think from the teeth of the gear pump), but overall it looks pretty good.



And here's the measured vs expected closed-loop frequency response, measured the same was as the open-loop frequency response with a chirp input:



Temperature Control

As boring as temperature control is, it turned out to be a real pain.  This prototype hardware (due to being thrown together out of mostly off-the-shelf sensors/fittings/etc) has the classic problem of non-collocated actuators and sensors.  Basically, the temperature sensors are relatively far away from the heaters, so there's significant dynamics between the heaters and sensors.  That, coupled with relatively slow time constants meant testing was very time consuming.  I frequently found myself filling up the water tank with ice cubes to cool things down faster.

There were a couple mildly interesting issues I ran into though.  One had to do with the SSR's I'm using to turn on and off the heating elements.  Typical SSR's only turn on and off at mains zero-crossings.  I just stuffed some PWM on the input to the SSR, in the hopes that the probability of the input to the SSR being high at the zero crossing would be equal to the PWM duty cycle.  Depending on the PWM frequency, this could work out.  But due to some timer constraints with the clock configuration for USB and CAN, my PWM frequency ended up evenly dividing into 60 Hz.  As a result, the PWM rising had a consistent alignment with the mains zero crossing, causing the SSR to turn on much more often than I expected given the PWM duty cycle.  Rather than trying to reconfigure the timers, I implemented software PWM so I could make the PWM period much larger than 1/60th of a second, to avoid this issue.

The other interesting issue I ran into had to do with my crappy hacked-together water heater design.  Here's a cartoon of (roughly) what happens in the water heater.  Water flows in at some inlet temperature, and out (hopefully) at the desired temperature.  Assuming the heater itself is a constant temperature, which is probably not an unreasonable assumption if it's highly thermally conductive, then the average water outlet temperature exponentially approaches the heater temperature throughout the heater.
  

Average is the key word.  As you can see from the cartoon plot, depending on the length of the heater and the convection coefficient between the heater wall and the water, the heater temperature could potentially be much higher than the outlet temperature.  And in reality, at a given cross section of the water flowing through the heater, the entire cross section will not be at the average temperature.  The water towards the walls of the heater will be hotter than the average, and the water in the middle of the cross section will be cooler than average.

For making espresso, the desired output temperature is typically somewhere in the low-90's C, which is awfully close to water's boiling temperature under ambient pressure.  The larger the difference between the heater temperature and the average outlet temperature, the larger the temperature gradient across the cross section of the water flow - so with a bad heater design (ahem) where the heater needs to be significantly hotter than the outlet, the water at the perimeter of the flow cross section can actually start boiling, even though the average water temperature is below boiling.  This results in the machine spluttering as it cycles water back to the tank while preheating, and introduces (highly compressible) steam into the group when the shot is happening, which affects the pressure control.

This issues seems solvable, and I think with a fresh heater and group design I can also significantly decrease the thermal mass and improve the temperature control bandwidth.  

This is getting long, so I'll pick up next time with a software post.