STM32F3xx + FreeRTOS. Modbus RTU avec matériel RS485 et CRC sans minuteries ni sémaphores

salut! Relativement récemment, après avoir obtenu mon diplôme universitaire, je suis entré dans une petite entreprise qui se consacrait au développement de l'électronique. L'un des premiers problèmes que j'ai rencontrés a été la nécessité d'implémenter le protocole Modbus RTU Slave en utilisant STM32. Avec un péché de moitié, je l'ai écrit alors, mais j'ai commencé à rencontrer ce protocole de projet en projet et j'ai décidé de refactoriser et d'optimiser la lib en utilisant FreeRTOS.



introduction



Dans les projets en cours, j'utilise souvent le bundle STM32F3xx + FreeRTOS, j'ai donc décidé de tirer le meilleur parti des capacités matérielles de ce contrôleur. En particulier:



  • RĂ©ception / envoi via DMA
  • PossibilitĂ© de calcul CRC matĂ©riel
  • Prise en charge matĂ©rielle RS485
  • DĂ©tection de fin de colis via les capacitĂ©s matĂ©rielles USART, sans utiliser de minuterie


Je vais faire une réservation tout de suite, ici je ne décris pas la spécification du protocole Modbus et comment le maître fonctionne avec, vous pouvez lire à ce sujet ici et ici .



fichier de configuration



Pour commencer, j'ai décidé de simplifier la tâche de transfert de code entre projets, au moins au sein d'une même famille de contrôleurs. J'ai donc décidé d'écrire un petit fichier conf.h qui me permettrait de reconfigurer rapidement les principales parties de l'implémentation.



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 */




Le plus souvent, Ă  mon avis, les choses suivantes changent:



  • Adresse de l'appareil et taille de l'espace d'adressage
  • FrĂ©quence d'horloge et paramètres des broches USART (pin, port, rcc, irq)
  • Paramètres de canal DMA (rcc, irq)
  • Activer / dĂ©sactiver le matĂ©riel CRC et RS485


Configuration du fer



Dans cette implémentation, j'utilise le CMSIS habituel, pas à cause de croyances religieuses, c'est juste plus facile pour moi et moins de dépendances. Je ne décrirai pas les paramètres du port, vous pouvez le voir sur le lien vers le github qui sera ci-dessous.



Commençons par configurer l'USART:



