MPLAB C30 - Flash an LED

The flashing LED - an iconic starting point for many embedded hobbyists and professionals alike. There are several reasons that contribute to the popularity of the humble flashing LED program, simplicity is likely the greatest influence followed closely by its reliability as a test program.

I should clear up something now rather than later: pre-made delay libraries or other "simpler" means for creating delays could have been employed in this project. Timer1 has been used as I wanted to get familiar with one of the other onboard peripherals at the same time. The code is still very easy to follow, so don't be mislead by the perceived difficulty (if there is any?)

The following video demonstrates the completed project. The code is explained further on in the article.


Correction - I say "once every second" though the program is designed to toggle every 500mS.

You too can replicate this project by either building your own circuit or using the Explorer 16 Development board by Microchip, with the PIC24FJ128GA010 installed. Note - If you're unfamiliar with these devices, perhaps the following article would be worth a read: "Handy Resources for PIC24 + C30". And there's always the Digital DIY forum for guidance as well.

 

The Code

This article was not intended to teach someone C from the ground up - if you're not familiar with C then have a read of this article. The program is quite simple - toggle each nibble of PORTA. The result is a pattern of four alternating LEDs as shown in the above video..

Flashing LEDs
#include <p24fxxxx.h>               // PIC24 definitions
 
_CONFIG1(JTAGEN_OFF & FWDTEN_OFF)   // disable JTAG and WDT
_CONFIG2(POSCMOD_XT & FNOSC_PRIPLL) // configure oscillator
 
#define DELAY_MILLI_SEC 500         // time to delay between toggles
                                    // valid range 0 to 2097 milli-seconds
 
#define FOSC 32000000               // oscillator speed (with PLL enabled, 8Mhz x 4 = 32Mhz)
#define TMR1_SCALE 256              // TMR1 prescale setting
#define FOSC_DIV 2                  // TMR1 FOSC division setting
// calculate the number of cycles required for 'DELAY_MILLI_SEC'
#define DELAY_CYCLES (DELAY_MILLI_SEC * ((FOSC / TMR1_SCALE / FOSC_DIV) / 1000.0))
// convert the above result to type 'long' for use in the program
const long delay_cycles = DELAY_CYCLES;
 
void main(void) {                   // program start
    TRISA = TRISA & 0xFF00;         // configure PORTA<7:0> as outputs
    PORTA = 0xF0;                   // set high nibble of PORTA
    T1CON = 0b1000000000110000;     // TMR1 on, prescaler 1:256 Fosc/2
 
    while (1) {                     // main program loop
        PORTA = ~PORTA;             // invert PORTA
        TMR1 = 0;                   // reset TMR1
        while (TMR1 < delay_cycles);// delay for defined time
    }
}

 

Device + Config

C30 has no idea what device your going to use unless you declare it. The same goes for the device configuration settings - both are listed below:

Device + Config Settings
#include <p24fxxxx.h>               // PIC24 definitions
 
_CONFIG1(JTAGEN_OFF & FWDTEN_OFF)   // disable JTAG and WDT
_CONFIG2(POSCMOD_XT & FNOSC_PRIPLL) // configure oscillator

 

Pre-Compile Definitions

The pre-compile definitions allow me to assign numbers and equations that will be calculated at compile. From there, the results can be accessed in code with minimal waste of precious CPU cycles. These definitions assist with calculating the correct settings for the Timer1 module which is used later for delays.

Pre-Compile Definitions
#define DELAY_MILLI_SEC 500         // time to delay between toggles
                                    // valid range 0 to 2097 milli-seconds
 
#define FOSC 32000000               // oscillator speed (with PLL enabled, 8Mhz x 4 = 32Mhz)
#define TMR1_SCALE 256              // TMR1 prescale setting
#define FOSC_DIV 2                  // TMR1 FOSC division setting
// calculate the number of cycles required for 'DELAY_MILLI_SEC'
#define DELAY_CYCLES (DELAY_MILLI_SEC * ((FOSC / TMR1_SCALE / FOSC_DIV) / 1000.0))


Now if the desired delay period, oscillator speed or Timer1 prescale changes, the end user only needs to update the above definitions. Everything else is calculated on compile.

Another interesting point here is C30 will treat pre-compile definitions as the widest object type found in its definition. Consider DELAY_CYCLES. If 1000.0 were changed to 1000 then DELAY_CYCLES would be treated as a long int. Encoding 1000 as 1000.0 ensures that C30 will treat DELAY_CYCLES as an object of type float (as the widest object type is now a float type).

Why go to all the hassle? If the widest object was of type long int (FOSC = 32000000 in the above program) then all decimal places would be truncated during the math operations, resulting in some rather large errors!

Using floats is incredibly inefficient and anyone who is familiar with the architecture of a small microcontroller would be familiar with the resources involved. I prefer not to work with them, if possible. With that said, the float is converted to a long type object with the following statement:

converting from float to long
// convert the above result to type 'long' for use in the program
const long delay_cycles = DELAY_CYCLES;


Here's a couple of activities for anyone that is overly interested with any of this:

  • Why is 2097 the maximum DELAY_MILLI_SEC can be set too?
  • What happens if DELAY_MILLI_SEC is set beyond 2097?
  • Change the declaration for delay_cycles to 'float' and review the difference in resources.

 

Device Initialisation

Now that all of the hardware has been defined, and I've got some handy pre-compile definitions to use - its time to begin the main program. Before the main program loop can commence, all of the software initialisation must take place..

Initialisation
TRISA = TRISA & 0xFF00;         // configure PORTA<7:0> as outputs
PORTA = 0xF0;                   // set high nibble of PORTA
T1CON = 0b1000000000110000;     // TMR1 on, prescaler 1:256 Fosc/2


The above code snippet configures the direction of PORTA pins, and sets the outputs with a default state. Following that, Timer1 is configured.

 

The Main Loop

Main Program Loop
while (1) {                     // main program loop
    PORTA = ~PORTA;             // invert PORTA
    TMR1 = 0;                   // reset TMR1
    while (TMR1 < delay_cycles);// delay for defined time
}


The above code is easy to follow. PORTA is inverted, and TMR1 increments until the desired number of cycles have elapsed. The result is as shown in the above video - an alternating LED pattern.

Forum Activity

Member Access