1、NUCLEO-F030R8,芯片为 STM32F030R8。该板子 RAM 为 8KB,FLASH 为 64KB,主频最高为48MHz。
2、一台 Win10 的机器,运行 Modbus Poll。
3、一个 USB 转 232 TTL 电平的小板。请特别注意,我是使用 RS232 进行通信的。如果是 RS485 或者 RS422 需要在对收发进行分别处理。
IAR EWARM 8.22.1 + Stm32CubeMX 6.3 + HAL 1.11.3 + freeModbus 最新版
本来是想用 libModbus 3.1.6,因为我的主站用的是这个。但是尝试移植到裸奔系统的时候才发现,libModbus 3.1.6 是在 Linux 下使用的。所以只好放弃,重新回到 freeModbus。
freeModbus 是一个很小开源的 Modbus 协议栈,支持 ascii,rtu 和 tcp 模式。移植 freeModbus 需要硬件支持包括如下:
1、一个串口。
2、一个定时器。用于产生 3.5T 时间中断。
3、Modbus 相关的回调函数。
NUCLEO-F030R8 板子没有晶振,最高的时钟频率为 48 48 48MHz,我这里配置为 40 40 40MHz。其实这个频率可以任意配置,没有什么特别的要求。
我是用了 usart1,波特率什么都可以随便设置,因为在一直 freeModbus 的时候都会重新配置。只需要硬件使能串口,并打开中断即可。下面是 CubeMX 的配置截图。
下图为串口中断配置。
主要目的是让 CubeMX 自动生成对应的代码。注意串口中断的优先级设置为最高。
你也可以不配置自动生成中断代码,如果这样配置,所有串口中断代码需要自己完成。因此我就选择配置自动生成中断代码。
任意选择一个 Timer 都可以。这里我选择了 TIM6,没有什么特别原因,就是它简单。配置如图。
也是随便配置一下。同样,在移植 freeModbus 的时候,会将 TIM6 重新配置的。
下图是 TIM6 中断配置。
同样注意 TIM6 的优先级设置为 2 2 2,比 USART1 的优先级低。
你也可以不配置自动生成中断代码,如果这样配置,所有串口中断代码需要自己完成。因此我就选择配置自动生成中断代码。
这样配置完成后,生成代码即可。
我遵守 CubeMX 的方式,也就是增加了 Middlewares 目录。然后将 freeModbus-master 压缩包中的 modbus 目录拷贝过来即可。对应的目录结构和文件如下图。
根据 freeModbus 官方文档定义,移植相关代码建议放在 port 目录下。建议参考 freeModbus-master/demo/bare 目录。
在该目录下有:文件 demo.c,该文件告诉你 main() 函数应该写什么,modbus 对应的回调函数应该如何实现。子目录 port 下有 4 4 4 个文件:
port.h
portevent.c 事件相关的移植文件
portserial.c 串口相关的移植文件
porttimer.c 定时器相关的移植文件
我就是将 bare 目录的 port 直接拷贝过来。具体的目录结构参考上图。
该文件不需要什么改动,只需要增加对应的板子头文件即可。我增加了如下代码:
#include "stm32f0xx_hal.h"
这个文件我没有做任何修改。
该函数为串口使能函数。根据自己的板子对串口中断进行使能即可。
我的代码如下:
void
vMBPortSerialEnable( BOOL xRxEnable, BOOL xTxEnable )
{
/* If xRXEnable enable serial receive interrupts. If xTxENable enable
* transmitter empty interrupts.
*/
if(xRxEnable == TRUE)
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
else
__HAL_UART_DISABLE_IT(&huart1, UART_IT_RXNE);
if(xTxEnable == TRUE)
__HAL_UART_ENABLE_IT(&huart1, UART_IT_TXE);
else
__HAL_UART_DISABLE_IT(&huart1, UART_IT_TXE);
}
该函数为串口初始化函数。根据自己的板子对串口初始化即可。
我的代码如下:
BOOL
xMBPortSerialInit( UCHAR ucPORT, ULONG ulBaudRate, UCHAR ucDataBits, eMBParity eParity )
{
HAL_UART_DeInit(&huart1);//DEINIT cubeMX中的初始化配置
(void)ucPORT;
huart1.Instance = USART1;
huart1.Init.BaudRate = ulBaudRate;
huart1.Init.StopBits = UART_STOPBITS_1;
switch (eParity)//使用校验位,就需要将uart的数据位配置为9位
{
case MB_PAR_ODD:
huart1.Init.WordLength = UART_WORDLENGTH_9B;
huart1.Init.Parity = UART_PARITY_ODD;
break;
case MB_PAR_EVEN:
huart1.Init.WordLength = UART_WORDLENGTH_9B;
huart1.Init.Parity = UART_PARITY_EVEN;
break;
default:
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.Parity = UART_PARITY_NONE;
break;
}
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
return FALSE;
}
return TRUE;
}
注意:
1、我是用的是 USART1,所以对应为 huart1,参考 usart.c。
2、由于 CubeMX 会对串口进行初始化,所以要先 DeInit。
发送一个字节。
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. */
if(HAL_UART_Transmit(&huart1,(uint8_t*)&ucByte,1,1) == HAL_OK)
return TRUE;
else
return FALSE;
}
接收一个字节。
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.
*/
if(HAL_UART_Receive(&huart1,(uint8_t*)pucByte,1,1) == HAL_OK)
return TRUE;
else
return FALSE;
}
Tx Ready ISR。
void prvvUARTTxReadyISR( void )
{
pxMBFrameCBTransmitterEmpty( );
}
注意:原来这个函数是一个静态函数,需要将 static 删除。我在 USART1 中断中调用了本函数。
Rx ISR。
void prvvUARTRxISR( void )
{
pxMBFrameCBByteReceived( );
}
注意:原来这个函数是一个静态函数,需要将 static 删除。我在 USART1 中断中调用了本函数。
定时器初始化函数。
BOOL
xMBPortTimersInit( USHORT usTim1Timerout50us )
{
HAL_TIM_Base_DeInit(&htim6);
TIM_MasterConfigTypeDef sMasterConfig = {0};
/* USER CODE BEGIN TIM6_Init 1 */
/* USER CODE END TIM6_Init 1 */
htim6.Instance = TIM6;
htim6.Init.Prescaler = 4499;//50us分频,这里使用的timer PCLK频率是40Mhz
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = usTim1Timerout50us-1;//modbus 规定的TIMEOUT时间
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
return FALSE;
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
{
return FALSE;
}
return TRUE;
}
感觉这个定时器不需要特别的准确。随便给一个时间就可以了。
使能定时器中断。
inline void
vMBPortTimersEnable( )
{
/* Enable the timer with the timeout passed to xMBPortTimersInit( ) */
__HAL_TIM_CLEAR_IT(&htim6,TIM_IT_UPDATE);
__HAL_TIM_SetCounter(&htim6,0);//这里一定要清零计数器
HAL_TIM_Base_Start_IT(&htim6);
}
inline void
vMBPortTimersDisable( )
{
/* Disable any pending timers. */
HAL_TIM_Base_Stop_IT(&htim6);
__HAL_TIM_SetCounter(&htim6,0);
__HAL_TIM_CLEAR_IT(&htim6,TIM_IT_UPDATE);
}
定时器结束中断。
void prvvTIMERExpiredISR( void )
{
( void )pxMBPortCBTimerExpired( );
}
注意:原来这个函数是一个静态函数,需要将 static 删除。我在 TIM 中断中调用了本函数。
在stm32f0_xx_it.c 中。增加对应的代码。
CubeMX 生成的函数为 USART1_IRQHandler。增加的代码如下:
/* USER CODE BEGIN USART1_IRQn 0 */
uint8_t tmp;
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_PE))//奇偶校验位判断
{
HAL_UART_Receive(&huart1,&tmp,1,1);
}
else if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE)&&__HAL_UART_GET_IT_SOURCE(&huart1,UART_IT_RXNE))
{
prvvUARTRxISR();
}
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_TXE)&&__HAL_UART_GET_IT_SOURCE(&huart1,UART_IT_TXE))
{
prvvUARTTxReadyISR();
}
/* USER CODE END USART1_IRQn 0 */
以上的代码是处理串口接收数据。
CubeMX 生成的函数为 TIM6_IRQHandler。增加的代码如下:
/* USER CODE BEGIN TIM6_IRQn 1 */
prvvTIMERExpiredISR();
/* USER CODE END TIM6_IRQn 1 */
由于 freeModbus 采用了回调函数的方法来实现具体的功能。所以我们必须实现对应的协议回调函数。
这部分代码的实现,可以参考 freeModbus-master/demo 目录中任意一个实现。
必须包含 mb.h。
#include "mb.h"
主要是定义开始地址,数据缓存区之类。
#define REG_INPUT_START 0x0001U //寻址地址是从1开始的
#define REG_INPUT_NREGS 4
#define REG_HOLDING_START ( 1 )
#define REG_HOLDING_NREGS ( 32 )
/* ----------------------- Static variables ---------------------------------*/
static uint16_t usRegInputStart = REG_INPUT_START;
static uint16_t usRegInputBuf[REG_INPUT_NREGS]={0x01,0x02,0x03,0x04};//为了验证使用的初始化值
static USHORT usRegHoldingStart = REG_HOLDING_START;
static USHORT usRegHoldingBuf[REG_HOLDING_NREGS];
Read Input Register。响应功能码 0x04。
eMBErrorCode
eMBRegInputCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
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;
}
Write Holding Register。响应功能码 0x06。
eMBErrorCode
eMBRegHoldingCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode )
{
eMBErrorCode eStatus = MB_ENOERR;
int iRegIndex;
if( ( usAddress >= REG_HOLDING_START ) && ( usAddress + usNRegs <= REG_HOLDING_START + REG_HOLDING_NREGS ) )
{
iRegIndex = ( int )( usAddress - usRegHoldingStart );
switch ( eMode )
{
case MB_REG_READ:
while( usNRegs > 0 )
{
*pucRegBuffer++ = ( unsigned char )( usRegHoldingBuf[iRegIndex] >> 8 );
*pucRegBuffer++ = ( unsigned char )( usRegHoldingBuf[iRegIndex] & 0xFF );
iRegIndex++;
usNRegs--;
}
break;
case MB_REG_WRITE:
while( usNRegs > 0 )
{
usRegHoldingBuf[iRegIndex] = *pucRegBuffer++ << 8;
usRegHoldingBuf[iRegIndex] |= *pucRegBuffer++;
iRegIndex++;
usNRegs--;
}
}
}
else
{
eStatus = MB_ENOREG;
}
return eStatus;
}
Read Coils。响应功能码 0x01。
eMBErrorCode
eMBRegCoilsCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNCoils, eMBRegisterMode eMode )
{
return MB_ENOREG;
}
也就是说我暂时没有实现。
Read Discrete Inputs。响应功能码 0x02。
eMBErrorCode
eMBRegDiscreteCB( UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNDiscrete )
{
return MB_ENOREG;
}
其参考 mb.c。代码如下:
static xMBFunctionHandler xFuncHandlers[MB_FUNC_HANDLERS_MAX] = {
#if MB_FUNC_OTHER_REP_SLAVEID_ENABLED > 0
{MB_FUNC_OTHER_REPORT_SLAVEID, eMBFuncReportSlaveID},
#endif
#if MB_FUNC_READ_INPUT_ENABLED > 0
{MB_FUNC_READ_INPUT_REGISTER, eMBFuncReadInputRegister},
#endif
#if MB_FUNC_READ_HOLDING_ENABLED > 0
{MB_FUNC_READ_HOLDING_REGISTER, eMBFuncReadHoldingRegister},
#endif
#if MB_FUNC_WRITE_MULTIPLE_HOLDING_ENABLED > 0
{MB_FUNC_WRITE_MULTIPLE_REGISTERS, eMBFuncWriteMultipleHoldingRegister},
#endif
#if MB_FUNC_WRITE_HOLDING_ENABLED > 0
{MB_FUNC_WRITE_REGISTER, eMBFuncWriteHoldingRegister},
#endif
#if MB_FUNC_READWRITE_HOLDING_ENABLED > 0
{MB_FUNC_READWRITE_MULTIPLE_REGISTERS, eMBFuncReadWriteMultipleHoldingRegister},
#endif
#if MB_FUNC_READ_COILS_ENABLED > 0
{MB_FUNC_READ_COILS, eMBFuncReadCoils},
#endif
#if MB_FUNC_WRITE_COIL_ENABLED > 0
{MB_FUNC_WRITE_SINGLE_COIL, eMBFuncWriteCoil},
#endif
#if MB_FUNC_WRITE_MULTIPLE_COILS_ENABLED > 0
{MB_FUNC_WRITE_MULTIPLE_COILS, eMBFuncWriteMultipleCoils},
#endif
#if MB_FUNC_READ_DISCRETE_INPUTS_ENABLED > 0
{MB_FUNC_READ_DISCRETE_INPUTS, eMBFuncReadDiscreteInputs},
#endif
};
参考 mbproto.h。支持的功能码如下:
#define MB_FUNC_READ_COILS ( 1 )
#define MB_FUNC_READ_DISCRETE_INPUTS ( 2 )
#define MB_FUNC_WRITE_SINGLE_COIL ( 5 )
#define MB_FUNC_WRITE_MULTIPLE_COILS ( 15 )
#define MB_FUNC_READ_HOLDING_REGISTER ( 3 )
#define MB_FUNC_READ_INPUT_REGISTER ( 4 )
#define MB_FUNC_WRITE_REGISTER ( 6 )
#define MB_FUNC_WRITE_MULTIPLE_REGISTERS ( 16 )
#define MB_FUNC_READWRITE_MULTIPLE_REGISTERS ( 23 )
#define MB_FUNC_DIAG_READ_EXCEPTION ( 7 )
#define MB_FUNC_DIAG_DIAGNOSTIC ( 8 )
#define MB_FUNC_DIAG_GET_COM_EVENT_CNT ( 11 )
#define MB_FUNC_DIAG_GET_COM_EVENT_LOG ( 12 )
#define MB_FUNC_OTHER_REPORT_SLAVEID ( 17 )
static uint16_t usRegInputBuf[REG_INPUT_NREGS]={0x01,0x02,0x03,0x04};//为了验证使用的初始化值