[Solved] Espresso Pump Modulation via Phase Angle Control with Arduino

I am building a controller for a Gaggia Coffee espresso machine. I have the PID temperature controller working fine but I am having issues with the pressure control section and would like to get another set of eyes on what I am doing.

I am using phase angle control to modulate the pressure. I’m taking the idea from espresso-for-geeks.kalaf.net and trying to make it work on the Arduino. There is a diode on the 115V pump so only half of the AC cycle is performing work.

I have a zero cross detection circuit built with a HCPL-817 which is powered on by a AQH3223 SSR. Pin 12 (zeroOnPin) drives the SSR and pin 2 (zeroInputPin) is the input for the circuit to the Arduino.

There is an interrupt when the state of zeroOnPin changes which runs a function that first reads the state of zeroOnPin. If the state is 0 then it is the start of the work half period, there is interrupt timer set up TimerOne.h to delay a set amount of time determined by ‘level’ then the pump SSR is turned on. If the state of zeroOnPin is 1 then it is the start of the non-work half period and the pump is turned off in the middle of the half period to avoid voltage spikes.

The problem is that no matter where I set the power on delay with the level setting, I get the same output from the pump, except for 0 which turns the pump off. So, 1-100 seems to be full power and 0 is off.

I have tried switching the 0 and 1 arguments against the zeroInputPin in PumpChange() as I don’t really know which side of the wave does the work to the same result.

I have inserted serial prints in the PumpStart() and PumpStop() functions and I can see that the commands are being run after each other but I can’t tell how long each is on based on the serial output, below is the output. I have added prints for ‘timeout_usec’ and I can see that the variable is changing with different ‘levels’.

I tried entering serial prints with micros() commands in the PumpStart() and PumpStop() functions but that slows the program down.

Can anyone see a flaw in the code? If not do you have any suggestions on how to tell the timing of the PumpStart() and PumpStop() functions so I can diagnose further? I’m out of ideas.

BOM:
Adafruit Metro Mini

IRM-03-05 5v, 600mA AC/DC converter
https://www.digikey.com/en/products/detail/mean-well-usa-inc/IRM-03-5/7704640

AQH3223 Random Fire SSR
https://www.digikey.com/en/products/detail/panasonic-electric-works/AQH3223/2125651

HCPL-817-000E Optoisolator
https://www.digikey.com/en/products/detail/broadcom-limited/HCPL-817-000E/768370

7k resistor
https://www.digikey.com/en/products/detail/vishay-dale/ALSR057K000JE12/257455

1N4007 diode

Arduino Code

#include <Bounce2.h>
#include <TimerOne.h>

#define pumpPin 5
#define zeroInputPin 2
#define brewPin 8
#define zeroOnPin 12

Bounce BrewSwitch = Bounce();

int pumpRunning = 0;

// estimate of lag caused by ZCD circuit in usec
// measured as per http://espresso-for-geeks.kalaf.net/mod-list/#hw-zero-cross
#define ZCD_DELAY 36

// power frequency half-period in usec
#define HALF_PERIOD_USEC 8333

// the time in usec to wait after zero-cross before switching off
// this eliminates the back-EMF by waiting for current to drop before switching off
#define DELAY_AFTER_STOP 4166

// These are precalculated values (usec) of the required phase delays for 0 to 100% power.
// Equation used is: 1000000 usec / 120 * (acos(2*x/100 - 1) / pi)
const uint16_t timeouts_usec[101] =
{ 0, 531, 753, 924, 1068, 1196, 1313, 1421, 1521, 1616,
  1707, 1793, 1877, 1957, 2035, 2110, 2183, 2255, 2324,
  2393, 2460, 2525, 2590, 2654, 2716, 2778, 2839, 2899,
  2958, 3017, 3075, 3133, 3190, 3246, 3303, 3358, 3414,
  3469, 3524, 3578, 3633, 3687, 3740, 3794, 3848, 3901,
  3954, 4007, 4061, 4114, 4167, 4220, 4273, 4326, 4379,
  4432, 4486, 4539, 4593, 4647, 4701, 4755, 4810, 4864,
  4919, 4975, 5031, 5087, 5144, 5201, 5258, 5316, 5375,
  5435, 5495, 5556, 5617, 5680, 5743, 5808, 5874, 5941,
  6009, 6079, 6150, 6223, 6299, 6376, 6457, 6540, 6626,
  6717, 6812, 6913, 7020, 7137, 7265, 7410, 7581, 7802,
  8333
};

int level = 95; //5
uint16_t timeout_usec = HALF_PERIOD_USEC - timeouts_usec[level] - ZCD_DELAY;

void setup() {
  Serial.begin(9600);

  BrewSwitch.attach(brewPin, INPUT_PULLUP);
  BrewSwitch.interval(25);

  pinMode (pumpPin, OUTPUT);

  pinMode (zeroOnPin, OUTPUT);
  digitalWrite(zeroOnPin, LOW);
  pinMode(zeroInputPin, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(zeroInputPin), PumpChange, CHANGE);
  Timer1.initialize();
}

void loop() {
  checkSwitch();
}

void PumpStart() {
  if (pumpRunning == 1) {
    digitalWrite(pumpPin, HIGH);
    //   Serial.println("on");
    Timer1.stop();
  }
}

void PumpStop() {
  digitalWrite(pumpPin, LOW);
  Timer1.stop();
  //    Serial.println("off");
}

