Enginursday: Detecting Knob Changes

A look into how to decide when a knob has moved, so that your program can take an action.

Favorited Favorite 3

I make a lot of projects that tend to fit the same formula. The project has a microcontroller; some core function the object is to perform; and knobs, LEDs, buttons and other UI elements. When it comes to writing the software for said project, I always need the same thing from the UI elements. When an input changes, my program must do something in response.

This “Enginursday” focuses on how to detect a deliberate change in a knob position while rejecting noise. I’ve made a Processing sketch that displays the parameters of the detection system live, which helps me feel the knob response while testing the UI — and is also a nice way to demonstrate what these essential filters do to the signal being processed.

I demonstrate:

  • Simple thisState lastState comparison – snippet
  • Masking LSBs – snippet
  • Arithmetic range, or threshold – snippet
  • Hysteretic behavior – snippet
  • Windowed behavior – snippet
  • Averaged, windowed behavior

Have a look at the video and read on for more information!



The code and resources here aren’t intended to be something you can just grab off the shelf and use; it’s a work in progress, and I generally do it for “fun.” But I do want to talk about a few things.

The Knob Box

The knob box is a Proto Pedal with Teensy 3.2 and Teensy Audio board installed and the addition of a TeensyView screen for debugging.

The Processing Sketch

Ok, ok, so you got me. There’s text on the OLED screen, so it can’t be the same sketch I show in the video. What’s actually going on is that the box is packetizing data from a structure, then serializing it as 7-bit ASCII and shipping it out the USB serial interface.

On the other end, the Processing sketch parses the serial stream, decodes back to binary and builds a structure that Java can understand.

The graphical elements are truly a kludge of operations that uses some custom fonts, builds the seven-segment look and draws the graph items. It’s been customized for each of the examples, and can be found in the associated “Graphic X” folders within the DynamicGraph repository.

The ACTUAL Implementation of the Knobs

In the video I hold up a book, Design Patterns: Elements of Reusable Object-Oriented Software (ISBN-13: 978-0201633610). As a hardware guy, this book is very difficult for me to wrap my head around, but has helped immensely with coming up with a scheme that solves a problem of divergent libraries. I was able to create a system that fits my needs. I call it uCModules.

The goals for the library:

  • Have abstract logic components that define behavior
  • Have abstract hardware components to communicate with hardware
  • Be extendable from the user code space without modifying libraries

The complexity increase of using design patterns has so far paid off. As an example, I’ve added a new hardware interface, which connects knob inputs to ADCs through analog muxes, to give me 64 ADCs while using only four physical channels and scanning. All of this is done without modifying the library itself. Also, for this post I needed to add getters to the logic so I could get the internal data, which is normally private. Again this was done without touching the library source.

If you’d like more information, there are class diagrams available, as well as an API reference. While you’re free to poke around, It is still a work in progress! I’m currently looking for a better way to document it, as well as making a timing scheme that isn’t convoluted — or better yet, using an RTOS.

Happy filtering,
Marshall


Comments 16 comments

  • Please write “Processing” instead of “processing” when you write about the specific software. Upper case is your friend.

  • I love hysteresis for detecting changes! It has made my life so easy so many times.

    A great companion post to this one would be: how to measure how much noise your knob reading has. I’ve seen too many programs where the knob (or other device) just can’t provide the accuracy the maker is trying to get, because there is too much noise in the reading. Noise measurement and statistics can remove a lot of frustration.

    My favorite technique of measuring noise is to log the raw readings when I’m not moving the knob, then perform some basic statistics on those readings. One simple statistic is the minimum and maximum value read during the test period. (maximum - minimum) / 2 gives a quick and simple (and probably over-conservative) measure of the noise threshold. Statistics folks prefer calculating Standard Deviation, because you can use that measurement to design how reliable you want your change detection to be. See https://www.mathsisfun.com/data/standard-deviation-formulas.html for how to calculate Standard Deviation.

  • I don’t know the expectation for the code, but I think this does it with a smaller footprint and maybe more clear logically.

    if( input != currentValue){  //Skip if value has not changed
        if( hystState==0 && input <  currentValue - hysteresis ) hystState = 1;          
        if( hystState==1 && input >  currentValue + hysteresis ) hystState = 0;
        NewData=1
    }
    currentValue = input;  //Always update 
    
    • Again, newData is being set whenever the input != the current value. We need it to only set newData when is trending one way, or passes the opposite threshold. The inner if statements will need to be expanded and newData=1 put in each maybe?

      I recall getting confused by this logic and ending up just writing something I could understand. I think no matter what it uses 3 comparisons per loop, though I’m using up a bit more program memory with the redundant code. I haven’t looked at the disassembly to see what’s actually going on.

      I found the windowed approach to me much better for usability, and think it’s interesting that the code for it came out so much shorter.

      • I’m missing something I guess. In the snippet, newData is set in all four cases, and they all fall though if there has been no change. Hmmm. Are you saying this detects changes as it executes? Not like a case statement, but dependent on the order of the IF statements, which execute based on the update that may have happened in the previous if statement? I think I see.

        • The order of the if statements don’t matter, their comparisons are contradictory so the second could (should) be an else if. I think what you’re missing is the hidden case, when both if statements (lines 4 and 10) are not true, newData is not set. This is the case where the movement is against the trend, but not past the threshold.

          • I looked at the problem a bit, and with some assumptions about the overall code, I think this will do it.

             if( abs( input-currentValue) > hysteresis){
                 newData = 1;
                 currentValue = input;
             }
            
            • I’ll have to throw this code in and see how it behaves! I think this will act like the windowed topology.

              I wonder what the difference in program size / memory usage would be?

              • I thought it fit the hysteresis version.

              • Yes, give it a try. I can think of reasons it will be faster and reasons it will be slower :-) Smaller, I’m pretty sure about.

                The other code does 1, 2, or 3 IF’s, and the inner IF’s test a math result, so this just might be faster on the average as well.

    • if( input != currentValue){ //Skip if value has not changed 
          if( hystState==0 && input < currentValue - hysteresis ) hystState = 1;
          if( hystState==1 && input > currentValue + hysteresis ) hystState = 0; 
          NewData=1 
      } 
      currentValue = input; //Always update
      
  • //—Hysteresis10BitKnob——————————————————–

    if(hystState == 0){ 
    
        if( input < currentValue - hysteresis ) hystState = 1;          
        if( input > currentValue + hysteresis ) hystState = 0;
    
        currentValue = input;
        NewData=1
    }
    
    • No good. In the original code, NewData is set only when a new value is detected. In this code, NewData is set every time when hystState is zero, and never when it is not zero.

  • The complexity increase of using design patterns…

    There’s no increase. In fact, there’s a net decrease since you’re removing a lot of complexity from the debug/test/certification phases.

    Think of design patterns as standard plumbing fixtures at Home Despot. Is it actually less complex to build your own fixtures from scratch to no recognized standards using any old part that happens to inspire you? Or is it better to use one that has a well publicized standard interface (1 ¼" drain pipe, handles that turn in the accepted directions, etc.)? Especially when you have to replace parts in an existing bathroom.

    Design patterns require you to follow some rules, but overall they make things simpler and easier. Of course, they can be misused by people who only think they understand them, and then they get the blame. But that’s true for anything, plumbing included.

  • A rule I like to use when doing averaging on ADC is try to keep them powers of two. Then you can use shifts to do your divided rather than using a divide which in some cases may take longer to preform.

Related Posts

Recent Posts

Tags


All Tags