# Tiny ASM: PWM LED brightness

#### Mosaic

##### Well-Known Member
Here's a 9 instruction snippet that is inserted in the regular program loop to manage LED brightness via their common anodes (or cathodes via bit flipping). This can be used to drive transistors that switch higher currents for 7 segment displays etc. Just note your bit polarity gets inverted. So you'd need an IORLW cmd to set bits instead of the ANDLW used to clr them. Once the access rate to this code is under 2mSec the LEDs should not have visible flicker, as a 16mS DC period translates into a 62.5hz refresh rate.

Code:
Brightness; S'ware PWM (uses 3 GPRs) on a 3 bit (8 step)  base, LEDdrive GPR bits 0,2 are the common anodes in this case. At about a 1.37ms prg loop speed (my app. prg)  100% DC period is  11mSec or 90Hz refresh.
incf LUXCTRL,w; 100% DC timebase. GPR register
andlw .7; lower 3 bits
movwf LUXCTRL; range 0-7.
subwf LUX_setpt,w; Brightness PWM setting, GPR register. Range = 0 (12.5%) thru 7 (100%) for dimmest to brightest.
movf LEDdrive,w; buffer GPR register for common anodes avoids Read Modify Write issues.
skpc
andlw  b'1111010'; disable active common anodes if no carry.
movwf  LEDport; mapped to actual pic PORT, eg #define LEDport    PORTB
return
EDIT: Northguy pointed out a flaw in this code...so its fixed in POST#21

Last edited:

#### NorthGuy

##### Well-Known Member
Or do this (without corrupting unused 5 bits of LEDPort)

Code:
rrf LuxSet, w
rrf LuxSet, f
skpc
return
movlw 0x07       ; mask for the pins to set
xorwf LEDPort, f ; preferably LAT
return
Set LuxSet to
Code:
00000000  - 100%
10000001  - 87.5 %
01000001  - 75%
00100001  - 62.5%
00010001  - 50%
00001001  - 37.5%
00000101  - 25%
00000011  - 12.5%
00000000 - 0%
Clear all pins after setting new LuxSet. When setting 100%, set them to 1.

#### Mosaic

##### Well-Known Member
Won't the Xorwf on the port become a candidate for the RMW issue. By using the LEDdrive as a GPR buffer of what is desired as the actual PORT output, the 6 other PORT pins are not corrupted, merely unchanged. Note that I am driving just two common anodes here, although up to 8 can be driven with the full PORT with little change except to the ANDLW statement.

#### NorthGuy

##### Well-Known Member
Won't the Xorwf on the port become a candidate for the RMW issue. By using the LEDdrive as a GPR buffer of what is desired as the actual PORT output, the 6 other PORT pins are not corrupted, merely unchanged. Note that I am driving just two common anodes here, although up to 8 can be driven with the full PORT with little change except to the ANDLW statement.
After andlw, you get w = LEDDrive & 0x7a; Then you write it to the port - PORTB = LEDDrive & 0x7a. Regardless of what was in PORTB, it will be overwritten. To preserve other bits in PORTB, you would need something more involved, such as PORTB = PORTB ^ (PORTB^LEDDrive) & 0x7a.

If RMW is a problem, you can create a new variable, call it LATB, and then do movff to PORTB, or get a chip which already has LATx. But I don't think RMW should be a problem here.

However, even with LATB, you still need to do all this (or similar) LATB = LATB ^ (LATB^value) & mask to preseve unused bits. It may be more efficient to do LATB = LATB | mask to set bits, and LATB = LATB & ~mask to clear them.

#### Mosaic

##### Well-Known Member
Logical operations that operate directly on a PORT are always subject to the RMW issue. I cannot foresee the LC loading of the PIC pins that might exist in another person's circuit and potential RMW fallout. A PORT buffer is intended to overwrite the PORT, all bitwise ops are done in the buffer and then written safely to the PORT. Also the coding I used permits a simple step up to 16 or more adjustments to the DC%. I built it that way since sometimes more granularity in the adjustment is needed to suit the application. Thus the code is reliable (RMW issue), scalable, works across perhaps all common 8 bit PICS and is easy to read. It isn't as efficient for the specific 8 step PWM solution you outline but then it's meant to be code that's simple to read and modify and use by anyone.
This is relevant:

#### NorthGuy

##### Well-Known Member
I understand you're afraid of RMW issues which may spuriously change pin values. But you can avoid it if you're careful. The best solution is to use newer PIC16s with LATs, which are cheaper and better.

As I understand, you're trying to deal with the RMW issue by maintaining a shadow register which you call LEDdrive. This would assume that the other parts of the code should use the same LEDdrive as a shadow register, and they should maintain it up to date. If they don't, you will overwrite all their actions with this:

Code:
movf LEDdrive,w
andlw b'1111010'
movwf LEDport
Say, somewhere else in the code, when you want to set bit #5 on PORTB (both real and the shadow) you do something like:

Code:
bsf LEDdrive, #5
movf LEDdrive
movwf LEDport
But, LEDdrive has bits #0 and #2 set to 1 (otherwise the code you have posted wouldn't work). So, this code which wants to set only bit #5 will unintentionally drive pins #0 and #2 high. How's that better than RMW?

Last edited:

#### Mosaic

##### Well-Known Member
Well if driving PB0 and PB2 high is undesirable at the time you wish to set PB5, simply set PB0 and PB2 to what you need in the shadow register of LEDdrive before writing LEDdrive to the actual PORT

#### NorthGuy

##### Well-Known Member
Well if driving PB0 and PB2 high is undesirable at the time you wish to set PB5, simply set PB0 and PB2 to what you need in the shadow register of LEDdrive before writing LEDdrive to the actual PORT
Imagine I'm doing 10kHz PWM on PB5. How do I know which values are desirable for B0 and B2 at the moment?

#### Pommie

##### Well-Known Member
The RMW problem is easily avoided. Don't overload the pins (causes port to read back wrong) and don't do two RMW instructions one after the other (pin capacitance has to be given time to charge). The most common cause of RMW problems come from people putting LEDs on pins without series resistors.

Mike.

#### Mosaic

##### Well-Known Member
Imagine I'm doing 10kHz PWM on PB5. How do I know which values are desirable for B0 and B2 at the moment?
What does RB5 have to do with the required settings of RB0 and RB2? It is what you want it to be based on the prg requirements....I don't see the point of the question. Whatever you want ANY pin to be , simply set it before you write to the port.

#### NorthGuy

##### Well-Known Member
What does RB5 have to do with the required settings of RB0 and RB2? It is what you want it to be based on the prg requirements....I don't see the point of the question. Whatever you want ANY pin to be , simply set it before you write to the port.
Ok. I'll repeat the logic.

As I understand, you're trying to deal with the RMW issue by maintaining a shadow register which you call LEDdrive. This would assume that the other parts of the code should use the same LEDdrive as a shadow register, and they should maintain it up to date. If they don't, you will overwrite all their actions with this:
Code:
movf LEDdrive,w
andlw b'1111010'
movwf LEDport
Say, somewhere else in the code, when you want to set bit #5 on PORTB (both real and the shadow) you do something like:
Code:
bsf LEDdrive, #5
movf LEDdrive
movwf LEDport
But, LEDdrive has bits #0 and #2 set to 1 (otherwise the code you have posted wouldn't work). So, this code which wants to set only bit #5 will unintentionally drive pins #0 and #2 high. How's that better than RMW?
I want to drive B5 at 10 kHz. If I use the code above, it'll set B0 and B2, which will render your posted code useless as these pins will be (almost) always high. If I clear #0 and #2 in LEDdrive as you suggested, your posted code will be useless again because now B0 and B2 are going to be always low.

So, how do I combine 10kHz PWM on B5 with your code?

#### Mosaic

##### Well-Known Member
If u need to s'ware address a port at 10 Khz...u simply fill LEDdrive with whatever bits u want and write to the port at the desired rate...there is no issue. I don't see your problem.
If I am writing to a PORT at 10Khz, I can change B0 and B2 at that same rate....or leave them the same.
If I can write directly to a port @ 10Khz I can also do so through a buffer register. I don't see what needs explaining further with such a simple concept.

You do see that if the code example I posted runs @ 10Khz it will update the port at that speed. No problem.

#### NorthGuy

##### Well-Known Member
You code runs at 60Hz (as you suggest) because there's no need to run it faster. The PWM code runs at 10kHz, meaning it's called about 167 between executions of your code.

Code:
movf LEDdrive,w
andlw b'1111010'
movwf LEDport
This sets B0 and B2 to something useful.

30 us later, the PWM code does this:

Code:
bsf LEDdrive, #5
movf LEDdrive
movwf LEDport
which sets B0 and B2 high. They stay high for about 15ms until your code comes again:

Code:
movf LEDdrive,w
andlw b'1111010'
movwf LEDport
which sets B0 and B2 as you intend, but then 30us later, the PWM code

Code:
bsf LEDdrive, #5
movf LEDdrive
movwf LEDport
sets B0 and B2 high again. They'll stay high for 15ms until your code executes again.

And so on.

You see - the values you set with your code only keep for 30us. Most of the time B0 and B2 are high - you get 100% brightness no matter how your code sets B0 and B2.

See the problem now?

#### Mosaic

##### Well-Known Member
The updating on the LEDdrive is not restricted to this code snippet .Just like masking and logical operations on a PORT are not restricted to any one subroutine.
Your approach to doing a 10Khz with this shadow register is not the proper one. It breaks the function of the posted code.

If I want to update bit 5 with a PWM signal at a high rate, i do it in another subroutine. Thus the sample code I posted does its particular duty and the 10Khz PWM, bit5, subroutine does its particular duty. They update independent bits in the LEDdrive buffer which can be redefined as a PORTB_shadow or anything convenient to suit readability. Thus there is no problem. Courses for Horses or Horses for Courses depending how u look at it.

Last edited:

#### NorthGuy

##### Well-Known Member
Your approach to doing a 10Khz with this shadow register is not the proper one. It breaks the function of the posted code.
Ok. Would you mind writing few lines of code for the 10kHz routine which would demonstrate how to set/clear B5 in a way which is compatible with your code?

#### Mosaic

##### Well-Known Member
The code is quite similar, it ought to be interrupt driven to achieve an 80Khz rate (due to 3 bit PWM stepping) . A 16 Mhz clocked PIC returns a 4Mhz instruction clk , so this interrupt would need to occur every 50 instructions to achieve 80Khz. Adding 18 instructions overhead for the interrupt handling implies 27 instructions to run this PWM occurring every 50 instruction clocks. Thus it consumes about 54% of an average PIC's processing cycles. I'd suggest a hardware PWM capable PIC for anything over 2Khz or so unless it's a dedicated PWM app.

Only bit 5 in the PORT buffer is being updated, thus this routine does not 'corrupt' the other bits. The PORT buffer is fully updated at the necessary rate (80Khz) in this routine. Elsewhere in the main program the PORT buffer's other bits can also be updated independently without affecting anything. Actually this quick interrupt update of the PORT probably negates the need to bother to directly write to the PORT at all elsewhere in the program. Just updating the port buffer with desired bits could be enough as the PORT is always updated at an 80Khz rate. BTW an 80Khz rate of PORT updates certainly increases the likelihood of a RMW problem for direct XORWF operations on the PORT as the time constant of the pin capacitive loading could be significant compared to the .0000125 s update period of the PORT.

In the realm of theory now:
A 70 ohm RDSon Pic Pin FET can potentially deliver about 65mA from a 4.5V logic HI into a dead short. Of course it will quickly overheat and die but for a very short time it can do this. Estimating at least 2RC (sec) time constants to ensure a logic state change, a capacitance load of >= .09uF on the pin will trigger a RMW error. This is napkin maths and can certainly be improved!

Code:
TenK_PWM; test of a simple 10K PWM on bit 5 of PORTB vi a shadow register.
incf PWMCTRL,w
andlw .7; 3 bit, 8 step, range of control.
movwf PWMCTRL
subwf PWMDC,w
bsf LEDdrive,5; default PWM condition = ON.
skpc
bcf LEDdrive,5
movf LEDdrive,w; PORT buffer GPR
movwf LEDport; mapped to actual PIC PORT
return

#### NorthGuy

##### Well-Known Member
Do you suggest that you can combine this PWM code running from 80kHz interrupt with the orginal brightness control code running at 60Hz in the main loop and they won't interfere with each other?

BTW: It's not a good idea to drive loads with high speed PWM pin directly. You would normally have a driving circuits or IC to drive the FET (or whatever), so the PIC needs only to provide logic signal, without much current.

#### Mosaic

##### Well-Known Member
The two sets of code write to different bits in the buffer register. Where's the interference? It's a single thread app.

#### NorthGuy

##### Well-Known Member
This set of code:

Code:
incf LUXCTRL,w; 100% DC timebase. GPR register
andlw .7; lower 3 bits
movwf LUXCTRL; range 0-7.
subwf LUX_setpt,w; Brightness PWM setting, GPR register. Range = 0 (12.5%) thru 7 (100%) for dimmest to brightest.
movf LEDdrive,w; buffer GPR register for common anodes avoids Read Modify Write issues.
skpc
andlw b'1111010'; disable active common anodes if no carry.
movwf LEDport; mapped to actual pic PORT, eg #define LEDport PORTB
return
works by setting B0, B2 and B7 to either high or low. For it to work, there has to be 1s in bits #0, #2 and #7 of LEDdrive. So it sets B0, B2 and B7 to somthing.

After it is done, the other code, which you suggest to run at 80kHz, will run. At 80kHz, we should should expect it to run somewhere within 10us:

Code:
incf PWMCTRL,w
andlw .7; 3 bit, 8 step, range of control.
movwf PWMCTRL
subwf PWMDC,w
bsf LEDdrive,5; default PWM condition = ON.
skpc
bcf LEDdrive,5
movf LEDdrive,w; PORT buffer GPR
movwf LEDport; mapped to actual PIC PORT
return
This set of code copies bytes #0, #2, and #7 from LEDdrive (which are all ones) into LEDport, which sets B0, B2, and B7 high. They will remain high until this piece of code executes in about 15 ms:

Code:
incf LUXCTRL,w; 100% DC timebase. GPR register
andlw .7; lower 3 bits
movwf LUXCTRL; range 0-7.
subwf LUX_setpt,w; Brightness PWM setting, GPR register. Range = 0 (12.5%) thru 7 (100%) for dimmest to brightest.
movf LEDdrive,w; buffer GPR register for common anodes avoids Read Modify Write issues.
skpc
andlw b'1111010'; disable active common anodes if no carry.
movwf LEDport; mapped to actual pic PORT, eg #define LEDport PORTB
return
This code sets B0, B2, and B7 somehow different, but it doesn't matter, because within 10 us, this piece of code will set them back to high:

Code:
incf PWMCTRL,w
andlw .7; 3 bit, 8 step, range of control.
movwf PWMCTRL
subwf PWMDC,w
bsf LEDdrive,5; default PWM condition = ON.
skpc
bcf LEDdrive,5
movf LEDdrive,w; PORT buffer GPR
movwf LEDport; mapped to actual PIC PORT
return
So, B0, B2, and B7 will be high 99.9% of the time no matter what your LuxCtrl code is doing. This means 100% brightness all the time.

Do you see the interference?

#### Mosaic

##### Well-Known Member
Actually, for the first bit of code it should set the common anodes( bit 0 ,2) high by default, until the LUX_setpt triggers the low part of the DC%, as it happens that occurs elsewhere in my main prg ( a blink sequence)...so that should be changed for the first code to stand on its own.
Bit7 isn't supposed to be altered, that's a typo. Only bit 0,2 are made low.

The 2nd bit of code (10Khz PWM) only alters bit 5 of LEDdrive, NOTHING ELSE. so what u say is incorrect. IF LEDdrive B0 & B2 are low, they stay low, if hi they stay hi, it's independent. Only the first 'slow' PWM code changes B0,B2.
All of this is w.r.t LEDdrive BTW. As nothing changes the PORT except a movwf LEDport when LEDdrive is placed in the Wreg.

I'm sorry but I just don't see the issue here.

Edit: When u say that the first bit of code sets b0,2 high, no it doesn't. It changes it from hi to low via the ANDLW command. Note the correction in the next post that defaults those 2 bits high so that the PWM works properly. This way the first bit of code stands on its own as a PWM driver.

Last edited: