Forum: Mikrocontroller und Digitale Elektronik Serielle Kommunikation / Modbus Format - wie implementieren?


von Matze (Gast)


Lesenswert?

Hallo Spezialisten,

ich bastle schon sein einiger Zeit an einem "Cockpit" für ein E-GoKart 
und habe nun eigentlich nur noch vor mir, verschiedene Werte von einem 
BMS abzufragen.

Das BMS selber hat einen UART und es versteht neben diversen 
proprietären Kommandos auch Anfragen im Modbus Format. Letzteres würde 
ich gerne verwenden, um folgende Daten abzufragen:

 - Gesamtspannung
 - Gesamtstrom
 - SoC
 - Temperatur

Für die Kommandos habe ich mir eine Senderoutine geschrieben, die dem 
Befehl das CRC anfügt und das Telegram dann sendet:
1
uint8_t _sendBMSRequest(uint8_t *req, uint8_t length) {
2
  uint8_t success = 0;
3
4
    // temporäres Array für die vollständige Befehlssequenz:
5
    // txdata = req + CRC (2 Byte)
6
  uint8_t *txdata;
7
  txdata = malloc(length+2/sizeof(uint8_t));
8
  memcpy(txdata, req, length);
9
  
10
    // CRC berechnen und an txdata anhängen.
11
  uint16_t crc = _CRC16(req, length);
12
  length +=2;
13
  txdata[length-2]  = crc & 0xFF;
14
  txdata[length-1]  = crc >> 8;
15
  
16
    // txdata an das BMS übertragen:
17
  for(uint8_t i=0; i<length; i++) {
18
    
19
    // Warten bis Senden möglich ist:
20
    while (!(UCSR1A & (1<<UDRE1))) { }
21
22
    UDR1 = txdata[i];
23
  }
24
25
  success = 1;
26
  
27
  return(success);
28
}

Mit dieser Funktion kann ich bereits erfolgreich KOmmandos absetzen, 
z.B.
1
void bmsReset() {
2
  uint8_t req[] = { 0xAA, 0x02, 0x05};
3
  _sendBMSRequest(req, sizeof(req) / sizeof(uint8_t)  );
4
}
5
6
void main() {
7
    ...
8
    bmsReset();
9
}

Nach etwas Recherche habe ich schon aufgenommen, dass die Verwendung von 
dynamischen Arrays eher nicht so schlau ist. Die Gründe sind 
nachvollziehbar und ich werde das noch auf einen statischen Sendepuffer 
umbauen. Aber meine Frage ist momentan erstmal eine andere...

Es geht mir nämlich um das Lesen der Antwort vom BMS.

Es gibt im Grunde zwei Möglichkeiten, wie das BMS sich auf das Kommando 
zurückmeldet. Entweder mit einer Fehlermeldung oder bspw. mit den 
gelesenen Registern

Fehlermeldung (Payload ist immer 0x00):
1
  Startzeichen:   <0xAA> 
2
  Kommando:       <0x03> 
3
  Payload-Length: <0x00> 
4
  Error-Code:     <0x01>
5
  CRC:            2 Byte

Messwert
1
  Startzeichen:   <0xAA>
2
  Kommando:       <0x03>
3
  Layload-Length: <0x04>
4
  Payload:        hier z.B. 4 Byte
5
  CRC:            2 Byte


Wenn ich mir nun eine Funktion schreibe, um die Pack-Spannung 
abzufragen, sähe die ungefähr so aus:
1
float bmsGetPackVoltage() {
2
  float voltage = 0.0f;
3
  uint8_t req[] = { 0xAA, 0x03, 0x00, 0x37, 0x00, 0x02 };
4
  _sendBMSRequest(req, sizeof(req) / sizeof(uint8_t));
5
6
        // Response lesen    
7
        // Auf Startzeichen 0xAA warten
8
        // Empfangens Byte No. 3 = 0x00 (dann Error, sonst Payload-Length)
9
        // CRC prüfen
10
        // Payload extrahieren
11
12
        // 4 Byte in float umwandlen:
13
  // uint8_t payloadData[4] = { r[3], r[4], r[5], r[6] };
14
  // memcpy(&voltage, &data, sizeof(float));
15
    
16
  return(voltage);
17
}

Aber wie liest man jetzt wirklich sinnvollerweise die Antwort vom UART1 
ein?
Es könnte ja theoretisch auch sein, dass das BMS gar nicht antwortet... 
oder dass das Startzeichen nicht gefunden wird... oder das die 
Übertragung vor Erreichen innerhalb des Telegrams abbricht.
Es müsste also irgendwie noch sowas wie ein Timeout her.

Kann mir vielleicht jemand ein Beispiel (gern auch Pseudocode) geben, 
wie man sowas implementiert? Ich würde gern ohne den Interrupt arbeiten 
und wirklich nach jedem Request an das BMS durch Polling die Antwort 
abwarten.

Freue mich riesig über Denkanstöße oder Beispiele.

LG, Matze

von c-hater (Gast)


