个人笔记,供个人查阅。
目录
FreeModbus是奥地利人写的Modbus协议,主要针对嵌入式应用的免费通用Modbus协议的移植。modbus通信协议栈包括两层:应用层协议(定义了数据模型和功能)和网络层。
FreeModbus提供了RTU/ASCII传输模式和TCP协议支持,遵循BSD许可证(意味着用户可以将FreeModbus应用于商业环境中)。
FreeModbus软硬件需求
FreeModbus协议对硬件的需求非常少,基本上任何具有串行接口,并且有一些能够容纳modbus数据帧的RAM微控制器都足够了。
拥有一个异步串行接口,能够支持接收缓冲区满和发送缓冲区空中断。
拥有一个能够产生RTU传输所需要的T3.5字符超时定时器的时钟。
对于软件部分,仅仅需要一个简单的事件队列。
物理层接口
在物理层,用户只需完成串行接口和T3.5字符超时定时器的配置即可。具体应修改portserial.c和porttimer.c。
portserial.c
void vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
if(xRxEnable == TRUE)
HAL_UART_Receive_IT(&huart1, (uint8_t *)&rxBuffer, 1);
else
HAL_UART_AbortReceive_IT(&huart1);
if(xTxEnable == TRUE)
prvvUARTTxReadyISR();
}
函数功能:设置串口状态。
当xRxEnable为真时,应使能串口接收和接收中断。在RS485通讯系统中,还要注意将RS485接口芯片设置为接收使能状态。
当xTxEnable为真时,应使能串口发送和发送中断。在RS485通讯系统中,还要注意将RS485接口芯片设置为发送使能状态。
void vMBPortClose( void )
{
HAL_UART_DeInit(&huart1);
}
函数功能:关闭modbus通讯端口。具体的,应在此函数中关闭通讯接口的发送使能及接收使能。
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity);
函数功能:初始化串口通讯接口。
若使用RTU模式,则ucDataBits=8;若使用ASCII模式,则ucDataBits=7。
BOOL
xMBPortSerialPutByte( CHAR ucByte )
{
txBuffer = ucByte;
HAL_UART_Transmit_IT(&huart1, (uint8_t *)&txBuffer, 1);
return TRUE;
}
函数功能:通讯端口发送一字节数据。
注意,由于使用的是中断发送,故只需要将数据放到发送寄存器即可。
BOOL
xMBPortSerialGetByte( CHAR * pucByte )
{
*pucByte = rxBuffer;
return TRUE;
}
函数功能:通讯端口接收一字节数据。
注意,由于使用的是中断接收,故只需要将接收寄存器的值放到*pucByte即可。
void prvvUARTTxReadyISR( void )
{
pxMBFrameCBTransmitterEmpty( );
}
函数功能:发送中断函数。此函数无需修改。只需在用户的发送中断函数中调用此函数即可。同时,用户应该在调用此函数后,清除发送中断标志位。
void prvvUARTRxISR( void )
{
HAL_UART_Receive_IT(&huart1, (uint8_t *)&rxBuffer, 1);
pxMBFrameCBByteReceived( );
}
函数功能:接收中断函数。此函数无需修改。只需在用户的接收中断函数中调用此函数即可。同时,用户应该在调用此函数后,清除接收中断标志位。
porttimer.c
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim7.Init.Period = usTim1Timerout50us - 1;
if (HAL_TIM_Base_Init(&htim7) != HAL_OK)
{
return FALSE;
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim7, &sMasterConfig) != HAL_OK)
{
return FALSE;
}
return TRUE;
}
函数功能:初始化超时定时器。usTim1Timerout50us为50us的个数。
用户应根据所使用的硬件初始化超时定时器,使之能产生中断时间为usTim1Timerout50us*50us的中断。
inline void
vMBPortTimersEnable( )
{
__HAL_TIM_CLEAR_IT(&htim7, TIM_IT_UPDATE);
__HAL_TIM_SetCounter(&htim7, 0);//这里一定要清零计数器
HAL_TIM_Base_Start_IT(&htim7);
}
函数功能:使能超时定时器。用户需要在此函数中清除中断标志位、清零定时器计数值,并重新使能定时器中断。
inline void
vMBPortTimersDisable( )
{
HAL_TIM_Base_Stop_IT(&htim7);
}
函数功能:关闭超时定时器。用户需要在此函数中清零定时器计数值,并关闭定时器中断。
void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired( );
}
函数功能:定时器中断函数。此函数无需修改。只需在用户的定时器中断函数中调用此函数即可。同时,用户应该在调用此函数后,清除定时器中断标志位。
应用层回调
在应用层,用户需要定义所需要使用的寄存器,并修改对应的回调函数。回调函数有如下几个:
eMBErrorCode eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs );
eMBErrorCode eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode );
eMBErrorCode eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode );
eMBErrorCode eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete );
主函数
用户只需要在主函数中调用协议初始化代码和消息处理函数即可。
eMBErrorCode
eMBInit( eMBMode eMode, UCHAR ucSlaveAddress, UCHAR ucPort, ULONG ulBaudRate, eMBParity eParity, eMBStopBit eStopBit, eMBDataBit eDataBit );
协议初始化函数。
eMBErrorCode eMBSetSlaveID( UCHAR ucSlaveID, BOOL xIsRunning,
UCHAR const *pucAdditional, USHORT usAdditionalLen );
从机ID设置函数。注意,ID表示的是设备的类型,不同于ucSlaveAddress(从机地址)。对同一通讯系统中,可以有相同的ucSlaveID,但不可以有相同的ucSlaveAddress。ucSlaveID为一字节的设备ID号;xIsRunning为设备的运行状态,0xFF为运行,0x00为停止;* pucAdditional为设备的附加描述,根据需要添加;usAdditionalLen为附加描述的长度(按字节计算)。此函数不是必须调用的。但当一个Modbus通讯系统中有不同种设备时,应调用此函数添加对应设备的描述。
eMBErrorCode eMBPoll( void );
轮训事件查询处理函数。用户需要在主循环中调用此函数。对于使用操作系统的程序,应单独创建一个任务,使操作系统能周期调用此函数。
运行流程
FreeModbus是基于消息队列的协议。协议通过检测相应的消息来完成相应功能。协议栈的初始化和运行流程如下:
首先调用mb.c的eMBInit()完成物理层设备的初始化,主要包括:串口初始化(设定波特率、数据位、校验方式)和定时器初始化(设定T35定时所需要的定时器常数)。
调用mbfuncother.c的eMBSetSlaveID()指定设备ID。(非必需流程)
调用mb.c的eMBEnable()使能协议栈,主要包括:pvMBFrameStartCur协议栈开始,将eRcvState设为STATE_RX_INIT状态,调用vMBPortSerialEnable()使能接收,调用vMBPortTimersEnable()使能超时定时器。经过T35时间后,发生第一次超时中断,在中断中,向协议栈发送消息EV_READY,并调用vMBPortTimersDisable()关闭超时定时器,同时将eRcvState设为STATE_RX_IDLE。此时,协议栈可以接收串口数据。注意,此处首先启用一次超时定时器是因为初始化完成时,串口有可能已经有数据,因为无法判断第一个数据请求的开始,故等待T35,接收下一帧请求。
此时,主函数调用eMBPoll()检测事件。(当串口接收到数据后重复以下内容)
若发生串口接收中断,且eRcvState为STATE_RX_IDLE(上面已将eRcvState设为STATE_RX_IDLE),则向接收缓存中存入接收到的字符,同时将eRcvState设为STATE_RX_RCV状态,并清零超时定时器。在下一个数据到来时,不断将数据存入接收缓存,并清零超时定时器。
如果没有接收完成,则不可能发生超时中断。发生超时中断,说明T35时间内未收到新的串口数据,根据Modbus协议的规定,这指示着一帧请求数据接收完成。在中断中,向协议栈发生消息EV_FRAME_RECEIVED,等待协议栈处理此消息。
主函数调用eMBPoll()检测到事件EV_FRAME_RECEIVED后,调用peMBFrameReceiveCur()作简单判断请求帧数据,并向协议栈发送消息EV_EXECUTE。
主函数调用eMBPoll()检测到事件EV_EXECUTE后,根据相应的请求代码查找处理该功能的函数指针来处理该功能。若不是广播消息,则调用peMBFrameSendCur()发送回复消息,在此函数中,只是把要回复的数据复制到了串口缓存中,同时将eSndState设为STATE_TX_XMIT,并通过调用vMBPortSerialEnable()使能发送中断。注意,发送中断使能后,由于串口发送寄存器本来就是空的,故在使能后将进入发送中断中。
在发送中断中,且eSndState为STATE_TX_XMIT(上面已将eSndState设为STATE_TX_XMIT),则将串口缓存中的数据发送出去,同时不断对发送字符个数统计。当发送完成后,向协议栈发送消息EV_FRAME_SENT。
主函数调用eMBPoll()检测到事件EV_FRAME_SENT后,不处理此消息。
功能码使用
Function Code:01,读线圈输出
发送 | 1从机地址 | 1功能码 | 2起始地址 | 2线圈数量 | 2CRC |
返回 | 1从机地址 | 1功能码 | 1字节数 | 1线圈状态 | 2CRC |
主机发送:01 01 00 00 00 01 FD CA
从机返回:01 01 01 00 51 88
Function Code:02,读离散输入
发送 | 1从机地址 | 1功能码 | 2起始地址 | 2线圈数量 | 2CRC |
返回 | 1从机地址 | 1功能码 | 1字节数 | 1状态 | 2CRC |
主机发送:01 02 00 00 00 01 B9 CA
从机返回:01 02 01 00 A1 88
Function Code:03,读保持寄存器
发送 | 1从机地址 | 1功能码 | 2起始地址 | 2线圈数量 | 2CRC |
返回 | 1从机地址 | 1功能码 | 1字节数 | n寄存器值 | 2CRC |
主机发送:01 03 00 00 00 01 84 0A
从机返回:01 03 02 00 00 B8 44
Function Code:04,读输入寄存器
发送 | 1从机地址 | 1功能码 | 2起始地址 | 2线圈数量 | 2CRC |
返回 | 1从机地址 | 1功能码 | 1字节数 | n寄存器值 | 2CRC |
主机发送:01 04 00 00 00 01 31 CA
从机返回:01 04 02 00 00 B9 30
Function Code:05,写单个线圈
线圈状态:0xFF00-ON,0x0000-OFF
发送 | 1从机地址 | 1功能码 | 2起始地址 | 2线圈状态 | 2CRC |
返回 | 1从机地址 | 1功能码 | 2起始地址 | 2线圈状态 | 2CRC |
主机发送:01 05 00 00 FF 00 C4 F7
从机返回:01 05 00 00 FF 00 C4 F7
Function Code:06,写单个寄存器
发送 | 1从机地址 | 1功能码 | 2起始地址 | 2寄存器数据 | 2CRC |
返回 | 1从机地址 | 1功能码 | 2起始地址 | 2寄存器数据 | 2CRC |
主机发送:01 06 00 00 00 00 89 CA
从机返回:01 06 00 00 00 00 89 CA
Function Code:0F,写多个线圈
发送 | 1从机地址 | 1功能码 | 2起始地址 | 2线圈数量 | 1后跟的字节数 | n线圈状态 | 2CRC |
返回 | 1从机地址 | 1功能码 | 2起始地址 | 2线圈数量 | - | - | 2CRC |
主机发送:01 0F 00 00 00 0A 02 0F F0 E0 8C
从机返回:01 0F 00 00 00 0A D5 CC
Function Code:10,写多个寄存器
发送 | 1从机地址 | 1功能码 | 2起始地址 | 2寄存器数量 | 1后跟的字节数 | n寄存器数据 | 2CRC |
返回 | 1从机地址 | 1功能码 | 2起始地址 | 2寄存器数量 | - | - | 2CRC |
写寄存器编号0开始的10个寄存器:0-3寄存器写1,4-9寄存器写0。
主机发送:01 10 00 00 00 0A 14 00 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 DD 92
从机返回:01 10 00 00 00 0A 40 0E