I'm trying to get some C code that runs on a PIC18F, working on an Arduino '328P. It uses an 8-bit timer, PWM, and a LUT to generate sine waves. I hope to get it going using Timer2 on a Nano.
The PIC code is from this blog post by Roman Black. I understand what the PIC code is doing, but I can't manage to translate the modes and flags into Nano-land.
The PIC is running at 8MHz, the Nano at 16MHz with no pre-scale. PIC uses a PWM period of 128, Nano uses 256. I've taken some liberties here with the PIC values but the spirit is the same.
const byte sin[64] = {...some values};
uint32_t wave;
uint32_t incr = 17179869; // for 1000Hz
PR2 = (128-1); // PWM period = 128
while(1) {
while(!PIR1.TMR2IF); // align to start of PWM cycle
PIR1.TMR2IF = 0;
wave += incr;
CCPR2L = sin[(wave >> 24) & 0x3F]; // load into PWM module
}
For completeness, the PIC data sheet is here
I have the following Nano setup, with the goal of Fast PWM, no pre-scale, output at pin 11 (the "B" channel of Timer2).
TCCR2A = bit(COM2A0)
| bit(COM2B1)
| bit(WGM21)
| bit(WGM20);
TCCR2B = bit(WGM22) | bit(CS20);
Generating the wave then looks like:
while (true) {
while ((TIMSK2 | bit(OCIE2B)) == 0) {}
TIMSK2 |= bit(OCIE2B);
wave += incr;
OCR2A = sinLUT[(wave >> 24) & 0x3f];
}
It doesn't generate anything like a sine wave at 1000Hz, so I'm clearly missing something. FWIW, I filter the output with 2.2K/100nf
EDIT: UPDATED
As requested, here's the latest setup. Output seems to glitch slightly, although I can't see where.
pinMode(4, OUTPUT); // for stable trigger
pinMode(3, OUTPUT); // timer2 output B
TIMSK2 |= bit(TOIE2); // enable TOV2 = timer2 overflow
TCCR2A = bit(COM2B1) // fast PWM, top = 0xFF
| bit(WGM20) // PWM Mode 3
| bit(WGM21);
TCCR2B = bit(CS20); // no prescale
Then:
ISR(TIMER2_OVF_vect) { // PWM cycle rate
static uint8_t div;
static uint32_t wave;
const unit32_t incr = 1024 * 64 * 65536 / 62500 // 1024 Hz
TIFR2 |= bit(TOV2); // clear the interrupt
if (div++ == 0) PORTD ^= bit(4); // scope trigger
wave += incr;
OCR2B = sinLUT[(wave >> 16) & 0x3f];
}
2 Answers 2
There are quite a few issues here.
First, you have to decide what channel you are going to use. You wrote that pin 11 is the B channel of Timer 2, but this is incorrect:
- pin 11 = PB3 = OC2A = A channel of Timer 2
- pin 3 = PD3 = OC2B = B channel of Timer 2
Next, you have to set the pin as output:
// either:
DDRB |= bit(PB3); // set pin 11 = PB3 = OC2A as output
// or:
DDRD |= bit(PD3); // set pin 3 = PD3 = OC2B as output
For enabling non-inverting PWM, you have to set either bit COM2A1
or
COM2B1
, depending on the channel you want. There is no reason to touch
the bit COM2A0
, which is for for the "toggle on compare match" mode
(not what you want).
Regarding the timer mode, you have chosen mode 7, (fast PWM with
TOP = OCR2A
). The ability to set the TOP value is meant to enable
frequency modulation, at the cost of loosing PWM capability on
channel A. Again, not what you want. Instead, you want the timer to
count all the way from 0x00
to 0xff
, which is what mode 3 gives you.
Then, there is the code that waits for the compare match in order to
update the PWM value at the right time. You are using the wrong bit:
OCIE2B
is an "interrupt enable" bit, which is used to enable the
corresponding interrupt. If you enable the interrupt and have not
defined a suitable interrupt service routine, then your program will
crash (restart). You do not need interrupts here. Instead, you can
simply test the "interrupt flag" OCF2A
or OCF2B
(depending on the
channel) from register TIFR2
.
There could be a race here: if the PWM value is very high, the interrupt
flag will raise just before the start of the next PWM cycle, and you may
end up updating the PWM value a bit too late. I would recommend instead
monitoring the TOV2
(overflow) interrupt flag, which raises at the
very beginning of a PWM cycle. Then you have a full cycle to update the
PWM value.
Edit: Here are some comments on the updated version of your code.
The expression 1024 * 64 * 65536 / 62500
overflows on both
multiplications. I suggest computing this as a float and assigning then
to an integer.
The line
TIFR2 |= bit(TOV2); // clear the interrupt
is only useful when you poll the interrupt flag with interrupts disabled. As you are now using the interrupt, you do not need this: the flag is automatically cleared when the ISR starts executing.
If you want to toggle an output pin, you can do
PIND |= bit(4);
which is a single machine instruction and more efficient than
PORTD ^= bit(4);
(a read-modify-write sequence). Then, for monitoring
the execution of the ISR on the scope, I would rather set and clear the
pin on every ISR execution.
I tested your program with the ISR modified as this:
ISR(TIMER2_OVF_vect) {
static uint32_t wave;
const uint32_t incr = 1.0 * 64 * 256 * 65536 / F_CPU;
PORTD |= bit(4);
wave += incr;
OCR2B = sinLUT[(wave >> 16) & 0x3f];
PORTD &= ~bit(4);
}
Note that I lowered the frequency to 1 Hz, in order to better see the behavior on the scope. The output looks really clean to me. There are some glitches on the pin 4 pulses, which occasionally are late by about 6.2 μs. This is due to the Timer 0 overflow interrupt, which is used by the Arduino core for timekeeping. If you don't need the Arduino timekeeping functions, you can avoid these glitches by disabling that interrupt. However, since the ISR is never late by more than one PWM cycle, the glitches should have no effect on the PWM signal generation.
-
Thanks a lot. I'd already realized that I had confused interrupt Enable (TIMSK2) with interrupt Flags (TIFR2) and fixed that. Mode 7 and Mode 3 acted the same because I had OCR2A = 0xFF (TOP = MAX). I've switched to Mode 3. I then enabled TOV and moved the updates into the ISR. It works as expected, but my scope shows it as a bit glitchy. Still, major progress.Jim Mack– Jim Mack2025年01月21日 15:39:50 +00:00Commented Jan 21 at 15:39
-
1@Jim Mack please add your latest code to your question. The PIC code looks remarkably compact with high claims for its accuracy. It is well worth pursuing the development of an Arduino equivalent of the that "Roman Black" solution (without glitches).6v6gt– 6v6gt2025年01月21日 15:45:40 +00:00Commented Jan 21 at 15:45
-
@6v6gt - will do. The basic technique for high precision is fairly well known, but I hadn't seen it applied to sine waves. I stepped back from a 24-bit to a 16-bit fixed-point fraction since that level of precision isn't warranted on a Nano.Jim Mack– Jim Mack2025年01月21日 17:28:41 +00:00Commented Jan 21 at 17:28
-
Thanks for the added notes. I actually computed a const float = 67.108864 and multiply by the frequency to get the increment. The chain of mults was just to show the derivation. Agreed that F_CPU should be a factor.Jim Mack– Jim Mack2025年01月22日 00:48:50 +00:00Commented Jan 22 at 0:48
There was a request to post a complete sketch showing the method in action, and while this is overkill for that purpose, I think anyone interested in generating sine waves will find this blog post of mine worthwhile. Thanks again to @edgarbonet.
Explore related questions
See similar questions with these tags.
TIMSK2 | bit(OCIE2B)
is always non-zero? -- Oh, and for us to reproduce, please consider to extend your code snippet to a minimal, complete, and reproducible sketch.