Configurer USART
    /*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;




Il y a plusieurs points ici:



  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 est configuré avec deux champs de bits : USART_CR1_DEAT et USART_CR1_DEDT . Ces champs de bits vous permettent de régler l'heure de suppression et de réglage du signal DE avant et après l'envoi en 1/16 ou 1/8 bits, en fonction du paramètre de suréchantillonnage du module USART. Il ne reste plus qu'à activer la fonction dans le registre CR3 avec le bit USART_CR3_DEM , le matériel se chargera du reste.


RĂ©glage DMA:



Configuration DMA
    /*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);




Puisque Modbus fonctionne en mode demande-réponse, nous utilisons un tampon pour la réception et la transmission. Reçu dans le tampon, traité là-bas et envoyé à partir de celui-ci. Aucune entrée n'est acceptée pendant le traitement. Le canal Rx DMA place les données du registre de réception USART (RDR) dans le tampon, le canal Tx DMA, au contraire, du tampon dans le registre d'émission (TDR). Nous devons interrompre le canal Tx pour déterminer que la réponse a disparu et nous pouvons passer en mode réception.



L'interruption du canal Rx est essentiellement inutile, car nous supposons que le package Modbus ne peut pas dépasser 256 octets, mais que se passe-t-il s'il y a du bruit sur la ligne et que quelqu'un envoie des octets au hasard? Pour ce faire, j'ai créé un tampon de 257 octets, et si une interruption Rx DMA se produit, cela signifie que quelqu'un "jette" la ligne, et nous jetons le canal Rx au début du tampon et écoutons à nouveau.



Gestionnaires d'interruption:



Gestionnaires d'interruption
/*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);
    }
}




Les gestionnaires DMA sont assez simples: tout envoyé - nettoyer les drapeaux, passer en mode réception, recevoir 257 octets - erreur de trame, nettoyer l'humidité, passer à nouveau en mode réception.



Le processeur USART nous dit qu'une certaine quantité de données est arrivée, puis il y a eu un silence. La trame est prête, nous déterminons le nombre d'octets reçus (le nombre maximum d'octets de réception DMA - le montant qui reste à recevoir), éteignons la réception, réveillons la tâche.



Une mise en garde, j'avais l'habitude d'utiliser un sémaphore binaire pour réveiller la tâche, mais les développeurs de FreeRTOS recommandent d'utiliser TaskNotification :

Le déblocage d'une tâche RTOS avec une notification directe est 45% plus rapide et utilise moins de RAM que le déblocage d'une tâche avec un sémaphore binaire

Parfois, dans FreeRTOS_Config.h, la fonction xTaskGetCurrentTaskHandle () n'est pas incluse dans l'assemblage , auquel cas vous devez ajouter une ligne Ă  ce fichier:



#define INCLUDE_xTaskGetCurrentTaskHandle 1


Sans utiliser de sémaphore, le firmware a perdu près de 1 Ko. Une bagatelle, bien sûr, mais agréable.



Fonctions d'envoi et de réception:



Envoyer et recevoir
/*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;
}


Les deux fonctions réinitialisent les canaux DMA. Lors de la réception, la fonction de suivi du timeout dans le registre CR2 est activée par le bit USART_CR2_RTOEN .



CRC



Passons au calcul CRC hardcore. Cette fonction du contrôleur oculaire m'a toujours ennuyé, mais d'une manière ou d'une autre, cela n'a jamais fonctionné, dans certaines séries, il était impossible de définir un polynôme arbitraire, dans certains, il était impossible de changer la dimension du polynôme, et ainsi de suite. En F3, tout va bien, définissez le polynôme et changez la taille, mais j'ai dû faire un squat:



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;
}


Il s'est avéré qu'il est impossible de simplement jeter octet par octet dans le registre DR - ce sera une mauvaise lecture, vous devez utiliser l'accès octet. J'ai déjà rencontré de tels "monstres" dans STM avec le module SPI dans lequel je veux écrire octet par octet.



Tâche



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();
        }
    }
}


Dans celui-ci, nous initialisons le pointeur vers la tâche, cela est nécessaire pour l'utiliser pour déverrouiller via TaskNotification, initialiser le matériel et attendre que nous dormions jusqu'à ce que la notification arrive. Si nécessaire, vous pouvez mettre une valeur de délai d'expiration au lieu de portMAX_DELAY pour déterminer qu'il n'y a pas eu de connexion depuis un certain temps. Si la notification est arrivée, nous traitons le colis, formons une réponse et l'envoyons, mais si le cadre est arrivé cassé ou à la mauvaise adresse, nous attendons simplement la suivante.



/*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;
}


Le gestionnaire lui-même n'a pas d'intérêt particulier: vérifier la longueur de trame / adresse / CRC et générer une réponse ou une erreur. Cette implémentation prend en charge trois fonctions principales: 0x03 - Read Registers, 0x06 - Write register, 0x10 - Write Multiple Registers. Habituellement, ces fonctions me suffisent, mais si vous le souhaitez, vous pouvez étendre la fonctionnalité sans problème.



Nous allons commencer:



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


Pour que la tâche fonctionne, une pile d'une taille de 32 x uint32_t (ou 128 octets) suffit ; c'est la taille que j'ai définie dans la définition configMINIMAL_STACK_SIZE . Pour référence: au départ, j'ai supposé à tort que configMINIMAL_STACK_SIZE était défini en octets, si je n'avais pas ajouté suffisamment, cependant, lorsque je travaillais avec des contrôleurs F0, où il y avait moins de RAM, j'ai dû compter la pile une fois et il s'est avéré que configMINIMAL_STACK_SIZE était défini dans les dimensions du type portSTACK_TYPE , qui est défini dans fichier portmacro.h

#define portSTACK_TYPE    uint32_t


Conclusion



Cette implémentation Modbus RTU utilise de manière optimale les capacités matérielles du microcontrôleur STM32F3xx.



Le poids du microprogramme de sortie avec le système d'exploitation et l'optimisation -o2 était: Taille du programme: 5492 octets, Taille des données: 112 octets. Dans un contexte de 6 Ko, perdre 1 Ko à cause des sémaphores semble significatif.



La portabilité vers d'autres familles est possible, par exemple F0 prend en charge le délai d'expiration et RS485, mais il y a un problème avec le CRC matériel, vous pouvez donc vous en tirer avec la méthode de calcul du logiciel. Il peut également y avoir des différences dans les gestionnaires d'interruption DMA, quelque part où ils sont combinés.



Lien vers github



Peut-ĂŞtre sera-t-il utile Ă  quelqu'un.



Liens utiles:






All Articles