Skip to content

Instantly share code, notes, and snippets.

@TG9541
Last active March 8, 2024 02:32
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TG9541/5c3405320794d91ef8129734a4bfc880 to your computer and use it in GitHub Desktop.
Save TG9541/5c3405320794d91ef8129734a4bfc880 to your computer and use it in GitHub Desktop.
STM8 I2C device experiments

The STM8 I2C Peripheral

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:

image

Reading using write

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.

image

A "current address read" or a "sequential current read" sequence is also possible (just set RPOINT and RCOUNT and issue i2s).

Improving the solution

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:

image

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).

Reusable ISR code for I2C

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).

@TG9541
Copy link
Author

TG9541 commented Dec 17, 2020

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:

\ Example: Simple I2C EEPROM access and I2C scanner

\res MCU: STM8S103

#require I2CMA

\ Temp. constants for I2CMA register access for user code
I2CMA 2 + CONSTANT TCOUNT  \ char number of bytes TX
I2CMA 3 + CONSTANT RCOUNT  \ char number of bytes RX
I2CMA 4 + CONSTANT TPOINT  \ points to TX buffer, starting with CMD/ADDR
I2CMA 6 + CONSTANT RPOINT  \ points to RX buffr

#require ]B!
#require ]C!

\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 EE24C \ slave address EEPROM 24C32 .. 24C512 w\ A2:0=low

NVM

VARIABLE EADDR
VARIABLE BUFFER 6 ALLOT

\ Standard speed 100 kHz
: I2I ( -- ) \ initialize peripheral - init values by @eelkhoorn
  \ STM8L only
  \ [ 1 CLK_PCKENR1 3 ]B! \ enable SYSCLK to I2C, e.g. for stm8l051

  [ 0  I2C_CR1  0 ]B!   \ I2C peripheral disable
  [ 1 I2C_CR2 7 ]B!     \ SWRST (in case I2C peripheral is in slave mode)
  [ 0 I2C_CR2 ]C!       \ POS "Method 1"
  [ 1 I2C_FREQR 4 ]B!   \ CPU freq 16 MHz
  \ [ 0 I2C_CCRH 6 ]B!    \ normal mode 
  \ [ $50 I2C_CCRL ]C!    \ i2c freq 100 kHz, CCR = f.master/(2 f.i2c)
  [ $80 I2C_CCRH ]C!    \ fast mode 
  [ $E  I2C_CCRL ]C!    \ I2C freq 381kHz (table 91)
  [ 17  I2C_TRISER ]C!  \ TRISER = CPU freq in MHz + 1
  [ 1   I2C_CR1 0  ]B!  \ Peripheral enable
;

\ write from BUFFER to EEPROM w/ 16 bit address
: 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
  EE24C I2S
;

\ read from EEPROM w/ 16 bit address to BUFFER
: 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
;

\ read next c bytes from EEPROM to BUFFER
: rnext ( c -- )
  ( c ) RCOUNT C! BUFFER RPOINT !
  EE24C I2S
;

\ simple I2C bus scanner
: scan ( -- )
  I2I
  127 FOR
    I 16 MOD 15 = IF CR THEN \ show 16 addresses in a row
    I I2S                    \ sample slave address using empty transfer
    I2W ?I2E IF              \ wait for result, check for success or failure
      ."  --"
    ELSE
      I .
    THEN
  NEXT
;

RAM

\\ Example

I2I
scan  \ this should show an I2C slave with address 80

12345 BUFFER ! $0506 BUFFER 2+ ! \ prepare some data
$0011 4 write                    \ write to EEPROM
BUFFER 4 ERASE                   \ clear buffer, just to show that we can read
$0011 2 read  BUFFER ?           \ read 2 bytes from EEPROM
1 rnext BUFFER C@ .              \ read next byte (using EEPROM int. pointer)

scan shows a table like the following:

scan           
 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 -- -- -- 60 -- -- -- -- -- -- -- -- -- -- -- --
 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ok

