Osmeoisis 2022-09-06 15-33-19PIC - Mid - C - 13

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 19

© Gooligum Electronics 2013 www.gooligum.com.

au

Introduction to PIC Programming


Programming Mid-Range PICs in C

by David Meiklejohn, Gooligum Electronics

Lesson 13: Using EEPROM Data Memory

Mid-range assembler lesson 19 described the mid-range PIC EEPROM data memory, showing how it can be
used to store data, such as configuration, current-state or logged data, which is retained when the PIC is
powered off.
This lesson demonstrates how to use C to initialise, read and write EEPROM data, re-implementing the
examples using Microchip’s XC8 compiler1 (running in “Free mode”).
In summary, this lesson covers:
 Using macros provided by XC8 to initialise, read and write EEPROM data
 Using XC8 library functions to read and write EEPROM data
 Storing variables in EEPROM

EEPROM Data Memory


An EEPROM is a type of memory which can be modified by a running program, but which will retain its
contents when powered off.
Unlike the flash memory used for program memory in modern PICs, individual bytes in an EEPROM can be
written to, without affecting any of the others2.
However, it’s not suitable as a replacement for data memory (the PIC’s general purpose registers) used for
short term working storage, because:
 writing to EEPROM is very slow – an EEPROM write operation on the PIC16F684 typically
requires 5 ms to complete, compared with less than 1 µs for data memory
 EEPROMs have a limited write endurance – each location can only be written a limited number of
times (typically 1 million on the PIC16F684) before it fails to retain new data
For these reasons, EEPROM data should only be updated sparingly, as necessary. A million writes sounds
like a lot, but if your application updated an EEPROM location every second, your device would typically
fail after 1 million seconds = 287 hours = 11.6 days. Your customers may not be happy with that…

The PIC16F684 includes a 256-byte EEPROM.

1
Available as a free download from www.microchip.com.
2
Program memory in PICs such as the 16F684 consists of NAND flash, which must be erased and written to a whole
block (some number of bytes) at a time.

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 1


© Gooligum Electronics 2013 www.gooligum.com.au

We saw in mid-range assembler lesson 19 that the data in the EEPROM is accessed indirectly, using a pair of
8-bit registers: EEDAT and EEADR, and that it is controlled by the EECON1 register.

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0

EECON1 – – – – WRERR WREN WR RD

The RD bit is used to initiate a read operation.


After loading the address of the location we wish to read into EEADR, setting the RD bit will cause the
contents of that location to appear in the EEDAT register.
The RD bit clears itself when the read operation is complete; it can only be set – you cannot clear it yourself.

Similarly, the WR bit is used to initiate a write operation.


After loading the data we wish to write into EEDAT, and the address of the location we wish to update into
EEADR, a specific sequence of values must be written to the EECON2 register3. Setting the WR bit will
then initiate the write operation.
When the write operation is complete, the WR bit is cleared automatically, and the EE write complete
interrupt flag (EEIF) in the PIR1 register will be set.
To detect that the write is complete, we can poll the WR bit (waiting for it to be cleared).
Or, because the EEPROM write operation takes a long time (5 ms or so) to complete and your program may have other
things to do than poll a flag, it may make more sense to use an EEPROM write interrupt handler, which will be
triggered whenever an EEPROM write completes. It is enabled by setting the EEIE bit in the PIE1 register (and of
course the PEIE bit in INTCON, as we’ve done for other peripheral interrupts), but which otherwise works the same
way as the other interrupt sources we have described.

The write operation will only succeed if the WREN bit is set. By default, on power-up, it is cleared, to avoid
accidental writes to the EEPROM.
Finally, the WRERR flag indicates whether the write operation completed successfully (WRERR = 0), or if
it did not complete because it was interrupted by a device reset (WRERR = 1). If you check the WRERR
flag after the write operation, and see that it had failed, you can repeat the write.

XC8 EEPROM Access Methods


As with all the other special function registers, the XC8 compiler makes these registers available as variables
(such as EEDAT and EEADR) and unions containing bit-fields (such as EECON1bits, containing bit-fields RD,
WR, WREN and WRERR).
However, unless you wish to use the WRERR flag to check for EEPROM write errors, you would not
normally used these variables or bit-fields directly to control and access the EEPROM.
Instead, you would normally use various higher-level EEPROM access methods provided by XC8, as
described here.

3
for details, see example 2 in mid-range assembler lesson 19

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 2


© Gooligum Electronics 2013 www.gooligum.com.au

