Projektbeispiel Vermessungsgerät Teil 5

Teil 1 widmete sich der Aufsetzung der Infrastruktur für dieses Entwicklungsprojekt.
Teil 2 widmete sich den Grundlagen der Drehgebereinbindung.
Teil 3 beleuchtete die Hardwareanbindung des BiSS / ACURO Busses an SoCs.
Teil 4 endete mit dem darauf laufenden Bus-Protokoll konkret für den ausgesuchten Drehgeber.

Dieser Teil widmet sich nun der unteren Softwareschichten, der Anbindung von BiSS via SPI und die Extraktion der Messwerte aus den Paketen samt CRC.

Definition der Zeitgruppen T1 bis T5

An dieser Stelle eine Kurzzusammenfassung der Zeiten aus Teil 4:

T1 enthält zwei fallende Flanken, während SL high ist, also 0b11.

T2 enthält mindestens eine fallende Flanke. Dazu kommt die Zeit \(busy\). Diese beträgt maximal 5,2µs. Damit ergibt sich für T2 eine Anzahl zwischen 1 und \(SPI_{Frequenz} \cdot busy + 1\) Bits. Das entspricht bei 100kHz einem, bei 400kHz drei, bei 1MHz 6 und bei 8MHz 42 Bits.

T3 besteht aus einem Bit mit Wert 1.

T4 enthält bei dem gewählten Drehgeber 42 Bits, welche Daten-, Füll- Warn- und CRC-Bits enthalten.

T5 ist direkt abhängig von \(timeout_{SENS}\). Diese liegt als default zwischen 9,9 und 14,9µs. Bei 1MHz sind das zwischen 10 und 15 Bits.

Im genannten Anwendungsfall genügte ein Takt von 1MHz. Und dies führte zu einer willkommenen Konsequenz: die Gruppen T1 bis T4 passen in jedem Fall in 8 Byte und damit in einen 64-Bit-Integer. Dieser Umstand macht die Extraktion auf den wegen Batterieversorgung leistungschwachen SoCs sehr effizient, per Bitshiftung.

  uint8_t buffer[8];
  ret = spi_read_packet(buffer, 64);
  if (ret != RET_OK)
    return ret;

  // Wir müssen die Byte Order umkehren, da der SoC Little Endian ist, Acuro Big Endian. Das ganze machen wir LINKSBÜNDIG in einen UInt64
  uint64_t bit_vector = 0;
  uint8_t* packet = (uint8_t*)&bit_vector;
  for (int pos = 0; pos < 8; pos++)
    packet[7-pos] = buffer[pos];

  // so, nun müssen wir die "Startsequenz" des Acuro-Pakets wegschneiden. Zunächst zwei führende 1 nach Spezifikation
  bool isValid = false;
  size_t position = 0;
  for (int pos = 0; pos < 4; pos++)
  {
    if (!(bit_vector & highest_bit))
    {
      isValid = true;
      break;
    }

    bit_vector <<= 1;
    position++;
  }
  
  if (!isValid)
    return ERR_ACURO_PROT_1;

  // nun muss der Sensor mit low quitiert haben. Variabel ist aber seine busy Zeit, in der er low bleibt. Wir schneiden nun führende 0 weg, auch deren Anzahl ist durch max busy time begrenzt, was wir natürlich testen.
  isValid = false;
  for (int pos = 0; pos < 6; pos++)
  {
    if (bit_vector & highest_bit)
    {
      isValid = true;
      break;
    }

    bit_vector <<= 1;
    position++;
  }
  
  if (!isValid)
    return ERR_ACURO_PROT_2;

  // Jetzt kommt noch das Startbit des Acuro-Pakets weg.
  bit_vector <<= 1;
  position++;

  // Jipi, der Rest ist nun unser Acuro Paket, linksbündig ausgerichtet. ACHTUNG: der Winkelwert ist natürlich ... Little Endian!
  // Wo wir das Paket so schön linksbündig haben, ist es ideal ausgerichtet für unseren CRC check. Daher machen wir uns mal hier eine Kopie für später.
  uint64_t value_for_crc = bit_vector;

  // Nun wollen wir den Inhalt "per Maske" auslesen. Daher machen wir das Paket jetzt rechtsbündig.	
  bit_vector >>= RIGHT_SHIFT;
  
   // Als Maske nehmen wir der Einfach halber einen struct. Kann man auch zu Fuß machen, muss man aber nicht.	
  const acuro_packet* a_packet = (const acuro_packet*)&bit_vector;
   
  // So, nun machen wir auch unseren CRC Test
  uint8_t crc = compute_crc(value_for_crc);

Wie zu sehen, wurde in diesem Fall zugunsten einfacheren Codes direkt auf dem 64-Bit Integer geshiftet. Leistungsschwache SoCs für Batterieanwendungen haben oft 32-Bit Register. Während Shift auf 32 Bit-Registern eine Instruktion benötigt, kann je nach Core die Umsetzung eines 64-Bit Shifts auf diese 3 bis 6 Instruktionen benötigen.

Die gleiche Frage betrifft den Einsatz mehrerer Drehgeber. Ideal im Sinne der Messung ist eine gleichzeitige Abfrage der Drehgeber mit einem identischen Taktsignal, wie explizit vom „Drehgeberpapst“ Josef Siraky vorgedacht (siehe Teil 2).

Simultane Übertragung von Absolutgebern, Figur 4 aus Patent EP0171579

Praktisch werden Dual- / Quad- / Octal-SPI jedoch oft von den üblichen RTOS auf oberer Treiberebene nur für Speicheranbindungen angeboten, etwa bei nRF via Zephyr. Positiv: RTOS wie Zephyr oder FreeRTOS sind FOSS, und daher kann man hier die unteren Treiberschichten entsprechend selbst ergänzen. Negativ: man ist hier so tief „im Maschinenraum“ in der Ebene von DMA und Interruptbehandlung, das stabile Entwicklungsarbeit nur bei großen Abnahmemengen oder speziellen Sicherheitsklassen wirtschaftlich vertretbar ist. Und jede Lösung ist auf dieser tiefen Stufe direkt hardwareabhängig.

Ein wesentlicher Punkt ist noch CRC. Ein Sprichwort lautet, wer billig kauft, kauft zweimal. CRC hier als obligatorisch anzusehen, bedeutet in der Realität, Bitkipper und damit Ausreißer „nach oben“ abzugeben. Und dort wird eine Erkennung und Ausfilterung immer teurer bis unmöglich. Solche Dinge auszulassen ist tatsächlich „Pfusch am Bau“.

Die CRC Umsetzung ist ein gutes Beispiel für die Entwicklungen der Sprachen C und C++ in den letzten Jahren. Gerade Veränderungen ab C++ 17 machen sie für den embedded Einsatz immer attraktiver. In diesem Fall ist das Stichwort constexpr.

#include <stdio.h>

/**
  Ein Generatorpolynom als Bitmaske direkt für die Verarbeitung im 64 Bit Register.
  
  \author    Dr. Torsten Thurow
*/
typedef struct
{
  uint64_t mask;                                ///< Die Bitmaske selbst, das MSB mit Wert 1 ist dabei linksbündig ausgerichtet (Bit 63 bei Zählweise startend mit 0)
  size_t length;                                ///< Die Anzahl der Bits des Polynoms, entspricht dem Grad des Polynoms + 1
} PolynomMask;

const static uint64_t testMask = ((uint64_t)1 << 63); ///< testMask dient der einfachen Prüfung, ob ein 64 Bit Wert linksbündig eine 1 besitzt (Bit 63 bei Zählweise startend mit 0)

/**
  Erstellt aus einem Generatorpolynom eine linksseitig orientierte Bitmaske.
  
  \author    Dr. Torsten Thurow
  
  \param    polynom        Das Generatorpolynom als Wert.
  
  \returns  Eine linksseitig orientierte Bitmaske generiert aus dem Generatorpolynom.
*/
constexpr PolynomMask CreatePolynomMask(uint64_t polynom)
{
  PolynomMask polynomMask = { polynom, 64 };    // zunächst wird die Maske mit dem Polynomwert und dem Längenwert, der noch bestimmt wird, mit maximal 64 Bit, initialisiert
  while (!(polynomMask.mask & testMask))        // solange das höchste Bit 63 (bei Zählweise startend mit 0) 0 ist
  {
    polynomMask.mask <<= 1;                     // schiebe die Bitsequenz eine Stelle weiter nach link
    polynomMask.length--;                       // das bedeutet aber auch, der Längenwert muss wieder ein Bit geringer sein, denn das Polynom startet prinzipbedingt immer mit 1
  }
  return polynomMask;                           // die Maske ist fertig
}

#ifdef CRC_OUTPUT
/**
  Gibt die einzelnen Bits einer Bitsequenz menschenlesbar aus.
  
  \author    Dr. Torsten Thurow
  
  \param    value         Die Bitsequenz als 64 Bit Integer.
  \param    offset        Nummer des ersten Bits, welches ausgegeben wird, in der Orientierung vom höchstwertigen Bit zum niederwertigsten Bit.
  \param    length        Anzahl der Bits, welche ausgegeben werden.
*/
void PrintBits(uint64_t value, size_t offset, size_t length)
{
  if (!length)
    return;

  value <<= offset;
  while (true)
  {
    std::cout << (value & testMask ? '1' : '0');
    value <<= 1;
    length--;
    if (!length)
      break;
    std::cout << ' ';
  }
  std::cout << std::endl << std::flush;
}
#endif // CRC_OUTPUT

/**
  Der eigentliche CRC-Algorithmus für die Berechnung des CRC Wertes wie auch dessen Kontrolle
  
  \author    Dr. Torsten Thurow
  
  \param    value          Die Bitsequenz als 64 Bit Integer.
  \param    length         Die Länge der Bitsequenz als Anzahl Bits.
  \param    polynomMask    Das Generatorpolynom als Bitmaske.
  
  \returns  An uint64_t.
*/
static uint64_t Calculate(uint64_t value, size_t length, PolynomMask polynomMask)
{
  // Zuerst wird der Rahmen links ausgerichtet
  value <<= 64 - length;
  while (true)
  {
#ifdef CRC_OUTPUT
    // Die Bitsequenz wird ausgegeben
    PrintBits(value, 0, length + polynomMask.length - 1);
#endif //  CRC_OUTPUT
    
    // Nun werden alle führenden Nullen entfernt
    while (!(value & testMask)) // solange das höchste Bit 63 (bei Zählweise startend mit 0) 0 ist
    {
      value <<= 1;                              // schiebe die Bitsequenz eine Stelle weiter nach link
      length--;                                 // von der Ursprungssequenz wird nach dem Shift nach Links ein Bit weniger vorgehalten
      if (!length)                              // wenn die gesamte Ursprungssequenz abgearbeitet wurde ...
        return value;                           // ... verbleibt nur noch der CRC Wert linksbündig
    }

#ifdef CRC_OUTPUT
    PrintBits(polynomMask.mask, 0, polynomMask.length);
#endif //  CRC_OUTPUT
    
    value ^= polynomMask.mask;                  // hier erfolgt die XOR-Operation zwischen (Rest)Wert und Generatorpolynom
  }
}