Generating a linear frequency ramp with accurate timings is not an easy
task. I am providing here only a partial answer, where I first go
through the math of the problem, then give some ideas for the
implementation.
The math
We are dealing with a frequency-modulated signal, with the frequency
ramping linearly from f = 0 at t = 0 up to a
maximum f = fM at
t = tM. Specifically, we have
fM = 48 kHz and
tM = 10 s.
Then, the instantaneous frequency is
f(t) = a t
where a = fM/tM = 4.8
kHz/s is the “acceleration” of the pulsed signal.
Let's define the “integrated frequency” as
n(t) = ∫ f(t) dt = a t²/2
This quantity can be interpreted as the number of cycles completed at
time t, except that it is a continuous variable. The first cycle
starts when n = 0 and ends when n = 1, the next
cycle starts when n = 1 and ends when n = 2, and
so on. The output signal would be high when the fractional part of n
is between 0.25 and 0.75 (or maybe between 0 and 0.5, depending on your
choice of initial phase).
From here, we can calculate at what time each cycle should start:
t(n) = √(2 n/a)
and, by successive differences, the length of each cycle:
n 0 1 2 3 ... 239,998 239,999 240,000
t 0 20,412 28,868 35,355 ... 9,999,958 9,999,979 10,000,000
length 20,412 8,455 6,488 ... 20.833 20.833
diff -11,957 -1,967 -1,018 ... -0.000043
approx +81,650 -1,408 -913 ... -0.000043
In the table above, all times are in microseconds. The lines are:
- t: time
- length: length of one cycle
- diff: difference between lengths of successive cycles
- approx: approximated formula for that difference
The columns are staggered because each cycles spans the duration between
two successive values of t, and duration differences are computed
between successive cycles. The approximated formula is based on the
Taylor expansion:
T(n+1) − T(n) ≈ dT/dn + 1/2 d²T/dn² = −aT³(1 − 3/2 aT²)
where T = 1/f, the instantaneous period, approximates the
length of one cycle. The approximation is not good for the very first
cycles, but it converges pretty fast to the correct value.
Implementation
I did not write an implementation, so I am giving here only some ideas
on how to do it.
I would use Timer 1 of the Arduino Uno, in one of the modes that allows
frequency-modulation, i.e. a PWM mode where the counter TOP is set by a
register (mode 8, 9, 10, 11, 14 or 15). I would set the prescaler to 8
in order to allow for the maximum required period length. The period
resolution would then be:
- 8 CPU cycles = 0.5 µs in modes 14 or 15 (fast PWM)
- 16 CPU cyels = 1 µs in the other modes
It could be noted that single-cycle resolution is possible with the
prescaler set to 1, but the first 5 cycles are too long for this
setting. Maybe the prescaler could be changed after the fifth cycle, but
it would be tricky to do so without any glitch.
The timer overflow interrupt would fire at the end of each cycle, and
it's ISR (namely TIMER1_OVF_vect
) would be responsible for updating
the timer period in order to make the next cycle shorter than the one
just elapsed. It would do something like
period -= delta(period);
The period should be computed with 32-bit arithmetic, because the
changes get tiny by the end of the ramp. Only the 16 most
significant bits of the computed period would actually be written to the
timer register. The delta()
function should be inlined and fast. I may
try the approximation from the previous section implemented in
fixed-point. Or maybe just a table lookup using only the most
significant bits of the period as an index, with the table in PROGMEM.
For details on how to actually program the timer, see
the datasheet of the ATmega328P, section 16.
Update: The following algorithm seems to work well. At least in
simulation, I did not put it inside an ISR. It assumes fast PWM, i.e. a
0.5 µs timer resolution. It remembers the current value of delta
and divides it by 2 each time the period falls below some threshold. The
computed f(t) is almost as smooth as you can expect given the
limited resolution of the timer.
uint32_t period = 40825UL << 16; // 1st cycle is 20412.5 us
void update_period() // should be in ISR(TIMER1_OVF_vect)
{
static const uint16_t thresholds[] = {
17633, 13309, 10229, 7954, 6230, 4905, 3873, 3063,
2426, 1923, 1525, 1211, 960, 762, 605, 480, 381,
302, 240, 190, 151, 120, 95, 76, 60, 48};
static uint16_t threshold = 29000;
static uint8_t index = 0;
static uint32_t delta = 6UL << 27;
uint16_t period_h = period >> 16;
if (period_h <= 42) return; // final frequency achieved
if (period_h < threshold) {
threshold = thresholds[index++];
delta >>= 1;
}
period -= delta;
// set the timer period register to (period >> 16) - 1.
}