void PumpChange() {
  if (digitalRead(zeroInputPin) == 0) { 
    if (level == 100) PumpStart();
    else if (level == 0) PumpStop();
    else {
      Timer1.setPeriod(timeout_usec);
      Timer1.attachInterrupt(PumpStart);
      Timer1.start();
    }
  }

  if (digitalRead(zeroInputPin) == 1) {
    Timer1.setPeriod(DELAY_AFTER_STOP);
    Timer1.attachInterrupt(PumpStop);
    Timer1.start();
  }
}

void checkSwitch() {
  BrewSwitch.update();
  if ( BrewSwitch.fell()) {
    digitalWrite(zeroOnPin, HIGH);
    pumpRunning = 1;
  }
  if ( BrewSwitch.rose()) {
    digitalWrite(pumpPin, LOW);
    digitalWrite(zeroOnPin, LOW);
    pumpRunning = 0;
  }
}

Serial Print PumpStart() and PumpStop() Output

15:51:39.842 -> off
15:51:39.842 -> on
15:51:39.842 -> off
15:51:39.842 -> on
15:51:39.842 -> off
15:51:39.842 -> on
15:51:39.889 -> off
15:51:39.889 -> on
15:51:39.889 -> off
15:51:39.889 -> on
15:51:39.889 -> off
15:51:39.937 -> on
15:51:39.937 -> off
15:51:39.937 -> on
15:51:39.937 -> off
15:51:39.937 -> on
15:51:39.937 -> off
15:51:39.983 -> on
15:51:39.983 -> off
15:51:39.983 -> on
15:51:39.983 -> off
15:51:39.983 -> on
15:51:39.983 -> off
15:51:40.029 -> on
15:51:40.029 -> off
15:51:40.029 -> on
15:51:40.029 -> off

Pressure control circuit diagram.pdf (781 KB)

It appears you are asking about controlling the triac. You did a nice block diagram but the details of the circuit are needed. A schematic would go a long way in solving the problem. You have zero cross, how did you get it. It appears you are firing the triac in probably third quadrant but that is just a guess. It states: "There is a diode on the 115V pump so only half of the AC cycle is performing work" that is not necessarily true a schematic could confirm that.

What other details of the circuit could help? The PDF has all of the components I added along with how they are wired together. I’m attaching a new version that has all the hanging wires going to components rather than just labeled.

As far as the diode in the pump I am getting that from other forums for this particular espresso machine and the link below. Unfortunately I don’t have access to an internal schematic of the pump to verify the claim.

The zero cross is coming from the HCPL-817, 7k resistor and 1N4007 section of my block diagram. The AQH3223 turns that section of the circuit on and off. Pin 4 of the HCPL-817 optocoupler. I put a red box around this part of the circuit on the diagram.

The SSR will fire at different locations of the first half of the AC cycle depending on the level setting. The code shows a level of 95 and that would be firing 12% through the first half of the AC cycle, it then shuts off at 50% in the second half of the AC cycle. At a level setting of 5 I am firing 85% through the first half of the AC cycle and it shuts off again at 50% in the second half of the AC cycle. For testing I am just updating the level and reuploading the code, eventually it will be a setting that can be changed with buttons.

Pressure control circuit diagram 2.pdf (1.28 MB)

For the optocoupler, you're using Rank 'O' (-000E) which has a minimum CTR of only 50%. Should probably use Rank 'C' (-00CE) for minimum CTR of 200%. Note they recommend an opto with at least 100% CTR. Example: HCPL-817-06CE

Your diagram doesn't show the critical pullup resistors on the transistor side of the optocouplers. See R2 in the Zero-cross detect circuit

Didn't check your code due to above.

dlloyd: For the optocoupler, you're using Rank 'O' (-000E) which has a minimum CTR of only 50%. Should probably use Rank 'C' (-00CE) for minimum CTR of 200%. Note they recommend an opto with at least 100% CTR. Example: HCPL-817-06CE

Your diagram doesn't show the critical pullup resistors on the transistor side of the optocouplers. See R2 in the Zero-cross detect circuit

Didn't check your code due to above.

Thank you, I missed the spec on the optocoupler. I'm going to place an order through Mouser soon for something Digikey doesn't have so I will get the below. As far as the R2 resistor, I have an internal pullup activated using the below. That should be sufficient right? https://www.mouser.com/ProductDetail/Broadcom-Avago/HCPL-817-00CE/?qs=s%252BgfxR24RQ7J0rhiKxuhPg%3D%3D

  pinMode(zeroInputPin, INPUT_PULLUP);

I think the internal pullup will be far too weak (30K-50K). This means the opto's transistor output will have a very slow rise time and might not even rise to the required level to detect digital high. It probably should be in the neighborhood of 4.7K to 10K.

dlloyd: I think the internal pullup will be far too weak (30K-50K). This means the opto's transistor output will have a very slow rise time and might not even rise to the required level to detect digital high. It probably should be in the neighborhood of 4.7K to 10K.

I modified the code to count the times the zero cross changes and pasted it below, I just pasted the code changes to make it smaller. It counts every time there is a cross so there should be 240 in 2 seconds. Starting at 11 to avoid any startup issues, 11 happened at 20:59:03.961 and 250 happened at 20:59:05.955 . They are 2 seconds apart so the zero cross seems to be working when ran by itself.

Are there any concerns that the zero cross could stop working when run in the full code because of the different optocoupler and lack of external pullup resistor?

It honestly feels more like the SSR pump driving the relay is sticking on. By putting serial print functions in them I can see the PumpStart() and PumpStop() functions being called every other print. It just doesn't seem like the SSR ever shuts off once it is on.

long counter = 0;

void setup(){
  attachInterrupt(digitalPinToInterrupt(zeroInputPin), zerocrossing, CHANGE);
}

void zerocrossing()
{
  ++counter;
  Serial.println(counter);
}
20:59:00.735 -> 1
20:59:03.868 -> 2
20:59:03.914 -> 3
20:59:03.914 -> 4
20:59:03.914 -> 5
20:59:03.914 -> 6
20:59:03.914 -> 7
20:59:03.914 -> 8
20:59:03.961 -> 9
20:59:03.961 -> 10
20:59:03.961 -> 11
20:59:03.961 -> 12
20:59:03.961 -> 13
20:59:03.961 -> 14
20:59:04.008 -> 15
20:59:04.008 -> 16
20:59:04.008 -> 17
20:59:04.008 -> 18
20:59:04.008 -> 19
20:59:04.054 -> 20
20:59:04.054 -> 21
20:59:04.054 -> 22
20:59:04.054 -> 23
20:59:04.054 -> 24
20:59:04.054 -> 25
20:59:04.100 -> 26
20:59:04.100 -> 27
20:59:04.100 -> 28
20:59:04.100 -> 29
20:59:04.100 -> 30
20:59:04.146 -> 31
20:59:04.146 -> 32
20:59:04.146 -> 33
20:59:04.146 -> 34
20:59:04.146 -> 35
20:59:04.146 -> 36
20:59:04.193 -> 37
20:59:04.193 -> 38
20:59:04.193 -> 39
20:59:04.193 -> 40
20:59:04.193 -> 41
20:59:04.239 -> 42
20:59:04.239 -> 43
20:59:04.239 -> 44
20:59:04.239 -> 45
20:59:04.239 -> 46
20:59:04.239 -> 47
20:59:04.285 -> 48
20:59:04.285 -> 49
20:59:04.285 -> 50
20:59:04.285 -> 51
20:59:04.285 -> 52
20:59:04.285 -> 53
20:59:04.332 -> 54
20:59:04.332 -> 55
20:59:04.332 -> 56
20:59:04.332 -> 57
20:59:04.332 -> 58
20:59:04.377 -> 59
20:59:04.377 -> 60
20:59:04.377 -> 61
20:59:04.377 -> 62
20:59:04.377 -> 63
20:59:04.377 -> 64
20:59:04.424 -> 65
20:59:04.424 -> 66
20:59:04.424 -> 67
20:59:04.424 -> 68
20:59:04.424 -> 69
20:59:04.471 -> 70
20:59:04.471 -> 71
20:59:04.471 -> 72
20:59:04.471 -> 73
20:59:04.471 -> 74
20:59:04.471 -> 75
20:59:04.516 -> 76
20:59:04.516 -> 77
20:59:04.516 -> 78
20:59:04.516 -> 79
20:59:04.516 -> 80
20:59:04.564 -> 81
20:59:04.564 -> 82
20:59:04.564 -> 83
20:59:04.564 -> 84
20:59:04.564 -> 85
20:59:04.564 -> 86
20:59:04.610 -> 87
20:59:04.610 -> 88
20:59:04.610 -> 89
20:59:04.610 -> 90
20:59:04.610 -> 91
20:59:04.610 -> 92
20:59:04.657 -> 93
20:59:04.657 -> 94
20:59:04.657 -> 95
20:59:04.657 -> 96
20:59:04.657 -> 97
20:59:04.704 -> 98
20:59:04.704 -> 99
20:59:04.704 -> 100
20:59:04.704 -> 101
20:59:04.704 -> 102
20:59:04.704 -> 103
20:59:04.751 -> 104
20:59:04.751 -> 105
20:59:04.751 -> 106
20:59:04.751 -> 107
20:59:04.751 -> 108
20:59:04.798 -> 109
20:59:04.798 -> 110
20:59:04.798 -> 111
20:59:04.798 -> 112
20:59:04.798 -> 113
20:59:04.798 -> 114
20:59:04.844 -> 115
20:59:04.844 -> 116
20:59:04.844 -> 117
20:59:04.844 -> 118
20:59:04.844 -> 119
20:59:04.844 -> 120
20:59:04.891 -> 121
20:59:04.891 -> 122
20:59:04.891 -> 123
20:59:04.891 -> 124
20:59:04.891 -> 125
20:59:04.936 -> 126
20:59:04.936 -> 127
20:59:04.936 -> 128
20:59:04.936 -> 129
20:59:04.936 -> 130
20:59:04.936 -> 131
20:59:04.982 -> 132
20:59:04.982 -> 133
20:59:04.982 -> 134
20:59:04.982 -> 135
20:59:04.982 -> 136
20:59:05.028 -> 137
20:59:05.028 -> 138
20:59:05.028 -> 139
20:59:05.028 -> 140
20:59:05.028 -> 141
20:59:05.028 -> 142
20:59:05.075 -> 143
20:59:05.075 -> 144
20:59:05.075 -> 145
20:59:05.075 -> 146
20:59:05.075 -> 147
20:59:05.119 -> 148
20:59:05.119 -> 149
20:59:05.119 -> 150
20:59:05.119 -> 151
20:59:05.119 -> 152
20:59:05.119 -> 153
20:59:05.166 -> 154
20:59:05.166 -> 155
20:59:05.166 -> 156
20:59:05.166 -> 157
20:59:05.166 -> 158
20:59:05.212 -> 159
20:59:05.212 -> 160
20:59:05.212 -> 161
20:59:05.212 -> 162
20:59:05.212 -> 163
20:59:05.212 -> 164
20:59:05.257 -> 165
20:59:05.257 -> 166
20:59:05.257 -> 167
20:59:05.257 -> 168
20:59:05.257 -> 169
20:59:05.304 -> 170
20:59:05.304 -> 171
20:59:05.304 -> 172
20:59:05.304 -> 173
20:59:05.304 -> 174
20:59:05.304 -> 175
20:59:05.350 -> 176
20:59:05.350 -> 177
20:59:05.350 -> 178
20:59:05.350 -> 179
20:59:05.350 -> 180
20:59:05.396 -> 181
20:59:05.396 -> 182
20:59:05.396 -> 183
20:59:05.396 -> 184
20:59:05.396 -> 185
20:59:05.396 -> 186
20:59:05.444 -> 187
20:59:05.444 -> 188
20:59:05.444 -> 189
20:59:05.444 -> 190
20:59:05.444 -> 191
20:59:05.444 -> 192
20:59:05.490 -> 193
20:59:05.490 -> 194
20:59:05.490 -> 195
20:59:05.490 -> 196
20:59:05.490 -> 197
20:59:05.537 -> 198
20:59:05.537 -> 199
20:59:05.537 -> 200
20:59:05.537 -> 201
20:59:05.537 -> 202
20:59:05.537 -> 203
20:59:05.584 -> 204
20:59:05.584 -> 205
20:59:05.584 -> 206
20:59:05.584 -> 207
20:59:05.584 -> 208
20:59:05.629 -> 209
20:59:05.629 -> 210
20:59:05.629 -> 211
20:59:05.629 -> 212
20:59:05.629 -> 213
20:59:05.629 -> 214
20:59:05.676 -> 215
20:59:05.676 -> 216
20:59:05.676 -> 217
20:59:05.676 -> 218
20:59:05.676 -> 219
20:59:05.676 -> 220
20:59:05.723 -> 221
20:59:05.723 -> 222
20:59:05.723 -> 223
20:59:05.723 -> 224
20:59:05.723 -> 225
20:59:05.770 -> 226
20:59:05.770 -> 227
20:59:05.770 -> 228
20:59:05.770 -> 229
20:59:05.770 -> 230
20:59:05.770 -> 231
20:59:05.817 -> 232
20:59:05.817 -> 233
20:59:05.817 -> 234
20:59:05.817 -> 235
20:59:05.817 -> 236
20:59:05.863 -> 237
20:59:05.863 -> 238
20:59:05.863 -> 239
20:59:05.863 -> 240
20:59:05.863 -> 241
20:59:05.863 -> 242
20:59:05.908 -> 243
20:59:05.908 -> 244
20:59:05.908 -> 245
20:59:05.908 -> 246
20:59:05.908 -> 247
20:59:05.955 -> 248
20:59:05.955 -> 249
20:59:05.955 -> 250
20:59:05.955 -> 251
20:59:05.955 -> 252
20:59:05.955 -> 253
20:59:06.003 -> 254
20:59:06.003 -> 255
20:59:06.003 -> 256
20:59:06.003 -> 257
20:59:06.003 -> 258
20:59:06.003 -> 259
20:59:06.048 -> 260
20:59:06.048 -> 261
20:59:06.048 -> 262
20:59:06.048 -> 263
20:59:06.048 -> 264
20:59:06.094 -> 265
20:59:06.094 -> 266
20:59:06.094 -> 267
20:59:06.094 -> 268
20:59:06.094 -> 269
20:59:06.094 -> 270
20:59:06.140 -> 271
20:59:06.140 -> 272
20:59:06.140 -> 273
20:59:06.140 -> 274
20:59:06.140 -> 275
20:59:06.186 -> 276
20:59:06.186 -> 277
20:59:06.186 -> 278
20:59:06.186 -> 279
20:59:06.186 -> 280
20:59:06.186 -> 281
20:59:06.233 -> 282
20:59:06.233 -> 283
20:59:06.233 -> 284
20:59:06.233 -> 285
20:59:06.233 -> 286
20:59:06.279 -> 287
attachInterrupt(digitalPinToInterrupt(zeroInputPin), zerocrossing, CHANGE);

This doesn't seem right ... you'll get 2 interrupts per 1/2 cycle. Doesn't the timer handle the other 1/2 cycle? Note that FALLING would happen once per cycle, near the zero-crossing.

Anyways, it would be good to see what the opto transistor signal looks like. How about checking what it looks like on the serial plotter If you change the delay here to 50µs, you'll be able to see about 3-cycles (500 samples). It would be easier if you had another Arduino as it wouldn't interfere with your code, but its possible to combine.

dlloyd:

attachInterrupt(digitalPinToInterrupt(zeroInputPin), zerocrossing, CHANGE);

This doesn’t seem right … you’ll get 2 interrupts per 1/2 cycle. Doesn’t the timer handle the other 1/2 cycle? Note that FALLING would happen once per cycle, near the zero-crossing.

Anyways, it would be good to see what the opto transistor signal looks like. How about checking what it looks like on the serial plotter If you change the delay here to 50µs, you’ll be able to see about 3-cycles (500 samples). It would be easier if you had another Arduino as it wouldn’t interfere with your code, but its possible to combine.

The Arduino oscilloscope is a really cool suggestion. I could grab another Arduino from a friend if needed. I just ran that code on the project Arduino and got the output attached. I’m also posting the modified code I used as the output seems to be pointing to a problem that I don’t understand.

I plotted the points assuming each point was 50 uSec apart. One cycle is 16,667 uSec but I seem to be getting 3 peaks in that time, so something is off there. Could that be caused by the wrong optocoupler? From my understanding the lack of an external pullup would make a messy signal, not a signal with the wrong frequency.

I was going to run it again with the pump running to see if that changed anything but I don’t feel good about the base results unless I’m doing something wrong or misunderstand the output.

//Check zero cross with internal oscilloscope

#define brewPin 8
#define zeroOnPin 12
#define oscilPin A1
#define zeroInputPin 2

int runOscil = 0;

const word readings = 500;
word capture[readings];
word previousReading = 0;
int readDelay = 50;


void setup() {
  Serial.begin(115200);
  pinMode(brewPin, INPUT_PULLUP);
  pinMode(zeroInputPin, INPUT_PULLUP);
  pinMode (zeroOnPin, OUTPUT);
  digitalWrite(zeroOnPin, HIGH);
}

void loop() {
//  checkSwitch();

  if (analogRead(oscilPin) < previousReading) { // check if oscilPin signal is falling
    if (!(digitalRead(brewPin))) {       // check if plot update is requested
      for (int i = 0; i < readings; i++) {     // record the readings
        capture[i] = analogRead(oscilPin);
        delayMicroseconds(readDelay);
      }
      for (int i = 0; i < readings; i++) {     // print the readings
        Serial.println(capture[i]);
        delayMicroseconds(readDelay);
      }
    }
  }
  previousReading = analogRead(oscilPin);
  delayMicroseconds(readDelay);


}

As far as CHANGE vs FALLING. I am using CHANGE for the interrupt and then the code below to see if it is on the falling or rising side of the change, so the CHANGE happens twice per cycle but the on or off side is only once. It’s my first time doing this and I’m making parts up as I go along so if there is a better way I could definitely fix it. I also thought about having the PumpOn Timer1 cascade into another that would change the attachInterupt and setPeriod values but thought this was simpler.

void PumpChange() {
  if (digitalRead(zeroInputPin) == 0) { 
    if (level == 100) PumpStart();
    else if (level == 0) PumpStop();
    else {
      Timer1.setPeriod(timeout_usec);
      Timer1.attachInterrupt(PumpStart);
      Timer1.start();
    }
  }

  if (digitalRead(zeroInputPin) == 1) {
    Timer1.setPeriod(DELAY_AFTER_STOP);
    Timer1.attachInterrupt(PumpStop);
    Timer1.start();
  }
}

Opto Chart.png

Oscillation Output Text.txt (60 KB)

Your signal has full swing (0 to > 1000) with a little bit of rise time obvious by the rounding near the top. This'll be more obvious if the signal was stretched to 1 cycle or even more so if stretched to 1/4 cycle. It looks like the opto is working fine.

The time per sample is approximately analogRead() time + readDelay = 100µs + 50µs. So for 500 readings, it takes 75,000µs which is 4.5 cycles.

If you make readDelay = 0, then 500 readings will take about 50,000µs = 3 cycles.

That's as fast as the analogRead function works, but it's set to a very conservative prescaler of 128. Below is a function you could add (analogReadFast) where the prescaler is set to 32. This means analogReadFast only takes about 26µs.

With readDelay at 0, then 500 readings using analogReadFast will take about 13,000µs = 78% of a cycle.

int analogReadFast(int oscilPin) {
  byte ADCregOriginal = ADCSRA;
  ADCSRA = (ADCSRA & B11111000) | 5; //32 prescaler
  int adc = analogRead(oscilPin);
  ADCSRA = ADCregOriginal;
  return adc;
}

Thank you for the clarification. I solved for one cycle to get to 160 uSec between each reading. I’m going to throw that loop in my regular code and set a flag rather than a pin with the PumpOn() and PumpOff() functions. I can see the ratio of how much time they are off and on and the timing that way.

I plotted the output from a flag set in the PumpOn() and PumpOff() functions instead of driving the pin. It is showing about 1/4 second off and 1/4 second on. This doesn’t reflect what is happening at the pump though, that is behaving like it is just on. It behaves the exact same and has the same spacing for level 5, 50, 75 and 95.

//Pressure control only code, output to flag instead, check flag with internal oscilloscope

#include <Bounce2.h>
#include <TimerOne.h>

#define pumpPin 5   //Wired to 5
#define zeroInputPin 2
#define brewPin 8
#define steamPin 7
#define zeroOnPin 12

Bounce BrewSwitch = Bounce();

int pumpRunning = 0;
int pumpFire=0;

// estimate of lag caused by ZCD circuit in usec
// measured as per http://espresso-for-geeks.kalaf.net/mod-list/#hw-zero-cross
#define ZCD_DELAY 36

// power frequency half-period in usec
#define HALF_PERIOD_USEC 8333

// the time in usec to wait after zero-cross before switching off
// this eliminates the back-EMF by waiting for current to drop before switching off
#define DELAY_AFTER_STOP 4166

// These are precalculated values (usec) of the required phase delays for 0 to 100% power.
// Equation used is: 1000000 usec / 120 * (acos(2*x/100 - 1) / pi)
const uint16_t timeouts_usec[101] =
{ 0, 531, 753, 924, 1068, 1196, 1313, 1421, 1521, 1616,
  1707, 1793, 1877, 1957, 2035, 2110, 2183, 2255, 2324,
  2393, 2460, 2525, 2590, 2654, 2716, 2778, 2839, 2899,
  2958, 3017, 3075, 3133, 3190, 3246, 3303, 3358, 3414,
  3469, 3524, 3578, 3633, 3687, 3740, 3794, 3848, 3901,
  3954, 4007, 4061, 4114, 4167, 4220, 4273, 4326, 4379,
  4432, 4486, 4539, 4593, 4647, 4701, 4755, 4810, 4864,
  4919, 4975, 5031, 5087, 5144, 5201, 5258, 5316, 5375,
  5435, 5495, 5556, 5617, 5680, 5743, 5808, 5874, 5941,
  6009, 6079, 6150, 6223, 6299, 6376, 6457, 6540, 6626,
  6717, 6812, 6913, 7020, 7137, 7265, 7410, 7581, 7802,
  8333
};

int level = 95; //5
uint16_t timeout_usec = HALF_PERIOD_USEC - timeouts_usec[level] - ZCD_DELAY;

const word readings = 500;
word capture[readings];
word previousReading = 0;
int readDelay = 50;

void setup() {
  Serial.begin(115200);

  BrewSwitch.attach(brewPin, INPUT_PULLUP);
  BrewSwitch.interval(25);

  pinMode(steamPin, INPUT_PULLUP);

  pinMode (pumpPin, OUTPUT);

  pinMode (zeroOnPin, OUTPUT);
  digitalWrite(zeroOnPin, LOW);
  pinMode(zeroInputPin, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(zeroInputPin), PumpChange, CHANGE);
  Timer1.initialize();
}

void loop() {
  checkSwitch();
  oscil();
}

void oscil ()
{
//    if (pumpFire < previousReading) { // check if oscilPin signal is falling
    if (!(digitalRead(steamPin))) {       // check if plot update is requested
      for (int i = 0; i < readings; i++) {     // record the readings
        capture[i] = pumpFire;
        delayMicroseconds(readDelay);
      }
      for (int i = 0; i < readings; i++) {     // print the readings
        Serial.println(capture[i]);
        delayMicroseconds(readDelay);
//      }
    }
  }
  previousReading = pumpFire;
  delayMicroseconds(readDelay);
}

void PumpStart() {
  if (pumpRunning == 1) {
    digitalWrite(pumpPin, HIGH);
pumpFire=1;
    Timer1.stop();
  }
}

void PumpStop() {
  digitalWrite(pumpPin, LOW);
pumpFire=0;
  Timer1.stop();
}

void PumpChange() {
  if (digitalRead(zeroInputPin) == 0) { 
    if (level == 100) PumpStart();
    else if (level == 0) PumpStop();
    else {
      Timer1.setPeriod(timeout_usec);
      Timer1.attachInterrupt(PumpStart);
      Timer1.start();
    }
  }

  if (digitalRead(zeroInputPin) == 1) {
    Timer1.setPeriod(DELAY_AFTER_STOP);
    Timer1.attachInterrupt(PumpStop);
    Timer1.start();
  }
}

void checkSwitch() {
  BrewSwitch.update();
  if ( BrewSwitch.fell()) {
    digitalWrite(zeroOnPin, HIGH);
    pumpRunning = 1;
  }
  if ( BrewSwitch.rose()) {
    digitalWrite(pumpPin, LOW);
    digitalWrite(zeroOnPin, LOW);
    pumpRunning = 0;
  }
}

Pump On Flag.png

They have a KiCAD project, so I plotted the schematic to pdf (attached). I notice there’s no series current limiting resistor in the PUMP_CTRL and VALVE_CTRL signals that power the IRLED in their respective SSRs.

From the datasheet, it recommends 15mA-25mA, so with a 5V signal and 20mA, the resistors should be R=(5-1.2)/0.02=190Ω. There’ll be a few tenths of a volt drop on the signals, so I think 180Ω would work fine and give close to 20mA.

espresso-on-crack.pdf (86.1 KB)

dlloyd: They have a KiCAD project, so I plotted the schematic to pdf (attached). I notice there's no series current limiting resistor in the PUMP_CTRL and VALVE_CTRL signals that power the IRLED in their respective SSRs.

From the datasheet, it recommends 15mA-25mA, so with a 5V signal and 20mA, the resistors should be R=(5-1.2)/0.02=190Ω. There'll be a few tenths of a volt drop on the signals, so I think 180Ω would work fine and give close to 20mA.

I found 220 and 1k ohm resistors lying around, I will put them in parallel to get to 180 ohm. Why are you subtracting 1.2V from the 5V voltage from the Arduino in the calculation?

That's the forward voltage drop across the SSR's IRLED (Vf) which is needed in the calculation.

It seems the pump's SSR can still turn off if you write the output to 0 (from your initial comments), but I'm wondering if the pump is compatible with phase controlled SSRs ... I did a quick check (Google) and it looks like most have an internal diode for 1/2 wave operation I presume. I guess it must be compatible as it's part of their design.

So if there's an internal diode for 1/2 cycle (rectified) operation of the pump, and the zero-cross detect circuit is designed to work 1/2 cycle, then perhaps the polarity of the connections to the pump or zero cross are reversed.

