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).
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.
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'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);
}
}
}
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();
}
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.