In my junior year at UCSB, I made a chromatic instrument tuner with a friend of mine. I'll be the first to admit that it doesn't really solve any novel problem, but I sure learnt a lot making it.
First, I'll explain the project for those unfamiliar with music---if the the phrase "chromatic instrument tuner" makes sense to you, go ahead and skip to the next paragraph. Otherwise, here goes. An instrument tuner helps you adjust your instrument so that it sounds at the right frequency. A guitar string for example over time will stretch out over time and therefore must be tightened just a little bit before playing. The tuner listens to the sound of the plucked guitar string and tells you how out of tune the note is. Then you tighten or loosen the string and pluck again. "Chromatic" just means that the tuner identifies every musical note, not just the ones in a single key e.g. C major. Some tuners will disregard out-of-key notes to make tuning a little easier for one key, at the expense of all the others. Not this one. It just displays the musical note you're closest to, and how much you need to adjust the note to get it spot on.
Hardware wise, the core of the tuner is an FPGA board (Digilent Nexys A7) with an onboard microphone, then also a TFT display connected over SPI and a rotary encoder connected over GPIO. Processing, output, and input.
Although the project is on an FPGA, we didn't write any Verilog; we used Vivado to integrate existing IP blocks for CPU, memory, core peripherals, and then we wrote code for the Microblaze soft processor, making use of the generated C files to drive the on-chip peripherals.
That's the 30,000-foot view of the project and how we developed it. Here's a video of its operation:
The software was built within the QP-Nano framework. It's a framework for creating Hierarchical State Machines (HSMs) authored by the excellent Miro Samek. The HSM approach allows one to define an exact set of states which the machine will always be in one of. So you avoid bugs that come up when the program is "in a weird state" by being forced to define exactly which states are accessible from which and how to get there. Actual operations like hardware control are done on state transitions (possibly a self-transition). Like emitting a token from a Deterministic Finite-state Automaton (DFA) which you learn about in compiler courses. Except the token is the execution of certain code. In fact HSMs are isomorphic with DFAs, but HSMs allow for a lot more concise definition.
Anyway, QP-Nano was very helpful in integrating GUI and hardware, for example in ensuring that a rotary encoder turn will always be handled with X code on screen A, and with Y code on screen B. Since the QP-Nano event loop is run at a lower priority than interrupts, and it is the signal handlers which did most of the processing rather than the ISRs, we got a Linux-workqueue-like system for free. Now, GUI frameworks often offer such facilities, but the cool thing about QP Nano is that it is totally separate from GUI, it could just as easily be ensuring that the right MQTT messages are published depending on program state and doing so at lower priority. Though in our case the appeal of QP-Nano was GUI ease. It let us write our GUI with a few draws/erases on state transitions using the display's raw SPI draw/erase commands.
We had only a couple ISRs. One for posting a QP-Nano signal upon rotary encoder interrupt and containing a small state machine for debouncing the quadrature signal. Another for posting a "timer-expired" signal after a few seconds of inactivity, used by certain "pop-up"-like screens to close after a timeout.
The signal processing was a soft real time affair. Essentially when there was nothing else to be done, we ran an FFT on the latest audio data and redrew the screen accordingly with either the identified note (on the tuner screen), or a heatmap of the FFT (on the spectrogram screen). The only constraint was that it be fast enough for the readout to look smooth, so about 100ms.
Two interesting problems we solved:
Our original FFT implementation was too slow, so we did some very basic profiling to find the slow point. Basically, we put goto labels at the beginning, middle, and end of the top-level FFT function. Then set an interrupt to run periodically and print whether the program counter was in the top half or bottom half of the function. See which one is slower, then place goto labels beginning, middle, and end of that half, and repeat. Basically we did a binary search until we found the slow line. Then if the slow line was a function call, repeat within the function call.
Dirty, inefficient, but it got the job done.
After the tuner was just about done, we did some stress-testing. Namely, twist the rotary encoder back and forth rapidly while toggling the push button too. The UI froze.
Attaching the debugger and walking through the stack trace revealed an overflow of the buffer containing pending signals, which corrupted some global variables used by the event loop. Turns out you could only have 20 pending signals. It was possible to adjust the limit with a compile-time constant, but that would have just been kicking the can down the road. Instead we introduced a global flag which was checked by the ISR before sending a signal. It was set by the ISR and only reset by the signal handler in the event loop. This sort of violates the QP-Nano HSM abstraction, but there was no better option to avoid filling up the pending-signal buffer.
The chromatic instrument tuner is one of my favorite projects, I hope you learnt something from my summary of it.
Please send comments to blogger-jack@pearson.onl.