Asynchronous serial + clock signal

Yes, data rate is 31.25 kHz

TxC and RxC pins are fed with a 31.25 kHz clock. TxC is fed with the locally generated clock; RxC is fed with the clock coming from the other party. So I suppose that UART is configured for a 1x clock.

I agree with you: this will be the first test I'll do when I'll receive the connector, a male Amphenol DDK 57-30140 14 pin.

That's the point! If you are lucky, your code sketch may work most of the times. But we cannot say that it will certainly work all the times. For this reason I supposed to generate the outbound clock via a timer-based interrupt and, in the same ISR, generate the serial/data signal at the same time

If I understood well, this was exactly was I supposed to do on Arduino side:

  • NOT using the Serial library
  • handling the reception via a rising edge interrupt on CLK coming from the other device: here the logic samples the inbound DATA signal
  • handling the transmission via a timer-based interrupt to generate outbound CLK and DATA
  • serial protocol compliance, for inbound and outbound flows, has to be handled in software.

In theory, it should work...

You don't even need a timing interrupt for the transmission since each bit has a clock edge the receiver uses for sampling.

boolean oddParity(byte x)
{
  x ^= x >> 4;
  x ^= x >> 2;
  x ^= x >> 1;
  return x & 1;
}

void sendSerialByte(byte TXPin, byte TXCPin, byte data)
{
  uint16_t binary = 0b11;  // Two Stop Bits
  binary = (binary << 1) | oddParity(data); // Insert parity bit
  binary = (binary << 8) | data;  // Insert eight data bits;
  binary <<= 1; // Insert start bit (0)

  // Send LSB First
  for (size_t i = 0; i < 12; i++)
  {
    digitalWrite(TXPin, (binary & (1 << i)) ? HIGH : LOW);
    delayMicroseconds(20);
    digitalWrite(TXCPin, HIGH);  // Rising edge to trigger receiver
    delayMicroseconds(20);
    digitalWrite(TXCPin, LOW);
  }
}

This is what I call a hack! :slight_smile:

Yes, it should work. I will try it when I get the connector; I will keep you updated, this discussion could also be useful for others in the future.

For now, thank you all for your contributions!

For future reference, I put hereunder the links to additional documentation that I found useful:

Roland Digital Communication Bus
https://en.wikipedia.org/wiki/Digital_Control_Bus
https://www.chd-el.cz/support/application/app003-dcb/

Roland Juno-60 Service Notes
https://www.synthxl.com/wp-content/uploads/2018/01/Roland-Juno-60-Service-Manual.pdf

Intel 8251A
https://www.datasheetq.com/datasheet-download/106838/1/Intel/8251A
https://devtasks.net/?p=63235

Hi dudes! I received the Amphenol 57-40140 and 57-30140 connectors:

After having built the bidirectional DCB cable, my first attempt was to receive data simply using the Serial library:

  • Serial1 is set to 31250, SERIAL_8O2
  • Digital pin 2 is set to OUTPUT and used as Ready To Send control signal
  • inbound clock signal is ignored
  • Schmitt-trigger inverters are used for Data and RTS signals

Well, it seemed to work.... But I soon discovered that sometimes some data bytes are lost during transmission!

Let me explain how the transmission is supposed to work.
The old electronic device sends packets of data continuously:

  • each packet consists of 7 bytes: one prefix byte + 6 data bytes
  • the prefix byte is always 0xFE
  • MSBit of each data byte specifies the NOTE ON/OFF status
  • 7 lower bits of each data byte specifies the NOTE NUMBER

This is a dump excerpt of received packets (one row per packet) when no one is playing the synthesizer (i.e. when all notes are off):