I added in the resistors before the SSR and am getting the same result.

I also used another arduino and the makeshift oscilloscope. I connected the analog input to the pin driving the pump SSR on the project arduino and connected the grounds together for both boards. I found that the SSR pin is high for about half of a cycle in all level settings in my code. In level settings 5, 50, 75 and 95 I am getting basically the same output from the project Arduino on the pump SSR pin. Levels 5, 50 and 75 are right on top of each other.

I then modified the code to have a FALLING interrupt in line 62 and then cascaded into two timer interrupts. The oscilloscope showed all 0s and the monitor seemed to be freezing as the timestamp would stick for a bit. I don’t know what to make of that. The pump turned on and behaved as it was on 100%.

I then took the same code with the cascaded interrupts but changed line 62 to a RISING interrupt. I again couldn’t read the values with the oscilloscope but the pump stayed off.

Below is the new code with FALLING and cascading interrupts.

//Pressure control only code

#include <Bounce2.h>
#include <TimerOne.h>

#define pumpPin 5
#define zeroInputPin 2
#define brewPin 8
#define zeroOnPin 12

Bounce BrewSwitch = Bounce();

int pumpRunning = 0;

// estimate of lag caused by ZCD circuit in usec
// measured as per http://espresso-for-geeks.kalaf.net/mod-list/#hw-zero-cross
#define ZCD_DELAY 36

// power frequency half-period in usec
#define HALF_PERIOD_USEC 8333

// the time in usec to wait after zero-cross before switching off
// this eliminates the back-EMF by waiting for current to drop before switching off
#define DELAY_AFTER_STOP 4166

// These are precalculated values (usec) of the required phase delays for 0 to 100% power.
// Equation used is: 1000000 usec / 120 * (acos(2*x/100 - 1) / pi)
const uint16_t timeouts_usec[101] =
{ 0, 531, 753, 924, 1068, 1196, 1313, 1421, 1521, 1616,
  1707, 1793, 1877, 1957, 2035, 2110, 2183, 2255, 2324,
  2393, 2460, 2525, 2590, 2654, 2716, 2778, 2839, 2899,
  2958, 3017, 3075, 3133, 3190, 3246, 3303, 3358, 3414,
  3469, 3524, 3578, 3633, 3687, 3740, 3794, 3848, 3901,
  3954, 4007, 4061, 4114, 4167, 4220, 4273, 4326, 4379,
  4432, 4486, 4539, 4593, 4647, 4701, 4755, 4810, 4864,
  4919, 4975, 5031, 5087, 5144, 5201, 5258, 5316, 5375,
  5435, 5495, 5556, 5617, 5680, 5743, 5808, 5874, 5941,
  6009, 6079, 6150, 6223, 6299, 6376, 6457, 6540, 6626,
  6717, 6812, 6913, 7020, 7137, 7265, 7410, 7581, 7802,
  8333
};

int level = 50; //5
uint16_t timeout_usec = HALF_PERIOD_USEC - timeouts_usec[level] - ZCD_DELAY;

uint16_t startTime = 4077;
uint16_t stopTime = 8422;

void setup() {
  Serial.begin(9600);

  BrewSwitch.attach(brewPin, INPUT_PULLUP);
  BrewSwitch.interval(25);

  pinMode (pumpPin, OUTPUT);

  pinMode (zeroOnPin, OUTPUT);
  digitalWrite(zeroOnPin, LOW);
  pinMode(zeroInputPin, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(zeroInputPin), PumpChange, FALLING);
  Timer1.initialize();
}

void loop() {
  checkSwitch();
}

void PumpStart() {
  if (pumpRunning == 1) {
    digitalWrite(pumpPin, HIGH);
    //   Serial.println("on");
    Timer1.stop();
  }
}

void PumpStop() {
  digitalWrite(pumpPin, LOW);
  Timer1.stop();
  //    Serial.println("off");
}

void PumpChange() {
  if (level == 100) PumpStart();
  else if (level == 0) PumpStop();
  else {
    Timer1.setPeriod(startTime);
    Timer1.attachInterrupt(PumpDelayStart);
    Timer1.start();
  }
}

void PumpDelayStart() {
  PumpStart();
  Timer1.setPeriod(stopTime);
  Timer1.attachInterrupt(PumpStop);
  Timer1.start();
}

void checkSwitch() {
  BrewSwitch.update();
  if ( BrewSwitch.fell()) {
    digitalWrite(zeroOnPin, HIGH);
    pumpRunning = 1;
  }
  if ( BrewSwitch.rose()) {
    digitalWrite(pumpPin, LOW);
    digitalWrite(zeroOnPin, LOW);
    pumpRunning = 0;
  }
}

Could be a problem with your interrupt implementation … have you seen this?

Maybe try a simple test program that just increments through the control range for the pump.

EDIT: For about 1/2 the values of timeouts_usec, the calculation here is negative. When converted to uint16_t, this creates a very long timeout:

uint16_t timeout_usec = HALF_PERIOD_USEC - timeouts_usec[level] - ZCD_DELAY;

Could limit the timeout result with something like:

int16_t timeout_temp = HALF_PERIOD_USEC - timeouts_usec[level] - ZCD_DELAY;
if (timeout_temp < 0) timeout_temp = 0;
uint16_t timeout_usec = timeout_temp;

I had t seen that guide before, I’ll read that tonight and rebuild the code with register interrupts instead of the library.

Wouldn’t timeout_usec only be negative on the last value? HALF_PERIOD_USEC = 8333
timeouts_usec [level] ranges from 0 to 8333
ZCD_DELAY=36

Right! (I guess I was looking at DELAY_AFTER_STOP 4166 by mistake). Anyways, for your last value of 8333, the uint16_t timeout_usec would work out to 65500.

I originally wrote the code using registers but couldn’t get it to work so I was using the TimerOne library thinking it would be easier. After reading your link and continued searching I found that I needed to turn on " TIMSK1 |= (1 << OCIE1B);"

It works now, below is the code if anyone stumbles upon this thread. Well atleast the pump sounds different at different settings. I’m assuming I’ll be able to see different pressure readings once I get the gauge back on there.

Thank you very much for helping guide me to the solution.

//Pressure control only code. Using registers and CHANGE 

#include <Bounce2.h>

#define pumpPin 5 
#define zeroInputPin 2
#define brewPin 8
#define steamPin 7
#define zeroOnPin 12

Bounce BrewSwitch = Bounce();

int pumpRunning = 0;
int pumpFire = 0;

// estimate of lag caused by ZCD circuit in usec
// measured as per http://espresso-for-geeks.kalaf.net/mod-list/#hw-zero-cross
#define ZCD_DELAY 36

// power frequency half-period in usec
#define HALF_PERIOD_USEC 8333

// the time in usec to wait after zero-cross before switching off
// this eliminates the back-EMF by waiting for current to drop before switching off
#define DELAY_AFTER_STOP 4166
#define DELAY_AFTER_STOP_CYCLE 260

// These are precalculated values (usec) of the required phase delays for 0 to 100% power.
// Equation used is: 1000000 usec / 120 * (acos(2*x/100 - 1) / pi)
const uint16_t timeouts_usec[101] =
{ 0, 531, 753, 924, 1068, 1196, 1313, 1421, 1521, 1616,
  1707, 1793, 1877, 1957, 2035, 2110, 2183, 2255, 2324,
  2393, 2460, 2525, 2590, 2654, 2716, 2778, 2839, 2899,
  2958, 3017, 3075, 3133, 3190, 3246, 3303, 3358, 3414,
  3469, 3524, 3578, 3633, 3687, 3740, 3794, 3848, 3901,
  3954, 4007, 4061, 4114, 4167, 4220, 4273, 4326, 4379,
  4432, 4486, 4539, 4593, 4647, 4701, 4755, 4810, 4864,
  4919, 4975, 5031, 5087, 5144, 5201, 5258, 5316, 5375,
  5435, 5495, 5556, 5617, 5680, 5743, 5808, 5874, 5941,
  6009, 6079, 6150, 6223, 6299, 6376, 6457, 6540, 6626,
  6717, 6812, 6913, 7020, 7137, 7265, 7410, 7581, 7802,
  8333
};

int level = 50; //5
uint16_t timeout_usec = HALF_PERIOD_USEC - timeouts_usec[level] - ZCD_DELAY;
uint16_t timeout_cycle = timeout_usec / 16;

void setup() {
  Serial.begin(115200);

  BrewSwitch.attach(brewPin, INPUT_PULLUP);
  BrewSwitch.interval(25);

  pinMode(steamPin, INPUT_PULLUP);

  pinMode (pumpPin, OUTPUT);

  pinMode (zeroOnPin, OUTPUT);
  digitalWrite(zeroOnPin, LOW);
  pinMode(zeroInputPin, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(zeroInputPin), PumpChange, CHANGE);

  //Enable timer interrupt
  noInterrupts();
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;//initialize counter value to 0

  // turn on CTC mode
  TCCR1B |= (1 << WGM12);

  // Set CS12 bit for 256 prescaler
  TCCR1B |= (1 << CS12);

  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  TIMSK1 |= (1 << OCIE1B);

  OCR1A = timeout_cycle;
  OCR1B = DELAY_AFTER_STOP;
  interrupts();

}

void loop() {
  checkSwitch();
}


void PumpStart() {
  if (pumpRunning == 1) {
    digitalWrite(pumpPin, HIGH);
  }
}

void PumpStop() {
  digitalWrite(pumpPin, LOW);
}

void PumpChange() {
  noInterrupts();
  if (digitalRead(zeroInputPin) == 0) {
    if (level == 100) PumpStart();
    else if (level == 0) PumpStop();
    else {
      OCR1A = timeout_cycle;    //timeout_cycle;
      TCNT1  = 0;  //reset timer
      OCR1B = 6500;    //make off comparator take a long time
    }
  }

  if (digitalRead(zeroInputPin) == 1) {
    OCR1B = DELAY_AFTER_STOP_CYCLE;
    TCNT1  = 0;  //reset timer
    OCR1A = 6500;    //make on comparator take a long time
  }
  interrupts();
}

ISR(TIMER1_COMPA_vect) { //comparator match
  PumpStart();
}

ISR(TIMER1_COMPB_vect) { //comparator match
  PumpStop();
}

void checkSwitch() {
  BrewSwitch.update();
  if ( BrewSwitch.fell()) {
    digitalWrite(zeroOnPin, HIGH);
    pumpRunning = 1;
  }
  if ( BrewSwitch.rose()) {
    digitalWrite(pumpPin, LOW);
    digitalWrite(zeroOnPin, LOW);
    pumpRunning = 0;
  }
}