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个,

FreeModbus应用系列之一 freemodbus stm32_IT


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 中进行适配。
主要是实现:

  1. 串口初始化
  2. 发一个字节
  3. 收一个字节
  4. 中断实现,直接写中断函数或者用回调函数

代码如下:

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指令读取

可以读取到我们写在数组里的参数。

FreeModbus应用系列之一 freemodbus stm32_串口_02

5. 参考链接