Here is a slightly more complex example for the well known SSD1306 128x64 OLED display:

\ I2CMA example code for SSD1306 OLED display

\res MCU: STM8S103

#require I2CMA

#require WIPE

NVM

CREATE dia  \ display initialisation array
\ * = vccstate dependant
 $00 C,  \ from ssdi
 $AE C,  \ SSD1306_DISPLAYOFF
 $D5 C,  \ SSD1306_SETDISPLAYCLOCKDIV
 $80 C,  \
 $A8 C,  \ SSD1306_SETMULTIPLEX
 $3F C,  \ SSD1306_LCDHEIGHT - 1
 $D3 C,  \ SSD1306_SETDISPLAYOFFSET
 $0  C,  \ no offset
 $40 C,  \ SSD1306_SETSTARTLINE \ line #0
 $8D C,  \ SSD1306_CHARGEPUMP
 $14 C,  \ *
 $20 C,  \ SSD1306_MEMORYMODE
 $0  C,  \ 0x0 act like ks0108
 $A0 C,  \ SSD1306_SEGREMAP
 $C0 C,  \ SSD1306_COMSCANDEC
 $DA C,  \ SSD1306_SETCOMPINS
 $12 C,  \
 $81 C,  \ SSD1306_SETCONTRAST
 $CF C,  \ *
 $D9 C,  \ SSD1306_SETPRECHARGE
 $F1 C,  \ *
 $DB C,  \ SSD1306_SETVCOMDETECT
 $40 C,  \
 $A4 C,  \ SSD1306_DISPLAYALLON_RESUME
 $A6 C,  \ SSD1306_NORMALDISPLAY
 $2E C,  \ SSD1306_DEACTIVATE_SCROLL
 $AF C,  \ SSD1306_DISPLAYON

