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 ((SngClass(targFreq) <> ClassNormal) Or (pwmMode > zxCorrectPWM)) Then
Exit Function
End If
' calculate the divide-by-1 PWM frequency for the requested mode
Dim pwmFreq as Single
If (pwmMode = zxFastPWM) 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(clockDivTbl)
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(clockDivTbl(i))
delta = Abs(targFreq - testFreq)
' see if this difference is smaller than for the last clock selector
If (delta >= lastDelta) Then
Exit For
End If
' save the values for the next iteration
lastSel = CByte(i)
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(channel, 3, zxFastPWM, stat)
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(sleepInterval)
Do
Call PWM8(channel, LoByte(maxDuty-duty), stat)
Call WaitForInterval(0)
If delta = 1 Then
duty = duty * 2 + 1
Else
duty = (duty - 1)\2
End If
If (duty >= maxDuty) Then
duty = maxDuty
delta = -DutyChange
ElseIf (duty <= minDuty) 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.