First of all: this is more academical than practical a problem! Just those little things that are fun to figure out and teach a bit more about the internals of the processor.
Yesterday I was tinkering a bit, and I was looking to create a high frequency block wave, target frequency around 1 MHz. This is of course trivial to do with a 555 timer, but as I was too lazy to dig up a 555 and wire it up I decided to use a 16 MHz Arduino board, to see how far I could push it.
First attempt: digitalRead() calls with a short delay between.
void setup() {
pinMode(11, OUTPUT);
}
void loop() {
digitalWrite(11, HIGH);
delayMicroseconds(1);
digitalWrite(11, LOW);
delayMicroseconds(1);
}
That gave a nice block wave but it was too low a frequency, about 300 kHz.
Next I changed the digitalWrite calls for direct port register calls:
void setup() {
pinMode(11, OUTPUT);
}
void loop() {
delayMicroseconds(1);
PORTB |= (1 << PB3);
delayMicroseconds(1);
PORTB &= ~(1 << PB3);
}
This increased the frequency dramatically, and I was getting about 2 MHz (0.5µs peak to peak)! Progress.
But also it didn’t give me a nice waveform any more. The pins simply can not follow this fast, probably too much stray capacitance to overcome. Also I noticed that the peak goes from low to high and back to low in about 0.2µs, after which it stays low for about 0.3 µs before going high again.
This taught me two things:
- delayMicroseconds() won’t do a 1 us delay, the minimum appears to be 4. I know the micros() counter increments in steps of 4, but didn’t see anything in the delayMicroseconds() documentation. A delay of 10 us is commonly used, though (but no-one of course measures to see if it’s really 10 us, not 8 or 12).
- digitalRead() and digitalWrite() are slow: taking about 3 us to complete (this was the half wave I saw when stripping out the delays.
- a direct port call takes two clock cycles (0.125µs)
- the looping of loop() takes 0.5 - 2 * 0.125 = 0.25µs - four clock cycles.
So… improvements:
void setup() {
pinMode(11, OUTPUT);
}
void loop() {
while (true) {
PORTB &= ~(1 << PB3);
PORTB |= (1 << PB3);
}
}
The while(true) loop brings the frequency a bit further up, now the period is just under 0.4µs. So three clock cycles to operate this loop, that’s three instructions, so I don’t think that can be improved on any further.
Now I still have the problem of a much shorter high than low. The high lasts 2 cycles, the low 2+3 = 5 cycles. Actually I want to get to 8 cycles high and 8 cycles low for 1 MHz. Or 6 and 6 for 1 1/2 MHz. Or 5 and 5, if possible.
Let’s try 8 cycles. 4x a port call for high, 2 times + loop() for low.
void setup() {
pinMode(11, OUTPUT);
}
void loop() {
PORTB |= (1 << PB3);
PORTB |= (1 << PB3);
PORTB |= (1 << PB3);
PORTB |= (1 << PB3);
PORTB &= ~(1 << PB3);
PORTB &= ~(1 << PB3);
}
1 MHz - beautiful Strip two lines and it’s at 1.5 MHz. Nice. And a pretty nice waveform. That looks like the limit, unless there is a command that takes one cycle (and doesn’t do anything really), or some built in hardware that can do this by itself that I’m not aware of.