FE  29  26  24  28  2B
FE  29  26  24  28  2B
FE  29  26  24  28  2B 
FE  29  26  24  28  2B
FE  29  26  24  28  2B
FE  29  26  2D  24  28  2B
FE  29  26  28  2B
FE  29  26  2D  24  28  2B 
FE  29  26  24  28  2B
FE  29  26  24  28  2B 
FE  29  26  24  28  2B
FE  29  26  24  28  2B
FE  29  26  24  28  2B 
FE  29  26  2D  24  28  2B
FE  29  2D  24  28  2B 
FE  29  2D  24  28  2B
FE  29  26  28  2B
FE  29  26  2D  24  28  2B
FE  29  26  24  28  2B
FE  29  26  24  28  2B
FE  29  26  24  28  2B 
FE  29  26  24  28  2B
FE  29  26  24  28  2B
FE  29  26  24  28  2B
FE  29  26  2D  28  2B
FE  29  26  2D  28  2B 
FE  29  26  2D  24  28  2B
FE  29  2D  24  28  2B
FE  29  26  24  28  2B
FE  29  26  24  28  2B
FE  29  26  24  28  2B
FE  29  26  24  28  2B
FE  29  26  24  28  2B 
FE  29  26  2D  24  28  2B
FE  29  2D  24  28  2B
FE  29  2D  24  28  2B

Each row should consist of 7 bytes in total (1 byte prefix + 6 data bytes), but usually 6 bytes are received; sometimes 5 bytes are received, sometimes 7.
Received values are always correct: the only problem is that usually almost 1 byte per packet gets lost.

I made the following three hypotheses:

  1. The transmission is affected by noise, so that the receiving UART discards bytes that do not pass the parity check
  2. There is a synchronization issue: I'm not using the inbound clock signal and I should instead!
  3. The sending 8251 could not like the RTS signal always on..

I lean towards the second hypothesis, because the missing byte is never the first or the second byte of the packet; and is never the second-last or last byte of the packet. If it was a noise problem, the error should be present randomicaly for each byte of the transmission. Anyway I will check the signal for noise.

Probably my new approach will be the one proposed by @johnwasser: considering the inbound clock by using a rising edge interrupt for receiving data.

You must use the inbound clock because there may be times when NO byte is being sent and the clock signal will tell you that. Remember you said the protocol was asynchronous with clock.

The clock signal is generated continuously, regardless of whether data is being sent or not

If you are not using their clock signal, how can you synchronize their data to your clock signal? Are you able to adjust your clock to match their data start bit?

I made a sort of SoftwareSerialWithClock and.. it worked!
This is the new circuit:


And this is the source code:

// DCB.h
//

#pragma once

#include <Arduino.h>
#include "cppQueue.h"   // https://github.com/SMFSW/Queue


class DCB {

    enum SerialStatus {
        ssPause = -1,
        ssNoError = 0,
        ssParityError = 1,
        ssFramingError = 2,
        ssBufferFull = 3
    };

    static volatile unsigned long _clock_counter;
    static volatile unsigned long _pause_counter;
    static volatile unsigned long _no_error_counter;
    static volatile unsigned long _parity_error_counter;
    static volatile unsigned long _framing_error_counter;
    static volatile unsigned long _buffer_full_error_counter;
    static volatile unsigned long _bytes_received;

    bool _initialized;

    static volatile uint8_t _rx_clock_pin;
    static volatile uint8_t _rx_data_pin;
    static volatile uint8_t _rx_busy_pin;
    static cppQueue* _q;
    static volatile SerialStatus _status;

    void init();

    static bool READ_DATA();
    static void SET_BUSY();
    static void CLEAR_BUSY();

    static bool oddParity(uint8_t data);
    static void ISR_IN_CLOCK();
    static SerialStatus ISR_READ_DATA();
    static SerialStatus ISR_DATA_AVAILABLE(SerialStatus status, uint8_t data);

public:
    DCB(uint8_t rx_clock_pin, uint8_t rx_data_pin, uint8_t rx_busy_pin);

    void startRx();
    void stopRx();

    void printRxStats(Stream& stream);

    bool isDataAvailable();
    bool read(uint8_t* data);
};
// DCB.cpp
//

#include "DCB.h"


volatile unsigned long DCB::_clock_counter = 0;
volatile unsigned long DCB::_pause_counter = 0;
volatile unsigned long DCB::_no_error_counter = 0;
volatile unsigned long DCB::_parity_error_counter = 0;
volatile unsigned long DCB::_framing_error_counter = 0;
volatile unsigned long DCB::_buffer_full_error_counter = 0;
volatile unsigned long DCB::_bytes_received = 0;
volatile uint8_t DCB::_rx_clock_pin = 0;
volatile uint8_t DCB::_rx_data_pin = 0;
volatile uint8_t DCB::_rx_busy_pin = 0;
cppQueue* DCB::_q = nullptr;
volatile DCB::SerialStatus DCB::_status = DCB::ssNoError;

