8 output PWM
Moderators: David Barker, Jerry Messina
8 output PWM
Does anyone have any suggestions on how to do a pwm output at a fairly low frequency on multiple outputs in software?
i only need to run at about 300 or 600Hz and i would probably use a 48MHz pic something like a 2550.
any suggestions ?
i only need to run at about 300 or 600Hz and i would probably use a 48MHz pic something like a 2550.
any suggestions ?
Hmmm..
pwm hardware module has the advantage of being able to work in background without the mcu intevention.
If you need to do it in software at low freq on multiple outputs, you can use a timer interrupt and make a full state machine implementation, ie. you describe your states (states changes exactly) in a state table, and the code in the interrupt has to switch the output state depending on the active state.
Regards
If you need to do it in software at low freq on multiple outputs, you can use a timer interrupt and make a full state machine implementation, ie. you describe your states (states changes exactly) in a state table, and the code in the interrupt has to switch the output state depending on the active state.
Regards
-
- Swordfish Developer
- Posts: 1473
- Joined: Fri Jan 30, 2009 6:27 pm
- Location: US
How many bits of resolution are you looking for?
If you're looking to use a single timer, I would think you'd have to run it a lot faster than 300 hz so you can get the variable duty cycle... basically PWM freq/number of bits resolution.
EDIT: oops...said that wrong...
timer would run at freq = PWM freq * number of steps (which is 2^number of bits)
If you're looking to use a single timer, I would think you'd have to run it a lot faster than 300 hz so you can get the variable duty cycle... basically PWM freq/number of bits resolution.
EDIT: oops...said that wrong...
timer would run at freq = PWM freq * number of steps (which is 2^number of bits)
Last edited by Jerry Messina on Tue May 04, 2010 7:11 pm, edited 1 time in total.
-
- Swordfish Developer
- Posts: 1473
- Joined: Fri Jan 30, 2009 6:27 pm
- Location: US
Well, if you only have one timer to use and you want to generate the 8 outputs via software....
300Hz = 3.333ms period
3.333ms/256 steps = 13us timer tick
So, you'd have to be able to handle an interrupt every 13us, decrement and test 8 byte-sized counter variables(256 steps), and set the output pins to the appropriate 0/1 level.
At 48MHz, you'd have ~156 clock cycles to do this, or basically 19-20 instructions per pwm output.
Doesn't sound very realistic with this approach. It might be doable, but you definitely wouldn't be doing much else.
300Hz = 3.333ms period
3.333ms/256 steps = 13us timer tick
So, you'd have to be able to handle an interrupt every 13us, decrement and test 8 byte-sized counter variables(256 steps), and set the output pins to the appropriate 0/1 level.
At 48MHz, you'd have ~156 clock cycles to do this, or basically 19-20 instructions per pwm output.
Doesn't sound very realistic with this approach. It might be doable, but you definitely wouldn't be doing much else.
-
- Swordfish Developer
- Posts: 1473
- Joined: Fri Jan 30, 2009 6:27 pm
- Location: US
Approach #2:
Instead of working directly with the 8 duty cycle registers, when you change the settings you could make a second working array that contained the duty cycles sorted from highest to lowest value. Once sorted, go through the array and just keep the difference from the previous entry. To start things, set all output pins to 1. Start at the top of the list, and every intr decrement the duty cycle register at the top of the list. When that duty cycle decrements to 0, set it's output pin to 0, and move on to the next entry. That way, you're only dealing with one counter and one output at a time.
If you have all the output pins on a single port, then you could keep a second array(that you sort along with the first one) that holds a bitmask for that output pin, and just combine it with the current output port setting when the duty cycle = 0
The trick would be implementing this in an efficient fashion...with such a rapid interrupt frequency you wouldn't want to be doing very much context saving. You could probably reduce it to using two of the FSR registers for the array accesses.
There are a couple of corner cases that would complicate things, like when multiple outputs have the same duty cycle value. There are simple ways to deal with this if you don't mind a little inaccuracy in the output in these cases.
Instead of working directly with the 8 duty cycle registers, when you change the settings you could make a second working array that contained the duty cycles sorted from highest to lowest value. Once sorted, go through the array and just keep the difference from the previous entry. To start things, set all output pins to 1. Start at the top of the list, and every intr decrement the duty cycle register at the top of the list. When that duty cycle decrements to 0, set it's output pin to 0, and move on to the next entry. That way, you're only dealing with one counter and one output at a time.
If you have all the output pins on a single port, then you could keep a second array(that you sort along with the first one) that holds a bitmask for that output pin, and just combine it with the current output port setting when the duty cycle = 0
The trick would be implementing this in an efficient fashion...with such a rapid interrupt frequency you wouldn't want to be doing very much context saving. You could probably reduce it to using two of the FSR registers for the array accesses.
There are a couple of corner cases that would complicate things, like when multiple outputs have the same duty cycle value. There are simple ways to deal with this if you don't mind a little inaccuracy in the output in these cases.
Stupid Question time - but does it absolutely have to be in software?
You could use something like a MAX7313 which would give you 16 individual PWM ports on a simple I2C control interface. Price is <$2.
Failing that, much as I hate to suggest it, but some Atmel ATmegas have > 8 PWM outputs.
You could use something like a MAX7313 which would give you 16 individual PWM ports on a simple I2C control interface. Price is <$2.
Failing that, much as I hate to suggest it, but some Atmel ATmegas have > 8 PWM outputs.
-
- Swordfish Developer
- Posts: 1473
- Joined: Fri Jan 30, 2009 6:27 pm
- Location: US
My only problem with the MAX7313 is that it's a Maxim part, which usually means you'll never be able to actually get a hold of them except as sample quantities, or wait 26 weeks. They have a great selection of datasheets... just about anything under the sun. If they'd only have a production dept as big as their tech pubs dept, they'd be at the top of my list.
There are some pics with 8 pwm channels, but currently they're only PIC24/PIC33's.
There is a PIC18F66K22 (and some others) coming, but they're not available yet. The most I see currently in production is 4 channels, so you'd have to use two of them.
There are some pics with 8 pwm channels, but currently they're only PIC24/PIC33's.
There is a PIC18F66K22 (and some others) coming, but they're not available yet. The most I see currently in production is 4 channels, so you'd have to use two of them.
Another possibility is the PCA9532. Max. PWM frequency is 152Hz - you can easily set it up to run at a selectable 100 or 120Hz. Available for <$2 from Mouser, Digikey, Future, etc.
The PCA9532 is a 16-bit I2C-bus and SMBus I/O expander optimized for dimming LEDs in 256 discrete steps for Red/Green/Blue (RGB) color mixing and back light applications.
The PCA9532 contains an internal oscillator with two user programmable blink rates and duty cycles coupled to the output PWM. The LED brightness is controlled by setting the blink rate high enough (> 100 Hz) that the blinking cannot be seen and then using the duty cycle to vary the amount of time the LED is on and thus the average current through the LED.
* 16 LED drivers (on, off, flashing at a programmable rate)
* Two selectable, fully programmable blink rates (frequency and duty cycle) between 0.591 Hz and 152 Hz (1.69 second and 6.58 milliseconds)
* 256 brightness steps
* Input/outputs not used as LED drivers can be used as regular GPIOs
* Internal oscillator requires no external components
* I2C-bus interface logic compatible with SMBus
* Internal power-on reset
Wow! lots to reply to
Jerry,
Approach 1 always looked to hard.
approach 2 looks much more workable.
I've setup my first program using timer 0 today and it seems to generate events correctly at 300Hz.
is it possible to read the value of the counter in my main program without affecting the counter?
my test program seemed to go wrong as soon as i read TMR0L i assume it resets the counter but it didn’t seem obvious from the datasheet that this would happen.
Rangerbob,
It doesn’t have to be in software….. But I’d prefer a single chip solution that can talk to a pc, and I would like the option to be able to add a high res adc to data back too(I know I didn’t mention it but it wasn't important)
Most importantly, this is a pic project and its as much about getting me to use the pic fully as it is about getting any kind of solution.
I could use a cpld but I want to use the pic .
I agree with jerry about getting max parts too, if I can’t get a chip from farnell or rs I think very carefully about using it.
Jerry,
Approach 1 always looked to hard.
approach 2 looks much more workable.
I've setup my first program using timer 0 today and it seems to generate events correctly at 300Hz.
is it possible to read the value of the counter in my main program without affecting the counter?
my test program seemed to go wrong as soon as i read TMR0L i assume it resets the counter but it didn’t seem obvious from the datasheet that this would happen.
Rangerbob,
It doesn’t have to be in software….. But I’d prefer a single chip solution that can talk to a pc, and I would like the option to be able to add a high res adc to data back too(I know I didn’t mention it but it wasn't important)
Most importantly, this is a pic project and its as much about getting me to use the pic fully as it is about getting any kind of solution.
I could use a cpld but I want to use the pic .
I agree with jerry about getting max parts too, if I can’t get a chip from farnell or rs I think very carefully about using it.
Hmmm..
First of all, thanks to Darryl Quinn for providing the timer0 module on the SF site,
http://www.sfcompiler.co.uk/wiki/pmwiki ... ser.Timer0
I managed to get a 300Hz square wave working straight away, but went a little odd when trying to update the Timer0.Preload reg within the event routine.
This seems to be down to a bug in the module. it was loading TMR0L first, i think it should be as below. Can someone confirm this, also can anyone update the wiki?
This was my test code in the end.
In this case led1 runs at 25% duty cycle and led2 runs at 50%
To run at 300 Hz I just need to make sure the total counts add up to 40000
In the final code I'll move the calculations to the main program
http://www.sfcompiler.co.uk/wiki/pmwiki ... ser.Timer0
I managed to get a 300Hz square wave working straight away, but went a little odd when trying to update the Timer0.Preload reg within the event routine.
This seems to be down to a bug in the module. it was loading TMR0L first, i think it should be as below. Can someone confirm this, also can anyone update the wiki?
Code: Select all
Public Sub SInterrupt()
OnISR()
TMR0H = FPreload.Byte1
TMR0L = FPreload.Byte0
ClearInterrupt()
End Sub
Code: Select all
Event LEDFlash()
counter = counter +1
Select counter
Case 1
LED1=1
LED2=1
Timer0.Preload=65535-10000
Case 2
LED1=0
Timer0.Preload=65535-10000
Case 3
LED2=0
Timer0.Preload=65535-20000
counter = 0
EndSelect
End Event
To run at 300 Hz I just need to make sure the total counts add up to 40000
In the final code I'll move the calculations to the main program
Hmmm..
-
- Swordfish Developer
- Posts: 1473
- Joined: Fri Jan 30, 2009 6:27 pm
- Location: US
I ran across a MUCH simpler implementation on the atmel site. They have an app note (AVR136) that describes doing software PWM. I adapted the code for SF, and simplified it a bit so it'd be faster (they run theirs at ~120Hz).
I don't have any hardware with me at the moment, but I ran it in the mplab simulator and it seems to work darn good! At 48 Mhz, the ISR consumes about 25% of the available time.
The code might need some tweaks of the timer values and such, but it's quite workable. As it stands, it uses TMR0, and requires the 8 pwm outputs to be on the same port (done for speed). Get the original app note and code for an explanation of how it works.
just call spwm.init(), and it starts running
EDIT: according to the logic analyzer in mplab, setting TIMER_RELOAD_OVERHEAD = 10 seems to adjust it pretty close to 300Hz
I don't have any hardware with me at the moment, but I ran it in the mplab simulator and it seems to work darn good! At 48 Mhz, the ISR consumes about 25% of the available time.
The code might need some tweaks of the timer values and such, but it's quite workable. As it stands, it uses TMR0, and requires the 8 pwm outputs to be on the same port (done for speed). Get the original app note and code for an explanation of how it works.
Code: Select all
module spwm
// select or deselect ISR Timing Debugging
#define ENABLE_PWM_DEBUG = true
#option _PWM_OUTPUT_PORT = PORTB
#option _PWM_OUTPUT_TRIS = GetTris(_PWM_OUTPUT_PORT)
public const
ipLow = 1,
ipHigh = 2
// maximum number of PWM channels
const CHMAX = 8
// default PWM value at start up for all channels
const PWMDEFAULT = $80
// Pin mappings
dim PWM_OUTPUT_PORT as _PWM_OUTPUT_PORT,
PWM_OUTPUT_TRIS as _PWM_OUTPUT_TRIS
const
PWM_CH0 = 0,
PWM_CH1 = 1,
PWM_CH2 = 2,
PWM_CH3 = 3,
PWM_CH4 = 4,
PWM_CH5 = 5,
PWM_CH6 = 6,
PWM_CH7 = 7
// Set bits corresponding to pin usage above
const PWM_PORT_MASK =
(1 << PWM_CH0) +
(1 << PWM_CH1) +
(1 << PWM_CH2) +
(1 << PWM_CH3) +
(1 << PWM_CH4) +
(1 << PWM_CH5) +
(1 << PWM_CH6) +
(1 << PWM_CH7)
//
// debug pin controls
//
#if (ENABLE_PWM_DEBUG)
dim DEBUG_PIN as PORTC.0
inline sub DEBUGPIN_ON()
DEBUG_PIN = 1
end sub
inline sub DEBUGPIN_OFF()
DEBUG_PIN = 0
end sub
inline sub DEBUGSET()
low(DEBUG_PIN)
end sub
#endif
// global buffers
dim compare(CHMAX) as byte
dim compbuff(CHMAX) as byte
// vars used by isr
dim pwm_output_reg as byte
dim pwm_count as byte
//
//------------------------------------------------------------------------------
// timer setup
//------------------------------------------------------------------------------
//
// tmr0 is used as the pwm timer
// pwm freq = 300Hz = 3.333ms period
// 3.333ms/256 pwm steps = 13.020us per tick (76.8kHz)
//
const TIMER_RELOAD_OVERHEAD = 0
// 76.8kHz @ 48 MHz clock
#if (_clock = 48)
const TIMER_RELOAD_VALUE as word = 65380 + TIMER_RELOAD_OVERHEAD
#elseif (_clock = 40)
const TIMER_RELOAD_VALUE as word = 65406 + TIMER_RELOAD_OVERHEAD
#else
#error "compute timer value for clock freq"
#endif
dim
T0CON_T0PS0 as T0CON.0,
T0CON_T0PS1 as T0CON.1,
T0CON_T0PS2 as T0CON.2,
T0CON_PSA as T0CON.3,
T0CON_T0SE as T0CON.4,
T0CON_T0CS as T0CON.5,
T0CON_T08BIT as T0CON.6,
T0CON_TMR0ON as T0CON.7
dim
TMR0IP as INTCON2.2,
TMR0IF as INTCON.2,
TMR0IE as INTCON.5
dim
TMR0 as TMR0L.AsWord
public sub init_pwm_timer()
// Timer0 Registers: 16-Bit Mode; Prescaler=1:1; Freq=76.92kHz; Period=13,000 ns
T0CON_TMR0ON = 0 // Timer0 On/Off Control bit:1=Enables Timer0 / 0=Stops Timer0
T0CON_T08BIT = 0 // Timer0 8-bit/16-bit Control bit: 1=8-bit timer/counter / 0=16-bit timer/counter
T0CON_T0CS = 0 // TMR0 Clock Source Select bit: 0=Internal Clock (CLKO) / 1=Transition on T0CKI pin
T0CON_T0SE = 0 // TMR0 Source Edge Select bit: 0=low/high / 1=high/low
T0CON_PSA = 1 // Prescaler Assignment bit: 0=Prescaler is assigned; 1=NOT assigned/bypassed
T0CON_T0PS2 = 0 // bits 2-0 PS2:PS0: Prescaler Select bits
T0CON_T0PS1 = 0
T0CON_T0PS0 = 0
// in 16-bit mode write timer highbyte, then lowbyte
TMR0 = TIMER_RELOAD_VALUE
// clear timer IF
TMR0IF = 0
// set high priority and enable the timer intr
TMR0IP = 1
TMR0IE = 1
// turn on timer
T0CON_TMR0ON = 1
end sub
//
//------------------------------------------------------------------------------
// interrupt service routine
//------------------------------------------------------------------------------
//
interrupt pwmISR(ipHigh)
// reload the timer. make sure to write highbyte then lowbyte
TMR0 = TIMER_RELOAD_VALUE
// clear timer IF
TMR0IF = 0
PWM_OUTPUT_PORT = pwm_output_reg // update outputs
#if (ENABLE_PWM_DEBUG)
DEBUGPIN_ON() // set debug pin
#endif
// increment modulo 256 counter and update
// the compare values only when counter = 0
inc(pwm_count)
if (pwm_count = 0) then
compare(0) = compbuff(0) // verbose code for speed
compare(1) = compbuff(1)
compare(2) = compbuff(2)
compare(3) = compbuff(3)
compare(4) = compbuff(4)
compare(5) = compbuff(5)
compare(6) = compbuff(6)
compare(7) = compbuff(7) // last element should equal (CHMAX-1)
// set all port pins high (next time through)
pwm_output_reg = PWM_PORT_MASK
endif
// clear port pin on compare match (executed on next interrupt)
if (compare(0) = pwm_count) then
pwm_output_reg.bits(PWM_CH0) = 0
endif
if (compare(1) = pwm_count) then
pwm_output_reg.bits(PWM_CH1) = 0
endif
if (compare(2) = pwm_count) then
pwm_output_reg.bits(PWM_CH2) = 0
endif
if (compare(3) = pwm_count) then
pwm_output_reg.bits(PWM_CH3) = 0
endif
if (compare(4) = pwm_count) then
pwm_output_reg.bits(PWM_CH4) = 0
endif
if (compare(5) = pwm_count) then
pwm_output_reg.bits(PWM_CH5) = 0
endif
if (compare(6) = pwm_count) then
pwm_output_reg.bits(PWM_CH6) = 0
endif
if (compare(7) = pwm_count) then
pwm_output_reg.bits(PWM_CH7) = 0
endif
#if (ENABLE_PWM_DEBUG)
DEBUGPIN_OFF() // clear debug pin
#endif
end interrupt
//
//
//
public sub Init()
dim i, pwm as byte
// set port pins to output
PWM_OUTPUT_TRIS = not PWM_PORT_MASK
// init vars used by isr
pwm_output_reg = PWM_PORT_MASK
pwm_count = $ff
#if (ENABLE_PWM_DEBUG)
DEBUGSET() // make debug pin output
pwm = 0 // worst-case default PWM level
#else
pwm = PWMDEFAULT
#endif
for i = 0 to (CHMAX-1) // initialise all channels
compare(i) = pwm // set default PWM values
compbuff(i) = pwm // set default PWM values
next
#if (ENABLE_PWM_DEBUG)
compare(0) = $01 // make one channel active
compbuff(0) = $01
compare(7) = 127 // make one channel active
compbuff(7) = 127
#endif
init_pwm_timer() // setup the pwm timer
enable(pwmISR) // enable interrupts
end sub
end module
EDIT: according to the logic analyzer in mplab, setting TIMER_RELOAD_OVERHEAD = 10 seems to adjust it pretty close to 300Hz
Hi Jerry sorry for the slow reply, but thanks for posting this, unfortunately my luck is out at the moment.
I tried compiling this at home but it just wont compile on my pc (windows 7 32bit).
At work it compiles fine but just does nothing.
This is my main program.
I'm running it on a pic milennium board from bluebird electronics that i know works
any ideas?
I tried compiling this at home but it just wont compile on my pc (windows 7 32bit).
At work it compiles fine but just does nothing.
This is my main program.
Code: Select all
Device = 18F4550
Clock = 48
Config
PLLDIV = 5,
CPUDIV = OSC1_PLL2,
USBDIV = 2,
FOSC = ECPLLIO_EC,'HSPLL_HS, ''ECPLLIO_EC, ECPLL_EC
LVP = OFF,
VREGEN = ON
Include "spwm.bas"
#option _PWM_OUTPUT_PORT = PORTD
spwm.Init()
while true
wend
any ideas?
Hmmm..