8-bit PWM
8-bit PWM
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]
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]
- Attachments
-
- PWM8.zip
- 8-bit PWM routines.
- (3.68 KiB) Downloaded 4247 times
Last edited by dkinzer on 02 July 2008, 13:47 PM, edited 1 time in total.
- Don Kinzer
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
2. I prefer return codes (stat) to be returned rather than a by reference parameter i.e.
3. The comment header for each routine should document each parameter, trange of valid values and return value if any. For example
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.
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"
Code: Select all
Function OpenPWM8(ByVal chan as Byte, ByVal prescaler as Byte, ByVal mode as Byte) as Boolean
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
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
Mike Perks
Actually, the nested conditional should read:mikep wrote:The second #ifdef code seems to be redundant.
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
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:I prefer return codes (stat) to be returned rather than a by reference parameter
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:The clockSelect parameter for OpenPWM doesn't match the 16-bit API.
Actually, timerSem is just an alias for Register.Timer0Busy or Register.Timer2Busy as needed.mikep wrote:Is it possible to set the Register.Timer2Busy flag? If so then timerSem can be eliminated (which is not really a semaphore).
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:What is the purpose of chanCount variable? Why not simply use the CHANNEL_COUNT constant?
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.mikep wrote:I changed the test code to loop around adjusting the PWM duty cycle [...]
- Don Kinzer
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).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.
-Don
My code is pretty simple minded. See the attached project which is intended for a 24-pin ZX.Don_Kirby wrote:I'm interested in seeing your code for [sweeping the duty cycle].
- Attachments
-
- sweepPWM.zip
- (497 Bytes) Downloaded 3462 times
- Don Kinzer
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.
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.
- Don Kinzer
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
- Don Kinzer
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).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.
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
Mike Perks
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.mikep wrote:Notice that with this technique there are only 9 different brightnesses [...]
- Don Kinzer
8-bit PWM
For viewing a throbbing LED, I've found a log curve works better than... the LED doesn't look like it is almost on all of the time.
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
Tom
Re: 8-bit PWM
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.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..
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.
Mike Perks