精华内容
下载资源
问答
  • 51单片机modbus实例

    2018-11-16 21:31:20
    51单片机modbus实例,内部提供了比较详细的代码,并且方便大家移植工程
  • 51单片机MODBUS RTU通信实例rar,51单片机MODBUS RTU通信实例
  • 51单片机MODBUS主站程序

    热门讨论 2009-11-24 20:08:04
    51单片机作为主站的MODBUS通信协议,可实现读写从站寄存器的值
  • 在STM8S003单片机最小系统上实现modbus通信协议,单片机做为从机,接收主机发送的指令,实现modbus简单通信,仅支持读多个保持寄存器(03),写单个保持寄存器(06),写多个保持寄存器(16),这三个指令。
  • STM32F103单片机modbus通信示例

    千次阅读 多人点赞 2020-10-16 11:31:45
    前两天在研究STM32F103单片机的串口空闲中断时,突然想起来Modbus通信非常适合用空闲中断来处理。先看看Modbus RTU模式下的通信规范。 可以看到Modbus RTU通信模式下,数据的开始和结束是由空闲字符间隔时间来...

           前两天在研究STM32F103单片机的串口空闲中断时,突然想起来Modbus通信非常适合用空闲中断来处理。先看看Modbus RTU模式下的通信规范。

      

           可以看到Modbus RTU通信模式下,数据的开始和结束是由空闲字符间隔时间来区分的,而STM32F103单片机自带串口空闲模式检测。

            在通常情况下Modbus通信一帧数据的检测可以用时间判断,不停的去读取接收数据的长度是否发送变化,如果在一定时间内,接收数据的长度没有发生变化,就认为一帧数据结束完毕。具体实现可参考 STM8学习笔记---Modbus通信协议简单移植

            通过时间来判断一帧数据是否接收完成,会导致单片机不停的进入中断。而STM32F103单片机自带了空闲数据检测功能,这个功能是由单片机内部的硬件完成的,不需要程序去处理,这样单片机的就有机会去执行更多了任务了。为了让单片机在处理Modbus RTU通信时更简单快速,可以用 串口DMA加上空闲中断的方式来实现。DMA+空闲中断的实现方法具体参考 STM32单片机串口空闲中断+DMA接收不定长数据

            下面直接用代码说明具体的实现过程,首先初始化串口。

    #define UART2_DMA  1																				//使用串口2  DMA传输
    #define  CHECK_NONE_ONE_STOP    1 //无校验位  1个停止位  1有效  0 无效 
    #define  CHECK_NONE_TWO_STOP    0 //无校验位  2个停止位  1有效  0 无效 
    #define  CHECK_EVEN    0          //偶数校验   1有效  0 无效 
    #define  CHECK_ODD     0          //奇数校验   1有效  0 无效 
    
    
    void  uart2_init( u16 baud )
    {
        GPIO_InitTypeDef GPIO_InitStructure;
        USART_InitTypeDef USART_InitStructure;
        NVIC_InitTypeDef  NVIC_InitStructure;
    
        RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
        RCC_APB1PeriphClockCmd( RCC_APB1Periph_USART2, ENABLE );
    
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;												 //推挽复用模式
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init( GPIOA, &GPIO_InitStructure );
    
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;									 //浮空输入模式
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init( GPIOA, &GPIO_InitStructure );
    
        NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    
        NVIC_Init( &NVIC_InitStructure );
    
        USART_InitStructure.USART_BaudRate = baud;
        USART_InitStructure.USART_WordLength = USART_WordLength_8b;
        USART_InitStructure.USART_StopBits = USART_StopBits_1;
        USART_InitStructure.USART_Parity = USART_Parity_No;
    		
    #if(CHECK_EVEN == 1) 																											 //如果定义了偶校验  数据位长度要改为9位
        USART_InitStructure.USART_WordLength = USART_WordLength_9b;
        USART_InitStructure.USART_Parity = USART_Parity_Even;
    #endif
    
    #if(CHECK_ODD == 1) 																											 //如果定义了奇校验  数据位长度要改为9位
        USART_InitStructure.USART_WordLength = USART_WordLength_9b;
        USART_InitStructure.USART_Parity = USART_Parity_Odd;
    #endif
    
    #if(CHECK_NONE_ONE_STOP==1)																							   //停止位为 一位
        USART_InitStructure.USART_StopBits = USART_StopBits_1;
    #endif
    
    #if(CHECK_NONE_TWO_STOP==1)																								 //停止位为 两位																	
        USART_InitStructure.USART_StopBits = USART_StopBits_2;
    #endif		
    		
    		
        USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
        USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    
        USART_Init( USART2, &USART_InitStructure );
    		
    #if (UART2_DMA == 1)
        USART_ITConfig( USART2, USART_IT_IDLE, ENABLE );											 //使能串口空闲中断
        USART_DMACmd( USART2, USART_DMAReq_Rx, ENABLE );											 //使能串口2 DMA接收
        uartDMA_Init();																												 //初始化 DMA
    #else
        USART_ITConfig( USART2, USART_IT_RXNE, ENABLE );			  							 //使能串口RXNE接收中断
    #endif
        USART_Cmd( USART2, ENABLE );																					 //使能串口2
    		
    //RXNE中断和IDLE中断的区别?
    //当接收到1个字节,就会产生RXNE中断,当接收到一帧数据,就会产生IDLE中断。比如给单片机一次性发送了8个字节,就会产生8次RXNE中断,1次IDLE中断。
    }

            在Modbus通信时,需要对每个字节的数据进行校验,所以这里使用宏定义来初始化串口数据使用的是那种校验方式。如果需要偶校验就将CHECK_EVEN 设置为1,如果需要奇校验就将CHECK_ODD设置为1,如果不需要校验位,就将这两个都设置为0。在串口初始化的时候,需要开启空闲中断和DMA功能。

            下来初始化 串口2的DMA功能

    void uartDMA_Init( void )
    {
        DMA_InitTypeDef  DMA_IniStructure;
    
        RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE ); 									 //使能DMA时钟
    
        DMA_DeInit( DMA1_Channel6 );																			     //DMA1通道6对应 USART2_RX
        DMA_IniStructure.DMA_PeripheralBaseAddr = ( u32 )&USART2->DR;					 //DMA外设usart基地址
        DMA_IniStructure.DMA_MemoryBaseAddr = ( u32 )dma_rec_buff; 						 //DMA内存基地址
        DMA_IniStructure.DMA_DIR = DMA_DIR_PeripheralSRC;											 //数据传输方向,从外设读取发送到内存
        DMA_IniStructure.DMA_BufferSize = DMA_REC_LEN;												 //DMA通道的DMA缓存的大小  也就是 DMA一次传输的字节数
        DMA_IniStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;				 //外设地址寄存器不变
        DMA_IniStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;								 //数据缓冲区地址递增
        DMA_IniStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设以字节为单位搬运
        DMA_IniStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; 				 //数据缓冲区以字节为单位搬入
        DMA_IniStructure.DMA_Mode = DMA_Mode_Normal;													 //工作在正常缓存模式
        DMA_IniStructure.DMA_Priority = DMA_Priority_Medium;									 //DMA通道 x拥有中优先级
        DMA_IniStructure.DMA_M2M = DMA_M2M_Disable;														 //DMA通道x没有设置为内存到内存传输
    
        DMA_Init( DMA1_Channel6, &DMA_IniStructure );
        DMA_Cmd( DMA1_Channel6, ENABLE );
    }

            串口2的接收对应的是DMA的通道6,所以DMA的通道选择通道6,然后将串口2的数据寄存器作为外设基地址,将数组dma_rec_buff作为DMA的内存地址。外设地址不自增,内存地址自增。这样串口2每接收一个字节的数据,DMA就将接收到的数据存入数组dma_rec_buff中,同时指向数组的地址增加一位,这样串口2接收到的数据就会被一直存放在数组dma_rec_buff中。

            这里要注意一点,数组的缓存区要大于串口2接收到的最长数据个数,否则如果接收到串口数据比较长,而数组比较小,数组中的数据就会被覆盖,导致接收到的数据错误。

         接下来就是串口中断的实现了

    void USART2_IRQHandler( void )
    {
        u8 tem = 0;
    #if (UART2_DMA == 1)			//如果使能了 UART2_DMA 则使用串口空闲中断和 DMA功能接收数据
    	if( USART_GetITStatus( USART2, USART_IT_IDLE ) != RESET )							 			  //空闲中断  一帧数据发送完成
        {
            USART_ReceiveData( USART2 );																			 			//读取数据注意:这句必须要,否则不能够清除空闲中断标志位。
            DMA_Cmd( DMA1_Channel6, DISABLE );																 			//关闭 DMA 防止后续数据干扰
            uart2_rec_cnt = DMA_REC_LEN - DMA_GetCurrDataCounter( DMA1_Channel6 );  //DMA接收缓冲区数据长度减去当前 DMA传输通道中剩余单元数量就是已经接收到的数据数量
           
            copy_data( dma_rec_buff, uart2_rec_cnt );														    //备份数据
            receiveOK_flag = 1;																											//置位数据接收完成标志位
            USART_ClearITPendingBit( USART2, USART_IT_IDLE );									 			//清除空闲中断标志位
            myDMA_Enable( DMA1_Channel6 );																		 			//重新恢复 DMA等待下一次接收
        }
    #else										//如果未使能 UART2_DMA 则通过常规方式接收数据  每接收到一个字节就会进入一次中断
        if( USART_GetITStatus( USART2, USART_IT_RXNE ) != RESET )						 //接收中断
        {
            tem = USART_ReceiveData( USART2 );
            USART_SendData( USART2, tem );
        }
    #endif
    }

            串口进入空闲中断后,说明一帧数据已经接收完成。关闭DMA通道,开始处理数据。获取DMA中接收到的数据长度,然后将数据拷贝到备份数组中,然后置接收完成标志位。开启 DMA进行下一次数据接收。

            在主程序中如果检测到了数据接收完成标志时,就开始处理接收到的数据。这样就可以将串口接收数据和处理数据分开,在处理上一笔数据的时候,就可以继续接收下一笔数据。提高了系统的实时性。

            这里如果直接使用DMA接收数组去处理数据的话,有可能会出现,旧的一笔数据未处理完成时,DMA接收到了新的一笔数据,然后存储到了 DMA缓存数组中。这样在处理数据过程中,旧的数据就会被新的数据覆盖掉,导致系统异常。如果Modbus的通信速率没有那么快,在下一次数据来临之前,系统完全有时间处理完上一笔数据,那么直接使用DMA的缓存数据也是可以的。

           不过上面这种将数据拷贝倒其他数据中处理的方法也是有风险的。比如第一笔数据还没处理完成,第二笔数据已经接收完成,此时进入串口空闲中断,系统依然会将第二笔数据拷贝到备份数组中去。这样备份数组中的数据依然会被覆盖。但是这种情况如果出现的话,就说明接收数据的频率高于系统处理数据的频率,系统本身就存在风险,需要重新设计。

         接收数据完成后,就需要在主程序中调用数据处理函数。

    int main(void)
    {
        u8  j = 0;
    	
    	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    	
        delay_init();       //延时函数初始化
        LED_Init();         //初始化与LED连接的硬件接口
        uart2_init(9600);
        while(1)
        {
          
            if( receiveOK_flag )				//一帧数据接收完成后开始处理数据
            {
                receiveOK_flag = 0;
                DisposeReceive();				//处理modbus通信
            }
    			
            j++;
            if(j > 50)
            {
                j = 0;
                LED = !LED;
            }
            delay_ms(10);
        }
    }

    最后是Modbus的协议解析

    void DisposeReceive( void )
    {
        u16 CRC16 = 0, CRC16Temp = 0;
        if( data_backup[0] == SlaveID )                                 					//地址等于本机地址 地址范围:1 - 32
        {
            CRC16 = App_Tab_Get_CRC16( data_backup, dataLen_backup - 2 );  				//CRC校验 低字节在前 高字节在后 高字节为报文最后一个字节
            CRC16Temp = ( ( u16 )( data_backup[dataLen_backup - 1] << 8 ) | data_backup[dataLen_backup - 2] );
            if( CRC16 != CRC16Temp )
            {
                err = 4;                                               						//CRC校验错误
            }
            StartRegAddr = ( u16 )( data_backup[2] << 8 ) | data_backup[3];
            if( StartRegAddr > (HoldRegStartAddr + HoldRegCount - 1) ) 
            {
                err = 2;                                               						//起始地址不在规定范围内 00 - 07    1 - 8号通道
            }
            if( err == 0 )
            {
                switch( data_backup[1] )                                					//功能码
                {
                    case 3:                                            						//读多个寄存器
                    {
                        Modbus_03_Slave();
                        break;
                    }
                    case 6:                                            						//写单个寄存器
                    {
                        Modbus_06_Slave();
                        break;
                    }
                    case 16:                                           						//写多个寄存器
                    {
                        Modbus_16_Slave();
                        break;
                    }
                    default:
                    {
                        err = 1;                                       						//不支持该功能码
                        break;
                    }
                }
            }
            if( err > 0 )
            {
                SendBuf[0] = data_backup[0];
                SendBuf[1] = data_backup[1] | 0x80;
                SendBuf[2] = err;                                      						//发送错误代码
                CRC16Temp = App_Tab_Get_CRC16( SendBuf, 3 );           						//计算CRC校验值
                SendBuf[3] = CRC16Temp & 0xFF;                         						//CRC低位
                SendBuf[4] = ( CRC16Temp >> 8 );                       						//CRC高位
                uart2_Send( SendBuf, 5 );					
                err = 0;                                               						//发送完数据后清除错误标志
            }
        }
    }
    
    /*
    函数功能:读保持寄存器  03
    主站请求报文:      0x01 0x03   0x0000  0x0001  0x840A     读从0开始的1个保持寄存器
    从站正常响应报文:  0x01 0x03   0x02    0x09C4  0xBF87     读到的2字节数据为 0x09C4
    */
    void Modbus_03_Slave( void )
    {
        u16 RegNum = 0;
        u16 CRC16Temp = 0;
        u8 i = 0;
        RegNum = ( u16 )( data_backup[4] << 8 ) | data_backup[5];        					//获取寄存器数量
        if( ( StartRegAddr + RegNum ) <= (HoldRegStartAddr + HoldRegCount) )      //寄存器地址+寄存器数量 在规定范围内 <=8
        {
            SendBuf[0] = data_backup[0];																					//地址	
            SendBuf[1] = data_backup[1];																					//功能码
            SendBuf[2] = RegNum * 2;																							//返回字节数量
            for( i = 0; i < RegNum; i++ )                             						//循环读取保持寄存器内的值
            {
                SendBuf[3 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2];
                SendBuf[4 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2 + 1];
            }
            CRC16Temp = App_Tab_Get_CRC16( SendBuf, RegNum * 2 + 3 );  						//获取CRC校验值
            SendBuf[RegNum * 2 + 3] = CRC16Temp & 0xFF;                						//CRC低位
            SendBuf[RegNum * 2 + 4] = ( CRC16Temp >> 8 );              						//CRC高位
            uart2_Send( SendBuf, RegNum * 2 + 5 );
        }
        else
        {
            err = 3;                                                   						//寄存器数量不在规定范围内
        }
    }
    /*
    函数功能:写单个保持寄存器 06
    主站请求报文:      0x01 0x06    0x0000  0x1388    0x849C   写0号寄存器的值为0x1388
    从站正常响应报文:  0x01 0x06    0x0000  0x1388    0x849C    0号寄存器的值为0x1388
    */
    void Modbus_06_Slave( void )
    {
        u16  RegValue = 0;
        u16 CRC16Temp = 0;
        RegValue = ( u16 )( data_backup[4] << 8 ) | data_backup[5];      					//获取寄存器值
        if( RegValue <= HoldMaxValue )                                          	//寄存器值不超过1000
        {
            HoldReg[StartRegAddr * 2] = data_backup[4];                 					//存储寄存器值
            HoldReg[StartRegAddr * 2 + 1] = data_backup[5];
            SendBuf[0] = data_backup[0];																					//地址
            SendBuf[1] = data_backup[1];																					//功能码
            SendBuf[2] = data_backup[2];																					//地址高位
            SendBuf[3] = data_backup[3];																					//地址低位
            SendBuf[4] = data_backup[4];																					//值高位
            SendBuf[5] = data_backup[5];																					//值低位
            CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 );              						//获取CRC校验值
            SendBuf[6] = CRC16Temp & 0xFF;                            						//CRC低位
            SendBuf[7] = ( CRC16Temp >> 8 );                          						//CRC高位
            uart2_Send( SendBuf, 8 );
        }
        else
        {
            err =  3;                                                  						//寄存器数值不在规定范围内
        }
    }
    /*
    函数功能:写多个连续保持寄存器值 16
    主站请求报文:       0x01 0x10    0x7540  0x0002  0x04  0x0000 0x2710    0xB731  写从0x7540地址开始的2个保持寄存器值 共4字节
    从站正常响应报文:   0x01 0x10    0x7540  0x0002  0x5A10                         写从0x7540地址开始的2个保持寄存器值
    */
    void Modbus_16_Slave( void )
    {
        u16 RegNum = 0;
        u16 CRC16Temp = 0;
        u8 i = 0;
        RegNum = ( u16 )( data_backup[4] << 8 ) | data_backup[5];        					//获取寄存器数量
        if( ( StartRegAddr + RegNum ) <= (HoldRegStartAddr + HoldRegCount) )     	//寄存器地址+寄存器数量 在规定范围内 <=8
        {
            for( i = 0; i < RegNum; i++ )                              						//存储寄存器设置值
            {
                HoldReg[StartRegAddr * 2 + i * 2] = data_backup[i * 2 + 7];
                HoldReg[StartRegAddr * 2 + 1 + i * 2] = data_backup[i * 2 + 8];
            }
            SendBuf[0] = data_backup[0];																					//起始地址
            SendBuf[1] = data_backup[1];																					//功能码
            SendBuf[2] = data_backup[2];																					//地址高位
            SendBuf[3] = data_backup[3];																					//地址低位
            SendBuf[4] = data_backup[4];																					//寄存器数量高位
            SendBuf[5] = data_backup[5];																					//寄存器数量低位
            CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 );                					//获取CRC校验值
            SendBuf[6] = CRC16Temp & 0xFF;                              					//CRC低位
            SendBuf[7] = ( CRC16Temp >> 8 );                            					//CRC高位
            uart2_Send( SendBuf, 8 );
        }
        else
        {
            err = 3;                                                   						//寄存器数量不在规定范围内
        }
    }
    

    Modbus的执行在这里只实现了最常用的三个。

    各个功能码的实现原理在这里就不说了,直接通过代码中和注释就可以看明白了。

    下面贴出串口和modbus的完整代码

    uart2.h

    #ifndef __UART2_H
    #define __UART2_H
    #include "sys.h"
    
    #define DMA_REC_LEN 50																			//DMA数据接收缓冲区
    
    void  uart2_init(u16 baud);
    void uartDMA_Init( void );
    void myDMA_Enable( DMA_Channel_TypeDef*DMA_CHx );
    void uart2_Send( u8 *buf, u16 len );
    void copy_data( u8 *buf, u16 len );
    #endif
    
    

    uart2.c

    //串口2空闲中断 + DMA数据传输
    #include "uart2.h"
    #define UART2_DMA  1																				//使用串口2  DMA传输
    #define  CHECK_NONE_ONE_STOP    1 //无校验位  1个停止位  1有效  0 无效 
    #define  CHECK_NONE_TWO_STOP    0 //无校验位  2个停止位  1有效  0 无效 
    #define  CHECK_EVEN    0          //偶数校验   1有效  0 无效 
    #define  CHECK_ODD     0          //奇数校验   1有效  0 无效 
    
    u8 dma_rec_buff[DMA_REC_LEN] = {0};
    u16 uart2_rec_cnt = 0;																			//串口接收数据长度
    
    u8 data_backup[DMA_REC_LEN] = {0}; 													//数据备份
    u16 dataLen_backup = 0;																			//长度备份
    _Bool receiveOK_flag = 0;																		//接收完成标志位
    
    /*
    空闲中断是什么意思呢?
    指的是当总线接收数据时,一旦数据流断了,此时总线没有接收传输,处于空闲状态,IDLE就会置1,产生空闲中断;又有数据发送时,IDLE位就会置0;
    注意:置1之后它不会自动清0,也不会因为状态位是1而一直产生中断,它只有0跳变到1时才会产生,也可以理解为上升沿触发。
    所以,为确保下次空闲中断正常进行,需要在中断服务函数发送任意数据来清除标志位。
    */
    
    void  uart2_init( u16 baud )
    {
        GPIO_InitTypeDef GPIO_InitStructure;
        USART_InitTypeDef USART_InitStructure;
        NVIC_InitTypeDef  NVIC_InitStructure;
    
        RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE );
        RCC_APB1PeriphClockCmd( RCC_APB1Periph_USART2, ENABLE );
    
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;												 //推挽复用模式
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init( GPIOA, &GPIO_InitStructure );
    
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;									 //浮空输入模式
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init( GPIOA, &GPIO_InitStructure );
    
        NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    
        NVIC_Init( &NVIC_InitStructure );
    
        USART_InitStructure.USART_BaudRate = baud;
        USART_InitStructure.USART_WordLength = USART_WordLength_8b;
        USART_InitStructure.USART_StopBits = USART_StopBits_1;
        USART_InitStructure.USART_Parity = USART_Parity_No;
    		
    #if(CHECK_EVEN == 1) 																											 //如果定义了偶校验  数据位长度要改为9位
        USART_InitStructure.USART_WordLength = USART_WordLength_9b;
        USART_InitStructure.USART_Parity = USART_Parity_Even;
    #endif
    
    #if(CHECK_ODD == 1) 																											 //如果定义了奇校验  数据位长度要改为9位
        USART_InitStructure.USART_WordLength = USART_WordLength_9b;
        USART_InitStructure.USART_Parity = USART_Parity_Odd;
    #endif
    
    #if(CHECK_NONE_ONE_STOP==1)																							   //停止位为 一位
        USART_InitStructure.USART_StopBits = USART_StopBits_1;
    #endif
    
    #if(CHECK_NONE_TWO_STOP==1)																								 //停止位为 两位																	
        USART_InitStructure.USART_StopBits = USART_StopBits_2;
    #endif		
    		
    		
        USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
        USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    
        USART_Init( USART2, &USART_InitStructure );
    		
    #if (UART2_DMA == 1)
        USART_ITConfig( USART2, USART_IT_IDLE, ENABLE );											 //使能串口空闲中断
        USART_DMACmd( USART2, USART_DMAReq_Rx, ENABLE );											 //使能串口2 DMA接收
        uartDMA_Init();																												 //初始化 DMA
    #else
        USART_ITConfig( USART2, USART_IT_RXNE, ENABLE );			  							 //使能串口RXNE接收中断
    #endif
        USART_Cmd( USART2, ENABLE );																					 //使能串口2
    		
    //RXNE中断和IDLE中断的区别?
    //当接收到1个字节,就会产生RXNE中断,当接收到一帧数据,就会产生IDLE中断。比如给单片机一次性发送了8个字节,就会产生8次RXNE中断,1次IDLE中断。
    }
    
    void uartDMA_Init( void )
    {
        DMA_InitTypeDef  DMA_IniStructure;
    
        RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE ); 									 //使能DMA时钟
    
        DMA_DeInit( DMA1_Channel6 );																			     //DMA1通道6对应 USART2_RX
        DMA_IniStructure.DMA_PeripheralBaseAddr = ( u32 )&USART2->DR;					 //DMA外设usart基地址
        DMA_IniStructure.DMA_MemoryBaseAddr = ( u32 )dma_rec_buff; 						 //DMA内存基地址
        DMA_IniStructure.DMA_DIR = DMA_DIR_PeripheralSRC;											 //数据传输方向,从外设读取发送到内存
        DMA_IniStructure.DMA_BufferSize = DMA_REC_LEN;												 //DMA通道的DMA缓存的大小  也就是 DMA一次传输的字节数
        DMA_IniStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;				 //外设地址寄存器不变
        DMA_IniStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;								 //数据缓冲区地址递增
        DMA_IniStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设以字节为单位搬运
        DMA_IniStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; 				 //数据缓冲区以字节为单位搬入
        DMA_IniStructure.DMA_Mode = DMA_Mode_Normal;													 //工作在正常缓存模式
        DMA_IniStructure.DMA_Priority = DMA_Priority_Medium;									 //DMA通道 x拥有中优先级
        DMA_IniStructure.DMA_M2M = DMA_M2M_Disable;														 //DMA通道x没有设置为内存到内存传输
    
        DMA_Init( DMA1_Channel6, &DMA_IniStructure );
        DMA_Cmd( DMA1_Channel6, ENABLE );
    }
    
    //重新恢复DMA指针
    void myDMA_Enable( DMA_Channel_TypeDef*DMA_CHx )
    {
        DMA_Cmd( DMA_CHx, DISABLE );  																				 //关闭DMA1所指示的通道
        DMA_SetCurrDataCounter( DMA_CHx, DMA_REC_LEN );												 //DMA通道的DMA缓存的大小
        DMA_Cmd( DMA_CHx, ENABLE );  																					 //DMA1所指示的通道
    }
    //发送len个字节
    //buf:发送区首地址
    //len:发送的字节数
    void uart2_Send( u8 *buf, u16 len )
    {
        u16 t;
        for( t = 0; t < len; t++ )																						 //循环发送数据
        {
            while( USART_GetFlagStatus( USART2, USART_FLAG_TC ) == RESET );
            USART_SendData( USART2, buf[t] );
        }
        while( USART_GetFlagStatus( USART2, USART_FLAG_TC ) == RESET );
    }
    
    //备份接收到的数据
    void copy_data( u8 *buf, u16 len )
    {
        u16 t;
        dataLen_backup = len;																									 //保存数据长度
        for( t = 0; t < len; t++ )
        {
            data_backup[t] = buf[t];																					 //备份接收到的数据,防止在处理数据过程中接收到新数据,将旧数据覆盖掉。
        }
    }
    // Modbus RTU 通信协议
    //地址码 功能码 寄存器高位 寄存器低位  数据高位  数据低位 CRC高位 CRC低位
    //01     03      00          00         03         e8      xx        xx
    //利用空闲中断接收串口不定长数据
    //串口接收一组数据结束后才会进入中断,在数据发送过程中,已经接收到的数据将会被存入DMA的缓冲区中
    void USART2_IRQHandler( void )
    {
        u8 tem = 0;
    #if (UART2_DMA == 1)			//如果使能了 UART2_DMA 则使用串口空闲中断和 DMA功能接收数据
    	if( USART_GetITStatus( USART2, USART_IT_IDLE ) != RESET )							 			  //空闲中断  一帧数据发送完成
        {
            USART_ReceiveData( USART2 );																			 			//读取数据注意:这句必须要,否则不能够清除空闲中断标志位。
            DMA_Cmd( DMA1_Channel6, DISABLE );																 			//关闭 DMA 防止后续数据干扰
            uart2_rec_cnt = DMA_REC_LEN - DMA_GetCurrDataCounter( DMA1_Channel6 );  //DMA接收缓冲区数据长度减去当前 DMA传输通道中剩余单元数量就是已经接收到的数据数量
            //uart2_Send( dma_rec_buff, uart2_rec_cnt );														//发送接收到的数据
    			  copy_data( dma_rec_buff, uart2_rec_cnt );														    //备份数据
            receiveOK_flag = 1;																											//置位数据接收完成标志位
            USART_ClearITPendingBit( USART2, USART_IT_IDLE );									 			//清除空闲中断标志位
            myDMA_Enable( DMA1_Channel6 );																		 			//重新恢复 DMA等待下一次接收
        }
    #else										//如果未使能 UART2_DMA 则通过常规方式接收数据  每接收到一个字节就会进入一次中断
        if( USART_GetITStatus( USART2, USART_IT_RXNE ) != RESET )						 //接收中断
        {
            tem = USART_ReceiveData( USART2 );
            USART_SendData( USART2, tem );
        }
    #endif
    }
    
    

    modbus.h

    #ifndef __MODBUS_H
    #define __MODBUS_H
    #include "sys.h"
    
    
    #define SlaveID  0x01     //从机地址
    
    void DisposeReceive( void );
    void Modbus_03_Slave(void);
    void Modbus_06_Slave(void);
    void Modbus_16_Slave(void);
    #endif
    

    modbus.c

    #include "modbus.h"
    #include "crc16.h"
    #include "uart2.h"
    
    #define HoldRegStartAddr    0x0000																						//保持寄存器起始地址
    #define HoldRegCount        8																									//保持寄存器数量
    #define HoldMaxValue        1000            																	//寄存器最大值
    
    extern u8 data_backup[DMA_REC_LEN]; 													  							//modbus接收到数据备份
    extern u16 dataLen_backup;																										//modbus接收数据长度备份
    
    
    u8 SendBuf[DMA_REC_LEN] = {0};
    u16 StartRegAddr = HoldRegStartAddr;
    u8 err = 0;                                                        						//错误代码
    
    u8 HoldReg[HoldRegCount*2] = {1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8}; //存储8路数据值
    
    
    /*
    错误代码说明:
    0x01  不是支持的功能码
    0x02  起始地址不在规定范围内
    0x03  寄存器数量不在规定范围内
    0x04  数据校验错误
    */
    //处理接收到的数据
    // 接收: [地址][功能码][起始地址高][起始地址低][总寄存器数高][总寄存器数低][CRC低][CRC高]
    void DisposeReceive( void )
    {
        u16 CRC16 = 0, CRC16Temp = 0;
        if( data_backup[0] == SlaveID )                                 					//地址等于本机地址 地址范围:1 - 32
        {
            CRC16 = App_Tab_Get_CRC16( data_backup, dataLen_backup - 2 );  				//CRC校验 低字节在前 高字节在后 高字节为报文最后一个字节
            CRC16Temp = ( ( u16 )( data_backup[dataLen_backup - 1] << 8 ) | data_backup[dataLen_backup - 2] );
            if( CRC16 != CRC16Temp )
            {
                err = 4;                                               						//CRC校验错误
            }
            StartRegAddr = ( u16 )( data_backup[2] << 8 ) | data_backup[3];
            if( StartRegAddr > (HoldRegStartAddr + HoldRegCount - 1) ) 
            {
                err = 2;                                               						//起始地址不在规定范围内 00 - 07    1 - 8号通道
            }
            if( err == 0 )
            {
                switch( data_backup[1] )                                					//功能码
                {
                    case 3:                                            						//读多个寄存器
                    {
                        Modbus_03_Slave();
                        break;
                    }
                    case 6:                                            						//写单个寄存器
                    {
                        Modbus_06_Slave();
                        break;
                    }
                    case 16:                                           						//写多个寄存器
                    {
                        Modbus_16_Slave();
                        break;
                    }
                    default:
                    {
                        err = 1;                                       						//不支持该功能码
                        break;
                    }
                }
            }
            if( err > 0 )
            {
                SendBuf[0] = data_backup[0];
                SendBuf[1] = data_backup[1] | 0x80;
                SendBuf[2] = err;                                      						//发送错误代码
                CRC16Temp = App_Tab_Get_CRC16( SendBuf, 3 );           						//计算CRC校验值
                SendBuf[3] = CRC16Temp & 0xFF;                         						//CRC低位
                SendBuf[4] = ( CRC16Temp >> 8 );                       						//CRC高位
                uart2_Send( SendBuf, 5 );					
                err = 0;                                               						//发送完数据后清除错误标志
            }
        }
    }
    
    /*
    函数功能:读保持寄存器  03
    主站请求报文:      0x01 0x03   0x0000  0x0001  0x840A     读从0开始的1个保持寄存器
    从站正常响应报文:  0x01 0x03   0x02    0x09C4  0xBF87     读到的2字节数据为 0x09C4
    */
    void Modbus_03_Slave( void )
    {
        u16 RegNum = 0;
        u16 CRC16Temp = 0;
        u8 i = 0;
        RegNum = ( u16 )( data_backup[4] << 8 ) | data_backup[5];        					//获取寄存器数量
        if( ( StartRegAddr + RegNum ) <= (HoldRegStartAddr + HoldRegCount) )      //寄存器地址+寄存器数量 在规定范围内 <=8
        {
            SendBuf[0] = data_backup[0];																					//地址	
            SendBuf[1] = data_backup[1];																					//功能码
            SendBuf[2] = RegNum * 2;																							//返回字节数量
            for( i = 0; i < RegNum; i++ )                             						//循环读取保持寄存器内的值
            {
                SendBuf[3 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2];
                SendBuf[4 + i * 2] = HoldReg[StartRegAddr * 2 + i * 2 + 1];
            }
            CRC16Temp = App_Tab_Get_CRC16( SendBuf, RegNum * 2 + 3 );  						//获取CRC校验值
            SendBuf[RegNum * 2 + 3] = CRC16Temp & 0xFF;                						//CRC低位
            SendBuf[RegNum * 2 + 4] = ( CRC16Temp >> 8 );              						//CRC高位
            uart2_Send( SendBuf, RegNum * 2 + 5 );
        }
        else
        {
            err = 3;                                                   						//寄存器数量不在规定范围内
        }
    }
    /*
    函数功能:写单个保持寄存器 06
    主站请求报文:      0x01 0x06    0x0000  0x1388    0x849C   写0号寄存器的值为0x1388
    从站正常响应报文:  0x01 0x06    0x0000  0x1388    0x849C    0号寄存器的值为0x1388
    */
    void Modbus_06_Slave( void )
    {
        u16  RegValue = 0;
        u16 CRC16Temp = 0;
        RegValue = ( u16 )( data_backup[4] << 8 ) | data_backup[5];      					//获取寄存器值
        if( RegValue <= HoldMaxValue )                                          	//寄存器值不超过1000
        {
            HoldReg[StartRegAddr * 2] = data_backup[4];                 					//存储寄存器值
            HoldReg[StartRegAddr * 2 + 1] = data_backup[5];
            SendBuf[0] = data_backup[0];																					//地址
            SendBuf[1] = data_backup[1];																					//功能码
            SendBuf[2] = data_backup[2];																					//地址高位
            SendBuf[3] = data_backup[3];																					//地址低位
            SendBuf[4] = data_backup[4];																					//值高位
            SendBuf[5] = data_backup[5];																					//值低位
            CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 );              						//获取CRC校验值
            SendBuf[6] = CRC16Temp & 0xFF;                            						//CRC低位
            SendBuf[7] = ( CRC16Temp >> 8 );                          						//CRC高位
            uart2_Send( SendBuf, 8 );
        }
        else
        {
            err =  3;                                                  						//寄存器数值不在规定范围内
        }
    }
    /*
    函数功能:写多个连续保持寄存器值 16
    主站请求报文:       0x01 0x10    0x7540  0x0002  0x04  0x0000 0x2710    0xB731  写从0x7540地址开始的2个保持寄存器值 共4字节
    从站正常响应报文:   0x01 0x10    0x7540  0x0002  0x5A10                         写从0x7540地址开始的2个保持寄存器值
    */
    void Modbus_16_Slave( void )
    {
        u16 RegNum = 0;
        u16 CRC16Temp = 0;
        u8 i = 0;
        RegNum = ( u16 )( data_backup[4] << 8 ) | data_backup[5];        					//获取寄存器数量
        if( ( StartRegAddr + RegNum ) <= (HoldRegStartAddr + HoldRegCount) )     	//寄存器地址+寄存器数量 在规定范围内 <=8
        {
            for( i = 0; i < RegNum; i++ )                              						//存储寄存器设置值
            {
                HoldReg[StartRegAddr * 2 + i * 2] = data_backup[i * 2 + 7];
                HoldReg[StartRegAddr * 2 + 1 + i * 2] = data_backup[i * 2 + 8];
            }
            SendBuf[0] = data_backup[0];																					//起始地址
            SendBuf[1] = data_backup[1];																					//功能码
            SendBuf[2] = data_backup[2];																					//地址高位
            SendBuf[3] = data_backup[3];																					//地址低位
            SendBuf[4] = data_backup[4];																					//寄存器数量高位
            SendBuf[5] = data_backup[5];																					//寄存器数量低位
            CRC16Temp = App_Tab_Get_CRC16( SendBuf, 6 );                					//获取CRC校验值
            SendBuf[6] = CRC16Temp & 0xFF;                              					//CRC低位
            SendBuf[7] = ( CRC16Temp >> 8 );                            					//CRC高位
            uart2_Send( SendBuf, 8 );
        }
        else
        {
            err = 3;                                                   						//寄存器数量不在规定范围内
        }
    }
    

    测试效果

    测试功能码 0x03  读一个或者多个保持寄存器值

    测试功能码 0x06 写单个保持寄存值

    测试功能码 0x10 写多个保持寄存器值

    再次读取所有寄存器值

    说明寄存器值修改成功。

    工程下载地址 https://download.csdn.net/download/qq_20222919/12937240

    串口调试软件下载地址: https://download.csdn.net/download/qq_20222919/18798579?spm=1001.2014.3001.5501

    展开全文
  • 51单片机Modbus_RTU

    2015-12-13 12:06:22
    void presetSingleRegister(void) //设置单个寄存器 { U8 addr; U8 tempAddr; U8 setCount; U16 crcData; U16 tempData; //addr = (receBuf[2]) + receBuf[3]; //tempAddr = addr & 0xfff; addr = receBuf...
  • 第四课S7-200作为主站与从站51单片机Modbus RTU通讯 本节主要完成PLC作主站,51单片机作从站,用ModBus协议进行通讯。PLC读取单片机保持寄存器区的数据。S7-200PLC程序主要通过调用Modubs RTU.

    转载地址:http://blog.sina.com.cn/s/blog_14f58a1920102x2qm.html

    系列地址:http://blog.sina.com.cn/s/articlelist_5626175890_9_1.html

    西门子plc200学习笔记

    第四课 S7-200作为主站与从站51单片机Modbus RTU通讯

    本节主要完成PLC作主站,51单片机作从站,用ModBus协议进行通讯。PLC读取单片机保持寄存器区的数据。S7-200PLC程序主要通过调用Modubs RTU 主站指令库完成。

    一、调用 Modbus RTU 主站初始化和控制子程序

    使用 SM0.0 调用 MBUS_CTRL 完成主站的初始化,并启动其功能控制:

    第四课 <wbr>S7-200作为主站与从站51单片机Modbus <wbr>RTU通讯

    各参数意义如下:

    a.

    EN

    使能:

    必须保证每一扫描周期都被使能(使用 SM0.0)

    b.

    Mode

    模式:

    为 1 时,使能 Modbus 协议功能;为 0 时恢复为系统 PPI 协议

    c.

    Baud

    波特率:

    支持的通讯波特率为1200,2400,4800,9600,19200,38400,57600,115200。

    d.

    Parity

    校验:

    校验方式选择

     

    0=无校验

    1=奇较验

    2=偶较验   

    e.

    Timeout

    超时:

    主站等待从站响应的时间,以毫秒为单位,典型的设置值为 1000 毫秒(1 秒),允许设置的范围为 1 - 32767。

     

    注意: 这个值必须设置足够大以保证从站有时间响应。

    f.

    Done

    完成位:

    初始化完成,此位会自动置1。可以用该位启动 MBUS_MSG 读写操作(见例程)

    g.

    Error

     

    初始化错误代码(只有在 Done 位为1时有效):

     

    0= 无错误

    1= 校验选择非法

    2= 波特率选择非法

    3= 模式选择非法

     

    二、调用 Modbus RTU 主站读写子程序MBUS_MSG,发送一个Modbus 请求;

    第四课 <wbr>S7-200作为主站与从站51单片机Modbus <wbr>RTU通讯

    各参数意义如下:

    a.

    EN

    使能:

    同一时刻只能有一个读写功能(即 MBUS_MSG)使能

     

      注意:建议每一个读写功能(即 MBUS_MSG)都用上一个 MBUS_MSG 指令的 Done 完成位来激活,以保证所有读写指令循环进行(见例程)。

    b.

    First

    读写请求位:

    每一个新的读写请求必须使用脉冲触发

    c.

    Slave

    从站地址:

    可选择的范围   1 - 247

    d.

    RW

    从站地址:

    0 = 读, 1 = 写

     

      注意:

    1. 开关量输出和保持寄存器支持读和写功能

    2. 开关量输入和模拟量输入只支持读功能

    e.

    Addr

    读写从站的数据地址:

     

    选择读写的数据类型

     

    00001 至 0xxxx - 开关量输出

     

    10001 至 1xxxx - 开关量输入

    30001 至 3xxxx - 模拟量输入

    40001 至 4xxxx - 保持寄存器

    f.

    Count

    数据个数

    通讯的数据个数(位或字的个数)

     

      注意: Modbus主站可读/写的最大数据量为120个字(是指每一个 MBUS_MSG 指令)

    g.

    DataPtr

    数据指针:

    1. 如果是读指令,读回的数据放到这个数据区中

     

    2. 如果是写指令,要写出的数据放到这个数据区中

    h.

    Done

    完成位

    读写功能完成位

    i.

    Error

    错误代码:

    只有在 Done 位为1时,错误代码才有效

     

    0 = 无错误

    1 = 响应校验错误

    2 = 未用

    3 = 接收超时(从站无响应)

    4 = 请求参数错误(slave address, Modbus address, count, RW)

    5 = Modbus/自由口未使能

    6 = Modbus正在忙于其它请求

    7 = 响应错误(响应不是请求的操作)

    8 = 响应CRC校验和错误

    -

    101 = 从站不支持请求的功能

    102 = 从站不支持数据地址

    103 = 从站不支持此种数据类型

    104 = 从站设备故障

    105 = 从站接受了信息,但是响应被延迟

    106 = 从站忙,拒绝了该信息

    107 = 从站拒绝了信息

    108 = 从站存储器奇偶错误

    常见的错误:

    如果多个 MBUS_MSG 指令同时使能会造成 6 号错误库存储区被程序其它地方复用,有时也会造成6 号错误从站 delay 参数设的时间过长会造成主站 3 号错误从站掉电或不运行,网络故障都会造成主站 3 号错误。

    三、需要从站支持的功能及Modbus 保持寄存器地址映射

    为了支持上述 Modbus 地址的读写,Modbus Master 协议库需要从站支持下列功能:

    需要从站支持的功能

    Modbus 地址

    读/写

    Modbus 从站须支持的功能

    00001 - 09999
    数字量输出

    功能 1

    功能 5:写单输出点
    功能 15:写多输出点

    10001 - 19999
    数字量输入

    功能 2

    30001 - 39999
    输入寄存器

    功能 4

    40001 - 49999
    保持寄存器

    功能 3

    功能 6:写单寄存器单元
    功能 16:写多寄存器单元

    Modbus 保持寄存器地址映射举例:

    第四课 <wbr>S7-200作为主站与从站51单片机Modbus <wbr>RTU通讯

    四、S7-200PLC程序

    第四课 <wbr>S7-200作为主站与从站51单片机Modbus <wbr>RTU通讯

    第四课 <wbr>S7-200作为主站与从站51单片机Modbus <wbr>RTU通讯

    第四课 <wbr>S7-200作为主站与从站51单片机Modbus <wbr>RTU通讯

    第四课 <wbr>S7-200作为主站与从站51单片机Modbus <wbr>RTU通讯

    第四课 <wbr>S7-200作为主站与从站51单片机Modbus <wbr>RTU通讯

    五、单片机程序;STC11F04E单片机,9600波特率

    START:   MOV TMOD,#21H ;定时器1是8位再装入,定时器0为16位定时器

             MOV TH1,#0FDH;预置初值(按照波特率9600BPS预置初值)

             MOV TL1,#0FDH; 0FDH=9600=11.0592

             MOV  TH0, #0DCH;88H     ;8800=12t,7000=stc1t

                MOV  TL0, #00H

             ORL IE, #92H  ;EA=1,ES=1;ET0=1

             SETB PS       ;串口中断优先

             SETB TR1      ;启动定时器1

             MOV 98H,#50H  ;scon

             MOV P1M0,#01000000b     ; P1M0=0 P1M1=0双向口 P1M0=1 P1M1=0输入口   P1M0=0 P1M1=1推挽输出20ma

             MOV P1M1,#10000000b

             MOV WDT_CONTR ,#27H 看门狗设置使能

    QL0:     MOV A,#00H

             MOV R0,#10H

             MOV R2,#9BH  ;10-ABH清零

    CLEAR:   MOV @R0,A

             INC R0

             DJNZ R2,CLEAR

             CLR  FLAG

             CLR  FLAG_0

             SETB TR0          ;启动定时器0

             ;ANL AUX,#07FH    ;p3.0p3.1当串口

             ORL AUX,#80H             ;p1.7,p1.6当串口

             CLR P3.7                        ;485芯片接收使能

     

    WA1:    ;MOV WDT_CONTR ,#37H;喂狗; SETB CW

            JNB FLAG_0,WA1     ;FLAG_0=1表示已经接收到上位机数据

            CLR TR0

            MOV A,2CH         ;检查设备地址是01h码,设本机地址码是1

            MOV R2,A

            XRL A,#01H

            JNZ QL0

            ACALL FSZJ  ;FH: DB 01H,03H,16,00H,01H,02H,03H,04H,05H,06H,07H,08H,09H,10H,11H,12H,0DH,0EH,0FH,10H,11H,12H,13H,14H,15H,16H,17H,18H,19H,1AH,1BH,1CH,1DH,1EH,1FH;18

            ACALL DELAY

            CALL FZJ

        AJMP QL0

    FZJ:     MOV R0,#2cH       ;向主机发送数据子程序

    FZJ0:    MOV R2,#10H

    FZJ1:    CLR EA

             ANL AUX,#07FH     ;p3.0p3.1当串口

    FZL1:     MOV A,@R0

             MOV SBUF,A

             JNB TI,$

             CLR TI

             INC R0

             DJNZ R2,FZL1

             SETB EA

             RET

     

    FSZJ:    MOV DPTR,#FH

             MOV R2,#19;

             ORL AUX,#80H

             SETB P3.7          ;发送数据

             MOV R0,#40H

    FSZJA:    MOV A,#0H

             MOVC A,@A+DPTR

             MOV @R0,A

             INC R0

             INC DPTR

             DJNZ R2,FSZJA

             MOV R0,#40H

             MOV CRCCD,#19

             LCALL CRC1

             MOV R2,#21

             MOV R0,#40H

    FSZJ2:   MOV A,@R0

             MOV SBUF,A

             JNB TI,$

             CLR TI

             INC R0

             DJNZ R2,FSZJ2

             SETB EA

             RET

    FH:DB 01H,03H,16,00H,01H,02H,03H,04H,05H,06H,07H,08H,09H,10H,11H,12H,0DH,0EH,0FH,10H,11H,12H,13H,14H,15H,16H,17H,18H,19H,1AH,1BH,1CH,1DH,1EH,1FH;18

     

    用串口助手检测到的数据如下图。

    第四课 <wbr>S7-200作为主站与从站51单片机Modbus <wbr>RTU通讯

    展开全文
  • MODBUS-寄存器与功能码学习

    千次阅读 2017-09-08 14:11:11
    MODBUS-寄存器与功能码学习 分类 简称 起始地址 结束地址 能够使用的功能码 输出逻辑线圈/(可读写位)/(DI/O)(如继电器开关控制) 0x...

    MODBUS-寄存器与功能码学习

     

    Modbus由MODICON公司于1979年开发,是一种工业现场总线协议标准。1996年施耐德公司推出基于以太网TCP/IP的Modbus协议:ModbusTCP

    Modbus协议是一项应用层报文传输协议,包括ASCII、RTU、TCP三种报文类型。

    标准的Modbus协议物理层接口有RS232、RS422、RS485和以太网接口,采用master/slave方式通信。

    ModbusTCP数据帧

    ModbusTCP的数据帧可分为两部分:MBAP+PDU

    报文头MBAP

    MBAP为报文头,长度为7字节,组成如下:

    事务处理标识协议标识长度单元标识符
    2字节2字节2字节1字节

     

    内容解释
    事务处理标识可以理解为报文的序列号,一般每次通信之后就要加1以区别不同的通信数据报文。
    协议标识符00 00表示ModbusTCP协议。
    长度表示接下来的数据长度,单位为字节。
    单元标识符可以理解为设备地址。

    帧结构PDU

    PDU由功能码+数据组成。功能码为1字节,数据长度不定,由具体功能决定。

    功能码

    Modbus的操作对象有四种:线圈、离散输入、保持寄存器、输入寄存器。

    对象含义
    线圈PLC的输出位,开关量,在Modbus中可读可写
    离散量PLC的输入位,开关量,在Modbus中只读
    输入寄存器PLC中只能从模拟量输入端改变的寄存器,在Modbus中只读
    保持寄存器PLC中用于输出模拟量信号的寄存器,在Modbus中可读可写

    根据对象的不同,Modbus的功能码有:

    功能码含义
    0x01读线圈
    0x05写单个线圈
    0x0F写多个线圈
    0x02读离散量输入
    0x04读输入寄存器
    0x03读保持寄存器
    0x06写单个保持寄存器
    0x10写多个保持寄存器

    说明更详细的表

    代码中文名称英文名位操作/字操作操作数量
    01读线圈状态READ COIL STATUS位操作单个或多个
    02读离散输入状态READ INPUT STATUS位操作单个或多个
    03读保持寄存器READ HOLDING REGISTER字操作单个或多个
    04读输入寄存器READ INPUT REGISTER字操作单个或多个
    05写线圈状态WRITE SINGLE COIL位操作单个
    06写单个保持寄存器WRITE SINGLE REGISTER字操作单个
    15写多个线圈WRITE MULTIPLE COIL位操作多个
    16写多个保持寄存器WRITE MULTIPLE REGISTER字操作多个

    PDU详细结构

    0x01:读线圈

    在从站中读1~2000个连续线圈状态,ON=1,OFF=0

    • 请求:MBAP 功能码 起始地址H 起始地址L 数量H 数量L(共12字节)
    • 响应:MBAP 功能码 数据长度 数据(一个地址的数据为1位)
    • 如:在从站0x01中,读取开始地址为0x0002的线圈数据,读0x0008位
      00 01 00 00 00 06 01 01 00 02 00 08
    • 回:数据长度为0x01个字节,数据为0x01,第一个线圈为ON,其余为OFF
      00 01 00 00 00 04 01 01 01 01

    0x05:写单个线圈

    将从站中的一个输出写成ON或OFF,0xFF00请求输出为ON,0x000请求输出为OFF

    • 请求:MBAP 功能码 输出地址H 输出地址L 输出值H 输出值L(共12字节)
    • 响应:MBAP 功能码 输出地址H 输出地址L 输出值H 输出值L(共12字节)
    • 如:将地址为0x0003的线圈设为ON
      00 01 00 00 00 06 01 05 00 03 FF 00
    • 回:写入成功
      00 01 00 00 00 06 01 05 00 03 FF 00

    0x0F:写多个线圈

    将一个从站中的一个线圈序列的每个线圈都强制为ON或OFF,数据域中置1的位请求相应输出位ON,置0的位请求响应输出为OFF

    • 请求:MBAP 功能码 起始地址H 起始地址L 输出数量H 输出数量L 字节长度 输出值H 输出值L
    • 响应:MBAP 功能码 起始地址H 起始地址L 输出数量H 输出数量L

    0x02:读离散量输入

    从一个从站中读1~2000个连续的离散量输入状态

    • 请求:MBAP 功能码 起始地址H 起始地址L 数量H 数量L(共12字节)
    • 响应:MBAP 功能码 数据长度 数据(长度:9+ceil(数量/8))
    • 如:从地址0x0000开始读0x0012个离散量输入
      00 01 00 00 00 06 01 02 00 00 00 12
    • 回:数据长度为0x03个字节,数据为0x01 04 00,表示第一个离散量输入和第11个离散量输入为ON,其余为OFF
      00 01 00 00 00 06 01 02 03 01 04 00

    0x04:读输入寄存器

    从一个远程设备中读1~2000个连续输入寄存器

    • 请求:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L(共12字节)
    • 响应:MBAP 功能码 数据长度 寄存器数据(长度:9+寄存器数量×2)
    • 如:读起始地址为0x0002,数量为0x0005的寄存器数据
      00 01 00 00 00 06 01 04 00 02 00 05
    • 回:数据长度为0x0A,第一个寄存器的数据为0x0c,其余为0x00
      00 01 00 00 00 0D 01 04 0A 00 0C 00 00 00 00 00 00 00 00

    0x03:读保持寄存器

    从远程设备中读保持寄存器连续块的内容

    • 请求:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L(共12字节)
    • 响应:MBAP 功能码 数据长度 寄存器数据(长度:9+寄存器数量×2)
    • 如:起始地址是0x0000,寄存器数量是 0x0003
      00 01 00 00 00 06 01 03 00 00 00 03
    • 回:数据长度为0x06,第一个寄存器的数据为0x21,其余为0x00
      00 01 00 00 00 09 01 03 06 00 21 00 00 00 00

    0x06:写单个保持寄存器

    在一个远程设备中写一个保持寄存器

    • 请求:MBAP 功能码 寄存器地址H 寄存器地址L 寄存器值H 寄存器值L(共12字节)
    • 响应:MBAP 功能码 寄存器地址H 寄存器地址L 寄存器值H 寄存器值L(共12字节)
    • 如:向地址是0x0000的寄存器写入数据0x000A
      00 01 00 00 00 06 01 06 00 00 00 0A
    • 回:写入成功
      00 01 00 00 00 06 01 06 00 00 00 0A

    0x10:写多个保持寄存器

    在一个远程设备中写连续寄存器块(1~123个寄存器)

    • 请求:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L 字节长度 寄存器值(13+寄存器数量×2)
    • 响应:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L(共12字节)
    • 如:向起始地址为0x0000,数量为0x0001的寄存器写入数据,数据长度为0x02,数据为0x000F
      00 01 00 00 00 09 01 10 00 00 00 01 02 00 0F
    • 回:写入成功
      00 01 00 00 00 06 01 10 00 00 00 01

    Modbus TCP 示例报文

    原文链接:https://blog.csdn.net/mikasoi/article/details/81782159

    ModBusTcp与串行链路Modbus的数据域是一致的,具体数据域可以参考串行Modbus。这里给出几个ModbusTcp的链路解析说明,辅助新人分析报文。

    Modbus异常码

    功能码域

    在正常响应中,服务器利用响应功能码域来应答最初请求的功能码。所有功能码的最高有效位(MSB)都为0(它们的值都低于十六进制80)。在异常响应中,服务器设置功能码的MSB 为1。这使得异常响应中的功能码值比正常响应中的功能码值高十六进制80。通过设置功能码的MSB,客户机的应用程序能够识别异常响应,并且能够检测异常码的数据域。

     

    数据域

    在正常响应中,服务器可以返回数据域中数据或统计表(请求中要求的任何报文)。在异常响应中,服务器返回数据域中的异常码。这就定义了产生异常的服务器状态。

    错误代码表

    代码名称含义
    01非法功能对于服务器(或从站)来说,询问中接收到的功能码是不可允许的操作,可能是因为功能码仅适用于新设备而被选单元中不可实现同时,还指出服务器(或从站)在错误状态中处理这种请求,例如:它是未配置的,且要求返回寄存器值。
    02非法数据地址对于服务器(或从站)来说,询问中接收的数据地址是不可允许的地址,特别是参考号和传输长度的组合是无效的。对于带有100个寄存器的控制器来说,偏移量96和长度4的请求会成功,而偏移量96和长度5的请求将产生异常码02。
    03非法数据值对于服务器(或从站)来说,询问中包括的值是不可允许的值。该值指示了组合请求剩余结构中的故障。例如:隐含长度是不正确的。modbus协议不知道任何特殊寄存器的任何特殊值的重要意义,寄存器中被提交存储的数据项有一个应用程序期望之外的值。
    04从站设备故障当服务器(或从站)正在设法执行请求的操作时,产生不可重新获得的差错。
    05确认与编程命令一起使用,服务器(或从站)已经接受请求,并且正在处理这个请求,但是需要长持续时间进行这些操作,返回这个响应防止在客户机(或主站)中发生超时错误,客户机(或主机)可以继续发送轮询程序完成报文来确认是否完成处理。
    07从属设备忙与编程命令一起使用,服务器(或从站)正在处理长持续时间的程序命令,当服务器(或从站)空闲时,客户机(或主站)应该稍后重新传输报文。
    08存储奇偶性差错与功能码20和21以及参考类型6一起使用,指示扩展文件区不能通过一致性校验。服务器(或从站)设备读取记录文件,但在存储器中发现一个奇偶校验错误。客户机(或主机)可重新发送请求,但可以在服务器(或从站)设备上要求服务。
    0A不可用网关路径与网关一起使用,指示网关不能为处理请求分配输入端口值输出端口的内部通信路径,通常意味着网关是错误配置的或过载的。
    0B网关目标设备响应失败与网关一起使用,指示没有从目标设备中获得响应,通常意味着设备未在网络中。

    ModbusTCP通信

    通信方式

    Modbus设备可分为主站(poll)和从站(slave)。主站只有一个,从站有多个,主站向各从站发送请求帧,从站给予响应。在使用TCP通信时,主站为client端,主动建立连接;从站为server端,等待连接。

    • 主站请求:功能码+数据
    • 从站正常响应:请求功能码+响应数据
    • 从站异常响应:异常功能码+异常码,其中异常功能码即将请求功能码的最高有效位置1,异常码指示差错类型
    • 注意:需要超时管理机制,避免无期限的等待可能不出现的应答

    IANA(Internet Assigned Numbers Authority,互联网编号分配管理机构)给Modbus协议赋予TCP端口号为502,这是目前在仪表与自动化行业中唯一分配到的端口号。

    通信过程

    1. connect 建立TCP连接
    2. 准备Modbus报文
    3. 使用send命令发送报文
    4. 在同一连接下等待应答
    5. 使用recv命令读取报文,完成一次数据交换
    6. 通信任务结束时,关闭TCP连接

    仿真软件

    • Modbus poll 和Modbus slave是一组Modbus仿真软件,可以实现Modbus RTU、TCP、串口仿真等。
    • 仿真软件网址:https://modbustools.com/download.html
    • 在ModbusTCP中,Modbus poll 作为客户端请求数据,Modbus slave 作为服务器端处理请求。
    • 使用c语言编写客户端连接Modbus slave时,注意数据格式,一条指令一次性发出,否则连接会出错。
    • 使用软件时,需要指定功能码,在setup->slave definition或者poll definition中进行设置。
      – slave ID:从站编号(事务标识符)
      – function:功能码,0x01对应线圈操作,0x02对应离散量操作,0x03对应保持寄存器操作,0x04对应输入寄存器操作
      – address:开始地址
      – quantity:寄存器/线圈/离散量 的数量

    参考:
    https://wenku.baidu.com/view/c2a9e1cc376baf1ffd4fad5c.html
    https://blog.csdn.net/zwxue251/article/details/24154951
    https://blog.csdn.net/lakerszhy/article/details/68927178?locationNum=4&fps=1

    一些概念

    原文链接:https://wenku.baidu.com/view/55595d0690c69ec3d5bb75ed.html

    在工业自动化控制中,经常会遇到开关量,数字量,模拟量,离散量,脉冲量等各种概念,而人们在实际应用中,对于这些概念又很容易混淆。现将各种概念罗列如下:

    1.开关量:

    一般指的是触点的“开”与“关”的状态,一般在计算机设备中也会用“0”或“1”来表示开关量的状态。开关量分为有源开关量信号和无源开关量信号,有源开关量信号指的是“开”与“关”的状态是带电源的信号,专业叫法为跃阶信号,可以理解为脉冲量,一般的都有220VAC, 110VAC,24VDC,12VDC等信号,无源开关量信号指的是“开”和“关”的状态时不带电源的信号,一般又称之为干接点。电阻测试法为电阻0或无穷大。

    2.数字量:

    很多人会将数字量与开关量混淆,也将其与模拟量混淆。数字量在时间和数量上都是离散的物理量,其表示的信号则为数字信号。数字量是由0和1组成的信号,经过编码形成有规律的信号,量化后的模拟量就是数字量。

    3.模拟量:

    模拟量的概念与数字量相对应,但是经过量化之后又可以转化为数字量。模拟量是在时间和数量上都是连续的物理量,其表示的信号则为模拟信号。模拟量在连续的变化过程中任何一个取值都是一个具体有意义的物理量,如温度,电压,电流等。

    4.离散量:

    离散量是将模拟量离散化之后得到的物理量。即任何仪器设备对于模拟量都不可能有个完全精确的表示,因为他们都有一个采样周期,在该采样周期内,其物理量的数值都是不变的,而实际上的模拟量则是变化的。这样就将模拟量离散化,成为了离散量。

    5.脉冲量:

    脉冲量就是瞬间电压或电流由某一值跃变到另一值的信号量。在量化后,其变化持续有规律就是数字量,如果其由0变成某一固定值并保持不变,其就是开关量。

    综上所述,模拟量就是在某个过程中时间和数量连续变化的物理量,由于在实际的应用中,所有的仪器设备对于外界数据的采集都有一个采样周期,其采集的数据只有在下一个采样周期开始时才有变动,采样周期内其数值并不随模拟量的变化而变动。
    这样就将模拟量离散化了,例如:某设备的采样周期为1秒,其在第五秒的时间采集的温度为35度,而第六秒的温度为36度,该设备就只能标称第五秒时间温度35度,第六秒时间温度36度,而第五点五秒的时间其标称也只是35度,但是其实际的模拟量是35.5度。这样就将模拟信号离散化。其采集的数据就是离散化了,不再是连续的模拟量信号。
    由于计算机只识别0和1两个信号,即开关量信号,用其来表示数值都是使用数字串来表示,由于计算能力的问题,其数字串不能无限长,即其表达的精度也是有限的,同样的以温度为例,由于数字串限制,其表达温度的精度只能达到0.1度,小于该单位的数值则不能被标称,这样就必须将离散量进行量化,将其变为数字量。即35.68度的温度则表示为35.6度。

        
         
         
         
         

    0x01:   读一组逻辑线圈,如读8个继电器输出状态。

    0x0F:  写一组逻辑线圈。如同时控制8个继电器

    0x05:  写单个逻辑线圈,如设定单个继电器

     

    0x02:  读一组开关量的输入,如读显示板是否有按键按下、水流开关的状态。

     

    0x03:  读一个或多个保持寄存器,如读一个或多个设置参数

    0x010: 写一个或多个保持寄存器,如写设置参数

    0x06:  写单个保持寄存器,如写单个设置参数

     

    0x04:   读一个或多个输入寄存器,如读具体的按键值,AD采集的信息等

    modbus协议功能码和常见问题

     

    Pasted from <http://blog.csdn.net/educast/article/details/8159510

     

    1单片机开发与PLC开发的异同:

    MODBUS协议是专门针对485总线设备(例PLC)开发,寄存器的定义要严格按照其地址范围;功能码的功能定义及定义的寄存器地址与功能码的使用要要严格符合。如上图。

    而当单片机开发用串口点对点,可能不会完全遵守MODBUS协议具体体现在2方面:

      首先是定义的寄存器地址范围,应结合不同单片机RAM的大小和项目得实际需求定义起始范围和大小。方法如在RAM区的不同区域定义不同功能的数组:

    IOX[N]:输出线圈,用来进行继电器的操作

    IX[M]:开关输入  ,用于识别按键是否按下的查询。

    其它:功能码的使用也不会严格限定在指定的PLC地址范围。

    2modbus寄存器的地址说明:

    有两套规则,一套称为PLC地址,为5位十进制数,例如40001,PLC地址40001意味着该参数类型为保持寄存器。另一套是协议地址,协议地址为0x0000,这里面有对应关系,去掉PLC地址的最高位,然后剩下的减1即可。这会存在一个问题,PLC地址30002和PLC地址40002的协议地址同为0x0001,此时访问时是不是会冲突呢。亲们,当然不会了,30001为输入寄存器,需要使用04指令访问,而40001为保持寄存器,可以使用03、06和16指令访问。注意定义的寄存器地址像数组一样都是从0开始的,即通讯中的地址比实际地址小“1”

    http://blog.sina.com.cn/s/blog_6ab9638f0100vqol.html

    3线圈寄存器:就是可以单独进行位控制的BITS寄存器,类似C51的位带结构体

    Typedef struct

    {

    Bit:0

    Bit:1

    ..

    Bit:14

    Bit:15

    }COIL_REG

    http://blog.sina.com.cn/s/blog_598b27cd0101rphm.html

     

    4modbus帧结构:

     

    ADU:应用数据单元

    PUD:协议数据单元

     

     

    freemodbus中如何判断帧结束

             modbus协议中没有明显的开始符和结束符,而是通过帧与帧之间的间隔时间T3.5来判断的。超过T3.5就认为收到了新的帧。接下来就可

    以处理数据了,首当其冲的就是判断帧的合法性。Modbus通过时间来判断帧是否接受完成,自然需要单片机中的定时器配合。

    使用串口发送完成中断:避免丢失最后一个字节内容

    展开全文
  • 基于51单片机modbusRTU从机设计

    万次阅读 多人点赞 2015-10-11 16:43:52
    modbus协议被设计出来是针对PLC应用的,这里我们可以简单的模拟PLC环境,可以在单片机里面设计一块共享区,该区域是上位机和下位机共享的,均可以读取或写入该区域的值,所有的modbus协议都是针对该快区域的操作,下...

    在了解modbus协议后就是基于该协议的设计了,下面先说一下基于航太电子HTM52单片机的从机设计。

    设计思想如下:

    modbus协议是以主从的方式通信的,也就是上位机发送指令,下位机应答机制,发起通信的一直是上位机,下位机只要应答就好了。

    modbus协议被设计出来是针对PLC应用的,这里我们可以简单的模拟PLC环境,可以在单片机里面设计一块共享区,该区域是上位机和下位机共享的,均可以读取或写入该区域的值,所有的modbus协议都是针对该快区域的操作,下位机也是根据这块区域的值做相应的操作。

    这块共享区我们用结构体来表示,这里我们只用了两个变量:

    /*modbus 16位值的定义,起始地址0000H,每一个值为16位 int型,占两个字节 */
    struct MODBUS_ADD{
    int LED_value;//地址:0000H  LED灯的值,该值得低8位代表分表代表LED1--LED8
    int LED_ctrl;//地址:0001H  控制指令
    };
    struct MODBUS_ADD modbus_Addt;//声明一个modbus结构体变量
    struct MODBUS_ADD *modbusAdd;//结构体指针,指向这个变量
    

    在主函数中,只需要查询这块区域的值,作出相应的动作就好了:

    void main()
    {
    	 SystemInit();
    	 init_MODBUS();
    	 modbus_Addt.LED_ctrl = COMM_PC;
    	while(1)
    	{				
    				//将需要交互的数据读取到公共区
    			  /*start*/
    		if(modbus_Addt.LED_ctrl != COMM_PC)
    		{
    			 modbus_Addt.LED_value = LED_PORT;
    		}
    			  /*end*/
    			 
    		 	//同步公共区数据到实际运行效果
    			  /*start*/
    			 switch(modbus_Addt.LED_ctrl)
    			 {
    				 case COMM_PC: 
    					 LED_PORT = ~(uchar)(modbus_Addt.LED_value & 0x00ff);
    				 break;
    				 case COMM_FLOW:
    					 LedFlow();
    				 break;
    				 default:
    					 LED_PORT = ~(uchar)(modbus_Addt.LED_value & 0x00ff);
    				 break;
    			 }
    			  /*end*/
    	}
    }

    接下来看modbus协议具体怎么实现的,可以看到在主函数中是没有参与这个协议的,也就是相当于modbus协议的实现是在另外一个线程中,主函数不需要关心实现的细节,这样做的好处的是主函数可以近针对于自己的实现任务,二不用考虑任务的参数从哪来的

    51单片机与上位机通信采用串口的方式,串口中断负责接收和发送数据,这里我们还用到了一个定时器,负责监控当前modbus的状态,判断这一帧数据是否完成,如果判断为一帧数据接收完成,就解析该帧数据,并执行相应的指令。

    注意一下rec_time_out这个变量,这个变量在定时器中断里面是不断自加的,但在串口中断里面就清零了,这样做的意义是判断一帧数据是否接收完成,如果rec_time_out这个变量值大于某个值,说明在一段时间是没有数据接收的,可以认为数据接收接收,当然上位机那边必须满足一帧数据是连续发送的

    串口中断程序如下,这里用到了串口中断发送数据帧,具体解析可以参考我的另一篇博客 http://blog.csdn.net/liucheng5037/article/details/48831993

    //串口中断
    void SerISR() interrupt 4 using 2
    {
    	if(RI == 1)
    	{
    		unsigned char data_value;
    		RI=0;
    		if(send_buf.busy_falg == 1) return;//发送未完成时禁止接收
    		data_value = SBUF;
    		rec_time_out = 0;//一旦接收到数据,清空超时计数
    		switch(rec_stat)
    		{
    			case PACK_START:
    				rec_num = 0;
    				if(data_value == PACK_START)//默认刚开始检测第一个字节,检测是否为本站号
    				{
    					modbus_recv_buf[rec_num++] = data_value;
    					rec_stat = PACK_REC_ING;
    				}
    				else
    				{
    					rec_stat = PACK_ADDR_ERR;
    				}
    				break;
    	
    			case PACK_REC_ING:	// 正常接收
    	
    				modbus_recv_buf[rec_num++] = data_value;
    				break;
    	
    			case PACK_ADDR_ERR:	// 地址不符合 等待超时 帧结束
    				break;
    			default : break;
    		}
    		
    	}
    	if(TI == 1)	 //进入发送完成中断,检测是否有需要发送的数据并进行发送
    	{
    		TI = 0;
    		send_buf.index++;
    		if(send_buf.index >= send_buf.length)
    		{
    			send_buf.busy_falg = 0;//发送结束
    			return;
    		}
    		SBUF = send_buf.buf[send_buf.index];//继续发送下一个	
    	}
    }
    

    定时器实现函数,注意超时检测方法:

    /* 定时器中断 1ms*/
    void Time0ISR() interrupt 1 using 1
    {
        TL0 = T1MS;                     //reload timer0 low byte
        TH0 = T1MS >> 8;                //reload timer0 high byte
    	if(PACK_REC_OK == time_out_check_MODBUS()) 
    	{
    		//成功接收一帧数据后,处理modbus信息,同步公共区数据
    		function_MODBUS(modbus_recv_buf);
    	}
    
    }
    /*超时帧检测,在1ms定时器里面运行,返回当前状态*/
    int time_out_check_MODBUS(void)
    {
    <span style="white-space:pre">	</span>rec_time_out++;
    <span style="white-space:pre">	</span>if(rec_time_out == 9)<span style="white-space:pre">				</span>// 数据接收超时5ms,给程式足够长的处理时间
    <span style="white-space:pre">	</span>{
    <span style="white-space:pre">		</span>rec_stat = PACK_START;
    <span style="white-space:pre">		</span>rec_num = 0;
    <span style="white-space:pre">	</span>}
    <span style="white-space:pre">	</span>else if((rec_time_out == 4) && (rec_num > 4)) // 超时数据帧结束4ms
    <span style="white-space:pre">	</span>{
    <span style="white-space:pre">		</span>rec_stat = PACK_REC_OK;
    //<span style="white-space:pre">		</span>modbus_rtu->rec_num = 0;
    <span style="white-space:pre">	</span>}
    <span style="white-space:pre">		</span>return rec_stat;<span style="white-space:pre">		</span>
    }
    

    一帧数据接收成功后,执行方法就在函数 function_MODBUS中,如下,指令解析和发动都是严格按照modbus协议来的,这里只是用到了协议的常用的几个指令,大家可以自由扩展,

    void function_MODBUS(unsigned char *rec_buff)
    {	
    	switch(rec_buff[1])	// 功能码索引
    	{
    		case 1:	// 01功能码:读取线圈(输出)状态  读取一组逻辑线圈的当前状态(ON/OFF)
    			//read_coil();
    			break;
    
    		case 2:	 //02功能码:读取输入状态  读取一组开关输入的当前状态(ON/OFF)
    			//read_input_bit();
    			break;
    
    		case 3:	//03功能码:读取保持型寄存器 在一个或多个保持寄存器中读取当前二进制值
    			read_reg(rec_buff);
    			break;
    
    		case 4:	//04功能码:读取输入寄存器 在一个或多个输入寄存器中读取当前二进制值
    			read_reg(rec_buff);
    			break;
    
    		case 5:	//05功能码 :强制(写)单线圈(输出)状态  强制(写)一个逻辑线圈通断状态(ON/OFF)
    			//force_coil_bit();
    			break;
    
    		case 6:	//06功能码:强制(写)单寄存器 把二进制写入一个保持寄存器
    			force_reg(rec_buff);
    			break;
    
    		case 15:
    			//force_coil_mul();
    			break;
    
    		case 16: //16功能码:强制(写)多寄存器 把二进制值写入一串连续的保持寄存器
    			force_reg(rec_buff);
    			break;
    
    		default:
    
    			//modbus_send_buff[1] = rec_buff[1] | 0X80;
    			//modbus_send_buff[2] = ERR_FUN_CODE;		// 不合法功能号
    			//send_num = 5;
    			break;
    	}
    	rec_stat = PACK_START;//发送之后使缓存回到初始状态
    	rec_num = 0;
    }
    
    /*
    function:对应modbus功能号03,04 批量读寄存器
    input:rec_buf接收到的指令 send_data需要发送的指令
    */
    void read_reg(unsigned char * rec_buff)
    {
    <span style="white-space:pre">	</span>unsigned char begin_add = 0;
    <span style="white-space:pre">	</span>unsigned char data_num = 0;
    <span style="white-space:pre">	</span>unsigned char *piont;
    <span style="white-space:pre">	</span>unsigned int send_CRC;
    <span style="white-space:pre">	</span>unsigned int send_num;
    <span style="white-space:pre">	</span>int i;
    
    
    <span style="white-space:pre">	</span>begin_add = rec_buff[3]*2;//地址1字节
    <span style="white-space:pre">	</span>data_num = rec_buff[5]*2;//需要读取的字节数
    <span style="white-space:pre">	</span>send_num = 5 + data_num;<span style="white-space:pre">	</span>// 5个固定字节+数据个数 addr1 + fun1 + num1 +【data】+ crc2 
    <span style="white-space:pre">	</span>rec_buff[2] = data_num;//字节数
    <span style="white-space:pre">	</span>piont = (unsigned char *)modbusAdd;  //将结构体转换为字符数组,便于后面的循环读取或写入
    <span style="white-space:pre">	</span>for(i=0;i<data_num;i++)
    <span style="white-space:pre">	</span>{
    <span style="white-space:pre">		</span>rec_buff[3+i] = piont[begin_add +i];
    <span style="white-space:pre">	</span>}
    <span style="white-space:pre">	</span>send_CRC = comp_crc16(rec_buff, send_num-2);
    <span style="white-space:pre">	</span>rec_buff[send_num-2] = send_CRC >> 8;
    <span style="white-space:pre">	</span>rec_buff[send_num -1] = send_CRC;
    <span style="white-space:pre">	</span>send_count = send_num;
    <span style="white-space:pre">	</span>PutNChar(rec_buff , send_count);
    }
    
    
    /*
    function:对应modbus功能号06和16,单个和批量写寄存器
    input:rec_buf接收到的指令 send_data需要发送的指令
    */
    void force_reg(unsigned char * rec_buf)
    {
    <span style="white-space:pre">	</span>unsigned char fun_code,begin_add,data_num;//功能码,开始地址,数据长度
    <span style="white-space:pre">	</span>unsigned int send_num;//发送数据长度
    <span style="white-space:pre">	</span>unsigned char *piont;
    <span style="white-space:pre">	</span>unsigned int send_CRC;
    <span style="white-space:pre">	</span>int i;
    
    
    //<span style="white-space:pre">	</span>send_data[0] = rec_buf[0]; //获取站号
    
    
    <span style="white-space:pre">	</span>fun_code = rec_buf[1];<span style="white-space:pre">	</span>//获取功能码
    //<span style="white-space:pre">	</span>send_data[1] = fun_code;
    
    
    //<span style="white-space:pre">	</span>send_data[2] = rec_buf[2];//获取起始地址
    //<span style="white-space:pre">	</span>send_data[3] = rec_buf[3];
    
    
    <span style="white-space:pre">	</span>begin_add = rec_buf[3]*2;
    <span style="white-space:pre">	</span>piont = (unsigned char *)modbusAdd;<span style="white-space:pre">	</span>//将结构体转换为字符数组,便于后面的循环读取或写入
    <span style="white-space:pre">	</span>
    <span style="white-space:pre">	</span>if(fun_code == 6)//写单个寄存器,返回指令与接收的指令完全一样
    <span style="white-space:pre">	</span>{
    <span style="white-space:pre">		</span>piont[begin_add] = rec_buf[4];//寄存器高位写入
    <span style="white-space:pre">		</span>piont[begin_add+1] = rec_buf[5];//寄存器低位写入
    <span style="white-space:pre">		</span>send_num = 8;//
    <span style="white-space:pre">	</span>}
    <span style="white-space:pre">	</span>else if(fun_code == 16)//写多个寄存器
    <span style="white-space:pre">	</span>{
    <span style="white-space:pre">		</span>data_num = rec_buf[5]*2;
    <span style="white-space:pre">		</span>send_num = 8;
    <span style="white-space:pre">		</span>for(i=0;i<data_num;i++)
    <span style="white-space:pre">		</span>{
    <span style="white-space:pre">			</span>piont[begin_add+i] = rec_buf[7+i];
    <span style="white-space:pre">		</span>}
    <span style="white-space:pre">	</span>}
    <span style="white-space:pre">	</span>send_CRC = comp_crc16(rec_buf, send_num-2);//CRC校验
    <span style="white-space:pre">	</span>rec_buf[send_num-2] = send_CRC >> 8;
    <span style="white-space:pre">	</span>rec_buf[send_num -1] = send_CRC;
    <span style="white-space:pre">	</span>send_count = send_num;
      PutNChar(rec_buf , send_count);
    }
    

    基于51单片机modbus下位机设计这里就结束了,这种方法是比较灵活了,将协议的实现单独放在一层,避免与主函数有太多交互,该工程的位置请关注我的git地址:

    https://github.com/zhui-ying/PC_MCU51Project/

    里面有多个工程,但设计方法是一致的。

    展开全文
  • Modbus背景及程序框架 主从通信程序及注释
  • PLC单片机,每一个单片机都有一堆寄存器。 RS485串口,与RS232差不多,都是串口的交互(具体百度吧,电气啥的稍微有点差别)。 MODBUS是工业通信协议,具体百度下吧。 -----------------------------------------...
  • 我们公司要用单片机与V90走modbus通信,地址40100控制字 我写进去伺服没有任何反应,ipos模式,先写0x46e,再写0x46f。我怀疑地址有问题,因为其他的寄存器我测试过,都没有...
  • 我们找了一个 Modbus 调试精灵,通过设置设备地址,读写寄存器的地址以及数值数量等参数,可以直接替代串口调试助手,比较方便的下发多个字节的数据,如下图所示。我们先来就图中的设置和数据来对 Modbus 做进一步的...
  • modbus 单片机c语言

    2018-09-11 11:15:05
    modbus单片机实现历程
  • 我这里开发的平台是新唐M031,它是Cortex-M0的内核、32位单片机。因为要和上位机进行RS485通讯,所以选用了Modbus-RTU来作为通讯协议。我这是用串口接收中断+定时器中断来接收一帧数据,然后modbus从机程序自己手撸...
  • 公司业务需要,用到modbus协议,本质上很简单,只是第一次接触,被这些词语搞得云里雾里的。这里整理一下,方便以后查询:  0x01: 读线圈寄存器  0x02: 读离散输入寄存器  0x03: 读保持寄存器  0x04: 读输入...
  • modbus server 库文件 基于stc8单片机测试 硬件平台是基于stc8系列单片机进行的测试,这个函数目前只能实现整数类型的数据读取与写入,没法做浮点数通信,且进行数据写入的时候最好是一个地址一个地址的写入,测试是...
  • 1、把modbus库文件夹和port文件夹复制到工程目录下 2、把两个文件夹添加到工程中 3、把包含路径添加进来 4、主程序中包含modbus相关头文件 //包含modbus相关文件 #include "mb.h" #include "mbport.h" #...
  • 自己对单片机modbus RTU的详细解释

    千次阅读 2014-12-24 21:20:27
    Modbus 一个工业上常用的通讯协议、一种通讯约定。Modbus协议包括RTU、ASCII、TCP。其中MODBUS-RTU最常用,比较简单,在单片机上很容易实现。虽然RTU比较简单,但是看协议资料、手册说得太专业了,起初很多内容都很...
  • 单片机串口和modbus poll 进行通信

    千次阅读 2018-11-30 18:20:18
    由于程序中我的stm32单片机都是做的从机来和主机进行通信的,都是modbus协议通过串口来实现通信的。 之前为了调通串口,所以用的是单片机的串口和串口助手进行的通信,为了更方便的模拟主机modbus,我决定采用软件...
  • //保持寄存器存储区域,列表两个值整合成为一个16位的数据代表保持寄存器的值。 uint datalenth,address=0; //数据长度以及数据开始地址,修改开始地址即可实现对数据地址更改,例如现在是0就代表modscan的40001,...
  • 51单片机MODBUS C语言程序…

    千次阅读 2013-06-09 11:41:20
    原文地址:C语言程序(从机)">51单片机MODBUS C语言程序(从机)作者:免费单片机教程及 #include "reg52.h" typedef unsigned char uint8 typedef unsigned int uint16 uint8 sendCount; uint8 receCount; uint8 ...
  • 基于单片机modbus例程详细

    千次阅读 2013-10-23 16:43:41
    void checkComm0Modbus(void); uint16 getRegisterVal(uint16 addr,uint16 *tempData); uint16 setRegisterVal(uint16 addr,uint16 tempData); void switch_BAUD(uint16 value); /************************...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 649
精华内容 259
关键字:

单片机modbus寄存器