Modbus是一个非常好用的通讯协议,经常用在串口通讯中,也可以用在网口。它既简洁又规范,尤其在工业中应用非常广泛。Modbus的程序实现也比较简单,用户可以自己实现,也可以移植开源的协议代码,比如今天要介绍的FreeModbus。
硬件环境:STM32F103C8T6
软件环境:STM32CubeMX v6.1.1
HAL库:STM32CubeF1 Firmware Package V1.8.3
FreeModbus版本:freemodbus-v1.6
1.FreeModbus文件说明
下载之后解压出来,可以看到文件夹内包含以下内容。我们需要关注的只有modbus文件夹和demo下的BARE文件夹。modbus文件夹下是协议的具体代码。Demo->BARE文件夹下是接口文件,需要用户进行移植和修改的。
2.STM32CubeMX配置
需要使能一个串口和一个定时器,定时器的功能是用于检测3.5个字符的空闲,以判断一帧数据的结束。这里以USART1和TIM4为例进行介绍。
定时器配置:
串口配置:
定时器和串口的参数任意即可,具体在程序中进行配置。打开定时器和串口的中断,且串口中断的优先级要高于定时器中断。
串口和定时器中断程序我们自己编写,所以这里需要取消串口和定时器中断自动生成代码的选项。如下图:
3.FreeModbus移植
生成代码后,将下载的FreeModbus的源码复制到工程目录中,在Keil工程中新建两个Group组:FreeModbus和Port。添加modbus文件夹下的全部文件到FreeModbus组。将Demo->BARE->Port文件夹下的文件添加到Port组。同时新建一个Port.c文件也添加到Port组。
另外,别忘了在工程中添加包含路径,否则编译出错。
下面开始程序移植,首先是portserial.c文件,该文件是串口的接口文件,包含以下函数:串口使能、串口初始化、发送一个字节、接收一个字节等。我们需要自己实现完成这些函数的内容。完成后的内容如下:
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
if(xRxEnable)
{
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); //使能接收中断
}
else
{
__HAL_UART_DISABLE_IT(&huart1, UART_IT_RXNE); //关闭接收中断
}
if(xTxEnable)
{
SET_DE; //485芯片设置为发送模式
__HAL_UART_ENABLE_IT(&huart1, UART_IT_TXE); //使能发送中断
}
else
{
__HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE); //关闭发送为空中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_TC); //使能发送完成中断
}
}
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
huart1.Instance = USART1;
huart1.Init.BaudRate = ulBaudRate;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
switch(eParity)
{
// 奇校验
case MB_PAR_ODD:
huart1.Init.Parity = UART_PARITY_ODD;
huart1.Init.WordLength = UART_WORDLENGTH_9B; //带奇偶校验数据位为9bits
break;
//偶校验
case MB_PAR_EVEN:
huart1.Init.Parity = UART_PARITY_EVEN;
huart1.Init.WordLength = UART_WORDLENGTH_9B; //带奇偶校验数据位为9bits
break;
//无校验
default:
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.WordLength = UART_WORDLENGTH_8B; //无奇偶校验数据位为8bits
break;
}
return HAL_UART_Init(&huart1) == HAL_OK ? TRUE : FALSE;
}
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
USART1->DR = ucByte;
return TRUE;
}
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
*pucByte = (USART1->DR & (uint16_t)0x00FF);
return TRUE;
}
static void prvvUARTTxReadyISR( void )
{
pxMBFrameCBTransmitterEmpty( );
}
static void prvvUARTRxISR( void )
{
pxMBFrameCBByteReceived( );
}
//串口中断函数
void USART1_IRQHandler(void)
{
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) //接收中断标
{
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_RXNE); //清除中断标记
prvvUARTRxISR(); //通知modbus有数据到达
}
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE)) //发送中断标记被置位
{
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TXE); //清除中断标记
prvvUARTTxReadyISR(); //通知modbus数据可以发松
}
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC)) //发送完成中断
{
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC); //清除中断标记
CLR_DE; //485芯片设置为接收模式
}
}
然后是porttimer.c文件,该文件是定时器的接口文件。定时器的作用是用于通知modbus协议栈3.5个字符的空闲时间已经到达。我们需要实现定时器的初始化和中断相关函数,定时需要配置为50us计数一次,具体计数周期与波特率有关。完成后的内容如下:
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim4.Instance = TIM4;
htim4.Init.Prescaler = 3599; //50us计数一次
htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
htim4.Init.Period = usTim1Timerout50us - 1;
htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
__HAL_TIM_ENABLE_IT(&htim4, TIM_IT_UPDATE); //使能定时器更新中断
return TRUE;
}
inline void
vMBPortTimersEnable( )
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
__HAL_TIM_SET_COUNTER(&htim4, 0); //清空定时器
__HAL_TIM_ENABLE(&htim4); //使能定时器
}
inline void
vMBPortTimersDisable( )
{
/* Disable any pending timers. */
__HAL_TIM_DISABLE(&htim4); //关闭定时器
}
static void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired( );
}
//定时器4中断函数
void TIM4_IRQHandler(void)
{
if(__HAL_TIM_GET_FLAG(&htim4, TIM_FLAG_UPDATE)) //判断更新中断
{
__HAL_TIM_CLEAR_FLAG(&htim4, TIM_FLAG_UPDATE); //清除中断标记
prvvTIMERExpiredISR(); //通知modbus3.5个字符等待时间到
}
}
接下来需要实现的就是port.c文件,需要实现各个寄存器/线圈的读写函数,可以参考demo->BARE文件夹下的demo.c文件。完成后的内容如下:
//输入寄存器
#define REG_INPUT_START 3000
#define REG_INPUT_NREGS 4
//保持寄存器
#define REG_HOLD_START 4000
#define REG_HOLD_NREGS 10
//线圈
#define REG_COILS_START 0
#define REG_COILS_NREGS 4
//开关寄存器
#define REG_DISCRETE_START 1000
#define REG_DISCRETE_NREGS 4
/* ----------------------- Static variables ---------------------------------*/
static USHORT usRegInputStart = REG_INPUT_START;
static USHORT usRegInputBuf[REG_INPUT_NREGS];
static USHORT usRegHoldStart = REG_HOLD_START;
static USHORT usRegHoldBuf[REG_HOLD_NREGS];
static USHORT usRegCoilsStart = REG_COILS_START;
static uint8_t usRegCoilsBuf[REG_COILS_NREGS];
static USHORT usRegDiscreteStart = REG_DISCRETE_START;
static uint8_t usRegDiscreteBuf[REG_DISCRETE_NREGS];
Modbus是一个非常好用的通讯协议,经常用在串口通讯中,也可以用在网口。它既简洁又规范,尤其在工业中应用非常广泛。Modbus的程序实现也比较简单,用户可以自己实现,也可以移植开源的协议代码,比如今天要介绍的FreeModbus。
硬件环境:STM32F103C8T6
软件环境:STM32CubeMX v6.1.1
HAL库:STM32CubeF1 Firmware Package V1.8.3
FreeModbus版本:freemodbus-v1.6
1.FreeModbus文件说明
下载之后解压出来,可以看到文件夹内包含以下内容。我们需要关注的只有modbus文件夹和demo下的BARE文件夹。modbus文件夹下是协议的具体代码。Demo->BARE文件夹下是接口文件,需要用户进行移植和修改的。
2.STM32CubeMX配置
需要使能一个串口和一个定时器,定时器的功能是用于检测3.5个字符的空闲,以判断一帧数据的结束。这里以USART1和TIM4为例进行介绍。
定时器配置:
串口配置:
定时器和串口的参数任意即可,具体在程序中进行配置。打开定时器和串口的中断,且串口中断的优先级要高于定时器中断。
串口和定时器中断程序我们自己编写,所以这里需要取消串口和定时器中断自动生成代码的选项。如下图:
3.FreeModbus移植
生成代码后,将下载的FreeModbus的源码复制到工程目录中,在Keil工程中新建两个Group组:FreeModbus和Port。添加modbus文件夹下的全部文件到FreeModbus组。将Demo->BARE->Port文件夹下的文件添加到Port组。同时新建一个Port.c文件也添加到Port组。
另外,别忘了在工程中添加包含路径,否则编译出错。
下面开始程序移植,首先是portserial.c文件,该文件是串口的接口文件,包含以下函数:串口使能、串口初始化、发送一个字节、接收一个字节等。我们需要自己实现完成这些函数的内容。完成后的内容如下:
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
if(xRxEnable)
{
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE); //使能接收中断
}
else
{
__HAL_UART_DISABLE_IT(&huart1, UART_IT_RXNE); //关闭接收中断
}
if(xTxEnable)
{
SET_DE; //485芯片设置为发送模式
__HAL_UART_ENABLE_IT(&huart1, UART_IT_TXE); //使能发送中断
}
else
{
__HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE); //关闭发送为空中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_TC); //使能发送完成中断
}
}
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
huart1.Instance = USART1;
huart1.Init.BaudRate = ulBaudRate;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
switch(eParity)
{
// 奇校验
case MB_PAR_ODD:
huart1.Init.Parity = UART_PARITY_ODD;
huart1.Init.WordLength = UART_WORDLENGTH_9B; //带奇偶校验数据位为9bits
break;
//偶校验
case MB_PAR_EVEN:
huart1.Init.Parity = UART_PARITY_EVEN;
huart1.Init.WordLength = UART_WORDLENGTH_9B; //带奇偶校验数据位为9bits
break;
//无校验
default:
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.WordLength = UART_WORDLENGTH_8B; //无奇偶校验数据位为8bits
break;
}
return HAL_UART_Init(&huart1) == HAL_OK ? TRUE : FALSE;
}
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
USART1->DR = ucByte;
return TRUE;
}
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
*pucByte = (USART1->DR & (uint16_t)0x00FF);
return TRUE;
}
static void prvvUARTTxReadyISR( void )
{
pxMBFrameCBTransmitterEmpty( );
}
static void prvvUARTRxISR( void )
{
pxMBFrameCBByteReceived( );
}
//串口中断函数
void USART1_IRQHandler(void)
{
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE)) //接收中断标
{
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_RXNE); //清除中断标记
prvvUARTRxISR(); //通知modbus有数据到达
}
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TXE)) //发送中断标记被置位
{
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TXE); //清除中断标记
prvvUARTTxReadyISR(); //通知modbus数据可以发松
}
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC)) //发送完成中断
{
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC); //清除中断标记
CLR_DE; //485芯片设置为接收模式
}
}
然后是porttimer.c文件,该文件是定时器的接口文件。定时器的作用是用于通知modbus协议栈3.5个字符的空闲时间已经到达。我们需要实现定时器的初始化和中断相关函数,定时需要配置为50us计数一次,具体计数周期与波特率有关。完成后的内容如下:
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim4.Instance = TIM4;
htim4.Init.Prescaler = 3599; //50us计数一次
htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
htim4.Init.Period = usTim1Timerout50us - 1;
htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim4) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
__HAL_TIM_ENABLE_IT(&htim4, TIM_IT_UPDATE); //使能定时器更新中断
return TRUE;
}
inline void
vMBPortTimersEnable( )
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
__HAL_TIM_SET_COUNTER(&htim4, 0); //清空定时器
__HAL_TIM_ENABLE(&htim4); //使能定时器
}
inline void
vMBPortTimersDisable( )
{
/* Disable any pending timers. */
__HAL_TIM_DISABLE(&htim4); //关闭定时器
}
static void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired( );
}
//定时器4中断函数
void TIM4_IRQHandler(void)
{
if(__HAL_TIM_GET_FLAG(&htim4, TIM_FLAG_UPDATE)) //判断更新中断
{
__HAL_TIM_CLEAR_FLAG(&htim4, TIM_FLAG_UPDATE); //清除中断标记
prvvTIMERExpiredISR(); //通知modbus3.5个字符等待时间到
}
}
接下来需要实现的就是port.c文件,需要实现各个寄存器/线圈的读写函数,可以参考demo->BARE文件夹下的demo.c文件。完成后的内容如下:
//输入寄存器
#define REG_INPUT_START 3000
#define REG_INPUT_NREGS 4
//保持寄存器
#define REG_HOLD_START 4000
#define REG_HOLD_NREGS 10
//线圈
#define REG_COILS_START 0
#define REG_COILS_NREGS 4
//开关寄存器
#define REG_DISCRETE_START 1000
#define REG_DISCRETE_NREGS 4
/* ----------------------- Static variables ---------------------------------*/
static USHORT usRegInputStart = REG_INPUT_START;
static USHORT usRegInputBuf[REG_INPUT_NREGS];
static USHORT usRegHoldStart = REG_HOLD_START;
static USHORT usRegHoldBuf[REG_HOLD_NREGS];
static USHORT usRegCoilsStart = REG_COILS_START;
static uint8_t usRegCoilsBuf[REG_COILS_NREGS];
static USHORT usRegDiscreteStart = REG_DISCRETE_START;
static uint8_t usRegDiscreteBuf[REG_DISCRETE_NREGS];
举报