Professional Documents
Culture Documents
Osmeoisis 2022-09-06 15-33-19PIC - Mid - C - 13
Osmeoisis 2022-09-06 15-33-19PIC - Mid - C - 13
Osmeoisis 2022-09-06 15-33-19PIC - Mid - C - 13
au
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
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.
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.
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.
3
for details, see example 2 in mid-range assembler lesson 19
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.
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;
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!
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.
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
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.
#include <xc.h>
#include <stdint.h>
// Pin assignments
#define sDIG1 sPORTC.RC5 // digit 1 enable (shadow)
#define sDIG2 sPORTA.RA5 // digit 2 enable
#define sDIG3 sPORTC.RC0 // digit 3 enable
// 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
// enable interrupts
INTCONbits.T0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts
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.
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)
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.
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.
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
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 *****/
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
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.
#include <xc.h>
#include <stdint.h>
// Pin assignments
#define sMINS sPORTC.RC5 // minutes digit enable (shadow)
#define sTENS sPORTA.RA5 // tens digit enable
#define sONES sPORTC.RC0 // ones digit enable
// 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
// enable interrupts
INTCONbits.T0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts
// Delay 1 second
__delay_ms(1000);
// Increment counters
if (++secs == 60)
{
secs = 0;
if (++mins == 10)
mins = 0;
}
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.
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’.
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.
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
// 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.
// enable interrupts
INTCONbits.T0IE = 1; // enable Timer0 interrupt
ei(); // enable global interrupts
// Increment counters
if (++secs == 60)
{
secs = 0;
if (++mins == 10)
mins = 0;
}
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)
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.
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.