Communicating with an odd Serial device and a Teensy

Rezo

Well-known member
I have a set of old Pioneer CDJ 1000 MK1s that I am working on converting to run off a Teensy 4.x
I want to use the CDJ's original buttons and LEDs and control them though the main display board (I am not going to use the VFD displays)

Looking at the service manuals, I was able to determine that the display board (using a uPD780204 microcontroller), which controls the entire front panel, is connected to the main assy via SPI or SPI like form.

There are 5 signals (pins 1-5 on the MFLB left connector below):
  • Serial Clock - which is generated by the main assy and fed to the display assy
  • Serial Data Out - from the main assy to the display assy
  • Serial Data In - from the display assy to the main assy
  • Reset - from the main assy to the display assy
  • Busy (Key1) - from the display assy to the main assy
There is no CS/SS line here, as this is the only device on the bus

1732829016767.png


I've hooked up a logic analyzer to try figure out how they are speaking to each other, but having a hard time figuring the message sent to the display assy
1732828470740.png

While most lines here are active-low, for some reason, the Serial Data In is active-high - is this common for SPI?
I also noticed that each "frame" contains up to 12 bytes, sent in 2.3ms windows apart


I would like to eventually use a T3.2 or T4.0 to control the display assy and read button inputs, but am unsure how to go at this.
First, I would like to read the Serial Data In line to catch button clicks etc, then be able to send payloads back to the display assy to control some of the LEDs


As this does not look like a standard SPI implementation, wondering if someone can guide me on how to get started here? Perhaps FlexIO shifters and timers might be a direction here for this "custom" serial protocol?

Also attached the LA log file
 

Attachments

  • cdj1000mk1_2.sal.zip
    184 KB · Views: 9
@KurtE @mjs513 @Paul would any of you have some suggestions on how to go at this here?
Can SPI transmit data and receive data at there same, or would I need to setup two DMA channels for this?
 
Yep, I read the SPI page on the PJRC site and found that mentioned there eventually
 
As @joepasquariello mentioned - SPI works full duplex...

And there are lots of examples of code that uses DMA. For example, most of our display drivers have DMA output. Most of them don't do
much if any DMA input.

