dwhinham / mt32-pi

🎹🎶 A baremetal kernel that turns your Raspberry Pi 3 or later into a Roland MT-32 emulator and SoundFont synthesizer based on Circle, Munt, and FluidSynth.
https://twitter.com/d0pefish
GNU General Public License v3.0
1.23k stars 78 forks source link

Control surface #16

Open kamencesc opened 4 years ago

kamencesc commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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.

raelik commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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:


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.

raelik commented 3 years ago

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 commented 3 years ago

@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 :smiley: )

raelik commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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

dwhinham commented 3 years ago

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:

jopdorp commented 3 years ago

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 commented 3 years ago

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

jopdorp commented 3 years ago

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

esver commented 3 years ago

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.