STM32F3xx + FreeRTOS. Modbus RTU mit Hardware RS485 und CRC ohne Timer und Semaphoren

Hallo! Vor relativ kurzer Zeit, nach dem Abitur, trat ich in eine kleine Firma ein, die sich mit der Entwicklung von Elektronik befasste. Eine der ersten Aufgaben, mit denen ich konfrontiert war, war die Notwendigkeit, das Modbus RTU-Slave-Protokoll mit STM32 zu implementieren. Mit einer Sünde in der Hälfte schrieb ich es dann, aber ich fing an, dieses Protokoll von Projekt zu Projekt zu erfüllen, und ich entschied mich, lib mit FreeRTOS umzugestalten und zu optimieren.



Einführung



In aktuellen Projekten verwende ich häufig das STM32F3xx + FreeRTOS-Bundle. Daher habe ich beschlossen, die Hardwarefunktionen dieses Controllers optimal zu nutzen. Insbesondere:



  • Empfangen / Senden mit DMA
  • Möglichkeit der Hardware-CRC-Berechnung
  • RS485-Hardwareunterstützung
  • Ende der Paketerkennung über USART-Hardwarefunktionen ohne Verwendung eines Timers


Ich werde sofort eine Reservierung vornehmen. Hier beschreibe ich nicht die Spezifikation des Modbus-Protokolls und wie der Master damit arbeitet. Sie können hier und hier darüber lesen .



Konfigurationsdatei



Zunächst habe ich beschlossen, die Übertragung von Code zwischen Projekten zu vereinfachen, zumindest innerhalb derselben Controller-Familie. Deshalb habe ich beschlossen, eine kleine conf.h-Datei zu schreiben, mit der ich die Hauptteile der Implementierung schnell neu konfigurieren kann.



ModbusRTU_conf.h
#ifndef MODBUSRTU_CONF_H_INCLUDED
#define MODBUSRTU_CONF_H_INCLUDED
#include "stm32f30x.h"

extern uint32_t SystemCoreClock;

/*Registers number in Modbus RTU address space*/
#define MB_REGS_NUM             4096
/*Slave address*/
#define MB_SLAVE_ADDRESS        0x01

/*Hardware defines*/
#define MB_USART_BAUDRATE       115200
#define MB_USART_RCC_HZ         64000000

#define MB_USART                USART1
#define MB_USART_RCC            RCC->APB2ENR
#define MB_USART_RCC_BIT        RCC_APB2ENR_USART1EN
#define MB_USART_IRQn           USART1_IRQn
#define MB_USART_IRQ_HANDLER    USART1_IRQHandler

#define MB_USART_RX_RCC         RCC->AHBENR
#define MB_USART_RX_RCC_BIT     RCC_AHBENR_GPIOAEN
#define MB_USART_RX_PORT        GPIOA
#define MB_USART_RX_PIN         10
#define MB_USART_RX_ALT_NUM     7

#define MB_USART_TX_RCC         RCC->AHBENR
#define MB_USART_TX_RCC_BIT     RCC_AHBENR_GPIOAEN
#define MB_USART_TX_PORT        GPIOA
#define MB_USART_TX_PIN         9
#define MB_USART_TX_ALT_NUM     7

#define MB_DMA                  DMA1
#define MB_DMA_RCC              RCC->AHBENR
#define MB_DMA_RCC_BIT          RCC_AHBENR_DMA1EN

#define MB_DMA_RX_CH_NUM        5
#define MB_DMA_RX_CH            DMA1_Channel5
#define MB_DMA_RX_IRQn          DMA1_Channel5_IRQn
#define MB_DMA_RX_IRQ_HANDLER   DMA1_Channel5_IRQHandler

#define MB_DMA_TX_CH_NUM        4
#define MB_DMA_TX_CH            DMA1_Channel4
#define MB_DMA_TX_IRQn          DMA1_Channel4_IRQn
#define MB_DMA_TX_IRQ_HANDLER   DMA1_Channel4_IRQHandler

/*Hardware RS485 support
1 - enabled
other - disabled 
*/  
#define MB_RS485_SUPPORT        0
#if(MB_RS485_SUPPORT == 1)
#define MB_USART_DE_RCC         RCC->AHBENR
#define MB_USART_DE_RCC_BIT     RCC_AHBENR_GPIOAEN
#define MB_USART_DE_PORT        GPIOA
#define MB_USART_DE_PIN         12
#define MB_USART_DE_ALT_NUM     7
#endif

/*Hardware CRC enable
1 - enabled
other - disabled 
*/  
#define MB_HARDWARE_CRC     1

#endif /* MODBUSRTU_CONF_H_INCLUDED */




Meiner Meinung nach ändern sich am häufigsten folgende Dinge:



  • Geräteadresse und Größe des Adressraums
  • Taktfrequenz und Parameter der USART-Pins (Pin, Port, RCC, IRQ)
  • DMA-Kanalparameter (rcc, irq)
  • Aktivieren / Deaktivieren von Hardware CRC und RS485


Eisenkonfiguration



In dieser Implementierung verwende ich das übliche CMSIS, nicht aufgrund religiöser Überzeugungen, es ist nur einfacher für mich und weniger Abhängigkeiten. Ich werde die Konfiguration der Ports nicht beschreiben, Sie können sie unter dem Link zum Github sehen, der unten sein wird.



Beginnen wir mit der Einrichtung des USART:



USART konfigurieren
    /*Configure USART*/
    /*CR1:
    -Transmitter/Receiver enable;
    -Receive timeout interrupt enable*/
    MB_USART->CR1 = 0;
    MB_USART->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_RTOIE);
    /*CR2:
    -Receive timeout - enable
    */
    MB_USART->CR2 = 0;

    /*CR3:
    -DMA receive enable
    -DMA transmit enable
    */
    MB_USART->CR3 = 0;
    MB_USART->CR3 |= (USART_CR3_DMAR | USART_CR3_DMAT);

#if (MB_RS485_SUPPORT == 1)
    /*Cnfigure RS485*/
     MB_USART->CR1 |= USART_CR1_DEAT | USART_CR1_DEDT;
     MB_USART->CR3 |= USART_CR3_DEM;
#endif

     /*Set Receive timeout*/
     //If baudrate is grater than 19200 - timeout is 1.75 ms
    if(MB_USART_BAUDRATE >= 19200)
        MB_USART->RTOR = 0.00175 * MB_USART_BAUDRATE + 1;
    else
        MB_USART->RTOR = 35;
    /*Set USART baudrate*/
     /*Set USART baudrate*/
    uint16_t baudrate = MB_USART_RCC_HZ / MB_USART_BAUDRATE;
    MB_USART->BRR = baudrate;

    /*Enable interrupt vector for USART1*/
    NVIC_SetPriority(MB_USART_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY);
    NVIC_EnableIRQ(MB_USART_IRQn);

    /*Enable USART*/
    MB_USART->CR1 |= USART_CR1_UE;




Hier gibt es mehrere Punkte:



  1. F3, F0, , - . . , F1 , . USART_CR1_RTOIE R1. , USART , RM!
  2. RTOR. , 3.5 , 35 (1 — 8 + 1 + 1 ). 19200 / 1.75 , :
    MB_USART->RTOR = 0.00175 * MB_USART_BAUDRATE + 1;
  3. OC, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY , FreeRTOS FromISR , . FreeRTOS_Config.h,
  4. RS485 ist mit zwei Bitfeldern konfiguriert: USART_CR1_DEAT und USART_CR1_DEDT . Mit diesen Bitfeldern können Sie die Zeit zum Entfernen und Einstellen des DE-Signals vor und nach dem Senden von 1/16 oder 1/8 Bit einstellen, abhängig vom Oversampling-Parameter des USART-Moduls. Es bleibt nur, um die Funktion im CR3-Register mit dem USART_CR3_DEM- Bit zu aktivieren , die Hardware kümmert sich um den Rest.


DMA-Einstellung:



DMA-Setup
    /*Configure DMA Rx/Tx channels*/
    //Rx channel
    //Max priority
    //Memory increment
    //Transfer complete interrupt
    //Transfer error interrupt
    MB_DMA_RX_CH->CCR = 0;
    MB_DMA_RX_CH->CCR |= (DMA_CCR_PL | DMA_CCR_MINC | DMA_CCR_TCIE | DMA_CCR_TEIE);
    MB_DMA_RX_CH->CPAR = (uint32_t)&MB_USART->RDR;
    MB_DMA_RX_CH->CMAR = (uint32_t)MB_Frame;

    /*Set highest priority to Rx DMA*/
    NVIC_SetPriority(MB_DMA_RX_IRQn, 0);
    NVIC_EnableIRQ(MB_DMA_RX_IRQn);

    //Tx channel
    //Max priority
    //Memory increment
    //Transfer complete interrupt
    //Transfer error interrupt
    MB_DMA_TX_CH->CCR = 0;
    MB_DMA_TX_CH->CCR |= (DMA_CCR_PL | DMA_CCR_MINC | DMA_CCR_DIR | DMA_CCR_TCIE | DMA_CCR_TEIE);
    MB_DMA_TX_CH->CPAR = (uint32_t)&MB_USART->TDR;
    MB_DMA_TX_CH->CMAR = (uint32_t)MB_Frame;

     /*Set highest priority to Tx DMA*/
    NVIC_SetPriority(MB_DMA_TX_IRQn, 0);
    NVIC_EnableIRQ(MB_DMA_TX_IRQn);




Da Modbus in einem Anforderungs-Antwort-Modus arbeitet, verwenden wir einen Puffer sowohl für den Empfang als auch für das Senden. Im Puffer empfangen, dort verarbeitet und von dort gesendet. Während der Verarbeitung wird keine Eingabe akzeptiert. Der Rx-DMA-Kanal legt Daten aus dem USART-Empfangsregister (RDR) in den Puffer, der Tx-DMA-Kanal dagegen aus dem Puffer in das Senderegister (TDR). Wir müssen den Tx-Kanal unterbrechen, um festzustellen, dass die Antwort weg ist, und wir können in den Empfangsmodus wechseln.



Das Unterbrechen des Rx-Kanals ist im Wesentlichen nicht erforderlich, da wir davon ausgehen, dass das Modbus-Paket nicht mehr als 256 Byte umfassen kann. Was ist jedoch, wenn auf der Leitung Rauschen auftritt und jemand zufällig Bytes sendet? Zu diesem Zweck habe ich einen Puffer von 257 Bytes erstellt. Wenn ein Rx-DMA-Interrupt auftritt, bedeutet dies, dass jemand die Leitung "verschmutzt". Wir werfen den Rx-Kanal an den Anfang des Puffers und hören erneut zu.



Interrupt-Handler:



Handler unterbrechen
/*DMA Rx interrupt handler*/
void MB_DMA_RX_IRQ_HANDLER(void)
{
    if(MB_DMA->ISR & (DMA_ISR_TCIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2)))
        MB_DMA->IFCR |= (DMA_IFCR_CTCIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2));
    if(MB_DMA->ISR & (DMA_ISR_TEIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2)))
        MB_DMA->IFCR |= (DMA_IFCR_CTEIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2));
    /*If error happened on transfer or MB_MAX_FRAME_SIZE bytes received - start listening*/
    MB_RecieveFrame();
}

/*DMA Tx interrupt handler*/
void MB_DMA_TX_IRQ_HANDLER(void)
{
    MB_DMA_TX_CH->CCR &= ~(DMA_CCR_EN);
    if(MB_DMA->ISR & (DMA_ISR_TCIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2)))
        MB_DMA->IFCR |= (DMA_IFCR_CTCIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2));
    if(MB_DMA->ISR & (DMA_ISR_TEIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2)))
        MB_DMA->IFCR |= (DMA_IFCR_CTEIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2));
    /*If error happened on transfer or transfer completed - start listening*/
    MB_RecieveFrame();
}

/*USART interrupt handler*/
void MB_USART_IRQ_HANDLER(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    if(MB_USART->ISR & USART_ISR_RTOF)
    {
        MB_USART->ICR = 0xFFFFFFFF;
        //MB_USART->ICR |= USART_ICR_RTOCF;
        MB_USART->CR2 &= ~(USART_CR2_RTOEN);
        /*Stop DMA Rx channel and get received bytes num*/
        MB_FrameLen = MB_MAX_FRAME_SIZE - MB_DMA_RX_CH->CNDTR;
        MB_DMA_RX_CH->CCR &= ~DMA_CCR_EN;
        /*Send notification to Modbus Handler task*/
        vTaskNotifyGiveFromISR(MB_TaskHandle, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}




DMA-Handler sind ganz einfach: Alles gesendet - Flags bereinigen, in den Empfangsmodus wechseln, 257 Bytes empfangen - Rahmenfehler, Feuchtigkeit reinigen, wieder in den Empfangsmodus wechseln.



Der USART-Prozessor teilt uns mit, dass eine bestimmte Datenmenge eingegangen ist und dann Stille herrschte. Der Frame ist fertig, wir bestimmen die Anzahl der empfangenen Bytes (die maximale Anzahl der DMA-Empfangsbytes - die Menge, die noch empfangen werden muss), schalten den Empfang aus und aktivieren die Aufgabe.



Eine Einschränkung: Ich habe ein binäres Semaphor verwendet, um die Aufgabe zu aktivieren , aber die FreeRTOS-Entwickler empfehlen die Verwendung von TaskNotification :

Das Entsperren einer RTOS-Aufgabe mit einer direkten Benachrichtigung ist 45% schneller und benötigt weniger RAM als das Entsperren einer Aufgabe mit einem binären Semaphor

Manchmal ist die Funktion xTaskGetCurrentTaskHandle () nicht in der Assembly in FreeRTOS_Config.h enthalten . In diesem Fall müssen Sie dieser Datei eine Zeile hinzufügen:



#define INCLUDE_xTaskGetCurrentTaskHandle 1


Ohne Verwendung eines Semaphors hat die Firmware fast 1 kB verloren. Eine Kleinigkeit natürlich, aber nett.



Sende- und Empfangsfunktionen:



Senden und empfangen
/*Configure DMA to receive mode*/

void MB_RecieveFrame(void)
{
    MB_FrameLen = 0;
    //Clear timeout Flag*/
    MB_USART->CR2 |= USART_CR2_RTOEN;
    /*Disable Tx DMA channel*/
    MB_DMA_RX_CH->CCR &= ~DMA_CCR_EN;
    /*Set receive bytes num to 257*/
    MB_DMA_RX_CH->CNDTR = MB_MAX_FRAME_SIZE;
    /*Enable Rx DMA channel*/
    MB_DMA_RX_CH->CCR |= DMA_CCR_EN;
}

/*Configure DMA in tx mode*/
void MB_SendFrame(uint32_t len)
{
    /*Set number of bytes to transmit*/
    MB_DMA_TX_CH->CNDTR = len;
    /*Enable Tx DMA channel*/
    MB_DMA_TX_CH->CCR |= DMA_CCR_EN;
}


Beide Funktionen initialisieren DMA-Kanäle neu. Beim Empfang wird die Funktion, die das Zeitlimit im CR2- Register verfolgt, durch das USART_CR2_RTOEN- Bit aktiviert .



CRC



Fahren wir mit der Hardcore-CRC-Berechnung fort. Diese Funktion des Augenreglers hat mich immer geärgert, aber irgendwie hat es nie geklappt, in einigen Serien war es unmöglich, ein beliebiges Polynom zu setzen, in einigen Serien war es unmöglich, die Dimension des Polynoms zu ändern und so weiter. In F3 ist alles in Ordnung, und setzen Sie das Polynom und ändern Sie die Größe, aber ich musste eine Hocke machen:



uint16_t MB_GetCRC(uint8_t * buffer, uint32_t len)
{
    MB_CRC_Init();
    for(uint32_t i = 0; i < len; i++)
        *((__IO uint8_t *)&CRC->DR) = buffer[i];
    return CRC->DR;
}


Es stellte sich heraus, dass es unmöglich ist, Byte für Byte in das DR- Register zu werfen - es ist falsch zu lesen, Sie müssen Bytezugriff verwenden. Ich habe solche "Freaks" in STM bereits mit dem SPI-Modul getroffen, in das ich Byte für Byte schreiben möchte.



Aufgabe



void MB_RTU_Slave_Task(void *pvParameters)
{
    MB_TaskHandle = xTaskGetCurrentTaskHandle();
    MB_HWInit();
    while(1)
    {
        if(ulTaskNotifyTake(pdTRUE, portMAX_DELAY))
        {
            uint32_t txLen = MB_TransactionHandler(MB_GetFrame(), MB_GetFrameLen());
            if(txLen)
                MB_SendFrame(txLen);
            else
                MB_RecieveFrame();
        }
    }
}


Darin initialisieren wir den Zeiger auf die Aufgabe. Dies ist erforderlich, um ihn zum Entsperren über TaskNotification zu verwenden, die Hardware zu initialisieren und zu warten, bis wir schlafen, bis die Benachrichtigung eintrifft. Bei Bedarf können Sie anstelle von portMAX_DELAY einen Timeout-Wert eingeben , um festzustellen, ob für eine bestimmte Zeit keine Verbindung besteht. Wenn die Benachrichtigung eingetroffen ist, bearbeiten wir das Paket, bilden eine Antwort und senden es. Wenn der Rahmen jedoch defekt oder an der falschen Adresse eingetroffen ist, warten wir nur auf die nächste.



/*Handle Received frame*/
static uint32_t MB_TransactionHandler(uint8_t * frame, uint32_t len)
{
    uint32_t txLen = 0;
    /*Check frame length*/
    if(len < MB_MIN_FRAME_LEN)
        return txLen;
    /*Check frame address*/
    if(!MB_CheckAddress(frame[0]))
        return txLen;
    /*Check frame CRC*/
    if(!MB_CheckCRC(*((uint16_t*)&frame[len - 2]), MB_GetCRC(frame, len - 2)))
        return txLen;
    switch(frame[1])
    {
        case MB_CMD_READ_REGS : txLen = MB_ReadRegsHandler(frame, len); break;
        case MB_CMD_WRITE_REG : txLen = MB_WriteRegHandler(frame, len); break;
        case MB_CMD_WRITE_REGS : txLen = MB_WriteRegsHandler(frame, len); break;
        default : txLen = MB_ErrorHandler(frame, len, MB_ERROR_COMMAND); break;
    }
    return txLen;
}


Der Handler selbst ist nicht von besonderem Interesse: Er überprüft die Länge des Frames / der Adresse / des CRC und generiert eine Antwort oder einen Fehler. Diese Implementierung unterstützt drei Hauptfunktionen: 0x03 - Leseregister, 0x06 - Schreibregister, 0x10 - Mehrere Register schreiben. Normalerweise reichen diese Funktionen für mich aus, aber wenn Sie möchten, können Sie die Funktionalität problemlos erweitern.



Nun, fang an:



int main(void)
{
    NVIC_SetPriorityGrouping(3);
    xTaskCreate(MB_RTU_Slave_Task, "MB", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
    vTaskStartScheduler();
}


Damit die Aufgabe funktioniert, reicht ein Stapel mit einer Größe von 32 x uint32_t (oder 128 Byte) aus . Dies ist die Größe, die ich in der Definition configMINIMAL_STACK_SIZE festgelegt habe . Als Referenz: Anfangs habe ich fälschlicherweise angenommen, dass configMINIMAL_STACK_SIZE in Bytes festgelegt ist. Wenn ich jedoch nicht genug hinzugefügt habe, musste ich bei der Arbeit mit F0-Controllern mit weniger RAM den Stapel einmal zählen, und es stellte sich heraus, dass configMINIMAL_STACK_SIZE in Dimensionen des Typs portSTACK_TYPE festgelegt wurde , der in definiert ist Datei portmacro.h

#define portSTACK_TYPE    uint32_t


Fazit



Diese Modbus RTU-Implementierung nutzt die Hardwarefunktionen des STM32F3xx-Mikrocontrollers optimal aus.



Das Gewicht der Ausgabe-Firmware zusammen mit dem Betriebssystem und der Optimierung -o2 betrug: Programmgröße: 5492 Bytes, Datengröße: 112 Bytes. Vor dem Hintergrund von 6 KB ist der Verlust von 1 KB durch Semaphoren von Bedeutung.



Die Portabilität auf andere Familien ist möglich, z. B. unterstützt F0 Timeout und RS485, es gibt jedoch ein Problem mit der Hardware-CRC, sodass Sie mit der Software-Berechnungsmethode auskommen können. Es kann auch Unterschiede in den DMA-Interrupt-Handlern geben, wo sie kombiniert werden.



Link zu Github



Vielleicht ist es für jemanden nützlich.



Nützliche Links:






All Articles