• 【stm32】FreeModbus 介绍 + 移植stm32f103 HAl库


    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个,
    
    • 1
    • 2

    在这里插入图片描述
    Demo中主要放了不同平台下的移植文件,以bare文件夹为例

    BARE
    	| - port 
    	|    | - portevent.c
    	|    | - portserial.c
    	|    | - porttimer.c
    	| - demo.c
    	| - Makefile
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这些就是移植层需要我们适配的接口了。其中 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;											//默认为接收模式		
    }
    
    • 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
    #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数据可以发松
        }
    }
    
    
    • 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

    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个字符等待时间
        }
    }
    
    • 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

    一个要注意的点就是记得把定时器的时钟 和 中断打开
    网上一些移植程序这里没写,应该是用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);          
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    比较奇怪的一点是,这三句配置我之前没写在HAL_TIM_Base_MspInit里,直接在init函数里调用的,结果中断一直不正常。
    后来放在里这个回调里才正常。

    4. 功能验证

    这里使用的是armfly的Modbus调试助手,界面比较简单。
    尝试使用 03指令,04指令去读取,用06指令写入后再用03指令读取
    可以读取到我们写在数组里的参数。
    在这里插入图片描述

    5. 参考链接

  • 相关阅读:
    测试应届生是去自研小公司好还是外包公司好?
    Vue官方文档(38):全局自定义指令
    浅谈综合管廊智慧运维管理平台应用研究
    HTML5和HTML的区别
    【Docker】ubuntu20.04 X86机器搭建NVIDIA ARM64 TX2的Docker镜像
    Apache ShardingSphere(二) 基本使用
    牛皮了!阿里面试官终于分享出了 2022 年最新的 java 面试题及答案
    港陆证券:电子竞技传来重磅消息!概念股上半年业绩普增
    园林绿化资质怎么办理,新办园林绿化资质资产要求解读
    第5章 Java 高级特性
  • 原文地址:https://blog.csdn.net/tao475824827/article/details/126188867