To specify initial data to be loaded into the EEPROM when the PIC is programmed, XC8 supplies the
__EEPROM_DATA() macro.
It is analogous to the de assembler directive described in mid-range assembler lesson 19, but it is less
flexible.
It takes eight parameters, each specifying a value which will be loaded into successive EEPROM locations,
for example:
__EEPROM_DATA(20,21,22,23,24,25,26,27);
__EEPROM_DATA(28,29,30,31,32,33,34,35);

would load the decimal values 20 to 35 into the first 16 EEPROM locations.
Note that this macro is not used to write to the EEPROM at run-time; it is only used to specify initial values,
which will be in place before the program starts running.

Your program can use either the eeprom_read() function or the EEPROM_READ() macro to read a single
byte of data from a specific location (or address) in the EEPROM.
For example:
uint8_t data1, data2;
data1 = eeprom_read(1);
data2 = EEPROM_READ(2);

will copy the contents of EEPROM location 1 into the single-byte variable data1, and the contents of
location 2 into data2.
The macro version, (EEPROM_READ) is faster than the function version, (eeprom_read), but if you have a
few different read operations (not a single operation repeated in a loop), the macro version will require more
program memory, because, being a macro, it is expanded into a number of lines of code in-place at compile
time.
An important difference between the two is that the eeprom_read() function checks and waits for any
current EEPROM write operation to complete before it performs the read, but the macro version does not.

You can use either the eeprom_write() function or the EEPROM_WRITE() macro to write a single byte of
data to a specific EEPROM address.
For example:
eeprom_write(1,20);
EEPROM_WRITE(2,50);

will write the decimal value 20 to EEPROM location 1, and the value 50 to location 2.
Again, the macro version (EEPROM_WRITE) is faster than the function version (eeprom_write), but is
likely to require more program memory if it is used multiple times in different ways.
Otherwise, they operate the same way – both the macro and function versions will wait for any current
EEPROM write operation to complete.

These EEPROM read and write macros and functions are very convenient if you wish to access a sequential
series of values stored in EEPROM, such as logged data.
However, you may wish to store a specific set of data in EEPROM, perhaps recording the device’s current
state, so that that data is retained when the device is powered down.

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 3


© Gooligum Electronics 2013 www.gooligum.com.au

The XC8 compiler makes it very easy to do this with ordinary variables – they can be automatically stored in
EEPROM, simply by adding the ‘eeprom’ qualifier to variable definitions.
These ‘eeprom’ variables can be initialised as part of their definition, just like any other variable, except that
the initial value is loaded into the EEPROM when the PIC is programmed, not by start-up code at run-time.
This means that, although you can initialise these variables when you define them, they also retain their
value when the device is powered down. We’ll see how this works in example 2, below.
Note that ‘eeprom’ variables must be global (defined outside of any function), or, if defined locally within a
function, they must be qualified as ‘static’.
For example:
eeprom uint8_t data1 = 0;

void main ()
{
static eeprom uint8_t data2 = 10;

++data1; // increment data1


data2 += data1; // add data2 to data1

for (;;) // loop forever


;
}

When the PIC is first programmed, the value 0 is loaded into the EEPROM variable data1, and the value 10
is loaded into data2.
As you can see, despite being stored in EEPROM, these variables can be used in expressions, in the same
way as any other variable. Of course, the code will run much more slowly than if they were ordinary
variables, but otherwise the variables are used as normal. So, when the program first runs, data1 will be
incremented to 1, and will then be added to data2, ending up with data2 storing the value 10+1 = 11.
Suppose we now power-cycle the device. If these were ordinary variables, this would all happen again, in
the same way. But, being stored in EEPROM, these variables will retain their values. Instead of being
initialised to 0, data1 retains its most recent value of 1. So, when it’s incremented, data1 now equals 2.
When this is now added to data2, which also retained its old value, we end up with data2 = 11+2 = 13.
As you can see, the fact that these variables are stored in EEPROM is transparent – it all just works. But
there’s a danger in it being so easy. You need to remember the EEPROM’s limited write endurance. If a
variable stored in EEPROM is updated too often (around 1 million times, on the PIC16F684), that EEPROM
location will eventually fail – and so will your device!

Example 1: Reading EEPROM data


As mentioned earlier, EEPROMs are sometimes used to store configuration or other data which is
programmed into the device, and then later accessed by the running program.
For example, the Gooligum Programmable Christmas Star uses the EEPROM on a PIC12F683 to store the
codes representing the patterns to be displayed. Since these patterns are stored in EEPROM instead of
program memory, they can be updated independently of the program, meaning that the users do not need
access to the program code in order to upload their own patterns.
More commonly, you might want to store just a couple of values which configure some aspects of your
device that might change from installation to installation, such as default output values or input reference
levels. Or you might want to record the hardware revision, so that your program can check which version of
the hardware it is running in, and behave appropriately. There are many possibilities!

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 4


