Compressed Air Engine - Rotary Encoder

Hi all,

Some might be aware of my project that I started when the old forum was up. I am working on a thesis for my university on an engine that is driven using compressed air.
Much of you helped me code the project, and it was much appreciated.

I was using a hall effect sensor interrupt to calculate my timing by pulsing every TDC.
After realizing the slight imperfections in the setup, and having an engine that was not so easy to control, I am back to the drawing board.

I just ordered a Capacitive Rotary Encoder http://search.digikey.com/scripts/DkSearch/dksus.dll?Detail&name=102-1307-ND which has multiple programmable ppr rates.

I plan on using the index leg to mark my TDC and the other legs to count my timing.

After searching for encoder programming and being left with fewer results then I expected, I have to ask for help.
I assume that I need to use all three legs as interrupt to avoid missing a count, but physically that is impossible since the Arduino only has two. That doesn't include the fact that if I'm counting 1024 pulses per rev, the Arduino won't have time to run any other code in between.

Does anyone have an opinion on this? How it should be connected? Any sample code? What ppr rate is too much for the Arduino?

Thanks in advance

KE7GKP:
The original simple method was essentially the same as most modern internal combustion engines use, was it not? Can you explain why it was insufficient to meet the needs of your project? Not necessarily explain to US, but do you understand what the problems were so that you can formulate a revised scheme that addresses the issue(s) with the original plan?

You have described to us your new sensor scheme (and are asking questions about it) but not why you thought you needed a different sensor and how you selected that one?

There is a nagging question in the back of my mind about how millions of production engines out on the roads seem to be able to operate with the simpler scheme? (Not to mention maintaining pollution standards, etc. etc.) What is different here? You may have a very legitimate explanation for that disparity, but it isn't clear to use at this end of the conversation?

I will try to answer your question.
A modern engine has a hall effect sensor on the gear teeth of the crankshaft plate. It counts the teeth as well as notices that there is one missing tooth at tdc. This is able to calculate where in the stroke the engine is by two methods. It knows its some point past tdc, and it also knows how many clicks away from tdc. Never minding the fact that physical timing of the intake and exhaust is done with a mechanical linkage.

My device had one pulse per revolution, TDC. In order for me to know where it was past TDC, I was using a simple formula using micros() to calculate the time in between the interrupts as well as "prediction" formula to try to prepare for the next TDC before it happened. This turns out to be not so accurate when the engine is not running steady. I am using solenoid valves to control the intake and exhaust and those solenoids are triggered by the timing which the arduino calculates. Since the valves have no physical connection to the crank or drive line, it is crucial to get the timing accurate. If the wrong valve is open at the wrong time, there will be much larger problems than code :wink:

Hope that helps. Let me know if it didn't.

EDIT: Forgot to mention, I'm not sure if I even need "channel" b on the encoder. The engine will always spin one direction, and as far as I understand, the only point of the two channels is to detect the direction. Correct me if I'm wrong.

KE7GKP:
Yes, thank you. That sounds like you have a good understanding of the problem and a promising remedy.

If you use the 1024 pulses/rev option, you will have a resolution of 0.352 degrees. Is that sufficient accuracy for the turn-on and turn-off timings? If not, then you will still need to do some time extrapolation between pulses.

I appreciate your quick responses. As you mention it in terms of degrees, I only need about half a degree of accuracy at most. The closest one to that of the programmable ppr's is 800 p/r which gives a .45 degree accuracy.
http://products.cui.com/GetSpecForDigiKey.aspx?MFGNum=AMT102-V%20KIT
They do state in the manual that the ppr is before quadrature decoding which is pprx4. That would make it 3200 counts a revolution. Too much for the arduino to handle? I can drop it down to 1 degree accuracy if it makes enough of a difference.

They do state in the manual that the ppr is before quadrature decoding which is pprx4. That would make it 3200 counts a revolution. Too much for the arduino to handle? I can drop it down to 1 degree accuracy if it makes enough of a difference.

Depending on how you handle the quadrature decoding/interrupts you can have an Arduino step counts equal to PPR, or X2 PPR or X4 PPR. To get X4 you have to interrupt on both A and B channels (wired to pins 2&3) and use change state for both interrupt triggers. To get X2, just interrupt on one channel (but read the other channel inside the ISR to get proper direction info) using change state. To get just PPR then interrupt on just one channel using falling (or rising) interrupt trigger.

Lefty

retrolefty:

They do state in the manual that the ppr is before quadrature decoding which is pprx4. That would make it 3200 counts a revolution. Too much for the arduino to handle? I can drop it down to 1 degree accuracy if it makes enough of a difference.

Depending on how you handle the quadrature decoding/interrupts you can have an Arduino step counts equal to PPR, or X2 PPR or X4 PPR. To get X4 you have to interrupt on both A and B channels (wired to pins 2&3) and use change state for both interrupt triggers. To get X2, just interrupt on one channel (but read the other channel inside the ISR to get proper direction info) using change state. To get just PPR then interrupt on just one channel using falling (or rising) interrupt trigger.

Lefty

Thanks for that explanation. It would seem I only need to interrupt one channel giving me the standard PPR. As stated before I don't think I need to know direction as it will always go the same direction.
Also as mentioned before, will using an interrupt on an 800 ppr prevent the Arduino from running any other code? It seems that at any RPM, the Arduino would be dedicated to just counting.

Also as mentioned before, will using an interrupt on an 800 ppr prevent the Arduino from running any other code? It seems that at any RPM, the Arduino would be dedicated to just counting.


What is the maximum rpm you need to handle? rpm X 60 x800 would be the interrupts per second. I'm thinking 800 ppr will be too fast to handle.

Lefty

What is the maximum rpm you need to handle? rpm X 60 x800 would be the interrupts per second. I'm thinking 800 ppr will be too fast to handle.

Lefty

At the moment it is dependent on the valves, but we are working on some solutions on the side.

2000 RPM would be a good start. Eventually I would like to see the ability to read 5000 RPM.

2000 x 60 x 800 = 96,000,000 :~
What can the arduino handle?

rpm X 60 x800 would be the interrupts per second

Hummm....

I would reckon: interrupts per second = rpm / 60 * 800.

-Fletcher

2000 rpm with 800 pulses will give you as mentioned a pulse frequency of 27 kHz or 37.5 µs per pulse or 600 clock cycles at 16 MHz or some 300-400 assembly instructions per pulse. If you processing for each tick is simple, that'll be enough time but don't expect to do too much in that time.

Depending on your needs, there are a few options to make things easier. If you're just interested in a few positions reasonable well apart, feed the impulses into the external counter for a hardware Timer (eg Pin5 for Timer1) and just create an interrupt when Timer/Counter reaches the desired target value. The datasheet of the ATmega328 had a quite good description on how to do that.

Also, if the position of the shaft is of importance, it's very important, that you have some way to detect the zero position in some way so that errors can be corrected at every rotation and don't accumulate over time.

Korman

Korman:
2000 rpm with 800 pulses will give you as mentioned a pulse frequency of 27 kHz or 37.5 µs per pulse or 600 clock cycles at 16 MHz or some 300-400 assembly instructions per pulse. If you processing for each tick is simple, that'll be enough time but don't expect to do too much in that time.

Depending on your needs, there are a few options to make things easier. If you're just interested in a few positions reasonable well apart, feed the impulses into the external counter for a hardware Timer (eg Pin5 for Timer1) and just create an interrupt when Timer/Counter reaches the desired target value. The datasheet of the ATmega328 had a quite good description on how to do that.

Also, if the position of the shaft is of importance, it's very important, that you have some way to detect the zero position in some way so that errors can be corrected at every rotation and don't accumulate over time.

Korman

At the bare minimum I need enough time to count each pulse and activate an intake and exhaust valve every half of a full rotation.

What is a reasonable position? 5 degrees? 10? I can always lower the PPR if it makes it more reasonable. (500,400,384,256,250,200,192,125,100,96,48) The most important thing is to recognize where 12 o'clock, 3 o'clock, 6 o'clock and 9 o'clock are. I realize even having a half degree of accuracy might be over kill at the moment.

The position of "zero" position is indicated by the index pulses coming from the encoder. How to implement it, I don't know.

The position of "zero" position is indicated by the index pulses coming from the encoder. How to implement it, I don't know.

If you do it without hardware counter, you'll need one interrupt for the pulses (on the raising edge) and one pin for the index. In the interrupt function, you check if the index hole is present, if yes you reset the counter to 0, if not you increment the counter. Don't forget to declare the counter variable as volatile. For checking the index hole input, you might want to avoid the digitalRead() function and read the io-register directly if you run out of time.

In your loop() function, you just check if the counter has reached one of the desired values and and do whatever necessary. This way, you have all the logic at which point things need to be done in one place and can easily shift the angles where things need to happen.

With a hardware counter (Timer1 would be a good choice) you would attach the index hole to an interrupt and the pulse to the external time reference for the right Timer. In the interrupt you just reset the counter register to restart from 0. In your loop(), as before, you check if you need to do something at the moment, but this time the hardware counter register and not some variable. If you want to be more fancy, you can have the ATmega328 generate interrupt when certain values are reached, but the handling of target values an become bothersome if you have more than 2 points where things should happen.

At the moment I have no Arduino at hand to write you a sample that works. If you need more details, let me know it and I'll post something later.

Korman

The position of "zero" position is indicated by the index pulses coming from the encoder. How to implement it, I don't know.

from above discussion you need one IRQ to read the pulses to determine the angle and you need one to get the zero position. Some simplified pseudo code:

#define PPR 800

volatile unsigend integer PPRcounter = 0;
volatile unsigend long RotCounter = 0;  // to measure RPM
 
// when X is rising it resets the PPR counter every rotation
void IRQ_X() 
{
  PPRcounter = 0;
  RotCounter++;
}

// line A
void IRQ_Angle()
{
  if (PPRcounter == 0 * PPR / 4)          digitalWrite(valvePin, OPEN);  // # define OPEN  value
  else if (PPRcounter == 1 * PPR /4)     digitalWrite(valvePin, CLOSE);  // # define CLOSE value
  else if (PPRcounter == 2 * PPR /4)     digitalWrite(valvePin, OPEN);   
  else if (PPRcounter == 3 * PPR /4)     digitalWrite(valvePin, CLOSE);   
  PPRcounter ++;
}

Korman:

The position of "zero" position is indicated by the index pulses coming from the encoder. How to implement it, I don't know.

If you do it without hardware counter, you'll need one interrupt for the pulses (on the raising edge) and one pin for the index. In the interrupt function, you check if the index hole is present, if yes you reset the counter to 0, if not you increment the counter. Don't forget to declare the counter variable as volatile. For checking the index hole input, you might want to avoid the digitalRead() function and read the io-register directly if you run out of time.

In your loop() function, you just check if the counter has reached one of the desired values and and do whatever necessary. This way, you have all the logic at which point things need to be done in one place and can easily shift the angles where things need to happen.

With a hardware counter (Timer1 would be a good choice) you would attach the index hole to an interrupt and the pulse to the external time reference for the right Timer. In the interrupt you just reset the counter register to restart from 0. In your loop(), as before, you check if you need to do something at the moment, but this time the hardware counter register and not some variable. If you want to be more fancy, you can have the ATmega328 generate interrupt when certain values are reached, but the handling of target values an become bothersome if you have more than 2 points where things should happen.

At the moment I have no Arduino at hand to write you a sample that works. If you need more details, let me know it and I'll post something later.

Korman

Thank you.
I should be able to do something with this information. I'm not very proficient at coding so I might need a few things answered later on.

robtillaart:

The position of "zero" position is indicated by the index pulses coming from the encoder. How to implement it, I don't know.

from above discussion you need one IRQ to read the pulses to determine the angle and you need one to get the zero position. Some simplified pseudo code:

#define PPR 800

volatile unsigend integer PPRcounter = 0;
volatile unsigend long RotCounter = 0;  // to measure RPM

// when X is rising it resets the PPR counter every rotation
void IRQ_X()
{
  PPRcounter = 0;
  RotCounter++;
}

// line A
void IRQ_Angle()
{
  if (PPRcounter == 0 * PPR / 4)          digitalWrite(valvePin, OPEN);  // # define OPEN  value
  else if (PPRcounter == 1 * PPR /4)     digitalWrite(valvePin, CLOSE);  // # define CLOSE value
  else if (PPRcounter == 2 * PPR /4)     digitalWrite(valvePin, OPEN);   
  else if (PPRcounter == 3 * PPR /4)     digitalWrite(valvePin, CLOSE);   
  PPRcounter ++;
}

Please Explain IRQ_x and IRQ_Angle Are those just different ways of labeling interrupt 1 and interrupt 2? What about the #define 800? I understand it is telling the arudino that there is 800 PPR, but exactly how does that work?
Also for the PPRcounter == 1*PPR/4, I assume that I just use this simple algebra to make it equal the count I am looking for. Is there a more elegant way of doing so? For example, this is only hypothetical, but if I wanted the valves to fire 16 different times in one revolution, I would have to write 16 different else statements... not including the extra code for how long the valves are open.

Thanks Rob.

Please Explain IRQ_x and IRQ_Angle. Are those just different ways of labeling interrupt 1 and interrupt 2?

Yes, these are two functions to be called by IRQ 0 and IRQ 1. IRQ_x contains minimal code for the X-line and IRQ_Angle() contains the minimal code to count the pulses per rotation.

What about the #define 800? I understand it is telling the arudino that there is 800 PPR, but exactly how does that work?

#define PPR 800
is a so called compiler directive, when the compiler read teh sourcecode it will replace the word PPR with the integer value 800. This makes it easy to use the same value anywhere in your code and just one place to maintain it. By choosing a good acronym PPR stands for PulsesPer Rotation as you have guessed :wink: it keeps your code readable to a certain extend. So the compiler replaces "2 * PPR /4" with "2 * 800/4" and the optimizer sees it are all constants so it will code "200".

Also for the PPRcounter == 1*PPR/4, I assume that I just use this simple algebra to make it equal the count I am looking for

.
Yes

Is there a more elegant way of doing so? For example, this is only hypothetical, but if I wanted the valves to fire 16 different times in one revolution, I would have to write 16 different else statements... not including the extra code for how long the valves are open.

Two thoughts come into my mind, the switch statement and using an array.

The switch variant might look better but it has no performance gain wrt the - if then else if ... - construct

void IRQ_Angle()
{
  switch(PPRcounter)
  { 
    case 0: digitalWrite(valvePin, OPEN);   
        break;
    case 200: digitalWrite(valvePin, CLOSE);     // replace 200 with 1*PPR/4
        break;
    case 400: digitalWrite(valvePin, OPEN);   
        break;
    case 600: digitalWrite(valvePin, CLOSE);   
        break;
  }
  PPRcounter ++;
}

An array implentation is a bit more complex, you need to define two arrays of values when to open and when to close the valves. In the code below the valves will stay open for 40 pulses 4 times every rotation (@800PPR => 1/20 rotation @ 2000RPM => 1.5 msec) This code is extendible by making the array larger e.g. 8, 12, 16 or 23 values.

int open[4] = { 0, 200, 400, 600};
int close[4] = { 40, 240, 440, 640};

void IRQ_Angle()
{
  PPRcounter ++;
  for (int i = 0; i< 4; i++)
  {
     if (PPRCounter == open[i]) 
     {
        digitalWrite(valvePin, OPEN); 
        return; 
     }
     if (PPRCounter == close[i]) 
     {
        digitalWrite(valvePin, CLOSE); 
        return;
     }
}

Here a simple sample sketch I made for you to test the rotary encoder. It does not include the switch or array variant but at least it compiles. It outputs the counters and the RPM every second. It let blink the Arduino LED at (0,3,6,9 oclock) Please give it a try

//
//    FILE: Rotary.pde
//  AUTHOR: Rob Tillaart
//    DATE: 2011-03-17
//
// PUPROSE: proof of concept IRQ's for rotary encoder
//


///////////////////////////////////////////////////
//
// VALVE SIMULATOR
//
#define OPEN HIGH
#define CLOSE LOW
#define VALVEPIN 13   // use the LED as pseudo valve

void valve(int newState)
{
	digitalWrite(VALVEPIN, newState);
}


///////////////////////////////////////////////////
//
// DEFINES AND VARS
//
#define PPR 800

volatile unsigned long PPRcounter = 0;
volatile unsigned long RotCounter = 0;  // to measure RPM

unsigned long start = 0;
unsigned long RPM = 0;

// when line X is rising it resets the PPRcounter every rotation
void IRQ_X() 
{
	PPRcounter = 0;
	RotCounter++;
}

// line A
void IRQ_Angle()
{
	if (PPRcounter == 0 * PPR / 4) valve(OPEN);
	else if (PPRcounter == 1 * PPR /4) valve(CLOSE);
	else if (PPRcounter == 2 * PPR /4) valve(OPEN);
	else if (PPRcounter == 3 * PPR /4) valve(CLOSE);
	PPRcounter++;
}


///////////////////////////////////////////////////
//
// INITIALIZE
//
void setup()
{
	Serial.begin(115200);
	Serial.println("Rotary test 0.1");

        pinMode(13, OUTPUT);  // ledpin;

	// LINE X connects to pin 2
	attachInterrupt(0, IRQ_X, RISING);
	
	// LINE A connects to pin 3
	attachInterrupt(1, IRQ_Angle, RISING);

	start = millis();
}

///////////////////////////////////////////////////
//
// GO !
//
void loop()
{
	// once per second
	if (millis() - start > 1000) 
	{
		start = millis();
		RPM = RotCounter * 60 + (PPRcounter * 60)/PPR;
		Serial.print(RotCounter, DEC);	
		Serial.print(", \t");
		Serial.println(PPRcounter, DEC);
		Serial.print(", \t");
		Serial.println(RPM, DEC);
	}
}

OK, enough to keep you busy I guess :slight_smile:

For example, this is only hypothetical, but if I wanted the valves to fire 16 different times in one revolution, I would have to write 16 different else statements... not including the extra code for how long the valves are open.

Yes, there is. In the end, you might end up with a table driven or algorithmic solution. From the little I know about motor control, you might also modify the angle and duration at which valves open or close depending on the current rpm, pressure, temperature and other parameters. Once you want to do that, the simplistic hard-coded approach with if won't work. But those enhancements can be left for later, once you have the basics working properly.

As I wrote earlier, this project is a very well suited to get into embedded system programming, where you can start out simple and add complexity over time.

If interrupts are confusing you still - which very well might be the case - you can even try to write your first version just using polling the encoder pins with digitalRead() in the loop() function.

void loop() {
   static int lastpulsestate = LOW;
   static int pulsecount = 0;

   // Read pulse pin
   int curpulse = digitalRead (pulsepin);
   // We process a pulse only once when pulsepin state changed
   if (curpulse != lastpulsestate) {
       if (curpulse == LOW) {
           // Check if the index is present and adjust pulse count
           if (digitalRead (indexpin) == LOW) {
               // Reset pulse counter
               pulsecount = 0;
           }
           else {
               // Increment pulse counter
               pulsecount++;
           }

          // Check if we have a pulse count where we need to so something
          switch (pulsecount) {
               case 143:
                     // Open valve 1
                     digitalWrite (valve1pin, HIGH);
                     break;
               case 156:
                     // Close valve 1
                     digitalWrite (valve1pin, LOW);
                     break;
               case 543:
                     // Open valve 2
                     digitalWrite (valve2pin, HIGH);
                     break;
               case 556:
                     // Close valve 2
                     digitalWrite (valve2pin, LOW);
                     break;
           }
       }
       lastpulsestate = curpulse;
   }
}

The code is as usual untested and all those HIGH and LOW might need to be reversed depending on what kind of signal levels are relevant. If you try that, you need to set up the constant variables and initialise the ports.

Korman

Thank you to both of you. You guys have official given me something physical to work with now.
I hope my encoder gets here before the end of the week so I can try this stuff.

I will attempt to use both of your ideas and write a program. When I am done I will post back here. It could be later tonight, or in the next day or so.

This could be from ignorance, but when looking through the pdf for the encoder, It does not state where the index is triggered.
Will this be obvious when I receive it, or is it a variable index dictating on the last position it started in? The latter would make finding TDC much more annoying.

This could be from ignorance, but when looking through the pdf for the encoder, It does not state where the index is triggered.
Will this be obvious when I receive it, or is it a variable index dictating on the last position it started in? The latter would make finding TDC much more annoying.

http://products.cui.com/CUI_AMT103-V_Product_Training_Presentations.pdf?fileID=6203 Sheet 7 shows how it works, but I can't find it either.

http://www.cui.com/Contact/Company - shows email addresses to contact the company.

robtillaart:

This could be from ignorance, but when looking through the pdf for the encoder, It does not state where the index is triggered.
Will this be obvious when I receive it, or is it a variable index dictating on the last position it started in? The latter would make finding TDC much more annoying.

http://products.cui.com/CUI_AMT103-V_Product_Training_Presentations.pdf?fileID=6203 Sheet 7 shows how it works, but I can't find it either.

http://www.cui.com/Contact/Company - shows email addresses to contact the company.

I sent them an email. I will post the answer when I receive it.

You might like to read the Agilent HCTL2032 data sheet. This chip or its single channel cousin the HCTL 2022 might help you out by taking some of the load tracking the pulses off the Arduino. I am using it (2032) to handle a two channel quadrature encoder system. Check it out. It may be of use to you

Vic

Victor Fraenckel
KC2GUI
windswaytoo ATSIGN gmail DOT com