Thursday, October 28, 2021

Pearl Fightman FM-8 Schematics

    I've drawn schematics for two of the Pearl Fightman's voices. The other voices are variations on these. I worked from my own boards along with board layouts found here.

    I had originally intended to draw, and write descriptions for all the voice circuits, but this resulted in me simply sitting on what I had done. Instead, I've polished these two enough to post them.

Hihat Schematic

Bass Schematic



 

Sunday, October 24, 2021

A CMOS FSK Modem

     While researching cassette storage, I found multiple different encodings, the most popular one being Frequency-shift Keying. It simply uses a high tone for a logical one, and a low tone for a zero. We'll be using the standard frequencies of 1,200Hz and 2,400Hz for our two tones.

FSK encoding - tutorialspoint

    Another choice we have to make is regarding phase. If we use two different oscillators for our tones, the phase will be inconsistent between them. This phase difference can cause a sharp "jump" in our output when we switch between them. The jump consists of extra harmonics that can complicate decoding, though they're actually utilized in a related encoding: "Phase-shift Keying".

Phase discontinuities (from Phase-shift keying) - Wikipedia

    If we instead change the pitch of a single oscillator, the phase will remain the same, and no extra harmonics will be created. This is called "continuous-phase" and it's the approach we'll be using.

The modulated signal is demonstrating continuous-phase - Wikipedia

    Another factor that can be important is where in the cycle the frequency change occurs. If we switch frequencies at a random point in a cycle, the length of the cycle can be anywhere between that of 1,200Hz and 2,400Hz (833-416uS). Some demodulators aren't affected by this, while some can be. In the interest of easy demodulation, we'll have a consistent switching point at the lowest extreme of the waveform.

Modulation

    So, how do we implement these features into a circuit? We can start with a simple oscillator built around a schmitt trigger. It works by charging a capacitor back and forth between two thresholds. The rate at which it charges/discharges defines the frequency, and that is set by the resistor/capacitor we select.

capacitor's charge, threshold(s), output - Wikipedia

    With our particular schmitt trigger, values of 66kΩ and 10nf give us 2,400Hz. If we can double either of these values, we'll get the other frequency we need, 1,200Hz. There are numerous ways of doing this with analog switches, FETs, etc. Here we will use a bipolar junction transistor to switch a 10nf capacitor in and out. When it's switched "in", it's in parallel with another 10nf capacitor giving an equivalent 20nf (for 1,200Hz). When it's switched "out", we're left with only the 10nf (for 2,400Hz). Now we have digital control over the frequency.

frequency switching simulation


    Notice that the frequency is switching in odd spots. In order to limit where in the cycle frequencies can shift, we add a D-type latch. It passes data through only when it sees a rising clock edge. We can use the square output of the schmitt oscillator to provide this clock edge, and the latch's output can control the frequency. This means the frequency can only change at the instant the oscillator begins to rise.

link to simulation

    Finally, we need to adapt the signal to something a cassette recorder can accept. We use the signal from the oscillator's capacitor, as it's close in amplitude to line level. We buffer it with an op-amp, since we don't want the cassette's input circuitry to load and disturb the capacitor's charging. Following that a capacitor and two resistors form a DC-removing highpass, as well as a voltage divider that gives us an additional mic-level output.

our completed modulator schematic

Demodulator

    Write-only memory isn't of much use, so let's work on reading. Some approaches use bandpass filters to determine if the signal contains a 1,200Hz tone or a 2,400Hz tone. We'll instead be detecting the difference in timing between peaks in the signal. This approach can detect frequency in as little as one cycle.
    
    Peaks can be a little tricky to detect, so we're actually going to convert our signal to a square wave, and measure between rising edges instead. After building our modulator oscillator, we have five schmitt triggers remaining. Just one will convert our signal to a square wave.
    We then highpass the square wave, and pass it through another schmitt trigger. This makes the pulses very narrow, benefiting the following stages. We'll call these pulses "edge-pulses".

square wave and pulse conversions (390Ω resistors model real schmitt outputs)

    Next, we add a monostable oscillator. It outputs a fixed-width pulse each time it gets triggered by the edge-pulses. This effectively extends the length of the edge-pulses. If we trigger it quickly enough, these extended pulses will overlap, and cease to be pulses. The output will instead be constant.
    The presence or absence of pulses is how we discriminate between frequencies. We can set the width (period) of the monostable output such that 1,200Hz will yield pulses, while 2,400Hz won't.

monostable 1

    This series of pulses isn't a very convenient signal to use for zero, so we'll convert it to a constant low signal instead.
    We can use the pulses to trigger a second monostable. If the period of this monostable is long enough, we will again have overlapping pulses that give us a constant low output.

    The period of this last monostable is somewhat critical as it impacts the maximum rate you can switch between high and low frequencies. The shorter the period, the faster the data can change. If it's too short though, there will be pulses in the zero signal. For this reason, we include a trim resistor that allows the period to be adjusted.

link to simulation

    Prior to this demodulation, we have a highpass, then a lowpassing amplifier. This brings the signal up to a level the schmitts can process, while also removing potential noise from outside the FSK range.

our complete demodulator schematic

    Combining the modulator and demodulator gives us the full modem.

a board layout


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.