© Gooligum Electronics 2013 www.gooligum.com.au

To illustrate this, we’ll use the 3-digit 7-segment display circuit from lesson 7:

To implement this circuit using the Gooligum training board, place shunts:
 across every position (all six of them) of jumper block JP4, connecting segments A-D, F and G to
pins RA0-1 and RC1-4
 in position 1 (‘RA/RB4’) of JP5, connecting segment E to pin RA4
 across pins 2 and 3 (‘RC5’) of JP6, connecting digit 1 to the transistor controlled by RC5
 in jumpers JP8, JP9 and JP10, connecting pins RC5, RA5 and RC0 to their respective transistors
All other shunts should be removed.

We’ll store a configuration value as a single byte, which we’ll display in hex on digits 1 and 2, and the
hardware revision as a letter between A and F, which we’ll display as a single hex digit on digit 3.
Of course a real application wouldn’t display these configuration values; it would act on them. But for this
example the idea is to show how to program data into the EEPROM and then read it – and to see that most
clearly, we should keep the program as simple as possible.

Firstly, we need to specify the values to be programmed into the EEPROM:


/***** EEPROM DATA *****/

// config data addresses


#define ee_CFG_VAL 0 // configuration value
#define ee_HW_REV 1 // hardware revision

// initial EEPROM data


__EEPROM_DATA(
// configuration value
0x23, // (single-byte)
// hardware revision
0x0E, // (single hex digit, A-F)
0, 0, 0, 0, 0, 0);

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 5


© Gooligum Electronics 2013 www.gooligum.com.au

The __EEPROM_DATA() macro specifies that the value 23h (you can use any single-byte value you want,
here) will be programmed into the start of the EEPROM (address 0), and that the value Eh (this could be any
value from 0 to F, but in keeping with the intent of this example, it should be a value between A and F) will
be loaded into the next EEPROM location (address 1).
Since the __EEPROM_DATA() macro always has to have eight parameters, we pad it out with six ‘0’s, even
though we don’t care about the values of any but the first two EEPROM locations.
Note that symbols (‘ee_CFG_VAL’ and ‘ee_HW_REV’) have been defined to represent the addresses of these
configuration values. It will make our code much easier to maintain if we can refer to these locations by
name instead of their numeric addresses.

To read the first byte of data from the EEPROM, we can use the eeprom_read() function:
config_val = eeprom_read(ee_CFG_VAL); // get config value

where ‘config_val’ is a single-byte (type uint8_t) variable.


Or, we could use the EEPROM_READ() macro instead:
config_val = EEPROM_READ(ee_CFG_VAL); // get config value

In a real application, you’d typically use a configuration value read from an EEPROM like this to set some
parameter to a default value, perhaps storing it in a variable for later access. But in this example, we’ll
simply display it (in hexadecimal) on digits 1 and 2, with the most significant nybble (“tens”) in digit 1.
To do this, we can adapt code from the hexadecimal output example in lesson 8.
We’ll store the digits to be displayed in single-byte variables: digit1, digit2 and digit3.
The content of these variables is then displayed in the background by an ISR driven by Timer0.
To read the configuration value from EEPROM and extract the hex digits from it for the ISR to display
(adapting code from the hex output example in lesson 8), we have:
// read and display configuration value
config_val = eeprom_read(ee_CFG_VAL); // get config value
digit2 = config_val & 0x0F; // extract ones digit and display in digit 2
digit1 = config_val >> 4; // extract "tens" and display in digit 1

Similarly, to read and display the hardware revision as a single hex digit on digit 3, we have:
// read and display hardware revision
hw_rev = eeprom_read(ee_HW_REV); // get hardware revision value
digit3 = hw_rev & 0x0F; // display it in digit 3
// (masked to ensure 0-F range)

Note the use of ‘& 0x0F’ (a masking operation) to ensure that the value copied to digit3 is restricted to the
single hex digit range (0h – Fh). Without this precaution, if you stored a value greater than 0Fh in the
‘ee_HW_REV’ location in the EEPROM, the code would attempt to lookup patterns beyond the end of the 7-
segment pattern tables, with potentially disastrous results.
In general, if you are going to use the EEPROM to store configuration data, it can be a good idea to include
some bounds-checking code in your application, to handle cases where the EEPROM has been programmed
with configuration values outside their acceptable range.

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 6


© Gooligum Electronics 2013 www.gooligum.com.au

Complete program (function version)


Most of the code will be familiar from the examples in lessons 7 and 8, but it’s worth seeing the full listing to
see where the EEPROM read functions and data declarations fit in:
/************************************************************************
* *
* Description: Lesson 13, example 1a *
* *
* Demonstrates use of __EEPROM_DATA() macro to declare EEPROM data *
* and EEPROM read operations, using eeprom_read() function *
* *
* Configuration data stored in EEPROM is displayed on 3-digit *
* 7-segment LED display: *
* "configuration value" in address 0 is displayed as *
* 2 x hex digits ("tens" in digits 1, "ones" in digit 2) *
* "hardware revision" (A-F) in address 1 is displayed as *
* 1 x hex digit (digit 3) *
* *
*************************************************************************
* *
* Pin assignments: *
* RA0-1,RA4, RC1-4 = 7-segment display bus (common cathode) *
* RC5 = digit 1 enable (active high) *
* RA5 = digit 2 enable *
* RC0 = digit 3 enable *
* *
************************************************************************/

#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 4000000 // oscillator frequency for _delay()

/***** CONFIGURATION *****/


// ext reset, no code or data protect, no brownout detect
#pragma config MCLRE = ON, CP = OFF, CPD = OFF, BOREN = OFF
// no watchdog, power-up timer enabled, int oscillator with I/O
#pragma config WDTE = OFF, PWRTE = ON, FOSC = INTOSCIO
// no failsafe clock monitor, two-speed start-up disabled
#pragma config FCMEN = OFF, IESO = OFF

// Pin assignments
#define sDIG1 sPORTC.RC5 // digit 1 enable (shadow)
#define sDIG2 sPORTA.RA5 // digit 2 enable
#define sDIG3 sPORTC.RC0 // digit 3 enable

/***** EEPROM DATA *****/

// config data addresses


#define ee_CFG_VAL 0 // configuration value
#define ee_HW_REV 1 // hardware revision

// initial EEPROM data


__EEPROM_DATA(
// configuration value
0x23, // (single-byte)
// hardware revision
0x0E, // (single hex digit, A-F)
0, 0, 0, 0, 0, 0);

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 7


© Gooligum Electronics 2013 www.gooligum.com.au

/***** PROTOTYPES *****/


void set7seg(uint8_t digit); // display digit on 7-segment display
// (using shadow regs)

/***** GLOBAL VARIABLES *****/


volatile union { // shadow copy of PORTA
uint8_t RA;
struct {
unsigned RA0 : 1;
unsigned RA1 : 1;
unsigned RA2 : 1;
unsigned RA3 : 1;
unsigned RA4 : 1;
unsigned RA5 : 1;
};
} sPORTA;

volatile union { // shadow copy of PORTC


uint8_t RC;
struct {
unsigned RC0 : 1;
unsigned RC1 : 1;
unsigned RC2 : 1;
unsigned RC3 : 1;
unsigned RC4 : 1;
unsigned RC5 : 1;
};
} sPORTC;

uint8_t digit1, digit2, digit3; // display variables (displayed by ISR)

/***** MAIN PROGRAM *****/


void main()
{
uint8_t config_val; // configuration value (read from EEPROM)
uint8_t hw_rev; // hardware revision (read from EEPROM)

/*** Initialisation ***/

// configure ports
TRISA = 0; // configure PORTA and PORTC as all outputs
TRISC = 0;

// configure Timer0
OPTION_REGbits.T0CS = 0; // select timer mode
OPTION_REGbits.PSA = 0; // assign prescaler to Timer0
OPTION_REGbits.PS = 0b010; // prescale = 8
// -> increment every 8 us
// -> TMR0 overflows every 2.048 ms

// Read config data from EEPROM


//
// read and display configuration value
config_val = eeprom_read(ee_CFG_VAL); // get config value
digit2 = config_val & 0x0F; // extract ones digit and display in digit 2
digit1 = config_val >> 4; // extract "tens" and display in digit 1
//
// read and display hardware revision
hw_rev = eeprom_read(ee_HW_REV); // get hardware revision value

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 8


© Gooligum Electronics 2013 www.gooligum.com.au

digit3 = hw_rev & 0x0F; // display it in digit 3


// (masked to ensure 0-F range)

// enable interrupts
INTCONbits.T0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts

/*** Main loop ***/


for (;;)
{
// do nothing
;
}
}

/***** INTERRUPT SERVICE ROUTINE *****/


void interrupt isr(void)
{
static uint8_t mpx_cnt = 0; // multiplex counter

// *** Service Timer0 interrupt


//
// TMR0 overflows every 2.048 ms
//
// Displays current count on 7-segment displays
//
// (only Timer0 interrupts are enabled)
//
INTCONbits.T0IF = 0; // clear interrupt flag

// Display current count on 3 x 7-segment displays


// mpx_cnt determines current digit to diplay
//
switch (mpx_cnt)
{
case 0:
set7seg(digit1); // output ones digit
sDIG1 = 1; // enable ones display
break;
case 1:
set7seg(digit2); // output tens digit
sDIG2 = 1; // enable tens display
break;
case 2:
set7seg(digit3); // output minutes digit
sDIG3 = 1; // enable minutes display
break;
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 3) // reset count if at end of digit sequence
mpx_cnt = 0;

// Copy shadow regs to ports


PORTA = sPORTA.RA;
PORTC = sPORTC.RC;
}

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 9


© Gooligum Electronics 2013 www.gooligum.com.au

/***** FUNCTIONS *****/

/***** Display digit on 7-segment display (using shadow regs) *****/


void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on port A
const uint8_t pat7segA[16] = {
// RA4 = E, RA1:0 = FG
0b010010, // 0
0b000000, // 1
0b010001, // 2
0b000001, // 3
0b000011, // 4
0b000011, // 5
0b010011, // 6
0b000000, // 7
0b010011, // 8
0b000011, // 9
0b010011, // A
0b010011, // b
0b010010, // C
0b010001, // d
0b010011, // E
0b010011 // F
};

// pattern table for 7 segment display on port C


const uint8_t pat7segC[16] = {
// RC4:1 = CDBA
0b011110, // 0
0b010100, // 1
0b001110, // 2
0b011110, // 3
0b010100, // 4
0b011010, // 5
0b011010, // 6
0b010110, // 7
0b011110, // 8
0b011110, // 9
0b010110, // A
0b011000, // b
0b001010, // C
0b011100, // d
0b001010, // E
0b000010 // F
};

// lookup pattern bits and write to shadow registers


sPORTA.RA = pat7segA[digit];
sPORTC.RC = pat7segC[digit];
}

Alternatively, you can use the EEPROM_READ() macro to read the EEPROM, in which case you simply
substitute “EEPROM_READ” for “eeprom_read”; the program code is otherwise exactly the same.

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 10


© Gooligum Electronics 2013 www.gooligum.com.au

Comparisons
We’ve seen that the eeprom_read()function and EEPROM_READ() macro are essentially interchangeable,
so which one should you use?
The macro will produce faster code than the function, but whether it uses less memory will depend on how
many times it is called.
The following table shows the source code length and program and data memory usage (as reported by
MPLAB) for the function and macro versions of this example, using v1.12 of the XC8 compiler running in
“Free mode”. The size of the corresponding assembly language example from mid-range assembler lesson
19 is also given, for comparison:
EEPROM_read
Source code Program memory Data memory
Version
(lines) (words) (bytes)

Assembly language 140 150 8


XC8 (function version) 111 217 19
XC8 (macro version) 111 204 17

With only two EEPROM read operations in this example, it’s not surprising that the version using the
EEPROM_READ() macro generates the smallest XC8 code in this instance.

Example 2: Writing EEPROM data


One of the key features of an EEPROM is that it can be written to at run-time. You might do this
periodically to log data, or to record changed configuration settings – or any situation where you need to
record variable data which will be retained when the PIC is powered off.
To illustrate this, we’ll re-implement the 3-digit timer from lesson 7, using the same circuit as in the previous
example.
In lesson 7, the timer counted minutes and seconds, starting from 0:00 every time it was powered on.
We will modify it so that, if the power is removed, it does not forget the current count; when it is powered on
again, it will continue from the previous count.
To achieve this, we will store a copy of the current count in the EEPROM, updating it every second4.

The count is stored as minutes in one single-byte variable and seconds in another:
uint8_t mins, secs; // time counters (displayed by ISR)

4
As mentioned earlier, the EEPROM’s limited write endurance (typically 1 million on the PIC16F684) means that this
is a poor strategy – if we update the same location every second, the EEPROM would typically fail by around 12 days
of continual use. One way to alleviate this problem is to spread the “damage”, by successively writing to different
locations in the EEPROM, so that no single location fails early from being written more times than the rest of the
EEPROM. A better solution, if we want to preserve data when the device is powered off, is to detect the imminent loss
of power and to only update the EEPROM at that point. However, to do so requires hardware support (e.g. power rail
monitoring and a capacitor large enough to hold the MCU up long enough to complete the EEPROM write), and
illustrating that would take away from the mechanics of writing to the EEPROM, which we are trying to demonstrate.

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 11


© Gooligum Electronics 2013 www.gooligum.com.au

Whenever the program starts, we need to read the stored count from the EEPROM, and copy it to these
variables.
We could do this in the same way as in the last example, with the eeprom_read()function:
mins = eeprom_read(ee_MINS); // get stored minutes
secs = eeprom_read(ee_SECS); // and seconds values

or the EEPROM_READ() macro:


mins = EEPROM_READ(ee_MINS); // get stored minutes
secs = EEPROM_READ(ee_SECS); // and seconds values

Either way, we need to program initial values into the EEPROM, so that when the program first runs the
count will start at some specified value (let’s say 0:00).
Again, this can be done in the same was as in the first example, using the __EEPROM_DATA() macro:
/***** EEPROM DATA *****/

// addresses of stored count values


#define ee_MINS 0 // minutes storage
#define ee_SECS 1 // seconds storage

// initial EEPROM data


__EEPROM_DATA(
0, // minutes = 0
0, // seconds = 0
// (start with count = 0:00)
0, 0, 0, 0, 0, 0);

So far, this is no different from the previous example. We specify initial values to be programmed into the
EEPROM, and read them when our program starts.
But in this example, we’re going to update the values stored in the EEPROM, whenever the count changes.

The 3-digit timer example in lesson 7 used nested for loops to cycle the minutes and seconds from 0 to 9
and 0 to 59 respectively, but we can’t take that approach here, because we need to start from the values
we’ve retrieved from EEPROM. Instead, we can implement the counting logic like this:
// Increment counters
if (++secs == 60)
{
secs = 0;
if (++mins == 10)
mins = 0;
}

After incrementing the count, we need to write the updated seconds and minutes values to the EEPROM,
which we can do with the eeprom_write()function:
eeprom_write(ee_SECS, secs); // store seconds
eeprom_write(ee_MINS, mins); // and minutes to EEPROM

Alternatively, you could use the EEPROM_WRITE() macro:


EEPROM_WRITE(ee_SECS, secs); // store seconds
EEPROM_WRITE(ee_MINS, mins); // and minutes to EEPROM

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 12


© Gooligum Electronics 2013 www.gooligum.com.au

As you can see, the function and macro versions are used exactly the same way; the only differences being
that the EEPROM_WRITE() macro will execute more quickly than the eeprom_write()function, but is
likely to require more program memory if used a number of times.

Complete program (macro version)


Most of the program code is taken from the 3-digit timer example in lesson 7, but it’s worth seeing the full
listing to see how the EEPROM read and write macros fit in:
/************************************************************************
* Description: Lesson 13, example 2b *
* *
* Demonstrates EEPROM data write operations, *
* using EEPROM_WRITE() macro *
* *
* 3 digit 7-segment LED display: 1 digit minutes, 2 digit seconds *
* counts in seconds 0:00 to 9:59 then repeats. *
* *
* The initial count is read from EEPROM. *
* The count is written to EEPROM whenever it is updated (each second) *
* *
*************************************************************************
* Pin assignments: *
* RA0-1,RA4, RC1-4 = 7-segment display bus (common cathode) *
* RC5 = minutes digit enable (active high) *
* RA5 = tens digit enable *
* RC0 = ones digit enable *
************************************************************************/

#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 4000000 // oscillator frequency for _delay()

/***** CONFIGURATION *****/


// ext reset, no code or data protect, no brownout detect
#pragma config MCLRE = ON, CP = OFF, CPD = OFF, BOREN = OFF
// no watchdog, power-up timer enabled, int oscillator with I/O
#pragma config WDTE = OFF, PWRTE = ON, FOSC = INTOSCIO
// no failsafe clock monitor, two-speed start-up disabled
#pragma config FCMEN = OFF, IESO = OFF

// Pin assignments
#define sMINS sPORTC.RC5 // minutes digit enable (shadow)
#define sTENS sPORTA.RA5 // tens digit enable
#define sONES sPORTC.RC0 // ones digit enable

/***** EEPROM DATA *****/

// addresses of stored count values


#define ee_MINS 0 // minutes storage
#define ee_SECS 1 // seconds storage

// initial EEPROM data


__EEPROM_DATA(
0, // minutes = 0
0, // seconds = 0
// (start with count = 0:00)
0, 0, 0, 0, 0, 0);

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 13


© Gooligum Electronics 2013 www.gooligum.com.au

/***** PROTOTYPES *****/


void set7seg(uint8_t digit); // display digit on 7-segment display
// (using shadow regs)

/***** GLOBAL VARIABLES *****/


volatile union { // shadow copy of PORTA
uint8_t RA;
struct {
unsigned RA0 : 1;
unsigned RA1 : 1;
unsigned RA2 : 1;
unsigned RA3 : 1;
unsigned RA4 : 1;
unsigned RA5 : 1;
};
} sPORTA;

volatile union { // shadow copy of PORTC


uint8_t RC;
struct {
unsigned RC0 : 1;
unsigned RC1 : 1;
unsigned RC2 : 1;
unsigned RC3 : 1;
unsigned RC4 : 1;
unsigned RC5 : 1;
};
} sPORTC;

uint8_t mins, secs; // time counters (displayed by ISR)

/***** MAIN PROGRAM *****/


void main()
{
/*** Initialisation ***/

// configure ports
TRISA = 0; // configure PORTA and PORTC as all outputs
TRISC = 0;

// configure Timer0
OPTION_REGbits.T0CS = 0; // select timer mode
OPTION_REGbits.PSA = 0; // assign prescaler to Timer0
OPTION_REGbits.PS = 0b010; // prescale = 8
// -> increment every 8 us
// -> TMR0 overflows every 2.048 ms

// get stored count from EEPROM


mins = EEPROM_READ(ee_MINS); // get stored minutes
secs = EEPROM_READ(ee_SECS); // and seconds values

// enable interrupts
INTCONbits.T0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts

/*** Main loop ***/


for (;;)
{

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 14


© Gooligum Electronics 2013 www.gooligum.com.au

// Delay 1 second
__delay_ms(1000);

// Increment counters
if (++secs == 60)
{
secs = 0;
if (++mins == 10)
mins = 0;
}

// Update count stored in EEPROM


EEPROM_WRITE(ee_SECS, secs); // store seconds
EEPROM_WRITE(ee_MINS, mins); // and minutes to EEPROM
}
}

/***** INTERRUPT SERVICE ROUTINE *****/


void interrupt isr(void)
{
static uint8_t mpx_cnt = 0; // multiplex counter

// *** Service Timer0 interrupt


//
// TMR0 overflows every 2.048 ms
//
// Displays current count on 7-segment displays
//
// (only Timer0 interrupts are enabled)
//
INTCONbits.T0IF = 0; // clear interrupt flag

// Display current count on 3 x 7-segment displays


// mpx_cnt determines current digit to diplay
//
switch (mpx_cnt)
{
case 0:
set7seg(secs%10); // output ones digit
sONES = 1; // enable ones display
break;
case 1:
set7seg(secs/10); // output tens digit
sTENS = 1; // enable tens display
break;
case 2:
set7seg(mins); // output minutes digit
sMINS = 1; // enable minutes display
break;
}
// Increment mpx_cnt, to select next digit for next time
mpx_cnt++;
if (mpx_cnt == 3) // reset count if at end of digit sequence
mpx_cnt = 0;

// Copy shadow regs to ports


PORTA = sPORTA.RA;
PORTC = sPORTC.RC;
}

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 15


© Gooligum Electronics 2013 www.gooligum.com.au

/***** FUNCTIONS *****/

/***** Display digit on 7-segment display (using shadow regs) *****/


void set7seg(uint8_t digit)
{
// pattern table for 7 segment display on port A
const uint8_t pat7segA[10] = {
// RA4 = E, RA1:0 = FG
0b010010, // 0
0b000000, // 1
0b010001, // 2
0b000001, // 3
0b000011, // 4
0b000011, // 5
0b010011, // 6
0b000000, // 7
0b010011, // 8
0b000011 // 9
};

// pattern table for 7 segment display on port C


const uint8_t pat7segC[10] = {
// RC4:1 = CDBA
0b011110, // 0
0b010100, // 1
0b001110, // 2
0b011110, // 3
0b010100, // 4
0b011010, // 5
0b011010, // 6
0b010110, // 7
0b011110, // 8
0b011110 // 9
};

// lookup pattern bits and write to shadow registers


sPORTA.RA = pat7segA[digit];
sPORTC.RC = pat7segC[digit];
}

Alternatively, you can use the eeprom_read() and eeprom_write() functions to read and write the
EEPROM, and the in which case you simply substitute “eeprom_read” for “EEPROM_READ” and
“eeprom_write” for “EEPROM_WRITE” ; the program code is otherwise exactly the same.

Example 3: Using EEPROM variables


Although it’s certainly possible to use EEPROM access macros or functions to initialise, read and write
variables stored in EEPROM, as we did in the last example, it’s often much easier to simply use the
complier’s ‘eeprom’ qualifier to place variables directly into EEPROM.
To illustrate this, we’ll re-implement the previous example, using EEPROM variables.

As explained earlier, EEPROM variables can be initialised as part of their definition, just like any other
variable. However, they must be defined as global variables, or, if defined within a function, they must be
qualified as ‘static’.

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 16


© Gooligum Electronics 2013 www.gooligum.com.au

So, we can define our variables, used to store a copy of the current count in EEPROM, within the main()
function as5:
static eeprom uint8_t ee_mins = 0; // stored count
static eeprom uint8_t ee_secs = 0; // (start with count = 0:00)

Note that there is no longer any need to use the __EEPROM_DATA() macro to initialise the EEPROM data; it
is done as part of the variable definition.

Reading the stored count is then as simple as:


mins = ee_mins; // get stored minutes
secs = ee_secs; // and seconds values

And updating the stored values, after updating the count, is just as easy:
ee_secs = secs; // store seconds
ee_mins = mins; // and minutes to EEPROM

That’s really all there is to it – it’s that simple.


The fact that these ‘ee_secs’ and ‘ee_mins’ variables happen to be stored in EEPROM is quite
transparent. The only difference is that using them (especially when writing to them) is very slow compared
with ordinary variables. And, as mentioned earlier, if you write to an EEPROM variable too many times, the
underlying EEPROM location will fail – so be careful and only update these variables when you need to!

Main code (variable version)


Most of the program is the same as in the previous example (and the interrupt and display functions are
exactly the same), but it’s worth seeing the main program code, to see where these EEPROM variable
definitions and read and write statements fit in:
/***** MAIN PROGRAM *****/
void main()
{
// EEPROM variables
static eeprom uint8_t ee_mins = 0; // stored count
static eeprom uint8_t ee_secs = 0; // (start with count = 0:00)

/*** Initialisation ***/

// configure ports
TRISA = 0; // configure PORTA and PORTC as all outputs
TRISC = 0;

// configure Timer0
OPTION_REGbits.T0CS = 0; // select timer mode
OPTION_REGbits.PSA = 0; // assign prescaler to Timer0
OPTION_REGbits.PS = 0b010; // prescale = 8
// -> increment every 8 us
// -> TMR0 overflows every 2.048 ms

5
These could have been defined before main(), without the ‘static’ qualifier, but because they will only be used
within main() it’s better to define them as (static) local variables, to avoid namespace conflicts and make the code
more maintainable.

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 17


© Gooligum Electronics 2013 www.gooligum.com.au

// get stored count from EEPROM


mins = ee_mins; // get stored minutes
secs = ee_secs; // and seconds values

// enable interrupts
INTCONbits.T0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts

/*** Main loop ***/


for (;;)
{
// Delay 1 second
__delay_ms(1000);

// Increment counters
if (++secs == 60)
{
secs = 0;
if (++mins == 10)
mins = 0;
}

// Update count stored in EEPROM


ee_secs = secs; // store seconds
ee_mins = mins; // and minutes to EEPROM
}
}

Comparisons
As we did in the first example, it’s worth looking that the size and resource usage of our various “EEPROM
write” examples, to help compare the different access methods: functions, macros and variables.
The following table shows the source code length and program and data memory usage (as reported by
MPLAB) for the function and macro versions of example 2, along with the EEPROM variable version from
this example, using v1.12 of the XC8 compiler running in “Free mode”. The size of the corresponding
assembly language example from mid-range assembler lesson 19 is also given, for comparison:
EEPROM_write
Source code Program memory Data memory
Version
(lines) (words) (bytes)

Assembly language 180 202 11


XC8 (function version) 101 522 26
XC8 (macro version) 101 529 23
XC8 (variable version) 97 610 31

Even with only two EEPROM write operations in this example, the version using macros generates larger
code than the version using functions.
Perhaps more interesting is the comparison with the version using EEPROM variables. We’ve seen that it is
simpler to use EEPROM variables, and that is reflected in the source code being shorter. But the generated
code is bigger, and uses more data memory. As often happens, there is a trade-off between ease of use and
efficiency… Not as extreme as the difference between C and assembly language, but still significant.

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 18


© Gooligum Electronics 2013 www.gooligum.com.au

Summary
We’ve seen in this lesson that through the availability of functions, macros and the ability to place variables
in EEPROM, the XC8 compiler makes it very easy to initialise, read and write EEPROM data.
In particular, XC8 shields us from the relative complexity of EEPROM write operations that we saw in mid-
range assembler lesson 19 – when using XC8, writing data to the EEPROM becomes as straightforward as
reading from it.

We have now described every major feature of the PIC16F684, concluding our introduction to programming
the “classic” mid-range PIC architecture in C.
Although some other, more advanced mid-range devices, such as the 16F690 and 16F887, include additional
peripherals, notably USART and MSSP modules used for serial communications, including SPI and I2C
interfacing, the original mid-range architecture described in this series of lessons has become dated. The
newer “enhanced mid-range” PIC architecture offers increased functionality, at a lower cost.
A new series of lessons will explore the enhanced mid-range PIC architecture, revisiting the topics covered
in this series, before moving on to more advanced peripherals, including serial communications.

Until then, enjoy programming mid-range PICs in C!

Mid-range PIC C, Lesson 13: Using EEPROM Data Memory Page 19

You might also like