RS485 Master - Slave

Im Zuge der Weiterentwicklung des Antennentuners stand auch ein Aufräumen in der Software auf dem Programm. Ein Schwerpunkt lag hier zunächst auf der Master-Slave-Kopplung über den RS485-Bus, zumal ein zweiter Slave hinzukommen sollte. Ergänzend zu RS485-Long Distance Call hier also die Ergebnisse.

1  Protokoll

Ein Protokoll, mit dem sich Master und Slaves verständigen, muss sein, keine Frage. Das Protokoll hat einen einheitlichen Aufbau und eine feste Länge von 7 Bytes.

Byte Bedeutung
1 Zieladresse
2 Absenderadresse
3 Kommando
4 Datenbyte 1
5 Datenbyte 2
6 Datenbyte 3
7 Checksumme aus Bytes 1 bis 6

Wenn neben dem Master mehrere Slaves am Bus hängen, sind Ziel- und Absenderadresse im Protokoll erforderlich, damit die Post hin wie zurück auch immer richtig zugestellt wird.

Das setzt auch voraus, dass es auf dem Bus gesittet zugeht, also nicht durcheinander gequakt wird. Vornehmer ausgedrückt, dass keine Kollisionen stattfinden. Das ist Sache des Masters.

2  Kommunikationsstrategie

Master und Slaves gehen unterschiedlich bei der Kommunikation über den Bus vor:

  • Der Master hat die Oberhoheit über den Bus. Er alleine ergreift die Initiative mit einem Befehl an einen Slave und wartet anschließend auf eine Rückmeldung. Erst mit der Rückmeldung ist solch ein Vorgang abgeschlossen. Wenn der angesprochene Slave nicht innerhalb einer festlegten Zeit antwortet, gibt es einen Timeout.
  • Ein Slave horcht ständig am Bus, um festzustellen, ob ein ihn betreffender Befehl vorliegt, und bestätigt dem Master die Ausführung.

Entsprechend sind Senden und Empfangen bei Master und Slaves mit unterschiedlichen Ansätzen programmiert.

Damit die Kommunikation technisch überhaupt möglich ist, müssen Baudrate und Übertragungsart der UART im AVR richtig eingestellt werden und bei Master und Slaves übereinstimmen. Mit "$Baud = xxxx" stellt BASCOM eine Baudrate von z.B. xxxx=9600 Baud und 8N1 (8 Bits, no parity, 1 Stoppbit) ein. Dazu passen muss die Taktfrequenz mit "$Crystal". 16 MHz ergeben für 9600 und 19.200 Baud z.B. einen tolerierbaren Baudratenfehler von 0,16%, mit einem 14,7456 MHz-Quarz wäre der Baudratenfehler in beiden Fällen 0%. Eine identische Taktfrequenz bei allen Busteilnehmern ist nicht erforderlich. Die daraus abgeleitete Baudrate muss nur stimmen.

3  Master-Software

3.1  Erklärungsteil

$regfile = "m32def.dat"                           'ATMega32
$crystal = 16000000                               '16 MHz, baud error 0.16%, works
'$crystal = 14745600                               '14.7456 MHz xtal, baud error 0%
$baud = 9600                                      'Baud rate RS485
$hwstack = 40
$swstack = 40
$framesize = 40

Für dieses Testprogramm ist ein ATmega32 wahrlich unterfordert. Er steckte nun mal auf dem Testboard.

Beim ATmega32 liegt die Hardware-UART an den Pins D.0 (RXD) und D.1 (TXD). Pin D.2 wird als Output konfiguriert. Er schaltet den MAX485 wahlweise auf Senden (Pegel high) oder auf Empfangen (Pegel low).

'Configure port D -------------------------------------------------------------
DDRD = &B00000100                                 'D.0   RS485 RXD
'                                                 'D.1   RS485 TXD
'                                                 'D.2   RS485 RE/DE output
PORTD = &B11111000                                'Pullup D.3 to D.7
bitRS485 Alias Portd.2                            '=1:RS485 Transmit, =0: Receive
Const On = 1
Const Off = 0

'Common Variables, multiply used ----------------------------------------------
Dim bytTmp0 As Byte
Dim bytTmp1 As Byte

'Variables RS485-Communication ------------------------------------------------
Const bytConAdr = &H00                            'ATU Controller  COM address
Const bytATUAdr = &H01                            'ATU remote unit COM address
Const bytTRXAdr = &H02                            'TRX Com         COM address
Const bytFreqCall = &HFA                          'Hex code for TRX freq call & response
Const bytDummy = &HFF                             'Dummy byte
Const bytFrameLen = 7                             'Length of RS485 frame
Const bytFrameLenMinus1 = bytFrameLen - 1         '... minus 1
Dim bitComBusy As Bit                             '=1: RS485 communication active
Dim bytComErr As Byte                             '0: Message read is OK, >0 else
Dim bytCRC8 As Byte                               'Crc8 checksum (1 byte)
Dim bytBuffCount As Byte                          'Buffer byte counter
Dim bytBuffInp(bytFrameLen) As Byte               'Input Buffer
Dim bytBuffOut(bytFrameLen) As Byte               'Output Buffer
Dim wrdTRXFreq As Word                            'TRX frequency (kHz)
Dim bytTRXFreq(2) As Byte At wrdTRXFreq Overlay

Beispielhaft soll hier nur die Kommunikation zwischen Master (Controller) und Slave 2 (TRX) betrachtet werden. Hier fragt der Master den Slave 2 periodisch nach der aktuellen Frequenz wrdTRXFreq. Die byteweise Übertragung ermöglicht bytTRXFreq(2) als Overlay über wrdTRXFreq. Die zeitliche Steuerung der periodischen Abfrage und für das Timeout erfolgt mit dem Timer0.

'Configure Timer0-Interrupt (Timer0 = 8bit, 2^8=256 counts) -------------------
'Configuration with xtal 16MHz for use with a frequency counter
'Overflow time: Overflow counts * Prescale / crystal frequency
'Overflow counts = 256 - PresetTimer0 = 200 (PresetTimer0 = 56)
'= 200 * 1024 / 16.000.000 = 12.8 msec = ~ 78 Hz (crystal 16 MHz)
'Possible Prescales: 8, 64, 256, 1024, here 1024
Config Timer0 = Timer , Prescale = 1024
Const PresetTimer0 = 56                           'Initialize Timer0 (shorten time, >0)
On Timer0 Timer0_isr                              'On Overflow go to Timer0_isr
Dim bytCallTRX_cnt As Byte                        'Timer counter for TRX call
Const bytCallTRX_runs = 35                        'Timer counts for TRX call ~0.5sec
Dim bytTimeout_cnt As Byte                        'Timer counter for time out
Const bytTimeout_runs = 70                        'Timer counts for timeout ~1sec

'Declare subs & functions -----------------------------------------------------
Declare Sub GetTRXFreq

'Start timer0, initialize data ------------------------------------------------
Enable Timer0
Timer0 = Presettimer0                             'initialize Timer0
SREG.7 = 1                                        'Enable all interrupts

bytCallTRX_cnt = 0                                'Initialize counter for TRX call
bytTimeout_cnt = 0                                'Initialize counter for time out
bytBuffCount = 0                                  'Initialize RS485 Input buffer counter
bitRS485 = Off                                    'RS485 receive
bitComBusy = 0                                    'RS485 communication not busy

3.2  Überprüfung der Slave-Betriebsbereitschaft

Zu Beginn ist es sinnvoll zu überprüfen, ob der/die Slave(s) überhaupt betriebsbereit sind. Dazu wird der Slave mit einem ihm zugehörigen Kommando angesprochen. Antwortet er mit einem Error-Code bytComErr = 0, ist alles OK. Drei Versuche hat der Slave. Wenn das nicht gereicht hat, läuft das Programm auf Stop. Der Slave scheint irgendwie unpässlich zu sein.

'Check TRX communication unit -------------------------------------------------
Locate 1 , 1
Lcd "TRX Com OK?"
Locate 2 , 1
bytTmp1 = 0
For bytTmp0 = 1 To 3                              'Try it 3 times
    Call GetTRXFreq                               'Send frequency request to TRX
    If bytComErr = 0 Then                         'Freq received, TRX unit is ready
       bytTmp1 = 1
       Exit For
    End If
    Waitms 100
Next bytTmp0
If bytTmp1 = 1 Then
   Lcd "... is OK!"
Else
   Lcd "... is not OK!"
   Stop                                           'Program stopped
End If
Wait 1
Cls

3.3  Hauptschleife, Do…Loop

Die Abfrage des Slave 2 erfolgt in der Do…Loop ca. alle 0,5 Sekunden, einstellbar oben mit " bytCallTRX_runs". Kollisionen auf dem Bus verhindert die in den Kommunikationsroutinen gesetzte Variable "bitComBusy". Sie ist nur erforderlich, wenn mehr als ein Slave zu bedienen ist. Es könnte ja sein, dass gerade die Kommunikation mit einem Slave aktiv ist, während in der Do…Loop, z.B. durch eine manuelle Aktion, die Kommunikation mit dem anderen Slave angestoßen werden soll. Beim nächsten Durchlauf durch die Do…Loop hat sich das dann möglicherweise erledigt.

Do
   'Check wait time for TRX call
   If bitComBusy = 0 Then                         'RS485 bus is not busy
      If bytCallTRX_cnt > bytCallTRX_runs Then    'Wait time elapsed
         Call GetTRXFreq                          'Send TRX frequency request
         bytCallTRX_cnt = 0                       'Reset Call timer counter

         'Insert bytComErr error handling here, bytComErr=0: all OK

      End If
   End If

   'Communication with second bus slave
   If bitComBusy = 0 Then                         'RS485 bus is not busy
      'Call second slave...
   End If
Loop

Die Kommunikation mit dem zweiten Slave ist hier nur angedeutet, um das Prinzip zu verdeutlichen.

3.4  Timer

In der Timer0-Interrupt Serviceroutine werden die Zähler für die Slave-Abfrage und das Timeout für eine Antwort hochgezählt. Mit der o.a. Parametrierung des Timer0 erfolgt das alle 12,8 msec.

Timer0_isr:
'Interrupt Service Routine for Timer0 (Time out & TRX call scheduler)

   Timer0 = Presettimer0

   'Communication timeout -----------------------------------------------------
   Incr bytTimeout_cnt                            'Counter for receive time out

   'TRX frequency request scheduler -------------------------------------------
   Incr bytCallTRX_cnt
Return

3.5  Slave-Abfrage

In der Routine GetTRXFreq wird das Sendeprotokoll aufgesetzt und mit "Printbin" auf den Bus gegeben, nachdem mit "bitRS485 = On" der MAX485 auf Senden gestellt wurde. Nach MAX485-Datenblatt kann dies bis zu 2ms dauern, deshalb nachfolgend ein Waitms 2.

Mit bytComBusy=1 wird ein Flag gesetzt, das signalisiert, dass nun der Bus belegt ist.

"Printbin" sollte der BASCOM-Beschreibung nach erst nach Abarbeitung des kompletten Ausgangspuffers "bytBuffout" beendet werden, worauf man sich aber offenbar nicht ganz verlassen kann. Deshalb wurde die Schleife Do…Loop Until UCSRA.6=1 angehängt, um das Register zuverlässig zu prüfen. Das 6. Bit im UCSRA-Register des ATmega32 ist "TXC " (UART Transmit Complete).

Sub GetTRXFreq
'Call frequency (kHz) from TRX (slave)
'Byte  Content
'1     Address of message destination ("to")
'2     Address of message source  ("from")
'3     Command
'4     Data byte 1
'5     Data byte 2
'6     Data byte 3
'7     Checksum of bytes 1 to 7 (CRC8, 1 byte)

'"PrintBin" is used (no ASCII transformation)
'Output:
'wrdTRXFreq    Frequency (kHz) received from TRX, overlayed with bytTRXfreq
'bytComErr     = 0: OK, frequency received
'              = 1: Time out, Input Buffer bytBuffInp not complete
'              = 2: Message destination is not me, the controller
'              = 3: Message source is not the TRX
'              = 4: Checksum is not OK
'              = 5: No frequency received
'              = 6: Command not OK

   bitComBusy = 1                                 'RS485 communication busy
   bitRS485 = On                                  'set MAX485 to transmit
   Waitms 2                                       'Wait for MAX485 to switch
   UCSRA.6 = 1                                    'Clear UART Transmit Complete bit
   'Send TRX frequency request, configure protocoll buffer
   bytBuffOut(1) = bytTRXAdr                      'destination address (TRX)
   bytBuffOut(2) = bytConAdr                      'Sender address (ATU controller)
   bytBuffOut(3) = bytFreqCall                    'Request TRX frequency
   bytBuffOut(4) = bytDummy                       'not used, dummy
   bytBuffOut(5) = bytDummy                       'not used, dummy
   bytBuffOut(6) = bytDummy                       'not used, dummy
   bytCRC8 = Crc8(bytBuffOut(1) , bytFrameLenMinus1)
   bytBuffOut(7) = bytCRC8                        'CRC8
   Printbin bytBuffOut(1) ; bytFrameLen           'Send array bytBuffOut
   Do
   Loop Until UCSRA.6 = 1                         'Wait until all bytes are sent
   bitRS485 = Off                                 'Set MAX485 to receive
   Waitms 2                                       'Wait for MAX485 to switch

Nun horcht der Master und wartet auf die Quittierung vom Slave. Die muss innerhalb des festgelegten Timeouts erfolgen.

   Do
      If Ischarwaiting() = 1 Then                 'Wait for character
         Inputbin bytTmp0                         'Read character
         Incr bytBuffCount                        'Next address in buffer
         bytBuffInp(bytBuffCount) = bytTmp0       'Save character in buffer
      End If
   Loop Until bytBuffCount = bytFrameLen Or bytTimeout_cnt > bytTimeout_runs

"Ischarwaiting()" ist solange 1, wie Zeichen im UART-Empfangsregister vorliegen, die noch nicht abgeholt wurden. Das besorgt das nachfolgende "Inputbin". Die Leseschleife wird durchlaufen, bis alle im Protokoll festgelegten Zeichen gelesen wurden oder aber die Timeout-Zeit überschritten ist.

Stimmt die Anzahl der gelesenen Zeichen, erfolgt die Auswertung nach den im Protokoll festgelegten Bedeutungen. Bei Unstimmigkeiten werden Fehlercodes aufgesetzt. Deren Behandlung im Hauptprogramm ist der Übersichtlichkeit halber weggelassen. Bei fehlerfreier Rückmeldung (bytComErr = 0) steht die übermittelte TRX-Frequenz wrdTRXFreq zur Verfügung.

   If bytBuffCount = bytFrameLen Then             'Response is complete
      If bytBuffInp(1) = bytConAdr Then           'It's me, the ATU controller
         bytCRC8 = Crc8(bytBuffInp(1) , bytFrameLenMinus1)
         If bytCRC8 = bytBuffInp(7) Then          'Checksum is OK
            If bytBuffInp(2) = bytTRXAdr Then     'It's from the TRX
               If bytBuffInp(3) = bytFreqCall Then       'It's frequency request
                  If bytBuffInp(4) = 0 Then       'Error code 0: OK, freq is sent
                     bytTRXFreq(1) = bytBuffInp(5)
                     bytTRXFreq(2) = bytBuffInp(6)
                     bytComErr = 0                'Frequency is received
                  Else
                     wrdTRXFreq = 0
                     bytComErr = 5                'No freq sent
                  End If
               Else
                  bytComErr = 6                   'command not OK
               End If
            Else
               bytComErr = 3                      'Source address not OK
            End If
         Else
            bytComErr = 4                         'Checksum not OK
         End If
      Else
         bytComErr = 2                            'destination address not OK
      End If
   Else                                           'Response not complete or missing
      bytComErr = 1                               'Timeout
   End If

   bitComBusy = 0                                 'RS485 communication not busy
   Waitms 10
End Sub

Zum Schluss wird mit "bytComBusy = 0" der Vorgang abgeschlossen und der Bus wieder freigegeben. Warum auch immer, muss mit dem "Waitms 10" danach gewartet werden, bis wieder Ruhe auf dem Bus ist. Welches Ereignis hier abzufragen wäre, um das mir unsympathische "Waitms 10" zu vermeiden, habe ich nicht herausgefunden.

4  Slave-Software

4.1  Erklärungsteil

Der Slave mit einem ATmega48 ist anders als der Master mit einem 14,7456 MHz-Quarz bestückt.

$regfile = "m48def.dat"                           'Controller ATmega48
$crystal = 14745600                               '14.7456 MHz xtal
$baud = 9600                                      'Baud rate RS485
$hwstack = 60
$swstack = 40
$framesize = 40

'RS485 communication at Port D.0...D.2, D.0=RXD, D.1=TXD
DDRD.2 = 1                                        'D.2 RS485 RE/DE is output
bitRS485 Alias Portd.2                            '=1:Transmit, =0: Receive

Die Initialisierung der Port-Pins ist hier nur für D.0 bis D.2 angegeben. D.0 und D.1 initialisiert "$Baud".
Alle nicht benutzten Ports sollten grundsätzlich als Input mit aktiviertem Pullup initialisiert werden.

Const On = 1
Const Off = 0

'Variables RS485-Communication ------------------------------------------------
Const bytConAdr = &H00                            'Master  COM address (Controller)
Const bytATUAdr = &H01                            'Slave 1 COM address (ATU)
Const bytTRXAdr = &H02                            'Slave 2 COM address (TRX)
Const bytDummy = &HFF                             'Dummy byte
Const bytFreqCall = &HFA                          'Hex code for TRX freq call & response
Const bytFrameLen = 7                             'Length of RS485 frame
Const bytFrameLenMinus1 = bytFrameLen - 1         '... minus 1
Dim bytBuffCount As Byte                          'Byte Buffer counter
Dim bytComErr As Byte
Dim bytCRC8 As Byte                               'Crc8 checksum (1 byte)
Dim bytBuffInp(bytFrameLen) As Byte               'Input Buffer
Dim bytBuffOut(bytFrameLen) As Byte               'Output Buffer
Dim bytFreqTest As Byte
Dim bytTRXSend As Byte                            ':1 Message OK, send response
Dim wrdTRXFreq As Word
Dim bytTRXFreq(2) As Byte At wrdTRXFreq Overlay

'Subs and functions -----------------------------------------------------------
Declare Sub SndTRXFreq

'Initialize data --------------------------------------------------------------
bitRS485 = OFF                                    'RS485 receive
wrdTRXFreq = 3500                                 'Start frequency
bytFreqTest = 0
bytBuffCount = 0                                  'Com buffer counter

'Enable UART interrupt service routine ----------------------------------------
On URXC RXD_isr                                   'Go to RXD_isr if character is received
Enable URXC                                       'Enable UART receive interrupt
SREG.7 = 1                                        'Enable all interrupts

Anders als beim Master liegt hier das Hauptaugenmerk auf das Horchen am Bus. Deshalb wird hier eine Empfangs-Interruptroutine verwendet. Sie wird bei jedem empfangenen Zeichen unabhängig vom Programmablauf aufgerufen, um auch ja nichts zu verpassen.

4.2  Hauptschleife, Do…Loop

Sobald ein Empfangsprotokoll vollständig ist, wird dieses ausgewertet.

Do

   If bytBuffCount = bytFrameLen Then             'Master request is complete
      'Don't know why, Waitms must be >= 15ms before setting bytBuffCount = 0 ???
      'Without or shorter less than bytFrameLen bytes will be sent to master
      'So master will encounter timeout
      Waitms 15
      bytBuffCount = 0
      Call SndTRXFreq                             'Check request & send frequency
      If bytFreqTest < 30 Then
         wrdTRXFreq = wrdTRXFreq + 10             'Loop 3.5...3.8MHz
         Incr bytFreqTest
      Else
         wrdTRXFreq = 3500
         bytFreqTest = 0
      End If
   End If

Loop

Rätselhaft, aber offensichtlich notwendig, ist das blau markierte "Waitms 15". Ohne bzw. mit geringerem Wert wird in der nachfolgenden Sub SndTRXFreq das Sendeprotokoll nicht vollständig abgeschickt, wodurch es im empfangenden Master zu einem Timeout kommt.

