1. 背景
最近朋友让帮忙做个小东西,需要用到modbus协议。RTU的协议之前用过,也总结过相关的知识点,具体见: Modbus RTU协议各知识点入门 + 实例.
之前项目用的命令很固定,就自己写了一个实现。现在这边要实现完整的协议栈,果断去找一些轮子来装。
2. FreeModbus介绍
Modbus相关的协议栈有不少轮子,比较有名是 LibModbus 和 FreeModbus。
LibModbus主要用在linux环境,而FreeModbus主要用在mcu环境,资源占用会小一些。
官方的FreeModbus只到v1.5,且只支持从机免费,做主机的功能要收费。
cwalter - FreeModbus v1.5 后来有兄弟基于这个开发了自己的v1.6,主从机都免费。
armlink- FreeModbus v1.6
我这边的话,因为只做从机,就下载了v1.5来用的。
3. 移植步骤
移植基于的原子的stm32f103zet6 精英板,串口2,用的开发库是HAL库。
3.1 下载代码 & 文件夹格式
从官网或者Github下载到源码, 进入文件夹,扫一下所有文件:
主要文件夹4个,
Demo中主要放了不同平台下的移植文件,以bare文件夹为例
BARE
| - port
| | - portevent.c
| | - portserial.c
| | - porttimer.c
| - demo.c
| - Makefile
这些就是移植层需要我们适配的接口了。其中 event基本不改。主要就是适配串口接口 和 定时器 接口。
然后选择demo.c 里面的
tools:工具
modbus:主要的协议栈实现,实际上不怎么需要修改
doc:文档
3.2 移植代码
主要我们需要修改的就是 串口的driver支持,定时器的driver支持,以及对应寄存器的设置。
网上的代码实在太多了。具体可以参见我的参考链接2。
这里我也把我的HAL库移植过的程序放上来,供大家参考。
3.2.1 串口
串口是我们使用modbus RTU的物理接口。
需要在portserial.c 中进行适配。
主要是实现:
- 串口初始化
- 发一个字节
- 收一个字节
- 中断实现,直接写中断函数或者用回调函数
代码如下:
void USART2_RS485_Init(u32 bound)
{
//GPIO端口设置
// PA2 TX
// PA3 RX
// PD7 DE
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能GPIOA时钟
__HAL_RCC_GPIOD_CLK_ENABLE(); //使能GPIOD时钟
__HAL_RCC_USART2_CLK_ENABLE(); //使能USART2时钟
GPIO_Initure.Pin=GPIO_PIN_2; //PA2
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA2
GPIO_Initure.Pin=GPIO_PIN_3; //PA3
GPIO_Initure.Mode=GPIO_MODE_AF_INPUT; //复用输入
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化PA3
//PD7推挽输出,485模式控制
GPIO_Initure.Pin=GPIO_PIN_7; //PD7
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速
HAL_GPIO_Init(GPIOD,&GPIO_Initure);
//USART 初始化设置
G_USART2_RS485Handler.Instance=USART2; //USART2
G_USART2_RS485Handler.Init.BaudRate=bound; //波特率
G_USART2_RS485Handler.Init.WordLength=UART_WORDLENGTH_8B; //字长为8位数据格式
G_USART2_RS485Handler.Init.StopBits=UART_STOPBITS_1; //一个停止位
G_USART2_RS485Handler.Init.Parity=UART_PARITY_NONE; //无奇偶校验位
G_USART2_RS485Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE; //无硬件流控
G_USART2_RS485Handler.Init.Mode=UART_MODE_TX_RX; //收发模式
HAL_UART_Init(&G_USART2_RS485Handler); //HAL_UART_Init()会使能USART2
__HAL_UART_DISABLE_IT(&G_USART2_RS485Handler,UART_IT_TC);
__HAL_UART_ENABLE_IT(&G_USART2_RS485Handler,UART_IT_RXNE);//开启接收中断
HAL_NVIC_EnableIRQ(USART2_IRQn); //使能USART1中断
HAL_NVIC_SetPriority(USART2_IRQn,3,3); //抢占优先级3,子优先级3
USART2_RS485_TX_EN=0; //默认为接收模式
}
#include "mb.h"
#include "mbport.h"
extern UART_HandleTypeDef G_USART2_RS485Handler;
#define MODBUS_UART G_USART2_RS485Handler
/* ----------------------- static functions ---------------------------------*/
static void prvvUARTTxReadyISR( void );
static void prvvUARTRxISR( void );
/* ----------------------- Start implementation -----------------------------*/
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
if(xRxEnable)
{
__HAL_UART_ENABLE_IT(&MODBUS_UART, UART_IT_RXNE); //使能接收寄存器非空中断
//HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET); //MAX485操作 低电平为接收模式
USART2_RS485_TX_EN = 0;
}
else
{
__HAL_UART_DISABLE_IT(&MODBUS_UART, UART_IT_RXNE); //禁能接收寄存器非空中断
//HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET); //MAX485操作 高电平为发送模式
USART2_RS485_TX_EN = 1;
}
if (TRUE == xTxEnable)
{
//HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET); //MAX485操作 高电平为发送模式
USART2_RS485_TX_EN = 1;
__HAL_UART_ENABLE_IT(&MODBUS_UART, UART_IT_TC); //使能发送完成中断
}
else
{
USART2_RS485_TX_EN = 0;
__HAL_UART_DISABLE_IT(&MODBUS_UART, UART_IT_TC); //禁能发送完成中断
}
}
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
BOOL ret = TRUE;
USART2_RS485_Init(ulBaudRate);
return ret;
}
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
/* Put a byte in the UARTs transmit buffer. This function is called
* by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
* called. */
//HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET); //MAX485操作 高电平为发送模式
USART2_RS485_TX_EN = 1;
#if 0
HAL_UART_Transmit_IT(&MODBUS_UART, (uint8_t *)&ucByte, 1);
#else
USART2->DR = ucByte;
#endif
return TRUE;
}
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
/* Return the byte in the UARTs receive buffer. This function is called
* by the protocol stack after pxMBFrameCBByteReceived( ) has been called.
*/
// HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET); //MAX485操作 低电平为接收模式
USART2_RS485_TX_EN = 0;
*pucByte = (USART2->DR & (uint16_t)0x00FF);
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.
*/
static 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.
*/
static void prvvUARTRxISR( void )
{
pxMBFrameCBByteReceived( );
}
void USART2_IRQHandler(void)
{
if(__HAL_UART_GET_FLAG(&MODBUS_UART, UART_FLAG_RXNE)) // 接收非空中断标记被置位
{
__HAL_UART_CLEAR_FLAG(&MODBUS_UART, UART_FLAG_RXNE); // 清除中断标记
prvvUARTRxISR(); // 通知modbus有数据到达
}
if(__HAL_UART_GET_FLAG(&MODBUS_UART, UART_FLAG_TXE)) // 发送为空中断标记被置位
{
__HAL_UART_CLEAR_FLAG(&MODBUS_UART, UART_FLAG_TXE); // 清除中断标记
prvvUARTTxReadyISR(); // 通知modbus数据可以发松
}
}
3.2.2 定时器
定时器主要是用于计算3.5位宽的一个时长,每次接受到一个字节的时候,会把定时器的count值清零,重新计时。
连续3.5位宽的时间内
/* ----------------------- Start implementation -----------------------------*/
BOOL xMBPortTimersInit( USHORT usTim1Timerout50us )
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM3_Handler.Instance = TIM3;
TIM3_Handler.Init.Prescaler = 3600 - 1; // 72M 时钟,72m/3600 = 10k 频率, 50us记一次数
TIM3_Handler.Init.CounterMode = TIM_COUNTERMODE_UP;
TIM3_Handler.Init.Period = usTim1Timerout50us - 1; // usTim1Timerout50us * 50即为定时器溢出时间
TIM3_Handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
TIM3_Handler.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&TIM3_Handler) != HAL_OK)
{
return FALSE;
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&TIM3_Handler, &sClockSourceConfig) != HAL_OK)
{
return FALSE;
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&TIM3_Handler, &sMasterConfig) != HAL_OK)
{
return FALSE;
}
__HAL_TIM_CLEAR_FLAG(&TIM3_Handler, TIM_FLAG_UPDATE); // 先清除一下定时器的中断标记,防止使能中断后直接触发中断
__HAL_TIM_ENABLE_IT(&TIM3_Handler, TIM_IT_UPDATE); // 使能定时器更新中断
HAL_TIM_Base_Start_IT(&TIM3_Handler);
return TRUE;
}
/* HAL_TIM_Base_Init 里会调用这个 */
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE();
HAL_NVIC_SetPriority(TIM3_IRQn,1,3);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
}
void vMBPortTimersEnable()
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
__HAL_TIM_CLEAR_IT(&TIM3_Handler,TIM_IT_UPDATE);
__HAL_TIM_ENABLE_IT(&TIM3_Handler,TIM_IT_UPDATE);
__HAL_TIM_SET_COUNTER(&TIM3_Handler, 0); // 清空计数器
__HAL_TIM_ENABLE(&TIM3_Handler); // 使能定时器
}
void vMBPortTimersDisable( )
{
/* Disable any pending timers. */
__HAL_TIM_DISABLE(&TIM3_Handler); // 禁能定时器
__HAL_TIM_SET_COUNTER(&TIM3_Handler,0);
__HAL_TIM_DISABLE_IT(&TIM3_Handler,TIM_IT_UPDATE);
__HAL_TIM_CLEAR_IT(&TIM3_Handler,TIM_IT_UPDATE);
}
/* Create an ISR which is called whenever the timer has expired. This function
* must then call pxMBPortCBTimerExpired( ) to notify the protocol stack that
* the timer has expired.
*/
static void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired( ); /* 指向 xMBRTUTimerT35Expired */
}
// 定时器5中断服务程序
//void TIM5_IRQHandler(void)
void TIM3_IRQHandler(void)
{
if(__HAL_TIM_GET_FLAG(&TIM3_Handler, TIM_FLAG_UPDATE)) // 更新中断标记被置位
{
__HAL_TIM_CLEAR_FLAG(&TIM3_Handler, TIM_FLAG_UPDATE);// 清除中断标记
prvvTIMERExpiredISR(); // 通知modbus3.5个字符等待时间
}
}
一个要注意的点就是记得把定时器的时钟 和 中断打开。
网上一些移植程序这里没写,应该是用cudeIDE直接生成的代码里默认打开了。
如果是自己写的记得开一下,不然进不去中断。
就是加上这个函数HAL_TIM_Base_MspInit, 会在 HAL_TIM_Base_Init 的 时候自动调用。
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE();
HAL_NVIC_SetPriority(TIM3_IRQn,1,3);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
}
比较奇怪的一点是,这三句配置我之前没写在HAL_TIM_Base_MspInit里,直接在init函数里调用的,结果中断一直不正常。
后来放在里这个回调里才正常。
4. 功能验证
这里使用的是armfly的Modbus调试助手,界面比较简单。
尝试使用 03指令,04指令去读取,用06指令写入后再用03指令读取
可以读取到我们写在数组里的参数。
5. 参考链接
- FreeModbus 官网
- STM32移植FreeModbus RTU教程
- FreeModbus源码结构分析