hex
create font   \ 5x8
  00 c, 00 c, 00 c, 00 c, 00 c, \
  00 c, 00 c, 4F c, 00 c, 00 c, \ !
  00 c, 03 c, 00 c, 03 c, 00 c, \ "
  14 c, 3E c, 14 c, 3E c, 14 c, \ #
  24 c, 2A c, 7F c, 2A c, 12 c, \ $
  63 c, 13 c, 08 c, 64 c, 63 c, \ %
  36 c, 49 c, 55 c, 22 c, 50 c, \ &
  00 c, 00 c, 07 c, 00 c, 00 c, \ '
  00 c, 1C c, 22 c, 41 c, 00 c, \ (
  00 c, 41 c, 22 c, 1C c, 00 c, \ )
  0A c, 04 c, 1F c, 04 c, 0A c, \ *
  04 c, 04 c, 1F c, 04 c, 04 c, \ +
  50 c, 30 c, 00 c, 00 c, 00 c, \ ,
  08 c, 08 c, 08 c, 08 c, 08 c, \ -
  60 c, 60 c, 00 c, 00 c, 00 c, \ .
  00 c, 60 c, 1C c, 03 c, 00 c, \ /
  3E c, 41 c, 49 c, 41 c, 3E c, \ 0
  00 c, 02 c, 7F c, 00 c, 00 c, \ 1
  46 c, 61 c, 51 c, 49 c, 46 c, \ 2
  21 c, 49 c, 4D c, 4B c, 31 c, \ 3
  18 c, 14 c, 12 c, 7F c, 10 c, \ 4
  4F c, 49 c, 49 c, 49 c, 31 c, \ 5
  3E c, 51 c, 49 c, 49 c, 32 c, \ 6
  01 c, 01 c, 71 c, 0D c, 03 c, \ 7
  36 c, 49 c, 49 c, 49 c, 36 c, \ 8
  26 c, 49 c, 49 c, 49 c, 3E c, \ 9
  00 c, 33 c, 33 c, 00 c, 00 c, \ :
  00 c, 53 c, 33 c, 00 c, 00 c, \ ;
  00 c, 08 c, 14 c, 22 c, 41 c, \ <
  14 c, 14 c, 14 c, 14 c, 14 c, \ =
  41 c, 22 c, 14 c, 08 c, 00 c, \ >
  06 c, 01 c, 51 c, 09 c, 06 c, \ ?
  3E c, 41 c, 49 c, 15 c, 1E c, \ @
  78 c, 16 c, 11 c, 16 c, 78 c, \ A
  7F c, 49 c, 49 c, 49 c, 36 c, \ B
  3E c, 41 c, 41 c, 41 c, 22 c, \ C
  7F c, 41 c, 41 c, 41 c, 3E c, \ D
  7F c, 49 c, 49 c, 49 c, 49 c, \ E
  7F c, 09 c, 09 c, 09 c, 09 c, \ F
  3E c, 41 c, 41 c, 49 c, 7B c, \ G
  7F c, 08 c, 08 c, 08 c, 7F c, \ H
  00 c, 41 c, 7F c, 41 c, 00 c, \ I
  38 c, 40 c, 40 c, 41 c, 3F c, \ J
  7F c, 08 c, 08 c, 14 c, 63 c, \ K
  7F c, 40 c, 40 c, 40 c, 40 c, \ L
  7F c, 06 c, 18 c, 06 c, 7F c, \ M
  7F c, 06 c, 18 c, 60 c, 7F c, \ N
  3E c, 41 c, 41 c, 41 c, 3E c, \ O
  7F c, 09 c, 09 c, 09 c, 06 c, \ P
  3E c, 41 c, 51 c, 21 c, 5E c, \ Q
  7F c, 09 c, 19 c, 29 c, 46 c, \ R
  26 c, 49 c, 49 c, 49 c, 32 c, \ S
  01 c, 01 c, 7F c, 01 c, 01 c, \ T
  3F c, 40 c, 40 c, 40 c, 7F c, \ U
  0F c, 30 c, 40 c, 30 c, 0F c, \ V
  1F c, 60 c, 1C c, 60 c, 1F c, \ W
  63 c, 14 c, 08 c, 14 c, 63 c, \ X
  03 c, 04 c, 78 c, 04 c, 03 c, \ Y
  61 c, 51 c, 49 c, 45 c, 43 c, \ Z
  00 c, 7F c, 41 c, 00 c, 00 c, \ [
  00 c, 03 c, 1C c, 60 c, 00 c, \ \
  00 c, 41 c, 7F c, 00 c, 00 c, \ ]
  0C c, 02 c, 01 c, 02 c, 0C c, \ ^
  40 c, 40 c, 40 c, 40 c, 40 c, \ _
  00 c, 01 c, 02 c, 04 c, 00 c, \ `
  20 c, 54 c, 54 c, 54 c, 78 c, \ a
  7F c, 48 c, 44 c, 44 c, 38 c, \ b
  38 c, 44 c, 44 c, 44 c, 44 c, \ c
  38 c, 44 c, 44 c, 48 c, 7F c, \ d
  38 c, 54 c, 54 c, 54 c, 18 c, \ e
  08 c, 7E c, 09 c, 09 c, 00 c, \ f
  0C c, 52 c, 52 c, 54 c, 3E c, \ g
  7F c, 08 c, 04 c, 04 c, 78 c, \ h
  00 c, 00 c, 7D c, 00 c, 00 c, \ i
  00 c, 40 c, 3D c, 00 c, 00 c, \ j
  7F c, 10 c, 28 c, 44 c, 00 c, \ k
  00 c, 00 c, 3F c, 40 c, 00 c, \ l
  7C c, 04 c, 18 c, 04 c, 78 c, \ m
  7C c, 08 c, 04 c, 04 c, 78 c, \ n
  38 c, 44 c, 44 c, 44 c, 38 c, \ o
  7F c, 12 c, 11 c, 11 c, 0E c, \ p
  0E c, 11 c, 11 c, 12 c, 7F c, \ q
  00 c, 7C c, 08 c, 04 c, 04 c, \ r
  48 c, 54 c, 54 c, 54 c, 24 c, \ s
  04 c, 3E c, 44 c, 44 c, 00 c, \ t
  3C c, 40 c, 40 c, 20 c, 7C c, \ u
  1C c, 20 c, 40 c, 20 c, 1C c, \ v
  1C c, 60 c, 18 c, 60 c, 1C c, \ w
  44 c, 28 c, 10 c, 28 c, 44 c, \ x
  46 c, 28 c, 10 c, 08 c, 06 c, \ y
  44 c, 64 c, 54 c, 4C c, 44 c, \ z
  00 c, 08 c, 77 c, 41 c, 00 c, \ {
  00 c, 00 c, 7F c, 00 c, 00 c, \ |
  00 c, 41 c, 77 c, 08 c, 00 c, \ }
  10 c, 08 c, 18 c, 10 c, 08 c, \ ~
decimal

WIPE

#require 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

NVM
: 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_CR1  0 ]B!   \ I2C peripheral disable
  [ 1 I2C_CR2 7 ]B!     \ SWRST
  [ 0 I2C_CR2 ]C!       \ POS "Method 1"
  [ 1 I2C_FREQR 4 ]B!   \ CPU freq 16 MHz
  \ [ 0 I2C_CCRH 6 ]B!    \ duty cycle
  \ [ $50 I2C_CCRL ]C!    \ i2c freq 100 kHz, CCR = f.master/(2 f.i2c)
  [ $80 I2C_CCRH ]C!    \ fast mode
  [ $E  I2C_CCRL ]C!    \ I2C freq 381kHz (table 91)
  [ 17  I2C_TRISER ]C!  \ TRISER = CPU freq in MHz + 1
  [ 1   I2C_CR1 0  ]B!  \ Peripheral enable
;

WIPE

#require WIPE

\ Temp. constants for I2CMA register access for user code
I2CMA 2 + CONSTANT TCOUNT  \ char number of bytes TX
I2CMA 3 + CONSTANT RCOUNT  \ char number of bytes RX
I2CMA 4 + CONSTANT TPOINT  \ points to TX buffer, starting with CMD/ADDR
I2CMA 6 + CONSTANT RPOINT  \ points to RX buffr

$3c CONSTANT SSD  \ SSD1306 slave address

NVM

VARIABLE BUFFER 7 AlLOT	\ command register

\ Initialise display
: ssdi  ( --)
   I2I  \ initialize I2C peripheral
   dia TPOINT !  27 TCOUNT C!  \ SSD1306 init table
   SSD I2S I2W 
;

: 0buf ( -- )
  BUFFER 9 ERASE
;

: ccmd ( -- )
  $40 BUFFER C!
;

: home ( -- )
	BUFFER $0010 OVER !  $B000 OVER 2+ !
  TPOINT !  4 TCOUNT C!  SSD I2S I2W
;

: cls ( -- )
  0buf ccmd  \ empty buffer, cmd
  127 FOR 9 TCOUNT C! BUFFER TPOINT ! SSD I2S I2W NEXT
  home
;

\ send n bytes @ a to display buffer, add space
: wc ( a n -- )
	BUFFER TPOINT !
  ( n ) DUP 2+ ( add cmd and space ) TCOUNT C! 
  BUFFER 1+ SWAP CMOVE ( copy bit pattern from a )
  ccmd SSD I2S I2W
;

\ Display character:
: ssdEMIT ( c --)
  \ Translates ASCII to address of bitpatterns:
  ( c ) &32 max &127 min  &32 - 5 * font +
  ( a ) 5 wc
;

\ print number through redirection of EMIT to SSD1306 
: ssd. ( n -- )
  'EMIT @ >R [ ' sddEMIT ] LITERAL 'EMIT !
  .
  R> 'EMIT !
;

WIPE

\\ Example

sddi \ initialize SDD1306 OLED display
cls  \ clear screen
12345 sdd. \ print number

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment