Showing posts with label Source Code. Show all posts
Showing posts with label Source Code. Show all posts

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


Saturday, March 15, 2025

A lightweight ADSR for AVRs

    I've been doing more projects that involve digital control of analog circuits, and I found myself needing to generate ADSR envelopes in software, on an Arduino. I found some approaches that implement a digital filter to simulate the RC curve you get from a capacitor charging. This usually involves slow floating-point calculations that the Arduino isn't well-suited to crunching.

RC curve ADSR - AudioMulch

Look up, it's a bird, it's a table

    A classic way to avoid slow math is to precompute it, and store the results in a table. Then you just look up the result for your given input. Storing the result for every possible input can result in an excessively large table though, so maybe you want to store fewer results and estimate values between them. This is called interpolation, and there are multiple ways of doing it. We could use the continuous curves of splines to connect our points, but this would be slower than the actual calculation we're trying to avoid. Instead we'll use fast linear interpolation, that just draws straight lines.

An interpolated attack curve

    Usually you would store a series of x and y coordinates, and then calculate the line between each pair. We really only care about the lines though, so we might as well precompute those also. Thinking back to algebra 1, we just need to store the slope and y-intercept to define them.

It's not a phase, period

    Now that we have a curve, we need to progress through it over time. A naive approach might be to march through every possible value, and simply do this as fast as needed. This is likely a waste of processing time, and we can instead strategically skip over some values to progress through the table more quickly. The Prophet 600 reportedly updates its envelopes at just 200Hz. It simply takes larger or smaller "steps" each update to change the length(period) of the stages.

Ain't no half‐steppin'

    Another beginner trap is to only think in terms of whole numbers, since we're avoiding floating points. Let's say we have 10 possible output values, and our smallest step is 1. It then takes 10 steps to get through all outputs. If our next smallest step is 2, it only takes 5, and that's twice as fast. It'd be nice to have 1.5 as an option. We can do this by using fixed point numbers. We can use steps of 15, and count up to 100 to get the same effect as steps of 1.5 counting up to 10. We just need to divide by 10 at the end to get the correct output. If we use powers of 2, instead of 10, the division becomes bit-shifting, and that's very fast.

Bit shifting to divide by 2 - Wikipedia

Non-canonical sections

    We just saw that a step size of 2 is twice as fast as 1. This hints at the funny relationship between the step size and the period. It's just period = max count / step, but this results in a very uneven response. When the step size is small, a slight change results in a very different period length. When the step is large, even significant changes have very little impact on the period length.

period = max count / step size

    Algebra 1 strikes again; This curve is a conic section called a hyperbola. It comes from us having a function in the form of 1/x. We can't really help that we're dividing a constant by a variable, but we can control the variable. If we replace x with 1/x (the reciprocal), we effectively multiply by x instead of dividing by it. 10/(1/x) = 10 * x. Now we have a straight line.

period = max count / (1 / step size)

    This looks better, but it doesn't feel right when mapped to a control. This is because the period is being adjusted with the same granularity on slow attacks vs fast attacks. That ends up being too coarse at one end, and too fine at the other. We need a curve after all.

The epitome of hyperbola

    The hyperbola we started with was too severe, and didn't pass through any particular points. What if we could fix those problems? Then a hyperbola might be a suitable curve.

    1 / (x + 1) / (10 - x) will approach points 0,10 and 10,0. That's helpful. We can change the severity of the curve by multiplying x by a coefficient in the numerator. Here's 1 / (0.25x + 1) / (10 - x)

period = max count / ((0.25 * step size + 1) / (10 - step size))

    By continuing to manipulate this formula, I landed on a new one that lets you adjust the maximum period without distorting the curve. I'm using 10 bits for the step size, so the number 1024 comes from the maximum step size. c sets the curve, and m sets the maximum period


    I'm using a sampling rate of 4kHz, and a maximum count of 2^20 = 1048576. This gives us the whole calculation to find the length of a period in seconds:

period length in seconds

    c, m and the constants are all known at compile time, so this can be simplified in our program prior to execution. A c of 4976 and an m of 3 simplify to roughly:

c = 4976, m = 3

    You can graph this function and adjust it in real time at this desmos link

Starting over

    There's another snag, restarting the envelope. It's tempting to begin counting from 0, but that's not how analog envelopes typically work. They start the attack stage from whatever their output level currently is. That would be simple, but we progress through our stages linearly, while the output is the result of our lookup table. So we have to convert from an output level, back to a position in the attack stage (that yields the same output). I chose to use a second lookup table to convert outputs back to attack stage positions. 

Release

    The C++ source code is on my github here. The focus is really the ADSR library, but the project is a functional example. It uses an Arduino nano, four potentiometers plus a trigger for input, and an MPC4728 for 12-bit analog output.

    Here's an incomplete prototype that I wrote in JavaScript. The gate is controlled in real time by mouse click. The red dots indicate the progress through each stage, and the black line is the actual output.




Saturday, October 16, 2021

Storing data on a cassette using Arduino and Python (Differential Manchester encoding)

    I've been building a retro computer, and it's gotten me interested in using cassettes as data storage. This poses an interesting challenge where binary information has to be converted into something that can be written to, and reliably read from, a cassette. We have to worry about immunity to noise (tape hiss), speed fluctuations (wow/flutter), and amplitude fluctuations (dropout).

    Another limitation is frequency response. Our signal has to stay safely within the range of frequencies a tape can reproduce. This range can be as narrow as 400-4,000Hz for something like a microcassette. We could send a stream of bits at a safe 2kHz, but what if we then have a very long run of all zeros (or ones)? Our signal would dip below 400Hz, and our data would be lost. 

frequency response of my Pearlcorder L400 microcassette recorder


    One solution is to toggle our output at least once per bit. Two bits would give a full cycle and guarantee a minimum frequency of 1kHz. The presence of an additional toggle can represent a zero, and its absence a one. If every bit had an additional toggle, it would yield the maximum frequency of 2kHz. This is the basis of Differential Manchester encoding. 

Differential Manchester encoding - wikipedia


    Besides it fitting nicely in a frequency range, Manchester has other advantages. Cassette recorders rarely concern themselves with the polarity of their signal (since it doesn't affect the sound) and will sometimes invert their output relative to their input. Manchester encoding only uses the presence of these "toggles" or edges, and is unaffected by being inverted.
    Also, each bit spends an equal amount of time high and low. This means we have no DC offset. If the offset were irregular, our signal would drift up and down, making decoding more difficult.
    It's fairly resilient in the face of speed warbles too, as we have an octave separating our ones and zeros. In other words, zero is always twice as fast as one. For comparison, one early modem standard used 1300Hz and 1700Hz for one and zero respectively.

    So, Manchester encoding it is! This settles how we encode individual bits, but not how we structure our data. I chose to mimic the standard serial packet structure of "8n1". This means a zero starting bit, 8 data bits, no parity bit, and a one stop bit. This makes it easy to figure out exactly how the data is aligned when receiving.

8n1 - wikimedia commons

    I opted to add a calibration tone to the beginning of my files. This gives the receiver time to detect the amplitude, and more importantly, the frequency of the signal. This tone is simply a long string of ones. The starting bit (zero) of the first byte signifies the end of the tone.

    I've written a python script that will take a binary file and output a Manchester encoded audio file that can be recorded directly onto a cassette.

 Python encoder:
#Converts binary file to Differential Manchester encoded audio
# outputs 32kHz, 8bit, mono WAV. 8N1 format at 3200 baud
# includes calibration tone, and checksum. Zack Nelson 2021
import struct, os
from sys import argv

smplrate = 32000 #Hz
baud = 3200 #needs integer ratio between baud and sample rate 

#functions-------------------------------------------------
#each bit starts by inverting the output
#zeros will invert again in the middle
def out_bit(bit):
    global bit_status
    bit_status = not bit_status #toggle
    for x in range(2): #2 half-cycles
        for y in range(int(smplrate/baud/2)): #samples
            if bit_status: buf.append(0xD8) # hi
            else: buf.append(0x28) # lo
        #toggle if bit 0
        if x == 0 and not bit: bit_status = not bit_status

def out_byte(byte):
    out_bit(0) #start bit
    for i in range(8):
        out_bit(bool(byte & (1<<7)))
        byte <<= 1
    out_bit(1) #stop bit
#---------------------------------------------------------
    
try: len(argv[1]) #load arguments
except IndexError:
    print("Input file needed")
    exit(2)

fi = open(argv[1],'rb') #open input
fo = open(os.path.splitext(argv[1])[0]+".wav", 'wb+') #open output

file = bytearray(fi.read())
fi.close()
buf = []

bit_status = False
checksum = 0

for i in range(smplrate): buf.append(0x80) #silence
for i in range(256): out_bit(1) #calibration bits

for byte in file: #add all bytes
    checksum += byte
    out_byte(byte)

out_byte(checksum) #add checksum

for i in range(smplrate): buf.append(0x80)#silence

#write wave header to file
fo.write(str.encode("RIFF"))
fo.write((len(buf) + 36).to_bytes(4, byteorder='little')) #length in bytes
fo.write(str.encode("WAVEfmt "))
fo.write((16).to_bytes(4, byteorder='little')) #Length of format data
fo.write((1).to_bytes(2, byteorder='little')) #PCM
fo.write((1).to_bytes(2, byteorder='little')) #Number of chans
fo.write((smplrate).to_bytes(4, byteorder='little')) #Sample Rate
fo.write((smplrate).to_bytes(4, byteorder='little')) #Sample Rate * bits * chans / 8
fo.write((1).to_bytes(2, byteorder='little')) #8bit mono
fo.write((8).to_bytes(2, byteorder='little')) #Bits per sample
fo.write(str.encode("data"))
fo.write(len(buf).to_bytes(4, byteorder='little')) #length in bytes

fo.write(struct.pack('B'*len(buf), *buf)) #write audio to file
fo.close()

Hardware Interface


    Now that we can store data onto a tape, we need a way to read it back. First we'll focus on the hardware required to connect the cassette recorder to a computer. 

Tape to computer interface schematic


    To read from a tape, the audio is first highpassed. This reduces potential DC offset, and noise from motor rumble. The audio is then amplified, bringing the tape's ~1V line-level output closer to the 5V we want for the digital signal. The amplification stage also lowpasses the audio, reducing some hiss and noise outside of the range of our signal.
    Next the audio is passed through a schmitt trigger. This transforms the smooth audio to a rigid, digital signal by comparing it to two thresholds. If the audio signal goes above the high (2.6V) threshold, the output is a digital one. If it goes below the low threshold (1.5V), the output is a zero. If the signal hangs out between the two, the output does not change. This provides some noise immunity. As long as the noise doesn't swing enough to push the signal over the wrong threshold, it will simply be ignored.

Schmitt trigger input(U) and thresholds (A, B) - wikipedia


    Now we have a digital signal, but it's still Manchester encoded. I selected an Arduino to run a proof-of-concept decoding program. It takes in the digital signal from our interface board (via pin D2) and outputs the decoded bytes over serial. 
    To do this, it listens to part of the calibration tone, and calculates the signal's timing. It uses this timing to discern ones from zeros. Three edges close together count as a zero. Two edges far apart count as a one.
    When it detects the first zero (start bit), it begins constructing and transmitting bytes.
    It has the ability to detect and report framing errors (incorrect start/stop bit placement), and invalid edge patterns. It's unable to recover from these errors though. It would be possible to correct framing issues by buffering bits and searching for valid frames within the buffer.


Arduino Decoder:
// Differential Manchester decoder
// Zack Nelson
const byte pulsePin = 2; //interrupt input

int byte_count = 0; //count for printing newlines
uint32_t last_ts = 0; //timestamp of prev edge
byte edge_count = 0; //edges per bit
byte bit_count = 0;
byte rec_Byte = 0;

//calibration-----------------------------------
unsigned int hi_threshold = 0; //hi pulse in uS
unsigned int cal_count = 0;
unsigned int cal_ts = 0;
bool lead_in_done = 0;

void setup() {
  pinMode(pulsePin, INPUT);
  
  attachInterrupt(digitalPinToInterrupt(pulsePin), count, CHANGE );
  
  Serial.begin(230400);
  Serial.print("Start. ");
  
  while(!hi_threshold); //Calibration done--------------------------
  Serial.print("High threshold(us): ");
  Serial.println(hi_threshold);
}
  
void loop() { }

void count() { //gets called on every transition of data pin
  if (!hi_threshold){ //Calibration---------------------------------
    if (cal_count == 32) cal_ts = 0; //skip 0-31 readings
    cal_ts += (micros() - last_ts); //average 16 pulses
    if (++cal_count == 48) hi_threshold = cal_ts / 21; //calc 75%
  } else { //Receive data--------------------------------------------
    bool bit_val = ((micros() - last_ts) > hi_threshold); //hi or lo?
  
    //lead in check--------------------------------------------------
    if (!lead_in_done && !bit_val) lead_in_done = 1; //first zero

    if (++edge_count > 2) { //error
      Serial.println("Edge cnt err");
      edge_count = 1;
    }
    
    //low bit = 2 fast pulses, high = 1 slow pulse
    if ((!bit_val && edge_count == 2) || (bit_val && edge_count == 1)){
      if (lead_in_done) bitDone(bit_val); //add bit to byte
      edge_count = 0;
    }
  }
  
  last_ts = micros();
}

void bitDone(bool bit_val) {
  //start bit lo, 8 bits MSB first, stop bit hi
  if (bit_val) rec_Byte |= (0x80 >> (bit_count-1));

  if (bit_count == 0 && bit_val) Serial.println("Start err");
  else if (bit_count == 9 && !bit_val) Serial.println("Stop err");
  
  if (++bit_count == 10) { //complete byte?
    //Uncomment to print hex
    /*if (rec_Byte < 16) Serial.print(0); //leading zero
    Serial.print(rec_Byte, HEX);
    Serial.print(", ");
    if (++byte_count % 16 == 0) Serial.println(""); */
    Serial.print((char)rec_Byte); //print ASCII character
    
    bit_count = 0;
    rec_Byte = 0;
  }
}

    Files are available on my github page.

    Here are some images of my setup to read from a microcassette. I was able to use it to read data out at around 3000 baud.



Thursday, June 11, 2020

DIY 1702a Programmer

I've resumed working on the PAiA 8700 computer. It uses some very old, and hard to program EPROMs: 1702As. They require +37V, +47V, +59V and ground to program.

I've come up with a relatively easy to build DIY programmer. It doesn't require a parallel port, or dozens of transistors. It's Arduino based, and uses mostly common parts.
Here's what it looks like:


And here's a schematic:


A boost converter daughter board is used to generate a high voltage, over 60V. It then gets regulated down to the various voltages needed. IC1 (TL783) has to be calibrated to +47V, then the other voltages will fall into place.
An Arduino nano is able to manipulate the high voltages through three ULN2003 transistor arrays, and one IRF520 MOSFET.

Right now the code is very crude, but functional. It's on pastebin here. The binary for the 1702A is included in the Arduino program. On power up/reset the programming sequence begins. It's finished when the pin 13 led stops flashing.

I've now added the Eagle CAD files to my github here.

Monday, February 17, 2020

Williams Defender Sound Disassembly

Here's something different, an in progress attempt to reverse engineer the sound board for the arcade game "Defender" (and others). The sounds are very recognizable, and unique to Williams arcade/pinball machines. They always interested me, so I'm making an attempt to understand them better.

The board is based around a 6808 CPU (relative of the 68000), and a DAC attached to an IO controller. The binary is floating around the web, as is this great disassembler: DASMx. I used it to disassemble the ROM into a code listing, and started commenting it.

Here are the schematics from one of the compatible service manuals:


I was able to use the schematics to figure out the memory map. This helps understand the significance of certain read/writes in the code.
RAM: $0000 - $007F (128 Bytes)
PIA: $0400 - $07FF
ROM: $F800 - $FFFF (2KB)

By loading the ROM into audacity I was able to see some recognizable shapes. These are the waveforms/look up tables stored alongside the code. Some are played directly, while others are used to modulate things like pitch, or volume.

There are some interesting tricks done in the code, and I hope to explain them here one day. Things like dynamically generated delay loops, and something akin to granular synthesis...

Until then, the current version of the commented disassembly lives here.

Sunday, February 17, 2019

PAiA 8700 Schematic Redraw

The PAiA 8700 computer is an optional part for the PAiA 4700 modular system. It gives you the ability to run software like a "pink music" generator.

I took interest in it because it combines my favorite CPU, the 6502 (6503 here), and analog synthesis. It's pretty hard to find one these days, but PAiA published the schematics and they're still available. I needed them in Eagle CAD format to be able to work on them, so I redrew them here. In the process, I like to think I've improved the clarity and layout of the schematic.
The board is as close a reproduction as I could manage.

I've also typed out the original assembly source file and assembled binary.

Files available on my github




Also, a memory map. Why not?


I was able to find some NOS and begin assembling a brand new 8700 reproduction. More to come.

Monday, July 4, 2016

Resurrecting "C Programming for MIDI"

I've been playing with a DOS computer that I recently added to my studio. It's pretty limited, but it still speaks MIDI.

Finding old software for it can be difficult, but programming books are still floating out there. Enter "C Programming for MIDI". It details an Alpha Juno patch editor/librarian that uses an MPU-401 interface.

I've tried to find the original files or a copy of the floppy, but they're nowhere to be found. So, I had to copy the program out of the book.

The book has a number of mistakes: typos, wrong variable names, missing values/functions, etc. I fixed enough typos, and added few enough, that it compiles and seems to run correctly.


The source and binary are on github here.

It's almost useless today and was fairly basic in its day, but I think it's a decent starting point for making a DOS MIDI program.