Tuesday, August 13, 2013

Discrete potentiometer

My car music player is finally coming along to the point where it looks like I might actually finish it. I will do a post about the software soon, and probably a more general post on the hardware later, but right now I want to talk about a knob I will be using on the device.

Your car stereo probably has a volume knob that spins all the way around. You can keep twisting it in either direction as much as you want, and the software will determine when you've topped- or bottomed-out. You probably didn't think too much about why this was the case, but there are some nice features that can be implemented thanks to it.

For one, you can have multiple volume controls. Often there are volume buttons on your steering wheel. A knob with set start and end positions means it is tied to a particular state, but changing the software state somewhere else doesn't change the physical state of the knob. Some car stereos (I know mine does, and I bet most do now) will set the volume to 0 when the car starts, and ramp it up gradually. That way, if a friend borrowed your car and cranked the volume up really high, you won't get a nasty surprise when you get in.

While most knobs are potentiometers with a continuous resistance from 0 to X, knobs like this have discrete states (they 'click' in small increments). To make one of these, I bought a cheap rotary switch. This is a SP12T (single pole, 12-throw) switch, so one input can be connected to any one of 12 outputs. Some rotary switches will let you twist all the way around forever in either direction, but this one did not, so I had to pry the case open and carefully cut out a little piece of plastic that was blocking the gap between positions 1 and 12. This particular switch had a small ball bearing and a spring, both of which I almost lost and I struggled for a few minutes to put everything back together (fair warning).

Now that the switch can spin freely, we need it to be able to turn a setting up or down. To know if the knob is spinning clockwise or counterclockwise (or not at all), we can simply compare the current state of the switch to the previous one. There are a few ways to read the switch state. Connecting every output to its own GPIO pin on your microprocessor would take up 12 pins which is a very inefficient use of your pins. One option would be to connect each pin to the first 12 inputs of a 16-to-4 encoder, which would encode your signal into a 4 bit number, taking up 4 GPIO pins. However, there is still a better way to do this.


By soldering resistors of equal magnitude between each of the positions, we've created what is essentially a discrete potentiometer. In this case, I used 100 ohm resistors, so this is a 11*100 = 1.1K ohm potentiometer (the first state is 0 for 0 ohms, so the last state is 11 for 1100 ohms), but one which can only take values 0, 100, 200, 300... Now we can connect it to an analog pin, and save our digital pins for other things.

There is still one issue here, however. The analogRead() function is going to give integer values from 0 to 1023. That means our values are going to be 0, 85, 171, 256, 341, 427, 512, 597, 683, 768, 853, 939. But these aren't exact. These resistors have a margin of error around 2% (for most resistors, it is around 5%, but I bought more accurate ones for this situation). The supply voltage can fluctuate as well. That means we need to allow for ranges, so maybe 0-42 means position 0, 85-128 means position 1, and so on.

A easy way to do this would be:
   round(12*analogRead(0)/1024);
However, this is a very inefficient command for a microprocessor. First it multiplies two integers, then it divides two integers, creating a float (microprocessors like the Ardunio are very slow at doing floating point math, particularly division). Then it converts that back to an integer. This one line translates to 4 logical steps, which translate into a whole lot of steps for the processor.

The way I implemented this was not nearly as slick, but it runs about 10 times faster. If we look at our predicted analogRead() values in binary:
0000000000
0001010101
0010101011
0100000000
0101010101
0110101011
1000000000
1001010101
1010101011
1100000000
1101010101
1110101011
Notice the first four bits (highlighted in red) are unique for each value. That means that we can get a good idea which state we are in just looking at the 4 most significant bits. If you think about it in decimal: let's say you were going to get numbers between 0 and 1000, and you wanted to quickly tell if that number fell in the first 250 number bin (0 to 249), the second (250 to 499), and so on. You don't really care what the number in the one's place is in this situation. Just by looking at the hundred's and ten's places, you can tell which bin it's in (26X is in the 2nd bin, and 8XX is in the 4th). So back to the binary above, if we trash all the lower-significance bits (which might vary slightly), we can still get the right answer.

There is one little bit of weirdness here. Four bits means 16 possible values, 0000 to 1111. But we only have 12 positions we care about. Look at the binary version of the predicted value of position 3 (0100000000 or 256). If analogRead() returned something a little high, like 275, we would be okay, since the first four bits are still 0100. But what if it returned something a little low, like 245 (binary 0011110101)? This has a smaller value for its four most significant bits. However, notice also that we skipped from 0010 for position 2 to 0100 for position 3 (right over 0011). This is because 1024 is not divisible by 12. In hindsight, getting a 8- or 16- position rotary switch would have been a better plan. If we give both 0011 and 0100 to position 3, we've solved this problem. We have to do this as well for positions 7 and 9 (thanks to the fact that gcd(12,1024)=4). The shortest margin of error that would return a wrong value is then 21 (for example, if the value for position 2 was high by 21-- 192, it would be read as position 3). That is a hair over a 2% margin of error. 

Below is the code I used for this. It is not nearly as concise or elegant as the single line from before, but it is faster by an order of magnitude, as bitwise shifts are computationally cheap. If you have a better way to do this, I would love to hear your suggestion.

   int c = analogRead(0)>>6;
   switch(c){
     case 0:
       return 0;
       break;
     case 1:
       return 1;
       break;
     case 2:
     case 3:
       return 2;
       break;
     case 4:
       return 3;
       break;
     case 5:
     case 6:
       return 4;
       break;
     case 7:
       return 5;
       break;
     case 8:
     case 9:
       return 6;
       break;
     case 10:
       return 7;
       break;
     case 11:
     case 12:
       return 8;
       break;
     case 13:
       return 9;
       break;
     case 14:
       return 10;
       break;
     case 15:
       return 11;
       break;
   }

No comments:

Post a Comment