Lesenswert?

Matze schrieb:

> Es könnte ja theoretisch auch sein, dass das BMS gar nicht antwortet...
> oder dass das Startzeichen nicht gefunden wird... oder das die
> Übertragung vor Erreichen innerhalb des Telegrams abbricht.
> Es müsste also irgendwie noch sowas wie ein Timeout her.

Modbus RTU strotzt nur so von Timing-Constraints, die alle eingehalten 
werden müssen bzw. beim Verstreichen eine definierte Reaktion erfordern.

Steht alles im Standard, man muss ihn nur Lesen. Und zwar bevor man 
anfängt, ziellos rumzufrickeln...

> Kann mir vielleicht jemand ein Beispiel (gern auch Pseudocode) geben,
> wie man sowas implementiert? Ich würde gern ohne den Interrupt arbeiten

Ohne Interrupt (bzw. sonstige nebenläufige Behandlung) kannst du bei 
Modbus RTU nix reißen.

Einziger Ausweg: (relativ) hochfrequenter Timer und Statemachine.

von Maschinenstürmer (Gast)


Lesenswert?

Was mich betrifft würde ich raten einen fertigen Modbus Stack 
einzusetzen. Da kannst Du Dir viel Ärger ersparen. Auch wenn zum Teil 
Dich das Lernen motiviert, ist es nicht schlecht die Sourcen 
durchzuarbeiten um das Gerüst besser zu verstehen. Es ist nämlich viel 
komplizierter wie Du Dir es möglicherweise vorstellst. Man kann 
natürlich von der Modbus Specification ausgehen umd es ist machbar. Aber 
es schadet nie auch über den Zaun zu spähen. Guck mal hier als Beispiel:

https://www.embedded-solutions.at/en/freemodbus/

Ich arbeitete mit diesem vor über zehn Jahren mit einer Version für 
Cortex (STM32) und es funktionierte ohne irgendwelche Probleme in 
Real-Time mit dem Rest des Programms. Die Timing ist Kenndaten konform.

Wichtig ist zum Testen und Verifizieren auch Zugang zu einen 
vertrauenswürdigen Modbus Master zu haben. Ich arbeitete damals zum 
Testen erfolgreich mit MDBUS. So sieht das aus (Ist nicht als Werbung 
gedacht sondern nur als Beispiel):

https://www.calta.com/mdbus.htm

Nur meine 2 Cents wert...

von Maschinenstürmer (Gast)


Lesenswert?

Nachtrag. CANBus wäre für so ein Projekt auch keine schlechte Wahl.
Auch RS485 ohne Modbus. Wir machten früher viele Steuerprojekte mit 
adressierbaren RS485 RTUs mit ASCII Command Protokoll mit CRC16 zur 
Sicherheit. Ungleich zu MB war die Timing kein Thema und normale UART 
Routinen konnten wie üblich eingesetzt werden. Auch war die FW viel 
schlanker (PIC16F877) Die Kundenanlagen liefen auf Jahrzehnte 
störungsfrei. Auf einem Leitungspaar liessen sich zehn oder mehr RS485 
RTUs betreiben. Als Transceiver empfehlen sich die 250kb Version wie zB. 
Ein MAX483. Die SN76176 sind da viel empfindlicher gegen Störungen und 
schmieren manchmal auch ohne ersichtlichen Grund trotz Schutzschaltungen 
ab.

von Maschinenstürmer (Gast)


Lesenswert?

Arduino MB Library Sourcen könnten Dir als Vorlage auch nützlich sein. 
Wenn Du zufällig mit Arduino arbeitest, könntest Du es zum Erfahrung 
sammeln ausprobieren. Abgesehen davon hast Du dann auch einen MB Master 
zum Testen zur Verfügung. Ich habe allerdings noch nicht mit der Arduino 
Lib gespielt und keine Erfahrung damit gemacht.

Ein Bekannter von mir hat übrigens die Arduino Lib (Master/Slave) mit 
Erfolg eingesetzt. Scheint also zu funktionieren.

von STK500-Besitzer (Gast)


Lesenswert?

Matze schrieb:
> Es müsste also irgendwie noch sowas wie ein Timeout her.

Zu Beginn deiner Übertragung startest du einen Timer, der so lange 
läuft, bis die komplette Übertragung beendet sein sollte.
Wenn vorher Daten eintreffen, hälst du ihn an.
Feritg.

Das reine Request (single Coil oder Register) hat ja immer eine 
konstante Länge.
Interessant wird es erst, wenn du mehr übertragen willst.

C-hater hatte ja schon das Thema "Timing" angesprochen. Das solltest du 
einhalten (wobei es eigentlich nur um den Abstand zweischen zwei 
Requests geht bzw. die Antwort und das darauf folgenden).

Maschinenstürmer schrieb:
> Nachtrag. CANBus wäre für so ein Projekt auch keine schlechte Wahl.
> Auch RS485 ohne Modbus.

Ein fertiges BMS mit CAN-bus ausrüsten? Sind wir bei CANopen?

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.