Go Down

Topic: 4MHz output from ports (Read 5376 times) previous topic - next topic

Amp

Over christmas vacation from college, I figured out how to generate any frequency up to 4MHz from the Arduino. This doesn't require burning new bootloaders or changing fuse bits, and can be done with the Arduino as-is with some low-level code.

Keep in mind 4MHz is a pulse HIGH and LOW. The outputs are changing once every 125nS (eg. 8MHz).

I performed the frequency measurements using my Digital Multi-Meter. I don't know if the pulses are perfect, since I haven't checked it with an oscilloscope, but they should be proper square waves of a valid voltage level.

This seemed like the place to post this information. It wasn't something obvious to me as a beginner so I thought I'd spill the code here and help somebody else. This probably isn't something for beginners, and falls under some "hack" category, I bet.

The gist of the program is pretty straight forward. In a tight loop, using digitalWrite HIGH & LOW on a pin didn't seem to go faster than 60KHz. I even read something that said 30KHz is about as fast as it can go!
I spent a long time looking for ways to send any kind of data or clock signal faster. Eventually I noticed the digitalWrite function's code had a lot of checks and safeguards that I didn't think were helping me, so I figured out how to write to a port without checks.

I stumbled across the PORTB variable here in the forums, and found that writing to it easily created 1.65MHz. My jaw dropped when I saw that the first time.
Then I found the cli() and sli() assembly macros (and learned about inline assembly, too). These commands disable and enable interrupts, which sped up the frequency to an even 2MHz.
By putting the PORTB=0xFF;PORTB=0x00; in a tight loop, with interrupts disabled, I reached 4MHz. (it might still be possible to go faster)
If interrupts aren't disabled, the highest frequency in a tight loop is 3.883MHz.

Setting PORTB=~PORTB; (inverting the bits) does about 1.6MHz. Reading the port slows it down a lot.

Here's the fastest version of the code I wrote. (4MHz)

void setup() {
 DDRB=0xFF;  // *evil laugh* why not change the data direction register manually, too!
 // this sets all pins on port b as outputs. (this includes pin 13 ~ that has an LED connected on the programmer)
}

void loop() {
 cli();  // disable interrupts

 do {
   PORTB=0xFF;
   PORTB=0x00;
 } while (true);  // infinite loop

 sli(); // enable interrupts (program never reaches here)
}


Useful/Neat Trick
-------------------
If you declare an 8 bit unsigned integer, increment it and write it's value to PORTB in a tight loop you can generate a series of frequencies.
With interrupts disabled, I measured them at 50KHz,100KHz,200KHz,400KHz and 800KHz.
If you increment using this assembly instruction "asm ("INC %0" : "=a" (VARIABLE));" then the frequencies are higher. Replace VARIABLE with the name of your 8 bit variable. *I thought this was funny since ++ is typically synonymous with the INCrement assembly instruction, but the assembly still outperformed it. Sorry about the "=a" constraint, using a is wrong - but it worked and I didn't know better at the time, or correct the older code.

For that matter, you could use the INC instruction directly on the PORTB register and get even faster results. (I haven't tried that yet - the existing PORTB might not be suitable to pass as a register address to an inline assembly instruction - you may need to use the physical memory address of the register as a constant...)

This all works because an overflow of the 8 bit value makes it go back to zero. So I'm relying on an intentional overflow for it to work. There's nothing wrong with this so long as it discards the carry bit instead of writing it to the next register or memory location. (which it shouldn't do, especially with the inline assembly)

here's the code using plain-old increment (the setup() from above is still required)

void loop() {
 cli();

 unsigned int VARIABLE = 0;
 do {
   PORTB=VARIABLE;  // generates highest frequency on lowest pin/bit, etc.
   VARIABLE++;
 } while (true);
}

then the assembly code for faster multiple frequencies

void loop() {
 cli();

 unsigned int VARIABLE=0;
 do {
   PORTB=VARIABLE;
   asm ("INC %0" : "=d" (VARIABLE));
 } while (true);

}

As ugly as this code might look to other people, the above code might be even faster if I used goto LABEL instead of a do...loop
You can also use __asm__ if there's a name conflict, the two keywords asm and __asm__ are synonymous.
There's a lot of cool stuff about inline assembly out there!


NOTES
--------
PORTB, PORTC and PORTD are all valid variables for reading or writing directly to the associated register.
DDRB,DDRC,DDRD all set the data direction bits for the corresponding port (aka port mode).

using "asm ("NOP");" preforms "NO oPeration" which can be useful for delays to lower the frequency to some other value.

David Cuartielles

Cool trick,

no doubt it will be useful for many projects!

Thanks for it!

/David

Amp

I forgot something at the end of the original post.
asm volatile ("NOP");
Using the keyword volatile keeps the compiler from optimizing the code and removing the instruction, or rearranging the instructions.

AnyMouse

I've used your work to make a Morse code beacon broadcasting at 1337 kHz. Details at http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1170901754.

Benoît ROUSSEAU

Very good post and many thank's !
Cordialement,
Benoît ROUSSEAU

samzuni

#5
Apr 21, 2007, 05:16 am Last Edit: Apr 21, 2007, 05:17 am by samzuni Reason: 1
Great stuff, Amp. Especially for me as a NOOB to the AVR chips, and microcontrollers. Haven't touched one since 2003.
I found this because I was looking into ways to program PORTB pins all at once, instead of trying to do each pin at a time to read dip switches.

Anyway, I tried the code and found it would not compile because of the "SLI" instruction for Global Interrupt Enable. It didn't work, but then I looked in the instruction set and found that the "SEI" mnemonic was the correct one to perform that function.

My real question is I wonder if that has been renamed from earlier chip instruction sets?

Anyway, great routine, and thanks.

I forgot to mention I was using the ATmega168.

erik

neat trick but i think you get even higher frequenties when doing the loop in assembler code, look what asm can do.

http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1166667354/11#11

erik

Go Up