The STM8 family I2C comes in two variants: one with basic features for 100 or 400 kHz "I2C master and slave" in STM8S RM0016 and STM8L RM0013 devices (e.g. STM8S103F3 and STM8L101F3), the other in STM8L RM0031 devices (e.g. STM8L051F3) offers more features (e.g. two slave addresses, DMA, SMB 2.0 and PMBus modes). The experiments with STM8 eForth here target the basic features common to both variants.
In my first tests I used @eelkhorn's code. It turned out that "polling" is needed for I2C peripheral flags and for potential error conditions in the same loop. This is prone to hanging. After having spent some time adjusting delays (using the reset button very often) I decided that the I2C peripheral was designed for an ISR.
What complicates things is that the EVx notation in the RM0016 or RM0031 sequence diagram is a mishmash of sequence and events and no clear specification of a state machine for using the peripheral is offered. I assumed that the event flags in I2C_SR1
can be interpreted as indicative of state and event of a suitable state machine which creates the required sequence as the result of some simple logic. That's not a clean approach but it's likely to work.
In my experiments I used an STM8S903K3T6C and the STM8 eForth 2.2.26 binary for the STM8S103F3. With minor modifications the code should work with any STM8 device (i.e. peripheral clock tree, GPIO setup for the STM8L families). I used register initialization values from @eelkhorns code after review.
The following shows the first step of an interrupt driven "master transmitter" which is quite simple:
\ ISR based I2C "Master transmitter"
\ Standard speed 100 kHz
\ hardcoded:
\ - slave address 80 (EEPROM ST24C64)
#require PINDEBUG
#require :NVM
#require ]B?
#require ]B!
#require ]C!
\res MCU: STM8S103
\ \res export CLK_PCKENR1
\res export PB_DDR PB_CR1
\res export I2C_CR1 I2C_CR2 I2C_FREQR I2C_OARL I2C_OARH
\res export I2C_CCRL I2C_CCRH I2C_TRISER I2C_DR
\res export I2C_SR1 I2C_SR2 I2C_SR3
\res export I2C_ITR INT_I2C
\ CR2 bits
0 CONSTANT START
1 CONSTANT STOP
\ SR1 bits
0 CONSTANT SB
1 CONSTANT ADDR
2 CONSTANT BTF
7 CONSTANT TXE
\ ITR bits
1 CONSTANT ITEVTEN
2 CONSTANT ITBUFEN
\ slave address i2c
80 CONSTANT SAI2C
NVM
VARIABLE EADDR
VARIABLE BUFFER 6 ALLOT
VARIABLE TPOINT
VARIABLE TCOUNT
:NVM
SAVEC
\ Master write sequence
[ I2C_SR1 SB ]B? IF
\ EV5
[ SAI2C 2* I2C_DR ]C! \ ADDR 7bit DR write
THEN
[ I2C_SR1 ADDR ]B? IF
\ EV6
I2C_SR3 C@ DROP \ CLR ADDR by reading SR3
ELSE
\ EV8_1 and EV8
[ I2C_SR1 TXE ]B? IF
TCOUNT @ ?DUP IF
1- TCOUNT !
TPOINT DUP @ ( a n ) DUP 1+ ROT ! C@ I2C_DR C!
THEN
THEN
[ I2C_SR1 BTF ]B? IF
\ EV8_2
[ 1 I2C_CR2 STOP ]B! \ STOP clears BTF
P1H
THEN
THEN
IRET
;NVM INT_I2C !
: i2i ( -- ) \ initialise peripheral - init values by @eelkhoorn
[ 0 I2C_CR1 0 ]B! \ I2C peripheral disable
\ STM8L only
\ [ 1 CLK_PCKENR1 3 ]B! \ enable SYSCLK to I2C, needed for stm8l052
\ only necessary if I2C_SR3.1 (BUSY) is active during init
\ [ $80 I2C_CR2 ]C! \ reset BSY
[ 0 I2C_CR2 ]C!
[ 1 I2C_FREQR 4 ]B! \ CPU freq 16 MHz
[ $A0 I2C_OARL ]C! \ own address 0xA0
[ $40 I2C_OARH ]C! \ 7 bit address mode
[ 0 I2C_CCRH 6 ]B! \ duty cycle
[ $50 I2C_CCRL ]C! \ i2c freq 100 kHz, CCR = f.master/(2 f.i2c)
[ $11 I2C_TRISER ]C! \ TRISER = CPU freq in MHz + 1
[ 1 I2C_CR1 0 ]B! \ i2c enable
[ 1 I2C_CR2 2 ]B! \ ACK enable
\ Interrupt events both "EVT" and "BUF"
[ 1 I2C_ITR ITBUFEN ]B!
[ 1 I2C_ITR ITEVTEN ]B!
PINDEBUG \ I'm using PD3, PD4
;
: i2s ( --) \ start
P1L
[ 1 I2C_CR2 START ]B!
;
: write ( a c -- )
( c ) 2+ TCOUNT ! \ BUFFER follows EADDR, c=0 at least writes the address
( a ) EADDR ! \ set EEPROM address
EADDR TPOINT ! \ initialize transfer pointer
i2s
;
RAM
\\ Example
i2i ok
$77CC BUFFER ! ok
$55AA 2 write ok
The example results in the following trace:
With a little creativity the write sequence can be turned into the address setting in a read sequence. The trick used here is to use a zero-byte write for setting the "i2c device's read address pointer" and, in the master ISR write sequence, the read count RCOUNT
as an indication that not a STOP
but re-START
with a following read sequence should be performed.
The result is surprisingly simple:
\ ISR based I2C "Master transmitter"
\ Standard speed 100 kHz
\ hardcoded:
\ - slave address 80 (EEPROM ST24C64)
#require PINDEBUG
#require :NVM
#require ]B?
#require ]B!
#require ]C!
\res MCU: STM8S103
\ \res export CLK_PCKENR1
\res export PB_DDR PB_CR1
\res export I2C_CR1 I2C_CR2 I2C_FREQR I2C_OARL I2C_OARH
\res export I2C_CCRL I2C_CCRH I2C_TRISER I2C_DR
\res export I2C_SR1 I2C_SR2 I2C_SR3
\res export I2C_ITR INT_I2C
\ CR2 bits
0 CONSTANT START
1 CONSTANT STOP
2 CONSTANT ACK
\ SR1 bits
0 CONSTANT SB
1 CONSTANT ADDR
2 CONSTANT BTF
6 CONSTANT RXNE
7 CONSTANT TXE
\ ITR bits
1 CONSTANT ITEVTEN
2 CONSTANT ITBUFEN
\ slave address i2c
80 CONSTANT SAI2C
NVM
VARIABLE EADDR
VARIABLE BUFFER 6 ALLOT
VARIABLE TPOINT
VARIABLE TCOUNT
VARIABLE RPOINT
VARIABLE RCOUNT
:NVM
SAVEC
\ Master write sequence
[ I2C_SR1 SB ]B? IF
\ EV5
TCOUNT @ IF
[ SAI2C 2* I2C_DR ]C! \ ADDR 7bit DR write
ELSE
RCOUNT @ IF
P2L
[ SAI2C 2* 1+ I2C_DR ]C! \ ADDR 7bit DR read
THEN
THEN
THEN
[ I2C_SR1 ADDR ]B? IF
\ EV6
I2C_SR3 C@ DROP \ CLR ADDR by reading SR3
ELSE
[ I2C_SR1 RXNE ]B? IF
\ EV7
I2C_DR C@ ( c )
RCOUNT @ ?DUP IF
( c n ) 1- DUP RCOUNT ! 0= IF
[ 0 I2C_CR2 ACK ]B! \ ACK disable
[ 1 I2C_CR2 STOP ]B! \ end read sequence
P2H \ PIN debug
THEN
( c ) RPOINT DUP @ ( a n ) DUP 1+ ROT ! C!
ELSE
DROP \ last RXNE after STOP
THEN
THEN
[ I2C_SR1 TXE ]B? IF
\ EV8_1 and EV8
TCOUNT @ ?DUP IF
1- TCOUNT !
TPOINT DUP @ ( a n ) DUP 1+ ROT ! C@ I2C_DR C!
THEN
THEN
[ I2C_SR1 BTF ]B? IF
\ EV8_2
RCOUNT @ IF
[ 1 I2C_CR2 ACK ]B! \ ACK enable
[ 1 I2C_CR2 START ]B! \ re-START for read sequence
ELSE
[ 1 I2C_CR2 STOP ]B! \ STOP clears BTF
THEN
P1H \ PIN debug
THEN
THEN
IRET
;NVM INT_I2C !
: i2i ( -- ) \ initialise peripheral - init values by @eelkhoorn
[ 0 I2C_CR1 0 ]B! \ I2C peripheral disable
\ STM8L only
\ [ 1 CLK_PCKENR1 3 ]B! \ enable SYSCLK to I2C, needed for stm8l052
\ only necessary if I2C_SR3.1 (BUSY) is active during init
\ [ $80 I2C_CR2 ]C! \ reset BSY
[ 0 I2C_CR2 ]C!
[ 1 I2C_FREQR 4 ]B! \ CPU freq 16 MHz
[ $A0 I2C_OARL ]C! \ own address 0xA0
[ $40 I2C_OARH ]C! \ 7 bit address mode
[ 0 I2C_CCRH 6 ]B! \ duty cycle
[ $50 I2C_CCRL ]C! \ i2c freq 100 kHz, CCR = f.master/(2 f.i2c)
[ $11 I2C_TRISER ]C! \ TRISER = CPU freq in MHz + 1
[ 1 I2C_CR1 0 ]B! \ i2c enable
[ 1 I2C_CR2 ACK ]B! \ ACK enable
\ Interrupt events both "EVT" and "BUF"
[ 1 I2C_ITR ITBUFEN ]B!
[ 1 I2C_ITR ITEVTEN ]B!
PINDEBUG \ PD3, PD4
P1H P2H
;
: i2s ( --) \ start
P1L
[ 1 I2C_CR2 START ]B!
;
: write ( a c -- )
( c ) 2+ TCOUNT ! \ BUFFER follows EADDR, c=0 at least writes the address
( a ) EADDR ! \ set EEPROM address
EADDR TPOINT ! \ initialize transfer pointer
i2s
;
: read ( a c -- )
BUFFER RPOINT ! \ set read pointer to buffer
( c ) RCOUNT ! \ set read count
( a ) 0 write \ zero-write sets EADDR and starts the read sequence
;
RAM
\\ Example
i2i
$AA55 BUFFER !
$0011 2 write
$0011 2 read
The debug pins P1
(write) and P2
(read) indicate the parts of the composed read sequence.
A "current address read" or a "sequential current read" sequence is also possible (just set RPOINT
and RCOUNT
and issue i2s
).
The first solution for read
and write
works. There are several things that need to be improved, though: read always reads one more byte than it should. A close inspection of the issue reveals that it's not that easy to read exactly the right number of bytes (there are different sequence prescriptions for n>2, n=2 and for n=1). The truth is that the I2C component isn't very well designed - ISR code needs to take care of many special case, and it better be fast!
At one point I had the impression that the Forth code isn't fast enough (although that wasn't the case), and I implemented IF .. ELSE .. THEN
like structures with BTJF
instead of ]B? IF
and JREQ
instead of reg C@ IF
. This may sound like assembler programming but actually it's Forth: I simply redefined ELSE
and THEǸ
with relative addressing (see >REL). The ISR code is essentially assembler with Forth control structures.
Now the ISR was really fast: any ISR call for driving now takes less than 4µs (including the logic). It also turned out that the I2C peripheral has one more quirk: after writing the last byte to I2C_DR
there is no way to clear TXE
(buffer transmit event) and the interrupt keeps running again and again until the flag is finally cleared by a STOP or a re-START condition:
I worked around this by activating ITBUFEN
(I2C_ITR
) for TXE
and RxNE
events in the SB
event and resetting it after writing the last byte into I2C_DR
in the TXE
event.
The events ADDR
, RXNE
, TXE
and BTF
now all have concise roles using either the ITEVTEN
or the (conditionally masked) ITBUFEN
.
The role of BTF
is now taking care of the end of transmissions after the last byte has been sent and it either ends a write transmission with STOP or it keeps the ISR chain running with a re-START.
The ADDR
event has a new role for ending reception after the first byte if n=1.
Here is the code:
\ ISR based I2C "Master transmitter"
\ Standard speed 100 kHz
\ hardcoded:
\ - slave address 80 (EEPROM ST24C64)
\ - transmission "address and buffer"
#require PINDEBUG
\res MCU: STM8S103
\res export I2C_CR1 I2C_CR2 I2C_DR
\res export I2C_SR1 I2C_SR2 I2C_SR3
\res export INT_I2C I2C_ITR
#require WIPE
#require :NVM
#require ]B!
#require ]C!
#require ]B@IF
#require ]C@IF
\ CR2 bits
0 CONSTANT START
1 CONSTANT STOP
2 CONSTANT ACK
\ SR1 bits
0 CONSTANT SB
1 CONSTANT ADDR
2 CONSTANT BTF
6 CONSTANT RXNE
7 CONSTANT TXE
\ SR3 bits
2 CONSTANT TRA
\ ITR bits
2 CONSTANT ITBUFEN
\ Slave Address I2C
80 CONSTANT SAI2C
NVM
VARIABLE TPOINT
VARIABLE TCOUNT
VARIABLE RPOINT
VARIABLE RCOUNT
:NVM \ I2C Master ISR
SAVEC
P1L
[ I2C_SR1 SB ]B@IF
\ EV5
[ TCOUNT 1+ ]C@IF
[ SAI2C 2* I2C_DR ]C! \ ADDR 7bit DR write
ELSE
[ RCOUNT 1+ ]C@IF
[ SAI2C 2* 1+ I2C_DR ]C! \ ADDR 7bit DR read
THEN
THEN
[ 1 I2C_ITR ITBUFEN ]B!
THEN
[ I2C_SR1 ADDR ]B@IF
\ EV6
[ $C6 C, I2C_SR1 , \ CLR ADDR by reading SR1
$C6 C, I2C_SR3 , ] \ followed by reading SR3
[ I2C_SR3 TRA ]B@IF
\ dummy
ELSE
P2L
[ $C6 C, RCOUNT 1+ , \ LD A,RCOUNT+1
$4A C, \ DEC A
$26 C, >REL ] \ JRNE rel
[ 0 I2C_CR2 ACK ]B! \ ACK disable
[ 1 I2C_CR2 STOP ]B! \ end read sequence
THEN
P2H \ PIN debug
THEN
THEN
[ I2C_SR1 RXNE ]B@IF
\ EV7
[ $C6 C, I2C_DR , \ LD A,I2C_DR
$88 C, \ PUSH A
RCOUNT 1+ ]C@IF [
$A103 , $2A C, >REL ]
[ 0 I2C_CR2 ACK ]B! \ ACK disable
[ 1 I2C_CR2 STOP ]B! \ end read sequence
THEN [
$725A , RCOUNT 1+ , \ DEC RCOUNT+1
$51 C, \ EXGW X,Y
$CE C, RPOINT , \ LDW X,RPOINT
$84 C, \ POP A
$F7 C, \ LD (X),A
$5C C, \ INCW X
$CF C, RPOINT , \ LDW RPOINT,X
$51 C, ] \ EXGW X,Y
ELSE [
$84 C, ] \ POP A
THEN
THEN
[ I2C_SR1 TXE ]B@IF
\ EV8_1 and EV8
[ TCOUNT 1+ ]C@IF [
$51 C, \ EXGW X,Y
$CE C, TPOINT , \ LDW X,TPOINT
$F6 C, \ LD A,(X)
$C7 C, I2C_DR , \ LD I2C_DR,A
$5C C, \ INCW X
$CF C, TPOINT , \ LDW TPOINT,X
$51 C, \ EXGW X,Y
$725A , TCOUNT 1+ , \ DEC (TCOUNT+1)
]
ELSE
[ 0 I2C_ITR ITBUFEN ]B!
THEN
THEN
[ I2C_SR1 BTF ]B@IF
[ RCOUNT 1+ ]C@IF
[ 1 I2C_CR2 ACK ]B! \ ACK enable
[ 1 I2C_CR2 START ]B! \ re-START for read sequence
ELSE
[ 1 I2C_CR2 STOP ]B! \ STOP clears TXE
THEN
THEN
P1H \ PIN debug
IRET
[ OVERT ( xt ) INT_I2C !
RAM WIPE
#require ]B!
#require ]C!
\ \res export CLK_PCKENR1
\res export PB_DDR PB_CR1
\res export I2C_ITR I2C_CR1 I2C_CR2
\res export I2C_FREQR I2C_OARL I2C_OARH
\res export I2C_CCRL I2C_CCRH I2C_TRISER
\ CR2 bits
0 CONSTANT START
2 CONSTANT ACK
\ ITR bits
1 CONSTANT ITEVTEN
NVM
VARIABLE EADDR
VARIABLE BUFFER 6 ALLOT
: i2i ( -- ) \ initialise peripheral - init values by @eelkhoorn
[ 0 I2C_CR1 0 ]B! \ I2C peripheral disable
\ STM8L only
\ [ 1 CLK_PCKENR1 3 ]B! \ enable SYSCLK to I2C, needed for stm8l052
\ only necessary if I2C_SR3.1 (BUSY) is active during init
\ [ $80 I2C_CR2 ]C! \ reset BSY
[ 0 I2C_CR2 ]C!
[ 1 I2C_FREQR 4 ]B! \ CPU freq 16 MHz
[ $A0 I2C_OARL ]C! \ own address 0xA0
[ $40 I2C_OARH ]C! \ 7 bit address mode
[ 0 I2C_CCRH 6 ]B! \ duty cycle
[ $50 I2C_CCRL ]C! \ i2c freq 100 kHz, CCR = f.master/(2 f.i2c)
[ $11 I2C_TRISER ]C! \ TRISER = CPU freq in MHz + 1
[ 1 I2C_CR1 0 ]B! \ Peripheral enable
[ 1 I2C_CR2 ACK ]B! \ ACK enable
\ Interrupt events both "EVT" and "BUF"
[ 1 I2C_ITR ITEVTEN ]B!
PINDEBUG \ PD3, PD4
P1H P2H
;
: i2s ( --) \ start
P1L
[ 1 I2C_CR2 START ]B!
;
: write ( a c -- )
( c ) 2+ TCOUNT ! \ BUFFER follows EADDR, c=0 at least writes the address
( a ) EADDR ! \ set EEPROM address
EADDR TPOINT ! \ initialize transfer pointer
i2s
;
: read ( a c -- )
BUFFER RPOINT ! \ set read pointer to buffer
( c ) RCOUNT ! \ set read count
( a ) 0 write \ zero-write sets EADDR and starts the read sequence
;
RAM
\\ Example
i2i
$AA55 BUFFER !
$0011 2 write
$0011 2 read
The ISR code can be considered a "driver" whith a rather minimal interface and the memory footprint can be further reduced by using a hidden RAM field for the variables TPOINT
, TCOUNT
, RPOINT
and RCOUNT
. In this implemenation of the user code the variables EADDR
and BUFFER
need to be defined together and in this order but other implementations are possible.
My first take at redefining ELSE
and THEN
(and, for good measure, IF
) is here:
\ STM8eForth : control structures with relative addressing TG9541-201124
\ ------------------------------------------------------------------------------
#require >Y
: THEN ( -- ) [COMPILE] [ HERE OVER - 1- SWAP C! [COMPILE] ] ; IMMEDIATE
: >REL ( -- ) HERE 0 C, ; \ like >MARK for rel. branch
: ELSE ( -- ) [COMPILE] [ $20 C, [COMPILE] ] >REL \ JRA rel
SWAP [COMPILE] THEN ; IMMEDIATE
: JREQ ( F:Z -- ) [COMPILE] [ $27 C, [COMPILE] ] >REL ; IMMEDIATE
: IF ( n -- ) COMPILE >Y [COMPILE] JREQ ; IMMEDIATE
\\ Example
#require >REL
: ]B@IF ( -- ) 2* $7201 + , , ] >REL ; \ BTJF a,#bit,rel
: ]@IF ( -- ) $90CE , , ( LDW Y,a ) ] [COMPILE] JREQ ;
: ]C@IF ( -- ) $C6 C, , ( LD A,a ) ] [COMPILE] JREQ ;
NVM
VARIABLE vt
: testb [ vt 1 ]B@IF ." set" ELSE ." not set" THEN ;
: testNZ [ vt ]@IF ." not " THEN ." zero" ;
RAM
By requiring >REL
, the counterpart of >MARK
in eForth Overview code can simply be compiled with JRxx
instructions that avoid using the stack and literals for optimized low level code (that's also useful for code that needs to be relocatable, e.g. for execution in RAM).
I experimented with the DS1621 thermostat chip (or a Chinese clone, I don't know) which has a 8bit "command" instead of an "address" like I2C EEPROM chips to find a good way split the code into a generic ISR and user code. What appears to work well is creating some kind of "register file" in RAM I2ISR
and a minimal set of control words I2S
(start) and I2W
(wait, should that be necessary).
\res MCU: STM8S103
#require I2ISR
\ temporary constants for the I2C user code
\ ISR based I2C "Master transmitter"
\ Standard speed 100 kHz
\ hardcoded:
\ - slave address 80 (EEPROM ST24C64)
\ - transmission "address and buffer"
I2ISR 2 + CONSTANT TCOUNT \ char number of bytes TX
I2ISR 3 + CONSTANT RCOUNT \ char number of bytes RX
I2ISR 4 + CONSTANT TPOINT \ points to TX buffer, starting with CMD/ADDR
I2ISR 6 + CONSTANT RPOINT \ points to RX buffr
#require ]B!
#require ]C!
\ \res export CLK_PCKENR1
\res export PB_DDR PB_CR1
\res export I2C_ITR I2C_CR1 I2C_CR2
\res export I2C_FREQR I2C_OARL I2C_OARH
\res export I2C_CCRL I2C_CCRH I2C_TRISER
80 CONSTANT I2CSA
NVM
VARIABLE EADDR
VARIABLE BUFFER 6 ALLOT
: i2i ( -- ) \ initialise peripheral - init values by @eelkhoorn
[ 0 I2C_CR1 0 ]B! \ I2C peripheral disable
\ STM8L only
\ [ 1 CLK_PCKENR1 3 ]B! \ enable SYSCLK to I2C, needed for stm8l052
\ only necessary if I2C_SR3.1 (BUSY) is active during init
\ [ $80 I2C_CR2 ]C! \ reset BSY
[ 0 I2C_CR2 ]C!
[ 1 I2C_FREQR 4 ]B! \ CPU freq 16 MHz
[ $A0 I2C_OARL ]C! \ own address 0xA0
[ $40 I2C_OARH ]C! \ 7 bit address mode
[ 0 I2C_CCRH 6 ]B! \ duty cycle
[ $50 I2C_CCRL ]C! \ i2c freq 100 kHz, CCR = f.master/(2 f.i2c)
[ $11 I2C_TRISER ]C! \ TRISER = CPU freq in MHz + 1
[ 1 I2C_CR1 0 ]B! \ Peripheral enable
;
: write ( a c -- )
\ BUFFER follows EADDR, c=0 at least writes the address
( c ) 2+ TCOUNT C! \ TCOUNT, # bytes incl. EADDR
( a ) EADDR ! \ set EEPROM address
EADDR TPOINT ! \ initialize transfer pointer
I2CSA I2S
;
: read ( a c -- )
BUFFER RPOINT ! \ set read pointer to buffer
( c ) RCOUNT C! \ RCOUNT
( a ) 0 write \ zero-write sets EADDR and starts the read sequence
;
RAM
\\ Example
i2i
$AA55 BUFFER !
$0011 2 write
$0011 2 read
The ISR code depends on the e4thcom efr file definition for the MCU type (here it's \res MCU STM8S103
) - this means that the ISR should work for any STM8S or STM8L device.
\ The I2C ISR acts as a driver with programmable I2C write/read transfers
\ - errors are indicated by CFGSR bit7 (CFGSR 0<)
\ - SA contains the 7 bit target decice I2C address
\ - reset errors and
\ - TCOUNT sets the number of bytes in the write phase
\ - RCOUNT sets the number of bytes in the read phase
\ - for "write transfer TPOINT must point to a buffer or variable that
\ contains the I2C target device "command" (e.g. DS1621 temperture sensor)
\ or "memory address" (e.g. 24C64 EEPROM) followed by the data to be written
\ - for "read transfer" TPOINT must point to I2C target device "command" or
\ address or data and RPOINT must point to the buffer or variable that
\ receives data from the device
\ - to start the transfer set I2C_CR2 bit0 (START)
\ - a transfer is complete when TCOUNT and RCOUNT and I2C_SR3 bit 1 (BUSY)
\ are all "0" ( TCOUNT @ [ 1 I2C_SR3 1 ]B? OR 0= ).
\res export I2C_CR1 I2C_CR2 I2C_DR
\res export I2C_SR1 I2C_SR2 I2C_SR3
\res export INT_I2C I2C_ITR
#require WIPE
#require :NVM
#require ]B!
#require ]C!
#require ]B?
#require ]@
#require ]B@IF
#require ]C@IF
#require ]A<IF
\ CR2 bits
0 CONSTANT START
1 CONSTANT STOP
2 CONSTANT ACK
\ SR3 bits
1 CONSTANT BUSY
2 CONSTANT TRA
\ ITR bits
2 CONSTANT ITBUFEN
NVM
VARIABLE I2ISR 6 ALLOT
:NVM \ I2C Master ISR
SAVEC
[ I2C_SR1 0 ( SB ) ]B@IF [
\ EV5
$C6 C, I2C_DR , \ LD A,I2C_DR ; reset SB
$C6 C, I2ISR 1+ , \ LD A,SA ; slave address
$48 C, \ SLL A ; shift left for R/W flag
$725D , I2ISR 2+ , \ TNZ TCOUNT
$26 C, >REL \ TCOUNT C@ 0= IF
$725D , I2ISR 3 + , ] \ TNZ RCOUNT
JREQ [ \ RCOUNT C@ IF
$4C C, \ INC A ; set R flag
THEN
THEN
[ $C7 C, I2C_DR , ] \ LD I2C_DR,A
THEN
[ I2C_SR1 1 ( ADDR ) ]B@IF
\ EV6
[ $C6 C, I2C_SR1 , \ CLR ADDR by reading SR1
$C6 C, I2C_SR3 , ] \ followed by SR3
[ I2C_SR3 TRA ]B@IF
\ dummy
ELSE [
$C6 C, I2ISR 3 + , \ LD A,RCOUNT
$4A C, ] \ DEC A
[ $26 C, >REL ] \ JRNE rel
[ 0 I2C_CR2 ACK ]B! \ ACK disable
[ 1 I2C_CR2 STOP ]B! \ end read sequence
THEN
THEN
[ 1 I2C_ITR ITBUFEN ]B! \ enable buffer interrupt
THEN
[ I2C_SR1 6 ( RXNE ) ]B@IF
\ EV7
[ $C6 C, I2C_DR , \ LD A,I2C_DR
$88 C, \ PUSH A
I2ISR 3 + ]C@IF \ like "?DUP IF" with TOS in A
[ 3 ]A<IF \ 2nd to last byte in DR, last in ShReg: set STOP
[ 0 I2C_CR2 ACK ]B! \ ACK disable
[ 1 I2C_CR2 STOP ]B! \ end read sequence
THEN [
$725A , I2ISR 3 + , \ DEC RCOUNT
$51 C, \ EXGW X,Y
$CE C, I2ISR 6 + , \ LDW X,RPOINT
$84 C, \ POP A
$F7 C, \ LD (X),A
$5C C, \ INCW X
$CF C, I2ISR 6 + , \ LDW RPOINT,X
$51 C, ] \ EXGW X,Y
ELSE [
$84 C, ] \ POP A
THEN
THEN
[ I2C_SR1 7 ( TXE ) ]B@IF
\ EV8_1 and EV8
[ I2ISR 2+ ]C@IF [ \ TCOUNT C@
$51 C, \ EXGW X,Y
$CE C, I2ISR 4 + , \ LDW X,TPOINT
$F6 C, \ LD A,(X)
$C7 C, I2C_DR , \ LD I2C_DR,A
$5C C, \ INCW X
$CF C, I2ISR 4 + , \ LDW TPOINT,X
$51 C, \ EXGW X,Y
$725A , I2ISR 2+ , \ DEC TCOUNT
]
ELSE
[ 0 I2C_ITR ITBUFEN ]B!
THEN
THEN
[ I2C_SR1 2 ( BTF ) ]B@IF
[ I2ISR 3 + ]C@IF \ RCOUNT C@
[ 1 I2C_CR2 ACK ]B! \ ACK enable
[ 1 I2C_CR2 START ]B! \ re-START for read sequence
ELSE
[ 1 I2C_CR2 STOP ]B! \ STOP clears TXE
THEN
THEN
[ I2C_SR2 ]C@IF [ \ any error flags set?
$4F C, \ CLR A
$C7 C, I2C_ITR , \ LD I2C_ITR,A ; disable all interrupts
$31 C, I2C_SR2 , \ EXG A,I2C_SR2
$AA80 , \ OR A,#$80
$C7 C, I2ISR , \ LD I2ISR,A ; indicate error
]
THEN
IRET
[ OVERT ( xt ) INT_I2C ! \ not ";" (RET) but IRET - xt is the ISR vector
: I2S ( c -- ) \ start i2C write/read - user code sets T/RCOUNT, T/RPOINT
( c ) I2ISR ! \ reset flag (MSB), set device address (LSB)
[ 3 I2C_ITR ]C! \ set ITERREN and ITEVTEN
[ 1 I2C_CR2 START ]B!
;
: I2W ( -- ) \ wait until I2C sequence complete (or error)
BEGIN
[ I2ISR 2+ ]@ [ I2C_SR3 BUSY ]B? OR 0= \ both T/RCOUNT 0 and BUSY 0
[ I2ISR ]@ 0< OR \ or error detected
UNTIL
;
WIPE
The ISR now also contains error handling (using the error event ITERREN
). Errors are indicated through a negative value in the I2ISR
"register": the LSB contains the target device address, the MSB is either zero or it contains the bits from I2C_SR2
(with bit7 set).
The I2C Master driver I2CMA for the STM8 I2C peripheral is now part of the STM8 eForth code base. The
I2CMA
code has a simple API on the level of "set-up", "start", and "check for results" with just a few words and none of the usual sequence programming. The API of the driver uses a "register file" for controlling the addressing, write and read phases.The following code shows how to use a generic "two wire serial" 24C64 EEPROM with 16bit addressing:
scan
shows a table like the following:Here is a slightly more complex example for the well known SSD1306 128x64 OLED display: