How to get accurate high resolution potentiometer readings

Status
Not open for further replies.

m6k

Member
Hello there!

I want to build a MIDI controller with potentiometers that sends out 14 bit MIDI messages.
I bought a Teensy 3.6 because of the supposedly high resolution and the usbMIDI function.
But first tests are very disappointing - I just connected 1 potentiometer on a breadboard and nothing else and the readings were catastrophic... it seemed to have something to do with the jumper cables, so i tried different ones, which surprisingly really worked better, but still I couldn't even get 10 bit stable resolution.
With the ResponsiveAnalogRead library with sleep mode enabled I could finally get it to read stable 10 bit values - but not more than that.
On an Arduino Nano I can get the same stability even with the bad cables.

So what can I do to get a stable reading anywhere near 13 bit?
Could it be that I have bad potentiometers? is there such a thing? I use 5kOhm - are other sizes preferrable?
I noticed an improvement when I touched the ground - could it be that the teensy is not properly grounded?
I have read about connecting capacitors to the inputs, but I don't have any. I will order a few 10pF capacitors, since that size was recommended somewhere. How is the size of the capacitor determined and how much does it help?

I want to build a midi controller with about 60 potentiometers, connected via 8 multiplexers (TC4051BP) on a breadboard (so I'm scared now that things will only get worse).
I would then probably connect a capacitor to each input of a multiplexer - if I would connect 1 to each of the 8 Analog inputs that read the multiplexers I would probably get strange readings since they switch really fast, right?

The code I use to send 14 bit MIDI is quite simple:
Code:
#include <ResponsiveAnalogRead.h>
const int ANALOG_PIN = A0;
ResponsiveAnalogRead analog(ANALOG_PIN, true);

int channel = 6;
int k1cc1 = 0;
int k1cc2 = 60;

int k1 = 0;int k1old = 0;

void setup() {
  analogReadResolution(10);
  analog.enableEdgeSnap();
  analog.setAnalogResolution(1024);
}

void loop() {
  analog.update();
  k1 = analog.getValue();
  if (k1 != k1old){
    usbMIDI.sendControlChange(k1cc1, (k1 >> 3), channel);
    usbMIDI.sendControlChange(k1cc2, (k1 << 4), channel);
    k1old = k1;
  }
}
It works fine, although I don't understand why it's (k1 << 4) for the least significant bits, but it gives me the correct output of the last 3 bits in MIDI.
 
Last edited:
Compared to the simple and cheap but robust 10bit ADCs in old fashioned AVR hardware, the 16bit (13bit usable) SAR ADCs in the KINETIS CPUs allow a 4 to 8 times higher resolution and up to 50 times more speed but, due to their much more sophisticated principle of operation, these have also much more requirements when it comes to the signal source impedance, decoupling, grounding, shielding, and noise filtering. All this is well documented in the processor’s reference manual and in several application notes which you should study thoroughly before putting just whatever potentiometers and multiplexers together. Potentiometers should for example not exceed 10kOhm to keep the inner resistance low enough and a 10nF capacitor should be added from each wiper to ground, to minimize the impact of the varying load due to the switches capacitor input during the SAR analog digital conversion process. CMOS multiplexers are far from being ideal switches, they add up to several hundreds Ohms to the source impedance which is naturally registered by these highly sensitive ADCs. Another common “problem” into which people run with such configurations is the higher speed of the Teensy processors vs that old AVR stuff: After setting a multiplexer via GPIO, additional wait cycles might be needed to allow the slow cmos multiplexer to settle before you start the ADC readings.

You write that you get better results when touching GND, which proves that your setup has obviously a grounding problem. The Teensy can not be blamed for that, it’s always a question of its power external supply. Powering it via USB from a computer gives not forcibly good grounding, these switches mode power supplies have most times dirty spikes which may not be noticeable with less sensitive ADCs but which can spill everything when raising the sensitivity bar.
 
I want to build a midi controller with about 60 potentiometers, connected via 8 multiplexers (TC4051BP) on a breadboard (so I'm scared now that things will only get worse).
...it will definitely get worse!

And passive filtering before a mux doesn't work because it delays a stable reading at the pin after each mux switch.

You can't be using single turn pots as you'd have 50-60 fine points difference with every degree of rotation.

Why would you need a controller lie this?

If you were to build this I'd say software filtering, hardware shielding, and a separate very quiet power supply with 1k pots would be the best bet to get close to 14 bits.
 
Thank you for your answers!
I'm pretty much a total noob in these things so I might have been a little naive... I will have to see if I can get above 10 bits when the missing parts arrive... if not I will probably settle for the Nano.

As to why I want this:
I want to have a controller that gives me the full experience of an analog synthesizer. With 10 bits I can still select distinct steps when turning very carefully, with 11 it will probably get very difficult and with 12 or more it would probably feel like a real analog connection. So 10 bits are quite nice and probably enough, with 11 I could for example control 2 octaves of tuning in ~1.2 cent steps and anything beyond would just be really nice and probably feel like the real thing - from how it looks I will probably settle for 10, but anything less would be really disappointing. Maybe I can manage to get the 4 knobs that matter (oscillator tuning and filter cutoff) at 11 or 12 bits and the rest at 10, which would probably be enough to have everything feeling real analog.
 
Thank you for your answers!
I'm pretty much a total noob in these things so I might have been a little naive... I will have to see if I can get above 10 bits when the missing parts arrive... if not I will probably settle for the Nano.

As to why I want this:
I want to have a controller that gives me the full experience of an analog synthesizer. With 10 bits I can still select distinct steps when turning very carefully, with 11 it will probably get very difficult and with 12 or more it would probably feel like a real analog connection. So 10 bits are quite nice and probably enough, with 11 I could for example control 2 octaves of tuning in ~1.2 cent steps and anything beyond would just be really nice and probably feel like the real thing - from how it looks I will probably settle for 10, but anything less would be really disappointing. Maybe I can manage to get the 4 knobs that matter (oscillator tuning and filter cutoff) at 11 or 12 bits and the rest at 10, which would probably be enough to have everything feeling real analog.
 
If you have for example a good microphone which is very sensitive, would you blame it for picking up too much ambient noise, although it‘s not the microphone‘s but the noisy environment‘s fault?

Away from rhetoric questions, back to your project: Why would one build such a device with many potentiometers in 2018 where large touch screens are affordable and would allow much finer control? Imagine two touch sliders returning values between 0 and 127 and thus allowing to tune halftone-steps with the first one and midi-cents with the second one? Or imagine to organize your controls in several screens with a hierarchical menu, so that using the device would become more intuitive instead of being confronted to 60 identical looking potentiometer knobs? One day, you feel the need for 10 additional potentiometers and this could be done in half an hour at zero material cost just by a software update?
 
Last edited:
I just like knobs more than touchscreens and really like the idea of high resolution knobs. I would love to have analog gear but can't afford it - but I could build something with great sound in reaktor and control it "analog."

In the meantime I have done another testrun on a Nano with a Toshiba TC4051BP Multiplexer and it was already more horrifying - it reads almost stable, but instead of 0-1023 it goes something like 270-980...
At first googleing it seemed most people can just read such a multiplexer without problems... this DIY stuff is somehow beginning to seem less fun than I expected :D
PS: I have tried with delays after switching but this is not the problem, at least on the Nano... will have to do another test with the teensy)

Since I already have most parts for my controller now I will probably try another multiplexer like the CD74HC4067, in hope that it's designed to work with an arduino.

This is the code of my second experiment (two potentiometers on the first two inputs of the multiplexer, other inputs grounded over 10k resistor - sending out custom 14-bit-midi 60CC's apart):
Code:
int channel = 3;
int k1cc1 = 0;int k1cc2 = 60;
int k2cc1 = 1;int k2cc2 = 61;
int k1 = 0;int k1old = 0;
int k2 = 0;int k2old = 0;
int LSB = 0;
int CC = 0;
int counter = 0;
bool bit1 = 0;
bool bit2 = 0;
bool bit3 = 0;

void setup() {
  CC = 175 + channel;
  pinMode(2,OUTPUT);
  pinMode(3,OUTPUT);
  pinMode(4,OUTPUT);
  Serial.begin(38400);
}

void loop() {
  for (counter=0;counter<=7;counter++) {
    bit1 = bitRead(counter,0);
    bit2 = bitRead(counter,1);
    bit3 = bitRead(counter,2);
    digitalWrite(2,bit1);
    digitalWrite(3,bit2);
    digitalWrite(4,bit3);
    if (counter==0) {
      k1 = 0.6*k1 + 0.4*analogRead(A0);
    }
    if (k1 != k1old){
      Serial.write(CC);
      Serial.write(k1cc1);
      Serial.write(k1>>3);
      LSB = k1<<4;
      bitClear(LSB,7);
      Serial.write(CC);
      Serial.write(k1cc2);
      Serial.write(LSB);
      k1old = k1;
    }
    if (counter==1) {
      k2 = 0.6*k2 + 0.4*analogRead(A0);
    }
    if (k2 != k2old){
      Serial.write(CC);
      Serial.write(k2cc1);
      Serial.write(k2>>3);
      LSB = k2<<4;
      bitClear(LSB,7);
      Serial.write(CC);
      Serial.write(k2cc2);
      Serial.write(LSB);
      k2old = k2;
    }
  }
}
 
Last edited:
The control response 'feel' you're after is largely the responsibility of the soft-synth.

Typically the resulting signal from the midi messages is low-pass filtered to smooth out the zipper noise.

This can be done at sample rate whereas trying to de-zipper with fine-adjustments in MIDI could cause massive floods of messages that would cripple the system before coming close to sample rate.

DIY softsyth packages typically have something for this. I don't know Reaktor but I'm pretty sure it would; SynthMaker (now FlowStone) has had a programming object for this from its early days. I'm sure Max for Live could handle this sort of thing if Live doesn't do it natively.

But there's nothing stopping you from sending interpolated midi messages even beyond the effective resolution of your voltage divider to mimic a smooth higher resolution transition.

Just send interpolated values at regular intervals at a maximum slew rate until the current reading is within some threshold.

Seven bits is plenty to capture lumbering human movements in most cases but if you want to smooth the curve further then message bandwith is your limiting factor as Teensy 3.6 has plenty of horsepower to pump out more messages than you would want. But I would resort to this only if needed as a midi barrage seems the wrong way to approach the problem.

Also... controller pins need time to stabilze after the MUX switches. Changing MUX won't likely give you better output if you're not allowing time for the pins to adjust after the MUX switches.
 
OK... so... I built a small prototype on a breadboard with a Nano and the 4051 Multiplexer with 8 knobs... it works OK but I have to smooth and cut off a bit on both ends so right now I get stable values from 0-1000.

My plans to get better Readings now are:

-put 0.1uF capacitors on the inputs of the multiplexer - and then maybe even shield the wires that go from multiplexer to Analog Input
-maybe put 0.01uF capacitors between the potentiometer wiper and ground
-put one of each of those two types of capacitors in parallel together with a ferrite to clean up the output of the Teensy/Nano, like so: http://andybrown.me.uk/2015/07/24/usb-filtering/
-put those ferrite enlcosure things on the USB cable (like the ones that are on old screen cables)

I think the last two measures are probably the most important ones. The noise gets louder when the pots are at maximum, so I probably get a lot of noise from the USB power supply.

About the device again : :)
I don't want interpolation for smooth automation, although smooth automation might be a nice side effect of a precise controller, it's more about precision and range for me.
10 bit might be the perfect resolution for a potentiometer knob since you can still select distinct steps - I will see if I can get 11 or 12 bits, but I will most certainly get at least 10 and be happy with it.
I used my small prototype to control oscillator pitches and filter of the NI Monark with 1000 steps, which is about twice the resolution you get with the mouse - and it's super nice, just like a real analog synth. With regular 7 bit midi this is not so much fun.
If I can get to 12 bit I would probably use oscillators with a range of 4 octaves on one knob and it would be like the analog oscillators on a modular system.
And if VCV Rack will get 14 bit midi support for knob automation I might even build me a modular midi controller with real eurorack size front plates and interchangeable modules.

Oh and... On the Nano the regular 4051 multiplexers work well without waiting or reading twice... on the Teensy I will probably have to insert a delay...
What would be the best way to do this? I guess using delay() would not be very clever... reading twice might be nice... will have to see on the next test run.

The thing I built last time (before my last post) was a total wreck... I now suspect that the breadboard I used was somehow damaged or didn't hold the wires well - with the new breadboard the multiplexers are fine (at least on the Nano - maybe I should switch over to the arduino forum :) I still don't know if I will use the Teensy or Nano for this one, but either way I will probably build a second device and at least one of those with a Teensy - which is an absolutely amazing device of course)
 
Last edited:
this is my code for now: (theres probably a more clever way to write it, but I guess the machine will perform it like this anyway?)

Code:
const int channel = 6;
const int k1cc1 = 0;const int k1cc2 = 60;
const int k2cc1 = 1;const int k2cc2 = 61;
const int k3cc1 = 2;const int k3cc2 = 62;
const int k4cc1 = 3;const int k4cc2 = 63;
const int k5cc1 = 4;const int k5cc2 = 64;
const int k6cc1 = 5;const int k6cc2 = 65;
const int k7cc1 = 6;const int k7cc2 = 66;
const int k8cc1 = 7;const int k8cc2 = 67;
const int b1cc = 120;
const int b1start = 2;const int b1step = 25;
int k1 = 0;int k1old = 0;
int k2 = 0;int k2old = 0;
int k3 = 0;int k3old = 0;
int k4 = 0;int k4old = 0;
int k5 = 0;int k5old = 0;
int k6 = 0;int k6old = 0;
int k7 = 0;int k7old = 0;
int k8 = 0;int k8old = 0;
int b1 = 0;bool b1state = LOW;
int LSB = 0;
int CC = 0;
int counter = 0;
bool bit1 = LOW;
bool bit2 = LOW;
bool bit3 = LOW;

void setup() {
  CC = 175 + channel;
  b1 = b1start;
  pinMode(4,OUTPUT);
  pinMode(3,OUTPUT);
  pinMode(2,OUTPUT);
  pinMode(5,INPUT);
  Serial.begin(38400);
}

void loop() {
  for (counter=0;counter<=7;counter++){
    bit1 = bitRead(counter,0);
    bit2 = bitRead(counter,1);
    bit3 = bitRead(counter,2);
    digitalWrite(4,bit1);
    digitalWrite(3,bit2);
    digitalWrite(2,bit3);
    if (counter==0) {
      k1 = 0.9*k1 + 0.1*analogRead(A0);
      if (k1 != k1old){
        Serial.write(CC);
        Serial.write(k1cc1);
        Serial.write(k1>>3);
        LSB = k1<<4;
        bitClear(LSB,7);
        Serial.write(CC);
        Serial.write(k1cc2);
        Serial.write(LSB);
        k1old = k1;
      }
    }
    if (counter==1) {
      k2 = 0.9*k2 + 0.1*analogRead(A0);
      if (k2 != k2old){
        Serial.write(CC);
        Serial.write(k2cc1);
        Serial.write(k2>>3);
        LSB = k2<<4;
        bitClear(LSB,7);
        Serial.write(CC);
        Serial.write(k2cc2);
        Serial.write(LSB);
        k2old = k2;
      }
    }
    if (counter==2) {
      k3 = 0.9*k3 + 0.1*analogRead(A0);
      if (k3 != k3old){
        Serial.write(CC);
        Serial.write(k3cc1);
        Serial.write(k3>>3);
        LSB = k3<<4;
        bitClear(LSB,7);
        Serial.write(CC);
        Serial.write(k3cc2);
        Serial.write(LSB);
        k3old = k3;
      }
    }
    if (counter==3) {
      k4 = 0.9*k4 + 0.1*analogRead(A0);
      if (k4 != k4old){
        Serial.write(CC);
        Serial.write(k4cc1);
        Serial.write(k4>>3);
        LSB = k4<<4;
        bitClear(LSB,7);
        Serial.write(CC);
        Serial.write(k4cc2);
        Serial.write(LSB);
        k4old = k4;
      }
    }
    if (counter==4) {
      k5 = 0.9*k5 + 0.1*analogRead(A0);
      if (k5 != k5old){
        Serial.write(CC);
        Serial.write(k5cc1);
        Serial.write(k5>>3);
        LSB = k5<<4;
        bitClear(LSB,7);
        Serial.write(CC);
        Serial.write(k5cc2);
        Serial.write(LSB);
        k5old = k5;
      }
    }
    if (counter==5) {
      k6 = 0.9*k6 + 0.1*analogRead(A0);
      if (k6 != k6old){
        Serial.write(CC);
        Serial.write(k6cc1);
        Serial.write(k6>>3);
        LSB = k6<<4;
        bitClear(LSB,7);
        Serial.write(CC);
        Serial.write(k6cc2);
        Serial.write(LSB);
        k6old = k6;
      }
    }
    if (counter==6) {
      k7 = 0.9*k7 + 0.1*analogRead(A0);
      if (k7 != k7old){
        Serial.write(CC);
        Serial.write(k7cc1);
        Serial.write(k7>>3);
        LSB = k7<<4;
        bitClear(LSB,7);
        Serial.write(CC);
        Serial.write(k7cc2);
        Serial.write(LSB);
        k7old = k7;
      }
    }
    if (counter==7) {
      k8 = 0.9*k8 + 0.1*analogRead(A0);
      if (k8 != k8old){
        Serial.write(CC);
        Serial.write(k8cc1);
        Serial.write(k8>>3);
        LSB = k8<<4;
        bitClear(LSB,7);
        Serial.write(CC);
        Serial.write(k8cc2);
        Serial.write(LSB);
        k8old = k8;
      }
    }
  }
  if (digitalRead(5) == HIGH){
    if (b1state==LOW){
      b1 = b1 + b1step;
      if (b1>127){b1 = b1start;}
      Serial.write(CC);
      Serial.write(b1cc);
      Serial.write(b1);
      b1state = HIGH;
    }
  }
  else {
    b1state = LOW;
  }
}
as you might notice theres also a push button in it now
 
Status
Not open for further replies.
Back
Top