Monday, October 30, 2017

Building a Seeburg Wall-O-Matic Interface (Part 3)

Decoding the Pulses

What the signal looks like

The way these wallboxes signal a song selection might seem a little weird from a modern perspective. When you press the buttons on the front, a collection of contacts are closed and a motor wipes a metal contactor across a studded disk:

Wall-O-Matic 100 Contact Wiper Mechanism

On the other side of this is a signal wire coming out of the wallbox. On that signal wire, you basically get a stream of pulses corresponding to the selection. Of course, those pulses aren't really a clean square wave. Rather, they're slightly noisy 25VAC. If you hook the signal wire up to an oscilloscope, it looks something like this:

Raw pulsed AC waveform

What the other projects did

Each of the other projects I looked at did things a little bit differently, but they all had a common theme: Rectify the AC, make sure its levels were brought in line with something a microcontroller could handle, and figure out the rest in software. Some of these projects also used an opto-isolator, so that sensitive electronics couldn't be damaged by crap coming from the wallbox.

The basic schematic looked something like this:

Rectifier, Regulator, Resistors, and Opto-Isolator
While this approach can be made to work, there's a fair amount of noise you have to account for in software. The pulses are not contiguous, and they are coming from a mechanism that is fundamentally prone to contact bounce.

Preprocessing the signal

Most of the elements of the basic design seemed good to me. I liked using a voltage regulator to bring down the levels, and I liked the idea of isolating the wallbox from the microcontroller. However, I didn't like the idea of having to reliably decode a pulse stream out of noisy rectified AC. With some additional circuitry, I figured that I could get to a significantly cleaner signal.

Circuit diagram

It took a fair amount of research and experimentation to come up with this, but here's the circuit I ended up with. On the input side, it takes pulsed AC from the wallbox's mechanism. On the output side, you get a clean digital pulse stream that is suitable for triggering interrupts and counting with minimal fuss.
Signal Processing Circuit

This circuit adds two main elements on top of the previous designs. On the input side, it adds an an appropriately sized capacitor. This capacitor's purpose is to smooth out the rectified wave just enough that it is invisible on the other side of the rectifier, but not so much that it obscures the pulse gaps. On the output side, it adds an RC debouncer designed to make sure the pulses are stable and have clean transitions. (I have to give credit to Jack Ganssle's page on the topic, for providing one of the most useful explanations and examples for figuring out this part.)

Tour of oscilloscope screenshots

Probably the clearest way to explain what this circuit does, is to actually show what the pulses look like across its elements. So here goes, with a sequence of oscilloscope screenshots:

Output of full wave bridge rectifier

Rectifier output with 2.2uF capacitor
Output of KA78R33 voltage regulator
Output of TPC817C opto-isolator
Input of 74HC14 Schmitt trigger
Output of 74HC14 Schmitt trigger

Figuring out the protocol

Reverse engineering

Once I had a clean digital-friendly output, it was time to document the actual protocol. I began by capturing traces like the ones shown below, for a wide range of song selections, while taking notes. Keep in mind that this is specific to the model 100 unit I was working with, and that its entirely possible that other units generate different looking pulse streams.

Pulse stream (Song A-6)
Pulse stream (Song B-6)

I made the following observations:
  • Pulses appear to be in two groups
  • Each pulse is ~50ms wide
  • If the first group has 10 pulses or less, the groups are separated by a long (~814ms) pulse
  • If there are more than than 10 pulses in the first group, then the groups are separated by a medium (~174ms) gap
  • The full pulse sequence is ~2.1 seconds in duration
  • The first group has 1-10, 12-21 pulses, and appears to be the least-significant figure
  • The second group has 1-5 pulses, and appears to be the most significant figure
If I chart this out to see how it maps to the song selection buttons, I end up with a sequence like this:
    A1 ( 1, 1), A2 ( 2, 1), ..., A10 (10, 1)
    B1 (12, 1), B2 (13, 1), ..., B10 (21, 1)
    C1 ( 1, 2), C2 ( 2, 2), ..., C10 (10, 2)
    D1 (12, 2), D2 (13, 2), ..., D10 (21, 2)
    (Note: The letter 'I' is skipped.)

From this information, a pulse decoding function can be written!

Reading the manual

I later discovered that the service manual actually did contain an excerpt explaining how these pulses work. In case anyone is curious, I've reflowed and pasted it below:

From this I gather that they were only really paying attention to the rising edge of the pulses. I'm glad I analyzed the full details, however, as it makes it easier for a robust decoder that can reject invalid/stalled/flaky selection sequences. It also makes it possible to implement short-but-effective timeouts depending on where in the pulse sequence we are.

Test decoding

Many years ago at an unrelated tech conference, I managed to acquire an Arduino Uno. It basically sat in its box until a few months ago, when I realized it could be useful as a "bench tinkering" microcontroller.
Arduino Uno
Despite never having used an Arduino before, this little device turned out to be the perfect way of testing my pulse decoding logic. I plugged it into the output of the signal processing circuit (built on a breadboard) from above, and whipped up a quick-and-dirty sketch that can successfully decode the pulses.

   Seeburg 3WA Wall-O-Matic 100
   Test sketch

const unsigned long USEC_PER_SEC = 1000000;
const int pin = 7;

void setup() {
  pinMode(pin, INPUT);
  Serial.println("Wall-O-Matic Pulse Tester");

void loop() {
  unsigned long lastTimeMs = millis();
  unsigned long durationUs;
  durationUs = pulseIn(pin, HIGH, 5 * USEC_PER_SEC);
  unsigned long pulseTimeMs = millis();
  if (durationUs == 0) {

  int p1 = 0;
  int p2 = 0;
  bool delimiter = false;
  Serial.println("Start of pulses...");

  do {
    unsigned long elapsed = (pulseTimeMs - lastTimeMs) - (durationUs / 1000);
    lastTimeMs = pulseTimeMs;
    Serial.print("Pulse: ");
    if (durationUs < 1000) {
      Serial.print(durationUs, DEC);
    } else {
      Serial.print(durationUs / 1000, DEC);
    Serial.print(", elapsed: ");
    Serial.print(elapsed, DEC);

    if (p1 > 0 && !delimiter && (durationUs / 1000) > 500) {
      delimiter = true;
      Serial.println("----DELIMITER (PULSE)----");
    else {
      if (p1 > 0 && !delimiter && elapsed > 100) {
        delimiter = true;
        Serial.println("----DELIMITER (GAP)----");
      if (!delimiter) {
      else {
    durationUs = pulseIn(pin, HIGH, (delimiter ? 1 : 3) * USEC_PER_SEC);
    pulseTimeMs = millis();
  } while (durationUs > 0);

  Serial.print("-> Signal: ");
  Serial.print(p1, DEC);
  Serial.print(", ");
  Serial.print(p2, DEC);

  if (p2 < 1 || p2 > 5) {
    Serial.println("Pulse 2 invalid value");

  char letter;
  int number;
  if (p1 >= 1 && p1 <= 10) {
    number = p1;
    letter = 'A' + (p2 - 1) * 2;
  else if (p1 >= 12 && p1 <= 21) {
    number = p1 - 11;
    letter = 'A' + ((p2 - 1) * 2) + 1;
  else {
    Serial.println("Pulse 1 invalid value");

  // Skipping 'I' for some reason
  if (letter > 'H') { letter++; }

  Serial.print("-> Song: ");
  Serial.print(number, DEC);

Concluding thoughts

Even though this post flows from start to finish, there was actually a lot of back-and-forth as I figured everything out. Part-way through the process, I upgraded from an ancient low-end analog oscilloscope to a modern digital storage oscilloscope. This tooling upgrade made a huge difference in my ability to experiment and refine this design. It enabled me to actually see all the signal transitions and glitches, and to determine all the necessary components to get to a clean pulse train. Early on, the Arduino code was actually capturing (and attempting to overcome) a lot of signal noise. The final version, however, can pretty much ignore it as a factor.

While this was a lengthy post, there's definitely more to come.

No comments: