Cliff Hacks Things.

Thursday, December 27, 2007

PropellerForth I2C EEPROM driver

This afternoon, I curled up with the datasheets for Atmel's I2C EEPROMs, like the one the Propeller uses to boot. I haven't written any I2C code in a while, so I had to relearn the protocol.

Then, I went out for phở. The place down the street is quite tolerant of me sitting for half an hour drawing state diagrams.

Noodles completed, I set to work interfacing PropellerForth to the boot EEPROM. It's been a resounding success. The sources are below; they should work on the old 20061124 image, if anyone still has a copy. Otherwise, they'll work on the 20080101 image once I make it available.

Executive summary: these words implement a hardcoded interface to an EEPROM on pins 29:28. This lets you access data stored in EEPROM (using ee@ and ee!). For the first time, you can save a bootable image of your current PropellerForth system to EEPROM, using the saveforth word.

The emphasis there shows how excited I am about this. Bootstrapping my NTSC display driver requires me to carefully type in Forth code for several minutes, each time I reboot! The words are factored so I can test as I go, but it's still painful. This is about as revolutionary as when I got cassette storage working with my TRS-80 Model 100 and no longer had to type in BASIC listings.

The interface is not spectacularly fast; page reads can stream data at about 20kbps, and page writes aren't implemented yet. saveforth can take a minute or two. It is, however, pretty simple and readable, assuming you read Forth.

If you don't read Forth, it's worth taking a glance at the code anyway. It's not the best Forth code -- I'm rusty -- but casual observers may notice that code toward the top of the listing tends to be low-level (written in terms of machine registers like OUTA). As you progress through the listing, the language changes: first to a physical-level description of I2C, then to a layer-2 description of how to talk to the EEPROM. In my day job we refer to this as a domain-specific language. Forth programmers didn't coin this term, because they didn't need one: it's the natural way to build systems in Forth.

So: the code! I've hand-escaped this, and may have introduced errors in the process.

Update: sneaky bug fixed by addition of mkboot word. Details below the source listing for anyone who doesn't want to figure it out for themselves. :-)


hex \ my dad will surely rib me for not using octal

\ Sets the value of an output pin (identified by 'mask').
\ Output is 0 if 'f' is false, 1 otherwise.
: pin! ( f mask -- )
swap if
OUTA L@ or
else
OUTA L@ swap -and
then OUTA L! ;

\ Sets a pin (identified by 'mask') to output.
: pin> ( mask -- )
DIRA L@ or DIRA L! ;

\ Sets a pin (identified by 'mask') to input.
: <pin ( mask -- )
dira L@ -and dira L! ;

\ I2C pin masks. Workaround for CONSTANT bug (CONSTANTs are 16-bit).
: eesdamask 20000000 ; \ pin 29
: eesclmask 10000000 ; \ pin 28

\ Words for working with the EEPROM SDA line.
: eesda! ( f -- ) eesdamask pin! ;
: eesda@ ( -- f ) INA L@ eesdamask and ;
: eesda> eesdamask pin> ;
: <eesda eesdamask <pin ;

\ Words for working with the EEPROM SCL line.
: eescl! ( f -- ) eesclmask pin! ;
: eescl> eesclmask pin> ;
: <eescl eesclmask <pin ;

\ Initializes the bus; useful for resetting a b0rked chip
: eeinit
\ SCL high/output, SDA input
1 eescl! eescl> <eesda

\ Drain the device until we get an ACK, or we try 9 times
9 0 do
0 eescl! 1 eescl!
eesda@ if unloop exit then
loop ;

\ I2C start condition
: eestart
1 eescl! eescl>
1 eesda! eesda> 0 eesda!
0 eescl! ;

\ I2C stop condition
: eestop
1 eescl!
1 eesda!
<eescl
<eesda ;

: eerxbit ( -- bit ) 1 eescl! eesda@ 0 eescl! ;

\ Transmits a byte, MSB first. Returns ack bit.
: eetx ( byte -- ackbit )
8 0 do
dup 80 and eesda!
1 lshift
1 eescl! 0 eescl!
loop drop
\ Read ack bit
<eesda eerxbit
0 eesda! eesda> ;

\ Receives 8 bits, sending 'ackbit' in response
: eerx ( ackbit -- byte )
<eesda
0 8 0 do
1 lshift
eerxbit if 1 or then
loop
swap eesda! eesda>
1 eescl! 0 eescl! ;

\ Like eetx, but throws file error if not acked.
: eetx? ( byte -- )
eetx if -25 throw then ;

\ Sends a two-byte big-endian address.
: eetxaddr? ( addr -- )
dup 8 rshift FF and eetx?
FF and eetx? ;

\ Reads a byte out of EEPROM. Throws on failure.
\ If you're feeling cryptic you could rename this
\ eec@.
: eeread ( addr -- byte )
eestart A0 eetx? eetxaddr?
eestart A1 eetx? 1 eerx
eestop ;

\ Writes a byte to EEPROM. Throws on failure.
: eewrite ( byte addr -- )
eestart A0 eetx? eetxaddr? eetx? eestop ;

\ Reads a little-endian 32-bit integer from EEPROM.
\ Throws on failure.
\ Eventually this should use page read.
: ee@ ( addr -- x )
0 4 0 do
8 rshift
over eeread 18 ( hex ) lshift or
swap 1+ swap
loop nip ;

\ Waits for the EEPROM to become available. Required
\ after a write operation unless you're damn sure you're
\ going to be busy for a while.
: eewait begin eestart A0 eetx eestop 0= until ;

\ Writes a little-endian 32-bit integer into EEPROM.
\ Throws on failure.
: ee! ( x addr -- )
4 0 do
2dup eewrite
1+ swap 8 rshift swap
eewait
loop 2drop ;

\ Reads 'count' bytes from the EEPROM starting at 'addr'
\ into a buffer at 'dest'.
: eereadpage ( dest count addr -- )
over 0= if drop drop drop exit then
eestart A0 eetx? eetxaddr?
eestart A1 eetx?
( count ) 1- 0 do
0 eerx over c!
1+
loop
1 eerx swap c!
eestop ;

\ Updates fields in the image preamble to allow
\ us to boot properly.
: mkboot
here 08 H!
here 08 + 0A H!
here 0C + 0E H!
here 10 - 10 H! ;

\ The word that made my evening: saves the
\ current dictionary, kernel, and bootloader
\ as a bootable image in EEPROM. Takes a
\ while, but prints cute little status updates.
\ The resulting image will boot to the interpreter
\ running the standard startup vector.
\
\ CAVEAT HACKER: this will erase whatever's in
\ EEPROM, in case that wasn't apparent.
: saveforth
." Saving 0 to " here aligned . cr
mkboot
here aligned 0 do
i @ i ee!
i FF and 0= if
." ..." i . cr
then
4 +loop
." Done." ;


Update about mkboot: the original listing I posted contained a subtle bug. When booting the saved EEPROM image, 12 bytes in the middle of your first user-added colon definition were destroyed. I didn't notice this because my first def was some test word that I didn't use after boot.

As described in my article on reverse-engineering the Propeller's image format, the SPIN interpreter needs some RAM to start. My image preamble (as used in propasm, and by extension PropellerForth) uses 12 bytes of RAM for the initial SPIN stack before the machine code is loaded.

The preamble tells SPIN where to put its stack; propasm indicates that it should go right after the end of the code. But if you've added code at runtime and saved Forth, the preamble needs to be updated -- otherwise SPIN will clobber an area just past the end of the original image, which is probably in the middle of a definition.

So, mkboot updates the preamble.

Labels:

217 Comments:

Post a Comment

<< Home