Monday, March 16, 2026

Gakken SX-150 mk II & MIDI Autotune fundamentals

     I'm a big fan of small, toy synths and today we have the decidedly chintzy Gakken SX-150 mark II. It's the follow up to the SX-150 that was sold with the July 2008 issue of Gakken magazine.

    There is a pre-production schematic for it, but I don't love the layout. As for board diagrams, there are none. So, I of course redrew it here.

Red-headed step ladder

    There's not a lot to the synth, but the filter is interesting. It's a diode ladder filter (made from transistors) that closely resembles the 303's.

TB-303 filter

SX-150 mk II filter

    The biggest departure comes right before the output. The 303 uses a differential pair (Q21), but the Gakken replaces it with a differential amplifier (IC1B). The original transistor pair is actually a matched pair contained in one ic: the 2SC1583. This part is now rare and expensive, so they redesigned around the extremely common "jellybean" LM358 op-amp.

    They let the other two matched pairs (Q22 and Q12) simply be separate transistors. Maybe they selected for close matches, but I'd guess they didn't, given the price point.

MIDI

    It's not much of an instrument as it is, since it's very difficult to play any specific notes. The only interface is a stylus and a resistive strip that you touch it to. Let's add MIDI instead.

    The combo of stylus and strip forms a voltage divider that yields values in the range of 2.5 to 5V. How exactly does this control the oscillator though? Let's look at the schematic.

Gakken Oscillator

    Well, it has an exponential converter. That's very convenient for us. We can send our control voltage in via the "Strip" node. This is the point at which the stylus and the resistive strip meet. We can connect to this by alligator-clipping to the stylus tip itself. But, t
here's a snag. 

Gate binds you

    The "Gate" signal is derived from the Strip voltage. If the Strip voltage dips too low, the Gate goes low and disables the oscillator. This feature exists to prevent the VCO from droning when no note is being played (because there is no VCA!).

    If we just remove R5, this decouples the pitch CV from the Gate signal. We can then use the "Strip" input to inject a gate signal instead, and connect a new resistor to the CV summing amplifier. What resistor should we use though? Well, let's look at the gain of the amp.

No pain, no gain

    The original input resistor is 1M (R5), and the feedback is 470k (R21). `470000 / 1000000 = .47`. This gain is a bit high to be driving an exponential converter, but the value of feedback resistor is very high. This makes it easy for noise to to creep in and impact the pitch of the VCO, especially when experimenting using breadboards and jumper wires. So, let's reduce R21 to 47k.

    Just from playing with the values, we can find that 120k gives a nice, wide pitch range. The gain is very similar to what it was before (.39 vs .47). It turns out it needs to be relatively high due to the voltage divider formed by R25 and R30 that lowers the gain down to .025.

Autotune

    Now that we have a way to control the VCO, what voltages do we need to send to play a musical scale? That's going to depend on the exact value of components in a given SX-150, along with temperature (to some extent). We can add trimmers to hand adjust it, but we can instead make a microcontroller do the work for us. Previously we looked at a proof-of-concept guitar tuner made with an Arduino Nano R4. We can build on this for our autotuner.

Music is Math

    The tuner code can only tell us what a given pitch is, but that's a good starting point. We can send a voltage to the VCO, measure the frequency, and then repeat with a different voltage. This gives us two points of data, and that's enough to tell us where all the notes lay (assuming a perfect exponential response). This is because only two points are needed to define the line that they fall on.

    To make things simpler, we'll be converting frequency to note number. Here's a formula that does that:

`12 * log_2(f / 440)`

    Here are two real world values that I got from one version of my tuning code. It tried to play note #12 and got note #-9.81. Tried to play note #36, got note #47.95. This gives us points (12, -9.81) and (36, 47.95). We can use our favorite middle-school-math™ to find the definition of this line, revealing the location of all notes. Remember slope-intercept form? `y = mx + b`

    The slope (`m`) is defined by the rise (change in `y`) over the run (change in `x`):

`m = (y_2-y_1)/(x_2-x_1) = (47.95-(-9.81))/(36-12) ~~ 2.41`

    The other component is the offset (intercept/`b`). We can get this by plugging one of our pairs into the equation, along with the slope that we just found.

`y = mx + b`
`47.95 = 2.41(36) + b`
`47.95 - 2.41(36) = b`
`-38.81 = b`

    We can graph this with Desmos, and see that the line: `y = 2.41x-38.81` does pass through our two points, and the intercept is at -38.81.

Desmos graph of `y = 2.41x-38.81` 

    Now we've worked out the relationship between the note that we request and the note that we ultimately get. How do we adjust the note we request so that we get the note we want though? We have to work out the opposite line that will compensate for the existing one. The inverse slope will fix the scaling, making a change of 1 note on the input cause a change of 1 note on the output.

    It's worth pointing out that we can't change the original line's formula (without changing the hardware somehow); We can only modify our value of `x` that we put into it. So, here's `1/2.41x` plugged in for `x`, giving us:

 `y = 2.41(1/2.41x)-38.81`

    Notice how the slope is a nice 45 degrees now, but it still starts on the very odd value of -38.81.

`y = 2.41(1/2.41x)-38.81` in green

    What value could we add that would make our curve start from 0? It's tempting to say 38.81, to offset our -38.81, but that's not quite it. Remember the value will be multiplied by the original slope of 2.41. So, we need to again compensate for the slope and do: 

`-b/m = -(-38.81)/2.41 = 16.10`

    We can combine this with our slope, and rearrange terms so that it's more clear this is the inverse of the original line:

`y = m((x-b)/m) + b`

`y = 2.41((x + 38.81)/2.41) -38.81`

`y = 2.41((x + 38.81)/2.41) -38.81` in green

    The Arduino can easily perform this math, and Tada; We have a synth that's in tune.

    All the program has to do is find `m` and `b`, then plug them into this formula: `(x-b)/m` where x is your note.

    In the future we may explore the tuning of non-ideal oscillators. In the real world you'll find that the SX-150 goes noticeably flat in the upper octaves. This is likely due to too much time spent resetting the saw waveform vs the short length of the cycle at high frequencies.

Sunday, March 1, 2026

Arduino Nano R4 Input Capture (Guitar Tuner)

    I bought some of the new Arduino Nano R4 boards to play with. These boards step up from AVR to ARM (RA4M1) microcontrollers while still running at 5V, a rarity these days. The complete change of architecture brings with it some growing pains. Nothing is the same under the hood, and few of the new features are meaningfully supported within the Arduino ecosystem.

    I'd like to implement some kind of autotune for a synth, but how do we measure frequency with the new R4 peripherals? Since Arduino S.r.l. (Qualcomm) isn't going to hold our hand, we have to dig into the manufacturer's datasheet.

RTFM

    There's a feature that some timers have, called "input capture". It doesn't get mentioned all that much, but it's very handy for measuring external pulses. It allows an incoming signal to trigger a "snapshot" (capture) of a timer's current count. When configured correctly, it can measure things like the timing/width of a pulse, or the period/frequency of a waveform. If we search the datasheet for this term, we can find Figure 22.17

P 445 of the Renesas RA4M1 Group 32 User’s Manual


    This is the process to set up a timer for input capture. Great, but how do we follow this, and what are these acronyms? GTCR, GTUDDTYC, GTICASR... These are memory-mapped registers that control the timers. Explanations of every timer (GPT) register exist in the datasheet starting with Table 22.4 on page 396.

P 396 of the Renesas RA4M1 Group 32 User’s Manual

    Ok, but how do we actually use these registers within the Arduino IDE? Buried in the Arduino files is a header that defines these: R7FA4M1AB.h

    Here's GTCR. We can see that it's the "General PWM Timer Control Register". The header defines this as a union. This is just a method of declaring multiple variables that live within one address. CST is a single bit of GTCR, the least significant, while MD spans bits 16 through 18 of the same address.

GTCR definition from R7FA4M1AB.h

    If we look higher in the header, we see that the GTCR union is within a struct named "R_GPT0". This serves to contain all the timer-related registers. If we want to refer to MD, like the datasheet instructs, we would use: R_GPT0->GTCR_b.MD

The Wind-up

    The following code will configure the timer using the syntax we've established. There is a "gotcha" though. The timer module has to be enabled before it will function.

  //Timer Setup------------------------------------------------------------------------------------
  R_MSTP->MSTPCRD_b.MSTPD5 = 0; //enable GPT0 module clock (General PWM Timer 321 to 320 Module Stop)
  delayMicroseconds(10);
  
  R_GPT0->GTCR_b.CST = 0; //stop timer
  R_GPT0->GTCR_b.MD = 0b000; //saw-wave PWM mode (000b)
  
  R_GPT0->GTUDDTYC = 0b11; //set 11b first (per datasheet Figure 22.17)
  R_GPT0->GTUDDTYC = 0b01; //01b for up-counting
  
  R_GPT0->GTPR = 0xFFFFFFFF; //max cycle
  R_GPT0->GTCNT = 0; //0 initial count

  //GTCCRA input capture enabled on the...
  R_GPT0->GTICASR_b.ASCARBL = 1; //rising edge of GTIOCA input when GTIOCB input is 0
  R_GPT0->GTICASR_b.ASCARBH = 1; //rising edge of GTIOCA input when GTIOCB input is 1
  
  R_GPT0->GTCR_b.CST = 1; //start count operation

    Some of these acronyms above aren't mentioned in Figure 22.17, but the datasheet can clarify them.

On Time    

    The timer is running, and is configured for input capture on the rising edge of input GTIOCA. Well, what is that? It's a pin of the microcontroller, but the Arduino board obscures the true names of the pins by holding to their naming convention of D0-D13.

    We can view the real pin names in the schematic of the Nano boardGTIOCA is P107_GPT0_A, and that maps to "D7" of the Arduino header.

Arduino Nano R4 schematic

    When this pin state changes to high, the counter's value will be dumped into register GTCCR and the TCFA flag will be set. We can wait for this flag, read the value from the register, then clear the flag.

   while (!R_GPT0->GTST_b.TCFA); //wait for input capture A

   uint32_t lastTS = R_GPT0->GTCCR[0];
   R_GPT0->GTST_b.TCFA = 0; //clear the flag

    We run the risk of not polling the flag quickly enough, and missing a captured value though. A better approach is to use interrupts.

Excuse Me

    Interrupts are another poorly covered topic when it comes to the R4 within the Arduino IDE. The first snag to getting an input capture interrupt is the GPIO. Using the "Port mn Pin Function Select Register", the pin must be switched to work as a peripheral instead of standard IO. We then need to select GTIOC0A as the peripheral. The correct value for this can be found in Table 19.6

P 370 of the Renesas RA4M1 Group 32 User’s Manual

    Plugging in the value gives us this:

  //GPIO Pin D7 (P107) Setup
  R_PFS->PORT[1].PIN[7].PmnPFS_b.PMR = 1; //Used as an I/O port for peripheral functions
  R_PFS->PORT[1].PIN[7].PmnPFS_b.PSEL = 0b11; //GTIOC0A (GPT peripheral function)

    Then we need to set up the interrupt. This process isn't exactly straightforward, but we can crib from those who have gone before us. This post on the Arduino forums by pertomaslarsson is very helpful. Referencing it, I came up with this:

//Asynchronous General Purpose Timer interrupt (from vector_data.h)
static const IRQn_Type IRQn_CCMPA = AGT0_INT_IRQn;

  //Interrupt Setup
  //assign GPT0 Capture to IRQn_CCMPA (AGT0_INT_IRQn #17)
  R_ICU->IELSR_b[IRQn_CCMPA].IELS = ELC_EVENT_GPT0_CAPTURE_COMPARE_A;
  NVIC_SetVector(IRQn_CCMPA, (uint32_t)captureISR); //point to the ISR function
  NVIC_SetPriority(IRQn_CCMPA, 12);
  NVIC_EnableIRQ(IRQn_CCMPA);

    Much like the pins, there are a finite number of interrupt vectors, and we have to select what functionality we'd like them to have. Here we assign our input capture interrupt to the generic AGT0 vector. We also give it a function(Interrupt Service Routine) to call when the interrupt happens.

    Here's a simple example of an ISR function:

uint32_t lastTS, currentTS, delta = 0; //input capture readings

void captureISR() {
  lastTS = currentTS;
  currentTS = R_GPT0->GTCCR[0]; //read from input capture register
  delta = currentTS - lastTS;
  
  //clear interrupt flag for compare match A
  R_ICU->IELSR_b[IRQn_CCMPA].IR = 0;
}

    It stores the previous reading, grabs a new reading, and clears the flag, allowing a new interrupt to fire. We can use the readings elsewhere in our code.

Alright Stop

    So far, this has all been academic. Let's finish with a functional example. I've created a simple guitar tuner that displays the frequency in Hz, the name of the note, and how far the tuning is from concert pitch.

Guitar tuner proof of concept


    The source code is available on pastebin