Jump to content
Why become a member? ×

Trampa v3 - DIY MIDI Controller


SamIAm
 Share

Recommended Posts

3 hours ago, 3below said:

Not that long ago I wrote a  page turner Blue Tooth app for ESP32 to drive my tablet with music tabs etc.  This involved short and long button detection, debounce etc.  I tried several libraries and eventually reached a simple approach adapted from somewhere that I can not now remember / find.  I have pmd you some code that is straightforward and seemed effective. HTH.

 

In Arduinoland, ezButton is pretty handy. https://arduinogetstarted.com/tutorials/arduino-button-library - the button array is good for multiple buttons. Setting the pressed time when isPressed is true and then comparing it when isReleased is true and seeing what the delay was.

 

I did a similar ESP32 page turner using bluetooth - made it to send page up/down, line up/down, or next/prev window with a display to say which it was doing. That was before I encountered ezButton so it debounced by spotting the button press, hanging on briefly, then checking if the button was still pressed. Subsequently I saw another method which I thought was very clever - there's a variant on it at https://www.e-tinkers.com/2021/05/the-simplest-button-debounce-solution/ (go down to "The simplest debounce function").

  • Like 3
Link to comment
Share on other sites

Mostly been working on some houskeeping.

 

  • I "think" I've got the Cmake configuration sorted (I'm new to writing CMakeLists so it's been ... fiddly!)
  • It has been a couple of years or so since I last used VSCode in anger (And that was for TypeScript work) ... I've been adding in all the nice extensions/settings that I like (eg Rainbow Indent, Git Lens, Error Lens, Commitizen, Better Comments)  I'm sure I've missed some but my dev  environment is now humming along nicely.  At some point I'll need to eat the frog of getting the debugger working ... but not just yet!
  • I've done the first checkin of the codebase to git. (My intention is to share this once I've reached a MVP).
  • The PICO SDK can automagically hook up printf etc to the serial output to make debug printg easy ... however ... when this approach is used, it it rather selfish and will not let other play with the USB and this is incompatabile with my USB for Serial and MIDI approach.  So I've written a crude but effective wrapper around sprintf and serial send to allow me to achieve a serial printf.  It's been my first forray into variadic functions.
  • Following on from the link that @tauzeroshared about easy button debouncing I stumbled across http://www.ganssle.com/ which has a wealth of information on writing code on embedded systems.

After a bunch of reading I am going to try to incorporate FreeRTOS into Trampa to keep the architecture simple.  It provides a widely used, robust and very tiny kernel that supports multitasking (including multicore!) with the stuff that makes it easy like queues, semaphores, mutexes etc.  It also has a set of libraries that bring in stuff like Http (when and if I reach the Trampa web based config functionality).

 

So my next goal is to convert my current "MIDI Note Player with Serial Debug Print" example so that it runs under FreeRTOS.

 

Fingers crossed ...

 

S'manth x

 

Edited by Smanth
  • Like 1
Link to comment
Share on other sites

Certainly sounds like you are diving straight in!

I hate cmake with a passion (and not a huge fan of VSCode either), but they are good for that sort of thing if you can deal with their stupid ways and mostly unavoidable if you are using a bunch of different other peoples libraries. 

 

I haven't done anything about footpedals yet - I am a few months behind you and I am still not sure how I want to use the device, it could well be I can use the mvave as it is.

  • Like 1
Link to comment
Share on other sites

Progress ...

Wired up my buttons with a long cable so I can connect it to the prototyping breadboard my PICO lives on.

Managed to integrate FreeRTOS so that now all activities are multi threading, it was not as fiendishly difficult as I feared ... the code was very straightforward, the CMake configuration took much longer.

 

I've now got a system that will print when it detects button presses, a hop skip and a jump away from being able to fire off simple MIDI control messages.

Then the logic to properly debounce the buttons as well as detect single/double/long taps.

 

S'manth x

 

  • Like 3
Link to comment
Share on other sites

Some progress made on detecting button presses.

I found a nifty algorithm to debounce presses (it uses an integration approach).

After a lot of reading it seems that detecting presses using interrupts is generally not considered a good idea, so I am now having a background timer fire a "button tick" every 5ms that will check button state, seems to be working.

think I've got a state machine sorted for the buttons, this will be run (for each button) when the "button tick" occurs.

 

image.thumb.png.4cce2b1a384c745a839baea52da59dcc.png

A button can be configured in one of two ways:

clickDetect OFF

A button will simply react to a press and release.  There should be minimal (a few ms) delay in doing so. This will be suitable for tap tempo/looper type situations where the latency needs to be as low as possible.  A button can be configured to be

latching- alternate press/release activity will alternately send a message A or message B when pressed (And optionally message A1 & B1 upon release).

non-latching - the same message will be sent every time it is pressed (optionally a differnet message when released)

 

clickDetect ON

A button will be able to detect single/double (even triple if desired) clicks and send a suitable singleClick/doubleClick/etc message when it has decided which situation applies.  The button needs to wait a short time (configurable) after a release before it decides that no more clicks are going to happen.  I'm going to try about 0.3 seconds initially and see how it goes.  Whilst not a huge delay (Hopefully responsive enough for changing snapshots or perhaps even turning on/off 'pedals') I suspect it will not be great for time sensitive uses (Hence the clickDetect OFF  mode). A button in theis mode can also detect if it is held down for a longer time (say 0.6s) and send a hold message and if it continues to be held down it will periodically send a repeat message, I see this as useful for scrollowing through patches/pedalboards between songs.

 

I'd appreciate those of you with a technical bent letting me know of any issues/gaffs you can see.

 

S'manth x

Edited by Smanth
Link to comment
Share on other sites

Certainly looks like you have looked into it a lot (a lot more than I would ever get round to doing!) - do you always want to wait for the button up? I know if I was only using a switch as a toggle I would want it to toggle on the leading edge (also tap tempo).

Another thought having done my early programming on keyboards that really had a debounce problem (Acorn atom keyboards were terrible), do modern footswtiches have much of a bounce? I know old ones didn't as they had a lever that flicked over - one smallish rc input on the board on it and you had no bounce at all from a digital point of view.

Good for not using an interrupt for a button (I wouldn't use a timer either, I would just be constantly monitoring, what more is a footpedal going to be doing?), that would make it a bit more complicated.

  • Like 1
Link to comment
Share on other sites

30 minutes ago, Woodinblack said:

 do you always want to wait for the button up?

Only in the clickDetect mode.  In the non clickDetect I will fire the 'pressed' event as soon as the the debounce code reports the button is pressed, ditto for the 'released' event ... I would hope that the debounce settle time will not add more than about 10ms or so (Of course I'll need to see how quickly the switches I've got do actually take to settle). (I've just noticed that my state diagram did not reflect this correctly, I've fixed it)

 

The system will be doing a number of decoupled things  including.

  • Detecting button state changes
  • Sampling the  expression pedal changes (via ADC)
  • Sending out MIDI messages
  • Checking for MIDI messages. I don't intend on doing any MIDI hub stuff at present, but I intend to use the Dwarf buttons to select/load snapshots/pedalboards. It is possible to configure things (using one of the MIDI plugins) so that a different MIDI message is sent from the Dwarf each time and I intend on using this to reconfigure the Trampa.
  • Reading/sending HTTP messages for the web configuration interface.
  • Periodic updating the OLED displays to allow for scrolling of long text content.

I'm using the FreeRTOS kernel to provide threading, I'm not sure yet if I'll run one per category (one button thread, one display thread) or one thread per individual input/output device ... the latter has the appeal that once I've created a deviceobject and kicked it off running I can pretty much forget about it and having to do my own interleaving/scheduling of checking this, that and the other.

 

S'manth x

Edited by Smanth
Link to comment
Share on other sites

29 minutes ago, Smanth said:

The system will be doing a number of decoupled things  including.

  • Detecting button state changes
  • Sampling the  expression pedal changes (via ADC)
  • Sending out MIDI messages
  • Checking for MIDI messages. I don't intend on doing any MIDI hub stuff at present, but I intend to use the Dwarf buttons to select/load snapshots/pedalboards. It is possible to configure things (using one of the MIDI plugins) so that a different MIDI message is sent from the Dwarf each time and I intend on using this to reconfigure the Trampa.
  • Reading/sending HTTP messages for the web configuration interface.
  • Periodic updating the OLED displays to allow for scrolling of long text content.

 

Huge reply snipped as this isn't a design meeting, its your project and I have finished work for the rest of the month!!

For me, i wouldn't consider most of those things decoupled, just 'operating system tasks' (the button state, screen state, midi monitoring and expression pedal) which I would all be doing just on a schedule with a few queues as a low level thing. The HTTPD stuff, obviously whole different ball game and that certainly needs its own world - as far as i see that is where 90% of the complication will be. But as you say you have a whole operating system now so you can just do them as a series of different objects and abstract it away and that is probably the easiest way to do it.

 

It is however checking how much worry you have to put into buttons and switches (and also expression pedals) to debounce as sometimes it is far less of an issue than you think, especially these days.

 

Going to be fun though, and also good to have some milestones along the way.

 

  • Like 1
Link to comment
Share on other sites

As always an incredible amount of progress and skill :)  The debounce integrator code is very elegant to say the least :).   @Woodinblack's keyboard comments and mention of rc leads me to ask have you considered hardware debouncing?  https://www.digikey.co.uk/en/articles/how-to-implement-hardware-debounce-for-switches-and-relays.  If you debounce using hardware you would then be able to poll the input buttons concurrently as a single port byte.  This would reduce the latency caused by software debouncing each input after serial reads.  Equally well I may have misunderstood the situation and this suggestion is irrelevant.

  • Like 1
