8 output PWM

Discuss PIC and electronic related things

Moderators: David Barker, Jerry Messina

richardb
Posts: 310
Joined: Tue Oct 03, 2006 8:54 pm

8 output PWM

Post by richardb » Tue May 04, 2010 10:33 am

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 ?
Hmmm..

User avatar
octal
Registered User
Registered User
Posts: 586
Joined: Thu Jan 11, 2007 12:49 pm
Location: Paris IDF
Contact:

Post by octal » Tue May 04, 2010 10:49 am

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

richardb
Posts: 310
Joined: Tue Oct 03, 2006 8:54 pm

Post by richardb » Tue May 04, 2010 11:18 am

hardware pwm would be nice but not possible for 8 outputs as far as i'm aware.


Ive never used the timers but i was expecting to use them for this .

if i setup the timer to generate an interrupt every 300Hz can i then monitor the counter without affecting the timer in my main program ?
Hmmm..

Jerry Messina
Swordfish Developer
Posts: 1473
Joined: Fri Jan 30, 2009 6:27 pm
Location: US

Post by Jerry Messina » Tue May 04, 2010 6:18 pm

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)
Last edited by Jerry Messina on Tue May 04, 2010 7:11 pm, edited 1 time in total.

Francis
Registered User
Registered User
Posts: 314
Joined: Sun Mar 25, 2007 9:40 am
Location: Devon

Post by Francis » Tue May 04, 2010 6:23 pm

If the code doesn't have to do much else you can do a slightly clunky version using a loop.
Is it for RGB control or similar?

richardb
Posts: 310
Joined: Tue Oct 03, 2006 8:54 pm

Post by richardb » Tue May 04, 2010 8:04 pm

i'll expand a little :)
I need 8 bits of resolution to change the brightness of high power led arrays used for general lighting and for lighing specific areas under cameras.

The pwm frequency needs to run at 300Hz so that its a multiple of either 50 or 60hz cameras.
Hmmm..

Jerry Messina
Swordfish Developer
Posts: 1473
Joined: Fri Jan 30, 2009 6:27 pm
Location: US

Post by Jerry Messina » Tue May 04, 2010 8:57 pm

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.

Jerry Messina
Swordfish Developer
Posts: 1473
Joined: Fri Jan 30, 2009 6:27 pm
Location: US

Post by Jerry Messina » Tue May 04, 2010 11:42 pm

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.

User avatar
RangerBob
Posts: 152
Joined: Thu May 31, 2007 8:52 am
Location: Beds, UK

Post by RangerBob » Wed May 05, 2010 9:48 am

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. :roll:

Jerry Messina
Swordfish Developer
Posts: 1473
Joined: Fri Jan 30, 2009 6:27 pm
Location: US

Post by Jerry Messina » Wed May 05, 2010 10:44 am

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.

rmteo
Posts: 237
Joined: Fri Feb 29, 2008 7:02 pm
Location: Colorado, USA

Post by rmteo » Wed May 05, 2010 2:35 pm

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

richardb
Posts: 310
Joined: Tue Oct 03, 2006 8:54 pm

Post by richardb » Wed May 05, 2010 10:32 pm

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.
Hmmm..

richardb
Posts: 310
Joined: Tue Oct 03, 2006 8:54 pm

Post by richardb » Thu May 06, 2010 11:21 am

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?


Code: Select all

Public Sub SInterrupt()
       OnISR()
       TMR0H = FPreload.Byte1
       TMR0L = FPreload.Byte0
       ClearInterrupt()
End Sub
This was my test code in the end.

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
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



:)
Hmmm..

Jerry Messina
Swordfish Developer
Posts: 1473
Joined: Fri Jan 30, 2009 6:27 pm
Location: US

Post by Jerry Messina » Thu May 06, 2010 3:13 pm

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.

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
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

richardb
Posts: 310
Joined: Tue Oct 03, 2006 8:54 pm

Post by richardb » Wed May 12, 2010 9:26 am

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.

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
I'm running it on a pic milennium board from bluebird electronics that i know works

any ideas?
Hmmm..

Post Reply