The Development of Univer Inter, part 1
From time to time, Stephen and Kris like to write a bit about the development of a product. Today we’re talking about Univer Inter: part history, part technical, part wacky look into what turned out to be the most frantic and wacky release we have probably ever had. Today is the first of two parts. Here we’ll talk about the history and some of the hardware challenges. Next time, we’ll talk a bit more about the history of the configuration app and some of the design and development behind that.
The History of the Univer Inter
We started working on Univer Inter mostly out of utilitarian desires: the options for a MIDI-to-CV module at the time just didn’t quite do what we wanted, so like any module maker, we figured we’d just make our own. How long could that take?...
The first prototyped version was a mostly fixed-function 8HP module. This version was functional but never got past the prototype stage. The second prototype was stripped down to only have gate outputs but added USB. This version was also prototyped (and in fact, it’s been in many of Stephen’s jams on YouTube: the ones with the Monome also use this USB-to-gate prototype. Once we had Univer Inter prototypes these were swapped out.
The goal of the current iteration of the Univer Inter was to have a product that allowed the user to configure any output to do any behavior MIDI could communicate and to make that configuration easy to do (no configuration files etc). We had grand plans of chaining eight at a time but reality eventually set in about hardware and performance constraints (and who is really going to buy eight of these?). Early testers had a great hand in helping us shape the feature set that we launched with (thanks to all of you).
Univer Inter is the first product we ever started on the STM line of chips that we now use exclusively (and that are very common in Eurorack). When we started the UI, we had a junior engineer, Ankoor. At that time, we were developing the UI on the STM042 and using HAL, the STM hardware abstraction library. Ankoor’s task was to get the firmware up and running and to create a configuration app that would go along with it. He wrote the app in Vue, got the firmware prototype up and running and off we went to NAMM 2020 with a grand public announcement.
While none of you have ever seen the UI app that Ankoor wrote, it became the basis for the Noise Engineering Customer Portal, which you have used if you’ve ever downloaded our plugins or changed the firmware on a Versio or Legio module.
Starting in 2020, of course, supply chains got complicated.
We were developing a load of smaller modules on the STM042. The first to be released was Fractio Solum. For us, the first hint of the pain to come was when we were building the second run of FS and there were no STMF042 anywhere at any price for a reasonable lead time. We opted to change our low-end CPU spec to the STMF072 as they were more available at that time. This caused its own headache as the firmware that was written for the F042 required changes to be compatible with both CPUs but once that was solved we were able to keep building FS. However, we were not able to obtain enough to release any new modules with this CPU for another year (see another delayed release: Vice Virga…and a few that we ended up canning as a result of the delays) which led us to focus our development time on modules that we could release (like the Versios, Quantus Ampla and redesigning everything else we make so we could keep manufacturing it in the face of the parts shortage).
Sometime in 2021, Ankoor decided to pursue another job. That delayed both app and firmware development, so the UI went to the back of the line and stayed there for quite a while.
In the intervening time, we did a lot that would end up affecting the remainder of the UI development. The biggest was the development of our own internal hardware abstraction library to replace STM’s HAL. Its name is Lacerti (Latin for lizard or arm) or Lac to friends. This was a many-year undertaking that started right about the same time we were working on the first UI prototype. The first product released with Lacerti was Desmodus. Every product we released added more functionality. Lacerti is a modern (aka heavily templatized) C++ library that was designed for getting maximum performance out of compiled code.
Finally, we said 2023 would be our year for UI, which we had of course never said before. We set our release date for June and buckled down to make it happen.
What were the challenges of making the Univer Inter?
Univer Inter represents a lot of firsts for us. Technically speaking, it’s our first product with USB. (Strictly speaking, a few other modules have USB ports, but those are used only for firmware flashing and not in the use of the module itself. This flashing functionality is built into the chips so we did not implement it.). It’s our first MIDI product (we got a MIDI Sysex ID! 0x00 0x02 0x44 We have cred! We felt very nerdy.). That meant a completely different testing paradigm for the team. Some of our team hadn’t ever used MIDi significantly before so there was a lot of education. It’s our first product with expander headers on the back…and that’s all we’ll say about that for the moment. Univer Inter’s app is integral to the use of the module, which is new (more on that next time). It’s also all done on a module with a CPU that has only 16k of RAM and 64k of flash.
Using our own library helped minimize RAM use and generated-code size while maximizing speed. As we’ll see in a minute, there’s a lot of data to handle and we needed to do it all in under 1ms.
Let’s talk about the technical specs of the Univer Inter.
Univer Inter features four bi-directional serial links: USB, classic MIDI, and two expander serial links which we’ll neglect for the time being.
On the input, we have to buffer all our data sources without losing data. Given the constraints of how much memory we have, we have to balance how often we check the input stream and how big a buffer we can store. The USB peripheral on the F0 has built-in flow control so if the processor is busy, the USB will not send data – this is a standard USB behavior and turns out to be quite useful. However, The UARTs we use for our other serial links have no hardware FIFO (a nifty helping function on some peripherals that lets the cpu check for data less often) so the firmware has to read a byte out of of the peripheral before a new one can be received: it interacts with the port at the protocol’s byte rate. For MIDI, this is approximately 32150/9. We use IRQs to handle received data for all sources. When an IRQ happens we just move the received data into a queue to be handled by the main loop.
For each input, the UI parses the enqueued data in the transport format into discrete MIDI messages. Normal MIDI messages are at most 3 bytes. We append a fourth byte with metadata that indicates where it came from and what format it is and put it in the main work queue. Incoming UI sysex commands are also encoded into four bytes. Non-UI sysex commands get forwarded to appropriate outputs.
To meet our desired performance spec, the UI has to respond to each received message within one millisecond. A response can either be generated MIDI data (there is an output work queue for that), or value to send to the output DAC possibly with a delay. Output value changes are added to a scheduling queue that sorts the desired values for a given output by the desired time for them to be outputted. This is the same scheduler we used in the Jam Jam module. For example, if UI receives a MIDI clock message, and there is an output programmed to clock sync out, it checks the clock divisor and possibly schedules an event that takes the output from zero to maximum at the appropriate time. It also schedules a second event to bring the clock back down after the specified clock-length time.
Every millisecond the processor looks at the event queue to see if outputs need to change. If it’s a local output event, it sends a value to the DAC. If it’s a chained output and chain is enabled, the value is converted to MIDI and put into the output work queue.
A final pass takes messages from the output work queue, converts them into MIDI and enqueues them into the various output queues that feed the serial peripherals.. Messages in the output work queue can be normal midi messages or UI sysex messages. The UI sysex messages are encoded into four bytes in this queue which allows us to keep the serial output queues much smaller (some UI sysex messages generate more than 300 bytes of data).
Internally the UI has full flow control so if the MIDI serial output is full we will not process the work output queue. If the work output queue is full, we don’t process the work input queue. If the work input queue is full, we do not process the serial input queues. If the serial MIDI output buffer is full, we do not accept more USB midi data as USB can easily outrun serial MIDI. The flow control plus the ability to compress the UI sysex messages into 4 bytes lets the UI behave smoothly in extreme situations.
Let’s talk about DACs, baby
For each event, we pass the DAC an array of values that get sent to the outputs at the end of each event pass. The processor only sends data that have changed; this helps minimize latencies. We also only do an event cycle if the DAC is ready to receive, though in practice we’re not really limited by the DAC: At its fastest we could send updates to all values at about 1.8Mhz.
The DAC is a 12-bit, 8-channel one that we like a lot. It’s extremely precise and has excellent linearity, so it’s great for pitch. We also use an external voltage reference for calibration to 5.333V (64 semitones). We use other versions of the same DAC in other places and we have been very happy with it. UI may really let us push it to its limits as we roll out further features, though.