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
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!
<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 )
0 8 0 do
1 lshift
eerxbit if 1 or then
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
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 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
here aligned 0 do
i @ i ee!
i FF and 0= if
." ..." i . cr
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.


PropellerForth stirrings

A few weeks ago, I was poking around on the Silicon Valley Forth Interest Group webpage. I was surprised to see this in their minutes:
There has been no success in contacting the author of Propeller Forth - written locally by Cliff L. Biffle

As of about February 2007, my email got so clogged with spam that I simply stopped checking it. (Apologies to anyone who's been trying to contact me.) So, I had no idea that the SVFIG was interested in PropellerForth. I've had isolated inquiries from a couple people, but all in all, my original posts (here and on the Parallax forums) were met with general disinterest.

So, I've resurrected the project. Not much progress yet -- mostly, I'm cleaning up the sources so I can release them without sullying my good name. :-) People poking around the PropellerForth site on might notice some documentation appearing in preparation for the source release.

Ever since I moved out of my folks' house, I've gone back and spent a few nights there for Christmas. Since my sleep schedule is rather different from theirs, I wind up having a lot of free time -- and since I'm away from work and distraction, it's traditionally been a great opportunity to hack. (Projects that got their start or reached significant milestones over Christmas include Mongoose, Cesta, PropellerForth -- and, 15 years earlier, WebElite.)

This year I learned the NTSC spec, the US analog television standard. The existing NTSC drivers for the Propeller are cryptic and don't build with propasm, so I wrote a very simple one with more lines of comment than code. It generates a 40x16 B&W text display, uses the ROM font to save space (compressing it on the fly to 8x16 pixels), and requires 336 bytes of code space and 640 bytes of framebuffer. As an experiment, I've tied it into PropellerForth.

It probably won't be part of the base image I release -- it bloats the image beyond my 8KiB target -- but between it and my PS/2 driver from Christmas 2006, I've nearly got a standalone console working.

(NTSC, incidentally, is a pain compared to VGA -- but most of the complexity, as my dad pointed out, is a result of sending all the data amplitude-modulated on a single channel, instead of VGA's five. It's kind of impressive that they got this to work in the 30s.)

So, if anyone following my blog is interested in PropellerForth, let this at least indicate that your interest may not be in vain.

Labels: ,