Monday, 7 September 2015

Reading and writing EEPROM on a PIC

It's amazing that's it's taken 'til now to need to write a routine like this. For ages, we've been using compilers that allow you to simply write

Read address, value
Write address, value

and store away byte values to the microcontroller's own internal eeprom.
But the latest Oshonsoft PIC compilers (still the best simulators, if not exactly the best compilers for PIC microcontrollers around) only have limited support for some of the latest PIC range.

Which means that we've had to roll our sleeves up and get dirty with some assembly language, just to write a couple of bytes to/from eeprom. We were at the stage of giving up (having worked almost all weekend getting nowhere with it) and finally made a breakthrough.

There are a couple of gotchas to look out for, so here's a couple of routines for anyone else having similar problems.

     EEADRL = eeprom_addr
     EECON1.CFGS = 0
     EECON1.RD = 1 'set the read bit to start reading
     ASM: nop
     eeprom_byte = EEDATL

     'disable interrupts while we write to eeprom
     b = INTCON.GIE
     INTCON.GIE = 0

     'set the eeprom address
     EEADRL = eeprom_addr

     EECON1.CFGS = 0
     EECON1.EEPGD = 0
     EECON1.WREN = 1 'set the eeprom write enable bit to start writing

     'set the eeprom value
     EEDATL = eeprom_byte

     'these commands need to be executed with no other instructions between them
     'so we've written them out in assembly, just to be sure the compiler doesn't
     'add in any extra crap that might stop it from working!
     ASM: banksel EECON2
     ASM: movlw 0x55 ;
     ASM: movwf EECON2 ;Write 55h
     ASM: movlw 0xaa ;
     ASM: movwf EECON2 ;Write aah
     ASM: bsf EECON1,WR ;set WR Bit To begin Write
     While EECON1.WR = 1
          'wait for it to finish
     'disable writing to eeprom until this has finished
     EECON1.WREN = 0

     'although we don't use eeprom interrupts, the EEIF flag will still be set.
     PIR2.EEIF = 0 'clear the eeprom write done interrupt flag
     'now re-enable global interrupts if they were previously set.
     INTCON.GIE = b
     'this shouldn't be necessary since we waited for the write cycle to complete
     WaitMs 4

When it comes to reading eeprom, the datasheet makes things nice a clear. Set the address you want to read from, set the "start reading" bit in the corresponding register, and the value you're after appears in the EEDATL register. Nice and easy.

Writing data to eeprom is not so easy.
And until we'd actually proved that our reading routine - which seemed so laughably simple by comparison - was working properly, finding the actual cause of the problems, when nothing seemed to work, was a real headache.

Now the main gotcha when writing to eeprom is that there's a very specific bit sequence that you have to write, in a very specific way, to a very specific register - that doesn't actually exist as a physical register - just in case the chip were to boot up in a peculiar state and go rogue, overwriting eeprom memory (and, in a worst case program) locations with junk.

The critical thing to do, when writing to eeprom, before you can set the "start writing" bit, is send the bit pattern 01010101 (0x55) followed by 10101010 (0xaa) to the virtual register EECON2. Then - and only then - can you set the "start writing" bit WR of EECON1. And you must do it immediately after sending the 0x55, 0xaa pattern.

The instructions have to be written as five consecutive commands, with no other instructions between them. That means no delays or wait commands between each, global interrupts really need to be turned off, and you need to be sure that your compiler isn't adding in any extra junk you don't want.

In fact, if you're not careful writing these instructions, you can easily get extra stuff in there that you didn't even think about. For example, EECON1.WR=1 seems innocuous enough - set the WR bit of the EECON1 register. But how is the compiler doing it?

Maybe it's taking the existing EECON1 value, bitwise OR-ing it with some bitmask that identifies the WR bit (if WR is the bit two, this mask would be 00000100) and then setting EECON1 to this new result. If this is how the compiler works, then there's a whole load of other junk in the middle of the eeprom-specific sequence.

So to make sure that our compiler isn't adding anything else in, we actually hand-crafted some assembly in the write_eeprom routine (it's been a good few years since any of us did any assembly programming, and took a while for the rusty cogs to get spinning again!). The key part is highlighted above

     ASM: movlw 0x55 ;
     ASM: movwf EECON2 ;Write 55h
     ASM: movlw 0xaa ;
     ASM: movwf EECON2 ;Write aah
     ASM: bsf EECON1,WR ;set WR Bit To begin Write

These five lines have to appear exactly as written in order to write to the eeprom register(s). Yet despite writing this assembly directly into the write_eeprom routine, we still couldn't get our values committed to eeprom between power-cycles (the sure-fire way of testing that the value has been written to non-volatile memory, rather than pluck a value from RAM).

It has taken us nearly three days, copious amounts of coffee and a massive "a-ha!" when the answer was revealed - before accessing the EECON2 register, we have to make sure that the correct bank is selected on the PIC.

This is a gotcha from over twelve years ago, when a few of us were starting out, writing simple (and sometimes not-so-simple) routines in assembly on the trusty old PIC16F877A range. Quite often we'd spend hours debugging assembly routines, only to find we'd not switched to the correct "bank" before trying to access a register.

And - over a decade later - the same problem once-again bit at least one of us cleanly on the arse. And, having remembered about bank-switching, our read and write routines suddenly started working properly! So here they are, for anyone else having a similar problem. Whatever language you're working in, here are a couple of routines for writing to onboard eeprom, old-school....