0%

STM32HAL库移植FreeModbus协议

简述

Modbus是一个非常好用的通讯协议,经常用在串口通讯中,也可以用在网口。它既简洁又规范,尤其在工业中应用非常广泛。Modbus的程序实现也比较简单,用户可以自己实现,也可以移植开源的协议代码,比如今天要介绍的FreeModbus。

  • 硬件环境:STM32F103C8T6

  • 软件环境:STM32CubeMXv6.1.1

  • HAL库:STM32CubeF1Firmware Package V1.8.3

  • FreeModbus版本:freemodbus-v1.6

freemodbus下载地址:https://github.com/cwalter-at/freemodbus

FreeModbus文件说明

下载之后解压出来,可以看到文件夹内包含以下内容。我们需要关注的只有modbus文件夹和demo下的BARE文件夹。modbus文件夹下是协议的具体代码。Demo->BARE文件夹下是接口文件,需要用户进行移植和修改的。

123227oux88224suxk28we.png

STM32CubeMX配置

需要使能一个串口和一个定时器,定时器的功能是用于检测3.5个字符的空闲,以判断一帧数据的结束。这里以USART1和TIM4为例进行介绍。
定时器配置:

123227vg4sx8r31s1v9ds1.png

123227k78rtghtckgrjzx4.png

定时器和串口的参数任意即可,具体在程序中进行配置。打开定时器和串口的中断,且串口中断的优先级要高于定时器中断。

123227ttsgsudzux2wzlrd.png

串口和定时器中断程序我们自己编写,所以这里需要取消串口和定时器中断自动生成代码的选项。如下图:

123227fmmrdrt04t44d4to.png

FreeModbus移植

生成代码后,将下载的FreeModbus的源码复制到工程目录中,在Keil工程中新建两个Group组:FreeModbus和Port。添加modbus文件夹下的全部文件到FreeModbus组。将Demo->BARE->Port文件夹下的文件添加到Port组。同时新建一个Port.c文件也添加到Port组。
另外,别忘了在工程中添加包含路径,否则编译出错。

123227fk80ybjc09ake1ke.png

下面开始程序移植,首先是portserial.c文件,该文件是串口的接口文件,包含以下函数:串口使能、串口初始化、发送一个字节、接收一个字节等。我们需要自己实现完成这些函数的内容。完成后的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
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 )
{
/* Put a byte in the UARTs transmit buffer. This function is called
* by the protocol stack if pxMBFrameCBTransmitterEmpty( ) has been
* called. */
USART1->DR = ucByte;
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.
*/
*pucByte = (USART1->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 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计数一次,具体计数周期与波特率有关。完成后的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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); //关闭定时器
}

/* 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( );
}

//定时器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文件。完成后的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
//输入寄存器
#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];
/* ----------------------- Start implementation -----------------------------*/
//int
//main( void )
//{
// eMBErrorCode eStatus;

// eStatus = eMBInit( MB_RTU, 0x0A, 0, 38400, MB_PAR_EVEN );

// /* Enable the Modbus Protocol Stack. */
// eStatus = eMBEnable( );

// for( ;; )
// {
// ( void )eMBPoll( );

// /* Here we simply count the number of poll cycles. */
// usRegInputBuf[0]++;
// }
//}
/****************************************************************************
* 名 称:eMBRegInputCB
* 功 能:读取输入寄存器,对应功能码是 04 eMBFuncReadInputRegister
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
* usAddress: 寄存器地址
* usNRegs: 要读取的寄存器个数
* 出口参数:
* 注 意:上位机发来的 帧格式是: SlaveAddr(1 Byte)+FuncCode(1 Byte)
* +StartAddrHiByte(1 Byte)+StartAddrLoByte(1 Byte)
* +LenAddrHiByte(1 Byte)+LenAddrLoByte(1 Byte)+
* +CRCAddrHiByte(1 Byte)+CRCAddrLoByte(1 Byte)
* 3 区
****************************************************************************/
eMBErrorCode
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
usAddress = usAddress - 1;

if( ( usAddress >= REG_INPUT_START ) && ( usAddress + usNRegs <= REG_INPUT_START + REG_INPUT_NREGS ) )
{
iRegIndex = ( int )( usAddress - usRegInputStart );
while( usNRegs > 0 )
{
*pucRegBuffer++ = ( unsigned char )( usRegInputBuf[iRegIndex] >> 8 );
*pucRegBuffer++ = ( unsigned char )( usRegInputBuf[iRegIndex] & 0xFF );
iRegIndex++;
usNRegs--;
}
}
else
{
eStatus = MB_ENOREG;
}

return eStatus;
}
/****************************************************************************
* 名 称:eMBRegHoldingCB
* 功 能:对应功能码有:06 写保持寄存器 eMBFuncWriteHoldingRegister
* 16 写多个保持寄存器 eMBFuncWriteMultipleHoldingRegister
* 03 读保持寄存器 eMBFuncReadHoldingRegister
* 23 读写多个保持寄存器 eMBFuncReadWriteMultipleHoldingRegister
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
* usAddress: 寄存器地址
* usNRegs: 要读写的寄存器个数
* eMode: 功能码
* 出口参数:
* 注 意:4 区
****************************************************************************/
eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
usAddress = usAddress - 1;

if((usAddress >= REG_HOLD_START) && ((usAddress+usNRegs) <= (REG_HOLD_START + REG_HOLD_NREGS)))
{
iRegIndex = (int)(usAddress - usRegHoldStart);
switch(eMode)
{
case MB_REG_READ://读寄存器
while(usNRegs > 0)
{
*pucRegBuffer++ = (uint8_t)(usRegHoldBuf[iRegIndex] >> 8);
*pucRegBuffer++ = (uint8_t)(usRegHoldBuf[iRegIndex] & 0xFF);
iRegIndex++;
usNRegs--;
}
break;
case MB_REG_WRITE://写寄存器
while(usNRegs > 0)
{
usRegHoldBuf[iRegIndex] = *pucRegBuffer++ << 8;
usRegHoldBuf[iRegIndex] |= *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
}
}
else//错误
{
eStatus = MB_ENOREG;
}

return eStatus;
}

/****************************************************************************
* 名 称:eMBRegCoilsCB
* 功 能:对应功能码有:01 读线圈 eMBFuncReadCoils
* 05 写线圈 eMBFuncWriteCoil
* 15 写多个线圈 eMBFuncWriteMultipleCoils
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
* usAddress: 线圈地址
* usNCoils: 要读写的线圈个数
* eMode: 功能码
* 出口参数:
* 注 意:如继电器
* 0 区
****************************************************************************/
eMBErrorCode
eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode )
{
eMBErrorCode eStatus = MB_ENOERR;
USHORT iRegIndex;
USHORT usCoilGroups = ((usNCoils - 1) / 8 + 1);
UCHAR ucStatus = 0;
UCHAR ucBits = 0;
UCHAR ucDisp = 0;
usAddress = usAddress - 1;

if((usAddress >= REG_COILS_START) && ((usAddress + usNCoils) <= (REG_COILS_START + REG_COILS_NREGS)))
{
iRegIndex = (int)(usAddress - usRegCoilsStart);
switch(eMode)
{
case MB_REG_READ://读线圈
while(usCoilGroups--)
{
ucDisp = 0;
ucBits = 8;
while((usNCoils--) != 0 && (ucBits--) != 0)
{
ucStatus |= (usRegCoilsBuf[iRegIndex++] << (ucDisp++));
}
*pucRegBuffer++ = ucStatus;
}
break;
case MB_REG_WRITE://写线圈
while(usCoilGroups--)
{
ucStatus = *pucRegBuffer++;
ucBits = 8;
while((usNCoils--) != 0 && (ucBits--) != 0)
{
usRegCoilsBuf[iRegIndex++] = ucStatus & 0X01;
ucStatus >>= 1;
}
}
}
}
else//错误
{
eStatus = MB_ENOREG;
}

return eStatus;
}
/****************************************************************************
* 名 称:eMBRegDiscreteCB
* 功 能:读取离散寄存器,对应功能码有:02 读离散寄存器 eMBFuncReadDiscreteInputs
* 入口参数:pucRegBuffer: 数据缓存区,用于响应主机
* usAddress: 寄存器地址
* usNDiscrete: 要读取的寄存器个数
* 出口参数:
* 注 意:1 区
****************************************************************************/
eMBErrorCode
eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
eMBErrorCode eStatus = MB_ENOERR;
USHORT iRegIndex;
USHORT usDiscreteGroups = ((usNDiscrete - 1) / 8 + 1);
UCHAR ucStatus = 0;
UCHAR ucBits = 0;
UCHAR ucDisp = 0;
usAddress = usAddress - 1;

if((usAddress >= REG_DISCRETE_START) && ((usAddress + usNDiscrete) <= (REG_DISCRETE_START + REG_DISCRETE_NREGS)))
{
iRegIndex = (int)(usAddress - usRegDiscreteStart);

while(usDiscreteGroups--)
{
ucDisp = 0;
ucBits = 8;
while((usNDiscrete--) != 0 && (ucBits--) != 0)
{ if(usRegDiscreteBuf[iRegIndex])
{
ucStatus |= (1 << ucDisp);
}
ucDisp++;
}
*pucRegBuffer++ = ucStatus;
}
}
else//错误
{
eStatus = MB_ENOREG;
}

return eStatus;
}

最后在主程序中初始化并调用相关函数即可:

1
2
3
4
5
6
7
eMBInit(MB_RTU, 0x01, 0, 9600, MB_PAR_NONE); // 初始化modbus为RTU方式,地址0x01, 波特率9600,无校验
eMBEnable(); // 使能modbus协议栈

while (1)
{
eMBPoll();
}

总结

FreeModbus实现了Modbus协议的全部功能,移植和使用起来也比较简单。唯一不足的是只有从机协议是开源的,而主机协议是收费的。FreeModbus_Slave-Master-RTT-STM32这个项目添加了主机模式,后面尝试移植一下

转载:http://www.openedv.com/forum.php?mod=viewthread&tid=323509&highlight=MODBUS