AVR-Kopplung über I2C

Um zwei AVR-Controller über I2C/TWI zu koppeln, kann man innerhalb einer Do...Loop empfangene Zeichen pollen, wie bei der GLCD-Kopplung beschrieben, oder den TWI-Interrupt verwenden. Der Empfang mit dem TWI-Interrupt soll hier beschrieben werden. Da ein Interrupt unmittelbar mit dem Eintreten des auslösenden Ereignisses und nur dann bearbeitet wird, hier mit dem Empfang eines jeden Zeichens, ist dies die zweckmäßigere Methode.

Nachtrag vom 20.03.2019: Ein kompletter Satz Master-Slave ist dem Download zugefügt. Die Pin-Belegungen für SDA/SCL und Test-Displays sind im Quellcode beschrieben.

Hier einige Code-Snippets dazu, zunächst der Erklärungsteil im TWI-Slave.

$regfile = "m48def.dat"                           'Controller ATmega48
$crystal = 14745600                               '14.7456 MHz xtal
$hwstack = 40
$swstack = 40
$framesize = 40
Config SDA = PortC.4                              'TWI SDA
Config SCL = PortC.5                              'TWI SCL

Bei der standardmäßigen Softwareemulation von I2C/TWI mit BASCOM können SDA und SCL auf wählbare Ports gelegt werden. Das ist im Masterprogramm (s.u.) möglich. Da hier aber auf die TWI-Register zugegriffen wird, müssen die für I2C/TWI vorgesehenen Ports des ATmega48, PC.4 (ADC4, SDA, PCINT12) und PC.5 (ADC5, SCL, PCINT13), verwendet werden. Bei anderen Slave-Controllern das Datenblatt konsultieren. Das sonst bei der Verwendung des TWI Hardwaremoduls notwendige Einbinden der i2c_twi.lbx entfällt hier. Wir befummeln die TWI-Register auf direktem Wege. Die gleichen TWI-Portzuordnungen haben z.B. ATmega88, 168 und 328, etwa auch die Arduino NANO und UNO.

'Variables TWI-Communication  -------------------------------------------------
Const bytTWIAdr = &H78                            'TWI Slave address
Dim bytTWICount As Byte                           'TWI receive buffer byte counter
Dim bytTWIcontrol As Byte                         'TWI Control register data
Dim bytTWIstatus As Byte                          'TWI status
'Subs & Functions -------------------------------------------------------------
Declare Sub TWISlaveInit

"bytTWIAdr" legt die Adresse des Slave-Controllers fest, hier 78hex. Mit dieser Adresse spricht der Master den Slave an.
Die restlichen Variablen werden in der Interruptroutine (s.u.) verwendet.

Da die Übertragung byteweise erfolgt, ist es zweckmäßig, Byte-Overlays zu der vom Master gesendeten und hier vom Slave auszulesenden Meldung zu verwenden. Im Fall einer 2 Byte langen Word-Variablen:

Const bytTWILen = 2                               'Length of TRX frequency message
Dim wrdFreq As Word                               'TRX frequency (kHz)
Dim bytFreq(2) As Byte At wrdFreq Overlay         'Hi / lo byte of wrdFreq

Mit voller Frequenzauflösung in Hz bräuchte man eine 4 Byte lange Long- oder DWord-Variable, "2" also jeweils durch "4" ersetzen.
Soll ein Textstring, z.B. der Länge 12 übertragen werden, sähe das so aus:

Const bytTWILen = 12                              'Length of Text message
Dim strMsg As String*12                           'Text message
Dim bytMsg(12) As Byte At strMsg Overlay          'strMsg as single bytes

strMsg belegt zwar intern einschließlich eines Null-Bytes am Ende 13 Bytes, das Null-Byte wird aber nicht übertragen, also reichen 12 Bytes für bytMsg.

Nun müssen wir den TWI-Interrupt definieren:

'Enable TWI interrupt service routine -----------------------------------------
On TWI TWI_isr                                    'Go to TWI_isr if character is received
Call TWISlaveInit                                 'Initialize TWI slave
Enable TWI                                        'Enable TWI receive interrupt
SREG.7 = 1                                        'Enable all interrupts

Die Initialisierung als I2C-Slave erfolgt in der Sub TWISlaveInit. Die TWI-Registerbezeichnungen und die weiter unten in der "TWI_isr" verwendeten Statuscodes findet man im jeweiligen AVR Data sheet.

Sub TWISlaveInit
'Initialize TWI slave function
'Input:
'bytTWIAdr  TWI slave address

   TWSR = 0                                       'Clear TWI status register
   TWBR = 0                                       'Clear baud rate register
   TWDR = &B11111111                              'TWI data register initial value
   TWAR = bytTWIAdr                               'TWI slave address
   TWCR = &B01000100                              'TWI control register
   '                                              'Bit 2: TWEN, enable TWI
   '                                              'Bit 6: TWEA, enable Ack
End Sub

Mit der hier gesetzten Slave-Adresse bytTWIAdr hebt dieser Slave nur dann den Hörer ab, wenn der Master diese am Anfang des Übertragungsprotokolls auf den Bus schickt, siehe unten.

Fehlt noch die Interrupt Serviceroutine "TWI_isr":

TWI_isr:
'TWI receive interrupt service routine
'For TWI registers and status codes see ATmega data sheet
'Output:
'wrdFreq TRX frequency in kHz, overlayed with bytFreq(1...2)

      bytTWIstatus = TWSR And &B11111000          'Status register, mask TWI status=bits 7...3
      If bytTWIstatus = &HA0 Then                 'Stop or repeated start condition
         bytTWICount = 0                          'Initialize byte counter
      ElseIf bytTWIstatus = &H60 Or bytTWIstatus = &H68 Then       'Control data byte received
         bytTWICount = 0                          'Initialize byte counter
      Elseif bytTWIstatus = &H80 Or bytTWIstatus = &H88 Then       'Data received
         Incr bytTWICount                         'Increment byte counter
         bytFreq(bytTWICount) = TWDR              'Read byte from TWI data register
      End If
      TWCR = &B11000101                           'Set TWI Control register
                                                  'Bit 0: TWIE , enable TWI interrupt
                                                  'Bit 2: TTWEN, TWI enable
                                                  'Bit 6: TWAE , TWI enable acknowledge bit
                                                  'Bit 7: TWINT, TWI interrupt flag
Return

Für die o.a. Text-Variante müsste in der blau markierten Zeile statt
bytFreq(bytTWICount) = TWDR → bytMsg(bytTWICount) = TWDR
geschrieben werden.

Die "Nutzlast", also die zu übertragenden Nettoinhalte ohne die Steuerzeichen werden nur dann aus dem Data Register TWDR übernommen, wenn der Status 80 oder 88hex ist.

Damit ist die zeitkritische Übernahme der vom TWI-Master gesendeten Daten erledigt. Die Auswertung kann nun in einer Do...Loop vorgenommen werden, etwa so

Do
   If bytTWICount = bytTWILen Then                'Message received complete
      'Do someting usefull with the received data
      Lowerline
      LCD wrdFreq
      'or
      Upperline
      LCD strMsg
      bytTWICount = 0
   End If
Loop

Nun die Masterseite. Die ist regelrecht langweilig, z.B.

Config SDA = PortB.1
Config SCL = PortB.2
Const bytSLVAdr = &H78                            'TWI Slave address

Const bytTWILen = 2                               'Length of TRX TWI frequency message
Dim wrdFreq As Word                               'TRX frequency (kHz)
Dim bytFreq(2) As Byte At wrdFreq Overlay         'Hi / lo byte of wrdFreq

wrdFreq = 3520                                    '3520 kHz = 3.520 MHz

I2CStart
   I2CWByte bytSLVAdr                             'Slave address
   I2CWByte bytFreq(1)                            'Send Hi frequency byte
   I2CWByte bytFreq(2)                            'Send Lo frequency byte
I2CStop

oder so:

Config SDA = PortB.1
Config SCL = PortB.2
Const bytSLVAdr = &H78                            'TWI Slave address

Const bytTWILen = 12                              'Length of Text message
Dim strMsg As String*12                           'Text message
Dim bytMsg(12) As Byte At strMsg Overlay          'strMsg as single bytes
Dim bytTmp0 As Byte

strMsg = "Hello world!"

I2CStart
   I2CWByte bytSLVAdr                             'Slave address
   For bytTmp0 = 1 To bytTWILen
      I2CWByte bytMsg(bytTmp0)                    'Send byte after byte
   Next bytTmp0
I2CStop

Ein Schnelltest am Anfang des Master-Programms könnte noch anzeigen, ob der Slave überhaupt ansprechbar ist.

Function CheckTWI(byVal bytAdr As Byte) As Byte
'Check TWI chip @ slave address bytAdr
'CheckTWI = 1: OK, 0: else

   I2CStart
      I2CwByte bytAdr                             'Try to write I2C address
   I2CStop
   If Err = 1 Then                                'Error raised
      CheckTWI = 0                                'Not OK
   Else
      CheckTWI = 1                                'Is OK
   End If
End Function

Alternativ könnte man das TWI-Statusregister auswerten, etwa so:

Function CheckI2C(byVal bytAddr As Byte) As Byte
'Check I2C chip @ address bytAddr
'CheckI2C = 1: OK, 0 or 2: else
'Success is checked with TWI status register TWSR
'Status code is stored in the 5 upper bits, masked out with "And &B11111000"
'Write status code:
'&H08:   Start command was sent
'&H10:   Repeated start command was sent
'&H18:   Slave address (send condition) was sent, ACK received
'&H20:   Slave address (send condition) was sent, NACK received

   I2cstart
      bytTWI_Status = TWSR And &B11111000         'Start status code
      If bytTWI_Status = &H08 Or bytTWI_Status = &H10 Then       'Start condition is set
         I2cwbyte bytAddr                         'Send Slave address
         bytTWI_Status = TWSR And &B11111000      'Slave address write status code
         If bytTWI_Status = &H18 Then             'Slave answered with ACK
            CheckI2C = 1                          'Slave found
         Else
            CheckI2C = 0                          'Slave not found
         End If
      Else
         CheckI2C = 2                             'Start error
      End If
   I2cstop

End Function

Hier werden mögliche Fehler bei der Ausführung von I2Cstart und I2Cwbyte (Ansprechen des Slave) unterschieden.

Einordung: