As has been mentioned before, in comments and in jwpat7's answer, your
main problem here is the time taken to compute the sines. I already
provided a link to a faster fixed-point implementation of
sin() but, since you have an Mega2560 with loads of RAM, I
would go to the simpler look-up table method. I would not even bother
optimizing the size of the table.
For controlling the timing, you can use the canonical method described
in the Blink Without Delay Arduino tutorial, with one twist: times
should be measures with micros() instead of millis(). Here is a
first version of the program:
const float FREQUENCY = 50; // hertz
const float PERIOD = 1e6 / FREQUENCY; // microseconds
const size_t TABLE_SIZE = 512;
const uint16_t SAMPLE_TIME = PERIOD/TABLE_SIZE + 0.5;
uint8_t wavetable[TABLE_SIZE];
void setup()
{
// Prepare the wavetable.
for (size_t i = 0; i < TABLE_SIZE; i++) {
wavetable[i] = 255 * abs(sin(M_PI * i / TABLE_SIZE));
}
// Set port A as output.
DDRA = 0xff;
}
void loop()
{
uint16_t now = micros();
static uint16_t last_sample;
static size_t phase;
if (now - last_sample > SAMPLE_TIME) {
last_sample += SAMPLE_TIME;
PORTA = wavetable[phase];
phase = (phase + 1) % TABLE_SIZE;
}
}
A few things worth noting:
- The float constants
FREQUENCY and PERIOD are used only at compile
time.
- The table size is a power of two because wrapping a table index
modulo a power of two is way faster than modulo, say, 500: the
compiler optimizes the modulo into a bitwise AND.
- The
+ 0.5 in the expression of SAMPLE_TIME is intended to ensure
that the result is rounded to the nearest integer. In this instance
it happens to be useless because rounding down would give the same
result.
- The
micros() function returns a 32-bit integer, but in this
application it is sufficient and faster to track only the 16 least
significant bits, hence the uint16_t timing variables. And no,
these variables overflowing is not a problem.
This should give you, in theory, a frequency of 50.08 Hz, i.e.
0.16% too fast. The discrepancy is due to SAMPLE_TIME, which should be
39.0625 µs, being rounded to 39 µs. You could fix this by
counting time with a higher resolution, but given the poor accuracy of
the ceramic resonator clocking the Arduino, this is probably not worth
the trouble.
A potentially worse problem is the large jitter you have with this
approach, on the order of 10 µs. This is due both to the time taken
to run through the loop and the occasional timer interrupt taking some
extra time. You can get rid of this jitter by using a timer interrupt
instead of micros(). Here is a second version of the program that uses
this approach:
#include <avr/sleep.h>
const float FREQUENCY = 50; // hertz
const float PERIOD = F_CPU / FREQUENCY; // CPU cycles
const size_t TABLE_SIZE = 512;
const uint16_t SAMPLE_TIME = PERIOD/TABLE_SIZE + 0.5;
uint8_t wavetable[TABLE_SIZE];
int main()
{
// Prepare the wavetable.
for (size_t i = 0; i < TABLE_SIZE; i++) {
wavetable[i] = 255 * abs(sin(M_PI * i / TABLE_SIZE));
}
// Set port A as output.
DDRA = 0xff;
// Configure Timer 1.
OCR1A = SAMPLE_TIME - 1; // set the period
TIMSK1 = _BV(OCIE1A); // enable TIMER1_COMPA interrupt
TCCR1B = _BV(WGM12) // CTC mode with TOP = OCR1A
| _BV(CS10); // clock at F_CPU / 1
// Sleep while waiting for interrupts.
sei();
sleep_enable();
for (;;) sleep_cpu();
}
// Service routine for the Timer 1 interrupt.
ISR(TIMER1_COMPA_vect)
{
static size_t phase;
PORTA = wavetable[phase];
phase = (phase + 1) % TABLE_SIZE;
}
Again, a few notes:
- The
SAMPLE_TIME is now computed in CPU cycles and, since it is an
integer number of cycles (namely 625), there should in principle be
no frequency discrepancy.
- For counting 625 cycles we need a 16-bit timer clocked at the full
CPU speed (prescaler = 1). We are using Timer 1 here. See the
datasheet of the ATmega2560, chapter 17, for details on
using the timer.
- Sleeping between interrupts is needed in order to obtain
cycle-accurate timings. If we had simply an infinite loop, there
could be a few CPU cycles of jitter, which may not be a big deal.
- Writing
main() instead of setup() and loop() is a way to skip
the initialization normally done by the Arduino core library. In this
application that initialization is better avoided, because it creates
an extra jitter-inducing interrupt and it messes with Timer 1 in a
way that we would need to undo.