Blinking LEDs without suspending main function in PIC

Thread Starter

jwilk13

Joined Jun 15, 2011
228
Hi all,

My self-taught knowledge of PICs has left me wondering about something I've just encountered. I have a PIC reading an analog voltage from 0-5V. This voltage determines the output duty cycle of a PWM waveform, but it also determines the flash rate of LEDs connected through a resistor to the MCU.

What I'm wondering is this: How do I do both "at the same time"? I know that's not entirely possible, but let's consider an example.

Say I wanted to flash one LED once per second if the voltage at the ADC goes above 1 VDC. As the voltage increases, I want to increase the PWM duty cycle, but if I'm using delays to flash the LED, the sampling rate of my ADC goes way down, right? How would I go about accomplishing both objectives?

As usual, I'm not looking for code, I can do that myself. I just need a friendly push in the right direction :)
 

ErnieM

Joined Apr 24, 2011
8,377
The rate you sample the A2D at and the flash rate are not related unless you make it that way.

How I would set this up would be governed by how fast I need the circuit to update. How fast should the PWM vary with the voltage? The last time I did this the lowest I could get my PWM to run was some 200Hz, which works fine with the LED and helps with the "flicker" issue. Sampling the A2D any faster then that would be to no use, you would not see it. 1Hz would appear to be "laggy" (the pot turns faster then the output changes), 10 or 20 Hz may work fine, 30Hz should appear instantaneous and isn't a huge computational burden.

That sets the sample rate, and it would probably be simplest if you have a timer interrupt running to set some base time to either trigger an A2D reading or start or stop a blink.

And of course to blink a PWM controlled LED you just set the PWM period to the intensity period for ON, or to zero for OFF.
 

takao21203

Joined Apr 28, 2012
3,702
It's a lot of effort to employ PWM just to blink a LED.

You can easily use a counter for LEDs,
and for instance program a on/off ratio.

For instance your main timer interrupt would be every 1ms,
and then you increment a counter, switch on LED,
increment another counter when the LED is on, and switch off again.

Usually I do things like this in the main function, using flags, not directly in the interrupt handler.
 

Thread Starter

jwilk13

Joined Jun 15, 2011
228
That sets the sample rate, and it would probably be simplest if you have a timer interrupt running to set some base time to either trigger an A2D reading or start or stop a blink.
I think we're on the same page here for the A2D sampling.

I think takao and I are on the same page for the LED blinking. Thanks for the suggestions!
 

ErnieM

Joined Apr 24, 2011
8,377
And if the object is to drive an LED that a user can see, it's impossible. The frequency for the hardware PWM won't go down anywhere near what you need to make the flashes visible.
Your missing the OP's point: the PWN is for brightness, and a zero period is a blink off.

And a hardware PWM always has a small software footprint then a software PWM. It's a set-it and forget-it kind of deal.
 

Thread Starter

jwilk13

Joined Jun 15, 2011
228
I'm not sure my explanation was good enough for anyone to understand what I'm doing :p

The goal was to read an ADC value between 0 and 5V. There are two outcomes based on this voltage: 1) If the ADC value was above a specified value, the LED flashes at a certain rate (let's say once per second, and this is a digital output). 2) The ADC value determines the PWM duty cycle of a DIFFERENT output (higher value, higher duty cycle).

My solution for the first part was to use timed interrupts to set a specified sampling rate for the ADC (I chose 50 Hz). I then counted the number of times the program entered the ISR and used that value to blink my LED once per second. My ISR is shown below, the main loop just controls the LED flashing. And I'm using a PIC18F45K20 right now set at 8MHz.

Rich (BB code):
void interrupt()
{
	
	if (intcon.2)
	{
		clear_bit(t0con,7);		// stop timer
		interruptCount++;
		if (interruptCount > 50)
		{
			interruptCount = 0;
		}

		adcon0 = 0b00110001;	// set AN12 (RB0) as A/D input
		set_bit(adcon0,1);		// Activate GO/DONE bit
		clear_bit(intcon,2);	// reset interrupt flag
		tmr0l = 100;			// set timer value to 100
		set_bit(t0con,7);		// restart timer
	}
	
	if (pir1.6)
	{
		adcValue = adresl;
		adcValue += (adresh << 8);
		clear_bit(pir1,6);
		
	}	
}
Basically, the first "if" statement checks to see if the interrupt flag set was for the timer overflow, and the second checks to see if the interrupt was from an A/D conversion completing. I did it this way so that the program never has to stop and wait for A/D conversions or for delays between LED blinks.
 
Last edited:

Markd77

Joined Sep 7, 2009
2,806
Another option is to start the AD conversion at the end of the interrupt and then assume it is ready the next time the timer interrupt happens. It slightly increases the time available in the main function at the expense of the AD result being slightly late.
I'd recommend using the flag names instead of bit numbers, it makes things easier to read / check. Eg. intcon, T0IF instead of intcon, 2
 

Thread Starter

jwilk13

Joined Jun 15, 2011
228
I'd recommend using the flag names instead of bit numbers, it makes things easier to read / check. Eg. intcon, T0IF instead of intcon, 2
That's a good suggestion. I didn't know until now that my compiler supports that. The only references I found in their documentation up to this point used the bit number, not the name.

I just tried it though and the compiler supports it in their set_bit and clear_bit macros as well as the IF statements. I changed some of them accordingly...

Rich (BB code):
void interrupt()
{
	
	if (intcon.T0IF)
	{
		clear_bit(t0con,TMR0ON);			// stop timer
		interruptCount++;
		
		// Check for 50 iterations.  This corresponds to one second.
		
		if (interruptCount > 50)
		{
			interruptCount = 0;
		}
		
		adcon0 = 0b00110001;		// set AN12 (RB0) as A/D input
		set_bit(adcon0,1);		// Activate GO/DONE bit
		clear_bit(intcon,T0IF);		// reset interrupt flag
		tmr0l = 100;			// set timer value to 100
		set_bit(t0con,TMR0ON);	// restart timer
	}
	
	if (pir1.ADIF)
	{
		adcValue = adresl;
		adcValue += (adresh << 8);	// Read value in ADC registers
		clear_bit(pir1,ADIF);		// Reset interrupt flag
	}	
}
The only one I wasn't sure about was the GO/DONE bit in ADCON0. You're right though, seeing something like TMR0ON as opposed to just a number is a lot easier to read through. Thanks for the advice!

EDIT: I looked through my compiler's documentation, and the GO/DONE bit can be referenced using GO, DONE, GO_DONE, NOT_DONE, or GO_NOT_DONE.
 
Last edited:

ErnieM

Joined Apr 24, 2011
8,377
Here's a style inquiry. I wonder why you would use:

clear_bit(pir1,ADIF);

when you also use:

if (pir1.ADIF)

Sure, the clear_bit() and set_bit() functions (or macros, I haven't looked them up) work just fine, but my preference is to use:

pir1.ADIF = 0;
 

Thread Starter

jwilk13

Joined Jun 15, 2011
228
Here's a style inquiry. I wonder why you would use:

clear_bit(pir1,ADIF);

when you also use:

if (pir1.ADIF)

Sure, the clear_bit() and set_bit() functions (or macros, I haven't looked them up) work just fine, but my preference is to use:

pir1.ADIF = 0;
I guess I've just gotten so used to using set_bit and clear_bit that I automatically use them. Either way works, but it does make more sense to do it the way you described, even if it's just to use periods across the board and avoid commas. I've gotten so used to it I guess I just don't notice it. It's kinda like driving a stickshift I suppose...I still reach down for the shift knob even when I'm driving an automatic :p

EDIT: My code must be pretty slick if you're picking out stuff like that :D I'll take it as a compliment!
 

ErnieM

Joined Apr 24, 2011
8,377
My code must be pretty slick if you're picking out stuff like that :D I'll take it as a compliment!
By all means take it as such. You have a clear indentation scheme marking off each block, the blocks themselves are commented, plus a nice comment on each line.

The best code makes you want to read it.
 

takao21203

Joined Apr 28, 2012
3,702
using XC8, it's sufficient actually only to write "T0IF", means

Rich (BB code):
 if(T0IF)T0IF=0;
what I recently figured out, things like that will work and produce nearly 1:1 assembler equivalent:

Rich (BB code):
v_data<<=1;if(CARRY)LB5=1;
where LB is the latch register for port B

quite interesting because until recently I used binary tables for that, requiring
a reference to constant array each time.

This is by no means documentated somewhere in form of a how-to instruction sheet. I figured it out by random, and a lot of guesswork.
 
Top