Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control surface #16

Open
kamencesc opened this issue Aug 4, 2020 · 20 comments
Open

Control surface #16

kamencesc opened this issue Aug 4, 2020 · 20 comments
Assignees

Comments

@kamencesc
Copy link

Now with the DAC, MIDI in and LCDs, will be fine to add some buttons to interact with the hardware. Maybe configurable via the cfg file instead of create reserved pins.

@dwhinham
Copy link
Owner

dwhinham commented Aug 4, 2020

This is certainly planned. 😃

I'm enlisting the help of some owners of real MT-32s so that I can try to accurately recreate the user interface.

@kamencesc
Copy link
Author

kamencesc commented Aug 4, 2020

Nice, so 12 buttons + volume (rotary encoder? = 2) I don't know if there's 14 gpio free pins (assuming that the 4bit hd44780 lcd is in use there's no to many free pins)

A way to add some inputs with less pin cost is using a I2C or SPI IO Expander (MCP23017 (i2c) or MCP23s17 (spi)), they come with 18 configurable IO ports and the i2c version has good libs for C, the spi versión I only used on Arduino so I can't talk about it on RPI.

@dwhinham
Copy link
Owner

dwhinham commented Aug 4, 2020

I'm only counting 10 buttons on the original MT-32, but yes, we'd still need more GPIOs for a rotary encoder, especially if we want to use one with an integrated push button.

I also had the idea of using an I2C I/O expander since implementing the I2C HD44780 screen driver as those screens also use such an expander. Some research required - I'm not sure how encoders behave through such interfaces (and encoders can be a nightmare to get right!).

I'll try to get a couple of these chips and see if I can prototype something on a breadboard soon.

@dwhinham dwhinham changed the title Input Switch Control surface Aug 10, 2020
@dwhinham dwhinham self-assigned this Aug 10, 2020
@raelik
Copy link

raelik commented Sep 2, 2020

Presumably you meant an I²C I/O expander, I²S is for PCM audio. It's definitely a good idea, way too many GPIOs would be required. Looks like the MCP23017 and the PCF8574 are two pretty popular I²C I/O expanders, and there's examples out there of using rotary encoders with either of them. Given the number of other buttons we'd need, the MCP23017 would be required. It has 16 available pins for I/O, and a rotary encoder needs at least 3 pins (including the 1 for the button), which would give us 13. The PCF8574 only has 8, so we'd be short by 5. Depending on exactly what kind of rotary encoder we need (I'm not sure how the knob on the MT-32 behaves, if it has detents, absolute positions, etc), we may need a few more pins (if it's using a 3+ bit Gray code), at most we'd be able to have 6 for the encoder. That would allow for 5-bit Gray code, or 32 positions. I doubt the MT-32 knob has more resolution than that.

@raelik
Copy link

raelik commented Sep 2, 2020

Ok.... so. I did some digging, and apparently, the MT-32 doesn't use a rotary encoder at all. It's far stranger than that. It uses a 50k pot... which connects DIRECTLY to the 8098 processor. That processor has a 4-channel, 10-bit ADC, and that pot uses one of those channels. Kinda insane. That said, you CAN do it that way if you want. TI sells a single-channel 10-bit ADC that's I²C compatible, the ADC101C021. They're about $1.50 on DigiKey. Only issue is that they're VSSOP, no through-hole option, so breadboarding it would be annoying. There's also an SOT-6 package, but the I²C address is less configurable on that one. It can go from 0x50 to 0x52, where the VSSOP-8 version can go from 0x50-0x52, 0x54-0x56, or 0x58-0x5a.

@dwhinham
Copy link
Owner

dwhinham commented Sep 2, 2020

Presumably you meant an I²C I/O expander, I²S is for PCM audio.

Of course, sorry. 😄 I must have mentioned I2C and I2S about 50 times in the README, I was bound to make a typo eventually. Edited.

It's definitely a good idea, way too many GPIOs would be required. Looks like the MCP23017 and the PCF8574 are two pretty popular I²C I/O expanders, and there's examples out there of using rotary encoders with either of them. Given the number of other buttons we'd need, the MCP23017 would be required.

I have a MCP23017 on my bench now, I'm slowly writing a driver for it when I get time in between work (and #21 has distracted me from working on new features... 😞 ).

Ok.... so. I did some digging, and apparently, the MT-32 doesn't use a rotary encoder at all. It's far stranger than that. It uses a 50k pot...

I think I want to stick to a basic 2-bit relative encoder, detents can be a user preference (and we can handle differences in what happens between each detent a bit like how FlashFloppy does it). I have plans to add multi-level menus and the ability to set various options as the project grows, so being able to turn the knob around and around makes the most sense here. I don't think messing around with ADC stuff is worth it, but I appreciate you looking into the options!

@raelik
Copy link

raelik commented Sep 2, 2020

I have a MCP23017 on my bench now, I'm slowly writing a driver for it when I get time in between work (and #21 has distracted me from working on new features... 😞 ).

Perfect! Yeah, that's how it goes.

I think I want to stick to a basic 2-bit relative encoder, detents can be a user preference (and we can handle differences in what happens between each detent a bit like how FlashFloppy does it). I have plans to add multi-level menus and the ability to set various options as the project grows, so being able to turn the knob around and around makes the most sense here. I don't think messing around with ADC stuff is worth it, but I appreciate you looking into the options!

Ah, makes sense, so you'd probably want something like this https://www.digikey.com/product-detail/en/bourns-inc/PEC12R-4217F-N0024/PEC12R-4217F-N0024-ND/4699280

Continuous rotation, push button switch, 24 detents with 24 pulses per rotation, 17.5mm flatted shaft, 2-bit quadrature output. If you want one with a washer and mounting nut, get a 3217F instead. There's also a few longer shaft options, and 1 shorter one, though you can't get that one with the washer & nut. The PEC11R series has knurled shaft options, which might be better as it's easier to find knobs for those.

@dwhinham
Copy link
Owner

dwhinham commented Oct 14, 2020

Just a status update on where this is at, and some info as I'm getting an influx of people who are asking about this and waiting to add controls. A very frequently asked question is what pins the controls will attach to, so I'm going to write this here so I can just link people to this thread. 🙂

Here's how this is going to work:

  • Once a control surface has been successfully prototyped, a hardware specification will be released with info on how the encoder/buttons/port expander - whatever we end up using (because currently I haven't decided) - are assigned to GPIOs.
  • There will be no config options to assign pins. The last thing I want is to have to make GPIO pins for buttons and encoders a config file option. I really don't want to have to parse several config options for this and do all the necessary error checking for when someone accidentally sets 2 pins to be the same device. That would be a maintainability disaster (adding unnecessary code bloat), and I also don't want the potential issue tickets being opened when things go wrong.
  • mt32-pi is meant to be simple for the user, so the above point is non-negotiable.
  • mt32-pi will not support anyone's custom PCB designs etc. that don't use this specification. So far, this hasn't happened, but I get an email/DM every week from someone with an itchy KiCad/JLCPCB trigger finger, so I want to make this clear. 😉

Now that that's out of the way... 😅

I've been evaluating some MCP23017 devices - talking to them is easy and reading their pin states is easy, no problems there.
The challenge is reading encoders reliably, and so far this has proven very difficult - at least doing so through the expander.

By "reliably", I mean not jumping ahead multiple steps, not going backwards, not missing clicks, etc. Quality of encoders varies a lot, so we need robust debouncing and rejection of invalid state transitions. I'm aware of most of the popular algorithms for doing this - this is not the problem. I also have lots of encoders of varying spec/quality to work with and test.

With nothing else going on, by polling the MCP in a tight loop I can almost get all of the transitions. But with LCD updates (which also sits on I2C) and audio rendering going on, the encoder becomes erratic/unusable.

Currently, mt32-pi runs entirely on Core 0 - one CPU core. I'm working on switching on the other cores and moving audio to its own core, which will help a lot and is necessary for a lot of the planned features anyway. There are some things we cannot move to other cores because of how Circle works.

The other way you can use the MCP is with interrupts; it can tell you when something's changed via a GPIO pin, and then you read it via I2C. Normally, you cannot do an I2C read from within an interrupt in Circle, but the author showed me how I can modify Circle easily to allow that to work. I feel like I'm getting close with this, but my sticking point is that because of noise/bounce, the MCP can raise another interrupt again during the read, meaning you need to keep reading again and again until the MCP decides there's no longer an interrupt condition.

Anyway - I'm still experimenting; this is no easy task and I need time to get it working reliably. If encoders end up being easier to use from Pi GPIOs, maybe we just use an MCP for the 10 buttons.

@dwhinham dwhinham pinned this issue Oct 14, 2020
@raelik
Copy link

raelik commented Oct 14, 2020

I've been evaluating some MCP23017 devices - talking to them is easy and reading their pin states is easy, no problems there.
The challenge is reading encoders reliably, and so far this has proven very difficult - at least doing so through the expander.

By "reliably", I mean not jumping ahead multiple steps, not going backwards, not missing clicks, etc. Quality of encoders varies a lot, so we need robust debouncing and rejection of invalid state transitions. I'm aware of most of the popular algorithms for doing this - this is not the problem. I also have lots of encoders of varying spec/quality to work with and test.

With nothing else going on, by polling the MCP in a tight loop I can almost get all of the transitions. But with LCD updates (which also sits on I2C) and audio rendering going on, the encoder becomes erratic/unusable.

The other way you can use the MCP is with interrupts; it can tell you when something's changed via a GPIO pin, and then you read it via I2C. Normally, you cannot do an I2C read from within an interrupt in Circle, but the author showed me how I can modify Circle easily to allow that to work. I feel like I'm getting close with this, but my sticking point is that because of noise/bounce, the MCP can raise another interrupt again during the read, meaning you need to keep reading again and again until the MCP decides there's no longer an interrupt condition.

Anyway - I'm still experimenting; this is no easy task and I need time to get it working reliably. If encoders end up being easier to use from Pi GPIOs, maybe we just use an MCP for the 10 buttons.

@dwhinham So I've also been testing the MCP23017, and came to the same conclusion you did, that polling isn't really workable, and you have to use interrupts. Also, I found that it's really necessary to do the interrupt handling in its own thread, so that you can clear the interrupt as quickly as possible. All my interrupt handler did was grab the INTF and INTCAP registers to see which pin(s) fired the interrupt, and what their captured state was when that happened, and then grabbed the GPIO registers to get the pin states and clear the interrupt. Then it just pushed all 4 of those (INTF, INTCAP, GPIOA and GPIOB) onto a shared queue. It was really basic, and I ran into the same sorts of bounce problems you were talking about, but I didn't miss any inputs that way. Adding a timestamp to the struct pushed onto the queue would allow for debounce logic though without really impacting the interrupt handler.

@dwhinham
Copy link
Owner

@raelik Thanks for sharing your experience, I'm glad it's not just me.

Are you trying this in Circle too, or from Linux with Wiring Pi and C++ or Python?

I was led to believe that reading either INTCAP or the GPIO reg was enough to clear the interrupt (provided another doesn't happen while doing so), so you could go with either time-of-interrupt value or latest value (or both, in your case) - though I haven't verified that yet.

For debounce, rather than timestamping I tried the approach from this article: http://www.buxtronix.net/2011/10/rotary-encoders-done-properly.html (there's a code repo linked at the end).

One issue I had was that I could get into a state where the MCP would be stuck in an interrupt condition - possibly because I'd cleared the interrupt, but between that point and returning from my ISR another one could appear, so there'd have to be code in the main loop polling it regardless to stop that from happening. Eurgh. Either that or I've completely gotten it wrong (most likely 😃 )

@raelik
Copy link

raelik commented Oct 15, 2020

@dwhinham I was trying it first in Python, but when I realized I probably needed a thread to handle the interrupt, I switched to C++. As far as I could glean from the datasheet, only reading the GPIO reg will clear the interrupt, though maybe reading INTCAP will clear it if you have it in default value mode. I had similar issues with getting stuck in the interrupt condition when I was trying to use a loop, which was why I went with an interrupt handler in a separate thread. I'm not sure how reliable it is, I'd barely gotten it working when I had to put it aside. Using threads for the interrupt handling SHOULD avoid that problem.

@raelik
Copy link

raelik commented Oct 15, 2020

@dwhinham That article is actually quite helpful, and I think the source of some of the issues I was having was improperly dealing with the nature of how the rotary encoder operates. I'd managed to very slowly turn it and get it to generate '11' patterns, I should have realized when looking at the quadrature output diagram of the PEC11R encoder I'm using, that it actually generates ALL of those patterns in sequence when going from detent to detent.

pec11r_quad

@raelik
Copy link

raelik commented Oct 15, 2020

@dwhinham I should mention that I was using libsoc's C++ bindings to do the interrupt handling, as it uses std::thread when spawning the ISR, and I used https://github.com/dehavenm/MCP23017 to interface with the MCP23017.

@dwhinham
Copy link
Owner

dwhinham commented Oct 16, 2020

@raelik

I did some more testing; just reading INTCAP is working for me, and I'm not using default value mode. The datasheet doesn't seem to suggest that it's limited to default value mode.

What you do have to watch out for is the address increment/the way it alternates between Port A and B if you don't write an address before reading. I was only reading port A in byte mode, which meant that the next time round I'd read port B (as I didn't write a new address pointer for efficiency). Reading both solved this, or you can mess with the BANK bit in IOCON to change how this works.

I've got things working somewhat alright - my ISR seems to work, and I can just keep reading until my interrupt pin goes high again (interrupt is clear). I'm not getting stuck any more this way.

That all said, my (cheap and nasty) encoders can be used to turn the volume up and down, with all the other stuff running (synth MIDI, LCD) although the accuracy isn't perfect. A "click" sometimes doesn't give you a single increment, sometimes it does nothing, sometimes it'll increment by two. It's usable, but I'm not happy with it.

At this point I think it's just a case of finding a better algorithm for tracking encoder state. I know FlashFloppy did a lot of work on handling all sorts of horrendous eBay encoders that people insist on using, so I'll see if I can mimic what Keir is doing. 🙂

I think I'll repeat all of this but with the encoder on Pi GPIOs and see if I can get any more accurate. I'm starting to lean further towards the MCP just being used for the buttons (maybe encoder-on-MCP could be an option for tidier wiring if it can be made to work 100%); and I guess this would make sense for users that just want to hook up an encoder and nothing else, that way they can leave out the MCP.

@raelik
Copy link

raelik commented Oct 16, 2020

@dwhinham that probably makes sense, since you only need 3 pins for the encoder (assuming it has a button)

@dwhinham
Copy link
Owner

dwhinham commented Jan 3, 2021

This has now finally been partially implemented in v0.8.0 - see the wiki page for details. 🙂

I've decided to take the following approach:

  • Two "simple" schemes for people wanting to build small integrated HAT-like designs with either 4 buttons, or 2 + rotary encoder - these are assigned directly to GPIO pins as shown in the wiki.
  • I abandoned the interrupt-on-change idea and reverted to polling - this has resulted in very reliable and deterministic reading of the controls with no effect on timing within the rest of the program (important!).
  • Encoder reading is based on this algorithm by Peter Dannegger which seems to work beautifully!
  • Button debouncing implemented using this paper by Jack G. Ganssle as a reference (Listing 3), which again works very well.
  • Full "10-button/MT-32" style using the MCP will be investigated later; hence I'm leaving this issue open for now.

@jopdorp
Copy link

jopdorp commented Mar 14, 2021

more buttons can also be attached by triggering multiple gpio's at the same time:

for example
button gpio
button 1: 17
button 2: 27
button 3: 12
button 4: 16
button 5: 17 + 27
button 6: 17 + 12
button 7: 17 + 16
button 8: 27 + 12
button 9: 27 + 16
button 10: 12 + 16
Screenshot_2021-03-14_08-16-23

@raelik
Copy link

raelik commented Mar 14, 2021

@jopdorp In theory, yes, but that would mean you can't press multiple buttons at the same time.

@jopdorp
Copy link

jopdorp commented Mar 16, 2021

depends on which buttons, does the mt-32 use button combinations?

@esver
Copy link

esver commented May 24, 2021

Have you tested the SX1509 I/O Expander ? It's use I2C and seems to have internal input debouncer and to support button matrices. As the original MT-32 seems to have a 2x5 button matrices as showed in mt-32 service note.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants