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
.
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 
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!