Introducción
En proyectos actuales, suelo usar el paquete STM32F3xx + FreeRTOS, así que decidí aprovechar al máximo las capacidades de hardware de este controlador. En particular:
- Recepción / envío mediante DMA
- Posibilidad de cálculo de CRC por hardware
- Soporte de hardware RS485
- Detección de fin de paquete a través de capacidades de hardware USART, sin usar un temporizador
Haré una reserva de inmediato, aquí no estoy describiendo la especificación del protocolo Modbus y cómo funciona el maestro con él, puede leer sobre esto aquí y aquí .
archivo de configuración
Para empezar, decidí simplificar la tarea de transferir código entre proyectos, al menos dentro de la misma familia de controladores. Así que decidí escribir un pequeño archivo conf.h que me permitiera reconfigurar rápidamente las partes principales de la implementación.
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 */
Muy a menudo, en mi opinión, las siguientes cosas cambian:
- Dirección del dispositivo y tamaño del espacio de direcciones
- Frecuencia de reloj y parámetros de los pines USART (pin, puerto, rcc, irq)
- Parámetros del canal DMA (rcc, irq)
- Activar / desactivar hardware CRC y RS485
Configuración de hierro
En esta implementación, uso el CMSIS habitual, no por creencias religiosas, simplemente es más fácil para mí y tiene menos dependencias. No describiré la configuración del puerto, puede verlo en el enlace al github que se encuentra a continuación.
Comencemos configurando el USART:
Configurar 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;
Aquí hay varios puntos:
- F3, F0, , - . . , F1 , . USART_CR1_RTOIE R1. , USART , RM!
- RTOR. , 3.5 , 35 (1 — 8 + 1 + 1 ). 19200 / 1.75 , :
MB_USART->RTOR = 0.00175 * MB_USART_BAUDRATE + 1; - OC, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY , FreeRTOS FromISR , . FreeRTOS_Config.h,
- RS485 está configurado con dos campos de bits: USART_CR1_DEAT y USART_CR1_DEDT . Estos campos de bits le permiten establecer el tiempo para eliminar y configurar la señal DE antes y después de enviar 1/16 o 1/8 bits, según el parámetro de sobremuestreo del módulo USART. Solo queda habilitar la función en el registro CR3 con el bit USART_CR3_DEM , el hardware se encargará del resto.
Configuración DMA:
Configuración de 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);
Dado que Modbus opera en un modo de solicitud-respuesta, usamos un búfer tanto para la recepción como para la transmisión. Recibido en el búfer, procesado allí y enviado desde él. No se aceptan entradas durante el procesamiento. El canal Rx DMA coloca datos del registro de recepción de USART (RDR) en el búfer, el canal Tx DMA, por el contrario, del búfer al registro de envío (TDR). Necesitamos interrumpir el canal Tx para determinar que la respuesta se ha ido y podemos cambiar al modo de recepción.
Interrumpir el canal Rx es esencialmente innecesario, porque asumimos que el paquete Modbus no puede tener más de 256 bytes, pero ¿qué pasa si hay ruido en la línea y alguien envía bytes al azar? Para hacer esto, hice un búfer de 257 bytes, y si ocurre una interrupción Rx DMA, significa que alguien está "ensuciando" la línea, y lanzamos el canal Rx al principio del búfer y escuchamos nuevamente.
Controladores de interrupciones:
Controladores de interrupciones
/*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);
}
}
Los manejadores DMA son bastante simples: enviaron todo - limpiar las banderas, cambiar al modo de recepción, recibir 257 bytes - error de trama, limpiar la humedad, cambiar al modo de recepción nuevamente.
El procesador USART nos dice que ingresó una cierta cantidad de datos y luego hubo silencio. La trama está lista, determinamos la cantidad de bytes recibidos (la cantidad máxima de bytes de recepción de DMA - la cantidad que queda por recibir), apagamos la recepción, reactiva la tarea.
Una advertencia, antes usé un semáforo binario para despertar la tarea, pero los desarrolladores de FreeRTOS recomiendan usar TaskNotification :
Desbloquear una tarea RTOS con una notificación directa es un 45% más rápido y usa menos RAM que desbloquear una tarea con un semáforo binarioA veces, en FreeRTOS_Config.h, la función xTaskGetCurrentTaskHandle () no está incluida en el ensamblaje , en cuyo caso debe agregar una línea a este archivo:
#define INCLUDE_xTaskGetCurrentTaskHandle 1
Sin usar un semáforo, el firmware ha perdido casi 1 kB. Un poco, por supuesto, pero agradable.
Funciones de envío y recepción:
Enviar y recibir
/*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;
}Ambas funciones reinicializan los canales DMA. Al recibir, la función de seguimiento del tiempo de espera en el registro CR2 está habilitada por el bit USART_CR2_RTOEN .
CRC
Pasemos al cálculo duro de CRC. Esta función del controlador ocular siempre me molestó, pero de alguna manera nunca funcionó, en algunas series era imposible establecer un polinomio arbitrario, en algunas era imposible cambiar la dimensión del polinomio, y así sucesivamente. En F3, todo está bien, y establezco el polinomio y cambia el tamaño, pero tuve que hacer una sentadilla:
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;
}
Resultó que es imposible simplemente introducir byte por byte en el registro DR ; será incorrecto leer, debe usar byte-access. Ya me he encontrado con tales "monstruos" en STM con el módulo SPI en el que quiero escribir byte a byte.
Tarea
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();
}
}
}
En él, inicializamos el puntero a la tarea, esto es necesario para poder usarlo para desbloquear mediante TaskNotification, inicializar el hardware y esperar a dormir hasta que llegue la notificación. Si es necesario, puede poner un valor de tiempo de espera en lugar de portMAX_DELAY para determinar que no ha habido conexión durante un tiempo determinado. Si ha llegado la notificación, procesamos el paquete, formamos una respuesta y lo enviamos, pero si el marco llegó roto o en la dirección incorrecta, solo esperamos el siguiente.
/*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;
}
El manejador en sí no es de particular interés: verificar la longitud de la trama / dirección / CRC y generar una respuesta o error. Esta implementación admite tres funciones principales: 0x03 - Leer registros, 0x06 - Escribir registro, 0x10 - Escribir múltiples registros. Por lo general, estas funciones son suficientes para mí, pero si lo deseas, puedes ampliar la funcionalidad sin problemas.
Empezaremos:
int main(void)
{
NVIC_SetPriorityGrouping(3);
xTaskCreate(MB_RTU_Slave_Task, "MB", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL);
vTaskStartScheduler();
}
Para que la tarea funcione, una pila con un tamaño de 32 x uint32_t (o 128 bytes) es suficiente ; este es el tamaño que he establecido en la definición configMINIMAL_STACK_SIZE . Como referencia: inicialmente asumí erróneamente que configMINIMAL_STACK_SIZE está configurado en bytes, si no agregué lo suficiente, sin embargo, cuando trabajaba con controladores F0, donde hay menos RAM, tuve que contar la pila una vez y resultó que configMINIMAL_STACK_SIZE se estableció en dimensiones del tipo portSTACK_TYPE , que se define en archivo portmacro.h
#define portSTACK_TYPE uint32_t
Conclusión
Esta implementación Modbus RTU hace un uso óptimo de las capacidades de hardware del microcontrolador STM32F3xx.
El peso del firmware de salida junto con el SO y la optimización -o2 fue: Tamaño del programa: 5492 Bytes, Tamaño de los datos: 112 bytes. En el contexto de 6 KB, perder 1 KB de semáforos parece significativo.
La portabilidad a otras familias es posible, por ejemplo, F0 admite tiempo de espera y RS485, pero hay un problema con el CRC del hardware, por lo que puede arreglárselas con el método de cálculo del software. También puede haber diferencias en los controladores de interrupciones DMA, en algún lugar se combinan.
Enlace a github
Quizás le sea útil a alguien.
Enlaces útiles: