SMPTE Sony Protocol Timecode Reader

This is not a finished project but will be part of another project that I cannot post but I figure someone may find it useful if you use deal with equipment using this type of signal.

This is an AVR/Arduino equivalent of a PIC based project I made over a decade ago that I felt needed dragging into the (slightly more) modern age. It was originally written in assembler due to the slow clock speed used on the PIC (4.43361875MHz) but has converted to C++ a lot easier than I expected.

I have tested it with PAL signals but it should also work with NTSC if the timings are altered.

// SMPTE Timecode Reader
// Uses INT0 (Pin 2)
/*
  PAL = 25FPS = 40ms per frame
  40ms / 80bits = 500us per bit
  250us per half bit
  125us per quarter bit
  0 bit max = 625us
  1 bit max = 375us (also 0 bit min)
  1 bit min = 125us

  NTSC = 30FPS (29.97) = 33.333ms per frame
  40ms / 80bits = 417us per bit
  208us per half bit
  104us per quarter bit
  0 bit max = 521us
  1 bit max = 312us (also 0 bit min)
  1 bit min = 104us
*/

#define uMax0 625 // Any time greater than this is to big
#define uMax1 375 // Midpoint timing between a 1 and 0 bit length
#define uMin1 125 // Any time less than this is to short

const word sync = 0xBFFC; // Sync word to expect when running tape forward

enum flagBits {
  tcValid,        // TC copied to xtc is valid (Used by main loop to determing if a timecode has arrived)
  tcFrameError,   // ISR edge out of timing bounds (only gets reset after next valid TC read)
  tcOverrun,      // TC was not read from xtc by main loop before next value was ready (so main loop can tell if timecodes have been lost)
  tcForceUpdate,  // Valid TC will always be copied to buffer even if last value not read (if set by main code then TC will always be copied)
  tcHalfOne       // ISR is reading a 1 bit so ignore next edge (Internal to ISR)
};

uint8_t tc[10] = {0};                                     // ISR Buffer to store incoming bits
volatile uint8_t xtc[8] = {0};                            // Buffer to store valid TC data - sync bytes
volatile uint8_t tcFlags = 0;                             // Various flags used by ISR and main code
uint32_t uSeconds;                                        // ISR store of last edge change time

char timeCode[12];                                        // For example code another buffer to write decoded timecode
char userBits[12];                                        // For example code another buffer to write decoded user bits


void setup(){
  Serial.begin(115200);
  pinMode(2,INPUT);
  attachInterrupt(0, int0ISR, CHANGE);
  Serial.println(F("Waiting For TC"));
}

void loop(){
  while(bitRead(tcFlags, tcValid) == 0){};                // Wait for valid timecode
  
  timeCode[0] = (xtc[0] & 0x03) + '0';                    // 10's of hours
  timeCode[1] = (xtc[1] & 0x0F) + '0';                    // hours
  timeCode[2] = ':';                
  timeCode[3] = (xtc[2] & 0x07) + '0';                    // 10's of minutes
  timeCode[4] = (xtc[3] & 0x0F) + '0';                    // minutes
  timeCode[5] =  ':';               
  timeCode[6] = (xtc[4] & 0x07) + '0';                    // 10's of seconds
  timeCode[7] = (xtc[5] & 0x0F) + '0';                    // seconds
  timeCode[8] =  '.';               
  timeCode[9] = (xtc[6] & 0x03) + '0';                    // 10's of frames
  timeCode[10] = (xtc[7] & 0x0F) + '0';                   // frames
  
  userBits[0] = ((xtc[7] & 0xF0) >> 4) + '0';             // user bits 1 
  userBits[1] = ((xtc[6] & 0xF0) >> 4) + '0';             // user bits 2  
  userBits[2] = '-';            
  userBits[3] = ((xtc[5] & 0xF0) >> 4) + '0';             // user bits 3
  userBits[4] = ((xtc[4] & 0xF0) >> 4) + '0';             // user bits 4
  userBits[5] = '-';            
  userBits[6] = ((xtc[3] & 0xF0) >> 4) + '0';             // user bits 5
  userBits[7] = ((xtc[2] & 0xF0) >> 4) + '0';             // user bits 6
  userBits[8] = '-';            
  userBits[9] = ((xtc[1] & 0xF0) >> 4) + '0';             // user bits 7
  userBits[10] = ((xtc[0] & 0xF0) >> 4) + '0';            // user bits 8
  
  bitClear(tcFlags, tcValid);                             // Finished with TC so signal to ISR it can overwrite it with next TC
  
  Serial.print(timeCode);
  Serial.print("\t");
  Serial.println(userBits);
}