The SPI library has a DMA transfer method:
Code:
bool SPIClass::transfer(const void *buf, void *retbuf, size_t count, EventResponderRef event_responder) {

There is code setup to setup the DMA and do a single transfer... Actually the single transfer, might be more complicated in that
if your transfer count is > how many bytes that can be transferred in single setup, it will detect that, and restart the transfer to
get the next chunk...
 
I think for now I will just send some dummy loads and read the input to see what bits change when I click buttons!

Will come back (for sure) once I get that part done
 
Got the T3.2 hooked up, streamed 12 bytes at a time and printed out the values - could see byte values changing when clicking some of the buttons. Others? Nothing

C++:
#include <SPI.h>

// Define pins
#define RESET_PIN 15
#define BUSY_PIN 14

// SPI settings
const uint32_t SPI_CLOCK = 1020000; // 1 MHz
const SPISettings spiSettings(SPI_CLOCK, MSBFIRST, SPI_MODE1);

// Timing
const uint32_t SEND_INTERVAL = 3000; // 2.3ms in microseconds
uint32_t lastSendTime = 0;

// Data buffers
uint8_t txBuffer[12] = {0xFF, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
uint8_t rxBuffer[12];

void setup() {
  // Initialize SPI
  SPI.begin();

  // Set pin modes
  pinMode(RESET_PIN, OUTPUT);
  pinMode(BUSY_PIN, INPUT);

  // Reset the slave
  digitalWrite(RESET_PIN, LOW);
  delay(10); // Hold reset low for 10ms
  digitalWrite(RESET_PIN, HIGH);

  // Start serial for debugging
  Serial.begin(115200);
}

void loop() {
  // Check if it's time to send data
  uint32_t currentTime = micros();
  if (currentTime - lastSendTime >= SEND_INTERVAL) {
    // Wait for the busy line to go low
    while (digitalRead(BUSY_PIN) == LOW) {
      // Optional: timeout or error handling can be added here
    }

    // Begin SPI transaction
    SPI.beginTransaction(spiSettings);

    // Send and receive 12 bytes
    for (int i = 0; i < 12; i++) {
      rxBuffer[i] = SPI.transfer(txBuffer[i]);
    }

    // End SPI transaction
    SPI.endTransaction();

    // Debug: Print received data
    Serial.print("Received: ");
    for (int i = 0; i < 12; i++) {
      Serial.print(rxBuffer[i], HEX);
      Serial.print(" ");
    }
    Serial.println();

    // Update last send time
    lastSendTime = currentTime;
  }
}


I believe I need to figure out hot the host is indicating the start of a sequence.

Now this is where I need help - can be on the T3.2 or a T4.x or MM - I need to sniff the MISO/MOSI lines via the Teensy and print them out - but how to I get it to detect the 2.3ms hold between frames in order to print the data out in the right order/grouping?

Can anyone help me with that? I know I need to set use the Teensy SPI in Slave mode., but need some help with the logic inside
 
As this does not look like a standard SPI implementation, wondering if someone can guide me on how to get started here?

First, use Teensy 4.x. It's about 11 times faster than Teensy 3.2 (which is discontinued) and it has 480 Mbit USB, which gives you far more ability to capture fast signals and transmit lots of info to your PC. So put that old Teensy 3.2 away for a less demanding project and use Teensy 4.x for this.

Can you zoom in and show a screenshot of the clock, so we can get an idea of how fast this signal is? I know you shared the raw logic analyzer data... and maybe someone with that software will dive in, but for the sake of conversation a screenshot of how fast the clock changes would give all of us a better idea of the challenge you're facing.

1733154925254.png


Also a couple more questions... I know you showed that schematic, but maybe you could explain in simple terms (or photos) how this equipment is built? I got the impression it's in 2 main parts, the user interface and a main processor board. Is that right? If so, have you figured out which side is transmitting the clock and which receive the clock? I'm guessing your initial goal is just to snoop the communication and learn the data format? But once you learn such things, are you aiming to replace the UI part or the main processor part (or other stuff, if I've misunderstood things... that schematic seems to show up to 9 parts)
 
@Paul I used the T3.2 because I had one laying around and it's 5v tolerant
I'll dig up my logic level shifters and use the T4

So your assumption is right about the modules - there is a main board that holds a processor, FPGA and some DSPs, and there is a control unit that has the uPD780240 that captures signals from push buttons and based on commands from the main unit, will light up some leds and the VFD display

My goal is to remove the main board and replace it with my SDRAM teensy (Devboard v5) with an LCD display, and then use the control panel and it's LEDs instead of having to rout that all to a multiplexer and write other complex logic

I do want to snoof to see if there is some pattern or sequence to the frames with the Teensy, as it's harder to do with the logic analyzer software.

The clock signal is generated by the main unit rated at 1Mhz

Here is a zoomed in image start of frame
1733159189600.png


Here is a zoomed in image end of frame (different frame)
1733159237572.png
 
Last edited:
Can you zoom in and show a screenshot of the clock, so we can get an idea of how fast this signal is? I know you shared the raw logic analyzer data... and maybe someone with that software will dive in, but for the sake of conversation a screenshot of how fast the clock changes would give all of us a better idea of the challenge you're facing.
For those who may want to look at the posted data, you can download the software without having to own one of their logic analyzers

 
Looks like the fastest clock speed is pretty slow 1 MHz. My initial impression is the data changes during falling edges and is valid/stable on the rising edge. So perhaps you do useful capture with attachInterrupt() on rising edge?

Here's a rough idea, with a trick to speed up response a bit by bypassing the normal attachInterrupt handler. This only works if you have a single pin you're using with attachInterrupt, and if you choose a different pin you'll need to loop up the GPIO register and bitmask.

Code:
void setup() {
        pinMode(6, INPUT); // data pin
        pinMode(5, INPUT_PULLUP); // clock pin
        while (digitalReadFast(5) == LOW) ; // wait for normally high
        attachInterrupt(5, mycapture, CHANGE);
        attachInterruptVector(IRQ_GPIO6789, capture); // respond faster
        NVIC_SET_PRIORITY(IRQ_GPIO6789, 48); // higher interrupt priority
}

void capture() {
        bool clock_pin = digitalReadFast(5);
        bool data_pin = digitalReadFast(6);
        static uint32_t prior_cycle_count;
        uint32_t cycle_count = ARM_DWT_CYCCNT;
        GPIO9_ISR = 1<<8; // clear interrupt status, pin 5 = EMC_08 = GPIO9.8

        // TODO: actually do stuff with the clock, data and cycle count...

        prior_cycle_count = cycle_count;
}

Looks like this protocol has 1 MHz clock for data, and also uses a long single clock pulse for some sort of reset or begin/end of data or other unknown circumstance. Inside this capture() function you would first check the clock read to tell if you've just captured a rising or falling edge. In the rising edge case, you'd subtract the prior_cycle_count from cycle_count and it the number if much larger than 300, then you've just detected the long pulse. Otherwise it's probably a data bit. You don't have a lot of time to waste, but with Teensy 4.x running at 600 MHz you can certainly do simple stuff like count the number of bits, shift them into a byte, and put the byte into a buffer and update volatile head & tail indexes. Then your main loop can monitor the buffer (look at HardwareSerial or similar libraries for examples) and use Serial.print() to tell you what happened. Teensy 4.x has plenty of memory, so you can use a buffer of 16 or 32 bit integers rather than just bytes, where you the other bits can give you info like detected timing.

What you'll do with all this, I'm not sure. Reverse engineering is usually a long road of incremental discoveries. But hopefully this quick recipe for capturing the clock and data and cycle count for each clock edge helps you get started on that road.
 
Thanks for the code example and guidance @Paul!

I chatGPT'd this a bit to help me put together a basic example of printing the data - would this be the right direction?


C++:
volatile uint8_t frame_buffer[12]; // Holds the current frame being built
volatile uint8_t frame_index = 0;  // Current byte index in the frame
volatile uint8_t bit_index = 0;    // Current bit index in the current byte
volatile bool frame_ready = false; // Flag to indicate a complete frame
volatile uint32_t prior_cycle_count = 0;

void capture() {
    static uint8_t current_byte = 0; // Current byte being built
    uint32_t cycle_count = ARM_DWT_CYCCNT; // Capture precise timing
    GPIO9_ISR = 1 << 8; // Clear interrupt status for GPIO9.8 (pin 5)

    // Determine clock state (rising or falling edge)
    bool clock_pin = digitalReadFast(5);

    if (clock_pin) { // Rising edge detected
        uint32_t delta_cycles = cycle_count - prior_cycle_count;
        prior_cycle_count = cycle_count;

        if (delta_cycles > 300) { // Long pulse detected (frame boundary)
            frame_index = 0;      // Reset frame
            bit_index = 0;
            frame_ready = false;  // Invalidate the previous frame
        } else { // Normal clock pulse - process data bit
            bool data_bit = digitalReadFast(6);
            current_byte = (current_byte << 1) | data_bit; // Shift in the data bit
            bit_index++;

            if (bit_index == 8) { // Byte complete
                frame_buffer[frame_index++] = current_byte;
                current_byte = 0;
                bit_index = 0;

                if (frame_index == 12) { // Frame complete
                    frame_ready = true;
                    frame_index = 0; // Reset for the next frame
                }
            }
        }
    }
}

void setup() {
        pinMode(6, INPUT); // data pin
        pinMode(5, INPUT_PULLUP); // clock pin
        while (digitalReadFast(5) == LOW) ; // wait for normally high
        attachInterrupt(5, mycapture, CHANGE);
        attachInterruptVector(IRQ_GPIO6789, capture); // respond faster
        NVIC_SET_PRIORITY(IRQ_GPIO6789, 48); // higher interrupt priority
        Serial.begin(115200);
}


void loop() {
    if (frame_ready) {
        Serial.print("Frame: ");
        for (uint8_t i = 0; i < 12; i++) {
            Serial.printf("%02X ", frame_buffer[i]); // Print each byte in hexadecimal
        }
        Serial.println();
        frame_ready = false; // Reset the flag
    }
}

I can throw the data into a larger buffer and then print that in bigger intervals to not overload serial - I guess that would be a better idea.
 
My main advice is to start as small and simple as possible, like even just increment a variable each time the interrupt triggers and occasionally print it from the loop() function. Get the smallest thing working first. What the Arduino Serial Monitor while also capturing on the logic analyzer. The idea is to get small and simple but reliable success. Then slowly build on that success little by little, watching the analyzer as you go. Even if you have a lot of experience, this is the path for reverse engineering because you will learn things about the protocol as you go.
 
Back
Top