Page 1 of 1

8-bit PWM

Posted: 01 July 2008, 9:40 AM
by dkinzer
I've implemented PWM using the 8-bit Serial Timer (Timer2 or Timer0) with an API that is similar to that of the 16-bit PWM routines in the ZBasic System Library. On most ZX devices, two 8-bit channels are supported, numbered 1 and 2. On ZX devices based on the mega32 or mega128, only one channel is supported.

The PWM8 routines support both Phase/Frequency Correct and Fast PWM modes but the specification of the PWM frequency and the duty cycle are different from the 16-bit PWM routines due to the differences between the 8-bit and 16-bit timers.

The PWM frequency is specified by giving one of up to seven clock selector indices corresponding to divisors of the main clock, e.g. divide-by-1, divide-by-8, etc. The set of available divisors varies depending on the underlying processor; some have the ability to use an external clock signal to control the PWM frequency.

The PWM duty cycle is specified by giving a value in the range 0-255 corresponding to the range 0% to 100%.

These routines have been tested on a ZX-24a and a ZX-1281 and have been compiled successfully for at least one ZX device for each underlying processor.

The code is somewhat complicated due to the need to support varying hardware but it does illustrate ways to write ZBasic code that deals with differences in ZX devices, e.g. different register names, different bit assignments, etc.

[Edit: updated to v1.1 on 02 July 2008]

Posted: 01 July 2008, 10:44 AM
by dkinzer
I just posted an updated version that validates the clock selector parameter on OpenPWM8(). The original .zip file was replaced so if you downloaded it prior to the time of this message, just download it again to get the update.

Posted: 01 July 2008, 14:37 PM
by mikep
Good code Don. I tried it on a ZX-128ne and it worked fine. Timer 2 PWM output is on pin 21 (B.7) of the ZX128ne which is also used for timer 1 PWM (channel 3). This means if you want 4 PWM channels and input capture, the input capture has to be on timer 1 (pin 8, D.4) and PWM on timers 2 and 3. As usual I have some suggestions for improvements:

1. The second #ifdef code seems to be redundant. See marked line below

Code: Select all

#if Option.CPUType = "mega32" Or Option.CPUType = "mega128"

'****************************************************************
#define CHANNEL_COUNT 1
Private regTCCRnA as Byte Based Register.TCCR2.DataAddress
Private regOCRnA as Byte Based Register.OCR2.DataAddress
Private timerSem as Boolean Based Register.Timer2Busy.DataAddress
Private Const WGMA_Fast as Byte = &H48
Private Const WGMA_PFC as Byte = &H40
Private Const WGMA_Mask as Byte = &H48

#if Option.CPUType = "mega32" Or Option.CPUType = "mega128"   ' not needed ??
Private regDDR_OC1 as Byte Based Register.DDRB.DataAddress
Private Const OC1_MASK as Byte = &H80
#else
Private regDDR_OC1 as Byte Based Register.DDRD.DataAddress
Private Const OC1_MASK as Byte = &H80
#endif

'****************************************************************

#elseif Option.CPUType = "mega644" Or Option.CPUType = "mega644P"
2. I prefer return codes (stat) to be returned rather than a by reference parameter i.e.

Code: Select all

Function OpenPWM8(ByVal chan as Byte, ByVal prescaler as Byte, ByVal mode as Byte) as Boolean
3. The comment header for each routine should document each parameter, trange of valid values and return value if any. For example

Code: Select all

' Parameters
'	channel       PWM channel (range is 1 or 2)
'	clockSelect   Clock Prescaler value as listed above (range is 1 to 7)
'	mode          PWM mode (zxCorrectPWM or zxFastPWM)
' Returns
'	boolean	    True if PWM channel was successfully opened
4. I would make the names of the formal parameters match the equivalent 16-bit PWM APIs e.g. channel versus chan.

5. The clockSelect parameter for OpenPWM doesn't match the 16-bit API. It would be good if this could be the PWM frequency and the implementation calculate the corresponding clock select value. Likewise the same is true of the dutyCycle parameter - can we make it match the 16-bit timer PWM() API?

6. Is it possible to set the Register.Timer2Busy flag? If so then timerSem can be eliminated (which is not really a semaphore).

7. What is the purpose of chanCount variable? Why not simply use the CHANNEL_COUNT constant?

8. I changed the test code to loop around adjusting the PWM duty cycle and the chan constant to 1 (instead of 2). This is useful for testing with a LED or oscilloscope. Here are the guts of the modified code that uses duty cycles from 0 to 255.

Code: Select all

Sub Main()
	Const chan as Byte = 1
	Const delayTime as Single = 5.0

	Dim stat as Boolean
	Dim i as Byte
	
	stat = OpenPWM8(chan, 3, zxFastPWM)
	Do	

		Call PWM8(chan, 0, stat)
		Call Sleep(delayTime)	
	
		For i=0 to 15
			Call PWM8(chan, i*16+15, stat)
			Call Sleep(delayTime)
		Next i	
	Loop	
	' Never executed	
	'Call ClosePWM8(chan, stat)
End Sub

Posted: 01 July 2008, 15:07 PM
by dkinzer
mikep wrote:The second #ifdef code seems to be redundant.
Actually, the nested conditional should read:

Code: Select all

#if Option.CPUType = "mega128"
Private regDDR_OC1 as Byte Based Register.DDRB.DataAddress
Private Const OC1_MASK as Byte = &H80
#else
Private regDDR_OC1 as Byte Based Register.DDRD.DataAddress
Private Const OC1_MASK as Byte = &H80
#endif
This is necessary because OC2 on a mega128 is B.7 while it is D.7 on a mega32 but all other aspects are the same between them.
mikep wrote:I prefer return codes (stat) to be returned rather than a by reference parameter
Generally, I do as well. This code was derived from the 16-bit PWM code for the mega32-based devices (which is written in ZBasic). That code uses Function rather than Sub.
mikep wrote:The clockSelect parameter for OpenPWM doesn't match the 16-bit API.
That was my intention originally. However, there are only 5 or 7 possible frequencies so it seemed a better option to allow the caller to decide, when the exact frequency is not available, whether to go higher or lower.
mikep wrote:Is it possible to set the Register.Timer2Busy flag? If so then timerSem can be eliminated (which is not really a semaphore).
Actually, timerSem is just an alias for Register.Timer0Busy or Register.Timer2Busy as needed.
mikep wrote:What is the purpose of chanCount variable? Why not simply use the CHANNEL_COUNT constant?
Quite right. Early in the evolution of the code I was was using a different #define constant to control control whether one or two TCCR registers were used and the values of the WGM mode bits. Later, this evolved into CHANNEL_COUNT and I didn't see that chanCount was then redundant.
mikep wrote:I changed the test code to loop around adjusting the PWM duty cycle [...]
I was just preparing to do something similar. I have some test code for the 16-bit PWM that sweeps the duty cycle up and down, producing a nice effect with an LED.

Posted: 01 July 2008, 15:18 PM
by mikep
dkinzer wrote:Actually, timerSem is just an alias for Register.Timer0Busy or Register.Timer2Busy as needed.
So it is. I missed that.

Posted: 01 July 2008, 15:25 PM
by Don_Kirby
dkinzer wrote:I have some test code for the 16-bit PWM that sweeps the duty cycle up and down, producing a nice effect with an LED.
I'm interested in seeing your code for this. I'd implemented this a while ago in order to make a flashing indicator look just a little more 'incandescent', but it took quite a while to get the ramping rates to look right (quicker on the upswing, slower on the decay).



-Don

Posted: 01 July 2008, 15:55 PM
by dkinzer
Don_Kirby wrote:I'm interested in seeing your code for [sweeping the duty cycle].
My code is pretty simple minded. See the attached project which is intended for a 24-pin ZX.

Posted: 02 July 2008, 13:46 PM
by dkinzer
I've posted an update to the 8-bit PWM routines, replacing the old .zip file with the new one. To get the update, download the .zip again from the first post in this thread. It is identified as v1.1 near the top of the PWM8.bas file.

In addition to implementing some of Mike's suggestions, I also fixed the conditional problem for the mega32 - the code has now been tested on a ZX-24. I also added a second test to the test driver which runs in a loop sweeping the duty cycle from 0% to 100% and back again. Using a conditional, you can compile with one or the other of the tests.

Posted: 02 July 2008, 16:54 PM
by dkinzer
The routine below can be used to calculate the clock selector index to be used as the second parameter to OpenPWM8() given a desired PWM frequency and PWM mode.

Code: Select all

'
'' clockSel
'
' Compute the clock selector to realize the PWM frequency as close as possible to the
' target frequency.  The returned clock selector, if non-zero, can be used as the second
' parameter to OpenPWM8().
'
' Parameter    Description
' ----------   -------------------------------------------------------------
' targFreq     Specifies the desired PWM frequency.  If the target frequency
'              is an invalid Single value, is zero, is negative or is larger
'              than the CPU clock frequency, zero will be returned.  Otherwise,
'              the clock selector that produces the PWM frequency closest to
'              the target frequency for the specified mode will be returned.
'
' pwmMode      Specifies the PWM mode:
'                   Fast PWM (zxFastPWM) = 0
'                   Phase/Frequency Correct PWM (zxFastPWM) = 1
'
Private Function clockSel(ByVal targFreq as Single, ByVal pwmMode as Byte) as Byte
  ' This table gives the valid clock divisors and, implicitly, the
  ' the corresponding clock selector index.
  #if Option.CPUType = "mega32" Or Option.CPUType = "mega644" Or Option.CPUType = "mega644P"
    Dim clockDivTbl as IntegerVectorData({ 1, 8, 32, 64, 128, 256, 1024 })
  #elseif Option.CPUType = "mega128" Or Option.CPUType = "mega1281" Or Option.CPUType = "mega1280"
    Dim clockDivTbl as IntegerVectorData({ 1, 8, 64, 256, 1024 })
  #else
    ' unrecognized processor type
    #error need processor-specific code
  #endif

  clockSel = 0
  Dim cpuFreq as Single
  cpuFreq = CSng(Register.CPUFrequency)

  ' validate the parameters
  pwmMode = pwmMode And &H7f
  If &#40;&#40;SngClass&#40;targFreq&#41; <> ClassNormal&#41; Or &#40;pwmMode > zxCorrectPWM&#41;&#41; Then
    Exit Function
  End If

  ' calculate the divide-by-1 PWM frequency for the requested mode
  Dim pwmFreq as Single
  If &#40;pwmMode = zxFastPWM&#41; Then
    pwmFreq = cpuFreq / 255.0
  Else
    pwmFreq = cpuFreq / 510.0
  End If

  ' find the divisor that yields a PWM frequency closest to the requested frequency
  Dim i as Integer
  Dim lastDelta as Single
  Dim lastSel as Byte = 0
  lastDelta = cpuFreq
  For i = 1 to UBound&#40;clockDivTbl&#41;
    Dim testFreq as Single, delta as Single
    
    ' Compute the frequency corresponding to this clock selector and
    ' the difference from the target frequency.
    testFreq = pwmFreq / CSng&#40;clockDivTbl&#40;i&#41;&#41;
    delta = Abs&#40;targFreq - testFreq&#41;
    
    ' see if this difference is smaller than for the last clock selector
    If &#40;delta >= lastDelta&#41; Then
      Exit For
    End If
    
    ' save the values for the next iteration
    lastSel = CByte&#40;i&#41;
    lastDelta = delta
  Next i
  
  ' use the highest selector tested
  clockSel = lastSel
End Function

Posted: 02 July 2008, 17:17 PM
by mikep
dkinzer wrote:I also added a second test to the test driver which runs in a loop sweeping the duty cycle from 0% to 100% and back again.
I cannot help improving good things and making them even better. In general the brightness of a LED is not linearly proportional to the PWM value. Here is some code that attempts to adjust for that. It assumes that the LED is connected as a current source (0 means on, 1 means off).

Code: Select all

'********** duty cycle sweep test case *************
	Call OpenPWM8&#40;channel, 3, zxFastPWM, stat&#41;
	Const DutyChange as Integer = 1
	Const sleepInterval as Single = 0.05
	Const maxDuty as Integer = 255
	Const minDuty as Integer = 0
	Dim duty as Integer
	Dim delta as Integer
	duty = 0
	delta = DutyChange
	Call SetInterval&#40;sleepInterval&#41;
	Do
		Call PWM8&#40;channel, LoByte&#40;maxDuty-duty&#41;, stat&#41;
		Call WaitForInterval&#40;0&#41;
		If delta = 1 Then
			duty = duty * 2 + 1
		Else
			duty = &#40;duty - 1&#41;\2
		End If
		If &#40;duty >= maxDuty&#41; Then
			duty = maxDuty
			delta = -DutyChange
		ElseIf &#40;duty <= minDuty&#41; Then
			duty = minDuty
			delta = DutyChange
		End If
	Loop
The variable "delta" is better named direction and made a boolean but I tried to keep as much of the original code intact. Notice that with this technique there are only 9 different brightnesses but at least the LED doesn't look like it is almost on all of the time.

Posted: 02 July 2008, 17:30 PM
by dkinzer
mikep wrote:Notice that with this technique there are only 9 different brightnesses [...]
It occurs to me that you could more efficiently employ a data table giving the PWM duty cycle for each step. Moreover, that would allow you to implement virtually any response curve you'd like. You could even use a different curve for ramping up than for ramping down or add more elements in one direction than in the other.

8-bit PWM

Posted: 02 July 2008, 18:12 PM
by GTBecker
... the LED doesn't look like it is almost on all of the time.
For viewing a throbbing LED, I've found a log curve works better than
linear changes - and simply shifting a bit works pretty well - if you
don't need linear resolution like you might if you were measuring the
optical density of a liquid.


Tom

Re: 8-bit PWM

Posted: 02 July 2008, 19:00 PM
by mikep
GTBecker wrote:For viewing a throbbing LED, I've found a log curve works better than linear changes - and simply shifting a bit works pretty well..
That's essentially what my code does. The values are 0 and 2^n-1 where n is between 1 and 8 i.e. 0, 1, 3, 7, 15...255.

I could have also done a data table as Don suggests with values on a log or even match the values of the LED brightness curve. However an 8-bit PWM doesn't have much resolution so I just opted for a simple improvement to the existing code to make the non-linear brightness look a little better.

Posted: 04 July 2008, 3:03 AM
by sturgessb
Great job on this Don.

Quick question. You mention Timer 2 is 'Serial Timer', does this mean if Timer 2 is used for PWM, there are problems with using UARTS?

Ah scrub that, I see they are just used for Software UARTS.

Many thanks.