Creating an offset angle using an absolute encoder

Firstly...I know this question has been answered many times with regards to millis() and micros() regarding rollover of long variables to do with counting, but I am still having trouble understanding.

I am working on a test rig to test impact resistance of light fittings.

This jig has a swinging arm with a large hammer at the end that will be used to smash the front face of the fitting. In order to monitor the amount of force behind the hammer, we have calculated the weight and the travel distance etc and come up with a table showing the angles we need to set the hammer to at the start of the test to give us different ratings

Im using an arduino Mega with a Bourns 1024 absolute encoder which provides a binary output giving the absolute co-ordinates. My software can read the data from the encoder, convert it to decimal and then convert to a float to give the exact angle. I then multiply the float and convert to a long value in order to divide and modulo each digit to send one at a time to a touch screen display. All of this is working perfectly.

I am struggling with the next part, which is to create an offset angle, so that regardless of the absolute position of the encoder, we have a button that will create a 0 offset. I then want this offset to plus or minus as the encoder moves, the same way as before, only this time to either add or subtract the offset value.

I am using long variables to give me 3 digits and 5 decimal places, which is a total of 8 digits. In order to fit this large number in, I have to use an unsigned long with a maximum of 2^16-1 or 4294967295.
The angle position will give a range of 0 - 35999999.

Due to the rollover, when my offset value is larger than the absolute value I am subtracting from, the long goes into the 4000000000 range and therefore messes up my calculations.

I have attached snippets from my code below, which hopefully someone will spot the errors instantly. lol.

Please can anybody help me out? Any help would be greatly appreciated. If I need to post more info etc, just let me know.

byte Encoder_Position[10] = {0,0,0,0,0,0,0,0,0,0};
unsigned int Encoder_Dec = 0; 
float Scale = 0.3515625, Angle = 000.00000;
unsigned long Angle_Integer = 0, Offset_Angle_Integer = 0;
byte LCD_Angle[8] = {0,0,0,0,0,0,0,0}, LCD_Offset_Angle[8] = {0,0,0,0,0,0,0,0};

void EncoderConvert(){
  Encoder_Dec = 0;
  Encoder_Dec += ((Encoder_Position[0]) * (pow(2, 9)));
  Encoder_Dec += ((Encoder_Position[1]) * (pow(2, 8)));
  Encoder_Dec += ((Encoder_Position[2]) * (pow(2, 7)));
  Encoder_Dec += ((Encoder_Position[3]) * (pow(2, 6)));
  Encoder_Dec += ((Encoder_Position[4]) * (pow(2, 5)));
  Encoder_Dec += ((Encoder_Position[5]) * (pow(2, 4)));
  Encoder_Dec += ((Encoder_Position[6]) * (pow(2, 3)));
  Encoder_Dec += ((Encoder_Position[7]) * (pow(2, 2)));
  Encoder_Dec += ((Encoder_Position[8]) * (pow(2, 1)));
  Encoder_Dec += ((Encoder_Position[9]) * (pow(2, 0)));
  Angle = Scale * Encoder_Dec;
  Angle_Integer = Angle * 100000;
  LCD_Angle[0] = (Angle_Integer / 10000000);
  LCD_Angle[1] = (Angle_Integer % 10000000) / 1000000;
  LCD_Angle[2] = (Angle_Integer % 1000000) / 100000;
  LCD_Angle[3] = (Angle_Integer % 100000) / 10000;
  LCD_Angle[4] = (Angle_Integer % 10000) / 1000;
  LCD_Angle[5] = (Angle_Integer % 1000) / 100;
  LCD_Angle[6] = (Angle_Integer % 100) / 10;
  LCD_Angle[7] = (Angle_Integer % 10);
}

void OffsetConvert(){
    LCD_Offset_Angle[0] = (Angle_Integer - Offset_Angle_Integer) / 10000000;
    LCD_Offset_Angle[1] = ((Angle_Integer - Offset_Angle_Integer) % 10000000) / 1000000;
    LCD_Offset_Angle[2] = ((Angle_Integer - Offset_Angle_Integer) % 1000000) / 100000;
    LCD_Offset_Angle[3] = ((Angle_Integer - Offset_Angle_Integer) % 100000) / 10000;
    LCD_Offset_Angle[4] = ((Angle_Integer - Offset_Angle_Integer) % 10000) / 1000;
    LCD_Offset_Angle[5] = ((Angle_Integer - Offset_Angle_Integer) % 1000) / 100;
    LCD_Offset_Angle[6] = ((Angle_Integer - Offset_Angle_Integer) % 100) / 10;
    LCD_Offset_Angle[7] = (Angle_Integer - Offset_Angle_Integer) % 10;

Try this test:

unsigned long Angle_Integer = 35999990, Offset_Angle_Integer = 0, Angle_With_Offset;

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

void loop() {
  if (Angle_Integer + Offset_Angle_Integer >= 36000000) {
    Angle_With_Offset = (Angle_Integer + Offset_Angle_Integer) - 36000000;
  } else {
    Angle_With_Offset = Angle_Integer + Offset_Angle_Integer;
  }
  Serial.print("Offset: ");
  Serial.print(Offset_Angle_Integer);
  Serial.print("\tAngle With Offset: ");
  Serial.println(Angle_With_Offset);
  Offset_Angle_Integer++;
  delay(250);
}

Where are you doing substractions ? You are pretty far from the max unsigned long so that feels a bit weird.

When you have long constant you need to give hints to the compiler

So I would write 1000000UL for example in your divisions and modulo

I also would use << instead of multiplication by powers of 2 to get the bits in the right position

I would also change this

Angle = Scale * Encoder_Dec;
Angle_Integer = Angle * 100000;

Given you don’t use Angle, and Scale is a constant you could go straight to

Angle_Integer = (int) ((float) Encoder_Dec * (float) 35156.25);

  Encoder_Dec += ((Encoder_Position[0]) * (pow(2, 9)));

Encoder_Dec += ((Encoder_Position[1]) * (pow(2, 8)));
  Encoder_Dec += ((Encoder_Position[2]) * (pow(2, 7)));
  Encoder_Dec += ((Encoder_Position[3]) * (pow(2, 6)));
  Encoder_Dec += ((Encoder_Position[4]) * (pow(2, 5)));
  Encoder_Dec += ((Encoder_Position[5]) * (pow(2, 4)));
  Encoder_Dec += ((Encoder_Position[6]) * (pow(2, 3)));
  Encoder_Dec += ((Encoder_Position[7]) * (pow(2, 2)));
  Encoder_Dec += ((Encoder_Position[8]) * (pow(2, 1)));
  Encoder_Dec += ((Encoder_Position[9]) * (pow(2, 0)));

Don’t do that. pow() does not work well with integers.
Try this instead:

  Encoder_Dec += ((Encoder_Position[0]) * (1 << 9));
  Encoder_Dec += ((Encoder_Position[1]) * (1 << 8));
  Encoder_Dec += ((Encoder_Position[2]) * (1 << 7));
  Encoder_Dec += ((Encoder_Position[3]) * (1 << 6));
  Encoder_Dec += ((Encoder_Position[4]) * (1 << 5));
  Encoder_Dec += ((Encoder_Position[5]) * (1 << 4));
  Encoder_Dec += ((Encoder_Position[6]) * (1 << 3));
  Encoder_Dec += ((Encoder_Position[7]) * (1 << 2));
  Encoder_Dec += ((Encoder_Position[8]) * (1 << 1));
  Encoder_Dec += ((Encoder_Position[9]) * (1 << 0));

Oh, and if one step of your encoder equals 0.3515625 degree, then any digits shown beyond tenths of a degree will be complete and utter garbage. Even the tenths of a degree will generally be wrong.

Thank you all for your help. I need to comment out some of my code and try these suggestions and see how I get on.

In terms of this part, all of the digits display correctly, including the decimal parts so I didn’t think this was causing any issues. I will try and change to << and see if this makes any difference.

odometer:
Don’t do that. pow() does not work well with integers.
Try this instead:

  Encoder_Dec += ((Encoder_Position[0]) * (1 << 9));

Encoder_Dec += ((Encoder_Position[1]) * (1 << 8));
 Encoder_Dec += ((Encoder_Position[2]) * (1 << 7));
 Encoder_Dec += ((Encoder_Position[3]) * (1 << 6));
 Encoder_Dec += ((Encoder_Position[4]) * (1 << 5));
 Encoder_Dec += ((Encoder_Position[5]) * (1 << 4));
 Encoder_Dec += ((Encoder_Position[6]) * (1 << 3));
 Encoder_Dec += ((Encoder_Position[7]) * (1 << 2));
 Encoder_Dec += ((Encoder_Position[8]) * (1 << 1));
 Encoder_Dec += ((Encoder_Position[9]) * (1 << 0));




Oh, and if one step of your encoder equals 0.3515625 degree, then any digits shown beyond tenths of a degree will be complete and utter garbage. Even the tenths of a degree will generally be wrong.

"When you have long constant you need to give hints to the compiler

So I would write 1000000UL for example in your divisions and modulo"

Does adding these constant formatters make any real difference to my code?

I modified your OffsetConvert function as follows.

void OffsetConvert() {
    unsigned long Display_Angle_Integer;
    if (Angle_Integer >= Offset_Angle_Integer) {
        // in this case the subtraction will not go negative
        // so we just go ahead and do the subtraction
        Display_Angle_Integer = Angle_Integer - Offset_Angle_Integer;
    }
    else {
        // in this case, the subtraction would go negative
        // so we do an addition instead
        Display_Angle_Integer = Angle_Integer + (36000000 - Offset_Angle_Integer);
    }
    LCD_Offset_Angle[0] = Display_Angle_Integer / 10000000;
    LCD_Offset_Angle[1] = (Display_Angle_Integer % 10000000) / 1000000;
    LCD_Offset_Angle[2] = (Display_Angle_Integer % 1000000) / 100000;
    LCD_Offset_Angle[3] = (Display_Angle_Integer % 100000) / 10000;
    LCD_Offset_Angle[4] = (Display_Angle_Integer % 10000) / 1000;
    LCD_Offset_Angle[5] = (Display_Angle_Integer % 1000) / 100;
    LCD_Offset_Angle[6] = (Display_Angle_Integer % 100) / 10;
    LCD_Offset_Angle[7] = Display_Angle_Integer % 10;
}

By the way, why are you showing degrees to five decimal places? Like I said, at least four of those digits will be complete garbage.

A 1024 step encoder can't even represent 0.1 degree accurately. The steps are 360/1024 = 0.35 degree.

Have you tried using 'long' instead of 'unsigned long'? Then the subtraction will just give you a negative number when appropriate.

Somewhere in the code, you would need to take into consideration twice the angle range (0-720) in order to allow for making 0-360 degree offset adjustments. This would solve any “overflow” concerns. Also need to consider if negative offsets are required. Could also use the constrain macro to prevent any out of range settings.

A 16 bit signed integer will work too.

jremington:
A 1024 step encoder can't even represent 0.1 degree accurately. The steps are 360/1024 = 0.35 degree.

Well actually 360/1024 = 0.3515625
As you can tell, I'm quite new to arduino, so this is not just a project I need to get working, but also acts as experimentation. I want to try and use the entire range of the encoder and give 100% precise measurements. You could say I am somewhat a perfectionist in the things I do, but I just believe you should always aim for the best results, not just something that 'will do'.
My display quite rightly will not display 0.1deg, but it can display intervals of 0.3515625

odometer:
I modified your OffsetConvert function as follows.

void OffsetConvert() {

unsigned long Display_Angle_Integer;
    if (Angle_Integer >= Offset_Angle_Integer) {
        // in this case the subtraction will not go negative
        // so we just go ahead and do the subtraction
        Display_Angle_Integer = Angle_Integer - Offset_Angle_Integer;
    }
    else {
        // in this case, the subtraction would go negative
        // so we do an addition instead
        Display_Angle_Integer = Angle_Integer + (36000000 - Offset_Angle_Integer);
    }
    LCD_Offset_Angle[0] = Display_Angle_Integer / 10000000;
    LCD_Offset_Angle[1] = (Display_Angle_Integer % 10000000) / 1000000;
    LCD_Offset_Angle[2] = (Display_Angle_Integer % 1000000) / 100000;
    LCD_Offset_Angle[3] = (Display_Angle_Integer % 100000) / 10000;
    LCD_Offset_Angle[4] = (Display_Angle_Integer % 10000) / 1000;
    LCD_Offset_Angle[5] = (Display_Angle_Integer % 1000) / 100;
    LCD_Offset_Angle[6] = (Display_Angle_Integer % 100) / 10;
    LCD_Offset_Angle[7] = Display_Angle_Integer % 10;
}




By the way, why are you showing degrees to five decimal places? Like I said, at least four of those digits will be complete garbage.

Thank you odo for this code...as soon as I get chance, I will try it out and see how I get on. Same response to you about the angles, I don't 'need' 5 decimal places, but wanted to do a complete working solution to the highest accuracy possible.
I don't see any of the digits as garbage, they all show correctly when I convert from decimal*scale to angle. Why is it you say they will be garbage, I'm not sure I follow what you're explaining.

Again, thanks to everyone for helping me out here. Unfortunately I am now experiencing upload issues with my board, so I can't try anything at the minute, but I'll get it figured out and try the suggestions on here.

I want to try and use the entire range of the encoder and give 100% precise measurements.

Then just use integer encoder steps to represent the angle. If you convert to any angular form (especially floating point representation) you lose precision.

clive_newell85:
' 5 decimal places, but wanted to do a complete working solution to the highest accuracy possible.
I don't see any of the digits as garbage, they all show correctly when I convert from decimal*scale to angle. Why is it you say they will be garbage, I'm not sure I follow what you're explaining.

From what you are telling me, you are using an encoder that can distinguish a little over a thousand positions. Yet you want the angle displayed in a way that suggests the ability to distinguish millions of positions.

clive_newell85:
Well actually 360/1024 = 0.3515625
As you can tell, I'm quite new to arduino, so this is not just a project I need to get working, but also acts as experimentation. I want to try and use the entire range of the encoder and give 100% precise measurements. You could say I am somewhat a perfectionist in the things I do, but I just believe you should always aim for the best results, not just something that 'will do'.
My display quite rightly will not display 0.1deg, but it can display intervals of 0,3515625

I think that's the point - you have about one third of a degree precision coming from your Rotary encoder so when your encoder tells you you are at tick 100, you want to display that you are at 35,15625° (with 5 decimal places) reality is that your arm can be anywhere between 35,15625 ° and 35,5078125°. So what you display is not 100% precise measurements assuming your mechanical setting is perfectly calibrated and leverages the full range of the encoder (i.e. rotary tick 0 is 0°)

So displaying 35,15625° is just as wrong or as right as displaying 35,2°, 35,3° or 35,4° or 35,5° . You just don't know for sure and you are giving your end user the false impression of super extra precision whereas reality is that you are not even sure of your first digit after the decimal point...

I guess what we are saying is that there is very limited value in going beyond the 1° precision, rounded up the right way...

makes sense?

that being said - if your arm is making a smooth movement (i.e. you have a good motor at the back of the arm), you could integrate once or twice the position to get angular speed and acceleration and estimate at a given point in time where you are, even if you did not get a new tick from the rotary encoder.

for example if at time 100ms you are at tick 100 and you know for sure your arm is moving at 360rpm and not accelerating then you can leverage that knowledge to gain probably 1 digit accuracy in real time position.

Ok, now this makes sense.

In truth, I do not need the accuracy, I just wanted to try out using the full scale of the encoder and working with all of the data it provided, just for experimentation purposes, as I said before.

So its not that the data I am calculating is incorrect, its just that the data is only precise at the instant the encoder changes from one tick to the next and any change of angle less than 0.3515625 can not be detected as the encoder will stay within the same tick.

I understand this and actually this does give correct angular measurements to +/- 0.3515624 degrees and will maintain this tolerance across the full range of 360 degrees.

This is not the important issue here though, I only actually need one decimal place tolerance, and I am happy with the encoder angles I am getting.

So its not that the data I am calculating is incorrect, its just that the data is only precise at the instant the encoder changes from one tick to the next and any change of angle less than 0.3515625 can not be detected as the encoder will stay within the same tick.

exactly

and nothing wrong for not shooting at "just good enough" when you can get perfection. Attention to details makes the difference between a great engineer and a "good enough" engineer :slight_smile:

I have a long way to go before I can say I am a great engineer. For starters, trying to get this code to work properly...haha...

I have finally managed to get my board uploading again after installing IDE 1.6.10 which completely stopped everything from working. I have had to downgrade back to 1.6.5 to get uploads to work again...I have no idea what the issue with 1.6.10 was, but does not work at all for me.

So now I can finally try some new sketches to get this offset working.

odometer:
I modified your OffsetConvert function as follows.

void OffsetConvert() {

unsigned long Display_Angle_Integer;
    if (Angle_Integer >= Offset_Angle_Integer) {
        // in this case the subtraction will not go negative
        // so we just go ahead and do the subtraction
        Display_Angle_Integer = Angle_Integer - Offset_Angle_Integer;
    }
    else {
        // in this case, the subtraction would go negative
        // so we do an addition instead
        Display_Angle_Integer = Angle_Integer + (36000000 - Offset_Angle_Integer);
    }
    LCD_Offset_Angle[0] = Display_Angle_Integer / 10000000;
    LCD_Offset_Angle[1] = (Display_Angle_Integer % 10000000) / 1000000;
    LCD_Offset_Angle[2] = (Display_Angle_Integer % 1000000) / 100000;
    LCD_Offset_Angle[3] = (Display_Angle_Integer % 100000) / 10000;
    LCD_Offset_Angle[4] = (Display_Angle_Integer % 10000) / 1000;
    LCD_Offset_Angle[5] = (Display_Angle_Integer % 1000) / 100;
    LCD_Offset_Angle[6] = (Display_Angle_Integer % 100) / 10;
    LCD_Offset_Angle[7] = Display_Angle_Integer % 10;
}

Well odo, what can I say, your code works!! I can now zero the start angle, make a note of the offset from absolute position and as the absolute position goes + or - the start angle does the same minus the offset. Perfect. Thank you very much for your help. Thank you to all for your contributions here. Hopefully with more projects like this one, I will learn enough to give help to others in the future.

Great news!

here is 1 Karma to encourage you to keep progressing.