0

Im trying to make a simple RPM meter using an ATMega328.

I have an encoder on the motor which has 306 interrupts per rotation (as the motor encoder has 3 spokes which interrupt on rising and falling edge, the motor is geared 51:1 and so 6 transitions * 51 = 306 interrupts per wheel rotation ) , and I am using a timer interrupting every 1ms, however in the interrupt it set to recalculate every 1 second.

There seems to be 2 problems. 1) RPM never goes below 60, instead its either 0 or RPM >= 60 2) Reducing the time interval causes it to always be 0 (as far as I can tell)

Here is the code

    int main(void){

    while(1){
        int temprpm = leftRPM;
        printf("Revs: %d \n",temprpm);
        _delay_ms(50);
    };

    return 0;
}

ISR (INT0_vect){
    ticksM1++;
}

ISR(TIMER0_COMPA_vect){

    counter++;

    if(counter == 1000){

        int tempticks = ticksM1;
        leftRPM = ((tempticks - lastM1)/306)*1*60;
        lastM1 = tempticks;
        counter = 0;    

    }

}

Anything that is not declared in that code is declared globally and as an int, ticksM1 is also volatile.

The macros are AVR macros for the interrupts.

The purpose of the multiplying by 1 for leftRPM represents time, ideally I want to use 1ms without the if statement so the 1 would then be 1000

binarysmacker
  • 982
  • 2
  • 9
  • 20

2 Answers2

3

For a speed between 60 and 120 RPM the result of ((tempticks - lastM1)/306) will be 1 and below 60 RPM it will be zero. Your output will always be a multiple of 60

The first improvement I would suggest is not to perform expensive arithmetic in the ISR. It is unnecessary - store the speed in raw counts-per-second, and convert to RPM only for display.

Second, perform the multiply before the divide to avoid unnecessarily discarding information. Then for example at 60RPM (306CPS) you have (306 * 60) / 306 == 60. Even as low as 1RPM you get (6 * 60) / 306 == 1. In fact it gives you a potential resolution of approximately 0.2RPM as opposed to 60RPM! To allow the parameters to be easily maintained; I recommend using symbolic constants rather than magic numbers.

#define ENCODER_COUNTS_PER_REV 306
#define MILLISEC_PER_SAMPLE 1000
#define SAMPLES_PER_MINUTE ((60 * 1000) / MILLISEC_PER_SAMPLE)

ISR(TIMER0_COMPA_vect){

    counter++;

    if(counter == MILLISEC_PER_SAMPLE)
    {

        int tempticks = ticksM1;

        leftCPS = tempticks - lastM1 ;

        lastM1 = tempticks;

        counter = 0;    

    }
}

Then in main():

int temprpm = (leftCPS * SAMPLES_PER_MINUTE) / ENCODER_COUNTS_PER_REV ;

If you want better that 1RPM resolution you might consider

int temprpm_x10 = (leftCPS * SAMPLES_PER_MINUTE) / (ENCODER_COUNTS_PER_REV / 10) ;

then displaying:

printf( "%d.%d", temprpm / 10, temprpm % 10 ) ;

Given the potential resolution of 0.2 rpm by this method, higher resolution display is unnecessary, though you could use a moving-average to improve resolution at the expense of some "display-lag".

Alternatively now that the calculation of RPM is no longer in the ISR you might afford a floating point operation:

float temprpm = ((float)leftCPS * (float)SAMPLES_PER_MINUTE ) / (float)ENCODER_COUNTS_PER_REV ;
printf( "%f", temprpm ) ;

Another potential issue is that ticksM1++ and tempticks = ticksM1, and the reading of leftRPM (or leftCPS in my solution) are not atomic operations, and can result in an incorrect value being read if interrupt nesting is supported (and even if it is not in the case of the access from outside the interrupt context). If the maximum rate will be less that 256 cps (42RPM) then you might get away with an atomic 8 bit counter; you cal alternatively reduce your sampling period to ensure the count is always less that 256. Failing that the simplest solution is to disable interrupts while reading or updating non-atomic variables shared across interrupt and thread contexts.

Clifford
  • 88,407
  • 13
  • 85
  • 165
  • I think im getting a bit confused. Let me see if im right first, geared 51 and 6 counts per back shaft revs gives 306 counts per front shaft revs (back shaft is straight from motor). So tickdifference/306. Then multiply by 1000 as its inside a 1ms interupt to get per second number then multiply by 60 for RPM. After testing it seems like 1ms is just too quick for the actual encoder as it sometimes only sees a change of 0 (need to work out if this is right!) so inside the ISR if statement for 25 counter counts. So now instead of multiplication by 1000 its only 40, after simplifying leftCPS*400 ? – binarysmacker Apr 03 '14 at 19:05
  • (leftCPS*40)/6 I mean sorry – binarysmacker Apr 03 '14 at 19:13
  • Also, if I can would it be best to get the division to be powers of 2 then it would mean divide would only need to be a bit shift opperation – binarysmacker Apr 03 '14 at 19:37
  • @binarysmacker: Now I am confused, your original question stated 306 counts, but your code appeared to be written for 360 counts - so I edited your question. Now you are back to talking about 306 counts - which is it? – Clifford Apr 03 '14 at 19:38
  • Sorry Clifford, it IS 306, someone edited my post to make it 360 (Im assuming he thought I meant 1 count per degree..which it isnt) and I missed the edit he made in my code also. – binarysmacker Apr 03 '14 at 19:40
  • 1
    @binarysmacker: with respect division, yes a power of two is ideal, but the value is determined by your encoder resolution. You could tweak the sample time (use a value other than 1000) to compensate for that, but it would be an optimisation at the expense of readability perhaps. – Clifford Apr 03 '14 at 19:41
  • The encoders would never change, and if they did it this is the main area they are used so it wouldnt be hard to fix for it. The reason I want to keep it in the ISR is because I ideally would like to then use this feedback in a control loop to perform a basic speed profile for the motors. – binarysmacker Apr 03 '14 at 19:43
  • @binarysmacker: No *I* edited your question to match your code which *is* still written for 360. Any how all you need to do is change the symbolic constants in my solution to match your hardware. You should correct the code in your question too. – Clifford Apr 03 '14 at 19:44
  • Ah, sorry, its all fixed up now. sorry about the confusion – binarysmacker Apr 03 '14 at 19:48
  • I have changed my answer to match your changes. The suggestion to simply divide by 6 no longer applies. – Clifford Apr 03 '14 at 19:52
  • I don't think the power of two divide optimisation will be necessary so long as you stop doing the operation in the ISR - unless this system ultimately has to perform other functions as well. If it meets performance requirements as is, there is no need to optimise it, beyond just not doing unnecessary operations in the ISR. – Clifford Apr 03 '14 at 19:55
  • The reason I want to keep it in the ISR is because I ideally would like to then use this feedback in a control loop to perform a basic speed profile for the motors. – binarysmacker Apr 03 '14 at 19:56
  • 1
    No! Do the feedback in CPS - your feedback loop does not care about the error signal units, whatever the range and resolution of the input, it is compensated for by the control-loop coefficients. You rather convert your set-point from RPM to CPS (or whatever your error signal units are), not the otherway around. In raw counts you have a very high resolution input, converting to RPM even at 0.2 RPM throws that away. See my answer to http://stackoverflow.com/questions/17237173/pid-feedback-position-controller-with-dc-motor-encoder/17247966#17247966 – Clifford Apr 03 '14 at 20:10
  • Ah! Clifford thank you very much! Just one thing when you mention timing accuracy on the linked answer, do you suggest performing the feedback loop inside the timers ISR? Or do you mean to use a timer to set a flag to then perform the control inside the main program when it gets back round to it? (Using CPS instead of RPM now of course :) ) – binarysmacker Apr 03 '14 at 20:18
  • Actually it is *counts-per-loop-update-period*; which may need to be faster than one second to get stable control - depending on your system dynamics (rate of change). PID loop stability relies on accurate timing, though again the precision necessary depends on system dynamics. Consider that for the same speed, if your timing is not accurate, you will get a different count, and the system will be compensating for timing jitter rather than speed error and may become unstable. How you ensure sufficient stability is really up to you. I'd use an RTOS if there was other work to do. – Clifford Apr 03 '14 at 20:25
  • 1
    This may be of use too: http://igor.chudov.com/manuals/Servo-Tuning/PID-without-a-PhD.pdf – Clifford Apr 03 '14 at 20:28
2

It's integer division. You would probably get better results with something like this:

leftRPM = ((tempticks - lastM1)/6);
Fred Larson
  • 60,987
  • 18
  • 112
  • 174
  • 1
    or enforce float division and then round – Andrey Apr 03 '14 at 17:53
  • 2
    For best effect, take the 60 out too: `leftRPM = (tempticks - lastM1)/60;` – Deduplicator Apr 03 '14 at 17:55
  • If you decide to use float conversions to perform the division make sure that's OK to do in an ISR. Often there are special considerations for floating point operations in an ISR. – Michael Burr Apr 03 '14 at 18:12
  • I dont understand, shouldn't it be atleast `(6*(tempticks - lastM1)/60);` – binarysmacker Apr 03 '14 at 18:31
  • @binarysmacker: Actually, I think it should be `leftRPM = ((tempticks - lastM1)/6);`60 / 360 == 1/6, right? – Fred Larson Apr 03 '14 at 18:51
  • 1
    @binarysmacker: I am unfairly blamed ;-) - I edited the text to match the code - it was the code that was wrong however! Should have raised a comment first perhaps? I suspect this answer used the code rather than the question text in any case, so the edit may not have changed this answer in any case. – Clifford Apr 03 '14 at 19:59
  • Ah, I missed the discrepancy and went by the code. Of course you can't do integer division by 5.1. You could multiply by 60 an divide by 306, or reduce that to multiplying by 10 and dividing by 51. – Fred Larson Apr 03 '14 at 20:20