Read timer's comparator output

Hi,

I want to get a "time-based" logic signal. That signal is from a RF receiver (and yes, I had tried to use RCSwitch).
It produces pulses of 400ns 5V, 750ns 0V; and 750ns 5V, 400ns 0V. Due to this very short time I can't use the micros() Arduino function.

The main idea it's to attach that signal to an interrupt (both rising and falling edge), resetting the Arduino's timer when a rising edge happends, and checking if the comparator (attached to that timer) is high or low when a falling edge happends.
The comparator would have something like 650ns (to give some tolerance).

My attempt:

#define COMPARE_CLOCK_PIN 9 // OCR0B comparator's output 


void setup() {
  Serial.begin(9600);
  pinMode(COMPARE_CLOCK_PIN, OUTPUT);
  
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;
  OCR1A = 8;   // test; set here 650ns
  TCCR1A |= (1 << COM1A1) | (1 << COM1A0);   // Set OC1A on Compare Match
  //TCCR1B |= (1 << WGM12);    // CTC mode
  TCCR1B |= (1 << CS10);     // no pre-scaler
}


void loop() {
    Serial.println(digitalRead(COMPARE_CLOCK_PIN));
}

What do you think?

rogermiranda1000:
I want to get a "time-based" logic signal.

What do you mean by "get"?!? Do you want to detect that particular pattern? Are there any other patterns that you want to ignore? If so, what do the other patterns look like?

johnwasser:
Do you want to detect that particular pattern?

I want to read the signal. If I get (400ns 5V + 750ns 0V) I want to associate it with a 0, but if I get (750ns 5V + 400ns 0V) I want to associate it with a 1.

Oh. Each pulse is 1150 nS. That is 18.4 instruction cycles. I don't think you are going to be able to do more than one interrupt per pulse.

What I would try: Interrupt on the rising edge. Use direct register access to read the input pin. If you get a 1 the pulse was long and if you get a 0 the pulse was short. Add NOP instructions to the front of the ISR to make sure it takes about 575 nS to get the pin value. Good luck storing the data in the few nanoseconds you have to spare. I don't think this is going to work.

I think you will either need to add external hardware to do the serial to parallel decoding or you need to switch to a faster processor. At 62.5 nS per instruction, the ATmega328p is a little slow to be reading 400 nS pulses.

johnwasser:
Interrupt on the rising edge. Use direct register access to read the input pin. If you get a 1 the pulse was long and if you get a 0 the pulse was short. Add NOP instructions to the front of the ISR to make sure it takes about 575 nS to get the pin value.

I was able to capture something, but I don't know if it's valid data or not.

#define PIN_IR 2
#define INTERRUPT_IR digitalPinToInterrupt(PIN_IR)

#define SIZE 12000/8
#define MS_TIME 15
volatile unsigned int current;
volatile unsigned char pos;
volatile unsigned char recording[SIZE];

void pulse() {
  if (pos == 1) {
    pos = 128;
    current++;
  }
  else pos >>= 1;
  recording[current] |= (uint8_t)((PIND & 4)>0) * pos; // D2 -> PD2 [PIND; 1<<2]
}

boolean set;
unsigned long init_time;
void setup() {
  Serial.begin(115200);
  
  current = 0;
  set = false;
  pos = 1;
  
  pinMode(PIN_IR, INPUT);
  
  attachInterrupt(INTERRUPT_IR, pulse, RISING);
}

void loop() {
  if (current > 0) {
    if (!set) {
      init_time = millis();
      set = true;
    }
    else if (millis()-init_time > MS_TIME) {
      detachInterrupt(INTERRUPT_IR);
      
      Serial.println("-- INIT --");
      for (int x = 0; x < current; x++) {
        for (int x = 1; x < 256; x*=2) Serial.print(recording[x] & (unsigned char)x > 0 ? "1" : "0");
      }
      Serial.println("\n-- END --");
      
      current = 0;
      set = false;
      
      attachInterrupt(INTERRUPT_IR, pulse, RISING);
    }
  }
}

Instead of a NOP I'm doing operations to avoid wasting clock time (I have only 12 cycles since the interrupt starts until another it's called).
Also, since every pack of information it's 10ms long I store it into 12,000 booleans (for some reason booleans uses 1 byte, so I had to pack 8 booleans into one char).

Do you think the interrupt uses less than 12 cycles? There's some really complex code right there.

According to ATmega328p: PWM can also be used to encode digital information. SAE protocol J1850-PWM (used in intra-vehicle networks) defines an encoding where a single bit 0 is represented by a (nominal) 33% duty cycle, and a bit 1 by a 66% duty cycle.

What a coincidence that 400ns/(400ns + 750ns) ~ 33% and 750ns/(400ns + 750ns) ~ 66%... Maybe I can capture it with the PWM module?

You initialize 'current' to 0 and 'pos' to 1 so the first thing your ISR will do is set 'pos' to 128 and 'current' to 1, skipping the first byte of 'recording'. You should initialize 'current' to 0 and 'pos' to 128, then put the increment of 'pos' AFTER you use it.

You fill the bytes in 'recording' from left to right but you display them from right to left. You should display them and fill them in the same order.

After the timeout you should display 'count' and 'pos' so you can see if the received length is what you expect.

Same as before, apparently random digits (I've attached the output file).

#define PIN_IR 2
#define PIN_IR_BIT 4 // 1<<2
#define PIN_IR_PORT PIND
#define INTERRUPT_IR digitalPinToInterrupt(PIN_IR)


#define NOP __asm__ __volatile__ ("nop\n\t");


#define SIZE 12000/8
#define MS_TIME 15


volatile unsigned int current;
volatile byte pos;
volatile byte recording[SIZE];


boolean set;
unsigned long init_time;


void pulse() {
  // wait at least 2 cycles (on 20MHz)
  NOP;
  NOP;
  
  // reached the 400ns
  if ((PIN_IR_PORT & PIN_IR_BIT)>0) recording[current] |= pos; // D2 -> PA0
  
  if (pos == 1) {
    pos = 128;
    current++;
  }
  else pos >>= 1;
}


void preparePulse() {
  for (int x = 0; x <= current; x++) recording[x] = 0;
  current = 0;
  set = false;
  pos = 128;
}


void setup() {
  Serial.begin(9600);
  
  pinMode(PIN_IR, INPUT);
  
  current = SIZE-1; // erase all recording array
  preparePulse();
  
  attachInterrupt(INTERRUPT_IR, pulse, RISING);
}


void loop() {
  if (current > 0) {
    if (!set) {
      init_time = millis();
      set = true;
    }
    else if (millis()-init_time > MS_TIME) {
      detachInterrupt(INTERRUPT_IR);
      
      Serial.println("-- INIT --");
      for (int x = 0; x <= current; x++) {
        Serial.print(recording[x], BIN);
      }
      Serial.print("\n-- END -- ");
      Serial.print(current);
      Serial.print(" bytes, ending at bit ");
      Serial.println(pos);
      
      preparePulse();
      
      attachInterrupt(INTERRUPT_IR, pulse, RISING);
    }
  }
}

out.txt (32.7 KB)

I think you must be missing a lot of samples. If each cycle is 1150 nanoseconds (400+750) then 161 bytes of pulses (1288 bits) take about 1.5 milliseconds. You said each packet lasted about 10 milliseconds so you seem to be missing about 85% of the cycles.

I think you are going to need a faster processor.

I've tried to write the interrupt function into assembly (I've never worked with pointers, nor arrays) and it's really promising.

Using interrupts each time the pulse arrives I have 7 cycles to perform the operation 'if ((PORTA.IN & 1)>0) *current |= pos;', I'll try it and we'll see.

If this don't work I have another way to do it: the first edge will trigger the interrupt and I'll stay there until the whole sequence stops (that way I don't waste 3+5 cycles in each pulse).

#define PIN_IR 2
#define INTERRUPT_IR digitalPinToInterrupt(PIN_IR)


#define NOP __asm__ __volatile__ ("nop\n\t");


#define SIZE 12000/8 // it takes ~10ms to complete, that's ~12kb
#define MS_TIME 15


volatile byte *current;
volatile byte pos;
volatile byte recording[SIZE];


boolean set;
unsigned long init_time;


__attribute__((naked)) void pulse(void) {
  asm volatile (
    // 3 cycles to reach interrupt
    
    "LDS  r16, 0b00000001         \n\t" // r16 = 1
    "CPSE %[pos_reg], r16         \n\t" // pos==1 -> skip [1/3 cycles]
    // pos>1:
    "RJMP GREATER_THAN_ONE        \n\t" // [2 cycles]
    "LDI  %[pos_reg], 0b10000000  \n\t" // pos = 128 [1 cycle]
    "NOP                          \n\t" // TODO: current++ [1 cycle?]
    "RJMP  IF_ELSE_END            \n\t" // [2 cycles]
    "GREATER_THAN_ONE:            \n\t"
    // pos==1:
    "ROR  %[pos_reg]              \n\t" // pos >>= 1 [1 cycle]
    "IF_ELSE_END:                 \n\t"
    // if-else closed [5 cycles if pos>1; 7(+1?) cycles if pos==1] (400ns/550ns)
    
    // 7 remaining cycles (worst case)
    // TODO: if ((PORTA.IN & 1)>0) *current |= pos;
    // Note: PORTA.IN is 0x0408
    "NOP                          \n\t"
    
    // 4/5 cycles to leave interrupt
    : // out
    : [pos_reg] "r" (pos) // in
    : "r16" // used registers
  );
}


void preparePulse() {
  for (int x = 0; recording + x <= current; x++) recording[x] = 0;
  current = recording;
  set = false;
  pos = 1;
}


void setup() {
  Serial.begin(9600);


  pinMode(PIN_IR, INPUT);
  pinMode(LED_BUILTIN, OUTPUT);


  current = &recording[SIZE - 1]; // erase all recording array
  preparePulse();


  attachInterrupt(INTERRUPT_IR, pulse, RISING);
}


void loop() {
  if (current > recording) {
    if (!set) {
      init_time = millis();
      set = true;
    }
    else if (millis() - init_time > MS_TIME) {
      detachInterrupt(INTERRUPT_IR);


      Serial.println("-- INIT --");
      for (int x = 1; recording + x <= current; x++) {
        Serial.print(recording[x], BIN);
      }
      Serial.print("\n-- END -- ");
      Serial.print(recording - current + 1);
      Serial.print(" bytes, ending at bit ");
      Serial.println(pos);


      preparePulse();


      attachInterrupt(INTERRUPT_IR, pulse, RISING);
    }
  }
}

I was able to capture up to 400 consecutive bytes.

#define PIN_IR 2
#define INTERRUPT_IR digitalPinToInterrupt(PIN_IR)


#define SIZE 12000/8 // it takes ~10ms to complete, that's ~12kb
#define MS_TIME 15


volatile byte *current;
volatile byte pos;
volatile byte recording[SIZE];


__attribute__((naked)) void pulse(void) {
  // ~5.22us cycles to reach interrupt
  while ((PORTA.IN & 0b00000001) == 1); // wait until pin is LOW
  
  asm volatile (
    "LDI  r16, 0b00000001         \n\t" // r16 = 0b00000001
    // wait until pin is HIGH
    "WAIT_RISE:                   \n\t"
    "LDI  r18, 246                \n\t" // r18 = 256-10
    "WAIT_RISE_LOOP:              \n\t"
    "ADD  r18, r16                \n\t" // r18++
    "BRBS 0, SAVE                 \n\t" // carry -> GOTO SAVE
    "LDS  r17, 0x0408             \n\t" // r17 = PORTA.IN
    "AND  r17, r16                \n\t" // r17 = r17 & r16 = PORTA.IN & 0b00000001 = PIN
    "BRBS 1,WAIT_RISE_LOOP        \n\t" // r17==0 -> GOTO WAIT_RISE_LOOP
    // PIN HIGH:
    
    // first sample discarded [4 / 9 cycles since the rising]
    // wait >4 cycles (but <6)
    "NOP                          \n\t"
    "NOP                          \n\t"
    "NOP                          \n\t"
    "NOP                          \n\t"
    "NOP                          \n\t"
    // waited 5 cycles
    
    
    "GET_SEQUENCE:                \n\t"
    "LDS  r17, 0x0408             \n\t" // r17 = PORTA.IN
    "AND  r17, r16                \n\t" // r17 = r17 & r16 = PORTA.IN & 0b00000001 = PIN
    "BRBS 1,PREPARE_NEXT          \n\t" // r17==0 -> GOTO PREPARE_NEXT
    // r17==1:
    "LD   r17, %a[current_reg]    \n\t" // r17 = *current
    "OR   r17, %[pos_reg]         \n\t" // r17 |= pos
    "ST   %a[current_reg], r17    \n\t" // *current = r17 = *current | pos
    // if PIN LOW -> 5; HIGH -> 7
    
    
    // PREPARE_NEXT: if pos > 1 -> 6; pos == 1 -> 8
    "PREPARE_NEXT:                \n\t"
    "CPSE %[pos_reg], r16         \n\t"
    "RJMP GREATER_THAN_ONE        \n\t"
    // pos == 1
    "LDI  %[pos_reg], 0b10000000  \n\t" // pos = 128
    "ADIW %[current_reg], 1       \n\t" // current++
    "RJMP WAIT_RISE               \n\t"
    // pos > 1
    "GREATER_THAN_ONE:            \n\t"
    "ROR  %[pos_reg]              \n\t" // pos >>= 1
    "RJMP WAIT_RISE               \n\t"
    
    
    "SAVE:                        \n\t"
    // save changes
    "MOV  %[pos_reg_save], %[pos_reg]                     \n\t"
    // copy this pointer to the global pointer (A -> upper 16b; B -> lower 16b)
    "MOV  %A[current_reg_save], %A[current_reg]           \n\t"
    "MOV  %B[current_reg_save], %B[current_reg]           \n\t"
    
    : [pos_reg_save] "=r" (pos), [current_reg_save] "=r" (current) // out
    : [pos_reg] "r" (pos), [current_reg] "e" (current) // in
    : "r16", "r17", "r18" // used registers
  );
  
  send();
  preparePulse();
}


void preparePulse() {
  for (int x = 0; recording + x <= current; x++) recording[x] = 0;
  current = recording;
  pos = 0b10000000;
}


void setup() {
  Serial.begin(9600);


  pinMode(PIN_IR, INPUT);
  pinMode(LED_BUILTIN, OUTPUT);


  current = &recording[SIZE - 1]; // erase all recording array
  preparePulse();


  attachInterrupt(INTERRUPT_IR, pulse, RISING);
}


void send() {
  Serial.println("-- INIT --");
  
  for (int x = 0; recording + x <= current; x++) {
    Serial.print(recording[x], BIN);
  }
  
  Serial.print("\n-- END -- ");
  Serial.print(recording - current + 1);
  Serial.print(" bytes, ending at bit ");
  Serial.println(pos);
}


void loop() { }

Your packets are about 8695 bits long (10 milliseconds at 1150 nanoseconds per bit). That's 1087 bytes so if you are getting 400 bytes you are still missing over half the bits.

As an experiment, change the ISR to do nothing but increment a bit counter. If you can get a consistent number around 8700 then at least you know the processor is capable of keeping up with the bit rate. You may need to temporarily disable the Timer 0 overflow interrupt because it interrupts about once per millisecond and you are likely to miss a bit or two while it is updating the millis() timer. WARNING: You can't use millis() for timing while the Timer 0 interrupt is disabled.

I didn't know that Arduino uses timer interrupts... After disabling it and using polling instead of interrupts (Arduino's response time was around ~2us) it worked like a charm!!!

Now that I have the full sequence I can pre-load the values and respond differently for each button (and using a 20MHz controller). Almost every button uses only 3 bytes to identify itself, so I can just ignore the rest.

Also, thanks for your help! I think it wouldn't have been possible without your advices.

Here's the final code:

#define PIN_IR 2
#define INTERRUPT_IR digitalPinToInterrupt(PIN_IR)


#define SIZE 12000/8 // it takes ~10ms to complete, that's ~12kb


volatile byte *current;
volatile byte pos;
volatile byte recording[SIZE];


__attribute__((naked)) void capture(void) {
  noInterrupts();
  
  asm volatile (
    "LDI  r16, 1                  \n\t" // r16 = 1
    
    // first time => wait rise without timeout
    "WAIT_RISE_FIRST:             \n\t"
    "SBIS 0x02, 0                 \n\t"
    "RJMP WAIT_RISE_FIRST         \n\t" // PIN LOW
    "RJMP SYNC                    \n\t" // PIN HIGH
    // min: 200ns; max: 350ns
    
    // wait until pin is HIGH
    "WAIT_RISE:                   \n\t"
    "LDI  r17, 246                \n\t" // r17 = 256-10 = 246 [~4 missing cycles]
    "WAIT_RISE_LOOP:              \n\t"
    "ADD  r17, r16                \n\t" // r17++
    "BRCS SAVE                    \n\t" // carry -> GOTO SAVE
    "SBIS 0x02, 0                 \n\t"
    "RJMP WAIT_RISE_LOOP          \n\t" // PIN LOW
    // PIN HIGH:
    // min: 150ns; max: 350ns
    
    "SYNC:                        \n\t"
    "NOP                          \n\t"
    "SBI  0x11, 2                 \n\t" // LED on
    "CBI  0x11, 2                 \n\t" // LED off
    
    
    // Note: get at this point at >400ns & <750ns since the pulse (T0 starts from here)
    "LD   r17, %a[current_reg]    \n\t" // r17 = *current [3 cycles]
    "SBIC 0x02, 0                 \n\t"
    "OR   r17, %[pos_reg]         \n\t" // PIN HIGH -> r17 |= pos
    "ST   %a[current_reg], r17    \n\t" // *current = r17 = *current | pos  [2 cycles]
    // if PIN LOW -> 7; HIGH -> 7
    // 350ns since T0
    
    
    // pos == 1 -> 5; pos > 1 -> 3
    "LSR  %[pos_reg]              \n\t" // pos >>= 1
    "BRCC WAIT_RISE               \n\t"
    // pos == 1
    "ROR  %[pos_reg]              \n\t" // pos >>= 1 (with carry) -> pos = 128
    "ADIW %[current_reg], 1       \n\t" // pos == 1 -> current++
    "RJMP WAIT_RISE               \n\t"
    
    
    "SAVE:                        \n\t"
    // save changes
    "MOV  %[pos_reg_save], %[pos_reg]                     \n\t"
    // copy this pointer to the global pointer (A -> upper 16b; B -> lower 16b) [the lower must be readed before]
    "MOV  %B[current_reg_save], %B[current_reg]           \n\t"
    "MOV  %A[current_reg_save], %A[current_reg]           \n\t"
    
    : [pos_reg_save] "=r" (pos), [current_reg_save] "=&r" (current) // out
    : [pos_reg] "r" (pos), [current_reg] "e" (current) // in
    : "r16", "r17" // used registers
  );
  
  interrupts();
  
  send();
  preparePulse();
}


void preparePulse() {
  current = recording;
  pos = 0b10000000;
}


void setup() {
  Serial.begin(9600);


  pinMode(PIN_IR, INPUT);
  pinMode(LED_BUILTIN, OUTPUT);


  for (int x = 0; x < SIZE; x++) recording[x] = 0; // erase all recording array
  preparePulse();
}


void printHex(byte info) {
  if (info < 16) Serial.print('0');
    Serial.print(info, HEX);
    Serial.print(':');
}


void printBin(byte info) {
    if (info == 0) Serial.print("000000");
    if ((info & 0b10000000) == 0) Serial.print('0');
    Serial.print(info, BIN);
}


void send() {
  int x = 0;
  while (recording + x <= current) {
    printHex(recording[x]);
    //printBin(recording[x]);
    
    recording[x++] = 0; // reset current & increment x
  }
  
  Serial.print(" ");
  Serial.print(x);
  Serial.print(" bytes; ending at bit ");
  Serial.println(pos);
}


void loop() {
  capture();
}

arduino capture.txt (21.5 KB)

The fastest way to read a pulse stream like that is indeed by polling, while using a timer to keep track of time.

And you can do it all in C. For example, input on Arduino pin 2 (PORTD, bit 2 on the Uno)

while( (PIND&4) == 0); wait for rising edge
TCNT1 = 0; //clear timer 1 count
while( (PIND&4) == 1); wait for falling edge
time_high = TCNT1;

If initialized properly, Timer1 can be used with this method to keep track of intervals with 62.5 ns resolution (+/- 2 counts).

Note: the compiler reduces the while statements to two machine instructions each.

jremington:
The fastest way to read a pulse stream like that is polling, while using a timer to keep track of time.

The problem of doing that it's saving the obtained bit into the array within the 400ns range, but if I wanted to get only one pulse I guess that would work even better.

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.