Well, 5.3 MHz sounds very good. But there is a theorem stated by Harry Nyquist and Claude E. Shannon saying the maximum signal frequency that can be handled must be less than half of the sampling frequency. In other words: if you take a look out of your window only once a year you would not notice there are seasons. In our case, all frequencies contained in the signal to analyze should be lower than 2.5 Mhz.
What it is all aboutWhen handling different signals some of them still unknown it will be very useful to examine them. Professional logic analyzers come very powerful, but for tracing problems with your Arduino even a simple one will do. The project shown here displays up to six digital signals using the Serial Plotter of the Arduino IDE 1.8.19 which shows 500 samples. The input pins will be pin-8 to pin-13 (all of them belonging to Port B). You may use internal timing references on one or two of the channels.
The picture above shows signals between four cascaded WS2812 strips of 8 LEDs each. Obviously, as expected, they do not end exactly at the same moment.
The secret to achieve that speed was getting rid of for-loops while sampling. In order not to paste and copy those many lines of code, I used two different methods to achieve this: one is the Duplicate-Macro, and the other one is the recursive method. Doing it this way the amount of code could be reduced to an absolute minimum. Reference signals produced by internal timers enable you to evaluate your unknown signals. Just connect pin-3 or pin-5 to any of the input channels.
A First ApplicationWhile standard binary frequency dividers are a bit boring, I found an old-fashioned TTL chip, an SN7492, acting as a divide-by-12.
Connections to be made:
Arduino, pin-3 --> 7492, pin-14
7492, pin-12 --> 7492, pin-1 and Arduino, pin-11
7492, pin- 11 --> Arduino, pin-10
7492, pin-9 --> Arduino, pin 9
7492, pin-8 --> Arduino, pin-8
Both of the RESET pins have to be grounded to enable the counter.
The output of pin-3 was set to 125 kHz and fed into the counter's input.
You can easily see the counter outputs A, B, C, and D.
Actually, the dividers inside the 7492 are independent of each other, so you can feed the trigger into pin-1 and connect pin-8 to pin-14 to get different results.
Now, let us make a Divide-By-9-counter out of an SN7493 (4-bit binary counter)What is the use of a divide-by-9-counter? No idea, just an exercise. A Divide-by-9-counter should count from zero to eight and then restart from zero. So, there must be an immediate reset when the counter value reaches the 'nine' which is equal to binary '1001'. The good news is the SN7493 offers two reset inputs, which can be triggered by the two '1's of the 'nine'.
As soon as these '1's show up, the counter state immediately changes from nine to zero, so the nine is only visible for a fraction of a microsecond. You would need a much better logic analyzer to detect this.
The lines from top to bottom are: black unused, clock (250 kHz) violet, output A yellow, output B green, output C red, output D blue. The black vertical bars indicate '1000' = 8, shortly after that a reset condition '0000' will happen. There should be a '1001' in between, but it is not visible.
Does the software use any tricks?Yes, there are some of them:
At first, there are variables that are not initalized by the system. This makes it possible to use the RESET key as a switch to toggle between different states. The actual frequency is printed to the Serial plotter which in turn will show it claiming it were variable labels. You can fool the plotter by hiding a number between characters other than space and tabs.
The next one is the triggering:
while (MYPORT == MYPORT);
So the input byte is read twice again and again, and both values are getting compared to each other. It must be confessed in some cases you might miss a change. But soon there will be another one.
Then there is a replacement for the delay function as Timer0 is getting mishandled.
If you would use a loop for sampling it might look like this:
"SAMPLOOP: in __tmp_reg__, %[CHANPINaddr]; read input data [1]\n\t"
"nop ; cycle padding [1]\n\t"
"st %a[LOGICDATAaddr]+, __tmp_reg__ ; store input & post incr. [2]\n\t"
"sbiw %A[DELAYCOUNT], 1 ; decrement delayCount [2]\n\t"
"brne SAMPLOOP ; continue until end [2]\n\t"
(found at https://hinterm-ziel.de/index.php/2021/10/11/real-programmers-write-assembly-code/#more-1065 ). When you unroll the loop it reduces to just three machine cycles as can be seen at he end of this article. Actually, there was a project published by Udo Klein in 2009 (https://playground.arduino.cc/Main/LogicAnalyzer/), long before the Serial plotter had been introduced, and you had to copy the two lines manually. Two different versions are shown here:
1) The version with the duplicate macro just uses these defines:
#define D(X) X X // each D doubles the sequence
#define D9(X) D(D(D(D(D(D(D(D(D(X)))))))))
// the instruction is(getting doubled many times):
#define INST "in __tmp_reg__, %[A] \n st %a[B]+, __tmp_reg__ \n "
So, the line
asm volatile(D9(INST)
causes the execution of the assembler instructions 2^9 = 512 times.
2) Now the recursive version with the include file. The macro __FILE__ is the name of the actual file. So, the line
#include __FILE__
forces to execute the same file as the caller. You are free to rename that include file, it still will call a copy of itself. The only problem is: the limit of recursive calls is set by the system to 200. That is why the value of DEPTH has to be below 200. In order not to get any rounding errors, we set it to 500/4=125. But as you still want 500 samples to be shown on the plotter you have to use several recursive calls, in this case four times. (The software was designed for the old IDE 1.8.x, as the plotter of the new IDE 2.x supports only 50 samples. You might use the new IDE for development and the old one to use its plotter.)
The asm statements cannot be shorter. The compiler transforms them into a long sequence of
800: 01 92 st Z+, r0
802: 03 b0 in r0, 0x03 ; 3
804: 01 92 st Z+, r0
806: 03 b0 in r0, 0x03 ; 3
808: 01 92 st Z+, r0
80a: 03 b0 in r0, 0x03 ; 3
...
(Only the first two of the lines have to be entered, all the rest will be done by the system.)
There is no increment and conditional jump necessary. Each pair of these instructions takes three clock cycles on the ATmega328, no way to get it any faster.
The download file contains both versions. Decide yourself which one pleases you most. Depending on your hardware, it might be necessary to reduce the baudrate from 115200 down to 57600 to get reliable results.
Comments