Elegant code to separate ADC result into a few discrete states?

Go To Last Post
21 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I have an ADC input set up to reads a rotary switches using a resistive divider chain to return some descrete states when a given selection is made.

 

I'm trying to come up with an elegant code function to robustly decode those states from the raw ADC result (10bit)

 

If the states were evenly spaced (by choosing the correct resistor values for the divider chain) then i could just (adc_result>>7) to get 7 states with 128bit wide bins.

 

That's easy.

 

But what if the states are not evenly spaced or i want say 11 states or something?

 

I guess a Switch / Case cascade is the next option, truth checking the result, ie

 

switch (adc_result)
{
case >911
switch_state = 11
break;

case > 785
switch_state = 10
break;

// add more cases here

}

 

But have i missed some obvious and easy way to do this ?

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Do you use avr-gcc? If so then using -std=gnuXX rather than -std-cXX gives access to "GNU extensions" and one of those is:

switch(foo) {
    case  0 .. 57: stuff; break
    case 58 .. 93: stuff; break
    case 94 .. 211: stuff; break
    etc etc
}

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 1

Sorry not enough dots!! Manual here:

 

https://gcc.gnu.org/onlinedocs/g...

 

tells me:

switch(foo) {
    case  0 ... 57: stuff; break;
    case 58 ... 93: stuff; break;
    case 94 ... 211: stuff; break;
    etc etc
}

so THREE dots each time.

 

You'd probably use something like -std=gnu99. If you use AS7 I think this is the default anyway.

Last Edited: Fri. Jun 1, 2018 - 03:11 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Interesting, I wasn't aware of that gcc extension.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 1

max_torque_2008 wrote:
I guess a Switch / Case cascade is the next option

No - a standard C switch cannot do that. You would need a non-standard language extension  - as Cliff suggests.

 

If you want to keep it standard, I think it'd have to be either a string of if/else, or search through lookup table.

 

The lookup table would only need to contain the transition points.

Top Tips:

  1. How to properly post source code - see: https://www.avrfreaks.net/comment... - also how to properly include images/pictures
  2. "Garbage" characters on a serial terminal are (almost?) invariably due to wrong baud rate - see: https://learn.sparkfun.com/tutorials/serial-communication
  3. Wrong baud rate is usually due to not running at the speed you thought; check by blinking a LED to see if you get the speed you expected
  4. Difference between a crystal, and a crystal oscillatorhttps://www.avrfreaks.net/comment...
  5. When your question is resolved, mark the solution: https://www.avrfreaks.net/comment...
  6. Beginner's "Getting Started" tips: https://www.avrfreaks.net/comment...
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

the Arduino LCD Keypad shield has something similar for the buttons, here is how it's handled:

int read_LCD_buttons(){               // read the buttons
    adc_key_in = analogRead(0);       // read the value from the sensor 

    // my buttons when read are centered at these valies: 0, 144, 329, 504, 741
    // we add approx 50 to those values and check to see if we are close
    // We make this the 1st option for speed reasons since it will be the most likely result

    if (adc_key_in > 1000) return btnNONE; 

    // For V1.1 us this threshold
    if (adc_key_in < 50)   return btnRIGHT;  
    if (adc_key_in < 250)  return btnUP; 
    if (adc_key_in < 450)  return btnDOWN; 
    if (adc_key_in < 650)  return btnLEFT; 
    if (adc_key_in < 850)  return btnSELECT;  

   // For V1.0 comment the other threshold and use the one below:
   /*
     if (adc_key_in < 50)   return btnRIGHT;  
     if (adc_key_in < 195)  return btnUP; 
     if (adc_key_in < 380)  return btnDOWN; 
     if (adc_key_in < 555)  return btnLEFT; 
     if (adc_key_in < 790)  return btnSELECT;   
   */

    return btnNONE;                // when all others fail, return this.
}

 

 

(Possum Lodge oath) Quando omni flunkus, moritati.

"I thought growing old would take longer"

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0
    if (adc_key_in < 50)   return btnRIGHT;  
    if (adc_key_in < 250)  return btnUP; 
    if (adc_key_in < 450)  return btnDOWN; 
    if (adc_key_in < 650)  return btnLEFT; 
    if (adc_key_in < 850)  return btnSELECT;  

I can hear the MISRA Gods turning in their grave !!

 

Functions with multiple exit points are unmanageable. All it needed was:

btnEnum read_LCD_buttons(){               // read the buttons
    btnEnum retval = btnNONE;

    adc_key_in = analogRead(0);       // read the value from the sensor 

    if (adc_key_in > 1000) {
        retval = btnNONE; 
    }
    else if (adc_key_in < 50) {
        retval = btnRIGHT;  
    }
    else if (adc_key_in < 250) {
        retval = btnUP; 
    }
    else if (adc_key_in < 450) {
        retval = btnDOWN; 
    }
    else if (adc_key_in < 650) {
        retval = btnLEFT; 
    }
    else if (adc_key_in < 850) {
        retval btnSELECT;  
    }

    return retval;                // when all others fail, return this.
}

(which also highlights a "hole" between 850 and 1000 !)

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

the code in #7 really show how MISRA code can make a nice and easy read code, big and bulgy!

(and the code give a wrong value if nothing is pushed )

 

 

and if 

adc_key_in < 450

had been 

450 > adc_key_in 

it would have been even worse but I guess more like how MISRA should be.

 

 

For OP

I don't really get your question, but the way I understand your setup you actually ask for an exponential/log function with ADC in and output 1..11 or whatever your resolution is.

 

To find that put your pot (encoder) in the different positions you want to use (it's probably the transitions points you want to tune), with those points make a curve fitting.

Probably a 2. or 3. order polynomial will do the trick.         

 

add:

I guess it's I missed the first line so the code is correct, but for me just show how 5 easy read and grouped lines, can be to 20+lines because it has to be easy to read !!!

 

Last Edited: Sat. Jun 2, 2018 - 04:54 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

"(and the code give a wrong value if nothing is pushed )"

 

No : the arduino LCd + potentiometric "keybeard" sends +Vcc (; once converted 1022...1023 depending on noise and luck, never between 860 and 1000, unless components are very old and rusty) if nothing is pressed (whatever the version....)

 

 if (adc_key_in > 1000) {
        retval = btnNONE;
    } // is followed by an ifelse cascad : retVal is affected only once in this cascade.... (at least, I hope

 

BTW : coding standards are meant (I suppose) to avoid typos such as "if (a = 5) " instead of "if (a == 5) " , same thing as "if (5 == a)" : omitting an equal sign wil lead to a compilation error., thus typo detection .. but with a "<" sign, which typo does it detect?.

 

A fully ridiculous, brute force , solution would be a huge array , linking each and every ADC output (a 10 bit index, with avr ADC; as things improve, array would become huger on other MCUs) to the button number. ... Nobody dared to write such a constant array...

Last Edited: Sat. Jun 2, 2018 - 04:42 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Probably the most efficient way to do what you want is to do what is effectively a "binary search". Use the first conditional statement to divide the reading into the upper half or the lower half.

Another conditional to divide that half into upper or lower quarters, and then do the same again. In three steps you have isolated one of eight potential states. One more step will be good for 16 different states. I think you will be able to achieve your goal with three or four comparisons, no more.

Switch-case statements essentially are multiple conditional statements in a row. If the reading is the 16th value, you will have had to process 16 conditionals to get there.

On average you will have eight conditionals to perform for a situation of 16 potential states.

The binary search pattern is much more efficient.

-Tony

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

so how do you know that the C compiler don't make a binary search on a switch ?

Last Edited: Sun. Jun 3, 2018 - 07:35 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Well, the only way to know for sure is to look at the assembly output of the compiler.

However, I am virtually certain, without checking, that switch-case statements turn into a series of conditionals and are not achieved by a binary search technique .

If you write the code properly, then you do not need to rely on the vagaries of an individual compiler with hopes that it will come up with optimal code for unusual situations.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I don't know you exactly mean with  write the code properly  ​but I will be surprised if a binary search for 7 to 11 state will be more "optimal" (faster and/or smaller) than a direct search (witch I have seen that some make a switch out of.).

Remember a switch is a key part of C so there are a lot of effort put into optimizing it, and for sure a binary search is one of many options to solve it.

 

But as I wrote in #8 I don't really thing that a switch is the better solution but and 2 order poly is the better solution.

 

But perhaps OP can give some numbers to play with :) , especially if the used AVR have a HW multiplier it can be both the smaller and faster solution.        

Last Edited: Sun. Jun 3, 2018 - 12:39 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Well, it would be simple to determine how the compiler handles the switch statement.  Check it on the simulator...   I am too busy to verify it myself.  As I said, I am 98% certain that switch-case statements turn in to a chain of something equivalent to if-then-else statements.  Therefore the time it takes to search through switch-case statements is proportional to N.  A binary search takes time proportional to log base 2 of N

 

For a few items in a switch-case tree, it is not a big difference between the two.  E.G. for a series of 8 items, the switch-case series will take on average 4 checks.  The binary search would be most of the time 3 checks.  Not a big difference at all.  But with 16 items the switch-case will take on average 8 checks, and the binary search would usually take 4 checks. 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I guess given the relatively small overhead, then the basic switch/case architecture is looking the easiest.  For this application that only needs to keep up with a humans inputs (slow) then i'm not going to be reading the value that often.  I think probably at 30 hz, ought to be fast enough. Then maybe, if we get 3 or 4 values the same (or close enough) (ie after 99 to 132ms) then i'll process the command through the decoder and return the "state"

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

But what if the states are not evenly spaced or i want say 11 states or something?

Why not go ahead and make them (nearly) evenly spaced? Assuming an ADC of 0-255, you can easily do an 8 bit divide to get the "setting".

When in the dark remember-the future looks brighter than ever.   I look forward to being able to predict the future!

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

So I wrote a little binary search by hand:


enum BUTTON {
	BTN_A;
	BTN_B;
	BTN_C;
	BTN_D;
	BTN_PLAY_NICE;
	BTN_ANNIHILATE;
	BTN_EXPLODE;
	BTN_NONE;
};

enum BUTTON getButton( uint16 adc)
{
	if( adc < 512) {
		if( adc < 256) {
			if( adc < 128 {
				retun BTN_A;
			}
			else {
				retun BTN_B;
			}
		}
		else { // adc 256 ... 511
			if( adc < 384) {
				retun BTN_C;
			}
			else {
				retun BTN_D;
			}
		}
	}
	else // adc >= 512
	{

	}
}

 

Not elegant, but still relatively easy to understand, and it executes fast.

But for readability I would probably go for a chain of if() else if().

All those nested if( ) statements are a bit messy.

Compromize:

Use 2 levels, First cut in half, then use chained if( ) else if( ).

Relatively fast and clean.

But it is probably micro seconds only to execute. Probably not worth the time.

 

I'm not sure if a function can return an enum.

I do know that you can use an enum in a switch( ) in the modern variants of C (++), and you can then let the compiler generates warnings if not all enum values are handled in the switch( ).

 

But encapsulating the button routine in a function removes the clutter of it's implementation from your main program.

 

In main you can then use a switch( )...

This is double, the work for the poor AVR.

But what you get back is more readable code with the enum names.


    switch( getButton( ADC)) {
    case BTN_A;
        break;
    case BTN_B;
        break;
    case BTN_C;
        break;
    case BTN_D;
        break;
    case BTN_EXPLODE;
        break;
}

Internal representation of a switch( ) is meat for the optimizer.

Compiler could make an if() else if() chain, do a binary search, or even a table driven approach with direct jumps.

 

Even though I am unlikely to ever use another compiler then GCC for my projects, still do not like non standard compiler  specific extensions.

 

Idea about the table...

If you use an array[], and make it 2 or 4x bigger then the number of buttons, you can compensate for trigger levels by the number of times you repeat a certain button in the array.

Fast & easy to understand / maintain.

You "waste" a few bytes for the table, but you avoid all the code for multiple if() else if() statements, and the end result is probably smaller.


int8_t buttonTable[] = {  // Should be in flash...
  BTN_A;
  BTN_A;
  BTN_B;
  BTN_C;
  BTN_C;
  BTN_C;
  BTN_D;
  BTN_MORE_BUTTONS;
};

switch ( buttonTable[ ADC >> 6]){
    case ...:

}

Doing magic with a USD 7 Logic Analyser: https://www.avrfreaks.net/comment/2421756#comment-2421756

Bunch of old projects with AVR's: http://www.hoevendesign.com

Last Edited: Mon. Jun 4, 2018 - 03:11 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

There are drawbacks with optimized searches :

readability is more difficult. What happens if a new resistive divider is released?

 

Resistive divider interfaces with avr through ADC, which is slow (10s of microseconds) . 11 tests (a 11 state resistive divider needs accurate resistors; arduino resistive divider has 4 +1 states) would be much faster (even with intervals and uint16_t).

 

Making a lookup table would involve loss of accuracy if one wants a reasonable length table, and a lot of typing if one finds a new resistive divider (and before, one should understand what was written months ago) .... (arduino's resistive divider  seems to have two versions ...at least).

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Just to note that the "easy to read" one:

typedef enum {
	BTN_A,
	BTN_B,
	BTN_C,
	BTN_D,
	BTN_PLAY_NICE,
	BTN_ANNIHILATE,
	BTN_EXPLODE,
	BTN_NONE
} BUTTON;

__attribute__((noinline)) BUTTON getButton(uint16_t n) {
    BUTTON retval = BTN_NONE;
    switch(n) {
        case   0 ... 127: retval = BTN_A; break;
        case 128 ... 255: retval = BTN_B; break;
        case 256 ... 383: retval = BTN_C; break;
        case 384 ... 512: retval = BTN_D; break;
    }
    return retval;
}

generates:

avr-gcc -mmcu=atmega16 --short-enums -Os avr.c -g -o avr.elf
00000082 <getButton>:
        BTN_NONE
} BUTTON;

__attribute__((noinline)) BUTTON getButton(uint16_t n) {
    BUTTON retval = BTN_NONE;
    switch(n) {
  82:   8f 3f           cpi     r24, 0xFF       ; 255
  84:   91 05           cpc     r25, r1
  86:   09 f0           breq    .+2             ; 0x8a <getButton+0x8>
  88:   28 f4           brcc    .+10            ; 0x94 <getButton+0x12>
  8a:   80 38           cpi     r24, 0x80       ; 128
  8c:   91 05           cpc     r25, r1
  8e:   58 f4           brcc    .+22            ; 0xa6 <getButton+0x24>
        case   0 ... 127: retval = BTN_A; break;
  90:   80 e0           ldi     r24, 0x00       ; 0
  92:   08 95           ret
        BTN_NONE
} BUTTON;

__attribute__((noinline)) BUTTON getButton(uint16_t n) {
    BUTTON retval = BTN_NONE;
    switch(n) {
  94:   80 38           cpi     r24, 0x80       ; 128
  96:   21 e0           ldi     r18, 0x01       ; 1
  98:   92 07           cpc     r25, r18
  9a:   38 f0           brcs    .+14            ; 0xaa <getButton+0x28>
  9c:   81 30           cpi     r24, 0x01       ; 1
  9e:   92 40           sbci    r25, 0x02       ; 2
  a0:   30 f4           brcc    .+12            ; 0xae <getButton+0x2c>
        case   0 ... 127: retval = BTN_A; break;
        case 128 ... 255: retval = BTN_B; break;
        case 256 ... 383: retval = BTN_C; break;
        case 384 ... 512: retval = BTN_D; break;
  a2:   83 e0           ldi     r24, 0x03       ; 3
  a4:   08 95           ret

__attribute__((noinline)) BUTTON getButton(uint16_t n) {
    BUTTON retval = BTN_NONE;
    switch(n) {
        case   0 ... 127: retval = BTN_A; break;
        case 128 ... 255: retval = BTN_B; break;
  a6:   81 e0           ldi     r24, 0x01       ; 1
  a8:   08 95           ret
        case 256 ... 383: retval = BTN_C; break;
  aa:   82 e0           ldi     r24, 0x02       ; 2
  ac:   08 95           ret
        BTN_EXPLODE,
        BTN_NONE
} BUTTON;

__attribute__((noinline)) BUTTON getButton(uint16_t n) {
    BUTTON retval = BTN_NONE;
  ae:   87 e0           ldi     r24, 0x07       ; 7
        case 128 ... 255: retval = BTN_B; break;
        case 256 ... 383: retval = BTN_C; break;
        case 384 ... 512: retval = BTN_D; break;
    }
    return retval;
}
  b0:   08 95           ret

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I just made a small test with the way I think I would do it (don't have the same solution as Cliff but more like the one OP needs)

And I'm surprised how similar the compiler output is :

__attribute__((noinline)) BUTTON getButton(uint16_t n) {
	BUTTON retval = BTN_NONE;
	switch(n) {
		case   0 ... 127: retval = BTN_A; break;
		case 128 ... 255: retval = BTN_B; break;
		case 256 ... 383: retval = BTN_C; break;
		case 384 ... 512: retval = BTN_D; break;
	}
	return retval;
}

__attribute__((noinline)) uint8_t getButtonjnl(uint16_t n) {
	uint8_t retval = 0;
	if (n>=128) retval++;
	if (n>=256) retval++;
	if (n>=384) retval++;
	if (n>=512) retval++;
	return retval;
}

and the two outputs  (without comments because they confuse more than help)

(I'm not sure the compiler version but at relative new update of studio 7 )

compiler with -Os

 

__attribute__((noinline)) BUTTON getButton(uint16_t n) {
  80:	8f 3f       	cpi	r24, 0xFF	; 255
  82:	91 05       	cpc	r25, r1
  84:	09 f0       	breq	.+2      	; 0x88 <getButton+0x8>
  86:	28 f4       	brcc	.+10     	; 0x92 <getButton+0x12>
  88:	80 38       	cpi	r24, 0x80	; 128
  8a:	91 05       	cpc	r25, r1
  8c:	58 f4       	brcc	.+22     	; 0xa4 <getButton+0x24>
  8e:	80 e0       	ldi	r24, 0x00	; 0
  90:	08 95       	ret
  92:	80 38       	cpi	r24, 0x80	; 128
  94:	21 e0       	ldi	r18, 0x01	; 1
  96:	92 07       	cpc	r25, r18
  98:	38 f0       	brcs	.+14     	; 0xa8 <getButton+0x28>
  9a:	81 30       	cpi	r24, 0x01	; 1
  9c:	92 40       	sbci	r25, 0x02	; 2
  9e:	30 f4       	brcc	.+12     	; 0xac <getButton+0x2c>
  a0:	83 e0       	ldi	r24, 0x03	; 3
  a2:	08 95       	ret
  a4:	81 e0       	ldi	r24, 0x01	; 1
  a6:	08 95       	ret
  a8:	82 e0       	ldi	r24, 0x02	; 2
  aa:	08 95       	ret
  ac:	87 e0       	ldi	r24, 0x07	; 7
  ae:	08 95       	ret


__attribute__((noinline)) uint8_t getButtonjnl(uint16_t n) {
  b0:	80 38       	cpi	r24, 0x80	; 128
  b2:	91 05       	cpc	r25, r1
  b4:	78 f0       	brcs	.+30     	; 0xd4 <getButtonjnl+0x24>
  b6:	8f 3f       	cpi	r24, 0xFF	; 255
  b8:	91 05       	cpc	r25, r1
  ba:	71 f0       	breq	.+28     	; 0xd8 <getButtonjnl+0x28>
  bc:	68 f0       	brcs	.+26     	; 0xd8 <getButtonjnl+0x28>
  be:	80 38       	cpi	r24, 0x80	; 128
  c0:	21 e0       	ldi	r18, 0x01	; 1
  c2:	92 07       	cpc	r25, r18
  c4:	58 f0       	brcs	.+22     	; 0xdc <getButtonjnl+0x2c>
  c6:	81 15       	cp	r24, r1
  c8:	92 40       	sbci	r25, 0x02	; 2
  ca:	10 f0       	brcs	.+4      	; 0xd0 <getButtonjnl+0x20>
  cc:	84 e0       	ldi	r24, 0x04	; 4
  ce:	08 95       	ret
  d0:	83 e0       	ldi	r24, 0x03	; 3
  d2:	08 95       	ret
  d4:	80 e0       	ldi	r24, 0x00	; 0
  d6:	08 95       	ret
  d8:	81 e0       	ldi	r24, 0x01	; 1
  da:	08 95       	ret
  dc:	82 e0       	ldi	r24, 0x02	; 2
  de:	08 95       	ret

  And it don't solve my code as written, and that surprise me a bit because with -Os I would expect that the end at least was something like:

L0:     inc r24
L1:     inc r24
L2:     inc r24
L3:     inc r24
        ret

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

   Sometimes (or often) you know which cases in a switch are likely to occur more often, so you place them (if possible) at the beginning. In case the switch is solved with a binary search, the advantage is lost, so the way the switch is resolved should be known to the programmer. Or is there a rule (be it unwritten) that says that in case of known occurrence use ifs, and in case of random (unknown) occurrence use a switch, hoping that is resolved with a binary search or other means other than a simple scan ?