1

I am investigating the use of a 3-position-switch for one of my projects to switch between different settings using just one analog input pin. For the three positions I am using ground/0V, ~2.5V (via 2x10k voltage divider) and full 5V. I am doing this also because I am planning to use a 5-position rotary switch in a similar way in the future.

Due to tolerances and noise, the readings on the analog pin are not precisely 0, 511, 1023, but rather 0-1, 509-512 and 1021-1023. To take this into account, I am using the map function like this:

const byte alarmSwitchPin = A0;
int alrmSwState;
byte alrmSet;
byte alrmPrevSet;

void setup() {
  Serial.begin(9600);
  pinMode(alarmSwitchPin, INPUT);

}

void loop() {

  alrmSwState = analogRead(alarmSwitchPin);       // Read input

  byte alrmSet = map(alrmSwState, 0, 1023, 0, 2); // Boil input down to three cases

  if (alrmSet != alrmPrevSet) {                   // Only if state of switch has changed
    switch (alrmSet) {
      case 0:
        Serial.println("Low");
        break;
      case 1:
        Serial.println("Medium");
        break;
      case 2:
        Serial.println("High");           
        break;
    }
    alrmPrevSet = alrmSet;
    delay(200);                                   // For testing purposes
  }
}

I only want the setting to change (or, in this case, the console to print) when the state has actually changed. This does not work, though, because the map function is doing its math in integer, which fails in mapping the High position (in this case).

What are my options if I want to stick with the switch method? I am curious if it is possible to get this done without if statements and conditions or, as a more general question, what the best option is to properly map the 10 bit analog input to just 3 cases/numbers.

PS: I thought analogReadResolution() might be an option but I am working with Arduino UNO where this is unavailable.

fertchen
  • 37
  • 8

4 Answers4

4

The simplest fix to your problem is to change the map() call to

byte alrmSet = map(alrmSwState, 0, 1024, 0, 3);

In the call above, the mapped intervals are of the semi-open type, e.g. [a, b), where the start values (namely 0) are understood as being inclusive and the end values (1024 and 3) are exclusive.

Although not clear from the documentation, this is the proper way to use the map() function. Otherwise, the truncated division gives you very uneven intervals. Compare:

     x       map(x, 0, 1023, 0, 2)
----------------------------------
   0 –  511     0
 512 – 1022     1
1023            2

     x       map(x, 0, 1024, 0, 3)
----------------------------------
   0 –  341     0
 342 –  682     1
 683 – 1023     2

The result you get is very close to Jeff Wahaus’ answer.

What I find annoying about this approach is that a 32-bit integer division, which map() uses internally, is a very expensive operation on the small 8-bit Arduinos. If instead of 342 and 683, you use 256 and 768 as thresholds, then you can make the decision by just looking at the high byte of the analog reading:

uint16_t alrmSwState = analogRead(alarmSwitchPin);

alrmSet = alrmSwState / 256;

if (alrmSet != alrmPrevSet) {
    switch (alrmSet) {
        case 0:
            Serial.println("Low");
            break;
        case 1:
        case 2:
            Serial.println("Medium");
            break;
        case 3:
            Serial.println("High");           
            break;
    }
    alrmPrevSet = alrmSet;
    delay(200);
}

Note that the division by 256 is optimized by the compiler into a much cheaper bit shift, but this is the case only if alrmSwState is of an unsigned integer type. That's why it is declared above as uint16_t.

Edgar Bonet
  • 45,094
  • 4
  • 42
  • 81
3

Replace the line:

byte alrmSet = map(alrmSwState, 0, 1023, 0, 2);

with

byte alrmSet = alrmSwState / ((1023 / 3) + 1);

And this should do what you want. Note that in C fractional results are truncated (not rounded)

Jeff Wahaus
  • 583
  • 2
  • 5
2

Just three lines of code are needed for the conversion.

almSet=0;
if (alrmSwState>300) almSet++;   // increment almSet
if (alrmSwState>600) almSet++;
jsotola
  • 1,554
  • 2
  • 12
  • 20
0

You might also consider reading the value several times and summing them, then base your decision on the sum.

This is equivalent to averaging the values but without dividing by 'n', since you don't care about the actual number, just the band it falls into. This will have the effect of improving the signal to noise ratio, since we assume that high-reading errors are as likely as low-reading errors, so their sum tends toward zero as the sum of the real value, V, tends toward (n*V).

Use an int or larger variable for the sum, depending on how many readings you choose to take each time.

JRobert
  • 15,407
  • 3
  • 24
  • 51