Project Digital Steam Engine

So the project is to control a small steam engine with an Arduino UNO board. We wanted to put a hall sensor on the crank and use that input (once per rev) to open and close a couple solenoids to control the flow of steam into the cylinder. Eventually we would like to add in an abilities to throttle the engine, advance or retard the timing as RPM changes, monitor steam pressure on the boiler and adjust the flame accordingly, maybe display this info to an LCD, but right now just a flat timing map just to run the engine will be a good first step.

This is the code so far, and it appears to work well, expect one problem: every few seconds it appears to hiccup. I'm wondering if I'm using the delay command incorrectly and that's causing problems. Another thought was maybe it runs out of memory (since the micros numbers get pretty big, pretty quick), the code just simply isn't written correctly and this approach is just wrong, or the maybe the UNO isn’t the best board for the job? I’m not sure.

I set up a couple LED’s on a bread board to simulate the intake and exhaust solenoid. I took a video of it running at a consistent 600rpm so you guys can get a better idea what I'm talking about. We are using a function generator to simulate the hall sensor input right now, its set at 10hz. Here is a link to the video. The green LED represents the intake solenoid, the red is exhaust.

I found this thread , where Newman180 is trying to accomplish something similar, but I wasn’t able to find what I was looking there.

http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1289168643/all

I did my best to comment the code as clearly as possible. I’m going to monitor this thread consistently, so if there is something in the code you don’t understand, or something I failed to clarify in this post, just ask and I’ll explain ASAP.

Thanks in advance guys, I’m looking forward to getting this running!

 unsigned long revPeriod;                  // Microseconds for 1 revolution of crankshaft
 unsigned long prevTDCtime;                // Previous time Hall Sensor Picked up
 unsigned long TDCtime;                    // Current TDC Time
 unsigned long crankAngle;                 // Microseconds per angle of Crankshaft Rotation 
 unsigned long intake;                     // Variable in microseconds for intake valve to open
 unsigned long intake2;                    // Variable in microseconds for intake valve to close
 unsigned long exhaust;                    // Variable in microseconds for exhaust valve to open
 unsigned long exhaust2;                   // Variable in microseconds for exhaust valve to close
 unsigned int RPM;                         // define RPM, used only as a serial output
 int number = 0;                           // number used to stop loop from running continuously

 
 void setup()
 {
   
   Serial.begin(9600);                        // Communicate to Serial Port
   pinMode (8, OUTPUT);                       // set pin 8 (intake) as an output
   pinMode (11, OUTPUT);                      // set pin 11 (exhaust) as an output
   attachInterrupt(0,TDCinterrupt, RISING);   // Interrupt 0 is Pin 2 Hall Effect Sensor
   
 }
 
 
 void TDCinterrupt()
 
{    
  prevTDCtime = TDCtime;                         // sets prevTDCtime equal to the last time the TDC sensor was triggered (used later to calculate angle)
  TDCtime = micros();                            // sets TDCtime equal to the current micros counter
  number = 1;                                    // sets "number" equal to 1 which will allow the next if statement to run
  }
 void loop ()
{
     
  if (number == 1)
  {
  revPeriod = (TDCtime - prevTDCtime);           // rev period is Microseconds for 1 revolution of crankshaft
  crankAngle = (revPeriod/360);                  // microseconds per angle of Crankshaft Rotation 
  RPM = (60000000/revPeriod);                    // divide 60 million (number of microseconds in a min) by the time it took the crank to go around once, to get RPM
  
  intake   = ((crankAngle * 2)/1000);            //how long to wait in microseconds to open the intake valve then divide by 1000 to get milliseconds
  intake2  = ((crankAngle * 100)/1000);          //how long to wait in microseconds to close the intake valve then divide by 1000 to get milliseconds
  exhaust  = ((crankAngle * 30)/1000);           //how long to wait in microseconds to open the exhaust valve then divide by 1000 to get milliseconds
  exhaust2 = ((crankAngle * 100)/1000);          //how long to wait in microseconds to close the exhaust valve then divide by 1000 to get milliseconds
                                                 //we found the delayMicroseconds didn't work as expected, which is the justification for converting micro to millis^^
                                                 
  delay(intake);                                 // delay "intake" number of milliseconds 
    digitalWrite (8, HIGH);                      // open intake valve
    
  delay(intake2);                                // delay "intake2" number of milliseconds
    digitalWrite (8, LOW);                       // close intake valve  
    
  delay(exhaust);                                // delay "exhaust" number of milliseconds
    digitalWrite (11, HIGH);                     // open exhaust valve
    
  delay(exhaust2);                               // delay "exhaust2" number of milliseconds
    digitalWrite (11, LOW);                      // close exhaust valve
    
  Serial.print("    ");                          // print out all the numbers just so we can see them and confirm the math was correct.
  Serial.print(prevTDCtime);
  Serial.print("   ");  
  Serial.print(revPeriod);
  Serial.print("   ");
  Serial.print(crankAngle);
  Serial.print("   ");
  Serial.print(RPM);
  Serial.println();
   Serial.print(intake);
  Serial.print("  ");
  Serial.print(intake2);
  Serial.print("  ");
  Serial.print(exhaust);
  Serial.print("  ");
  Serial.print(exhaust2);
  Serial.println();
  Serial.println(number);
  }
  number = 0;                                  // set number to 0 so the loop won't run again until the interrupt sets it back to 1 (simplest way I could think to do it)
}

Here are some suggestions:

  1. Declare variables locally where possible (not the reason for your problem, but good programming style).

  2. Variables modified by the ISR but read elsewhere should be declared volatile.

  3. When reading a variable outside an ISR that is more than 1 byte long and modified within an ISR, disable interrupts while you take a local copy. Also, you need to read both TDCtime and prevTDCtime with interrupts disabled for the duration, so that you can't get an interrupt after reading one but before reading the other. [This is probably the main reason for the hiccups.]

Here is my suggested modified version, untested.

 volatile unsigned long prevTDCtime;                // Previous time Hall Sensor Picked up
 volatile unsigned long TDCtime;                    // Current TDC Time
 volatile uint8_t number;

 void setup()
 {   
   Serial.begin(9600);                        // Communicate to Serial Port
   pinMode (8, OUTPUT);                       // set pin 8 (intake) as an output
   pinMode (11, OUTPUT);                      // set pin 11 (exhaust) as an output
   attachInterrupt(0,TDCinterrupt, RISING);   // Interrupt 0 is Pin 2 Hall Effect Sensor
 }
 
void TDCinterrupt()
{    
  prevTDCtime = TDCtime;                         // sets prevTDCtime equal to the last time the TDC sensor was triggered (used later to calculate angle)
  TDCtime = micros();                            // sets TDCtime equal to the current micros counter
  number = 1;                                    // sets "number" equal to 1 which will allow the next if statement to run
  }

 void loop ()
{
     
  if (number == 1)
  {
  noInterrupts();
  copyTdcTime = TDCtime;
  copyPrevTdcTime = prevTDCtime;
  interrupts();

  unsigned long revPeriod = (copyTdcTime - copyPrevTdcTime );           // rev period is Microseconds for 1 revolution of crankshaft
  unsigned long crankAngle = (revPeriod/360);                  // microseconds per angle of Crankshaft Rotation 
  unsighed int RPM = (60000000u/revPeriod);                    // divide 60 million (number of microseconds in a min) by the time it took the crank to go around once, to get RPM
  
  unsigned long intake   = ((crankAngle * 2)/1000);            //how long to wait in microseconds to open the intake valve then divide by 1000 to get milliseconds
  unsigned long intake2  = ((crankAngle * 100)/1000);          //how long to wait in microseconds to close the intake valve then divide by 1000 to get milliseconds
  unsigned long exhaust  = ((crankAngle * 30)/1000);           //how long to wait in microseconds to open the exhaust valve then divide by 1000 to get milliseconds
  unsigned long exhaust2 = ((crankAngle * 100)/1000);          //how long to wait in microseconds to close the exhaust valve then divide by 1000 to get milliseconds
                                                 //we found the delayMicroseconds didn't work as expected, which is the justification for converting micro to millis^^
                                                 
  delay(intake);                                 // delay "intake" number of milliseconds 
    digitalWrite (8, HIGH);                      // open intake valve
    
  delay(intake2);                                // delay "intake2" number of milliseconds
    digitalWrite (8, LOW);                       // close intake valve  
    
  delay(exhaust);                                // delay "exhaust" number of milliseconds
    digitalWrite (11, HIGH);                     // open exhaust valve
    
  delay(exhaust2);                               // delay "exhaust2" number of milliseconds
    digitalWrite (11, LOW);                      // close exhaust valve
    
  Serial.print("    ");                          // print out all the numbers just so we can see them and confirm the math was correct.
  Serial.print(copyPrevTdcTime );
  Serial.print("   ");  
  Serial.print(revPeriod);
  Serial.print("   ");
  Serial.print(crankAngle);
  Serial.print("   ");
  Serial.print(RPM);
  Serial.println();
   Serial.print(intake);
  Serial.print("  ");
  Serial.print(intake2);
  Serial.print("  ");
  Serial.print(exhaust);
  Serial.print("  ");
  Serial.print(exhaust2);
  Serial.println();
  Serial.println(number);
  }
  number = 0;                                  // set number to 0 so the loop won't run again until the interrupt sets it back to 1 (simplest way I could think to do it)
}

How does it behave without those prints?

wildbill:
How does it behave without those prints?

Exactly the same. We thought about that being an issue as well, but it doesn't seem to change anything.

Thanks

dc42:
Here are some suggestions:

I just copied your code to the Arduino, and it works much better, but still has the same hiccup you see in the video.

The hiccup doesn't seem to be consistent, it will run fine for roughly 10 seconds, then hiccup a few times then run for another few seconds, the hiccup again.

But that defiantly helped dc42, thanks!

dc42,

Here is a video of it running at 600 rpm with your suggestion.

I would have gone about a few things on the mechanical interface differently. Most engine controllers operate in PLL mode, where machine speed and angle are compared to 2 refernces that are 90degrees apart. One reference comparison is used to slow the machine, the other to speed it up. User speed setting affects the steady state frequency of the PLL clock. Loading rising will cause the machine to slow making the phase shift toward the speedup refence, throttles track the net error and are dampened to hold a fixed rate of change in error until speed lock is re established( prevents overshoot). I would also sense the crank at a multiple of 16 or more to 1 via a daisy wheel, TDC is accomplished by one shutter being 2x as wide as the remainder. The arduino would provide a reference clock based on a voltage read from a user controlled pot or a routine for increment/decrement via momentary switches. Rate of the sync pulse would be used for determining location on the timing advance curve.
As for the hiccough you observe I'll bet its a phase related wrap around, likely the result of accumulated math errors, not on your part but the ones that result from binary manipulations, another driving reason to use circuity to do the PLL comparison and send the cpu an analog signal relating to phase error.

After I posted my previous reply, I realised that if your system was working as intended, the reading of the ISR variables in the main loop would be synchronised to just after the ISR has occurred, because the loop waits for number=1. So it should not be possible for the loop to be interrupted while it is reading the variables. Although the changes I suggest are good things to do anyway, they can't be the whole story.

I've been looking at the timing of your system at 600rpm:

revPeriod = 100,000 us = 100ms
crankAngle = 277 us/degree
intake = (277 * 2)/1000 = 0ms
intake2 = (277 * 100)/1000 = 27ms
exhaust = (277 * 30)/1000 = 8ms
exhaust2 = 27ms

Total of all the delay calls = 62ms

Serial data printed (worst case):

____4294967295___100000___277___60\r\n0__27__8__27\r\n

(52 characters). Time to print 51 characters at 9600b is about 54ms assuming 1 start bit, 1 stop bit, no parity.

Total time from sensing pulse to being ready for the next = 62 + 53ms = 115ms. Oops! this is greater than the time per revolution (100ms). So the Serial.print calls are definitely taking too much time. I suggest you either get rid of them or increase the baud rate to 115200.

Also of note is that the timing resolution you are getting from the delay calls with 1ms resolution corresponds to about 4 degrees at 600rpm. You really need to use something more fine-grained than delay(). The reason that delayMicroseconds didn't work for you is that it takes a parameter of type unsigned int (not unsigned long, limiting the value you can pass to 65535. Even worse, it contains a bug that means it only works properly up to 16383us on a 16MHz Arduino. You could use something like this instead:

void myDelayMicroseconds(unsigned long t)
{
  unsigned long start = micros();
  while (micros() - start < t) { }
}

Even better would be to loop until a certain time after the saved TDC time, so that the time to execute the code that opens and closes valves doesn't add to the delay:

void delayTillAfter(unsigned long startTime, unsigned long t)
{
  while (micros() - startTime < t) {}
}

void loop
{
  ...
  unsigned long intake   = crankAngle * 2;            //how long to wait in microseconds after TDC to open the intake valve
  unsigned long intake2  = crankAngle * 102;        //how long to wait in microseconds after TDC to close the intake valve
  unsigned long exhaust  = crankAngle * 132;        //how long to wait in microseconds after TDC to open the exhaust valve
  unsigned long exhaust2 = crankAngle * 232;       //how long to wait in microseconds after TDC to close the exhaust valve
  ...
  delayTillAfter(copyTdcTime,intake);
    digitalWrite (8, HIGH);                      // open intake valve
  delayTillAfter(copyTdcTime,intake2);
  ...
  delayTillAfter(copyTdcTime, exhaust);
  ...
  delayTillAfter(copyTdcTime, exhaust2);
  ...

btw doing this also means that you can do other things (e.g. some of the Serial.print calls) at the start of the longer intervals between valve operations.

@dc42 Your model is ideal, and engine isn't. Velocity varies within any portion of a revolution...diminishing cylinder pressure, changes of crankshaft effective radius with position, exhaust losses. To control something you cannot fixate on where it has been, but on where it is and where it is going to be. 16 reads of shaft position gives you 8 distinct positions in the power stroke that's 22.5degrees between verified positions. in the end the engine has to run somewhat normally without the intervention of the arduino.

ajofscott:
@dc42 Your model is ideal, and engine isn't. Velocity varies within any portion of a revolution...diminishing cylinder pressure, changes of crankshaft effective radius with position, exhaust losses. To control something you cannot fixate on where it has been, but on where it is and where it is going to be. 16 reads of shaft position gives you 8 distinct positions in the power stroke that's 22.5degrees between verified positions. in the end the engine has to run somewhat normally without the intervention of the arduino.

I'm fully aware that the angular velocity will vary with position - and of course it will vary when the engine is accelerating or decelerating. I agree that it is highly desirable to measure the crankshaft position more than once per revolution. But this has nothing to do with the problem reported by the OP. My model was for the code being used by the OP at the steady 600rpm that he is testing with, in order to help understand the problem he reports.

I don't understand your comment "in the end the engine has to run somewhat normally without the intervention of the arduino". My understanding is the the OP wants the Arduino to be the control system for the engine, in particular the valve timing.

dc42:
...Time to print 51 characters at 9600b is about 54ms assuming 1 start bit, 1 stop bit, no parity.

Total time from sensing pulse to being ready for the next = 62 + 53ms = 115ms. Oops! this is greater than the time per revolution (100ms). So the Serial.print calls are definitely taking too much time. I suggest you either get rid of them or increase the baud rate to 115200.

I just remembered that in Arduino 1.0 the hardware serial output is interrupt driven and used a 64-character output buffer. So the above analysis is incorrect if you are using Arduino 1.0 software, since the serial data for one cycle should fit completely in the buffer and the time taken to transmit it should be less than 100ms. Nevertheless, I suggest increasing the baud rate.

When the hiccup occurs, what do you see in the serial data?

ajofscott:
@dc42 Your model is ideal, and engine isn't. Velocity varies within any portion of a revolution...diminishing cylinder pressure, changes of crankshaft effective radius with position, exhaust losses. To control something you cannot fixate on where it has been, but on where it is and where it is going to be. 16 reads of shaft position gives you 8 distinct positions in the power stroke that's 22.5degrees between verified positions. in the end the engine has to run somewhat normally without the intervention of the arduino.

Right but isn't that how automotive engines run? My car (OHC 4 cylinder) has a 4 wire cam angle sensor. Two of them are power and ground, one is a top dead center signal (once per rev), and the other is a four per rev signal that controls the fuel injectors. Since the timing for the fuel injector's never change, it just fires directly from the CAS signal, all the ECU does is decide how long to hold them open. Timing moves around constantly, and it only knows when the last time it was at TDC. Maybe there is a way to average the last ~15-20 time between TDC signals to show a trend in RPM and base timing off of that?

If the computer only knows where the crank is 16 times or every 22.5*, wouldn't you need math to advance or retard the timing by, lets say, 2 degrees? Which would end up looking something similar to what I have here?

6Bolt2g:
If the computer only knows where the crank is 16 times or every 22.5*, wouldn't you need math to advance or retard the timing by, lets say, 2 degrees? Which would end up looking something similar to what I have here?

IMO an arrangement that is close to ideal would be to have 4 Hall sensors that trigger at each of the earliest crankshaft positions you ever want for intake open, intake close, exhaust open and exhaust close. Then you could trigger each of those 4 events from the corresponding sensor, with a small delay calculated according to how much you want to retard the event from that position. I think this would work better at low speeds when the engine is accelerating and the speed is varying. But I suggest you get your existing arrangement working properly first, then see if something better is needed..

dc42:
IMO an arrangement that is close to ideal would be to have 4 Hall sensors that trigger at each of the earliest crankshaft positions you ever want for intake open, intake close, exhaust open and exhaust close. Then you could trigger each of those 4 events from the corresponding sensor, with a small delay calculated according to how much you want to retard the event from that position. I think this would work better at low speeds when the engine is accelerating and the speed is varying. But I suggest you get your existing arrangement working properly first, then see if something better is needed..

I agree completly. I have the engine running now with mechanical valves, but the idea was to learn how to use the Arduino/continue to learn C. I could probably do this with out an Arduino all together, just have the hall sensors fire a SSR to open the valves. I just didn't think this was going to be as complex as its turning out to be.

No! I am the one in the wrong here, its your baby birth it as you deem fit. Who am I to tell you how to build your creation. My point was meant to indicate that some functionality is best served in the hardware side, and let the micro manage overall operation. Before or after is a point of relative position to the observer, in the car's case the cylinder fires after the strobe from the pickup. Both signals are used together for ignition and injector strobe. I hope you can forgive me.