2 Master mit TWI an stummen Slave

Gespeichert von DL6GL am Di., 28.01.2020 - 09:16

Das Verhältnis zwischen Master und Slave im ersten Abschnitt ist ja nun recht einseitig. Nur der Master hat ein Mitteilungsbedürfnis. Der Slave reagiert nur mit verschämten ACK's oder NACK's, indem er mal eben kurz an der SDA-Leitung zupft oder auch nicht. Das erledigt die TWI-Hardware. Aus der Entwicklung der Software zur Steuerung eines Grafik-TFT-Shield war seinerzeit noch ein Wunsch übrig geblieben, Touch-Ereignisse vom TFT-Slave zurück an den Master zu geben.

Das Rad muss ja nicht ständig neu erfunden werden, daher wurde das Web befragt mit der Bedingung "BASCOM". Die BASCOM-Fraktion zeigte sich allerdings, gemessen an veröffentlichten Lösungen, arg bedeckt. Realisierungen in C waren aber reichlich zu finden, z.B. [1], [2], [3], auf denen die hier beschriebene Lösung basiert. Wohl oder übel musste ich mich mal in C reinknien. Für einen Physiker aus der 68'er-Generation, der aus der FORTRAN- und MS Visual Basic-Ecke kommt, war ein etwas dickeres Brett zu bohren. Nachdem insbesondere die anfangs schwer zu deutenden C-Operatoren einigermaßen verstanden waren, ließen sich die C-Quelltexte doch ziemlich flüssig lesen. Zugegeben, die C-Syntax ist elegant, für meinen Geschmack aber dann doch reichlich verdichtet, ja kryptisch. Egal wie, so nahm die BASCOM-Variante von [1], Version "TWI_USI_master_slave_c_120303.zip" (auf der Seite unten), so langsam Gestalt an.

Doch der Reihe nach: Zunächst sollte ein Äquivalent zu der Lösung in Abschnitt 1, also stummer Slave, gefunden werden, d.h. zentrale Aufgaben:

  • Ersetzen der BASCOM-Befehle wie I2CStart, I2CWByte, I2CStop im Master durch ein simples TWI_MasterWrite,
  • Bearbeitung der kompletten I2C-Kommunikation innerhalb einer Interrupt-Serviceroutine sowohl im Master als auch im Slave.

2.1       TWI-Register

Mit Verzicht auf die BASCOM-Standards müssen wir uns eingehender mit den TWI-Registern befassen. Die müssen wir nun - näher an der Hardware - hantieren. Neben TWAR (TWI Slave Address Register) und TWBR (TWI Bit Rate Register) sind die in den Interrupt-Serviceroutinen benutzten Register

  • TWCR (TWI Control Register)
Bit 7 6 5 4 3 2 1 0
Name TWINT TWEA TWSTA TWSTO TWWC TWEN - TWIE
Default 0 0 0 0 0 0 0 0
Zugriff R/W R/W R/W R/W R/W R/W R R/W
TWINT TWI Interrupt flag, Bit 7
1 Wird von der Hardware auf 1 gesetzt, wenn eine TWI-Operation beendet wurde.
Um eine neue Operation zu starten, muss TWINT mit TWINT=1 (ja 1) per Software gelöscht werden.
Wenn globale Interrupts mit SREG.7=1 aktiviert sind und TWIE=1 gesetzt ist, wird ein TWI-Interrupt ausgelöst, der in der Interrupt-Serviceroutine bearbeitet werden kann. Hier kann die Software entsprechend reagieren.
0 Eine TWI-Operation wurde gestartet, ist aber noch nicht beendet.
TWEA TWI Enable Acknoledge Bit, Bit 6
1 Die Hardware sendet ein ACK (acknoledged) nachdem Daten angekommen sind.
0 Die Hardware sendet ein NACK (not acknoledged) nachdem Daten angekommen sind.
Daten können in beiden Fällen die Slave-Adresse oder Nutzdaten sein.
TWSTA TWI START Condition Bit, Bit 5
1 Der Master reserviert sich den Bus, ggf. wartet er, bis der Bus durch ein STOP
freigegeben wurde. Danach wird erneut ein START abgesetzt.
0 Wenn das START-Bit abgeschickt wurde, muss es per Software wieder zurückgesetzt werden.
TWSTO TWI STOP Condition Bit, Bit 4
1 Der Master gibt den Bus frei (SDA und SCL gehen auf high (+VCC).
0 Die Hardware setzt TWSTO nach Senden einer STOP-Bedingung automatisch zurück.
TWWC TWI Write Collision Flag, Bit 3
1 Die Hardware erkennt eine Kollision auf dem Bus, wenn versucht wird, in das Datenregister TWDR Daten zu schreiben und TWINT=0 ist (noch offene Operation).
0 Die Hardware löscht TWWC automatisch, wenn in das Datenregister TWDR Daten geschrieben werden und TWINT=1 ist (keine noch offene Operation).
TWEN TWI Enable Bit, Bit 2
1 TWI ist aktiviert. Damit werden die Mehrfachfunktionen zweier spezifizierter I/O-Pins, z.B. beim ATmega16 PC.0 und PC.1, auf I2C/TWI umgeschaltet, d.h. diese Pins werden mit dem AVR-internen  TWI-Interface verbunden.
0 TWI ist deaktiviert.
Bit 1 Reserviert, nur Lesezugriff. Wird grundsätzlich auf 0 gesetzt.
TWIE TWI Interrupt enable Bit, Bit 0
1 TWI-Interrupt ist aktiviert. Wenn Interrupts global aktiviert (SREG.7=1) und TWINT = 1 sind, wird ein Interrupt Request ausgelöst. Damit kann eine Interrupt-Serviceroutine (TWI_isr) angesprungen werden.
0 Kein Interrupt.
  •  TWSR (TWI Status Register)
  Das 8 Bit breite TWI Status Register hat drei Abschnitte:
1 Bits 7 bis 3, TWS7…TWS3 als eigentliche Status-Bits.
Hieraus werden die Status-Codes einer vorangegangenen Operation ermittelt.
Mit Ausmaskieren dieser 5 Bits erhält man den Status-Code in Hex. Diese sind in den Datenblättern angegeben für Master Transmit, Master Receive, Slave Transmit und Slave Receive. In den Interrupt-Serviceroutinen TWI_isr in den Beispielen werden die verwendeten Hex-Codes gezeigt.
2 Bit 2 ist reserviert. Wird immer 0 gesetzt.
3 Bits 1 und 0: TWPS (TWI Prescaler Bits), Vorteiler für die Busgeschwindigkeit, siehe Datenblatt. In den Programmen hier sind TWPS1 = TWPS0 = 0, also Vorteiler 1.
  • TWDR (TWI Data Register)
  Enthält das zu sendende oder empfangene Datenbyte.
  Schreiben: TWDR = DataByte
  Lesen: DataByte = TWDR

 2.2  Das Master-Programm

Ein "Config SDA" und "…SCL" in BASCOM ist schnell hingeschrieben. Die direkte Benutzung der TWI-Register zur Initialisierung ist aber auch nicht aufwendiger. Es müssen die jeweils vorgesehenen SDA- und SCL-Pins verwendet werden, beim ATmega 16 im Master also Port C.1 (SDA ) und Port C.0 (SCL).

Sub TWI_MasterInit
'Configure TWI on dedicated controller SCL/SDA hardware ports -----------------
'For ATmega16: SCL = PortC.0 and SDA = PortC.1, no "Config" necessary

   TWBR = 72                                      'Bit rate register, 100kHz SCL clock
   'TWBR = 32                                      'Bit rate register, 200kHz SCL clock
   'TWBR = 12                                      'Bit rate register, 400kHz SCL clock
   TWDR = &HFF                                    'Default data
   TWSR = &B0000_0000                             'Clear status register, prescaler 1
   'Enable TWI, nothing else
   TWCR = &B0000_0100                             'Enable TWI, TWEN is set

End Sub

Die Formel zur Berechnung des TWBR ist im Datenblatt zu finden. Hier sind die Prescaler TWPS=0. Zunächst wird nur das TWI mit TWCR.TWIE (Bit 2) aktiviert.

Im Hauptprogramm werden mit …

'Enable TWI interrupt service routine -----------------------------------------
On TWI TWI_isr                                    'Go to TWI_isr on TWI events
Call TWI_MasterInit                               'Initialize TWI master
SREG.7 = 1                                        'Enable all interrupts

… die Interrupts und die Interrupt-Serviceroutine komplett aktiviert.

Für unseren Test werden fortlaufend jeweils 3 Datenbytes an den Slave geschickt. Das entspricht dann der ersten Zeile im Aufmacherbild oben. Die erste Zahl dort ist die Slave-Adresse in hex.

Do
   For bytRun = 1 To 5
      'Set up user defined message buffer for test ----------------------------
      'bytMsg_Buf(1) = slave address, set in TWI_MasterWrite
      bytMsg_Buf(2) = bytRun
      bytMsg_Buf(3) = bytRun + 1
      bytMsg_Buf(4) = bytRun + 2
      Call TWI_MasterWrite(4 , bytSLV_Addr)       'Send data to slave
      Locate 1 , 1
      LCD "Out " ; Hex(bytMsg_Buf(1)) ; " " ; bytMsg_Buf(2) ; " " ; bytMsg_Buf(3) ; " " ; bytMsg_Buf(4)
      Wait 3
   Next bytRun
Loop

Die Sub TWI_MasterWrite nimmt uns alle Arbeit ab. Wir müssen nur das zu sendende Byte-Array bytMsg_Buf wie oben mit Daten besetzen.

Sub TWI_MasterWrite(byVal bytMsg_Size As Byte , byVal bytSLA As Byte)
'Write a message aray bytMsg_Buf to slave, address bytSLA, via TWI_isr.
'Input:  bytMsg_Buf     Message array to send
'        bytMsg_Size    Number of message bytes incl. slave address (bytMsg_Buf(1))
'        bytSLA         Slave Address
'Output: bytTWI_Success =FALSE before beeing transmitted via TWI_isr
'        bytTWI_Buf     Data buffer, copy of bytMsg_Buf to be processed in TWI_isr

   Call TWI_Ready                                 'Wait while TWI is busy
   bytSLV_AddrW = bytSLA And &B1111_1110          'slave address + Write bit 0=0
   bytMsg_Buf(1) = bytSLV_AddrW                   'Slave write address
   bytTWI_BufLen = bytMsg_Size                    'Handled in TWI_isr
   For bytMsg_Cnt = 1 To bytMsg_Size              'Loop data bytes...
      bytTWI_Buf(bytMsg_Cnt) = bytMsg_Buf(bytMsg_Cnt)       '...into Transmit buffer
      bytTWI_Success = False                      'To be set True in TWI_isr

      'Enable TWI & TWI interrupt, set START condition
      TWCR = &B1010_0101                          'TWINT, TWSTA, TWEN, TWIE are set
      'Now TWI_isr starts runing, transmit data to slave...
   Next bytMsg_Cnt

End Sub

Nachdem die Slave-Write-Adresse in bytMsg_Buf(1) geschrieben ist und das Datenarray bytMsg_Buf in das Array bytTWI_Buf zur Bearbeitung in der Interrupt-Serviceroutine umgespeichert ist, rennt diese mit dem Befehl TWCR = &B1010_0101 los. TWINT=1 (Bit 7) löst das Ereignis aus; Mit TWSTA=1 (Bit 5) wird ein  START abgesetzt, TWEN=1 (Bit 2) aktiviert TWI, TWIE=1 (Bit 0) aktiviert den TWI-Interrupt.

Unzweckmäßig ist es hier jedoch, den TWI-Befehl "TWCR = &B1010_0101" in der For...Next-Schleife anzuordnen. Nach der Schleife, die zunächst den Puffer bytTWI_Buf für die TWI_isr füllt, wäre er besser aufgehoben so wie in der Sub TWI_MasterWrite in Abschnitt 3.

Ein Ausschnitt aus der Interrupt-Serviceroutine TWI_isr zeigt, was diese damit anfängt.

   bytTWI_Status = TWSR And &B1111_1000           'Status, TWSR status=bits 2...0

   Select Case bytTWI_Status

      'START (0x08) has been transmitted
      Case &H08                                   'Start condition
         GoTo ISR1                                'I don't like GoTo's

      'Repeated START (0x10) has been transmitted
      Case &H10                                   'Repeated Start condition
ISR1:                                             'From here &H08 or &H10
         bytTWI_BufCnt = 1                        'Buffer counter for slave address
         GoTo ISR2

      'Master transmitter mode ------------------------------------------------
      'TWINT is cleared in any case (TWINT=1) to continue operation.

      'SLA+W has been transmitted; ACK has been received     (0x18)
      'Byte to be sent is slave address SLA+W (bytTWI_BufCnt=1)or data byte
      Case &H18                                   'SLA+W has been transmitted
ISR2:
         GoTo ISR3

      'Data byte has been transmitted; ACK has been received (0x28)
      Case &H28                                   'Data byte has been transmitted
ISR3:                                             'From here &H18 or &H28
         If bytTWI_BufCnt <= bytTWI_BufLen Then   'More bytes to be sent
            TWDR = bytTWI_Buf(bytTWI_BufCnt)      'Put it into TWDR register to send
            Incr bytTWI_BufCnt                    'Next byte to send
            'Enable TWI and TWI interrupt, clear TWINT to continue
            TWCR = &B1000_0101                    'TWINT, TWEN, TWIE are set
         Else                                     'Was the last byte, send STOP
            bytTWI_Success = True                 'Message completed
            'Enable TWI, disable TWI interrupt, set STOP condition, clear TWINT
            TWCR = &B1001_0100                    'TWINT, TWSTO, TWEN, are set
         End If
...

   End Select

Es zeigte sich eine richtig nette Eigenart von C. Dort werden im "Switch" verschiedene Einstiegspunkte "Case xx" hintereinander abgearbeitet solange, bis ein "break" einen Ausstieg veranlasst. "Switch" entspricht dem BASCOM "Select Case" mit dem Unterschied, dass immer nur ein "Case xx" bearbeitet wird. Hier mussten ein paar unschöne GoTo's herhalten, um die Case-Bedingungen ohne "break" nachzubilden. Einfacher und übersichtlicher erscheint mir dagegen das Setzen der TWCR-Bits wie gezeigt statt des Bit-Schiebens und bitweisen Verknüpfens in C.

  • In "Case &H10" (Repeated START) wird der Zählindex bytTWI_BufCnt mit 1 initialisiert.
     
  • In "Case &H18" (SLA+W has been transmitted) oder "Case &H28" (Data byte has been transmitted) wird mit "TWDR = bytTWI_Buf(bytTWI_BufCnt)" das aktuelle Datenbyte auf das Datenregister TWDR zum Absenden geschrieben. Die Anzahl zu sendender Datenbytes ist in "bytTWI_BufLen" definiert. Wenn die erreicht ist, wird mit "bytTWI_Success = True" der Vollzug gemeldet und anschließend mit "TWCR = &B1001_0100" ein STOP (Bit 4) veranlasst. Damit ist der Vorgang abgeschlossen und der Bus wieder frei. TWINT (Bit 7) wird in allen Fällen gesetzt, um den jeweiligen Vorgang auszulösen.
     
  • Zusammengefasst: Der Master schickt alle mit "bytTWI_BufLen" festgelegten Daten-Bytes einschließlich der Slave-Adresse aus dem Array "bytTWI_Buf" nacheinander an den Slave und beendet den Vorgang mit einem STOP, alles erledigt, und gibt damit den Bus wieder frei.

2.3       Das Slave-Programm

Das Slave-Programm hat gegenüber dem alten in Abschnitt 1 nicht viel Neues zu bieten bis auf das Gegenstück zum obigen TWI_MasterWrite, einer universellen Leseroutine TWI_SlaveRead, und eine erweiterte Interrupt-Serviceroutine.

Das Hauptprogramm ist schon etwas ärmlich:

Do
   Call TWI_SlaveRead                             'Collect data from master
Loop

Hier die Leseroutine:

Sub TWI_SlaveRead
'Copy TWI buffer data bytTWI_Buf_RX received in TWI_isr to message buffer bytMsg_Buf
'Input:  bytTWI_BufCnt_RX  Current receive buffer counter in TWI_isr (plus 1)
'                          TWI_isr increments counter for the next transfer
'                          if some data have been received
'        bytTWI_Buf_RX     Receive buffer counter
'Output: bytMsg_Buf        Copied message buffer

   While TWI_Busy() = True                        'Wait while TWI is busy
   Wend

   If bytTWI_Success = True Then                  'Last transmission complete
      bytTWI_BufLen = bytTWI_BufCnt_RX - 1        'Current received data number
      If bytTWI_BufLen > 0 Then
         For bytMsg_Cnt = 1 To bytTWI_BufLen      'Copy data from TWI buffer
            bytMsg_Buf(bytMsg_Cnt) = bytTWI_Buf_RX(bytMsg_Cnt)
         Next bytMsg_Cnt
      End If
   End If

   'TWI buffer data are now saved.
   'If you want to show received data make it here before
   'restarting TWI_isr with the subsequent statements.
   Call ShowReceivedData

   'Continue reception via TWI_isr
   bytTWI_BufCnt_RX = 1                           'Initialize buffer counter
   'Enable TWI and TWI interrupt and Acknoledge , clear TWINT to continue
   TWCR = &B1100_0101                             'TWINT, TWEA, TWEN, TWIE are set

End Sub

Der Aufbau ist vergleichbar mit der o.a. Sub TWI_MasterWrite. Zunächst muss mit "While TWI_Busy() = True" solange gewartet werden, bis der Master mit Schreiben fertig ist. Das erfolgt hier, indem geprüft wird, ob TWIE (Bit 0) im TWCR nun 0 geworden ist:

Function TWI_Busy() As Byte
'Check if TWI Bus is busy (TWI.isr is activated), i.e. TWCR.TWIE = 1
'So check TWCR.TWIE bit
'Input:  TWCR  TWI control register
'Output: TWI_Busy =1=True: Is busy, =0=False: Is not busy

   TWI_Busy = TWCR AND &B0000_0001                '=0 if TWIE=0, =1 if TWIE=1

End Function

Weiter mit TWI_SlaveRead: Mit "bytTWI_Success = True" meldet die TWI_isr, dass ein Datenpaket vom Master vollständig gelesen wurde. Nur wenn außer der Slave-Adresse noch Daten-Bytes gelesen wurden, wird der TWI-Lesepuffer auf den Output-Puffer bytMsg_Buf umgespeichert.

Bis hierher ist Ruhe auf dem Bus, so dass nun die empfangenen Daten mit der Routine ShowReceivedData auf LCD ausgegeben werden können. Das braucht ja einen Augenblick. Erst danach wird der Bus mit einem neuen TWCR wieder aktiviert.

Der Vollständigkeit halber noch der Ausschnitt aus der TWI_isr:

   bytTWI_Status = TWSR And &B1111_1000           'Status, TWSR status=bits 7...3

   'Slave reveive mode -----------------------------------------------------
   'Own SLA+W has been received or data byte has been transmitted

   Select Case bytTWI_Status

      'Own address SLA + W has been received, ACK returned (0x60)
      Case &H60
         bytTWI_BufCnt_RX = 1                     'Initialize receive byte counter
         bytTWI_Buf_RX(bytTWI_BufCnt_RX) = TWDR   'Slave address
         Incr bytTWI_BufCnt_RX                    'Next buffer position for data
         'Enable TWI, TWI interrupt and Acknoledge, clear TWINT
         TWCR = &B1100_0101                       'TWINT, TWEA, TWEN, TWIE are set

      'Previously addressed with own SLA+W, data received, ACK returned (0x80)
      Case &H80
         'Max. bytTWI_BufSize bytes will be received
         If bytTWI_BufCnt_RX < bytTWI_BufSize Then       'Below max buffer size
            bytTWI_Buf_RX(bytTWI_BufCnt_RX) = TWDR       'Received byte into input buffer
            Incr bytTWI_BufCnt_RX                 'Increment byte counter
         End If
         'Enable TWI, TWI interrupt and Acknoledge, clear TWINT
         TWCR = &B1100_0101                       'TWINT, TWEA, TWEN, TWIE are set

      'STOP or Repeated START condition has been received (0xA0)
      Case &HA0
         'Enable TWI, disable TWI interrupt and Acknoledge, clear TWINT
         bytTWI_Success = True                    'Operation successful
         TWCR = &B1000_0100                       'TWINT, TWEN are set
. . .
   End Select

Die Logik ist vergleichbar mit der Master TWI_isr, nur dass hier die Inhalte des Datenregisters TWDR gelesen und auf den Empfangspuffer bytTWI_Buf_RX geschrieben werden.

  • In "Case &H0" wird Lesevorgang eines Datenpakets vom Master dann als beendet erkannt, wenn dieser ein STOP gesetzt hat (siehe 2.2). Das wird mit "bytTWI_Success = True" quittiert, worauf TWI_SlaveRead reagiert.
     
  • Der Empfang wird daraufhin beendet, in dem in TWCR TWIE (Bit 0) auf 0 gesetzt wird (Interrupt aus).
     
  •  Zusammengefasst: Der Slave liest solange, bis der Master mit einem STOP dem ein Ende macht und den Bus wieder frei gibt.
     

Beide TWI_isr enthalten auch schon die Teile Master Receive bzw. Slave Transmit. Die werden im Folgeabschnitt behandelt.