UNTZtrument is just the start. We hope you’ll hack it to your heart’s content. Some ideas:

  • New types of MIDI musical instruments or sequencers.
  • Beyond music…UNTZtrument could trigger video clips and effects for live “veejay” performances.
  • The Arduino Leonardo can also act as a HID keyboard or mouse…how about a game controller or an assistive device?

Not just code…there’s a little space for hardware hacking as well:

  • Encoders or potentiometers could be added to control different music parameters (volume, tempo, etc.).
  • A popular mod for the Arduinome (an inspiration for the UNTZtrument) is an accelerometer, for more expressive performance.
  • “Bling it up” with internal LEDs.

UNTZtrument hacking is not for the meek; it requires patience, tools and a willingness to improvise. Tolerances inside the case are very tight. If you’ve done “circuit bending” before (modifying electronic instruments), you can probably handle this.

Hardware Considerations


SDA and SCL are connected to digital pins 2 and 3 on the Arduino Leonardo. Those pins are not available as inputs or outputs. Everything else is fair game!

A USB port provides current up to 500 milliamps. UNTZtrument uses about 150 mA, allowing up to 350 mA for your own additions (but aim a little lower to play it safe). That’s enough for a few NeoPixels or other LEDs, or a small piezo buzzer or vibration motor for feedback. HELLA UNTZtrument uses close to 300 mA, so there’s about 200 mA available for adding your own bling.

Small slots between each of the “cells” in the UNTZtrument case can be used for routing wires. Unruly wiring can be kept at bay with some clear tape or a dot of hot glue.

There are three available GND pins on the Arduino board, one of which is used by the Trellis. Because we’re using USB for power, both 5V and VIN provide 5 Volts (VIN is normally the higher unregulated voltage from the DC jack, but that’s not being used here); UNTZtrument uses one of these 5V pins, leaving the other available. If you need to split power in more directions, 3-way and 5-way block connectors are available, as are smaller 2-way cold splices.

Acrylic requires special techniques for drilling; normal drill bits will crack it! This optional plastic part (separately available from Adafruit) has cutouts for adding encoders or potentiometers.

Three sides of UNTZtrument are identical, so you can add controls to any (or all) sides. On the HELLA UNTZtrument, these fit only on the left and right sides, or there’s a longer front part specifically for the HELLA.

If you have access to a laser cutter, the UNTZtrument case design can be downloaded from the last page of this guide, so you can make any special cutouts you like!

If adding more than one potentiometer, the legs must be bent back to fit inside the case. If two potentiometers are installed in the same quadrant, they install back-to-back.
Potentiometers don’t care which outer leg is ground or +V, but the choice does affect which direction has increasing output.

If you want increasing values when turning clockwise, and if using the 10K pots in the Adafruit shop, wire them as shown here.

Other pots may behave differently. If the output is reversed from what you wanted, just swap the outer connections, or compensate in code using the map() function.
If using rotary encoders, we have the opposite problem when fitting in the case: rather than bending the legs back, we need to splay them outward.

You need to be REALLY CAREFUL when doing this. DO NOT straighten the existing bends — the legs will crack off! Instead, add extra bends a little further down.
Whatever controls you add, you’ll then need to get really creative with routing wires. Narrow gauge (24 or 26 AWG) fits in tight spaces a little better.

Potentiometers especially are a tight squeeze.

In the second photo you can see all of the hackishness that was required for one project. A clamp provides a temporary hold while working. Hot glue keeps loose wires at bay. Cold splices provide multiple +5V and GND connections.

Software Considerations


If adding I2C devices (such as certain accelerometers or other sensors), be aware that our example code does a crass thing to activate 400 KHz I2C (this makes the LED updates a bit more responsive), and it might not be compatible with other devices you’re interfacing. If this creates a problem, one option is simply to disable this line of code, reverting to the standard 100 KHz communication.

On 8-bit AVR boards, the offending line, commented out, would be:

Download: file
// TWBR = 12;

On 32-bit SAMD-based boards (e.g. M0), the line in question is:

Download: file
// Wire.setClock(400000L);

Another option (on AVR) is to save the original value of the TWBR register in a global variable following initialization, then set it to the 100 or 400 KHz rates as needed.

Global variable, before setup():

Download: file
uint8_t i2c_save;
In setup() function, after trellis.begin():
Download: file
i2c_save = TWBR; // Original 100 KHz value
In loop() or other functions:
Download: file
TWBR = 12; // 400 KHz
untztrument.writeDisplay();
…
TWBR = i2c_save; // 100 KHz
// Read/write other I2C devices here

These are architecture-specific hacks! If you’re thinking of adapting the UNTZtrument code to any other microcontroller, don’t do this, it’s non-portable and may be more trouble than it’s worth.

Using the UNTZtrument Arduino Library


The Adafruit_UNTZtrument library provides a few utilities to help with creating UNTZtrument-specific sketches. We foresee every application being quite distinctive, so the library is pretty minimal and doesn’t impose a lot of policy.

To use the library, you need to include three header files:

Download: file
#include <Wire.h>
#include <Adafruit_Trellis.h>
#include <Adafruit_UNTZtrument.h>

The first enables I2C communication. Second is the core Trellis library for reading button presses and changing the LEDs. The third has our UNTZtrument-specific utilities.

The Adafruit_UNTZtrument library contains two object classes. The first is called Adafruit_UNTZtrument and is simply a slight extension of the Adafruit_TrellisSet class from the Adafruit_Trellis library…in fact, declaring and initializing the Adafruit_UNTZtrument object is identical to a TrellisSet:

Download: file
Adafruit_Trellis     T[4];
Adafruit_UNTZtrument untztrument(&T[0], &T[1], &T[2], &T[3]);
const uint8_t        addr[] = { 0x70, 0x71, 0x72, 0x73 };
void setup() {
  untztrument.begin(addr[0], addr[1], addr[2], addr[3]);
}

Four Adafruit_Trellis objects are declared (eight for a HELLA UNTZtrument) and passed to the UNTZtrument constructor. Their I2C addresses are passed to the begin() function in setup(). All the same Adafruit_Trellis or Adafruit_TrellisSet functions are then available to the Adafruit_UNTZtrument object.

The Adafruit_UNTZtrument object provides two new functions for converting between button/LED index numbers (as normally used by the Trellis library) and (X,Y) coordinates (more useful for programs like the step sequencer).

i2xy() converts a Trellis button or LED index (0–63 for an 8x8 UNTZtrument, or 0–127 for a HELLA UNTZtrument) to separate X (column) and Y (row) values. You must provide the addresses of two unsigned char (or uint8_t) variables to store the results:

Download: file
uint8_t x, y;
untztrument.i2xy(i, &x, &y);
xy2i() is the complementary function, converting an X (column) and Y (row) pair to a button/LED index:
Download: file
uint8_t i = untztrument.xy2i(x, y);

Adafruit_UNTZtrument also provides the enc object for interfacing quadrature encoders. Unlike analog potentiometers (which have a set range of 0–1023 using analogRead()), encoders are more flexible (and they can spin around and around, no end stops).

Each encoder requires two pins (sometimes called the “A” and “B” channels). The third pin connects to GND. The shaft “click” function of some encoders is not explicitly handled by the library…but it works just like any normally-open contact switch and is pretty straightforward to code for.

Do not connect encoders to Leonardo pins 2 or 3 — those link up to the SDA and SCL pins required for I2C communication. Anything else is fair game though (even pins 0 and 1, which Arduino Uno coders often have to avoid).

To use an encoder, just pass the index of the A and B pins to the constructor:

Download: file
// You can declare encoders onesy-twosey:
enc e(4, 5);

// Or create a whole array of them:
enc e[] = {
  enc( 4, 5),
  enc( 6, 7),
  enc( 8, 9),
  enc(12, A2) };

The encoder pins do not need to be sequential, nor are they limited to pin-change-interrupt pins. Any two pins will do.

An optional third parameter to the enc() constructor enables or disables the Arduino’s internal pull-up resistor on that pin. Some encoders (such as the ones previously shown) use the pull-ups…this is the default case and the third parameter is not needed. Others (“active” encoders, requiring a 5V connection) drive their output high or low on their own; the pull-up is not needed. Pass a third parameter of false when using these. This is rare.

In your setup() function, you must initialize the encoders as follows:

Download: file
void setup() {
  // Initialize any/all previously-declared encoders:
  enc::begin();
}

The encoders need to be frequently polled in order to update their values (this is the downside to not using interrupt pins). There are a couple of ways to do this. Simplest is just to put this line in your loop() function:

Download: file
void loop() {
  enc::poll();
}
You only need one of these; it’s not necessary to poll every encoder individually, it handles the lot.

This requires the loop() function to iterate quickly. But if your program occasionally has to do something more time-consuming, the encoders may “lose counts” (the value won’t change as the knob is turned). To avoid this, you can use a timer interrupt to call enc::poll() at regular intervals (e.g. once per millisecond). This requires some homework…the library does not provide this — timers are a limited resource and every application or timer library sets its own rule for these — so you’ll have to wrap a bit of your own code around it.

To read the value from an encoder, use the getValue() function:
Download: file
// Reading from one encoder named "e":
int16_t bpm = e.getValue();

// Reading one encoder in an enc array:
int16_t bpm = e[0].getValue();
The value is a signed 16-bit integer value. By default, this allows a range from -32768 to +32767. The range can be limited using the setBounds() function:
Download: file
// Limit encoder "e" to 0-100 (inclusive):
e.setBounds(0, 100);
An optional third parameter sets whether the value stops (further turns won’t further change the value) or “wraps around” (turning past the upper limit returns the value to the lower limit, and vice-versa). Pass true to enable wrap-around, otherwise pass false (or just leave off the third parameter — false is the default behavior).

The value of an encoder can be set or changed with the setValue() function:
Download: file
e.setValue(42);
When setting bounds, if the current value of the encoder falls outside the requested range it will be clipped or wrapped as appropriate. Therefore, if you need to change both the range and value of an encoder, it’s recommended to set the range first:
Download: file
e.setBounds(0, 100);
e.setValue(42);
Some encoders have detents — little clicky stops you can feel as the knob is turned. With the Adafruit encoders, there are four “counts” per detent…that is, it tends to stop on the values 0, 4, 8, 12, etc. But maybe you’re using other encoders that spin freely. The library doesn’t dictate the use of encoders with detents, so you may want to compensate in code by setting a 4X larger range and dividing the value read by 4 (or whatever detent size your specific encoder uses).

For example, let’s suppose we wanted an encoder for setting a tempo between 60 and 480 beats per minute (240 by default). We’re using an Adafruit encoder or another type with the 4-count detents, so the relevant parts of our code might look like this:
Download: file
enc e(4, 5); // Encoder on pins 4 & 5

// in setup():
e.setBounds(60 * 4, 480 * 4 + 3);
e.setValue(240 * 4);

// in loop():
bpm = e.getValue() / 4;
This then provides the expected one step per detent.

I instinctively always want to say détente, but no, that’s a different word for a different thing. Detent. De-tent, like having one’s tent removed. Even Wikipedia notes this confusion. :)
This guide was first published on Jun 20, 2014. It was last updated on Jun 20, 2014. This page (Hacking) was last updated on Aug 21, 2019.