First of all, my hardware is right out of Garth's
https://wilsonminesco.com/6502primer/potpourri.html#BITBANG_I2C, where I just use pullup resistors, put a 0 in the VIA output register, and then play with the DDR (Data Direction Register) to fake an open collector. I was originally using external transistors to make actual open collector outputs, but then saw Garth's solution and it was much simpler hardware-wise. The only odd thing about it is that a "0" in the data direction register (making the pin an input) results in a "1" on the bus and vice versa.
adrianhudson wrote:
As for the book - that was what I was eluding to all those years ago - I think I read it in 1981 - it is great for learning Forth at the conceptual level but actually poking the hardware is what I am after. It nearly gets there in the I-O chapter and you get me a lot closer in your i2c page - I just need to put it all together.
Well, my sources have both Forth and assembly versions for some of the the same words (the Forth words for clocking were a bit slow), however I wrote them before Tali had an assembler so I was just using
C, (compile a byte into the dictionary) to stuff the opcodes directly into the word. Tali is an STC (Subroutine Threaded Code) Forth so you don't need to do anything special to insert assembly and you can use Forth and assembly when defining words - other Forths have special words for getting into assembly mode, like CODE (similar to
: eg. immediately followed by name of new word being defined, but then followed by assembly) in Garth's code. Also, I hadn't learned about the fancy bit manipulation instructions of the 65C02 yet, so my code does the longer "read in value, OR/AND to change the bit you want, write back to hardware" method. It runs fast enough for me that I haven't tried to make it faster. I didn't originally post it because it's "ugly" code that I played with until it worked well enough and then left it
My routines are similar to Garth's and somewhat based on an I2C library I had already written for a different micro. In particular, we handle the NAK (often used to signal the last byte of a transfer) a bit differently, and I poll (see if EEPROM is ready to talk to) after an EEPROM page write while Garth polls before reading or writing. I think both Garth's and my code should give you an idea on how to interface with hardware. Look at the commented out words in my code for the pure-Forth versions (just above the op-code assembly versions). For 8-bit registers, you'll want to know
C@ and
C! which fetch and store a single byte. The
allow-native after some words is Tali-specific and allows short words to be inlined (the assembly code is literally copy/pasted into the target word) instead of being compiled as a JSR (for speed at the expense of memory usage).
Allow-native affects the the word most recently defined and the words can't have any loops or branches in them - see the TaliForth2 manual for more info.
This code assumes SDA on bit 7 and SCL on bit 0 of a VIA port. You can put it in forth_code/user_words.fs and rebuild Tali for your platform - it will be added to your ROM and compiled by Tali on startup (there will be a slight delay at startup). You'll need to change the address to suit your hardware and block2eeprom could be simplified to just the true section of the if (and change the EEPROM I2C address there as well to (desired_I2C_address_in_hex * 2)):
Code:
\ Set up the VIA for I2C. I'm using the VIA DDR method.
\ PTA7 is data
\ PTA0 is clock
hex
7F01 constant via.porta
7F03 constant via.ddra
\ Make port A an input so the bus starts idle.
: i2c-setup 0 via.porta c! 0 via.ddra c! ;
\ Data on PORTA7 (note that 0 = 1 on the I2C bus for writing)
\ : i2c-sda0 via.ddra c@ 80 or via.ddra c! ; allow-native
\ : i2c-sda1 via.ddra c@ 7f and via.ddra c! ; allow-native
: i2c-sda0
[
AD c, 03 c, 7f c, ( lda $7f03 )
09 c, 80 c, ( ora #$80 )
8D c, 03 c, 7f c, ( sta $7f03 )
] ; allow-native
: i2c-sda1
[
AD c, 03 c, 7f c, ( lda $7f03 )
29 c, 7F c, ( and #$7F )
8D c, 03 c, 7f c, ( sta $7f03 )
] ; allow-native
\ Clock is on PORTA0 (note that 0 = 1 on I2C bus)
\ : i2c-scl0 via.ddra c@ 01 or via.ddra c! ; allow-native
\ : i2c-scl1 via.ddra c@ FE and via.ddra c! ; allow-native
: i2c-scl0
[
AD c, 03 c, 7f c, ( lda $7f03 )
09 c, 01 c, ( ora #$01 )
8D c, 03 c, 7f c, ( sta $7f03 )
] ; allow-native
: i2c-scl1
[
AD c, 03 c, 7f c, ( lda $7f03 )
29 c, FE c, ( and #$FE )
8D c, 03 c, 7f c, ( sta $7f03 )
] ; allow-native
\ Clock the bus high, then low.
: i2c-clock
i2c-scl1 i2c-scl0 ; allow-native
\ Generate a START condition on the bus.
: i2c-start
i2c-sda1 i2c-scl1 i2c-sda0 i2c-scl0 ; allow-native
\ Generate a STOP condition on the bus.
: i2c-stop
i2c-sda0 i2c-scl1 i2c-sda1 ; allow-native
\ Transmit a single bit.
: i2c-txbit ( bit -- )
if i2c-sda1 else i2c-sda0 then i2c-clock ;
\ Receive a single bit.
: i2c-rxbit ( -- bit )
i2c-sda1 i2c-scl1 via.porta c@
80 and if 1 else 0 then i2c-scl0 ;
: i2c-tx ( byte -- nak )
8 0 do dup 80 and i2c-txbit 2* loop drop ( Send the byte )
i2c-rxbit ; ( Get the NAK flag )
: i2c-rx ( nak -- byte )
0 8 0 do 2* i2c-rxbit + loop ( Receive the byte )
swap i2c-txbit ; ( Send the NAK flag )
: block2eeprom ( u -- u u ) ( blocknum -- eeprom_address i2c_address )
dup 40 < if
( Blocks 0-63[decimal] )
400 * ( multiply block number by 1024[decimal] )
A0 ( use $50 [shifted left one place] as I2C address )
else
( Blocks 64-127[decimal] - no limit check )
40 - ( subtract 64[decimal] from block number )
400 * ( multiply block number by 1024[decimal] )
A8 ( use $54 [shiften left one place] as I2C address )
then ;
: eeprom-pagewrite ( addr u u -- ) ( buffer_address eeprom_address i2c_address -- )
dup >r ( save the i2c address for later )
i2c-start i2c-tx drop ( start the i2c frame using computed i2c address )
100 /mod i2c-tx drop i2c-tx drop ( send the 16-bit address as two bytes )
80 0 do ( send the 128[decimal] bytes )
dup i + ( compute buffer address )
c@ i2c-tx drop ( send the byte )
loop drop i2c-stop ( end the frame )
r> begin ( recall the i2c address and poll until complete )
dup
i2c-start i2c-tx ( start the i2c frame using computed i2c address )
0= until drop
i2c-stop
;
: eeprom-blockwrite ( addr u -- ) ( buffer_address blocknum -- )
( Write the entire block buffer one eeprom page [128 bytes] at a time )
8 0 do
over i 80 * + ( offset by eeprom pages into block buffer )
over block2eeprom
swap i 80 * + swap ( offset by eeprom pages into eeprom )
eeprom-pagewrite
loop
2drop ;
: eeprom-blockread ( addr u -- ) ( buffer_address blocknum -- )
block2eeprom dup
i2c-start i2c-tx drop ( start the i2c frame using computed i2c address )
swap ( move the eeprom internal address to TOS )
100 /mod i2c-tx drop i2c-tx drop ( send the 16-bit address as two bytes )
i2c-start 1+ i2c-tx drop ( send I2C address again with R/W* bit set )
3FF 0 do ( loop though all but the last byte )
0 i2c-rx over i + c!
loop
( Read last byte with NAK to stop )
1 i2c-rx over 3FF + c! i2c-stop drop ;
\ Connect to Fourth BLOCK words
' eeprom-blockread BLOCK-READ-VECTOR !
' eeprom-blockwrite BLOCK-WRITE-VECTOR !
decimal
If you'd like to see those opcode words in Tali assember, that would be enough for me to rewrite them and post them.