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.
-
1Solve 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– Jot2018年10月27日 14:47:14 +00:00Commented Oct 27, 2018 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– st20002018年10月27日 14:54:24 +00:00Commented Oct 27, 2018 at 14:54
-
map will do alrmSwState/512, returning 2 only for 1023Juraj– Juraj ♦2018年10月27日 16:02:10 +00:00Commented Oct 27, 2018 at 16:02
4 Answers 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
.
-
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– fertchen2018年10月28日 07:18:01 +00:00Commented Oct 28, 2018 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 theif
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– fertchen2018年10月28日 15:34:36 +00:00Commented Oct 28, 2018 at 15:34 -
@fertchen: The switching between 1 and 2 in the mid position is normal and expected. That's why
case 1
andcase 2
lead to the same statements in the example code above. If you have more than three cases, the simplest option may be to usemap()
. Otherwise, a chain ofelse if
, though less elegant, is likely more efficient thanmap()
.Edgar Bonet– Edgar Bonet2018年10月28日 20:21:23 +00:00Commented Oct 28, 2018 at 20:21
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, 02018年10月27日 15:50:57 +00:00Commented Oct 27, 2018 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– Jeff Wahaus2018年10月27日 15:57:57 +00:00Commented Oct 27, 2018 at 15:57
-
sorry, I forgot that Calc rounds to the nearest. I upvoted2018年10月27日 15:59:33 +00:00Commented Oct 27, 2018 at 15:59
-
and map will do alrmSwState/512 which is not correct2018年10月27日 16:01:07 +00:00Commented Oct 27, 2018 at 16:01
-
Why 342 and not 343? I like to see comments in the code that explains all the numbers.Jot– Jot2018年10月27日 18:34:48 +00:00Commented Oct 27, 2018 at 18:34
Just three lines of code are needed for the conversion.
almSet=0;
if (alrmSwState>300) almSet++; // increment almSet
if (alrmSwState>600) almSet++;
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.