I stumbled upon a solution via trial and error coupled with watching the results on an o'scope.
The best guidance came from a Stack Exchange QA. Setup isn't particularly special, however the combination of WGM bits here worked and I didn't investigate further. Of the biggest importance are the prescaler divider bits, the least three significant bits of TCCRxB. 001
means no dividing or prescaling of the clock.
It all boiled down to 4 registers and the pinMode()
call. Ultimately, seven lines of code:
#define PULSE_PIN 45
void setup() {
// IMPORTANT: Clear Timer/Counter Control Registers
TCCR5A = 0;
TCCR5B = 0;
// Set Timer/Counter Control Registers
TCCR5A = B00101001; // Phase and frequency correct PWM change at OCRA
TCCR5B = B00010010; // least sig bits sets "carrier frequency" by dividing CPU clock
// Observation:
// TCCR5B = B00010010 and OCR5A = 16667 results in period of 16.70 mSec
// meaning OCR5A is period in uSec when TCCR5B = B00010010
// TCCR5B = B00010001 : OCR5A = 20000 results in period = 2.5 mSec 399.3Hz
pinMode(PULSE_PIN, OUTPUT);
}
In the sketch, I watch an analog pin with a potentiometer on it and map its sweep to an effective RPM range of 950 to 9000 RPM. Hence the arbitrary range of periods I want to handle. When loop()
sees the ADC change, the below code adjusts the PWM accordingly.
Note that no floating point data types are used. The magic numbers below were chosen by trial and error in a spreadsheet with values rounded to whole numbers.
void setRpm(){
// Getting to period in uSec from rpm the long way to keep
// the answer in range of the data type:
unsigned int rpm = map(pot_adc, 0, 1023, RPM_MIN, RPM_MAX);
unsigned long period = rpm/50;
period = 24000/period;
period = period*50;
// The trigger lasts 26°, so the PW is 26/360 * period.
dwell = period/36;
dwell = dwell*26;
dwell = dwell/10;
// OCR5A is a counter the carrier frequency counts up to,
// which sets output period / frequency
// meaning OCR5A is period in uSec when TCCR5B = B00010010
OCR5A = period; // OCR5A is period in uSec when TCCR5B = B00010010
// OCR5B is the width of the pulse. If we used floats,
// we could use duty cycle * period.
OCR5B = dwell;
}
Obviously, this code will have limitations to the range of frequencies it can handle, but the Internet has plenty of means for higher frequency PWM, especially when the desired PWM is fixed and doesn't need varying.
The most helpful way to see the control registers is as binary positions and not to rely upon macros, masks, and bit shifting. This comment block helped greatly:
// WGM modes:
// TCCR1A
// Bit 7 6 5 4 3 2 1 0
// Bit Name COM1A1 COM1A0 COM1B1 COM1B0 ----- ----- WGM11 WGM10
// Initial Value 0 0 0 0 0 0 0 0
// changed to 1 1 1 1 0 0 0 1
// TCCR1B
// Bit 7 6 5 4 3 2 1 0
// Bit Name ICNC1 ICES1 ----- WGM13 WGM12 CS12 CS11 CS10
// Initial Value 0 0 0 0 0 0 0 0
// changed to 0 0 0 0 1 0 0 1
// CS12,CS11,CS10 = (0,1,1) = prescaler divide by 64