Nach Bearbeiten der Masteranfrage in Sub SndTRXFreq wird für diesen Test die Frequenz um 10kHz für die nächste Anfrage erhöht. Im reellen Programm des ATU-TRX-Kommunikationsmoduls erhält dieses die jeweils aktuelle Frequenz über eine I2C-Kopplung vom TRX Local Oscillator (LO).

4.3  UART Interruptroutine

Jedes empfangene Zeichen löst den Interrupt aus. Erst nach Umspeichern des Zeichens im UART Data Register UDR in den Input-Puffer kann das nächste anstehende Zeichen empfangen werden.

RXD_isr:
'URXC UART receive complete interrupt service routine
'Read current received Byte from UART register UDR
'Output:  bytBuffInp(1...bytBuffCount), Input buffer
'         bytBuffCount, current length of input buffer

   Incr bytBuffCount
   bytBuffInp(bytBuffCount) = UDR                 'Put next byte into buffer
Return

4.4  Auswerten des Protokolls und Quittierung an den Master

Zunächst die Auswertung des Master-Protokolls. An erster Stelle die Frage, ob mit der Zieladresse dieser Slave gemeint war. Es werden entsprechende Fehlercodes aufgesetzt.

Sub SndTRXFreq
'Get frequency request (kHz) from Master
'If it's OK, send frequency back to Master
'Check the received message:
'- is the destination my address?
'- is the source the ATU controller?
'- is the received checksum OK?
'- is the 'Send frequency' command OK?

bytTRXSend = 1                                 'Send response
If bytBuffInp(1) = bytTRXAdr Then              'It's for me, the TRX
   If bytBuffInp(2) = bytConAdr Then           'It's from the ATU controller
      bytCRC8 = Crc8(bytBuffInp(1) , bytFrameLenMinus1)
      If bytCRC8 = bytBuffInp(7) Then          'Checksum is OK
         If bytBuffInp(3) = bytFreqCall Then   'It's frequency request
            bytComErr = 0                      'Message is OK
         Else
            bytComErr = 1                      'No frequency request
         End If
      Else
         bytComErr = 2                         'Checksum not OK
      End If
   Else
      bytComErr = 3                            'Source is not ATU controller
      bytTRXSend = 0                           'Do not send response
   End If
Else
   bytComErr = 4                               'Message was not for me
   bytTRXSend = 0                              'Do not send response
End If

Wenn feststeht, dass der Master eine Antwort erwartet, wird diese aufgesetzt und vergleichbar zum Masterprogramm oben abgeschickt. Der ATmega48 hat mehrere UARTs, hier daher der Index "0" in "UCSR0A".

If bytTRXSend = 1 Then                         'Send TRX frequency response
   bitRS485 = On                               'set MAX485 to transmit
   Waitms 2                                    'Wait for MAX485 to switch
   UCSR0A.6 = 1                                'Clear UART Transmit Complete bit
   bytBuffOut(1) = bytConAdr                   'Destination address (Controller)
   bytBuffOut(2) = bytTRXAdr                   'Source address (TRX)
   bytBuffOut(3) = bytFreqCall                 'TRX frequency call command
   bytBuffOut(4) = bytComErr                   'Com error code
   If bytComErr = 0 Then                       'No Com error
      bytBuffOut(5) = bytTRXFreq(1)            'Frequency high byte
      bytBuffOut(6) = bytTRXFreq(2)            'Frequency low  byte
   Else
      bytBuffOut(5) = bytDummy                 'not used, dummy
      bytBuffOut(6) = bytDummy                 'not used, dummy
   End If
   bytCRC8 = Crc8(bytBuffOut(1) , bytFrameLenMinus1)
   bytBuffOut(7) = bytCRC8                     'CRC8
   Printbin bytBuffOut(1) ; bytFrameLen        'Send array bytBuffOut
   Do
   Loop Until UCSR0A.6 = 1                     'Wait until all bytes are sent
   bitRS485 = OFF                              'ready for receive again
   Waitms 2                                    'Wait for MAX485 to switch
End If
End Sub

Das Aufsetzen und Abschicken des Protokolls erfolgt analog zum obigen Beispiel im Master.

Einordung: