Before MIDI, and long before USB, different synthesisers used to talk to each other the same way that different parts of a single synthesiser did: using control voltages, gates and triggers.
Triggers are very straightforward: they're a short pulse sent when something happens, such as when a key is pressed on a keyboard, or when a drum sound is due to play on a drum machine. Usually, it's a 0V signal when nothing is happening, and a brief burst of +5V when something starts to happen, although there's no one true standard. Some manufacturers use +12V, and some have the voltage applied only when the trigger isn't occuring. I favour products by Roland and Doepfer, however, so 0V with the occasional +5V burst is enough for me to concentrate on.
Gates are like triggers, only they last for the duration of an event. For example, they last as long as a key is being held down on a keyboard, signifying how long a note should last.
Control voltages are used for everything else. They're analogue in the sense that they can be any voltage in a given range, not just on or off. You can use a control voltage to convey the information of what any one parameter should be, regardless of what that parameter is. The parameter in question could be anything from the note's velocity, to the filter's cutoff point, or even the pitch of the note itself.
Continuing the theme of no real standards, different manufacturers allow control voltages in different ranges. About the safest is again 0V to +5V, as a lowest common denominator. Even with pitch specifically, there's no real consensus on which voltages represent which frequencies. Some manufacturers (most notably Korg and Yamaha) prefer a linear approach such as representing a set number of hertz per volt, such as 1kHz per volt. Others prefer an exponential approach such as a set number of octaves per volt. (Remember that the human ear is exponential in how it perceives the volume and pitch of a sound, resulting in our representation of music being exponential too. Each time you play a note an octave higher, you're doubling its frequency.)
Even amongst the manufacturers embracing octaves per volt, there are several implementations, such as having 1.2 volts per octave (used by Buchla and EML), 0.32 volts per octave (used by EMS), or exactly 1 volt per octave (used by ARP, Doepfer, Moog, Roland, and many others). Again, as I'm primarily interested in equipment made by Roland and Doepfer, and as it's the closest there is to a standard, I'm going to concentrate on 1 volt per octave.
With 1 volt per octave, it's pretty straightforward to convert a note's pitch into the voltage required, then convert that into an actual frequency. C is always represented by an integral voltage, so C0 is 0V, C1 is 1V, and so on. F♯ (being half way between two Cs) should be x.5V, and D# (being half way between C and F#) should be x.25V, and so on. A range of 0 to +5V is good to aim for. It covers five octaves, and I believe 0 to +5V is the lowest common demoninator range that shouldn't blow anything up, providing you check whether the electricity's flowing from the ring to the tip or vice versa. I'm clearly not an expert at electronics, so this advice comes with absolutely no warranty whatsoever. I just hooked up a Doepfer A-190-3 MIDI to CV converter to a cheap voltmeter, then hooked up our homemade step sequencer to a Doepfer A-110 VCO.
Basically, the simple formula to convert pitches into volts is V = P / 60 * 5, or to put it more succinctly, V = P / 12, where V is the voltage to output and P is 0 for C, 1 for C♯, and so on. This changes the range from "0 to 60" to "0 to 5". Or, in Python terms:
for pitch in range(61): voltage = pitch / 12 print(voltage)
The oscillator then has to convert this into a frequency, F. If we use A4 = 440Hz = 4.75V as a reference point, we can use the formula F = 440 / 24.75 * 2V to work out the frequency of any voltage. Again, here's the Python equivalent:
for pitch in range(61): voltage = pitch / 12 frequency = 440 / 2 ** 4.75 * 2 ** voltage print(frequency)
If you're using a 12-bit DAC with a 0V to +5V range to convert the pitch into a voltage, then instead of dividing by 60 and multiplying by 5, divide by 60 and multiply by 4095, or to put it more succinctly again, just multiply it by 68.25. This will change the range from "0 to 60" to "0 to 4095", with 4095 being the maximum number you can specify with twelve bits.
for pitch in range(61): twelveBits = pitch * 68.25 print(twelveBits)
Even more ideal, if you don't need portamento or alternate tunings compared to twelve-tone equal temperament, is to get a 7-bit DAC and convert its range into 0V to +10.58(3)V, or get a 6-bit DAC and convert its range into 0V to +5.25V. Each increment will be exactly one semitone.
Similarly, we can use a 12-bit ADC to convert that voltage back into an integer, and work out what the oscillator's frequency should be from that. This should work:
for pitch in range(61): twelveBits = pitch * 68.25 frequency = 440 / 2 ** 3890.25 * 2 ** twelveBits print(frequency)
...but I for one can't test it because 2several thousand is a scarily high number! So it's probably easier to convert the twelve bit value into volts first:
for pitch in range(61): twelveBits = pitch * 68.25 # A physical lead can carry the analogue CV signal at this point volts = twelveBits / 819 frequency = 440 / 2 ** 4.75 * 2 ** volts print(frequency)
Again, dividing by 819 is simply a more terse way of dividing by 4095 and multiplying by 5.
Using this information, it's reasonably straightforward to experiment with making simple (and eventually complex) step sequencers, as my partner and myself have been doing.