记录一次Modbus的移植过程
软件准备:
需要移植的FreeModbus源码:FreeModbus 主机和从机源码地址
移植好的FreeModbus(主从机)源码:HAL库版本的主机和从机源码地址
开发工具:Keilv5和CubeMX
硬件平台:STM32F407VET6(带RS485接口)
从机和主机的移植过程一样,这里就拿从机移植来举例
代码段大部分地方我都写了注释,需要注意的地方也单独做了说明和解释
1.用CubeMX生成Keil工程:
其他基础部分就跳过,这里讲讲需要注意的地方。
配置系统调试引脚,选择基础定时器作为FreeRTOS时钟源。
这里UART1作为Debug接口,设置为异步通信模式,其他默认即可。
UART2作为Modbus通讯接口,设置为异步通讯模式。
开启UART2中断
不使用HAL的中断服务函数,因为HAL库的中断服务函数太长了,影响串口接受的性能。(嫌弃)故把这个勾勾取消掉!
使能FreeRTOS使用V2接口
添加Modbus任务
CubeMX生成Keli工程
2.代码移植
打开下载的源码,把Freemodbus文件全部拷贝到自己工程的根目录。
拷贝到工程
在工程中找到stm32f4xx_hal_conf.h文件,修改中断回调函数的宏定义
#define USE_HAL_TIM_REGISTER_CALLBACKS 1U /* TIM register callback disabled */
#define USE_HAL_UART_REGISTER_CALLBACKS 1U /* UART register callback disabled */
打开Freemodbus文件夹里面有modbus和port文件夹,其中port文件下的接口代码需要我们仔细修改,其余不需要理会。(有些文件结尾带_m,这些是与主机有关文件,本次是移植从机得教程,故忽略掉此类文件。)
打开port文件夹发现里面有个rtt文件夹,因为源码是基于RTT实现的。我们这里新建一个文件夹命名为FreeRTOS。把rtt里面所有文件都拷贝到里面。
先来看看FreeRTOS文件夹下的文件:
port.c:实现与上下文保护有关的接口
portevent.c:实现任务的事件通知和同步
portserial.c:实现串口的接收和发送
porttimer.c:实现一个微秒级的定时器
先实现port.c中的接口
/*进入临界段保护*/
void EnterCriticalSection(void) { taskENTER_CRITICAL(); }
void ExitCriticalSection(void) { taskEXIT_CRITICAL(); }
/*判断是否进入在中断中*/
#ifndef IS_IRQ()
extern __asm uint32_t vPortGetIPSR(void); //调用FreeRTOS API
__inline bool IS_IRQ(void) //使用内联函数提高速度
{
if (vPortGetIPSR()) {
return TRUE;
}
return FALSE;
}
实现portevent.c的接口
/* ----------------------- Variables ----------------------------------------*/
static EventGroupHandle_t xSlaveOsEvent;
/* ----------------------- Start implementation -----------------------------*/
BOOL xMBPortEventInit(void) {
xSlaveOsEvent = xEventGroupCreate();
if (xSlaveOsEvent != NULL) {
MODBUS_DEBUG("xMBPortEventInit Success!\r\n");
} else {
MODBUS_DEBUG("xMBPortEventInit Faild !\r\n");
return FALSE;
}
return TRUE;
}
BOOL xMBPortEventPost(eMBEventType eEvent) {
BaseType_t flag;
MODBUS_DEBUG("Post eEvent=%d!\r\n", eEvent);
if (xSlaveOsEvent != NULL) {
if (IS_IRQ()) {
xEventGroupSetBitsFromISR(xSlaveOsEvent, eEvent, &flag);
} else {
xEventGroupSetBits(xSlaveOsEvent, eEvent);
}
}
return TRUE;
}
BOOL xMBPortEventGet(eMBEventType *eEvent) {
uint32_t recvedEvent;
/* waiting forever OS event */
recvedEvent = xEventGroupWaitBits(xSlaveOsEvent,
EV_READY | EV_FRAME_RECEIVED | EV_EXECUTE |
EV_FRAME_SENT, /* 接收任务感兴趣的事件
*/
pdTRUE, /* 退出时清除事件?? */
pdFALSE, /* 满足感兴趣的所有事?? */
portMAX_DELAY); /* 指定超时事件,无限等待 */
switch (recvedEvent) {
case EV_READY:
*eEvent = EV_READY;
break;
case EV_FRAME_RECEIVED:
*eEvent = EV_FRAME_RECEIVED;
break;
case EV_EXECUTE:
*eEvent = EV_EXECUTE;
break;
case EV_FRAME_SENT:
*eEvent = EV_FRAME_SENT;
break;
}
return TRUE;
}
实现porttimer.c的接口,注册定时器中断回调函数
注意:使用软件定时器,软件定时器的优先级至少大于协议栈任务的优先级
在FreRTOS的FreeRTOSConfig.h中修改软件定时器的优先级(我这里图省事直接设置为48):
/* Software timer definitions. */
#define configUSE_TIMERS 1
#define configTIMER_TASK_PRIORITY ( 48 )
#define configTIMER_QUEUE_LENGTH 10
#define configTIMER_TASK_STACK_DEPTH 256
porttimer.c的代码如下
/* ----------------------- static functions ---------------------------------*/
static TimerHandle_t timer;
static void prvvTIMERExpiredISR(void);
static void timer_timeout_ind(TIM_HandleTypeDef *xTimer);
/* ----------------------- Start implementation -----------------------------*/
BOOL xMBPortTimersInit(USHORT usTim1Timerout50us) {
/*
Freertos can't create timer in isr!
So,I use hardware timer here! 锛丗req=1Mhz
*/
timer = xTimerCreate(
"Slave timer",
(50 * usTim1Timerout50us) / (1000 * 1000 / configTICK_RATE_HZ) + 1,
pdFALSE, (void *)2, timer_timeout_ind);
if (timer != NULL)
return TRUE;
}
void vMBPortTimersEnable() {
if (IS_IRQ()) {
xTimerStartFromISR((TimerHandle_t)timer, 0);
} else {
xTimerStart((TimerHandle_t)timer, 0);
}
}
void vMBPortTimersDisable() {
if (IS_IRQ()) {
xTimerStopFromISR((TimerHandle_t)timer, 0);
} else {
xTimerStop((TimerHandle_t)timer, 0);
}
}
void prvvTIMERExpiredISR(void) { (void)pxMBPortCBTimerExpired(); }
static void timer_timeout_ind(TIM_HandleTypeDef *xTimer) {
prvvTIMERExpiredISR();
}
实现portserial.c接口,这里是最重要的也是最复杂的一部分
在该文件中需要实现uart的的串口接收完成中断,RS485切换模式,以及一个环形缓冲队列提升串口接收效率。因为环形队列也可以在modbus主站程序中使用,所以我把环形队列放到port.c文件里。
环形队列代码如下:
/*put bytes in buff*/
void Put_in_fifo(Serial_fifo *buff, uint8_t *putdata, int length)
{
portDISABLE_INTERRUPTS();
while (length--)
{
buff->buffer[buff->put_index] = *putdata;
buff->put_index += 1;
if (buff->put_index >= MB_SIZE_MAX)
buff->put_index = 0;
/* if the next position is read index, discard this 'read char' */
if (buff->put_index == buff->get_index)
{
buff->get_index += 1;
if (buff->get_index >= MB_SIZE_MAX)
buff->get_index = 0;
}
}
portENABLE_INTERRUPTS();
}
/*get bytes from buff*/
int Get_from_fifo(Serial_fifo *buff, uint8_t *getdata, int length)
{
int size = length;
/* read from software FIFO */
while (length)
{
int ch;
/* disable interrupt */
portDISABLE_INTERRUPTS();
if (buff->get_index != buff->put_index)
{
ch = buff->buffer[buff->get_index];
buff->get_index += 1;
if (buff->get_index >= MB_SIZE_MAX)
buff->get_index = 0;
}
else
{
/* no data, enable interrupt and break out */
portENABLE_INTERRUPTS();
break;
}
*getdata = ch & 0xff;
getdata++;
length--;
/* enable interrupt */
portENABLE_INTERRUPTS();
}
return size - length;
}
portserial.c全部内容如下
/* ----------------------- Static variables ---------------------------------*/
/* software simulation serial transmit IRQ handler thread */
static TaskHandle_t thread_serial_soft_trans_irq = NULL;
/* serial event */
static EventGroupHandle_t event_serial;
/* modbus slave serial device */
static UART_HandleTypeDef *serial;
/*
* Serial FIFO mode
*/
static volatile uint8_t rx_buff[FIFO_SIZE_MAX];
static Serial_fifo Slave_serial_rx_fifo;
/* ----------------------- Defines ------------------------------------------*/
/* serial transmit event */
#define EVENT_SERIAL_TRANS_START (1 << 0)
/* ----------------------- static functions ---------------------------------*/
static void prvvUARTTxReadyISR(void);
static void prvvUARTRxISR(void);
static void serial_soft_trans_irq(void *parameter);
static void Slave_TxCpltCallback(struct __UART_HandleTypeDef *huart);
static void Slave_RxCpltCallback(struct __UART_HandleTypeDef *huart);
static int stm32_getc(void);
static int stm32_putc(CHAR c);
/* ----------------------- Start implementation -----------------------------*/
BOOL xMBPortSerialInit(UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits,
eMBParity eParity) {
/**
* set 485 mode receive and transmit control IO
* @note MODBUS_SLAVE_RT_CONTROL_PIN_INDEX need be defined by user
*/
// rt_pin_mode(MODBUS_SLAVE_RT_CONTROL_PIN_INDEX, PIN_MODE_OUTPUT);
/* set serial name */
if (ucPORT == 1) {
#if defined(USING_UART1)
extern UART_HandleTypeDef huart1;
serial = &huart1;
MODBUS_DEBUG("Slave using uart1!\r\n");
#endif
} else if (ucPORT == 2) {
#if defined(USING_UART2)
extern UART_HandleTypeDef huart2;
serial = &huart2;
MODBUS_DEBUG("Slave using uart2!\r\n");
#endif
} else if (ucPORT == 3) {
#if defined(USING_UART3)
extern UART_HandleTypeDef huart3;
serial = &huart3;
MODBUS_DEBUG("Slave using uart3!\r\n");
#endif
}
/* set serial configure */
serial->Init.StopBits = UART_STOPBITS_1;
serial->Init.BaudRate = ulBaudRate;
switch (eParity) {
case MB_PAR_NONE: {
serial->Init.WordLength = UART_WORDLENGTH_8B;
serial->Init.Parity = UART_PARITY_NONE;
break;
}
case MB_PAR_ODD: {
serial->Init.WordLength = UART_WORDLENGTH_9B;
serial->Init.Parity = UART_PARITY_ODD;
break;
}
case MB_PAR_EVEN: {
serial->Init.WordLength = UART_WORDLENGTH_9B;
serial->Init.Parity = UART_PARITY_EVEN;
break;
}
}
if (HAL_UART_Init(serial) != HAL_OK) {
Error_Handler();
}
__HAL_UART_DISABLE_IT(serial, UART_IT_RXNE);
__HAL_UART_DISABLE_IT(serial, UART_IT_TC);
/*registe recieve callback*/
HAL_UART_RegisterCallback(serial, HAL_UART_RX_COMPLETE_CB_ID,
Slave_RxCpltCallback);
/* software initialize */
Slave_serial_rx_fifo.buffer = rx_buff;
Slave_serial_rx_fifo.get_index = 0;
Slave_serial_rx_fifo.put_index = 0;
/* 创建串口发送线程*/
event_serial = xEventGroupCreate(); //创建事件
if (NULL != event_serial) {
MODBUS_DEBUG("Create Slave event_serial Event success!\r\n");
} else {
MODBUS_DEBUG("Create Slave event_serial Event Faild!\r\n");
}
BaseType_t xReturn =
xTaskCreate((TaskFunction_t)serial_soft_trans_irq, /* 任务函数*/
(const char *)"slave trans", /* 任务名称*/
(uint16_t)128, /* 栈*/
(void *)NULL, /* 入口参数 */
(UBaseType_t)12, /* 优先级*/
(TaskHandle_t *)&thread_serial_soft_trans_irq); /*任务句柄*/
if (xReturn == pdPASS) {
MODBUS_DEBUG("xTaskCreate slave trans success\r\n");
}
return TRUE;
}
void vMBPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable) {
/*清除中断标志,这一步不要省略*/
__HAL_UART_CLEAR_FLAG(serial,UART_FLAG_RXNE);
__HAL_UART_CLEAR_FLAG(serial,UART_FLAG_TC);
if (xRxEnable) {
/* enable RX interrupt */
__HAL_UART_ENABLE_IT(serial, UART_IT_RXNE);
/* switch 485 to receive mode */
MODBUS_DEBUG("RS485_RX_MODE\r\n");
SLAVE_RS485_RX_MODE;
} else {
/* switch 485 to transmit mode */
MODBUS_DEBUG("RS485_TX_MODE\r\n");
SLAVE_RS485_TX_MODE;
/* disable RX interrupt */
__HAL_UART_DISABLE_IT(serial, UART_IT_RXNE);
}
if (xTxEnable) {
/* start serial transmit */
xEventGroupSetBits(event_serial, EVENT_SERIAL_TRANS_START);
} else {
/* stop serial transmit */
xEventGroupClearBits(event_serial, EVENT_SERIAL_TRANS_START);
/*测试帧数*/
// printf("ms=%.2f,fps=%.2f\r\n", __HAL_TIM_GetCounter(&htim7) / 100.f,
// 1000.f / (__HAL_TIM_GetCounter(&htim7) / 100.f));
}
}
void vMBPortClose(void) { __HAL_UART_DISABLE(serial); }
/*Send a byte*/
BOOL xMBPortSerialPutByte(CHAR ucByte) {
stm32_putc(ucByte);
return TRUE;
}
/*Get a byte from fifo*/
BOOL xMBPortSerialGetByte(CHAR *pucByte) {
Get_from_fifo(&Slave_serial_rx_fifo, (uint8_t *)pucByte, 1);
return TRUE;
}
/*
* Create an interrupt handler for the transmit buffer empty interrupt
* (or an equivalent) for your target processor. This function should then
* call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that
* a new character can be sent. The protocol stack will then call
* xMBPortSerialPutByte( ) to send the character.
*/
void prvvUARTTxReadyISR(void) { pxMBFrameCBTransmitterEmpty(); }
/*
* Create an interrupt handler for the receive interrupt for your target
* processor. This function should then call pxMBFrameCBByteReceived( ). The
* protocol stack will then call xMBPortSerialGetByte( ) to retrieve the
* character.
*/
void prvvUARTRxISR(void) { pxMBFrameCBByteReceived(); }
/**
* Software simulation serial transmit IRQ handler.
*
* @param parameter parameter
*/
static void serial_soft_trans_irq(void *parameter) {
uint32_t recved_event;
while (1) {
/* waiting for serial transmit start */
xEventGroupWaitBits(event_serial, /* 事件对象句柄 */
EVENT_SERIAL_TRANS_START, /* 接收任务感兴趣的事件 */
pdFALSE, /* 退出时清除事件?? */
pdFALSE, /* 满足感兴趣的所有事?? */
portMAX_DELAY); /* 指定超时事件,无限等待 */
/* execute modbus callback */
prvvUARTTxReadyISR();
}
}
/**
* @brief Rx Transfer completed callbacks.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @retval None
*/
void Slave_RxCpltCallback(UART_HandleTypeDef *huart) {
int ch = -1;
while (1) {
ch = stm32_getc();
if (ch == -1)
break;
Put_in_fifo(&Slave_serial_rx_fifo, (uint8_t *)&ch, 1);
}
prvvUARTRxISR();
}
/*UART发送一个字节*/
static int stm32_putc(CHAR c) {
serial->Instance->DR = c;
while (!(serial->Instance->SR & UART_FLAG_TC))
;
return TRUE;
}
/*UART接收一个字节*/
static int stm32_getc(void) {
int ch;
ch = -1;
if (serial->Instance->SR & UART_FLAG_RXNE) {
ch = serial->Instance->DR & 0xff;
}
return ch;
}
串口中断服务函数示例
主站和从站的串口中断服务函数是一样的
/**
* @brief This function handles USART2 global interrupt.
*/
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_RXNE))
{
huart2.RxCpltCallback(&huart2);
__HAL_UART_CLEAR_FLAG(&huart2, UART_FLAG_RXNE);
}
if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_ORE))
{
uint16_t pucByte = (uint16_t)((&huart2)->Instance->DR & (uint16_t)0x01FF);
__HAL_UART_CLEAR_OREFLAG(&huart2);
}
if (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TC))
{
__HAL_UART_CLEAR_FLAG(&huart2, UART_FLAG_TC);
}
/* USER CODE END USART2_IRQn 0 */
/* USER CODE BEGIN USART2_IRQn 1 */
/* USER CODE END USART2_IRQn 1 */
}
在freertos.c文件创建任务示例
需要注意的地方:
这里最好在CubeMX生成的void MX_FREERTOS_Init(void)函数中去初始化协议栈,因为此时任务调度器是锁了的,以避免高优先级的任务在协议栈还没准备好前就先调用modbus的api。这种方式很不安全,因为协议栈里有很多同步的信号量和事件通知,若是这些信号量还没被创建就被调用,会造成程序卡死或其他未知错误。这一点在主机modbus中尤为明显,须特别注意!
osThreadId_t MasterTaskHandle;
const osThreadAttr_t MasterTask_attributes = {
.name = "MasterTask",
.priority = (osPriority_t)osPriorityNormal,
.stack_size = 128 * 4};
osThreadId_t SlaveTaskHandle;
const osThreadAttr_t SlaveTask_attributes = {
.name = "SlaveTask",
.priority = (osPriority_t)osPriorityNormal,
.stack_size = 128 * 4};
void MX_FREERTOS_Init(void)
{
/* USER CODE BEGIN Init */
/*主机部分的协议栈初始化*/
eMBMasterInit(MB_RTU, 3, 115200, MB_PAR_NONE);
eMBMasterEnable();
/*从机部分的协议栈初始化*/
eMBInit(MB_RTU, 0x01, 2, 115200, MB_PAR_NONE);
eMBEnable();
MasterTaskHandle = osThreadNew(MasterTask, NULL, &MasterTask_attributes); //主站的协议栈任务
SlaveTaskHandle = osThreadNew(SlaveTask, NULL, &SlaveTask_attributes); //从站的协议栈任务
}
void SlaveTask(void *argument)
{
for (;;)
{
eMBPoll();
]
}
void MasterTask(void *argument)
{
for (;;)
{
eMBMasterPoll();
}
}
3.移植完成!
完善了的代码已经放在文章开头了。
看下效果
每秒ping10次,一晚上ping30多万次,0个Err。舒服!