Understanding the ATmega328P PWM using Forth

Where I use Forth to develop a better understanding of the ATmega328P PWM capabilities. Revised from original post on April 17, 2021

Introduction

I spent a fair amount of time reading the late Jeff Fox’s treatise on developing code in Forth. As I read it, I realized I was doing what he was criticizing, which was rewriting “C” code in Forth. He was right, Chuck Moore was right and they will continue to be right. Forth is different and you solve problems differently using Forth than you do in other languages.

The key development points I took away from “Thoughtful Programming and Forth” were:

  1. Forth is interactive.
  2. Forth is extensible.
  3. Use the stack.

C is not any of these three things. In embedded C, it is painful to be interactive, extending C is not easy and there isn’t the stack. However, I was writing Forth code as I would C code.

For example, I was testing a very simple timing test, to determine if the word ms was accurate. I wanted to toggle a pin at a specific interval than use an oscilloscope to view the waveform.

: time_10ms ( -- ) \ very accurate timing, can range down to 1ms/loop
	D3 output
	begin
		D3 toggle
		10 ms
	again
;

The above routine has the time embedded in the code. Why? Because that is how you have to do it in C! Once I replaced the 10 with dup, I was able to easily and quickly test the timing loop for any length of time. Further more, my loop was more efficient! (There is less overhead in using the stack than pulling a number from flash.)

I found myself in a similar predicament when I started developing code using the ATmega328 PWM capabilities. I attempted to “re-write” my C PWM code in Forth. Once again, it didn’t go well. Once I broke away from the C paradigm, and began to play in Forth, the PWM became much easier!

Documentation

To understand how the PWM works:

  1. Read the datasheet.
  2. Play with the registers and watch the results on an oscilloscope.
  3. Repeat.

Datasheet

For this exercise, I focused on Timer/Counter 1, however it is helpful to know which pins are controlled by a specific Timer/Counter.

\ PWM Pins - in order of Timer/Counters
PIN6 DDRD 2constant ARD6  \   6 PD6 PWM OC0A 16bit T/C 0
PIN5 DDRD 2constant ARD5  \   5 PD5 PWM OC0B 16bit T/C 0
PIN2 DDRB 2constant ARD10 \  10 PB2 PWM OC1B 16bit T/C 1
PIN1 DDRB 2constant ARD9  \   9 PB1 PWM OC1A 16bit T/C 1
PIN3 DDRB 2constant ARD11 \  11 PB3 PWM OC2A 8bit T/C 2
PIN3 DDRD 2constant ARD3  \   3 PD3 PWM OC2B 8bit T/C 2

For Timer/Counter 1, I started here: (It’s helpful to note the page numbers in the datasheet.)

\ Timer 1 definitions pgs 140-166 DS40002061B
\ TCCR1A [ COM1A1 COM1A0 COM1B1 COM1B0 0 0 WGM11 WGM10 ]
\ TCCR1B [ ICNC1 ICES1 0 WGM13 WGM12 CS12 CS11 CS10 ]

Play

Once I started playing with the registers and watching the results on the scope, I realized the PWM could be simplified to:

  1. Fast PWM WGM13:0 = 5, 6, 7, 14, or 15
  2. Use both OC1A and OC1B, WGM13:0 = 5, 6, 7 so WGM13 is 0
  3. WGM12 remains 1, while WGM1:0 range from 1-3 and control the resolution of the counter
  4. The frequency can be easily controlled by CS2:0 1-5

Which meant these parameters will control the PWM:

  • frequency - (1-5) defining the scalar for the frequency of the PWM
  • resolution - (1-3) resolution of the counter, 8, 9 or 10 bits
  • dcA - duty cycle numerator of pin 9 (OC1A) duty cycle = dcA/resolution
  • dcB - duty cycle numerator of pin 10 (OC1B)

And by writing a word which stores those parameters appropriately will create a PWM based on Timer 1 using Fast PWM (single-slope operation):

: FPWM_1 ( freq res dcA dcB -- )
  OCR1BL ! \ n/resolution dcB
  OCR1AL ! \ n/resolution dcA
  %10110000 + TCCR1A c! \ COM1A1 COM1B1 res
  %00001000 + TCCR1B c! \ WGM12 freq
  D9 output
  D10 output
;

Play

Using this word and entering different values for frequency, resolution and duty cycle I was able to cycle through 15 different frequencies, all with a duty cycle between 0-100%. The more I played and explored the PWM, I realized I could simplify the interface even more. The resolution defined the frequency, so there wasn’t value in keeping them separate. The resolution also defined the range for the duty cycle, however, I didn’t see value in creating a check to determine if the value was in range. Nor did I see value in checking if there were two duty cycle values.

This left me with an interface which could use these constants:

\ 8-bit resolution => duty cycle n/255
1 1 2constant 62K_8
2 1 2constant 8K_8
3 1 2constant 975_8
4 1 2constant 244_8
5 1 2constant 60_8

\ 9-bit resolution => duty cycle n/512
1 2 2constant 31K_9
2 2 2constant 4K_9
3 2 2constant 488_9
4 2 2constant 122_9
5 2 2constant 30_9

\ 10-bit resolution => duty cycle n/1023
1 3 2constant 16K_0
2 3 2constant 2K_0
3 3 2constant 244_0
4 3 2constant 61_0
5 3 2constant 15_0

Using the constants, made the values self-documenting. One, you knew what the frequency would be and the last digit defined the range for the duty cycle, 8-bits, 9-bits, or 0 (for 10-bits). So the final usage would be something like this:

62K_8 127 63 FPWM_1 \ 62.4KHz, 50% duty cycle on Pin 9, 75% positive duty cycle on Pin 10
4K_9 127 255 FPWM_1 \  3.9KHz, 25% positive duty cycle on Pin 9, 50% duty cycle on pin 10 

Final Thoughts

Duplicating this effort for Timer/Counter 0 and Timer/Counter 2 would be extremely easy as neither T/C has the resolution variable to worry about. The frequency is determined solely by the input scalar.

I believe this approach is much more simple and more powerful than analogWrite(duty cycle) of the Arduino framework as it allows one to choose a frequency along with the duty cycle of the signal.

Comments powered by Talkyard.