Motivation

Für ein Projekt musste eine schnelle serielle, kabelgebundene Datenübertragung (2 Mbit/s oder besser) zwischen einem μC und einem PC erfolgen. Am wenigsten Aufwand hat man, wenn man die RS232-Schnittstelle verwendet. Es gibt eine Reihe von RS232-to-USB-Konverter, die diese Geschwindigkeit ermöglichen (z.B. FTD232 oder PL2303).

Kritisch ist jedoch die Beschickung der Schnittstelle auf der μC-Seite. Üblich ist die Implementierung über einen Ring-Puffer, der vom Programm beschickt, von der USART-Firmware per Interrupt ausgelesen und byte-weise versendet wird. Solch eine Implementierung besitzt eine großen Overhead und schafft die geforderte Rate bei weitem nicht.

Die STM32F1-Prozessoren (hier Bluepill, STM32F103C8T6 ) besitzen ein komfortables DMA-System, mit dem es möglich ist, dass große Datenmengen nach einem Anstoß ohne weitere Aktionen der CPU von der Hardware versandt werden. Die CPU steht in dieser Zeit anderen Aufgaben zur Verfügung.

In­halts­ver­zeich­nis

Klasse UrsUsart1DmaClass

Verwendung

Beispiel

Methodenübersicht

Implementierung

Download


Versionshistorie

Version Anpassungen
1.0 (2021-10-18) Basis-Version
1.1 (2021-10-22) Für die Initialisierung der GPIOs wird die libmaple-Funktion gpio_set_mode benutzt.

Hinweise:

     Zur Installation des Arduino-Bootloaders auf einem STM32 siehe Arduino-FAQ.

     Die Bibliothek setzt auf der STM32duino-Bibliothek auf.

Klasse UrsUsart1DmaClass

Beim STM32 können die DMA-Kanäle für die Kontrolle der Peripherie nicht beliebig gewählt werden. Einzelne Peripherieeinheiten sind dedizierten DMA-Kanälen zugeordnet.

Es ging konkret darum, die USART1 des Chips mit hoher Geschwindigkeit zu betreiben. Deshalb wurde die Klasse auf Geschwindigkeit optimiert und nicht auf generelle Verwendung. Sie ist nicht flexibel für alle möglichen USARTs, sondern speziell für die USART1 ausgelegt.

Da es nur eine USART1 gibt, wurde die Klasse nach dem Singleton-Pattern konzipiert und die Methoden wegen des Geschwindigkeitsvorteils als statische Methoden implementiert. Die Portierung auf andere USARTs ist nicht schwierig.

Auch wegen des Geschwindigkeitsvorteils wurde die Methode zum Datenversand (xmit) als inline bereits in der Header-Datei definiert.

Verwendung

Über die vordefinierte Instanz ursUsart1Dma der Klasse kann, analog zu den Arduino Serialx-Einheiten, direkt mit der Schnittstelle gearbeitet werden. Eine weitere Instanziierung ist nicht notwendig.

Die Methode begin initialisiert die Schnittstelle, xmit versendet ein Datenpaket und über isBusy kann abgefragt werden, ob eine Übertragung abgeschlossen ist. Die Daten werden im Standardformat 8 Daten-Bit, kein Parity-Bit, 1 Stopp-Bit (8N1) übertragen. Das Empfangen von Daten ist nicht implementiert.

Arduino veröffentlicht die USART1 über die Variable Serial1 (Klasse HardwareSerial). Man kann beide Klassen, ursUsart1DmaClass und HardwareSerial, wechselseitig betreiben, indem man die Methode begin für die entsprechende Klasse aufruft. Im Beispiel wird die USART1 über ein USB-To-RS232-Dongle mit dem PC verbunden. Über die RTS-Leitung wird zwischen den beiden Schnittstellen-Klassen hin und her gewechselt. Somit kann man über die gleiche Schnittstelle sowohl z.B. Konfigurationsanweisungen (per Serial1) austauschen, als auch schnellen Datentransport ermöglichen (über ursUsart1Dma).

Methodenübersicht

Methode Funktion Anmerkung
void begin(uint32_t baud) Initialisiert die Schnittstelle mit der angegebenen Baud-Rate. Maximal 4,5 MBaud.
void xmit(const uint8_t* source, uint16_t size) Versendet size Bytes aus der Quelle source.  
bool isBusy() Gibt an, ob eine lfd. Übertragung noch nicht abgeschlossen ist. true, solange der Sendevorgang läuft.

Beispiel

Das Beispiel implementiert zwei Betriebsmodi:

  1. Das schnelle Versenden von Datenpaketen per DMA
  2. Der Austausch von Konfigurationsdaten über "normale" serielle Kommunikation

Der Betriebsmodus wird über einen Pins festgelegt, der mit dem RTS-Signal eines USB-To-RS232-Dongles verbunden ist.

Hardware

Ein Bluepill wird mit einem USB-To-RS232-Dongle (z.B. FTD232) verbunden:

GND ⇔ GND
TX  ⇔ A10 (RX der USART1)
RX  ⇔ A9   (TX der USART1)
RTS ⇔ A8  (Anforderung des Betriebsmodus)
CTS ⇔ B15 (Rückmeldung des Betriebsmodus)

Über die Pins A9 (PA9 im Code) und A10 erfolgt die Datenübertragung. Über Pin A8 kann der Betriebsmodus eingestellt werden. Die Umschaltung des Modus kann erst dann geschehen, wenn die aktuellen Übertragungen abgeschlossen sind. Nach dem Umschaltung wird der Pin B15 (Rückmeldung des aktuellen Betriebsmodus) entsprechend eingestellt.

Der Bluepill ist außerdem mit der USB-USART zum Programmieren und zur Protokollierung angeschlossen.

Der Beispiel-Code ist ausführlich dokumentiert. Der funktionale Kern besteht nur aus wenigen Anweisungen. Für den DMA-Modus:

const char pangram1[] = "The quick brown fox jumps over the lazy dog\n";
const char pangram2[] = "Jeder wackere Bayer vertilgt bequem zwo Pfund Kalbshaxen\n";

ursUsart1Dma.xmit((const uint8_t*)pangram1, sizeof(pangram1) - 1);
ursUsart1Dma.xmit((const uint8_t*)pangram2, sizeof(pangram2) - 1);

Und für den seriellen Modus:

if (Serial1.available())                // Nicht blockieren!
   Serial1.write((char)Serial1.read()); // Echo

Der Rest ist im wesentlichen für den Wechsel des Betriebsmodus zuständig.

Implementierung

Das Referenz-Handbuch zu den STM32F103-Prozessoren findet man bei ST: RM0008

Die Einstellung der DMA, der USART1 und die Kopplung der beiden Komponenten geschieht durch direktes Beschreiben der entsprechenden Register.

Zunächst müssen Pointer auf die zugehörigen Registerstrukturen angelegt werden:

dma_tube_reg_map* UrsUsart1DmaClass::channel = DMA1CH4_BASE;   // Zeiger auf DMA1, Channel 4 Register Struktur
usart_dev* usart = USART1;  // Zeiger auf USART1 Register Struktur

Die Datentypen sind in der Bibliothek libmaple hinterlegt.

initDMA

Diese Methode konfiguriert Channel4 im DMA-Modul DMA1. Dieser DMA-Kanal ist für die Datenausgabe (TX) der USART1 vorgesehen.

void UrsUsart1DmaClass::initDMA() {
   RCC_BASE->AHBENR |= 1 << 0; // Enable DMA1 Clock

   channel->CCR |= 1 << 4;     // Set the Data Direction: Read from memory
   channel->CCR &= ~(1 << 5);  // Circular mode disabled
   channel->CCR |= 1 << 7;     // Enable the Memory Increment (MINC)
   channel->CCR &= ~(3 << 8);  // Set the Peripheral data size(PSIZE)
   channel->CCR &= ~(3 << 10); // Set the Memory data size (MSIZE) 00 : 8 Bit Data
   channel->CCR &= ~(3 << 12); // Set the Priority Level, PL = 0
   channel->CCR |= 1 << 1;     // Transfer complete interrupt enable
   channel->CPAR = (uint32_t)&usart->regs->DR; //  Set the  peripheral address in PAR Register

   dma_attach_interrupt(DMA1, DMA_CH4, DMA1_Channel4_IRQHandler);
}

Von den möglichen Interrupts wird nur der TCI (Transfer Complete Interrupt) freigegeben. Der Interrupt-Handler setzt die DMA-Flags zurück und setzt busy auf false. Damit ist die Schnittstelle bereit für den nächsten Übertagungsvorgang.

void UrsUsart1DmaClass::DMA1_Channel4_IRQHandler(void) {
   // Aufräumen:
   DMA1_BASE->IFCR |= 1 << 12; // Channel 4 global interrupt clear
   channel->CCR &= ~(1 << 0);  // Disable The DMA To Work

   busy = false; // Übertragung beendet
}

initUSART

Neben der USART müssen auch noch die zugehörigen GPIOs initialisiert werden. Da die Schnittstelle nur versendet, wird nur A9 (PA9) konfiguriert.

void UrsUsart1DmaClass::initUSART1(uint32 baud) {
   RCC_BASE->APB2ENR |= 1 << 2;   // Enable PORTA  Clock
   RCC_BASE->APB2ENR |= 1 << 14;  // Enable USART1 Clock

   GPIOA->regs->CRH |= 1 << 4; GPIOA->regs->CRH &= ~(1 << 5); // PIN9(TX) Output With 10MHZ Speed
   GPIOA->regs->CRH &= ~(1 << 6); GPIOA->regs->CRH |= 1 << 7; // Make Pin9 Alternative Func PushPull

/*
Die folgenden drei Anweisungen reichen aus, um die USART nach einem Reset zu initialisieren.
Es ist jedoch ungewiss, welche Werte die Arduino-Bibliothek nach der Verwendung von Serial1 einsetzt.

   usart->regs->CR3 |= 1 << 7; // Enable Usart To Work With DMA As Transmitter (DMAT bit)
   usart->regs->CR1 |= 1 << 3; // Enable The Transmitter (TE Bit) and Enable The USART (UE bit)
   usart->regs->CR1 |= 1 << 13;

Sämtliche Register-Werte wurden deshalb einmal nach der korrekten Initialisierung ausgelesen und 
werden nun als komplette Registerinhalte übergeben. Lediglich die Baudrate wird flexibel eingestellt.

USART Register nach der Initialisierung:
CR1:  0x00002008
CR2:  0x00000000
CR3:  0x00000080
GTPR: 0x00000000
*/

   usart->regs->CR1 = 0x00002008;
   usart->regs->CR2 = 0x00000000;
   usart->regs->CR3 = 0x00000080;
   usart->regs->GTPR = 0x00000000;
   usart->regs->SR = 0x00000000;

   // Using The Deafult M Size = 8 bit And 1 Stop bit
   usart_set_baud_rate(usart, USART_USE_PCLK, baud); // Baudrate setzen
}

Eigentlich würde es ausreichen die Bits DMAT, TE und UE zu setzen. Wenn man jedoch im Wechsel mit Serial1 der Arduino-Implementierung arbeitet, ist nicht ganz klar, wie dort die Register bestückt werden. Es wurden deshalb sämtliche relevanten Registerwerte nach einer funktionierenden Initialisierung direkt nach einem Reset ausgelesen. Diese ausgelesenen Werte werden nun zur Initialisierung der USART in deren Register geschrieben. So ist sicher gestellt, dass alle Bits den korrekten Wert besitzen.

Lediglich die Baudrate (BRR-Register) wird flexibel über libmaple-Funktion usart_set_baud_rate eingestellt.

xmit

// Versendet size Bytes aus der Quelle source
inline static void xmit(const uint8_t* source, uint16_t size) {
   while (isBusy()); // Auf Beendigung einer laufenden Übertragung warten.
   busy = true;

   channel->CNDTR = size; // Set the data size in CNDTR Register
   channel->CMAR = (uint32_t)source; // Set the  Memory address in MAR Register
   channel->CCR |= 1 << 0; // Enable The DMA To Work
}

xmit ist als inline-Methode in der Klassendefinition implementiert. Da es um Geschwindigkeit geht, spart man so den Overhead eines Funktionsaufrufs.

Download

Das ZIP-Archiv für Bibliothek UrsUsart1Dma zum Download. Die entpackten Dateien ins Verzeichnis <user>\Documents\Arduino\libraries kopieren (siehe Installing Additional Arduino Libraries).

Das Archiv enthält die Bibliotheksdateien