Pulse Width Modulator using PIC16F84a

Thread Starter

farscape

Joined May 16, 2010
26
Hi,

I'm trying to build a Pulse Width Modulator using a PIC16F84a.

If possible, I want one of my inputs to increase the duty factor of the LED every time it is asserted and the other input to decrease the duty factor.

I think I've got a decent schematic drawn up and a rough start on the code, but I'm having trouble coming up with an effecient way to program the inputs so that they continually increase or decrease the duty factor each time one is asserted.

Any help would be appreciated! Thanks


Rich (BB code):
;    cpu equates (memory map)
portB    equ    0x06        
portA    equ    0x05
duty    equ    0x0c


    org    0x000

start    movlw    0x00        
tris      portB        
clrf      portB        
movlw 0x1F        
tris      portA        
clrf      portA

test0   btfsc   portA, '0'
           call    test1
           call    upLED

test1    btfsc    portA, '1'
            goto    test0
            call    downLED


upLED   movlw     d'64'
            movwf     duty
            bsf          portB, '0'
light     decfsz     duty
            goto     light
            clrf     portB
            movlw     d'255
            movwf     duty
blank     decfsz     duty
             goto     blank
             goto     upLED
 

Attachments

jordan22

Joined May 30, 2010
2
not sure yet how you did the whole duty cycle part so i don't know how to get the downLED part but i think this is what I meant about using a loop to make it repeat the process every 250ms

correct me if I'm misunderstanding how this works.

Rich (BB code):
;    cpu equates (memory map)
portB    equ    0x06        
portA    equ    0x05
duty    equ    0x0c


    org    0x000

start    movlw    0x00        
tris      portB        
clrf      portB        
movlw 0x1F        
tris      portA        
clrf      portA

loop     goto pause(250ms)
          test0   btfsc   portA, '0'
               call    test1
               call    upLED
          test1    btfsc    portA, '1'
               goto    test0
               call    downLED
          goto loop

upLED   movlw     d'64'
            movwf     duty
            bsf          portB, '0'
light     decfsz     duty
            goto     light
            clrf     portB
            movlw     d'255
            movwf     duty
blank     decfsz     duty
             goto     blank
             goto     upLED
             return

downLED ...
             return

pause(25ms) ...
             return

end
 

Markd77

Joined Sep 7, 2009
2,806
For phase 2 of the project it would be useful to have a ADC on the PIC. Might be worth considering the 12F675. That has enough pins for what you need, an ADC and an internal oscillator. Should be cheaper than the 84a as well.
 

Thread Starter

farscape

Joined May 16, 2010
26
Okay thanks. I'm not sure whether or not we have access to the 12F675 in our lab... Would the programming for that be much different than with the 84a?
 

Markd77

Joined Sep 7, 2009
2,806
There aren't too many differences, RAM starts at a different location, there are a few extra things to do setting up the oscillator and the ADC. Best to check that the programming hardware supports it before you order one (it probably does).
 

Thread Starter

farscape

Joined May 16, 2010
26
Thanks Mark. Unfortunately, due to my poor planning, we (Jordan22 and I) need to have this done by Thursday so I think we may need to stick with the 84a... We'll be sure to plan this a little better in the future. Hopefully it wont be too much of a headache this way...

I updated what I had for the code and added the pause sequence you suggested Jordan.

In the new code, the inputs will be checked every .25 sec so were hoping we can increase the duty cycle every time the program sees that RA0 is asserted and alternatively, decrease it every time is sees that RA1 is asserted.
-I'm having difficulty seeing how we can program this feature. The way it is right now, if RA0 is asserted it sends the LED to a 25% duty cycle. Is it possible to increase it to 50%, then 75% if RA0 is still asserted when the inputs are checked?

Thanks

Rich (BB code):
cpu equates (memory map)

portB    equ    0x06        
portA    equ    0x05
duty      equ    0x0c
duty1    equ    0x0e

org    0x000

start    movlw    0x00        
tris portB clrf portB movlw 0x1F tris portA clrf portA​
test0 call pause btfsc portA, '0' call test1 call upLED test1 btfsc portA, '1' goto test0 call downLED upLED bsf portB, '0' movlw d'64' movwf duty decfsz duty goto upLED bcf portB, '0' movlw d'192' movwf duty dim decfsz duty goto dim goto upLED pause movlw 0xD3 ;pause is ~.25SEC movwf duty movlw 0x31 movwf duty1 Delay_0 decfsz d1, f goto $+2 decfsz d2, f goto Delay_0 goto $+1 return
 
Last edited:

upand_at_them

Joined May 15, 2010
940
It's been so long since I messed with the F84, but I remember that the TRIS "command" is not the preferred way of doing it. It'll generate warnings, for one thing. The proper way is to BANKSEL TRISA and then write the value to TRISA/TRISB. If you don't want to use BANKSEL you can just set the RP bits in STATUS; BANKSEL just makes it easier.

You also need to debounce the switches.

Before you try to throw everything together, I would get each sub-step working:
* Light an LED.
* Read a switch with debounce, have it just toggle the LED: push on, push off.
* Read a switch every .25 seconds (with debounce).
* Pulse an LED when you press a switch.

Then write the final program. And it would be best to write separate routines for things: UpLED, DownLED, .25s pause, switch debounce pause. You call then and they all have return statements.
 

Markd77

Joined Sep 7, 2009
2,806
Can you post the whole code? I don't see the downLED subroutine.
I think checking every 0.25 seconds means that debouncing is unnecessary.
 

MMcLaren

Joined Feb 14, 2010
861
No need to debounce the switches if you're sampling them at intervals greater than 10-20 msecs.

You might consider something like a simple isochronous loop. Use something like 256 pwm steps (8-bit resolution) and 50-usecs per step for an overall 12.8-msec period and 78.125-Hz refresh rate. Setup another timer within your 50-usec pwm "step" loop to detect 250-msec intervals and sample the switches and adjust the LED PWM duty cycle then. This would provide a 250-msec repeat rate when you press and hold the push button switches. Do you need to see a "cycle accurate" DelayCy() routine?

Cheerful regards, Mike

Rich (BB code):
led     equ     0x28            ; led duty cycle, 0..255
dcy     equ     0x29            ; duty cycle counter, 0..255
tmrl    equ     0x2A            ; 250-msec switch timer
tmrh    equ     0x2B            ;
shadow  equ     0x2C            ; PORTB shadow reg'

        radix   dec
;
;
;
Start
        movlw   low(250000/50)  ; setup 250-msec timer            |B0
        movwf   tmrl            ;                                 |B0
        movlw   high(250000/50) ;                                 |B0
        movwf   tmrh            ;                                 |B0
        clrf    dcy             ; clear duty cycle counter        |B0
        movlw   0x80            ;                                 |B0
        movwf   led             ; led duty cycle = 128 (50%)      |B0
;
;  256 50-usec steps -> 12.8-msec period, 78.125-Hz refresh rate
;
loop0
        DelayCy(50*usecs-15)    ; maintain 50-usec "step" time    |B0
loop1
        clrf    shadow          ;                                 |B0
        movf    led,W           ; led duty cycle, 0..255          |B0
        subwf   dcy,W           ; C = (dcy >= led)                |B0
        rlf     shadow,W        ; file = 0000000R (active lo)     |B0
;       xorlw   1               ; invert for active hi output     |B0
        movwf   PORTB           ; update led                      |B0
        incf    dcy,F           ; bump duty cycle counter         |B0 <---
        movf    tmrl,W          ;                                 |B0
        skpnz                   ;                                 |B0
        decf    tmrh,F          ;                                 |B0
        decf    tmrl,F          ;                                 |B0
        movf    tmrl,W          ;                                 |B0
        iorwf   tmrh,W          ; 250-msec interval?              |B0
        skpz                    ; yes, skip, else                 |B0
        goto    loop0           ; branch (loop)                   |B0
;
;  sample active hi "up" and "dn" switches and modify duty cycle
;
        movlw   low(250000/50)  ; reset 250-msec timer            |B0
        movwf   tmrl            ;                                 |B0
        movlw   high(250000/50) ;                                 |B0
        movwf   tmrh            ;                                 |B0
        movf    led,W           ; duty cycle == 0?                |B0
        skpz                    ; yes, skip, else                 |B0
        decf    led,W           ; wreg = led - 1                  |B0
        btfsc   PORTA,1         ; Dn sw pressed? no, skip, else   |B0
        movwf   led             ; update duty cycle               |B0
        incf    led,W           ; duty cycle == 255?              |B0
        skpnz                   ; no, skip, else                  |B0
        movf    led,W           ; wreg = 255                      |B0
        btfsc   PORTA,0         ; Up sw pressed? no, skip, else   |B0
        movwf   led             ; update duty cycle               |B0
        DelayCy(50*usecs-30)    ; maintain 50-usec "step" time    |B0
        goto    loop1           ;                                 |B0
;
 
Last edited:

Thread Starter

farscape

Joined May 16, 2010
26
I didn't include the downLED loop because that is where I am kind of lost. I'm stumped on how to go about adjusting the duty cycle when additional inputs are asserted after an initial state is already in place.
-I guess, my upLED code is probably totally wrong since it has no associated adjustment to an already existing duty cycle.
-Ill keep trying to figure this part out


MMcLaren, I'm trying to understand your code, but there are a few things I'm struggling with.
I don't want to be a pain, but would you mind briefly walking me through what is happening in loop1?
-Where does the adjustment in duty cycle take place?
Is it here? "subwf dcy,W"
 

Markd77

Joined Sep 7, 2009
2,806
Loop1/loop0 is supposed to count up or down "dcy" until it matches "led" which is the duty cycle set in the last part of the program which happens every quarter of a second.
Some of it deals with updateing the 16 bit timer (trml and tmrh).
I have a feeling there should be a decf dcy, or incf dcy somewhere.
 

Thread Starter

farscape

Joined May 16, 2010
26
Is it possible to use a table to read off values for another register I create that records what level the duty should be at? I'm going to try to write the code, but I wanted to check and see if I am on the right track.

I'm thinking that every time the program sees one of the inputs is asserted, it either adds "1" or subtracts "1" from an address I can call "level" and if this address has a value of 2 for instance, the output will be at a 50% duty cycle.

If the program then recognizes that the opposite input has been asserted, then it subtracts "1" from the "level" address and reads from the table that it should now be at a 25% duty.

Can table reads be used in this way? I'll work on it and try to get some code up as soon as I can.

Thanks
 

MMcLaren

Joined Feb 14, 2010
861
Loop1/loop0 is supposed to count up or down "dcy" until it matches "led" which is the duty cycle set in the last part of the program which happens every quarter of a second.
Some of it deals with updateing the 16 bit timer (trml and tmrh).
I have a feeling there should be a decf dcy, or incf dcy somewhere.
Good catch! I added the errant incf dcy,F instruction...

farscape,

I used the "led" variable for the duty cycle value in that example program. It holds an 8-bit duty cycle value of 0 (off or 0%) through 255 (100%). Each time we go through the 50-usec "step" loop we compare the duty cycle counter value with the "led" duty cycle value and we turn off the led output when the counter value is equal to or greater than the "led" duty cycle. If the "led" variable contains a value of 2 the led will be lighted for 2 50-usec "steps" and turned 'off' for 254 50-usec "steps". The "dcy" duty cycle counter variable is incremented at the end of each 50-usec "step" loop and rolls over from 255 to 0.

I use the "tmrl" and "tmrh" variables to count off 250000 usecs worth of 50-usec loops. So, every 250-msecs we venture outside of the 50-usec PWM "step" loop to sample the switches and increment or decrement the "led" duty cycle value accordingly.

The code may look a bit funky because it's been written to be isochronous. That is, it uses the same number of instruction cycles to get through the code wether the switches are pressed or released. Isochronous code is a pain if you need to add another code feature or function but it can be very useful for some time critical applications, for example, a Sony SIRC controlled (8-bit PWM) RGB Accent Lighting controller running on a 6-pin 10F200.

Regards, Mike
 
Last edited:

Thread Starter

farscape

Joined May 16, 2010
26
Thanks Mike, Ill take a closer look and see if I can follow now.

I rewrote my code this afternoon using the table idea I had above... I hope its not too tedious to follow but maybe if I layout my basic ideas it will be less of a pain.

This is what I hope the program does so far...

-When the program starts, it loops through "test0" and "test1" until one input is asserted and it then jumps to "upLED" (if input 0 is asserted) or "downLED" (if input 1 is asserted).
-In "upLED", the "level" address is increased by "1" which is then read from a table which at the moment has 5 levels corresponding to 0%, 20%,40%,... duty cycles. If "level" is at "1", the LED is turned on for 51 clock cycles and off for 204 cycles which is ~20% duty.
-I included a pause sequence within upLED so that this repeats for ~.25 sec and then jumps back to checking the inputs once again.
-The downLED routine does the same thing but subtracts "1" from the level address.

Rich (BB code):
cpu equates (memory map)

portB    equ    0x06        
portA    equ    0x05
duty    equ    0x0c
timer    equ    0x0e
timer1    equ    0x0f
level    equ    0x0d

org    0x000

start    movlw    0x00        
    tris    portB        
    clrf    portB        
    movlw    0x1F        
    tris    portA        
    clrf    portA
    clrf    level

test0    btfsc    portA, '0'
    call    test1
    call     upLED

test1    btfsc    portA, '1'
    goto    test0
    call    downLED
    goto   test0

upLED     movlw    0xD3
    movwf    timer
    movlw    0x31
    movwf    timer1
    incf    level
    movf    level, w
    call    tableU
    movwf    duty
repeat    bsf    portB, '0'
onU    decfsz    duty
    goto    onU
    call    tableD
    movwf    duty
    bcf    portB, '0'
offU    decfsz    duty
    goto     offU
    decfsz    timer
    goto    $+2
    decfsz    timer1
    goto    repeat
    return

downLED    movlw    0xD3
    movwf    timer
    movlw    0x31
    movwf    timer1
    decf    level
    movf    level, w
    call    tableU    
    movwf    duty
repeat    bsf    portB, '0'
onD    decfsz    duty
    goto    onD
    call    tableD
    movwf    duty
    bcf    portB, '0'
offD    decfsz    duty
    goto    offD
    decfsz    timer
    goto    $+2
    decfsz    timer1
    goto    repeat
    return

tableU    addwf    PCL
    retlw    d'0'
    retlw    d'51'
    retlw    d'102'
    retlw    d'153'
    retlw    d'204'
    retlw    d'255'

tableD    addwf    PCL
    retlw    d'255'
    retlw    d'204'
    retlw    d'153'
    retlw    d'102'
    retlw    d'51'
    retlw    d'0'

end
Thanks for all the help!
 
Last edited:

jordan22

Joined May 30, 2010
2
this code seems to work w/o errors in MPLAB. How should I test if it is working like its actually suppose to though?

Rich (BB code):
processor 16F84A
INCLUDE <p16f84A.inc>
__CONFIG _CP_OFF & _WDT_OFF & _XT_OSC & _PWRTE_ON
org H'00'

portB    equ    0x06        
portA    equ    0x05
duty    equ    0x0c
timer    equ    0x0e
timer1    equ    0x0f
level    equ    0x0d

org    0x000

start    movlw    0x00        
        tris    portB        
        clrf    portB        
        movlw    0x1F        
        tris    portA        
        clrf    portA
        clrf    level

test0    btfsc    portA, H'0'
        call    test1
        call    upLED

test1    btfsc    portA, H'1'
        goto    test0
        call    downLED
        goto    test0

upLED    movlw    0xD3
        movwf    timer
        movlw    0x31
        movwf    timer1
        incf    level
        movf    level, w
        call    tableU
        movwf    duty
repeat    bsf    portB, H'0'

onU        decfsz    duty
        goto    onU
        call    tableD
        movwf    duty
        bcf    portB, H'0'

offU    decfsz    duty
        goto    offU
        decfsz    timer
        goto    $+2
        decfsz    timer1
        goto    repeat
        return

downLED    movlw    0xD3
        movwf    timer
           movlw    0x31
        movwf    timer1
        decf    level
        movf    level, w
        call    tableU    
        movwf    duty
repeat2    bsf portB, H'0'

onD        decfsz duty
        goto    onD
        call    tableD
        movwf    duty
        bcf    portB, H'0'

offD    decfsz    duty
        goto    repeat2
        decfsz    timer
        goto    $+2
        decfsz    timer1
        goto    onD
        return

tableU    addwf    PCL
        retlw    d'0'
        retlw    d'51'
        retlw    d'102'
        retlw    d'153'
        retlw    d'204'
        retlw    d'255'

tableD    addwf    PCL
        retlw    d'255'
        retlw    d'204'
        retlw    d'153'
        retlw    d'102'
        retlw    d'51'
        retlw    d'0'

end
 

Thread Starter

farscape

Joined May 16, 2010
26
There are is at least one big thing that is not right.

The program isn't staying at whatever duty cycle is currently set while checking the inputs. It only goes through the duty cycle until the delay routine runs out then goes blank until it reads another input assertion.
 

Markd77

Joined Sep 7, 2009
2,806
@Farscape - call test1 should be goto test1
I don't think it will quite work properly yet, but keep going.

@ Jordan
Select Debugger - Select Tool - MPLAB SIM
Select Debugger - Stimulus - New Workbook and set up something to change the input pins.
Select View - Simulator Logic Analyzer and then you will be able to see the output.
 
Last edited:

Thread Starter

farscape

Joined May 16, 2010
26
So here's the updated code once again. I think its now set up so that the program will continue looping through at the existing duty level until reading another assertion.

I am trying to find a way to constrain the "level" register values to be from 0-5 inclusive. Is there a way to ignore the increment command if "level" is already at 5?

I hope I'm getting a little closer now...


Rich (BB code):
processor 16F84A
INCLUDE <p16f84A.inc>
__CONFIG _CP_OFF & _WDT_OFF & _XT_OSC & _PWRTE_ON
org H'00'

portB    equ    0x06        
portA    equ    0x05
duty     equ    0x0c
timer    equ    0x0e
level    equ    0x0d

org     0x000

start 

movlw   0x00        
tris    portB        
clrf    portB        
movlw   0x1F        
tris    portA        
clrf    portA
clrf    level


test0     btfss   portA, b'0'
          call    test1
          call    upLED

test1     btfss   portA, b'1'
          goto    test0
          call    downLED

upLED           incf     level
                 goto     $+2
downLED         decf     level
same            movlw    0xD3
                 movwf    timer
                 movf     level, w
                 call     tableON
                 movwf    duty
repeat         bsf      portB, b'0'
on             decfsz   duty
                goto     on
                call     tableOFF
                movwf    duty
                bcf      portB, b'0'
off            decfsz   duty
                goto     off
                decfsz   timer
                goto     repeat
                btfss     portA, b'0'
                goto     $+2
                goto     upLED
                btfss     portA, b'1'
                goto     same    
                goto     downLED
    
tableON    addwf    PCL
                retlw    d'0'
                retlw    d'51'
                retlw    d'102'
                retlw    d'153'
                retlw    d'204'
                retlw    d'255'

tableOFF   addwf    PCL
                retlw    d'255'
                retlw    d'204'
                retlw    d'153'
                retlw    d'102'
                retlw    d'51'
                retlw    d'0'

end
 
Last edited:

Markd77

Joined Sep 7, 2009
2,806
This is one way of limiting level to 5 or less:

movf level, W
sublw 5
btfsc STATUS, Z
decf level, F ;if level was 5 make it 4, then the next line makes it 5 again
incf level, F

Have a look at the calls in your program, if you use a call there has to be a return or a retlw otherwise things go very wrong.
Also if no button is pressed it gets stuck in the test0/test1 loop.
 
Top