Link to comment
Share on other sites

34 minutes ago, 3below said:

As always an incredible amount of progress and skill :)  The debounce integrator code is very elegant to say the least :).   @Woodinblack's keyboard comments and mention of rc leads me to ask have you considered hardware debouncing?  https://www.digikey.co.uk/en/articles/how-to-implement-hardware-debounce-for-switches-and-relays.  If you debounce using hardware you would then be able to poll the input buttons concurrently as a single port byte.  This would reduce the latency caused by software debouncing each input after serial reads.  Equally well I may have misunderstood the situation and this suggestion is irrelevant.

I did come across a s/w debounce that handles parallel inputs and the PICO does have a read all GPIO pins at once function, but I steered away from that approach simply because it seems cleaner (tho admittedly not as effecient) to have a single button object per GPIO pin.

 

I had considered h/w debouncing, but I'm using SPST switches and I got the impression that the approaches to debounce these in h/w still require about 20ms to reach a steady output signal (This sorta makes sense at a gut level too, regardless of how you debounce something, you need it to stop bouncing to do so), so it didn't seem worth bothering.

 

S'manth x

Edited by Smanth
  • Like 1
Link to comment
Share on other sites

Thinking some more about this, consider four buttons (B1 -> B4) being serially polled.  On each each poll a detection event must happen for say 20ms.  This means that an event on  B4  could be missed while B1 -> B3 are being polled serially and detected (say 60ms total). Using a dedicated hardware debounce with 4 channels continuously detecting and reading the buttons as a port byte every 20ms would reduce the worst case detection latency to 40ms.  The advantage of hardware debounce is not in the single case, it is when you have parallel concurrent switch operation.  On the plus side I suspect in real operation foot switch timing changes are not this critical with only 4 switches.

  • Like 1
Link to comment
Share on other sites

1 minute ago, 3below said:

Thinking some more about this, consider four buttons (B1 -> B4) being serially polled.  On each each poll a detection event must happen for say 20ms.  This means that an event on  B4  could be missed while B1 -> B3 are being polled serially and detected (say 60ms total). Using a dedicated hardware debounce with 4 channels continuously detecting and reading the buttons as a port byte every 20ms would reduce the worst case detection latency to 40ms.  The advantage of hardware debounce is not in the single case, it is when you have parallel concurrent switch operation.  On the plus side I suspect in real operation foot switch timing changes are not this critical with only 4 switches.

Sound thinking.

I am using FreeRTOS to prempetively multithread.  Each button object has its own thread with a wait 5ms then check the button kinda logic, so I would not anticipate any stacking up of latency as you describe.  I've not (and I'm not totally sure how I could without falling foul of Heisenberg) timed how long it takes the code to read the button state and run the simple FSM, but my gut says it is likely to be really fast (and I've some performance tuning ideas  I could implement if needed.) 

 

Having said all that, I've not a lot of experience with microcontrollers so ... hic sunt dracones! 🤣

 

S'manth x

  • Like 1
Link to comment
Share on other sites

1 minute ago, Smanth said:

Sound thinking.

I am using FreeRTOS to prempetively multithread.  Each button object has its own thread with a wait 5ms then check the button kinda logic, so I would not anticipate any stacking up of latency as you describe.  I've not (and I'm not totally sure how I could without falling foul of Heisenberg) timed how long it takes the code to read the button state and run the simple FSM, but my gut says it is likely to be really fast (and I've some performance tuning ideas  I could implement if needed.) 

 

Having said all that, I've not a lot of experience with microcontrollers so ... hic sunt dracones! 🤣

 

S'manth x

 

✴️ :)  Multithreading :) ✴️   I have been working in 'old world' single thread linear microcontroller program mode.  Carry on, ignore me lol, and yes we have dragons in Wales.

  • Haha 1
Link to comment
Share on other sites

34 minutes ago, Woodinblack said:

Not sure why you would be taking 20ms for a detection event on a button, I would be very worried about my code if it was taking more than 100us to scan all the buttons, although I haven't done any timings on this. 

 

I was working on the 20ms 'bounce/settling' time,  sample the button and keep sampling until a change of state (or not) is certain.  However, from your question I now realise we could sample through B1 -> B4  store results and then every 20ms or so do a state check. Slowly getting brain into gear :) will get electronics kit out, think and play tomorrow. 

Edited by 3below
Link to comment
Share on other sites

Indeed - sample all the buttons, adc etc every milisecond, and then when you have decided that the switch is not bouncing (and it doesn't need to be anything like 20ms) report any state changes. The worst latency you would have would be the time you have given for the button stabilisation + a max of just less than 1ms.

  • Like 1
Link to comment
Share on other sites

13 hours ago, Woodinblack said:

Not sure why you would be taking 20ms for a detection event on a button, I would be very worried about my code if it was taking more than 100us to scan all the buttons, although I haven't done any timings on this. 

I've just got some crude timings and it appears that a scan of my 4 buttons comes in at under 10us when they are quiescent and no higher than about 75us when I'm pressing them. edit: I've just realised most of this increase is probably due to me serial printing when the button is pressed!

S'manth x

Edited by Smanth
  • Like 2
Link to comment
Share on other sites

16 hours ago, Woodinblack said:

Indeed - sample all the buttons, adc etc every milisecond, and then when you have decided that the switch is not bouncing (and it doesn't need to be anything like 20ms) report any state changes. The worst latency you would have would be the time you have given for the button stabilisation + a max of just less than 1ms.

 

Or maybe it has to be up to 157mS.

 

http://www.ganssle.com/debouncing.htm

  • Like 1
Link to comment
Share on other sites

So button logic code written and checked.

Flashed the PICO and nothing worked in any logical way 😖

Checked the code again ... and then again.

Removed the debounce logic ... same thing.

But then I realised that I had the state wrong for my button. My code was looking for active HIGH and the button is configured as active LOW ... D'oh!

After correcting this, it now seems to work as intended ... well pleased :)

Welcome to minicom 2.8

OPTIONS:
Compiled on Oct 24 2022, 11:16:41.
Port /dev/tty.usbmodemTrampa1, 16:36:52

Press Meta-Z for help on special keys

Button[1]- Press
Button[1]- Release
Button[2]- Press
Button[2]- Release
Button[3]- Click
Button[3]- Click
Button[3]- DoubleClick
Button[3]- TripleClick
Button[3]- Hold
Button[3]- Repeat
Button[3]- Repeat
Button[3]- Repeat
Button[3]- Repeat
Button[3]- Hold Release
Button[3]- Hold(Shifted)
Button[3]- Repeat(Shifted)
Button[3]- Repeat(Shifted)
Button[3]- Repeat(Shifted)
Button[3]- Repeat(Shifted)
Button[3]- Hold(Shifted) Release
Button[4]- Click
Button[4]- Hold(Shifted)
Button[4]- Repeat(Shifted)
Button[4]- Repeat(Shifted)
Button[4]- Repeat(Shifted)
Button[4]- Hold(Shifted) Release

Press and Release are instant!

Click (Single/Double/Triple) are detected about 0.2s after the last release.

The shifted Hold (etc) are triggered by a short click followed by a hold ... so in all I am able to get 5 different actions from one button (if it is configured as clickDetect ON).

I want to add some logic to detect simulataneous presses of adjacent buttons, but first I'm going to add in the "Let's send a MIDI CC message" based on the events raised.

S'manth x

  • Like 1
Link to comment
Share on other sites

26 minutes ago, 3below said:

Great stuff, did you use the Kenneth A. Kuhn debounce method?

Yes, it's part of my button class :)

/*
--.--
  |  ,---.,---.,-.-.,---.,---.
  |  |    ,---|| | ||   |,---|
  `  `    `---^` ' '|---'`---^
                    |
(C) 2023 Samantha-uk
https://github.com/samantha-uk/trampa

Adapted from debounce.c https://www.kennethkuhn.com/electronics/debounce.c
/******************************************************************************
debounce.c
written by Kenneth A. Kuhn
version 1.00

This is an algorithm that debounces or removes random or spurious
transistions of a digital signal read as an input by a computer.  This is
particularly applicable when the input is from a mechanical contact.  An
integrator is used to perform a time hysterisis so that the signal must
persistantly be in a logical state (0 or 1) in order for the output to change
to that state.  Random transitions of the input will not affect the output
except in the rare case where statistical clustering is longer than the
specified integration time.

The following example illustrates how this algorithm works.  The sequence
labeled, real signal, represents the real intended signal with no noise.  The
sequence labeled, corrupted, has significant random transitions added to the
real signal.  The sequence labled, integrator, represents the algorithm
integrator which is constrained to be between 0 and 3.  The sequence labeled,
output, only makes a transition when the integrator reaches either 0 or 3.
Note that the output signal lags the input signal by the integration time but
is free of spurious transitions.

real signal 0000111111110000000111111100000000011111111110000000000111111100000
corrupted   0100111011011001000011011010001001011100101111000100010111011100010
integrator  0100123233233212100012123232101001012321212333210100010123233321010
output      0000001111111111100000001111100000000111111111110000000001111111000

I have been using this algorithm for years and I show it here as a code
fragment in C.  The algorithm has been around for many years but does not seem
to be widely known.  Once in a rare while it is published in a tech note.  It
is notable that the algorithm uses integration as opposed to edge logic
(differentiation).  It is the integration that makes this algorithm so robust
in the presence of noise.
******************************************************************************/

#include "button.h"

Button::Button(int id, ButtonConfig* buttonConfig, uint pin) {
  _id = id;
  _buttonConfig = buttonConfig;
  _pin = pin;
}

void Button::init(void) {
  gpio_init(_pin);
  gpio_set_dir(_pin, GPIO_IN);
  gpio_pull_up(_pin);
}

bool Button::pressed() {
  /*
  Step 1: Update the integrator based on the input signal.  Note that the
  integrator follows the input, decreasing or increasing towards the limits as
  determined by the input state (0 or 1).
  */
  if (gpio_get(_pin) == 1) {
    if (_integrator > 0) _integrator--;
  } else if (_integrator < INTEGRATOR_MAX)
    _integrator++;

  /*
  Step 2: Update the output state based on the integrator.  Note that the
  output will only change states if the integrator has reached a limit, either
  0 or MAXIMUM.
   */
  if (_integrator == 0)
    _pressed = false;
  else if (_integrator >= INTEGRATOR_MAX) {
    _integrator =
        INTEGRATOR_MAX; /* defensive code if integrator got corrupted */
    _pressed = true;
  }
  return _pressed;
}
void Button::setState(ButtonState state) {
  // write_serial("Button[%d] from [%d]->[%d]\r\n", _id, _state, state);
  _state = state;
}

void Button::check(void) {
  // Get the debounced current debounced pressed state of the button
  bool isPressed = pressed();
  // bool isPressed = gpio_get(_pin);

  // Process the state machine for the button
  switch (_state) {
    case ButtonState::IDLE:
      if (isPressed) {
        // Capture the time
        _buttonPressedTime = get_absolute_time();

        // Reset _clickCount
        _clickCount = 0;

        // should we send a PRESS event
        if (!_buttonConfig->_clickDetect) fireEvent(ButtonEvent::PRESS);

        // Change to PRESSED state
        setState(ButtonState::PRESSED);
      }
      break;

    case ButtonState::PRESSED:
      if (!isPressed) {                     // The button is now released
        if (_buttonConfig->_clickDetect) {  // We ARE detecting clicks
          // Capture the time
          _buttonReleasedTime = get_absolute_time();

          // Change to RELEASED state
          setState(ButtonState::RELEASED);
        } else {  // We ARE NOT detecting clicks
          fireEvent(ButtonEvent::RELEASE);
          // Reset _clickCount
          _clickCount = 0;

          // Change to IDLE state
          setState(ButtonState::IDLE);
        }
      } else {  // The button is still pressed
        // If we are detecting clicks and the time since _buttonDownTime is
        // longer than _holdDelay
        if (_buttonConfig->_clickDetect &&
            absolute_time_diff_us(_buttonPressedTime, get_absolute_time()) >=
                _buttonConfig->_holdDelay) {
          fireEvent(ButtonEvent::HOLD);

          // Capture the time
          _buttonRepeatTime = get_absolute_time();

          // Change to HOLD state
          setState(ButtonState::HOLD);
        }
      }
      break;

    case ButtonState::RELEASED:
      if (isPressed) {  // The button is pressed
        // Capture the time
        _buttonPressedTime = get_absolute_time();

        // Increment _clickCount
        _clickCount++;

        // Change to PRESSED state
        setState(ButtonState::PRESSED);
      } else {  // The button is still releasaed
        // check to see if _clickDelay has passed since we entered RELEASED
        // state
        if (absolute_time_diff_us(_buttonReleasedTime, get_absolute_time()) >=
            _buttonConfig->_clickDelay) {
          // _clickDelay time has passed so we report the number of clicks
          fireEvent(ButtonEvent::CLICK);

          // Reset _clickCount
          _clickCount = 0;

          // Change to IDLE state
          setState(ButtonState::IDLE);
        }
      }
      break;

    case ButtonState::HOLD:
      // If the button is now released
      // _clickCount is used to indicate if any single clicks preceeded the hold
      // These are used as "shifts" to potentially alter the behaviour of
      // hold/repeat events.
      if (!isPressed) {  // The button is now released
        fireEvent(ButtonEvent::HOLD_RELEASE);

        // Reset _clickCount
        _clickCount = 0;

        // Change to IDLE state
        setState(ButtonState::IDLE);
      } else {  // The button is still pressed
        // If time down is longer than _repeatDelay
        if (absolute_time_diff_us(_buttonRepeatTime, get_absolute_time()) >=
            _buttonConfig->_repeatDelay) {
          fireEvent(ButtonEvent::REPEAT);

          // Reset _buttonHoldTime
          _buttonRepeatTime = get_absolute_time();
        }
      }
      break;
  }
}

void Button::fireEvent(ButtonEvent event) {
  // Check to make sure that we have a cbFunction
  if (_buttonConfig->cbButtonEvent) {
    _buttonConfig->cbButtonEvent(_id, event, _clickCount, _latched);
  }
  if (_buttonConfig->_latching) _latched = !_latched;
}

ButtonConfig::ButtonConfig() {}

S'manth x

Edited by Smanth
  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...