I'm planning an exp-ramp generator with the following characteristics:

  • Implemented on the PIC16F1773, an 8-bit device with somewhat complex capture/compare/PWM hardware.
  • PWM module clock of 500 kHz * 32 = 16 MHz
  • System clock (Fosc) of 16 MHz / 2 * 4 = 32 MHz
  • Fixed PWM frequency of 233 Hz
  • Duty cycle starting at 0%, ending at 100% after 4.82 s
  • Approximating the exponential curve \$ y = 1 - 0.1^{t/2} \$, which has an equivalent time constant of \$ \tau \approx 0.8686 \$.

The strategy to modify the duty cycle is:

  • always increment the high byte of the duty cycle register PWMxDCH by 1, ignoring the low byte for simplicity
  • only perform a DC increment after a post-scaled counter rolls over whose input clock is the period match event of the PWM
  • the post-scale factor starts at 1, ends at 128, and is multiplied by 2 every ~140 PWM cycles

In a basic numerical simulation of this strategy, the approximation is quite good:

numerical sim

The bumps are where the post-scale factor is changed.

I have a little bit of decision paralysis around how best to implement the post-scaler. I could use a 16-bit PWM module:

16-bit PWM

interrupt on PRIF, maintain a software post-scale count variable, and on overflow of this count, increment the duty cycle. Or I could use the simpler 10-bit PWM module:

10-bit PWM

which does not support period interrupt; short the PWM output pin to the clock input pin TMRx_clk of a timer like this:

Timer 2

Where PRx is the post-scale value, the actual timer pre- and post-scale are not needed, and on TMR2IF, increment PWMxDCH.

I kind of prefer the secondary-timer approach because it means (1) easier external scope debugging, and (2) fewer runtime instructions required. Is there a way to make better use of the 1773's hardware to accomplish this thing?

  • 1
    \$\begingroup\$ Now that I've read more of the datasheet I think I'm going to use the complementary waveform generator (COG) which will require no external shorts; if I figure this out to my satisfaction I'll post a self-answer. \$\endgroup\$
    – Reinderien
    Oct 25 '20 at 19:40

Here's my best prototype-design:

Choose a base that is easy to multiply in RISC. The easiest is 1/2 which only requires shift operations. Successive accuracy is gained by using an exponent of 1/2, 3/4, 7/8 (etc), which progressively require more and more computation. For these purposes 1/2 is totally fine.

The ideal curve is

$$ y = 1 − 0.1 ^ {t/2} $$

Approximation requires converting the base:

$$0.1 ^ {t/2} = 2 ^ {-t / \tau}$$ $$ \tau = - \frac {2 \ln 2} {\ln 0.1} \approx 0.6021 s $$

where \$ \tau \$ is the differential halving time.

To approximate this with PWM, the output is the mean of the duty cycle. From here on in I don't examine the PWM waveform directly, and only the "instantaneous equivalent duty cycle" ranging from 0.0 to 1.0.

The PIC16F1773 includes a powerful complementary output generator (COG) that supports "steered PWM". In this mode, there is a rising and falling block, each accepting a variety of input clocks. For these purposes the most appropriate clocks are PWM5 and 6, respectively; each has 16-bit period resolution. The duty cycle of the individual PWM inputs does not matter and should be small; it's only used for the timing of its edge. Using superimposed pulse-modulated waves with a given beat frequency in this manner produces a duty cycle whose derivative is based on the beat frequency. In this scheme it's crucial to have a starting phase for the two sub-PWMs that is equal, and for no phase error to be introduced during execution.

PIC COG diagram

For set and reset frequencies,

$$ f_{reset} = \frac {f_{set}^2} { \frac {\partial DC} {\partial t} + f_{set} } $$


$$ \frac {\partial DC} {\partial t} = \frac {\ln 2} \tau \cdot 2 ^{-t/\tau} $$

Over the first segment, the mean derivative of the duty cycle is

$$ DC'(0) = \frac 1 \tau \int_0^\tau \frac {\ln 2} \tau \cdot 2 ^{-t/\tau} dt = \frac 1 {2 \tau} \approx 0.8304 $$

Getting it into the discrete domain is a little tricky. I want to select a fixed set period that is a multiple of 256 and has an initial corresponding reset period offset by exactly 128:

$$ P_{set} \mod 2^8 := 0 $$ $$ P_{set} + 2^7 := P_{reset} (0) $$ $$ 2^8 \le P_{set} \le P_{reset} < 2^{16} $$

This property allows the low byte of the reset period to be shifted in one operation every time a segment update occurs without needing to touch the high byte and without loss of accuracy.

The initial beat frequency between the set and reset PWMs is:

$$ f_{set} - f_{reset} (0) = f_{set} \left( 1 - \frac {f_{set}} {f_{set} + DC'(0)} \right) $$

In reality the clock is 16 MHz (HFINTOSC before the PLL). In theory, with a 32 MHz clock, the lowest \$ f_{set} \$ that offers sufficient resolution to satisfy the above is

  • Prescaler = 8
  • \$ P_{set} = 24,832 \$ = 0x6100
  • \$ P_{reset} (0) = 24,960 \$ = 0x6180
  • \$ f_{set} = \frac {32 MHz} {prescale (P_{set} + 1)} \approx 161.08 Hz \$
  • \$ f_{reset} (0) = \frac {32 MHz} {prescale (P_{reset} + 1)} \approx 160.25 Hz \$
  • Segment length (samples) = \$ \tau f_{set} \approx 97 \$

The ramp routine is considered complete when the beat frequency drops to 0, i.e. the set and reset periods become equal, the duty cycle becomes constant, and the duty cycle reaches approximately 100%.

A numerical simulation shows that, for my purposes, the result is a good approximation:

num approx

Switching to a base of 3/4 improves the approximation even further but that's not necessary for me. The real advantage to this approach is very minimal runtime instruction execution needed, and ability to run (mostly) during sleep.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.