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.

  • 1
    Solve it with the analog value, not with the map function. The tilting points are at 256 and 768. Below 256 is 0, between 256 and 768 is 1 and above 768 is 2. – Jot Oct 27 '18 at 14:47
  • You have chosen a tricky method. Likely you will need to use both hysteresis and averaging. Averaging will slow down the response - no matter how fast the processor is. If dead set on using a rotary input device, most would choose a rotary encoder which is digital in nature. – st2000 Oct 27 '18 at 14:54
  • map will do alrmSwState/512, returning 2 only for 1023 – Juraj Oct 27 '18 at 16:02
5

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.

  • Thank you very much for pointing out how to properly use the map function (never heard of semi-open type before in this context). Also thanks for pointing me to a more efficient method. – fertchen Oct 28 '18 at 7:18
  • I just tried using the uint: In mid position the case sometimes switches between 1 & 2 due to noise like touching an insulated wire (alrmSwState ~ 509-514). Therefore the if condition won't work as intended. As a workaround I am dividing by 250 with resulting usable cases 0, 2 & 4. This is not as efficient as you intended. Considering I am using an odd number of switch positions (meaning one position very close to the center / break point) am I right in my assumption that there is no way for me to get the division done as elegant as you suggested without adding a substantl amount of code? – fertchen Oct 28 '18 at 15:34
  • @fertchen: The switching between 1 and 2 in the mid position is normal and expected. That's why case 1 and case 2 lead to the same statements in the example code above. If you have more than three cases, the simplest option may be to use map(). Otherwise, a chain of else if, though less elegant, is likely more efficient than map(). – Edgar Bonet Oct 28 '18 at 20:21
4

Just three lines of code are needed for the conversion.

almSet=0;
if (alrmSwState>300) almSet++;   // increment almSet
if (alrmSwState>600) almSet++;
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)

  • this is wrong. it will give 3, 2, 1, 0 – Juraj Oct 27 '18 at 15:50
  • It is correct. The max value returned is 1023 when divided by 342 = 2.991... which will give the integer division result of 2. – Jeff Wahaus Oct 27 '18 at 15:57
  • sorry, I forgot that Calc rounds to the nearest. I upvoted – Juraj Oct 27 '18 at 15:59
  • and map will do alrmSwState/512 which is not correct – Juraj Oct 27 '18 at 16:01
  • Why 342 and not 343? I like to see comments in the code that explains all the numbers. – Jot Oct 27 '18 at 18:34
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.