void DCB::init() {
    if(!_initialized) {
        // setup DCB pins
        pinMode(_rx_busy_pin, OUTPUT);
        SET_BUSY();
        pinMode(_rx_data_pin, INPUT);
        attachInterrupt(digitalPinToInterrupt(_rx_clock_pin), ISR_IN_CLOCK, FALLING);

        // initialize counters
        _clock_counter = 0;
        _pause_counter = 0;
        _no_error_counter = 0;
        _parity_error_counter = 0;
        _framing_error_counter = 0;
        _buffer_full_error_counter = 0;
        _bytes_received = 0;

        // initialize serial status
        _status = ssNoError;

        // initialize data buffer
        _q = new cppQueue(sizeof(uint8_t));

        _initialized = true;
    }
}

bool DCB::READ_DATA() {
    return (digitalRead(_rx_data_pin) == HIGH);
}

void DCB::SET_BUSY() {
    digitalWrite(_rx_busy_pin, HIGH);
}

void DCB::CLEAR_BUSY() {
    digitalWrite(_rx_busy_pin, LOW);
}

DCB::DCB(uint8_t rx_clock_pin, uint8_t rx_data_pin, uint8_t rx_busy_pin) {
    _initialized = false;
    _rx_clock_pin = rx_clock_pin;
    _rx_data_pin = rx_data_pin;
    _rx_busy_pin = rx_busy_pin;
}

void DCB::startRx() {
    init();
    CLEAR_BUSY();
}

void DCB::stopRx() {
    SET_BUSY();
}

void DCB::printRxStats(Stream& stream) {
    stream.print("CLK: ");
    stream.print(_clock_counter);
    stream.print(" PAU: ");
    stream.print(_pause_counter);
    stream.print(" NOE: ");
    stream.print(_no_error_counter);
    stream.print(" PAE: ");
    stream.print(_parity_error_counter);
    stream.print(" FRE: ");
    stream.print(_framing_error_counter);
    stream.print(" BUF: ");
    stream.print(_buffer_full_error_counter);
    stream.print(" RCV: ");
    stream.println(_bytes_received);
}

bool DCB::isDataAvailable() {
    noInterrupts();
    const bool b = !_q->isEmpty(); 
    interrupts();
    return b;
}

bool DCB::read(uint8_t* data) {
    noInterrupts();
    const bool b = _q->pop(data); 
    interrupts();
    return b;
}

bool DCB::oddParity(uint8_t data) {
    // https://forum.arduino.cc/u/johnwasser
    // https://forum.arduino.cc/t/asynchronous-serial-clock-signal/955997/23?u=algia71

    data ^= data >> 4;
    data ^= data >> 2;
    data ^= data >> 1;
    return data & 1;
}

void DCB::ISR_IN_CLOCK() {
    ++_clock_counter;
    _status = ISR_READ_DATA();
    switch(_status) {
    case SerialStatus::ssPause:
        ++_pause_counter;
        break;
    case SerialStatus::ssNoError:
        ++_no_error_counter;
        break;
    case SerialStatus::ssParityError:
        ++_parity_error_counter;
        break;
    case SerialStatus::ssFramingError:
        ++_framing_error_counter;
        break;
    case SerialStatus::ssBufferFull:
        ++_buffer_full_error_counter;
        break;
    }
}

DCB::SerialStatus DCB::ISR_READ_DATA() {
    static bool in_frame = false;
    static uint8_t data_bit_counter = 0;
    static uint8_t data_buffer = 0;
    static bool parity_bit_read = false;
    static bool stop_bit_read = false;

    const bool bit = READ_DATA();

    if(bit && !in_frame) {
        return SerialStatus::ssPause;
    }
    if(!bit && !in_frame) {
        in_frame = true;
        data_bit_counter = 0;
        data_buffer = 0;
        parity_bit_read = false;
        stop_bit_read = false;
        return SerialStatus::ssNoError;
    }
    if(data_bit_counter <= 7) {
        data_buffer >>= 1;
        data_buffer &= 0b01111111;
        data_buffer |= bit ? 0b10000000 : 0;
        ++data_bit_counter;
        return SerialStatus::ssNoError;
    }
    if(!parity_bit_read) {
        parity_bit_read = true;
        if(oddParity(data_buffer) == bit) {
            in_frame = false;
            return SerialStatus::ssParityError;
        }
        else {
            return SerialStatus::ssNoError;
        }
    }
    if(bit) {
        if(!stop_bit_read) {
            SET_BUSY();
            stop_bit_read = true;
            in_frame = false;
            SerialStatus status = ISR_DATA_AVAILABLE(ssNoError, data_buffer);
            CLEAR_BUSY();
            return status;
        }
    }
    in_frame = false;
    return SerialStatus::ssFramingError;
}

DCB::SerialStatus DCB::ISR_DATA_AVAILABLE(SerialStatus status, uint8_t data) {
    ++_bytes_received;
    return _q->push(&data) ? status : SerialStatus::ssBufferFull;
}

Please note that I actually attached the CLOCK IN interrupt by setting the mode parameter to FALLING, instead of RISING, despite what the oscilloscope suggested:

I think that the interrupt is triggered with some delay (I know this is documented elsewhere, but I had no time to investigate): having the clock running 1x in respect of the data signal, this delay may sometimes cause a delayed read of the data signal line, generating a large number of framing errors when mode is set to RISING. When mode is set to FALLING, I have zero frame and parity errors.

PS1: I'm sure that the code can be optimized and better written, but for the moment I'm happy with it :slight_smile: .

PS2: One necessary improvement will be the automatic activation of the BUSY line when the receiving buffer on the Arduino is about to fill up :sunglasses:

This is the test program:

//  main.cpp
//

#include <Arduino.h>
#include "DCB.h"

#define MONITOR_SPEED 115200
#define DCB_RxC_PIN   2       // Rx Clock pin
#define DCB_RxD_PIN   3       // Rx Data pin
#define DCB_RxB_PIN   4       // Rx Busy pin

DCB dcb(DCB_RxC_PIN, DCB_RxD_PIN, DCB_RxB_PIN);

void setup() {
    Serial.begin(MONITOR_SPEED);
    while (!Serial);

    dcb.startRx();
}

cppQueue q(sizeof(uint8_t));

void loop() {
    static unsigned long last_stat = 0;

    uint8_t data;
    while(dcb.read(&data)) {
        if(data == 0xFE) {
            uint8_t data;
            while(q.pop(&data)) {
                Serial.print(data, HEX);
                Serial.print(' ');
            }
            Serial.println("");
        }
        q.push(&data);
    }

    unsigned long now = millis();
    if(now - last_stat >= 250) {
        last_stat = now;
        dcb.printRxStats(Serial);
    }
}

And finally this is an excerpt of the generated data captured via the serial monitor:

FE 39 30 32 37 35 34
FE 39 30 32 37 35 34
FE 39 30 32 37 35 34
FE 39 30 32 37 35 34 
FE 39 30 32 37 35 34
FE 39 30 32 37 35 34
FE 39 30 32 37 35 34
FE 39 30 32 37 35 34 
CLK: 1958944 PAU: 607176 NOE: 1351832 PAE: 0 FRE: 0 BUF: 0 RCV: 122899
FE 39 30 32 37 35 34
FE 39 30 32 37 35 34
FE 39 30 32 37 35 34
FE 39 30 32 37 35 34 

For debugging purposes, every 250 ms I print a debug string that traces the total elapsed clocks (CLK), the pauses (PAU) in the serial streams (number of clocks where the data signal is 1 after the stop bit), the no-error conditions (NOE), the number of parity-error conditions (PAE), the number of framing-errors (FRE), the number of buffer overrun conditions (BUF) and the total number of bytes received.

It is worth noting that the ~30% of the time, the serial transmission is actually paused (all logical 1s).

Thanks to all for your precious contributions.

This was for the receiver logic... now let's begin to work on the transmitter logic!

In my first attempt I tried to treat the signal as a classic asynchronous serial line with baudrate = 31250