void int0ISR(){
  uint32_t edgeTimeDiff = micros() - uSeconds;            // Get time difference between this and last edge
  uSeconds = micros();                                    // Store time of this edge
  
  if ((edgeTimeDiff < uMin1) or (edgeTimeDiff > uMax0)) { // Drop out now if edge time not withing bounds
    bitSet(tcFlags, tcFrameError);
    return;
  }
  
  if (edgeTimeDiff > uMax1)                               // A zero bit arrived
  {
    if (bitRead(tcFlags, tcHalfOne) == 1){                // But we are expecting a 1 edge
      bitClear(tcFlags, tcHalfOne);
      clearBuffer(tc, sizeof(tc));
      return;
    }
    // 0 bit
    shiftRight(tc, sizeof(tc));                           // Rotate buffer right
    // Shift replaces top bit with zero so nothing else to do
    //bitClear(tc[0], 7);                                   // Reset the 1 bit in the buffer
  }
  else                                                    // Not zero so must be a 1 bit
  { // 1 bit
    if (bitRead(tcFlags, tcHalfOne) == 0){                // First edge of a 1 bit
      bitSet(tcFlags, tcHalfOne);                         // Flag we have the first half
      return;
    }
    // Second edge of a 1 bit
    bitClear(tcFlags, tcHalfOne);                         // Clear half 1 flag
    shiftRight(tc, sizeof(tc));                           // Rotate buffer right
    bitSet(tc[0], 7);                                     // Set the 1 bit in the buffer
  }
  // Congratulations, we have managed to read a valid 0 or 1 bit into buffer
  if (word(tc[0], tc[1]) == sync){                        // Last 2 bytes read = sync?
    bitClear(tcFlags, tcFrameError);                      // Clear framing error
    bitClear(tcFlags, tcOverrun);                         // Clear overrun error
    if (bitRead(tcFlags, tcForceUpdate) == 1){
      bitClear(tcFlags, tcValid);                         // Signal last TC read
    }
    if (bitRead(tcFlags, tcValid) == 1){                  // Last TC not read
      bitSet(tcFlags, tcOverrun);                         // Flag overrun error
      return;                                             // Do nothing else
    }
    for (uint8_t x = 0; x < sizeof(xtc); x++){            // Copy buffer without sync word
      xtc[x] = tc[x + 2];
    }
    bitSet(tcFlags, tcValid);                             // Signal valid TC
  }
}

void clearBuffer(uint8_t theArray[], uint8_t theArraySize){
  for (uint8_t x = 0; x < theArraySize - 1; x++){
    theArray[x] = 0;
  }
}

void shiftRight(uint8_t theArray[], uint8_t theArraySize){
  uint8_t x;
  for (x = theArraySize; x > 0; x--){
    uint8_t xBit = bitRead(theArray[x - 1], 0);
    theArray[x] = theArray[x] >> 1;
    theArray[x] = theArray[x] | (xBit << 7);
  }
  theArray[x] = theArray[x] >> 1;
}

I just found this helpful site that you can download timecode audio from. I might be tempted to check the NTSC timings at some point.

Dear Riva!

I try this program, but the serial monitor only write waiting for TC. When i connect the TC to pin 2 repeatedly write "waiting for TC", but no valid timecode... i try the input from soundcard (tested the output on oscilloscope, 5V level is ok) and from the camer TC out with this schematic:

http://s1158.photobucket.com/user/jackytwoshoes/media/arduino-simple-timecode-input_zpsc4b41f18.jpg.html

Please help what's the problem. Sorry for the bad english.

Did you also use the circuit with the sound card output or bias it’s output to remove negative voltages?

What camera are you using and is it’s TC output definitely SMPTE as shown in the first post here. I only tested using a PAL sample from the website I linked and it worked fine so if your using NTSC the code timings will need altering and I never tested NTSC so maybe it does not work.
Unfortunately I’m at work today and won’t have time to check the code on my days off as decorators in means no computer for the next 2 days :frowning:
I can maybe check on Friday if your still having problems.
I had a bit of spare time so knocked up a circuit to test the code. I can confirm the sketch works with LTC samples downloaded from here and I also tested with both PAL & NTSC samples.
I have slightly modified the sketch (attached to this post) to make switching between PAL/NTSC easier. Currently it’s setup for PAL but just delete the #define PAL line to get NTSC timings.

SMPTE_Timecode_Reader.ino (7.97 KB)

what was the circuit you used to get input from an external device. Ive been trying different things with no luck yet

Kunkles:
what was the circuit you used to get input from an external device. Ive been trying different things with no luck yet

I used the attached circuit.
SMPTE_01.jpg

1 Like