STM32F3xx + FreeRTOS. Modbus RTU com hardware RS485 e CRC sem temporizadores e semáforos

Olá! Há relativamente pouco tempo, depois de me formar na universidade, entrei em uma pequena empresa que se dedicava ao desenvolvimento de eletrônicos. Um dos primeiros problemas que encontrei foi a necessidade de implementar o protocolo Modbus RTU Slave usando STM32. Com um pecado pela metade, eu o escrevi então, mas comecei a atender esse protocolo de projeto a projeto e decidi refatorar e otimizar lib usando FreeRTOS.



Introdução



Em projetos atuais, costumo usar o pacote STM32F3xx + FreeRTOS, então decidi aproveitar ao máximo os recursos de hardware deste controlador. Em particular:



  • Recebendo / enviando usando DMA
  • Possibilidade de cálculo de CRC de hardware
  • Suporte de hardware RS485
  • Detecção de fim de pacote via recursos de hardware USART, sem usar um cronômetro


Vou fazer uma reserva agora mesmo, aqui não estou descrevendo a especificação do protocolo Modbus e como o mestre funciona com ele, você pode ler sobre isso aqui e aqui .



arquivo de configuração



Para começar, decidi simplificar a tarefa de transferência de código entre projetos, pelo menos dentro da mesma família de controladores. Portanto, decidi escrever um pequeno arquivo conf.h que me permitiria reconfigurar rapidamente as partes principais da implementação.



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




Na maioria das vezes, na minha opinião, as seguintes coisas mudam:



  • Endereço do dispositivo e tamanho do espaço de endereço
  • Freqüência de relógio e parâmetros dos pinos USART (pin, port, rcc, irq)
  • Parâmetros do canal DMA (rcc, irq)
  • Habilitar / Desabilitar Hardware CRC e RS485


Configuração de ferro



Nesta implementação, eu uso o CMSIS usual, não por causa de crenças religiosas, é apenas mais fácil para mim e menos dependências. Não vou descrever as configurações da porta, você pode ver no link para o github que estará abaixo.



Vamos começar configurando o USART:



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




Existem vários pontos aqui:



  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 é configurado com dois campos de bits : USART_CR1_DEAT e USART_CR1_DEDT . Esses campos de bits permitem definir o tempo para remover e definir o sinal DE antes e depois de enviar em 1/16 ou 1/8 bits, dependendo do parâmetro de sobreamostragem do módulo USART. Resta apenas habilitar a função no registro CR3 com o bit USART_CR3_DEM , o hardware fará o resto.


Configuração DMA:



Configuração 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);




Como o Modbus opera em um modo de solicitação-resposta, usamos um buffer para recepção e transmissão. Recebido no buffer, processado e enviado a partir dele. Nenhuma entrada é aceita durante o processamento. O canal Rx DMA coloca dados do registro de recepção USART (RDR) no buffer, o canal Tx DMA, ao contrário, do buffer no registro de envio (TDR). Precisamos interromper o canal Tx para determinar se a resposta se foi e podemos mudar para o modo de recepção.



Interromper o canal Rx é essencialmente desnecessário, porque supomos que o pacote Modbus não pode ter mais de 256 bytes, mas e se houver ruído na linha e alguém estiver enviando bytes aleatoriamente? Para fazer isso, fiz um buffer de 257 bytes, e se uma interrupção Rx DMA acontecer, significa que alguém está "bagunçando" a linha, e jogamos o canal Rx no início do buffer e ouvimos novamente.



Interromper manipuladores:



Manipuladores de interrupção
/*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);
    }
}




Os manipuladores de DMA são bastante simples: enviar tudo - limpar os sinalizadores, alternar para o modo de recepção, receber 257 bytes - erro de quadro, limpar umidade, alternar para o modo de recepção novamente.



O processador USART nos diz que uma certa quantidade de dados entrou e então houve silêncio. O quadro está pronto, determinamos o número de bytes recebidos (o número máximo de bytes de recebimento de DMA - a quantidade que resta a ser recebida), desligamos a recepção, despertamos a tarefa.



Uma advertência: eu costumava usar um semáforo binário para ativar a tarefa, mas os desenvolvedores do FreeRTOS recomendam o uso de TaskNotification :

Desbloquear uma tarefa RTOS com uma notificação direta é 45% mais rápido e usa menos RAM do que desbloquear uma tarefa com um semáforo binário

Às vezes, em FreeRTOS_Config.h, a função xTaskGetCurrentTaskHandle () não está incluída na montagem , caso em que você precisa adicionar uma linha a este arquivo:



#define INCLUDE_xTaskGetCurrentTaskHandle 1


Sem usar um semáforo, o firmware perdeu quase 1 kB. Uma bagatela, claro, mas legal.



Funções de envio e recebimento:



Enviar e receber
/ * Configure o DMA para o modo de recepção * /

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


Ambas as funções reinicializam os canais DMA. Ao receber, a função que rastreia o tempo limite no registro CR2 é habilitada pelo bit USART_CR2_RTOEN .



CRC



Vamos passar para o cálculo CRC hardcore. Essa função do controlador de olho sempre me incomodou, mas de alguma forma nunca funcionou, em algumas séries era impossível definir um polinômio arbitrário, em algumas era impossível alterar a dimensão do polinômio, e assim por diante. Em F3 está tudo bem, configure o polinômio e mude o tamanho, mas tive que fazer um agachamento:



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


Descobriu-se que é impossível apenas inserir byte a byte no registro DR - será errado ler, você deve usar o acesso por byte. Já conheci essas "aberrações" no STM com o módulo SPI em que desejo escrever byte a byte.



Tarefa



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


Nele, inicializamos o ponteiro para a tarefa, isso é necessário para usá-lo para desbloquear através de TaskNotification, inicializar o hardware e esperar dormirmos até que a notificação chegue. Se necessário, em vez de portMAX_DELAY , você pode colocar um valor de tempo limite para determinar que não houve conexão por um determinado período. Se chegou a notificação, processamos o pacote, formamos uma resposta e enviamos, mas se o frame chegar quebrado ou no endereço errado, é só aguardar o próximo.



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


O manipulador em si não tem nenhum interesse particular: verificar o comprimento do quadro / endereço / CRC e gerar uma resposta ou erro. Esta implementação suporta três funções principais: 0x03 - Read Registers, 0x06 - Write register, 0x10 - Write Multiple Registers. Normalmente, essas funções são suficientes para mim, mas se desejar, você pode expandir a funcionalidade sem problemas.



Bem, comece:



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


Para que a tarefa funcione, uma pilha com um tamanho de 32 x uint32_t (ou 128 bytes) é suficiente ; este é o tamanho que eu configurei na definição configMINIMAL_STACK_SIZE . Para referência: inicialmente eu assumi erroneamente que configMINIMAL_STACK_SIZE é definido em bytes, se eu não adicionei o suficiente, no entanto, ao trabalhar com controladores F0, onde há menos RAM, tive que contar a pilha uma vez e descobri que configMINIMAL_STACK_SIZE foi definido nas dimensões do tipo portSTACK_TYPE , que é definido em arquivo portmacro.h

#define portSTACK_TYPE    uint32_t


Conclusão



Esta implementação Modbus RTU otimiza o uso dos recursos de hardware do microcontrolador STM32F3xx.



O peso do firmware de saída junto com o sistema operacional e a otimização -o2 foi: Tamanho do programa: 5492 bytes, tamanho dos dados: 112 bytes. No contexto de 6 KB, perder 1 KB de semáforos parece significativo.



A portabilidade para outras famílias é possível, por exemplo F0 suporta timeout e RS485, mas há um problema com o CRC do hardware, então você pode seguir com o método de cálculo do software. Também pode haver diferenças nos manipuladores de interrupção DMA, em algum lugar onde eles estão combinados.



Link para o github



Talvez seja útil para alguém.



Links Úteis:






All Articles