Is there a way to calibrate analog inputs?

Status
Not open for further replies.

Geekness

Well-known member
So after a very long time, I finally have my code functioning (some what).
The problem I have now is that the output sbus value doesn't match the percentage of the pot.

The pot (joystick) im using is a 3.3V, 3-axis joystick (X,Y,Z), with hall sensors for each axis.
The values for the end points I should be getting are 172 and 1811.
What im actually getting are 520 and 1520
My code (Started from the example on brtaylor's sbus library on github) already uses a scale factor and a bias, that is for all channels, but how can I re-scale or re-bias the individual channels?

Im thinking of using something like a calibration script (activated by perhaps making a digital pin go high) to determine the actual min and max values and store them for each analog input. I just don't know where to start.
Does anybody have suggestions?

Also, if the teensy input voltage varies at all, will the output value vary?

Code:
#include <TimerOne.h>
#include "SBUS.h"

// output SBus on serial port 1
SBUS Transmit(Serial1);

//CH01: Roll (pin [A13])
uint16_t CH01 = A13;
int CH01Value = 0;
int CH01out = 0;

const int ledPin = 13; //just a heartbeat to show that the code is going

void setup() {
    pinMode(ledPin,OUTPUT);
    pinMode(A13,INPUT);
    
  // serial to display the channel commands for debugging
  Serial.begin(115200);

  // begin the SBUS communication
  Transmit.begin();

  // setup the analog read resolution to 16 bits
  analogReadResolution(16);

  // setup an interrupt to send packets every 9 ms
  Timer1.initialize(9000);
  Timer1.attachInterrupt(sendSBUS);
}

void loop() {
 //heartbeat timing
  digitalWrite(ledPin, HIGH);  // set the LED on
  delay(200);                  // wait for a 0.2 seconds
  digitalWrite(ledPin, LOW);   // set the LED on
  delay(100);                  // wait for a 0.1 seconds
  digitalWrite(ledPin, HIGH);  // set the LED on
  delay(200);                  // wait for a 0.2 seconds
  digitalWrite(ledPin, LOW);   // set the LED off
  delay(1000);                 // wait for a second
}

// reads analog and digital inputs and sends an SBUS packet
void sendSBUS() {
  float scaleFactor = 1639.0f / 65535.0f;
  float bias = 172.0f;
  uint16_t channels[16];
  
  CH01Value = analogRead(CH01); // read the analog input
    CH01out = (uint16_t)(((float)CH01Value) * scaleFactor + bias); // linearly map the analog measurements (0-65535) to the SBUS commands (172-1811)
    Serial.print("CH01: ");
    Serial.print(CH01out); // print the channel command (172-1811)
    Serial.print("\t");

    Serial.println();

  // write the SBUS packet to an SBUS compatible servo
    Transmit.write(&channels[0]);
}
 
If it were me, I would do some analog reads and check the MIN/MAX values and then map them to the range you need.

Again you can do something simple like:
Code:
uint16_t CH01 = A13;
uint16_t min_value = 0xffff;
uint16_t max_value = 0;
uint16_t last_value = 0;

void setup() {
    while (!Serial) ; // wait for serial
    Serial.begin(115200);
    analogReadResolution(16);
}
void loop() {
    uint16_t new_value = analogRead(CH01);
    if (new_value != last_value) {
        last_value = new_value;
        if (new_value > max_value) max_value = new_value;
        if (new_value < min_value) min_value = new_value;
        Serial.printf("%d : %d %d\n", new_value, min_value, max_value)
    }
    delay(50); // not sure if delay needed or how long to go between samples...
}

Then you would need to run for each of your analog values and remember the min/max values you recorded for each axis/pot...

Note with joysticks, I tended to record the min/max of moving directly up/down left/right and not the diagonal values as the diagonal values were probably larger or smaller, but I did not want max output values to be only available at those angles, I wanted them when I was straight up...

Again not knowing how the analog inputs are coming in... It is hard to know how tell for sure what what inputs you are actually receiving... So sorry if I am going slightly off topic, but:

A long while ago when I made my own Remote control (Lynxmotion days), I would have a calibrate mode for the remote control, that I would move all of the controls to their logical min/max values and save these values away (EEPROM) and use those to map into the ranges you want...

Also if some of the controls were something like spring loaded joysticks, I would also remember the center analog value.

Again depending on if the control was simple thing like Knob or slider, it was a simple map function to map, your input value to the correct value. Note: Map did not check end points, so my code would check if the value was > MAX use max and < MIN use min...

For those controls that were spring loaded joysticks, I would do the map in a couple of steps. Again check and handle values out of range. I would map all values within some delta of CENTER to the mid value, and if the value < Center I would use the map function to map value from (LOW, CENTER, low output, mid output) and if above center from (CENTER, HIGH, mid output, high output)...
 
Hehehe, I just did this a couple of days ago for a pin that measures a 12V power supply voltage.

I used a high precision volt meter to measure the voltage input to the ADC pin, and then based on that I came up with a correction factor that is used to multiply the result. The correction factor is saved into EEPROM. If you use a single point for correction the code is very simple: V_actual = V_computed * correction_factor. If you use 2 or more points it can get very complicated, but one point should be enough for most usage.
 
Is there anyway for the teensy to record the min and max values for each analog input itself?
Or do i just have to read the values from the serial monitor and record them myself?
 
You can save calibration factors to EEPROM
https://www.arduino.cc/en/Reference/EEPROM
but you do need to work out if you will order a cal and then exercise the axis through full range or if you will do a cyclic process watching for max/min values ever recieved and work from those - do be careful using that since doing things like unplugging the input will produce new and incorrect max/min values you would need to fix.
 
Is there anyway for the teensy to record the min and max values for each analog input itself?
Or do i just have to read the values from the serial monitor and record them myself?

You can program some calibration routine that checks for max and min values during the first, say, 10 seconds after you plugged in your joystick, and subsequently use those as max and min values.
Obviously, you need to reach all extremes during that 10 seconds for the calibration to work.

You can check for max & min all the time, but that will likely to make your code much more complicated and will use more CPU time.

If you were to do a one-time calibration (like what I did with a 12V measuring resistive divider) then the serial monitor would be perfectly fine.
 
Thanks all.
I'll have to start looking into EEPROM. I've only just learned how to read an analog input, now I'm learning this. So much to learn, so little time. :p

Anyway, if I don't use the eeprom for now, how can I use the actual input max and min values in my formula?
I can get them by just using the serial monitor to read the analog values then record them manually. I just can't figure out the math.
 
You can program some calibration routine that checks for max and min values during the first, say, 10 seconds after you plugged in your joystick, and subsequently use those as max and min values.
Obviously, you need to reach all extremes during that 10 seconds for the calibration to work.

I think I will program the code to activate the calibration script only when one of my spare digital inputs goes high. maybe to look for that high input only in the first 5 seconds or so.
 
You can save calibration factors to EEPROM ... do be careful using that since doing things like unplugging the input will produce new and incorrect max/min values you would need to fix.


You could define a reset variable set to True if the EEPROM values are nil OR (optionally) when a switch is pulled LOW.

In the code for my guitar pick joystick the maxRead[] array could be replaced by a call to read from an EEPROM segment and then add an 'AND' condition that reset = True along with the inequality.

Then reset would be set back to False after some timeout to allow the user to explore the full range while tracking the new max/min values.

In my application it's not unreasonable to ask the user to calibrate on starting as the only impact is the controls are overly sensitive on the first push in each of four directions and the player would likely explore the physical range instinctively before playing.
 
I just posted my example as it was similar but my case I was able to assume the center return was in the middle of the range. Tracking movement differently by quadrant adds a lot of complexity so see if you need it first.
...The values for the end points I should be getting are 172 and 1811...
What resolution is this. 11 bit? And why would you not expect the full range to start? I only went back to the op to see if an offset was likely in your case but now I fear I don't understand.

I may have joined a thread without studying it suffiencently.
 
When reading the analog inputs to store in the eeprom, should I read them as 16bit?
The eeprom can only store an 8bit number (0-255), but my overall sbus output code uses 16bit reads.

Maybe for storing the data in the eeprom, I should just use 8 bit reads as it just end points?
 
Yes - 16 bit or values can easily be written. May be other ways but perhaps this example will be enough:

Open: File / examples / EEPROM / eeprom_put

There is a corresponding GET example - and doing a web search on that may find other examples
 
You can use EEPROM.put(address, variable); to store variables of any size in the EEPROM. Make sure to increase the address by the size in bytes when storing the next value.
Code:
uint16_t aTwoByteInt = 0xABCD;
uint32_t aFourByteInt = 0x12345678;
uint32_t address = 0;
EEPROM.put(address, aTwoByteInt);
address = address +2; //or use address + sizeof(aTwoByteInt)
EEPROM.put(address, aFourByteInt);
address = address +4;

EEPROM.get(address, variable); will read the data back
 
I was looking at the write example.
I'll check out the put example.

Can you please have a look at my code so far.
When it comes to the part to assign the address for "EEPROM.write", im not sure im calling the addresses correctly from the array.

Code:
#include <EEPROM.h>

unsigned int addr[] = {0,1,2,3,4,5,6,7,8,9,10}; //EEPROM address'
int pins[] = {A13,A11,A19,A1,A18,A14,A0,A12,A20,A10}; //Analog pins to read
int pinCount = 10; //number of analog pins

void setup(){  //Setup the pins as inputs
for(int p=0; p<pinCount; p++)
    pinMode(pins[p], INPUT);
}

void loop(){
for(uint8_t i = 0; i < pinCount; i++) {
    pins[i] = analogRead(i) / 4;} //Read the analog inputs
for(unsigned int a = 0; a < pinCount; a++) {
    addr = a;
}
    EEPROM.write(addr, pins);
}

ps, I haven't started working on just the min and max values yet, just working on being able to write to the eeprom first.
 
Using Ben's code - perhaps dropped into the indicated example would be better. Unless you want to lose resolution - the 'write' examples are misleading at best.

For analog read the pins should not be set to input - that makes them digital/binary - that code commented.

Writing and rewriting the EEPROM this often is not sound - it has limited life a few 10's of thousands or more writes and it will be unusable. It should be done only as often as needed to recover the data.

This is not tested but should be closer - updated to use .put and made a few corrections as I saw them:

Code:
#include <EEPROM.h>

unsigned int addr[] = {0,1,2,3,4,5,6,7,8,9,10}; //EEPROM address'
uint16_t pins[] = {A13,A11,A19,A1,A18,A14,A0,A12,A20,A10}; //Analog pins to read
int pinCount = 10; //number of analog pins

void setup(){  //Setup the pins as inputs
// for(int p=0; p<pinCount; p++)
//     pinMode(pins[p], INPUT);
}

void loop(){
  for (uint8_t i = 0; i < pinCount; i++) {
    pins[i] = analogRead(i);} //Read the analog inputs
  }
  for (unsigned int a = 0; a < pinCount; a++) {
    EEPROM.put( sizeof(uint16_t)*addr[a], pins[a]);
  }

[B]  delay( 10000 ); // even this is too often for extended running - with no delay it will kill EEPROM in under an hour if not minutes perhaps.
[/B]
}
 
Awesome, thanks. Ill work through this over the weekend.

I didn't realise that setting the pinmode to input makes it digital. I have some code that seems to be working, and I have my pinmode set as input. See below.
With this code, my analog input is outputting the values I mentioned in the original post above.

For every time I do a calibration, I only want to write once. The calibration will be set for when a pin goes high. Then it will only write the min and max values taken after 10 seconds of calibration. then it will stop and wait for the pin to go low again, and continue to operate as normal.

Code:
#include <TimerOne.h>
#include "SBUS.h"

// output SBus on serial port 1
SBUS Transmit(Serial1);

//CH01: Roll (pin [A13])
uint16_t CH01 = A13;
int CH01Value = 0;
int CH01out = 0;

const int ledPin = 13; //just a heartbeat to show that the code is going

void setup() {
    pinMode(ledPin,OUTPUT);
    pinMode(A13,INPUT);
    
  // serial to display the channel commands for debugging
  Serial.begin(115200);

  // begin the SBUS communication
  Transmit.begin();

  // setup the analog read resolution to 16 bits
  analogReadResolution(16);

  // setup an interrupt to send packets every 9 ms
  Timer1.initialize(9000);
  Timer1.attachInterrupt(sendSBUS);
}

void loop() {
 //heartbeat timing
  digitalWrite(ledPin, HIGH);  // set the LED on
  delay(200);                  // wait for a 0.2 seconds
  digitalWrite(ledPin, LOW);   // set the LED on
  delay(100);                  // wait for a 0.1 seconds
  digitalWrite(ledPin, HIGH);  // set the LED on
  delay(200);                  // wait for a 0.2 seconds
  digitalWrite(ledPin, LOW);   // set the LED off
  delay(1000);                 // wait for a second
}

// reads analog and digital inputs and sends an SBUS packet
void sendSBUS() {
  float scaleFactor = 1639.0f / 65535.0f;
  float bias = 172.0f;
  uint16_t channels[16];
  
  CH01Value = analogRead(CH01); // read the analog input
    CH01out = (uint16_t)(((float)CH01Value) * scaleFactor + bias); // linearly map the analog measurements (0-65535) to the SBUS commands (172-1811)
    Serial.print("CH01: ");
    Serial.print(CH01out); // print the channel command (172-1811)
    Serial.print("\t");

    Serial.println();

  // write the SBUS packet to an SBUS compatible servo
    Transmit.write(&channels[0]);
}
 
Status
Not open for further replies.
Back
Top